
NVIDIA のエージェント実行ランタイム NeMo Relay を試してみた
はじめに
こんにちは、クラスメソッド製造ビジネステクノロジー部の森茂です。
NVIDIA の NeMo ファミリーに、エージェントの実行そのものを引き受けるランタイムが加わっているのをご存知でしょうか。NeMo Relay です。
エージェントを動かしていると、中で何が起きているのかは案外見えません。どのツールを何回呼んだのか、LLM との往復は何度あったのか、1 ターンでどれだけトークンを使ったのか。Relay はそのあたりをエージェントの外側から捕まえて、OpenTelemetry などの形で流してくれます。既存のエージェントに手を入れなくていいのが売りですね。
NeMo で agent とくると NeMo Agent Toolkit を思い浮かべる方もいるかもしれませんが、こちらは別物です。Agent Toolkit が Python でエージェントのワークフローを組み立てるライブラリなのに対し、Relay は Rust 製のランタイムで、組み上がったエージェントの実行境界を下から支える層にあたります。公式のレイヤー図でも Agent Toolkit の下に Relay が置かれていて、フレームワークから直接 Relay を叩く経路も描かれています。
対応エージェントの一覧に Claude Code が入っていたので、手元の環境で動かしてみました。
先に感想を書いておくと、導入は 1 コマンドで済む一方、出口ごとに取れるものがかなり違います。OpenTelemetry の出口からはトークン数が出てきません。この手の食い違いを先に知っておくと、あとから「欲しかった数字が入っていない」と気づかずに済みます。
NVIDIA の LLM ルーティング基盤については前に書きました(2026-07-03 時点の記事です)。あのとき「どのモデルに投げるか」を決めていた層の、1 つ上に乗るのが今回の Relay です。
この記事では、NeMo Relay でできることをひととおり紹介したうえで、Claude Code に相乗りさせて実際に何が取れたのかを書きます。
NeMo Relay は何を引き受けて何を引き受けないのか
NeMo Relay は NVIDIA が 2026 年 3 月末に公開した OSS で、ライセンスは Apache-2.0、本体は Rust で書かれています。安定版は 0.6.0(2026-07-22)。この記事はこのバージョンで検証しました。
もともとは NeMo Flow という名前で、0.1.0 と 0.2.0 はその名義でリリースされています。0.3.0 で改名されたのですが、リポジトリはそのまま引き継がれていて、旧 URL も現在の NeMo Relay にリダイレクトされます。ドキュメントに NeMo Flow からの移行支援機能が残っているのはこの経緯からですね。
位置づけとして印象的だったのが、ドキュメントの次の一節です。
A framework asks, "What should the agent do next?" NeMo Relay asks, "When the agent does work, which scope owns it, which middleware applies, what events are emitted, and which subscribers can consume the result?"
エージェントに「次に何をするか」を考えさせるのはフレームワークの仕事で、Relay が引き受けるのは「実際に仕事をしたとき、誰がそれを所有し、何が記録され、誰がその結果を受け取れるか」のほうだ、という切り分けですね。
引き受けないものも明示されています。エージェントフレームワーク、モデルプロバイダー、ガードレールの記述系、デプロイ基盤。これらの置き換えではないと書かれていて、Langfuse や Arize Phoenix のような観測バックエンドも「Relay の外側」に置かれています。Relay はイベントを出す側で、貯めて見せるのは別レイヤーの仕事という整理です。
Switchyard との役割分担
同じ NeMo ファミリーの Switchyard と混ざりやすいので、先に整理しておきます。
| 観点 | NeMo Switchyard | NeMo Relay |
|---|---|---|
| 担当する問い | どのバックエンドに送るか | 送った結果をどう実行し、どう記録するか |
| 出力 | ルーティング判断と stats | ATOF / ATIF / OpenTelemetry / OpenInference |
| 粒度 | リクエスト単位のモデル選択 | エージェント・LLM 呼び出し・ツール実行の各段 |
| Claude Code との接続 | OpenAI 互換の経路として | ローカルゲートウェイ + hook |
Relay が Switchyard の Decision API を呼ぶ形なので、階層としては Relay が上です。ただし Switchyard 連携は Experimental 扱いで既定のビルドに入っていないため、今回は触っていません。
対応しているのは 3 つのエージェントだけ
対応表を見て意外だったのが、対象がかなり絞られていることです。
| エージェント | 最小バージョン | 対応 |
|---|---|---|
| Claude Code | 2.1.121 | ✅ |
| Codex CLI | 0.143.0 | ✅ |
| Hermes Agent | 0.18.2 | ✅ |
| Cursor | — | ❌ |
| opencode | — | ❌ |
| Gemini CLI | — | ❌ |
Cursor は 0.4 系までは対応表に載っていたのですが、0.5 で消えています。検索すると Cursor 対応と書かれた情報が出てくることがありますが、古いバージョンのページを拾っているだけでした。
Claude Code についても但し書きがあって、対応するのは CLI だけです。
Claude desktop, web, and application sessions are unsupported unless they expose the same local hook and gateway controls.
デスクトップアプリや Web からのセッションは観測対象外ということですね。同じく Codex についても「ローカルマシンを経由しないクラウド実行は LLM の捕捉が部分的か、まったくできない」と書かれています。手元で動いているぶんしか見えない、という制約は最初に理解しておいたほうがよさそうです。
配布プラットフォームは Linux の x86_64 と ARM64、macOS の Apple Silicon、Windows の x86_64 と ARM64。Intel Mac にはビルド済みバイナリがありません(cargo install でソースからビルドする回避策は案内されています)。チームに 1 人でも Intel Mac の人がいると、その人だけ観測から抜けることになります。
なお、調べた範囲では日本語でも英語でも実際に触ってみた記事が見当たりませんでした。情報源が公式ドキュメントと GitHub にほぼ限られている状態なので、この記事の数字を突き合わせる先が今のところ存在しない点はご了承ください。
テレメトリの出口は 4 つあって取れるものが違う
ここからが本題です。Relay の観測プラグインには出口が 4 つあり、同じイベントを別々の形に射影します。この違いを把握しないまま組むと、後で「欲しかった数字が入っていない」ことになります。
まず 2 つがファイル出力です。ATOF(Agent Trajectory Observability Format)は生のイベントストリームで、JSONL で 1 行 1 イベント。プロバイダーが返した usage をそのまま保持します。ATIF(Agent Trajectory Interchange Format)はエージェントのスコープごとに 1 ファイルで、正規化済みのトラジェクトリです。
残り 2 つが OTLP で外に飛ばす projection で、OpenTelemetry と OpenInference。どちらも同じ OTLP エンドポイントに送れます。
公式ドキュメントに Exporter Field Mapping という表があり、ここが今回いちばん効きました。
| 取れるもの | ATOF | ATIF | OpenTelemetry | OpenInference |
|---|---|---|---|---|
| prompt / completion | ✅ 生の usage | ✅ | ❌ not emitted | ✅ llm.token_count.{prompt,completion} |
| cache read / write | ✅ 別々 | 🟡 cached_tokens に合算 |
❌ | ✅ prompt_details.{cache_read,cache_write} |
| cost | ✅ | ✅ total_cost_usd(USD のみ) |
✅ nemo_relay.llm.cost.total(任意通貨) |
✅ llm.cost.total(USD のみ) |
| point-in-time mark | ✅ | ❌ 落ちる | 🟡 span 化 | 🟡 span 化 |
OpenTelemetry の列だけトークンが全滅している点に注目してください。ドキュメントにもはっきり書かれています。
Token counts are not emitted as discrete attributes.
意図的な仕様で、リリースノートにも "OpenTelemetry emits cost only, not token counts." と明記されています。将来変わる可能性はあるとも書かれていました。
ATIF のほうも癖があります。cache read と cache write が cached_tokens に合算されてしまうので、内訳を見たいなら ATOF か OpenInference が要ります。mark(点イベント)も ATIF では落ちるので、正本は ATOF という整理になります。
つまり、cost を OpenTelemetry で、トークン数を OpenInference で拾う。この併用が現実解になりました。幸い 0.6 では両者が独立したセクションとして並立できます。
観測以外にも一式そろっている
観測が主役ですが、プラグインは他にもあります。今回は使っていないので、どんなものがあるかだけ触れておきます。
PII redaction は、送信前に個人情報を伏せる仕組みです。0.6 時点では決定論的なローカルバックエンドのみで、モデルを使った判定は将来予約になっています。NeMo Guardrails 連携もあり、managed な LLM 呼び出しの周辺にランタイムのガードレールを挟めます。ポリシーを書くのは上位層の仕事で、Relay はそれをホストする側です。
Adaptive というカテゴリには Cache Governor や tool parallelism が入っていて、こちらは観測ではなく最適化の系統になります。プラグイン機構自体も、プロセス内で動くネイティブ型と、gRPC worker として別プロセスで動く型の 2 種類が用意されています。
拡張点が一式そろっているのは頼もしいのですが、0.x の OSS で毎月マイナーリリースが出ている状況なので、どこまで手を出すかは慎重に決めたいところです。今回は観測だけに絞りました。
インストールして Claude Code に相乗りさせる
導入は 1 コマンドです。
curl -fsSL https://raw.githubusercontent.com/NVIDIA/NeMo-Relay/main/install.sh | sh
バージョンを固定する場合は環境変数を渡します。ここに 1 つ落とし穴があって、変数の位置を間違えると効きません。
NEMO_RELAY_VERSION=0.6.0 curl -fsSL https://.../install.sh | sh
curl -fsSL https://.../install.sh | NEMO_RELAY_VERSION=0.6.0 sh
インストーラは 258 行のシェルスクリプトで、sudo は呼ばず、シェルの設定ファイルも書き換えません。GitHub Releases からバイナリを落として SHA-256 を検証し、一致しなければ既存のバイナリを置き換えずに失敗する作りです。$HOME/.local/bin に 31MB の実行ファイルが 1 つ置かれるだけなので、ここは安心して流せました。
persistent と ephemeral のどちらで使うか
Claude Code に組み込む方法が 2 つあります。この選択が後々効いてくるので、先に表で整理しておきます。
| 観点 | persistent(install claude-code) |
ephemeral(run -- claude) |
|---|---|---|
~/.claude/settings.json を書き換える |
✅ 書き換える | ❌ 触らない |
| ゲートウェイ | 固定 127.0.0.1:47632 を共有 |
起動ごとの動的ポート |
| プロジェクト単位の設定 | ❌ user スコープのみ | ✅ .nemo-relay/ が効く |
| 常用のしやすさ | ✅ 意識しなくていい | 🟡 起動コマンドを変える |
自分は ephemeral を選びました。理由は後半の壁 2 に書きますが、persistent は ~/.claude/settings.json に手を入れます。この設定ファイルを dotfiles リポジトリの symlink にしている人は、そのままだと壊れます。
ephemeral が本当に何も触らないかは実測で確かめました。--dry-run --print を付けると、何をするつもりかだけ表示してくれます。
$ nemo-relay run --dry-run --print -- claude
agent = claude
gateway_url = http://127.0.0.1:65412
anthropic_base_url = https://api.anthropic.com
argv = claude --plugin-dir <temporary-claude-plugin-dir> --settings <temporary-claude-settings>
env.NEMO_RELAY_GATEWAY_URL = http://127.0.0.1:65412
env.NEMO_RELAY_TRANSPARENT_RUN = 1
env.ANTHROPIC_BASE_URL = http://127.0.0.1:65412
note = would generate a temporary Claude Code plugin directory
ANTHROPIC_BASE_URL はプロセスの環境変数にだけ渡り、プラグインディレクトリと設定ファイルは一時領域に作られます。実行の前後で ~/.claude/settings.json の sha256 を取って比較したところ、値は変わっていませんでした。symlink も symlink のままです。
疎通を確認する
doctor で設定を診断できます。ここでもう 1 つ気づいたことがあって、doctor には 2 つの引数の渡し方があります。
nemo-relay doctor claude # 位置引数(claude / codex / hermes)
nemo-relay doctor --plugin claude-code # フラグ(codex / claude-code / hermes / all)
表記ゆれに見えますが、取る値が違います(claude と claude-code)。位置引数はエージェントの診断、--plugin は永続インストールしたプラグインの診断で、別物でした。ephemeral 運用なら前者か、引数なしの全体診断を使います。
引数なしで走らせると、設定ファイルの探索結果と観測プラグインの検証がまとめて出ます。
Observability
✓ Plugin validation validation passed
✓ ATOF file sink sinks[0]: ~/relay-poc/atof (appears writable)
✓ ATIF dir ~/relay-poc/atif (appears writable)
! OpenTelemetry endpoint http://<収集サーバー>:4318/v1/traces (HTTP 405)
! OpenInference endpoint http://<収集サーバー>:4318/v1/traces (HTTP 405)
エンドポイントの HTTP 405 は警告として出ますが、これは doctor が GET でプローブしているためです。OTLP は POST しか受け付けないので 405 が返ってきます。到達自体はできているので、この警告は無視して大丈夫でした。
設定ファイルは ~/.config/nemo-relay/plugins.toml に置きます。探索順序が少し変わっていて、system → project → user の順で user がいちばん強くなります。近いほうが勝つ一般的な設定システムとは逆なので、プロジェクトごとに値を変えたい項目を user 側に書くと負けます。
DGX Spark に受け先を立てて trace を眺める
OTLP の受け先は手元の DGX Spark に立てました。OpenTelemetry Collector、Tempo、Grafana の 3 つで、実測のメモリ使用量は合わせて 220MB ほど。常駐させても気にならない軽さです。
Relay 側は plugins.toml にエンドポイントを書くだけです。0.6 では OpenTelemetry と OpenInference が独立したセクションなので、両方を同時に有効にできます。
[components.config.opentelemetry]
enabled = true
transport = "http_binary"
endpoint = "http://<収集サーバー>:4318/v1/traces"
service_name = "claude-code"
[components.config.openinference]
enabled = true
transport = "http_binary"
endpoint = "http://<収集サーバー>:4318/v1/traces"
service_name = "claude-code-openinference"
同じエンドポイントに送っていますが、service_name を分けておくと後で TraceQL で絞れます。
Collector と Tempo の compose まわりは、以前 DGX Spark に Langfuse を立てた記事(2026-05-02 時点の記事です)とほぼ同じ流れなので、そちらに譲ります。
Claude Code を 1 ターン走らせると、Grafana から trace が見えるようになります。

1 ユーザーターンが claude-code-turn という 1 本の trace になる。所要 11.95 秒のうち、最初の LLM 呼び出しが 4.25 秒、Read が 21.88ms、Bash が 367.54ms と内訳まで見える。
span の名前は 4 種類が観測できました。trace の root になる claude-code-turn、LLM 呼び出しの anthropic.messages、そしてツール実行はツール名がそのまま span 名になります。session.start のような点イベントは mark:session.start という長さゼロの span として出ます。
公式ドキュメントに span 名の一覧は載っていないので、これは実測です。ただしサブエージェントやコンテキストの圧縮はまだ発火させていないため、種類はもっと増えるはずです。
なお、OpenTelemetry と OpenInference を両方有効にすると、同じ trace に同じ span が 2 本ずつ入ります。上の画面で anthropic.messages や Read が重複して見えるのはそのためです。trace ID が同じなので Grafana では 1 本のツリーに混ざります。読むときは service.name で絞る必要がありました。
OpenTelemetry projection から token は 1 つも出てこない
さて、肝心の中身です。
Tempo に入っている属性を一覧してみると、cost 関連が 3 つ入っていました。
nemo_relay.llm.cost.total = 0.0541685
nemo_relay.llm.cost.currency = USD
llm.cost.total = 0.0541685
トークンのほうはどうかというと、こちらは OpenInference 側にしかありません。
llm.token_count.prompt = 2
llm.token_count.completion = 4
llm.token_count.total = 6
llm.token_count.prompt_details.cache_read = 108117
llm.token_count.prompt_details.cache_write = 0
属性名の接頭辞を見ると分かるとおり、llm.token_count.* は OpenInference projection が出しているものです。nemo_relay.* のほうにはトークン数がありません。ドキュメントどおりの挙動でした。
ここで面白いのが nemo_relay.end.data.usage という属性で、こちらには生の usage が JSON 文字列としてまるごと入っています。
{
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 108117,
"input_tokens": 2,
"output_tokens": 4,
"output_tokens_details": { "thinking_tokens": 0 }
}
つまり情報自体は OTLP で飛んでいます。個別の属性に分解されないだけです。TraceQL で llm.token_count.prompt > 1000 のような条件を書きたい場合は、OpenInference 側を見るか、Collector で属性を加工することになります。
「ok と答えてください」と頼んだだけの 1 ターンで、キャッシュ読み出しが 108,117 トークン。CLAUDE.md や skills、ツール定義がまとめてキャッシュに載っているためですね。この数字を見た瞬間、コスト分析の主戦場はキャッシュだと分かりました。
cache write の値段だけ書く場所がない
cost 属性が出ていた、と書きましたが、これは価格表を設定した後の話です。設定する前は状況が違いました。
Relay は価格表を同梱していません。ドキュメントにも「設定されたソースがなければ全モデルが unknown 扱い」と書かれています。プロバイダーがレスポンスで金額を返してくれる場合はそれを優先するのですが、Anthropic の usage には金額のフィールドがありません。Claude Code のセッションログを見ても入っているのは 4 種類のトークン数だけなので、Anthropic 経由では常に価格表からの推定に落ちます。
実際、価格表を入れる前に Tempo の属性を数えたら 230 個で、そのうち cost を含むものは 0 個でした。価格表を追加すると 233 個になり、先ほどの 3 つが増えます。
価格表を書いてみる
カタログは JSON で書いて CLI に登録します。
{
"version": 1,
"entries": [
{
"provider": "anthropic",
"model_id": "claude-opus-5",
"currency": "USD",
"unit": "per_token",
"rates": {
"input_per_million": 5.0,
"output_per_million": 25.0,
"cache_read_per_million": 0.5
},
"prompt_cache": { "read_accounting": "separate" },
"pricing_as_of": "2026-08-04"
}
]
}
nemo-relay model-pricing validate claude-pricing.json
nemo-relay model-pricing add-source claude-pricing.json --user
nemo-relay model-pricing resolve claude-opus-5 --provider anthropic \
--prompt-tokens 1000 --completion-tokens 500
resolve は estimated_total = 0.0175 を返しました。入力 1000 トークンで $0.005、出力 500 トークンで $0.0125。計算は合っています。
お気づきかもしれませんが、rates に書けるのは 3 つだけです。入力、出力、キャッシュ読み出し。キャッシュ書き込みの単価を書く場所がありません。
Relay がキャッシュ書き込みを知らないわけではありません。codec のドキュメントを読むと、Usage.cache_write_tokens は Anthropic の cache_creation_input_tokens から正しくマップされると書かれていますし、コスト構造体のほうにも cache_write というカテゴリが用意されています。トークンとしては持っているのに、値段を教える経路だけがない状態です。
しかも cache_write_per_million というキーを勝手に足しても、バリデーションは通ってしまいます。
$ nemo-relay model-pricing validate test.json
Valid model pricing catalog: test.json (2 entries)
エラーにも警告にもならず、黙って無視されます。書けたつもりになれてしまうのが厄介ですね。
何が起きるか実測する
同じプロンプトを、キャッシュの状態を変えて 2 回流しました。
まずキャッシュが当たったターンです。
生の usage : cache_creation 0 / cache_read 108,117 / input 2 / output 4
ATIF : total_cost_usd 0.0541685
検算すると 2×$5/M + 4×$25/M + 108,117×$0.50/M = 0.0541685。ぴったり合います。キャッシュ読み出しは正しく計上されています。
次にキャッシュ書き込みが発生したターン。別のディレクトリで走らせて、プロンプトを変えました。
生の usage : cache_creation 59,017 / cache_read 0 / input 2 / output 4
ATIF : total_cached_tokens 59,017 / total_cost_usd 0.00011
0.00011 は入力 2 トークンと出力 4 トークンだけの金額です。59,017 トークンのキャッシュ書き込みが、金額から丸ごと消えています。 トークン数としては total_cached_tokens に入っているのに、値段には反映されません。
Anthropic の料金表によると、キャッシュ書き込みは 1 時間 TTL で入力単価の 2 倍です。Claude Code が使っているのは 1 時間 TTL でした(生の usage に ephemeral_1h_input_tokens として残っています)。Opus 5 の入力単価が $5/M なので、$10/M で計算すると本来は $0.59 ほどになります。
| ターン | Relay が出した cost | 本来の相当額 | 差 |
|---|---|---|---|
| キャッシュ読み出しのターン | $0.0541685 | $0.0541685 | ぴたり |
| キャッシュ書き込みのターン | $0.00011 | $0.5903 | 5,366 倍 |
5,366 倍という数字はインパクトがありますが、これはキャッシュ書き込みだけが発生した 1 ターンの話なので、そのまま月額に効くわけではありません。実運用の感覚を掴むために、手元の実測と突き合わせてみます。
ccusage で 2026 年 6 月のフル稼働月を集計したところ、キャッシュ書き込みが 7,977 万トークンありました。Opus 5 の単価で見ると $798 相当です。同じ月の総額は $3,008 だったので、2 割強がコスト集計から抜ける計算になります。桁が違う話ではないにせよ、無視できる差でもありません。
キャッシュを多用するエージェントのコストを Relay だけで見ようとするのは、現時点では厳しそうです。トークン数は正確に取れているので、金額は自分で計算するのが現実的かなと思っています。
プロンプト本文は既定で中央に届く
もう 1 つ、導入判断に直結する話です。
観測データを手元のマシンから外に出すとき、自分は「プロンプト本文とコード本文は送らない」という前提を置いていました。作業の中身まで流れていくのは、観測の目的から外れるからです。
実際に流れているものを確認したら、既定では送られていました。
nemo_relay.start.data.prompt = Reply with exactly: ok
input.value = user: <system-reminder> As you answer the user's ...
output.value = ok
llm.input_messages.0.message.content = ...
送ったプロンプトも、システムリマインダーを含む会話全体も、応答本文も、span の属性に入っています。

8span を展開すると input.value に user: <system-reminder> As y… と会話の中身が見える。Resource attributes 側の service.name は claude-code-openinference。*
さらに llm.tools.0 から llm.tools.116 まで、117 個のツール定義の JSON スキーマが属性として載っていました。ATIF のファイルが 1 ターンで 1.25MB になるのは主にこれが理由です。
本文を送らない構成にするには、sanitizer や PII redaction を明示的に設定します。ここは「安全側が既定」ではないので、チームに配る前に確認しておきたいところですね。自分の環境だけで回している間は気になりませんが、他人の画面の中身が自分の Grafana に流れてくるとなると話が変わります。
相乗りするまでに踏んだ壁
導入自体は 1 コマンドと書きましたが、そこに至るまでに 3 つ踏みました。どれも読者の環境で再現しうるものなので、詳細は畳んでおきます。
3 つの壁
壁 1: バージョン固定が効かず 0.7 系が入りかけた
NEMO_RELAY_VERSION を curl の前に置いていたため、変数が curl プロセスにしか渡らず、最新版が入ろうとしました。パイプの右側に渡す必要があります。
curl -fsSL https://.../install.sh | NEMO_RELAY_VERSION=0.6.0 sh
インストーラは main ブランチから取得されるので、ドキュメントのバージョンを 0.6.0 に切り替えて読んでいても、固定しなければ最新の安定版が入ります。0.7 系は設定ファイルの形式が変わっているため、この記事の手順とは互換性がありません。
壁 2: settings.json が実ファイルに置き換えられた
これがいちばん厄介でした。永続インストールを犠牲用のホームディレクトリで試したところ、nemo-relay install claude-code は ~/.claude/settings.json に 3 つのキーを書き込みます。
{
"env": { "ANTHROPIC_BASE_URL": "http://127.0.0.1:47632" },
"enabledPlugins": { "nemo-relay-plugin@nemo-relay-local": true },
"extraKnownMarketplaces": { "nemo-relay-local": { "source": { "path": "...", "source": "directory" } } }
}
既存の設定は壊さずマージしてくれます。hooks も無傷でした。問題はここからで、この設定ファイルを dotfiles への symlink にしていると、symlink が実ファイルに置き換えられます。
install 前: lrwxr-xr-x .claude/settings.json -> dotfiles/settings.json
install 後: -rw-r--r-- .claude/settings.json
書き込む主体が 2 つに分かれているのが原因です。claude plugin marketplace add と plugin install(Claude Code 自身)は symlink をたどって実体を書き換えます。そのあと Relay 本体が ANTHROPIC_BASE_URL を足すときに一時ファイル経由の置き換えを行うため、symlink そのものが消えます。
uninstall しても symlink は戻りません。 dotfiles 側に書き込まれた nemo-relay-plugin と nemo-relay-local の 2 エントリも残ったままです。dotfiles を git 管理して複数マシンに配っている場合、コミットすると他のマシンにも伝播します。
バックアップは ~/.claude/settings.json.nemo-relay.bak に作られていました(ドキュメントには場所の記載がありません)。ただし中身を見ると、プラグイン登録が済んだ後の状態が保存されています。「provider route を足す直前」のバックアップなので、完全な原状には戻せません。
自分はこれを見て ephemeral 運用に切り替えました。
壁 3: doctor がエラーを並べるが問題はない
ephemeral 運用だと、プラグイン診断は軒並み失敗します。
Install state: failed (missing or invalid state at .../claude-code.json)
Host registration: failed (plugin or marketplace registration is incomplete)
claude provider routing: failed (not configured)
installer error: Claude Code plugin doctor checks failed; remediation: nemo-relay install claude-code --force
--plugin claude-code は永続インストールを前提にした診断なので、していなければ当然 failed になります。最後の行で install --force を勧めてきますが、ここで実行すると壁 2 に戻ります。ephemeral で運用する場合は引数なしの nemo-relay doctor を使うのが正解でした。
気になっているところ
ひととおり触ってみて、現時点で気になった点もまとめておきます。
いちばん大きいのは観測範囲です。公式の Known Issues にも書かれているとおり、Relay が捕まえられるのはローカルのフックとゲートウェイを通ったトラフィックだけで、リモートやクラウドでの実行は捕捉できません。CLI 以外の Claude Code セッションも対象外です。使われ方を漏れなく把握したい場合には、ここが効いてきます。Intel Mac にビルド済みバイナリがない件も同じ系統ですね。
バージョンの動きも速いです。安定版は 0.6.0 ですが、0.7.0 の RC が毎日のように出ています(記事を書いている間に rc.4 から rc.6 まで進みました)。0.7 では観測プラグインの設定が version 2 から 3 に上がり、「version 2 の OTLP セクションの形は version 3 では拒否される」と明記されています。今日書いた設定ファイルは、0.7 が GA になったらそのままでは動きません。0.5 から 0.6 のときも 5 領域の移行が必要だったようなので、設定を構成管理に入れると毎月踏むことになりそうです。
最後に、この記事の数字自体の確度についても触れておきます。検証したのは自分のマシン 1 台と、数十ターンぶんのセッションだけです。サブエージェントやコンテキスト圧縮を含む長いセッションは流していないので、span の種類はもっとあるはずです。プロバイダーが金額を返す経路そのものも検証できていません。Anthropic が返さない以上、この構成では常に価格表からの推定に落ちるためです。
まとめ
NeMo Relay を Claude Code に相乗りさせて、何が観測できるのかを確かめてみました。
ローカルのゲートウェイを 1 枚通すだけで、エージェントのライフサイクルがまるごと取れます。ターン単位で trace が立ち、ツール呼び出しも LLM 呼び出しも階層で見えるのは素直に便利でした。外部 SaaS に送らず手元で完結できるのも、社内で使うことを考えると助かります。
一方で、出口ごとに取れるものが違うことは先に知っておく必要があります。OpenTelemetry からはトークン数が出ず、金額もキャッシュ書き込み分が抜ける。プロンプト本文は既定で流れる。このあたりを踏まえて OpenInference や ATIF を併用する前提なら、十分に実用の範囲かなと思っています。
今回試したのは Claude Code だけですが、対応エージェントには Codex CLI と Hermes Agent もあります。永続インストールなら同じゲートウェイを共有する設計なので、複数のエージェントをまとめて観測したときにどう見えるのかは気になっているところです。0.7 が GA になったら設定の移行も必要になるので、そのタイミングで改めて触ってみます。
参考リンク
- NVIDIA/NeMo-Relay — Apache-2.0、Rust 実装
- Ecosystem — 何を引き受けて何を引き受けないかの整理
- Support Matrix — 対応エージェントとプラットフォームの一覧
- Provider Response Codecs and Model Pricing — Exporter Field Mapping はここ
- Model Pricing — 価格表カタログのスキーマ
- OpenTelemetry exporter — 0.6 系の設定リファレンス
- Release Notes — 0.7 の破壊的変更と Known Issues
- Prompt caching — キャッシュ書き込みの単価倍率(Anthropic)







