From a8d580b1c7ffefcd37c1bbc9f9e6fb5a0b5a68c6 Mon Sep 17 00:00:00 2001 From: n3wr1ch <40690535+n3wr1ch@users.noreply.github.com> Date: Thu, 6 Aug 2026 08:10:33 +0900 Subject: [PATCH 01/16] fix(pi): keep loopback models visible --- .../src/content/docs/guides/integrations.md | 2 +- docs-site/src/content/docs/guides/pi.md | 42 ++++++----------- docs-site/src/content/docs/ja/guides/pi.md | 27 ++++------- .../content/docs/ja/reference/cli/agents.md | 6 +-- docs-site/src/content/docs/ko/guides/pi.md | 45 ++++++++---------- .../content/docs/ko/reference/cli/agents.md | 8 ++-- .../src/content/docs/reference/cli/agents.md | 19 ++++---- docs-site/src/content/docs/ru/guides/pi.md | 46 +++++++------------ .../content/docs/ru/reference/cli/agents.md | 8 ++-- docs-site/src/content/docs/zh-cn/guides/pi.md | 27 ++++------- .../docs/zh-cn/reference/cli/agents.md | 6 +-- .../apikeys-workspace/ClientConfigDialog.tsx | 6 ++- gui/tests/client-config-panel.test.tsx | 9 ++-- src/clients/config-export.ts | 38 +++++++-------- tests/cli-export-command.test.ts | 8 ++-- .../client-config-export-new-clients.test.ts | 4 +- tests/client-config-export.test.ts | 19 ++++---- tests/client-config-new-clients.test.ts | 4 +- tests/management-client-config-route.test.ts | 7 ++- 19 files changed, 143 insertions(+), 188 deletions(-) diff --git a/docs-site/src/content/docs/guides/integrations.md b/docs-site/src/content/docs/guides/integrations.md index c917fd273a..f69063acf5 100644 --- a/docs-site/src/content/docs/guides/integrations.md +++ b/docs-site/src/content/docs/guides/integrations.md @@ -9,7 +9,7 @@ file, and removes it again. Six clients work this way, each with a switch: | Client | Config file | Format | When the change takes effect | Credential | |---|---|---|---|---| | OpenCode | `~/.config/opencode/opencode.json` | JSON | next direct launch | `OPENCODEX_OPENCODE_API_KEY` | -| Pi | `~/.pi/agent/models.json` | JSON | new sessions | `OPENCODEX_API_KEY` | +| Pi | `~/.pi/agent/models.json` | JSON | new sessions | loopback placeholder | | Hermes | `~/.hermes/config.yaml` | YAML | new sessions | `OPENCODEX_HERMES_API_KEY` | | OpenClaw | `~/.openclaw/openclaw.json` | JSON5 | immediately, on a running gateway | `OPENCODEX_OPENCLAW_API_KEY` | | Kimi Code | `~/.kimi-code/config.toml` | TOML | on restart, or `/reload` | loopback placeholder | diff --git a/docs-site/src/content/docs/guides/pi.md b/docs-site/src/content/docs/guides/pi.md index fa44d2754f..7b96a21ca7 100644 --- a/docs-site/src/content/docs/guides/pi.md +++ b/docs-site/src/content/docs/guides/pi.md @@ -5,8 +5,7 @@ description: Use any routed model from Pi — ocx export writes a custom provide Pi reads its providers from a single global JSON file rather than environment variables, so opencodex does not launch it. Instead, `ocx export` serializes the `opencodex` provider block — -base URL, model list, and the env reference Pi interpolates — and you merge it into your own -config. +base URL, model list, and a placeholder admission key — and you merge it into your own config. ## Quickstart @@ -17,8 +16,8 @@ ocx start ocx export --client pi ``` -The output leads with the JSON, then prints the destination path, the merge warning, the env -export line, and how many models carry authoritative context limits. +The output leads with the JSON, then prints the destination path, the merge warning, and how many +models carry authoritative context limits. ```json { @@ -26,7 +25,7 @@ export line, and how many models carry authoritative context limits. "opencodex": { "baseUrl": "http://127.0.0.1:10100/v1", "api": "openai-completions", - "apiKey": "$OPENCODEX_API_KEY", + "apiKey": "opencodex-loopback", "models": [ { "id": "anthropic/claude-opus-5", @@ -70,27 +69,16 @@ provider or changing model visibility, and merge the new block over the old one. ## The admission key -Two different keys are easy to confuse here, and only the first one appears in this file: - -| Key | What it is | Where it lives | -| --- | --- | --- | -| Proxy admission key | opencodex's own credential, generated on the dashboard's **API** tab | referenced by `apiKey` as `$OPENCODEX_API_KEY`; the value stays in your environment | -| Provider key | your Anthropic / OpenAI / OpenRouter key | opencodex's own config, per [Providers](/guides/providers/) | - -The exported config carries only the reference, never a secret. Pi interpolates a bare `$NAME`, so -the variable is: - -```bash -export OPENCODEX_API_KEY= -``` +**A loopback proxy needs no key at all.** opencodex binds `127.0.0.1` by default and authenticates +nothing there, so the exported block carries the literal placeholder `opencodex-loopback` rather +than a real credential — no environment variable is involved. -That name is Pi's alone. opencode uses a different variable -(`OPENCODEX_OPENCODE_API_KEY`, in `{env:…}` form) — see the [opencode guide](/guides/opencode/). +The placeholder is load-bearing, not cosmetic: Pi resolves `apiKey` while building its model list +and hides the whole provider when the value is an env reference that is not set. A literal keeps +every routed model visible, and the proxy never checks it on loopback. -**A loopback proxy needs no key at all.** opencodex binds `127.0.0.1` by default and authenticates -nothing there, so the `$OPENCODEX_API_KEY` reference is inert and you can leave the variable unset. -It matters only when `hostname` is set beyond loopback, which is also the case where the proxy -refuses to start without a token — see [Remote access](/reference/configuration/#remote-access). +Your provider keys are a different matter — the Anthropic / OpenAI / OpenRouter key lives in +opencodex's own config, per [Providers](/guides/providers/), and never appears in this file. ## Model metadata @@ -109,9 +97,9 @@ a guess. ## Schema status -:::note[Unverified against a real install] -The shape above follows Pi's published custom-provider documentation. It has **not** been verified -against a real `~/.pi/agent/models.json` on a machine with Pi installed. If Pi rejects the exported +:::note[Verified against a real install] +The shape above has been verified against Pi 0.83.x on a real `~/.pi/agent/models.json`: the block +validates and every routed model appears in Pi's picker. If a newer Pi rejects the exported block, the mismatch is on our side — please [open an issue](https://github.com/lidge-jun/opencodex/issues) with what Pi reported. ::: diff --git a/docs-site/src/content/docs/ja/guides/pi.md b/docs-site/src/content/docs/ja/guides/pi.md index 788fe48c60..865c981796 100644 --- a/docs-site/src/content/docs/ja/guides/pi.md +++ b/docs-site/src/content/docs/ja/guides/pi.md @@ -3,7 +3,7 @@ title: 円周率 description: Pi からルーティングされたモデルを使用します。ocx エクスポートは、実行中のプロキシに接続された Pi の models.json のカスタム プロバイダー ブロックを書き込みます。 --- -Pi は環境変数ではなく単一のグローバル JSON ファイルからプロバイダーを読み取るため、opencodex はそれを起動しません。代わりに、`ocx export` は `opencodex` プロバイダー ブロック (ベース URL、モデル リスト、Pi が補間する環境参照) をシリアル化し、それを独自の設定にマージします。 +Pi は環境変数ではなく単一のグローバル JSON ファイルからプロバイダーを読み取るため、opencodex はそれを起動しません。代わりに、`ocx export` は `opencodex` プロバイダー ブロック (ベース URL、モデル リスト、プレースホルダーのアドミッション キー) をシリアル化し、それを独自の設定にマージします。 ## クイックスタート @@ -14,7 +14,7 @@ ocx start ocx export --client pi ``` -出力は JSON で始まり、宛先パス、マージ警告、env エクスポート行、および権威コンテキスト制限を持つモデルの数を出力します。 +出力は JSON で始まり、宛先パス、マージ警告、および権威コンテキスト制限を持つモデルの数を出力します。 ```json { @@ -22,7 +22,7 @@ ocx export --client pi "opencodex": { "baseUrl": "http://127.0.0.1:10100/v1", "api": "openai-completions", - "apiKey": "$OPENCODEX_API_KEY", + "apiKey": "opencodex-loopback", "models": [ { "id": "anthropic/claude-opus-5", @@ -60,22 +60,11 @@ ocx export --client pi --json > ~/opencodex-pi-models.json # or redirect the b ## アドミッションキー -ここでは 2 つの異なるキーが混同されやすいため、このファイルには最初のキーのみが表示されます。 +**ループバック プロキシにはキーはまったく必要ありません。** opencodex はデフォルトで `127.0.0.1` をバインドし、そこでは何も認証しないため、エクスポートされたブロックには実際の認証情報ではなくリテラルのプレースホルダー `opencodex-loopback` が入ります。環境変数は一切関与しません。 -|キー |それは何ですか |それが住んでいる場所 | -| --- | --- | --- | -|プロキシ アドミッション キー | opencodex 自身の認証情報。ダッシュボードの **API** タブで生成されます。 `apiKey` では `$OPENCODEX_API_KEY` として参照されます。値は環境内に残ります。 -|プロバイダーキー | Anthropic / OpenAI / OpenRouter キー | opencodex 独自の設定、[プロバイダー](/guides/providers/) ごと | +このプレースホルダーは飾りではなく必須です。Pi はモデル リストを構築する際に `apiKey` を解決し、その値が未設定の環境変数参照である場合はプロバイダー全体を隠します。リテラルであればルーティングされたすべてのモデルが表示され、ループバックではプロキシがこの値を検査することはありません。 -エクスポートされた設定には参照のみが含まれ、シークレットは含まれません。 Pi は裸の `$NAME` を補間するため、変数は次のようになります。 - -```bash -export OPENCODEX_API_KEY= -``` - -その名前はパイだけです。 opencode は別の変数 (`OPENCODEX_OPENCODE_API_KEY`、`{env:…}` 形式) を使用します。[オープンコードガイド](/guides/opencode/) を参照してください。 - -**ループバック プロキシにはキーはまったく必要ありません。** opencodex はデフォルトで `127.0.0.1` をバインドし、そこでは何も認証しないため、`$OPENCODEX_API_KEY` 参照は不活性であり、変数を設定しないままにすることができます。これは、`hostname` がループバックを超えて設定されている場合にのみ問題になります。これは、プロキシがトークンなしでの開始を拒否する場合でもあります。[リモートアクセス](/reference/configuration/#remote-access) を参照してください。 +プロバイダー キーは別の話です。Anthropic / OpenAI / OpenRouter のキーは opencodex 自身の設定にあり ([プロバイダー](/guides/providers/) を参照)、このファイルには決して現れません。 ## モデルのメタデータ @@ -87,8 +76,8 @@ export OPENCODEX_API_KEY= ## スキーマのステータス -:::note[実際のインストールに対して未検証] -上の形状は、Pi が公開しているカスタム プロバイダーのドキュメントに従っています。 Pi がインストールされたマシン上の実際の `~/.pi/agent/models.json` に対して検証されていません**。 Pi がエクスポートされたブロックを拒否した場合、不一致は私たちの側にあります。Pi が報告した内容を [問題を開く](https://github.com/lidge-jun/opencodex/issues) してください。 +:::note[実際のインストールで検証済み] +上の形状は、Pi 0.83.x がインストールされたマシン上の実際の `~/.pi/agent/models.json` に対して検証されています。ブロックは検証を通過し、ルーティングされたすべてのモデルが Pi のピッカーに表示されます。より新しい Pi がエクスポートされたブロックを拒否した場合、不一致は私たちの側にあります。Pi が報告した内容を添えて [問題を開く](https://github.com/lidge-jun/opencodex/issues) してください。 ::: ## 要件 diff --git a/docs-site/src/content/docs/ja/reference/cli/agents.md b/docs-site/src/content/docs/ja/reference/cli/agents.md index c970804e7b..7fec7f33da 100644 --- a/docs-site/src/content/docs/ja/reference/cli/agents.md +++ b/docs-site/src/content/docs/ja/reference/cli/agents.md @@ -149,15 +149,15 @@ ocx export --client opencode --out ~/opencodex-opencode.json |クライアント |正規の宛先 |ダウンロードファイル名 |環境変数 | | --- | --- | --- | --- | | `opencode` | `~/.config/opencode/opencode.json` (設定すると `XDG_CONFIG_HOME` が勝ち) | `opencode.json` | `OPENCODEX_OPENCODE_API_KEY` | -| `pi` | `~/.pi/agent/models.json` | `pi-models.json` | `OPENCODEX_API_KEY` | +| `pi` | `~/.pi/agent/models.json` | `pi-models.json` | なし - ブロックにリテラル `opencodex-loopback` が入ります | -2 つの環境変数名は異なり、各クライアントは独自の名前のみを補間します。 opencode は `{env:OPENCODEX_OPENCODE_API_KEY}` を読み取ります。 Pi は `$OPENCODEX_API_KEY` を読み取ります。 +opencode は `{env:OPENCODEX_OPENCODE_API_KEY}` を補間します。Pi は環境変数をまったく使いません。Pi はモデル リストを構築する際に `apiKey` を解決し、その値が未設定の環境変数参照である場合はプロバイダー全体を隠すため、エクスポートされたブロックにはリテラルのプレースホルダー `opencodex-loopback` が入ります。ループバックではプロキシがこの値を検査することはありません。 :::caution[マージし、決して置き換えないでください] `ocx export` は実際のクライアント設定を書き込むことはありません。宛先は手動でマージできるように出力されます。`--out` は、`--force` なしで既存のファイルを上書きすることを拒否します。これは、設定を置き換えると、その中にすでに含まれている他のプロバイダー、エージェント、および MCP エントリが破壊されるためです。 ::: -キーはシリアル化されません。設定にはクライアントの環境参照のみが含まれるため、シークレットは環境内に残ります。ループバック プロキシ (`127.0.0.1`、デフォルト) にはアドミッション キーはまったく必要ありません。参照は単に使用されないだけです。プロキシがループバックを超えてバインドする場合にのみ変数を設定します。アドミッションキーの発行方法については、[リモートアクセス](/reference/configuration/#remote-access) を参照してください。上流プロバイダー自体のキーは完全に別のものであり、[プロバイダー](/guides/providers/) ごとに構成されます。 +キーはシリアル化されません。opencode の設定には環境参照のみが含まれるためシークレットは環境内に残り、Pi の設定には認証情報ではなくプレースホルダーが入ります。ループバック プロキシ (`127.0.0.1`、デフォルト) にはアドミッション キーはまったく必要ありません。opencode の変数は、プロキシがループバックを超えてバインドする場合にのみ設定します。アドミッションキーの発行方法については、[リモートアクセス](/reference/configuration/#remote-access) を参照してください。上流プロバイダー自体のキーは完全に別のものであり、[プロバイダー](/guides/providers/) ごとに構成されます。 同じペイロードが `GET /api/client-config` によって提供され、ダッシュボードの [API] タブにレンダリングされるため、CLI、API、および GUI は同じバイトを使用します。 diff --git a/docs-site/src/content/docs/ko/guides/pi.md b/docs-site/src/content/docs/ko/guides/pi.md index 648d71060e..7fa8222640 100644 --- a/docs-site/src/content/docs/ko/guides/pi.md +++ b/docs-site/src/content/docs/ko/guides/pi.md @@ -5,7 +5,7 @@ description: Pi에서 라우팅된 모델을 그대로 쓸 수 있습니다. `oc Pi는 provider를 환경 변수 대신 하나의 전역 JSON 파일에서 읽기 때문에, opencodex가 Pi를 직접 실행하지 않습니다. 대신 `ocx export`가 `opencodex` provider 블록, -즉 base URL, 모델 목록, 그리고 Pi가 치환하는 환경 변수 참조를 직렬화해서 사용자가 +즉 base URL, 모델 목록, 그리고 placeholder 인증 키를 직렬화해서 사용자가 자신의 설정에 병합하도록 합니다. ## 빠른 시작 @@ -17,8 +17,8 @@ ocx start ocx export --client pi ``` -출력은 JSON으로 시작하고, 이어서 대상 경로, 병합 경고, 환경 변수 export 줄, 그리고 -공식 context limit이 있는 모델 수를 보여줍니다. +출력은 JSON으로 시작하고, 이어서 대상 경로, 병합 경고, 그리고 공식 context limit이 +있는 모델 수를 보여줍니다. ```json { @@ -26,7 +26,7 @@ ocx export --client pi "opencodex": { "baseUrl": "http://127.0.0.1:10100/v1", "api": "openai-completions", - "apiKey": "$OPENCODEX_API_KEY", + "apiKey": "opencodex-loopback", "models": [ { "id": "anthropic/claude-opus-5", @@ -70,28 +70,19 @@ ocx export --client pi --json > ~/opencodex-pi-models.json # or redirect the b ## 인증 키 -여기서는 서로 헷갈리기 쉬운 키가 두 개 있고, 이 파일에 등장하는 것은 첫 번째뿐입니다. - -| 키 | 무엇인지 | 어디에 있는지 | -| --- | --- | --- | -| Proxy admission key | opencodex의 자체 인증 정보이며, 대시보드의 **API** 탭에서 생성됩니다 | `apiKey`로 `$OPENCODEX_API_KEY`를 참조하며, 값은 환경 변수에 둡니다 | -| Provider key | Anthropic / OpenAI / OpenRouter 키입니다 | opencodex의 자체 config에 있으며, [Providers](/guides/providers/)마다 따로 둡니다 | - -내보낸 config에는 비밀값이 아니라 참조만 들어갑니다. Pi는 `$NAME` 형태를 그대로 -치환하므로 변수는 다음과 같습니다. - -```bash -export OPENCODEX_API_KEY= -``` +**루프백 프록시는 키가 전혀 필요 없습니다.** opencodex는 기본적으로 `127.0.0.1`에 +바인드하고 그곳에서는 아무 것도 인증하지 않으므로, 내보낸 블록에는 실제 인증 정보가 +아니라 리터럴 placeholder인 `opencodex-loopback`이 들어갑니다. 환경 변수는 전혀 +관여하지 않습니다. -이 이름은 Pi 전용입니다. opencode는 다른 변수를 씁니다 -(`OPENCODEX_OPENCODE_API_KEY`, `{env:…}` 형식) - 자세한 내용은 [opencode 가이드](/guides/opencode/)를 보세요. +이 placeholder는 겉치레가 아니라 필수입니다. Pi는 모델 목록을 만들 때 `apiKey`를 +해석하는데, 값이 설정되지 않은 환경 변수 참조이면 provider 전체를 숨겨 버립니다. +리터럴이어야 라우팅된 모든 모델이 보이며, 루프백에서 프록시는 이 값을 검사하지 +않습니다. -**루프백 프록시는 키가 전혀 필요 없습니다.** opencodex는 기본적으로 `127.0.0.1`에 -바인드하고 그곳에서는 아무 것도 인증하지 않으므로, `$OPENCODEX_API_KEY` 참조는 -실제로는 비어 있어도 됩니다. 이 값은 `hostname`이 루프백 바깥으로 설정될 때만 -의미가 있으며, 그 경우에는 프록시가 토큰 없이 시작하지 않습니다. 자세한 내용은 -[Remote access](/reference/configuration/#remote-access)를 보세요. +Provider 키는 별개의 문제입니다. Anthropic / OpenAI / OpenRouter 키는 opencodex의 +자체 config에 있으며([Providers](/guides/providers/) 참조), 이 파일에는 절대 +나타나지 않습니다. ## 모델 메타데이터 @@ -110,9 +101,9 @@ export OPENCODEX_API_KEY= ## 스키마 상태 -:::note[실제 설치에서 검증하지 않음] -위의 형태는 Pi가 공개한 custom-provider 문서를 따른 것입니다. Pi가 설치된 머신의 -실제 `~/.pi/agent/models.json`으로는 아직 검증하지 않았습니다. Pi가 내보낸 블록을 +:::note[실제 설치에서 검증됨] +위의 형태는 Pi 0.83.x가 설치된 머신의 실제 `~/.pi/agent/models.json`으로 검증했습니다. +블록이 유효하고 라우팅된 모든 모델이 Pi 선택기에 표시됩니다. 더 새로운 Pi가 이 블록을 거부하면 문제는 우리 쪽에 있습니다. Pi가 무엇을 보고했는지와 함께 [issue를 열어주세요](https://github.com/lidge-jun/opencodex/issues). ::: diff --git a/docs-site/src/content/docs/ko/reference/cli/agents.md b/docs-site/src/content/docs/ko/reference/cli/agents.md index 13f95f948f..0b1cc0c434 100644 --- a/docs-site/src/content/docs/ko/reference/cli/agents.md +++ b/docs-site/src/content/docs/ko/reference/cli/agents.md @@ -150,20 +150,20 @@ ocx export --client pi --json > pi-models.json # byte-exact JSON for a pipe or ocx export --client opencode --out ~/opencodex-opencode.json ``` -`--json`이 없으면 JSON이 먼저 나오고, 그다음 표준 대상 경로, merge 경고, env export 줄, 그리고 context limit을 생략한 row 수를 포함한 model count가 이어집니다(이 경우 client는 자체 기본값을 적용합니다). +`--json`이 없으면 JSON이 먼저 나오고, 그다음 표준 대상 경로, merge 경고, 해당 client에 env 변수가 있는 경우 env export 줄, 그리고 context limit을 생략한 row 수를 포함한 model count가 이어집니다(이 경우 client는 자체 기본값을 적용합니다). | 클라이언트 | 표준 대상 경로 | 다운로드 파일명 | 환경 변수 | | --- | --- | --- | --- | | `opencode` | `~/.config/opencode/opencode.json` (`XDG_CONFIG_HOME`이 설정되어 있으면 우선합니다) | `opencode.json` | `OPENCODEX_OPENCODE_API_KEY` | -| `pi` | `~/.pi/agent/models.json` | `pi-models.json` | `OPENCODEX_API_KEY` | +| `pi` | `~/.pi/agent/models.json` | `pi-models.json` | 없음 - 블록에 리터럴 `opencodex-loopback`이 들어갑니다 | -두 환경 변수 이름은 서로 다르며, 각 client는 자기 것만 보간합니다. opencode는 `{env:OPENCODEX_OPENCODE_API_KEY}`를 읽고, Pi는 `$OPENCODEX_API_KEY`를 읽습니다. +opencode는 `{env:OPENCODEX_OPENCODE_API_KEY}`를 보간합니다. Pi는 환경 변수를 전혀 쓰지 않습니다. Pi는 모델 목록을 만들 때 `apiKey`를 해석하는데, 그 값이 설정되지 않은 env 참조이면 provider 전체를 숨겨 버리므로, 내보낸 블록에는 리터럴 placeholder인 `opencodex-loopback`이 들어갑니다. 루프백에서 proxy는 이 값을 검사하지 않습니다. :::caution[Merge, never replace] `ocx export`는 실제 client config를 절대 쓰지 않습니다. 대상 경로는 손으로 병합하라고 출력되며, `--out`은 `--force` 없이 기존 파일을 덮어쓰지 않습니다. config를 바꾸어 덮어쓰면 이미 들어 있던 다른 provider, agent, MCP entry가 사라지기 때문입니다. ::: -어떤 key도 직렬화되지 않습니다. config에는 client의 env reference만 들어가므로 secret은 환경 변수에 남습니다. loopback proxy(`127.0.0.1`, 기본값)는 admission key가 전혀 필요하지 않습니다. reference는 단지 사용되지 않을 뿐입니다. proxy가 loopback을 넘어 바인딩할 때만 변수를 설정하십시오. admission key가 어떻게 발급되는지는 [Remote access](/reference/configuration/#remote-access)를 보십시오. upstream provider 자체의 key는 완전히 별개의 것으로, 각 [Providers](/guides/providers/)에 맞게 설정합니다. +어떤 key도 직렬화되지 않습니다. opencode config에는 env reference만 들어가므로 secret은 환경 변수에 남고, Pi config에는 인증 정보가 아니라 placeholder가 들어갑니다. loopback proxy(`127.0.0.1`, 기본값)는 admission key가 전혀 필요하지 않습니다. opencode 변수는 proxy가 loopback을 넘어 바인딩할 때만 설정하십시오. admission key가 어떻게 발급되는지는 [Remote access](/reference/configuration/#remote-access)를 보십시오. upstream provider 자체의 key는 완전히 별개의 것으로, 각 [Providers](/guides/providers/)에 맞게 설정합니다. 같은 payload는 `GET /api/client-config`로 제공되고 dashboard의 API 탭에도 렌더링되므로, CLI, API, GUI가 모두 같은 바이트를 사용합니다. diff --git a/docs-site/src/content/docs/reference/cli/agents.md b/docs-site/src/content/docs/reference/cli/agents.md index 4944a483d2..d2dfc236c9 100644 --- a/docs-site/src/content/docs/reference/cli/agents.md +++ b/docs-site/src/content/docs/reference/cli/agents.md @@ -170,16 +170,18 @@ ocx export --client opencode --out ~/opencodex-opencode.json ``` Without `--json` the JSON leads, then the canonical destination path, the merge warning, the env -export line, and a model count with how many rows omit context limits (the client applies its own -defaults for those). +export line where the client has one, and a model count with how many rows omit context limits (the +client applies its own defaults for those). | Client | Canonical destination | Download filename | Env var | | --- | --- | --- | --- | | `opencode` | `~/.config/opencode/opencode.json` (`XDG_CONFIG_HOME` wins when set) | `opencode.json` | `OPENCODEX_OPENCODE_API_KEY` | -| `pi` | `~/.pi/agent/models.json` | `pi-models.json` | `OPENCODEX_API_KEY` | +| `pi` | `~/.pi/agent/models.json` | `pi-models.json` | none — the block carries the literal `opencodex-loopback` | -The two env var names are different, and each client only interpolates its own. opencode reads -`{env:OPENCODEX_OPENCODE_API_KEY}`; Pi reads `$OPENCODEX_API_KEY`. +opencode interpolates `{env:OPENCODEX_OPENCODE_API_KEY}`. Pi does not use a variable at all: it +resolves `apiKey` while building its model list and hides the whole provider when the value is an +unset env reference, so the exported block carries the literal placeholder `opencodex-loopback`, +which the proxy never checks on loopback. :::caution[Merge, never replace] `ocx export` never writes your real client config. The destination is printed for you to merge by @@ -187,9 +189,10 @@ hand, and `--out` refuses to overwrite an existing file without `--force`, becau config destroys the other providers, agents, and MCP entries already in it. ::: -No key is ever serialized. The config carries only the client's env reference, so the secret stays -in your environment. A loopback proxy (`127.0.0.1`, the default) requires no admission key at all — -the reference is simply unused. Set the variable only when the proxy binds beyond loopback; see +No key is ever serialized. An opencode config carries only the env reference, so the secret stays +in your environment, and a Pi config carries a placeholder rather than any credential. A loopback +proxy (`127.0.0.1`, the default) requires no admission key at all. Set the opencode variable only +when the proxy binds beyond loopback; see [Remote access](/reference/configuration/#remote-access) for how admission keys are issued. Keys for the upstream providers themselves are a separate thing entirely, configured per [Providers](/guides/providers/). diff --git a/docs-site/src/content/docs/ru/guides/pi.md b/docs-site/src/content/docs/ru/guides/pi.md index 0960ecf49a..2615064231 100644 --- a/docs-site/src/content/docs/ru/guides/pi.md +++ b/docs-site/src/content/docs/ru/guides/pi.md @@ -5,8 +5,8 @@ description: Используйте любую маршрутизируемую Pi читает провайдеров из одного глобального JSON-файла, а не из переменных окружения, поэтому opencodex не запускает его сам. Вместо этого `ocx export` сериализует блок провайдера -`opencodex` — base URL, список моделей и env-ссылку, которую интерполирует Pi, — а вы сливаете -его в свою конфигурацию. +`opencodex` — base URL, список моделей и ключ-заглушку допуска, — а вы сливаете его в свою +конфигурацию. ## Быстрый старт @@ -17,8 +17,8 @@ ocx start ocx export --client pi ``` -Сначала выводится JSON, затем путь назначения, предупреждение о merge, строка `export` для -переменной окружения и число моделей, для которых есть авторитетные контекстные лимиты. +Сначала выводится JSON, затем путь назначения, предупреждение о merge и число моделей, для +которых есть авторитетные контекстные лимиты. ```json { @@ -26,7 +26,7 @@ ocx export --client pi "opencodex": { "baseUrl": "http://127.0.0.1:10100/v1", "api": "openai-completions", - "apiKey": "$OPENCODEX_API_KEY", + "apiKey": "opencodex-loopback", "models": [ { "id": "anthropic/claude-opus-5", @@ -71,28 +71,16 @@ ocx export --client pi --json > ~/opencodex-pi-models.json # or redirect the b ## Admission key -Здесь легко перепутать два разных ключа, и в этом файле появляется только первый: - -| Ключ | Что это | Где хранится | -| --- | --- | --- | -| Ключ допуска прокси | собственная учётная запись opencodex, генерируемая на вкладке **API** в дашборде | указывается в `apiKey` как `$OPENCODEX_API_KEY`; само значение остаётся в окружении | -| Ключ провайдера | ваш ключ Anthropic / OpenAI / OpenRouter | хранится в конфигурации самого opencodex, см. [Провайдеры](/guides/providers/) | - -Экспортируемая конфигурация несёт только ссылку, а не секрет. Pi интерполирует голый `$NAME`, -поэтому переменная должна выглядеть так: - -```bash -export OPENCODEX_API_KEY= -``` +**Прокси на loopback вообще не требует ключа.** По умолчанию opencodex привязывается к +`127.0.0.1` и ничего там не аутентифицирует, поэтому экспортируемый блок несёт не настоящие +учётные данные, а литеральную заглушку `opencodex-loopback`; переменные окружения тут ни при чём. -Это имя переменной относится только к Pi. opencode использует другую переменную -(`OPENCODEX_OPENCODE_API_KEY` в форме `{env:…}`) — см. [руководство по opencode](/guides/opencode/). +Заглушка нужна не для красоты: Pi разрешает `apiKey`, когда строит список моделей, и прячет +провайдера целиком, если значение — ссылка на незаданную переменную окружения. Литерал +сохраняет видимость всех маршрутизируемых моделей, а на loopback прокси это значение не проверяет. -**Прокси на loopback вообще не требует ключа.** По умолчанию opencodex привязывается к -`127.0.0.1` и ничего там не аутентифицирует, поэтому ссылка `$OPENCODEX_API_KEY` инертна и -переменную можно не задавать. Она нужна только когда `hostname` выходит за пределы loopback — а -именно в этом случае прокси и отказывается запускаться без токена; см. -[Удалённый доступ](/reference/configuration/#remote-access). +Ключи провайдеров — отдельная история: ключ Anthropic / OpenAI / OpenRouter хранится в +конфигурации самого opencodex, см. [Провайдеры](/guides/providers/), и в этом файле не появляется. ## Метаданные моделей @@ -111,10 +99,10 @@ export OPENCODEX_API_KEY= ## Статус схемы -:::note[Не проверено на реальной установке] -Форма выше соответствует опубликованной документации Pi по custom-провайдерам. Она **не была -проверена** на реальном `~/.pi/agent/models.json` на машине с установленным Pi. Если Pi отвергнет -экспортированный блок, несоответствие на нашей стороне — пожалуйста, +:::note[Проверено на реальной установке] +Форма выше проверена на реальном `~/.pi/agent/models.json` с установленным Pi 0.83.x: блок +проходит валидацию, и все маршрутизируемые модели видны в выборе моделей Pi. Если более новый Pi +отвергнет экспортированный блок, несоответствие на нашей стороне — пожалуйста, [создайте issue](https://github.com/lidge-jun/opencodex/issues) и приложите то, что сообщил Pi. ::: diff --git a/docs-site/src/content/docs/ru/reference/cli/agents.md b/docs-site/src/content/docs/ru/reference/cli/agents.md index 2d738ab9b4..66181fd468 100644 --- a/docs-site/src/content/docs/ru/reference/cli/agents.md +++ b/docs-site/src/content/docs/ru/reference/cli/agents.md @@ -182,10 +182,12 @@ limit'а (для них клиент применяет собственные d | Клиент | Канонический путь | Имя скачиваемого файла | Переменная окружения | | --- | --- | --- | --- | | `opencode` | `~/.config/opencode/opencode.json` (`XDG_CONFIG_HOME` имеет приоритет, если задан) | `opencode.json` | `OPENCODEX_OPENCODE_API_KEY` | -| `pi` | `~/.pi/agent/models.json` | `pi-models.json` | `OPENCODEX_API_KEY` | +| `pi` | `~/.pi/agent/models.json` | `pi-models.json` | нет — блок несёт литерал `opencodex-loopback` | -Имена этих двух env-переменных различаются, и каждый клиент интерполирует только свою. opencode -читает `{env:OPENCODEX_OPENCODE_API_KEY}`; Pi читает `$OPENCODEX_API_KEY`. +opencode интерполирует `{env:OPENCODEX_OPENCODE_API_KEY}`. Pi не использует переменную вовсе: он +разрешает `apiKey`, когда строит список моделей, и прячет провайдера целиком, если значение — +ссылка на незаданную переменную окружения, поэтому экспортируемый блок несёт литеральную заглушку +`opencodex-loopback`, которую прокси на loopback никогда не проверяет. :::caution[Сливать, а не заменять] `ocx export` никогда не пишет в ваш реальный клиентский конфиг. Путь назначения лишь diff --git a/docs-site/src/content/docs/zh-cn/guides/pi.md b/docs-site/src/content/docs/zh-cn/guides/pi.md index ad868e3194..9b7982ac00 100644 --- a/docs-site/src/content/docs/zh-cn/guides/pi.md +++ b/docs-site/src/content/docs/zh-cn/guides/pi.md @@ -3,7 +3,7 @@ title: Pi description: 在 Pi 中使用任意已路由模型 - `ocx export` 会为 Pi 的 `models.json` 写入一个自定义 provider 块,并连接到正在运行的代理。 --- -Pi 从一个全局 JSON 文件而不是环境变量中读取 providers,所以 opencodex 不会启动它。相反,`ocx export` 会序列化 `opencodex` provider 块 - 基础 URL、模型列表,以及 Pi 会插值的 env 引用 - 然后你把它合并到自己的配置中。 +Pi 从一个全局 JSON 文件而不是环境变量中读取 providers,所以 opencodex 不会启动它。相反,`ocx export` 会序列化 `opencodex` provider 块 - 基础 URL、模型列表,以及一个占位准入 key - 然后你把它合并到自己的配置中。 ## 快速开始 @@ -14,7 +14,7 @@ ocx start ocx export --client pi ``` -输出会先显示 JSON,然后打印目标路径、合并警告、env 导出行,以及有多少模型带有权威上下文窗口限制。 +输出会先显示 JSON,然后打印目标路径、合并警告,以及有多少模型带有权威上下文窗口限制。 ```json { @@ -22,7 +22,7 @@ ocx export --client pi "opencodex": { "baseUrl": "http://127.0.0.1:10100/v1", "api": "openai-completions", - "apiKey": "$OPENCODEX_API_KEY", + "apiKey": "opencodex-loopback", "models": [ { "id": "anthropic/claude-opus-5", @@ -60,22 +60,11 @@ ocx export --client pi --json > ~/opencodex-pi-models.json # or redirect the b ## 准入密钥 -这里有两个很容易混淆的 key,但这个文件里只会出现第一个: +**回环代理根本不需要 key。** opencodex 默认绑定 `127.0.0.1`,在那里不做任何认证,所以导出的块携带的是字面占位值 `opencodex-loopback`,而不是真实凭据 - 完全不涉及环境变量。 -| Key | 它是什么 | 它存放在哪里 | -| --- | --- | --- | -| 代理准入密钥 | opencodex 自己的凭据,在仪表盘的 **API** 选项卡中生成 | 通过 `apiKey` 以 `$OPENCODEX_API_KEY` 形式引用;实际值保存在你的环境中 | -| Provider key | 你的 Anthropic / OpenAI / OpenRouter key | opencodex 自己的配置中,见 [Providers](/guides/providers/) | +这个占位值不是装饰,而是必需的:Pi 在构建模型列表时会解析 `apiKey`,一旦该值是未设置的环境变量引用,它就会隐藏整个 provider。使用字面值才能让所有已路由模型保持可见,而回环上的代理从不校验这个值。 -导出的配置只包含引用,从不包含 secret。Pi 会插值裸的 `$NAME`,所以变量是: - -```bash -export OPENCODEX_API_KEY= -``` - -这个名字只属于 Pi。opencode 使用不同的变量(`OPENCODEX_OPENCODE_API_KEY`,以 `{env:…}` 形式出现) - 见 [opencode 指南](/guides/opencode/)。 - -**回环代理根本不需要 key。** opencodex 默认绑定 `127.0.0.1`,在那里不做任何认证,所以 `$OPENCODEX_API_KEY` 引用是无效的,你可以不设置这个变量。它只在 `hostname` 超出回环范围时才有意义,而这也是代理会在没有 token 的情况下拒绝启动的时候 - 见 [远程访问](/reference/configuration/#remote-access)。 +Provider key 是另一回事:你的 Anthropic / OpenAI / OpenRouter key 保存在 opencodex 自己的配置中,见 [Providers](/guides/providers/),它绝不会出现在这个文件里。 ## 模型元数据 @@ -87,8 +76,8 @@ export OPENCODEX_API_KEY= ## Schema 状态 -:::note[未在真实安装上验证] -上面的结构遵循了 Pi 已公开的自定义 provider 文档。它**尚未**在一台安装了 Pi 的机器上、针对真实的 `~/.pi/agent/models.json` 进行验证。如果 Pi 拒绝这个导出块,问题在我们这边 - 请带上 Pi 的报错信息[提交 issue](https://github.com/lidge-jun/opencodex/issues)。 +:::note[已在真实安装上验证] +上面的结构已在安装了 Pi 0.83.x 的机器上、针对真实的 `~/.pi/agent/models.json` 完成验证:该块通过校验,并且所有已路由模型都会出现在 Pi 的模型选择器中。如果更新版本的 Pi 拒绝这个导出块,问题在我们这边 - 请带上 Pi 的报错信息[提交 issue](https://github.com/lidge-jun/opencodex/issues)。 ::: ## 需求 diff --git a/docs-site/src/content/docs/zh-cn/reference/cli/agents.md b/docs-site/src/content/docs/zh-cn/reference/cli/agents.md index dc1960adb3..20e0b70b53 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli/agents.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli/agents.md @@ -150,15 +150,15 @@ ocx export --client opencode --out ~/opencodex-opencode.json | 客户端 | 规范目标路径 | 下载文件名 | 环境变量 | | --- | --- | --- | --- | | `opencode` | `~/.config/opencode/opencode.json`(设置了 `XDG_CONFIG_HOME` 时以其为准) | `opencode.json` | `OPENCODEX_OPENCODE_API_KEY` | -| `pi` | `~/.pi/agent/models.json` | `pi-models.json` | `OPENCODEX_API_KEY` | +| `pi` | `~/.pi/agent/models.json` | `pi-models.json` | 无 - 块中携带字面值 `opencodex-loopback` | -这两个环境变量名称不同,而且每个客户端只会插入自己的那个。opencode 读取 `{env:OPENCODEX_OPENCODE_API_KEY}`;Pi 读取 `$OPENCODEX_API_KEY`。 +opencode 会插值 `{env:OPENCODEX_OPENCODE_API_KEY}`。Pi 完全不使用环境变量:它在构建模型列表时会解析 `apiKey`,一旦该值是未设置的环境变量引用,就会隐藏整个 provider,因此导出的块携带的是字面占位值 `opencodex-loopback`,而回环上的代理从不校验这个值。 :::caution[合并,不要替换] `ocx export` 从不写入你的真实客户端配置。该命令只会打印目标路径供你手动合并,而 `--out` 在没有 `--force` 的情况下拒绝覆盖已有文件,因为替换配置会破坏其中已有的其他 providers、agents 和 MCP 条目。 ::: -任何密钥都不会被序列化。配置里只包含客户端的环境引用,因此密钥仍保留在你的环境中。环回代理(`127.0.0.1`,默认值)根本不需要准入密钥——该引用只是不会被使用。只有当代理绑定到环回地址之外时才设置该变量;关于准入密钥如何签发,请参见 [远程访问](/reference/configuration/#remote-access)。上游 providers 自身的密钥则完全是另一回事,需要按 [Providers](/guides/providers/) 单独配置。 +任何密钥都不会被序列化。opencode 配置里只包含环境引用,因此密钥仍保留在你的环境中;Pi 配置里携带的是占位值而不是任何凭据。环回代理(`127.0.0.1`,默认值)根本不需要准入密钥。只有当代理绑定到环回地址之外时才需要设置 opencode 的那个变量;关于准入密钥如何签发,请参见 [远程访问](/reference/configuration/#remote-access)。上游 providers 自身的密钥则完全是另一回事,需要按 [Providers](/guides/providers/) 单独配置。 同一份负载会通过 `GET /api/client-config` 提供,并在仪表盘的 API 选项卡中渲染,因此 CLI、API 和 GUI 使用的是同一字节内容。 diff --git a/gui/src/components/apikeys-workspace/ClientConfigDialog.tsx b/gui/src/components/apikeys-workspace/ClientConfigDialog.tsx index 0d315825f1..cad0dbcd25 100644 --- a/gui/src/components/apikeys-workspace/ClientConfigDialog.tsx +++ b/gui/src/components/apikeys-workspace/ClientConfigDialog.tsx @@ -78,9 +78,11 @@ export default function ClientConfigDialog({ {t("api.clientConfig.missingLimits", { count: envelope.modelsWithoutLimits, total: envelope.modelCount })}

)} - {!hasKeys && ( + {!hasKeys && envelope.apiKeyEnv !== "" && ( // Informational, never blocking: an agent may legitimately want the shape - // first, so both actions stay enabled. + // first, so both actions stay enabled. A client with no env var at all + // (Pi, Kimi) reads its credential from its own file, so naming a + // variable there would render an empty name.

{t("api.clientConfig.noKeyYet", { env: envelope.apiKeyEnv })}

diff --git a/gui/tests/client-config-panel.test.tsx b/gui/tests/client-config-panel.test.tsx index ce2336c7d6..1dd7287f8d 100644 --- a/gui/tests/client-config-panel.test.tsx +++ b/gui/tests/client-config-panel.test.tsx @@ -47,14 +47,14 @@ const PI_ENVELOPE_BASE = { client: "pi", filename: "pi-models.json", destination: "/home/dev/.pi/agent/models.json", - apiKeyEnv: "OPENCODEX_PI_API_KEY", - exportHint: "export OPENCODEX_PI_API_KEY=", + apiKeyEnv: "", + exportHint: "Pi reads credentials from its models.json; loopback needs no key.", modelCount: 2, modelsWithoutLimits: 1, format: "json", mediaType: "application/json", // Pi keys its models as an ARRAY — the shape swap is what proves a real refetch. - config: { providers: { opencodex: { models: [{ id: "gpt-5.4" }, { id: "claude-sonnet-4-6" }] } } }, + config: { providers: { opencodex: { apiKey: "opencodex-loopback", models: [{ id: "gpt-5.4" }, { id: "claude-sonnet-4-6" }] } } }, }; /** @@ -474,6 +474,9 @@ test("no-key state is informational and leaves copy and download enabled", async await act(async () => { rowButton(container, "OpenCode", "Details").click(); }); expect(container.querySelector(".awi-clientconfig-nokey")?.textContent) .toContain("OPENCODEX_OPENCODE_API_KEY has no key behind it yet"); + await act(async () => { button(container, "Close").click(); }); + await act(async () => { rowButton(container, "Pi", "Details").click(); }); + expect(container.querySelector(".awi-clientconfig-nokey")).toBeNull(); await act(async () => { root.unmount(); }); }); diff --git a/src/clients/config-export.ts b/src/clients/config-export.ts index 0a4a84ed00..0853ea85c6 100644 --- a/src/clients/config-export.ts +++ b/src/clients/config-export.ts @@ -107,15 +107,9 @@ export const OPENCODE_API_KEY_ENV = "OPENCODEX_OPENCODE_API_KEY"; /** Env reference shared by apiKey and the dedicated proxy admission header. */ export const OPENCODE_API_KEY_ENV_REF = `{env:${OPENCODE_API_KEY_ENV}}`; -/** Env var Pi interpolates. Pi takes bare `$NAME`, not opencode's `{env:NAME}`. */ -export const PI_API_KEY_ENV = "OPENCODEX_API_KEY"; - -/** Pi's reference form for the admission key. Never the value. */ -export const PI_API_KEY_ENV_REF = `$${PI_API_KEY_ENV}`; - /** * Hermes interpolates `${VAR}` anywhere in config.yaml, so the credential stays - * in the environment exactly as it does for OpenCode and Pi. + * in the environment exactly as it does for OpenCode. */ export const HERMES_API_KEY_ENV = "OPENCODEX_HERMES_API_KEY"; export const HERMES_API_KEY_ENV_REF = `\${${HERMES_API_KEY_ENV}}`; @@ -125,12 +119,17 @@ export const OPENCLAW_API_KEY_ENV = "OPENCODEX_OPENCLAW_API_KEY"; export const OPENCLAW_API_KEY_ENV_REF = `\${${OPENCLAW_API_KEY_ENV}}`; /** - * Kimi Code reads credentials ONLY from its config file — it never falls back - * to the shell environment. A loopback bind needs no real admission key, so we - * emit the same placeholder the Grok managed block uses rather than a user - * secret; a non-loopback bind is refused by the writer instead of papered over. + * Placeholder credential for loopback-only clients (Kimi, Pi). A loopback + * bind requires no real admission key, so we emit the same placeholder the + * Grok managed block uses rather than a user secret; a non-loopback bind is + * refused by the writer instead of papered over. + * + * For Pi this is load-bearing, not cosmetic: Pi resolves `apiKey` while + * building its model list and hides the WHOLE provider when the value is an + * env reference that is not set, so `$OPENCODEX_API_KEY` left every routed + * model invisible until the user exported a variable nothing ever checked. */ -export const KIMI_LOOPBACK_PLACEHOLDER = "opencodex-loopback"; +export const LOOPBACK_API_KEY_PLACEHOLDER = "opencodex-loopback"; /** * Gajae's `apiKeyEnv` is env-name-only and fail-closed. Its sibling `apiKey` @@ -728,7 +727,7 @@ function buildPiClientConfig(ctx: ExportContext): PiGeneratedConfig { [OPENCODE_PROVIDER_ID]: { baseUrl: ctx.baseUrl, api: PI_API_DIALECT, - apiKey: PI_API_KEY_ENV_REF, + apiKey: LOOPBACK_API_KEY_PLACEHOLDER, models, }, }, @@ -808,7 +807,7 @@ function buildKimiClientConfig(ctx: ExportContext): KimiGeneratedConfig { [OPENCODE_PROVIDER_ID]: { type: "openai", base_url: ctx.baseUrl, - api_key: KIMI_LOOPBACK_PLACEHOLDER, + api_key: LOOPBACK_API_KEY_PLACEHOLDER, }, }, models, @@ -949,15 +948,16 @@ export const EXPORT_CLIENTS: Record = { id: "pi", filename: "pi-models.json", destination: () => join(homedir(), ".pi", "agent", "models.json"), - apiKeyEnv: PI_API_KEY_ENV, - exportHint: `export ${PI_API_KEY_ENV}=`, + // Pi carries the placeholder literal in models.json (see + // LOOPBACK_API_KEY_PLACEHOLDER), so there is no env var to export. + apiKeyEnv: "", + exportHint: "Pi reads credentials from its models.json; loopback needs no key.", build: buildPiClientConfig, format: "json", summarize: summarizePi, buildContribution: buildPiContribution, - // No header field in Pi's provider block (and the schema is unverified - // against a real install), so there is nowhere to put the dedicated - // admission header a remote bind requires. + // No header field in Pi's provider block, so there is nowhere to put the + // dedicated admission header a remote bind requires. loopbackOnly: true, }, hermes: { diff --git a/tests/cli-export-command.test.ts b/tests/cli-export-command.test.ts index 108dbcaa3d..3c2089aa38 100644 --- a/tests/cli-export-command.test.ts +++ b/tests/cli-export-command.test.ts @@ -139,11 +139,11 @@ describe("ocx export human output (accept criterion 2)", () => { expect(result.stdout).toContain("3 models; 1 omit context limits"); }); - test("Pi names its own destination and env var", async () => { + test("Pi names its own destination and needs no env var", async () => { const proxy = fakeProxy(); const result = await run(["--client", "pi"], { baseUrl: proxy.baseUrl }); expect(result.stdout).toContain(join(".pi", "agent", "models.json")); - expect(result.stdout).toContain("export OPENCODEX_API_KEY="); + expect(result.stdout).toContain("loopback needs no key"); }); }); @@ -297,8 +297,8 @@ describe("ocx export never serializes a key (accept criterion 6)", () => { for (const [args, envRef] of [ [["--client", "opencode"], "{env:OPENCODEX_OPENCODE_API_KEY}"], [["--client", "opencode", "--json"], "{env:OPENCODEX_OPENCODE_API_KEY}"], - [["--client", "pi"], "$OPENCODEX_API_KEY"], - [["--client", "pi", "--json"], "$OPENCODEX_API_KEY"], + [["--client", "pi"], "opencodex-loopback"], + [["--client", "pi", "--json"], "opencodex-loopback"], ] as Array<[string[], string]>) { logs = []; errors = []; diff --git a/tests/client-config-export-new-clients.test.ts b/tests/client-config-export-new-clients.test.ts index 9de47024f6..8f5173b14c 100644 --- a/tests/client-config-export-new-clients.test.ts +++ b/tests/client-config-export-new-clients.test.ts @@ -5,7 +5,7 @@ import { EXPORT_CLIENT_IDS, GAJAE_API_KEY_ENV, HERMES_API_KEY_ENV_REF, - KIMI_LOOPBACK_PLACEHOLDER, + LOOPBACK_API_KEY_PLACEHOLDER, OPENCLAW_API_KEY_ENV_REF, OPENCODE_PROVIDER_ID, buildClientConfig, @@ -235,7 +235,7 @@ describe("kimi", () => { test("uses the loopback placeholder because Kimi reads no environment", () => { const doc = buildClientConfig("kimi", ctx()) as KimiGeneratedConfig; - expect(doc.providers[OPENCODE_PROVIDER_ID]!.api_key).toBe(KIMI_LOOPBACK_PLACEHOLDER); + expect(doc.providers[OPENCODE_PROVIDER_ID]!.api_key).toBe(LOOPBACK_API_KEY_PLACEHOLDER); }); test("never emits capabilities it cannot assert", () => { diff --git a/tests/client-config-export.test.ts b/tests/client-config-export.test.ts index 8ab7faeab5..274f7fd4ea 100644 --- a/tests/client-config-export.test.ts +++ b/tests/client-config-export.test.ts @@ -6,8 +6,7 @@ import { EXPORT_CLIENT_IDS, OPENCODE_API_KEY_ENV, OPENCODE_API_KEY_ENV_REF, - PI_API_KEY_ENV, - PI_API_KEY_ENV_REF, + LOOPBACK_API_KEY_PLACEHOLDER, SCHEMA_REQUIRED_OUTPUT_BUDGET, buildClientConfig, buildClientConfigText, @@ -160,12 +159,13 @@ describe("Pi serializer (accept criterion 2)", () => { ]); }); - test("provider envelope names the OpenAI-compatible dialect and the env reference", () => { + test("provider envelope names the OpenAI-compatible dialect and the loopback placeholder", () => { const provider = piConfig().providers.opencodex!; expect(provider.baseUrl).toBe(BASE_URL); expect(provider.api).toBe("openai-completions"); - expect(provider.apiKey).toBe(PI_API_KEY_ENV_REF); - expect(provider.apiKey).toBe("$OPENCODEX_API_KEY"); + // Pi hides the whole provider when apiKey is an unset env reference, so + // the loopback-only client carries the placeholder literal instead. + expect(provider.apiKey).toBe(LOOPBACK_API_KEY_PLACEHOLDER); }); test("cost is omitted on every entry — zeros would assert routed models are free", () => { @@ -236,7 +236,7 @@ describe("no credential ever reaches the output (accept criterion 3)", () => { test("each client emits only its own documented env reference", () => { expect(JSON.stringify(opencodeConfig())).toContain(OPENCODE_API_KEY_ENV_REF); - expect(JSON.stringify(piConfig())).toContain(PI_API_KEY_ENV_REF); + expect(JSON.stringify(piConfig())).toContain(LOOPBACK_API_KEY_PLACEHOLDER); expect(JSON.stringify(piConfig())).not.toContain("{env:"); }); }); @@ -348,7 +348,7 @@ describe("EXPORT_CLIENTS registry", () => { "opencodex": { "baseUrl": "http://127.0.0.1:10100/v1", "api": "openai-completions", - "apiKey": "$OPENCODEX_API_KEY", + "apiKey": "opencodex-loopback", "models": [ { "id": "anthropic/claude-opus-5", @@ -449,8 +449,9 @@ describe("EXPORT_CLIENTS registry", () => { test("apiKeyEnv and exportHint name the variable the config references", () => { expect(EXPORT_CLIENTS.opencode.apiKeyEnv).toBe(OPENCODE_API_KEY_ENV); expect(EXPORT_CLIENTS.opencode.exportHint).toContain(OPENCODE_API_KEY_ENV); - expect(EXPORT_CLIENTS.pi.apiKeyEnv).toBe(PI_API_KEY_ENV); - expect(EXPORT_CLIENTS.pi.exportHint).toContain(PI_API_KEY_ENV); + // Pi reads the placeholder literal from its own file — no env var exists. + expect(EXPORT_CLIENTS.pi.apiKeyEnv).toBe(""); + expect(EXPORT_CLIENTS.pi.exportHint).toContain("loopback"); for (const id of EXPORT_CLIENT_IDS) { expect(EXPORT_CLIENTS[id].exportHint).not.toContain("ocx_"); } diff --git a/tests/client-config-new-clients.test.ts b/tests/client-config-new-clients.test.ts index 51fd6de1f4..eaf500ca54 100644 --- a/tests/client-config-new-clients.test.ts +++ b/tests/client-config-new-clients.test.ts @@ -4,7 +4,7 @@ import { EXPORT_CLIENT_IDS, GAJAE_API_KEY_ENV, HERMES_API_KEY_ENV_REF, - KIMI_LOOPBACK_PLACEHOLDER, + LOOPBACK_API_KEY_PLACEHOLDER, OPENCLAW_API_KEY_ENV_REF, OPENCODE_PROVIDER_ID, buildClientConfig, @@ -56,7 +56,7 @@ describe("no client config ever carries a credential", () => { test("kimi uses the loopback placeholder because it cannot read env vars", () => { const doc = buildClientConfig("kimi", ctx()) as KimiGeneratedConfig; - expect(doc.providers[OPENCODE_PROVIDER_ID]!.api_key).toBe(KIMI_LOOPBACK_PLACEHOLDER); + expect(doc.providers[OPENCODE_PROVIDER_ID]!.api_key).toBe(LOOPBACK_API_KEY_PLACEHOLDER); }); test("gajae uses apiKeyEnv, not the apiKey footgun", () => { diff --git a/tests/management-client-config-route.test.ts b/tests/management-client-config-route.test.ts index c5fa46912b..1e20c446ee 100644 --- a/tests/management-client-config-route.test.ts +++ b/tests/management-client-config-route.test.ts @@ -4,8 +4,7 @@ import { OPENCODE_API_KEY_ENV, OPENCODE_CONFIG_SCHEMA, OPENCODE_PROVIDER_ID, - PI_API_KEY_ENV, - PI_API_KEY_ENV_REF, + LOOPBACK_API_KEY_PLACEHOLDER, buildClientConfig, normalizeExportModels, opencodeGlobalConfigPath, @@ -149,11 +148,11 @@ describe("GET /api/client-config", () => { expect(body.client).toBe("pi"); expect(body.filename).toBe("pi-models.json"); - expect(body.apiKeyEnv).toBe(PI_API_KEY_ENV); + expect(body.apiKeyEnv).toBe(""); const provider = (body.config as PiGeneratedConfig).providers[OPENCODE_PROVIDER_ID]; expect(Array.isArray(provider.models)).toBe(true); - expect(provider.apiKey).toBe(PI_API_KEY_ENV_REF); + expect(provider.apiKey).toBe(LOOPBACK_API_KEY_PLACEHOLDER); expect(provider.baseUrl).toBe("http://127.0.0.1:10100/v1"); expect(provider.models.map(model => model.id)).toContain("a/m1"); }, 15_000); From 1b2d57b465bfa4c93d84daece4fd6c9878ac77d9 Mon Sep 17 00:00:00 2001 From: n3wr1ch <40690535+n3wr1ch@users.noreply.github.com> Date: Thu, 6 Aug 2026 08:12:17 +0900 Subject: [PATCH 02/16] docs(devlog): add Pi config preview evidence --- .../evidence/pi-config-preview.png | Bin 0 -> 63468 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 devlog/_fin/260806_pi_loopback_models/evidence/pi-config-preview.png diff --git a/devlog/_fin/260806_pi_loopback_models/evidence/pi-config-preview.png b/devlog/_fin/260806_pi_loopback_models/evidence/pi-config-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..26693304e2b086d39fa4334968131f6072356415 GIT binary patch literal 63468 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 YKA3ti^5eNZ!R&7$hHjZ&FtPRdKbgL%`~Uy| literal 0 HcmV?d00001 From aeb97ba887703f45901ffcabdecf7db4c0984749 Mon Sep 17 00:00:00 2001 From: n3wr1ch <40690535+n3wr1ch@users.noreply.github.com> Date: Thu, 6 Aug 2026 08:22:15 +0900 Subject: [PATCH 03/16] docs(pi): align localized launch guidance --- .../src/content/docs/ja/reference/cli/agents.md | 2 +- .../src/content/docs/ru/reference/cli/agents.md | 14 +++++++------- .../src/content/docs/zh-cn/reference/cli/agents.md | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs-site/src/content/docs/ja/reference/cli/agents.md b/docs-site/src/content/docs/ja/reference/cli/agents.md index 7fec7f33da..43d9847a72 100644 --- a/docs-site/src/content/docs/ja/reference/cli/agents.md +++ b/docs-site/src/content/docs/ja/reference/cli/agents.md @@ -144,7 +144,7 @@ ocx export --client pi --json > pi-models.json # byte-exact JSON for a pipe or ocx export --client opencode --out ~/opencodex-opencode.json ``` -`--json` がない場合、JSON が先頭に続き、正規の宛先パス、マージ警告、環境エクスポート行、およびコンテキスト制限を省略する行数を含むモデル数が続きます (クライアントはこれらに対して独自のデフォルトを適用します)。 +`--json` がない場合、JSON が先頭に続き、正規の宛先パス、マージ警告、クライアント固有の起動前ガイダンス、およびコンテキスト制限を省略する行数を含むモデル数が続きます (クライアントはこれらに対して独自のデフォルトを適用します)。 |クライアント |正規の宛先 |ダウンロードファイル名 |環境変数 | | --- | --- | --- | --- | diff --git a/docs-site/src/content/docs/ru/reference/cli/agents.md b/docs-site/src/content/docs/ru/reference/cli/agents.md index 66181fd468..b161731a37 100644 --- a/docs-site/src/content/docs/ru/reference/cli/agents.md +++ b/docs-site/src/content/docs/ru/reference/cli/agents.md @@ -175,9 +175,9 @@ ocx export --client pi --json > pi-models.json # byte-exact JSON for a pipe or ocx export --client opencode --out ~/opencodex-opencode.json ``` -Без `--json` сначала идёт JSON, затем канонический путь назначения, предупреждение о merge, строка -экспорта переменной окружения и количество моделей с указанием, сколько строк не имеют context -limit'а (для них клиент применяет собственные default'ы). +Без `--json` сначала идёт JSON, затем канонический путь назначения, предупреждение о merge, +клиентская подсказка перед запуском и количество моделей с указанием, сколько строк не имеют +context limit'а (для них клиент применяет собственные default'ы). | Клиент | Канонический путь | Имя скачиваемого файла | Переменная окружения | | --- | --- | --- | --- | @@ -196,10 +196,10 @@ opencode интерполирует `{env:OPENCODEX_OPENCODE_API_KEY}`. Pi не MCP-записи. ::: -Никакой ключ никогда не сериализуется. Конфиг несёт только env-reference клиента, так что секрет -остаётся в вашем окружении. Loopback-прокси (`127.0.0.1`, по умолчанию) вообще не требует -admission key — ссылка просто остаётся неиспользованной. Задавайте переменную только если прокси -слушает не на loopback; как выдаются admission key, описано в +Никакой ключ никогда не сериализуется. Конфиг opencode несёт только env-reference, так что секрет +остаётся в вашем окружении, а конфиг Pi несёт заглушку вместо учётных данных. Loopback-прокси +(`127.0.0.1`, по умолчанию) вообще не требует admission key. Задавайте переменную opencode только +если прокси слушает не на loopback; как выдаются admission key, описано в [Удалённом доступе](/reference/configuration/#remote-access). Ключи upstream-провайдеров — это совсем отдельная история и настраиваются в [Провайдерах](/guides/providers/). diff --git a/docs-site/src/content/docs/zh-cn/reference/cli/agents.md b/docs-site/src/content/docs/zh-cn/reference/cli/agents.md index 20e0b70b53..04fab65008 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli/agents.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli/agents.md @@ -145,7 +145,7 @@ ocx export --client pi --json > pi-models.json # byte-exact JSON for a pipe or ocx export --client opencode --out ~/opencodex-opencode.json ``` -不使用 `--json` 时,JSON 会先输出,随后是规范目标路径、合并警告、环境变量导出行,以及一个模型计数,并标明有多少行省略了上下文限制(客户端会对这些项应用自己的默认值)。 +不使用 `--json` 时,JSON 会先输出,随后是规范目标路径、合并警告、客户端专属的启动前提示,以及一个模型计数,并标明有多少行省略了上下文限制(客户端会对这些项应用自己的默认值)。 | 客户端 | 规范目标路径 | 下载文件名 | 环境变量 | | --- | --- | --- | --- | From edad24644371a60fd77b1362ed3f7a548241caf4 Mon Sep 17 00:00:00 2001 From: n3wr1ch <40690535+n3wr1ch@users.noreply.github.com> Date: Thu, 6 Aug 2026 08:28:44 +0900 Subject: [PATCH 04/16] docs(pi): qualify generated credential behavior --- docs-site/src/content/docs/ja/reference/cli/agents.md | 2 +- docs-site/src/content/docs/ko/reference/cli/agents.md | 2 +- docs-site/src/content/docs/reference/cli/agents.md | 9 +++++---- docs-site/src/content/docs/ru/reference/cli/agents.md | 9 +++++---- docs-site/src/content/docs/zh-cn/reference/cli/agents.md | 2 +- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/docs-site/src/content/docs/ja/reference/cli/agents.md b/docs-site/src/content/docs/ja/reference/cli/agents.md index 43d9847a72..eb9f97fae6 100644 --- a/docs-site/src/content/docs/ja/reference/cli/agents.md +++ b/docs-site/src/content/docs/ja/reference/cli/agents.md @@ -151,7 +151,7 @@ ocx export --client opencode --out ~/opencodex-opencode.json | `opencode` | `~/.config/opencode/opencode.json` (設定すると `XDG_CONFIG_HOME` が勝ち) | `opencode.json` | `OPENCODEX_OPENCODE_API_KEY` | | `pi` | `~/.pi/agent/models.json` | `pi-models.json` | なし - ブロックにリテラル `opencodex-loopback` が入ります | -opencode は `{env:OPENCODEX_OPENCODE_API_KEY}` を補間します。Pi は環境変数をまったく使いません。Pi はモデル リストを構築する際に `apiKey` を解決し、その値が未設定の環境変数参照である場合はプロバイダー全体を隠すため、エクスポートされたブロックにはリテラルのプレースホルダー `opencodex-loopback` が入ります。ループバックではプロキシがこの値を検査することはありません。 +opencode は `{env:OPENCODEX_OPENCODE_API_KEY}` を補間します。opencodex が生成する Pi のエクスポートには環境変数が不要で、リテラルのプレースホルダー `opencodex-loopback` が入ります。この値は必須です。Pi はモデル リストを構築する際に `apiKey` を解決し、既存の設定に未設定の環境変数参照がある場合はプロバイダー全体を隠すためです。ループバックでは、生成されたプレースホルダーをプロキシが検査することはありません。 :::caution[マージし、決して置き換えないでください] `ocx export` は実際のクライアント設定を書き込むことはありません。宛先は手動でマージできるように出力されます。`--out` は、`--force` なしで既存のファイルを上書きすることを拒否します。これは、設定を置き換えると、その中にすでに含まれている他のプロバイダー、エージェント、および MCP エントリが破壊されるためです。 diff --git a/docs-site/src/content/docs/ko/reference/cli/agents.md b/docs-site/src/content/docs/ko/reference/cli/agents.md index 0b1cc0c434..f3b109a492 100644 --- a/docs-site/src/content/docs/ko/reference/cli/agents.md +++ b/docs-site/src/content/docs/ko/reference/cli/agents.md @@ -157,7 +157,7 @@ ocx export --client opencode --out ~/opencodex-opencode.json | `opencode` | `~/.config/opencode/opencode.json` (`XDG_CONFIG_HOME`이 설정되어 있으면 우선합니다) | `opencode.json` | `OPENCODEX_OPENCODE_API_KEY` | | `pi` | `~/.pi/agent/models.json` | `pi-models.json` | 없음 - 블록에 리터럴 `opencodex-loopback`이 들어갑니다 | -opencode는 `{env:OPENCODEX_OPENCODE_API_KEY}`를 보간합니다. Pi는 환경 변수를 전혀 쓰지 않습니다. Pi는 모델 목록을 만들 때 `apiKey`를 해석하는데, 그 값이 설정되지 않은 env 참조이면 provider 전체를 숨겨 버리므로, 내보낸 블록에는 리터럴 placeholder인 `opencodex-loopback`이 들어갑니다. 루프백에서 proxy는 이 값을 검사하지 않습니다. +opencode는 `{env:OPENCODEX_OPENCODE_API_KEY}`를 보간합니다. opencodex가 생성한 Pi 블록에는 환경 변수가 필요 없으며, 리터럴 placeholder인 `opencodex-loopback`이 들어갑니다. 이 값은 필수입니다. Pi는 모델 목록을 만들 때 `apiKey`를 해석하고, 기존 config에 설정되지 않은 env 참조가 있으면 provider 전체를 숨기기 때문입니다. 루프백에서 proxy는 생성된 placeholder를 검사하지 않습니다. :::caution[Merge, never replace] `ocx export`는 실제 client config를 절대 쓰지 않습니다. 대상 경로는 손으로 병합하라고 출력되며, `--out`은 `--force` 없이 기존 파일을 덮어쓰지 않습니다. config를 바꾸어 덮어쓰면 이미 들어 있던 다른 provider, agent, MCP entry가 사라지기 때문입니다. diff --git a/docs-site/src/content/docs/reference/cli/agents.md b/docs-site/src/content/docs/reference/cli/agents.md index d2dfc236c9..1515bd13bc 100644 --- a/docs-site/src/content/docs/reference/cli/agents.md +++ b/docs-site/src/content/docs/reference/cli/agents.md @@ -178,10 +178,11 @@ client applies its own defaults for those). | `opencode` | `~/.config/opencode/opencode.json` (`XDG_CONFIG_HOME` wins when set) | `opencode.json` | `OPENCODEX_OPENCODE_API_KEY` | | `pi` | `~/.pi/agent/models.json` | `pi-models.json` | none — the block carries the literal `opencodex-loopback` | -opencode interpolates `{env:OPENCODEX_OPENCODE_API_KEY}`. Pi does not use a variable at all: it -resolves `apiKey` while building its model list and hides the whole provider when the value is an -unset env reference, so the exported block carries the literal placeholder `opencodex-loopback`, -which the proxy never checks on loopback. +opencode interpolates `{env:OPENCODEX_OPENCODE_API_KEY}`. The generated Pi export does not require +an environment variable: it carries the literal `opencodex-loopback` placeholder. This is +load-bearing because Pi resolves `apiKey` while building its model list and hides the whole +provider when an existing config contains an unset env reference. The proxy never checks the +generated placeholder on loopback. :::caution[Merge, never replace] `ocx export` never writes your real client config. The destination is printed for you to merge by diff --git a/docs-site/src/content/docs/ru/reference/cli/agents.md b/docs-site/src/content/docs/ru/reference/cli/agents.md index b161731a37..b3c08052bf 100644 --- a/docs-site/src/content/docs/ru/reference/cli/agents.md +++ b/docs-site/src/content/docs/ru/reference/cli/agents.md @@ -184,10 +184,11 @@ context limit'а (для них клиент применяет собствен | `opencode` | `~/.config/opencode/opencode.json` (`XDG_CONFIG_HOME` имеет приоритет, если задан) | `opencode.json` | `OPENCODEX_OPENCODE_API_KEY` | | `pi` | `~/.pi/agent/models.json` | `pi-models.json` | нет — блок несёт литерал `opencodex-loopback` | -opencode интерполирует `{env:OPENCODEX_OPENCODE_API_KEY}`. Pi не использует переменную вовсе: он -разрешает `apiKey`, когда строит список моделей, и прячет провайдера целиком, если значение — -ссылка на незаданную переменную окружения, поэтому экспортируемый блок несёт литеральную заглушку -`opencodex-loopback`, которую прокси на loopback никогда не проверяет. +opencode интерполирует `{env:OPENCODEX_OPENCODE_API_KEY}`. Сгенерированный opencodex экспорт для +Pi не требует переменной окружения и несёт литеральную заглушку `opencodex-loopback`. Это значение +обязательно: Pi разрешает `apiKey`, когда строит список моделей, и прячет провайдера целиком, если +существующий конфиг содержит ссылку на незаданную переменную окружения. На loopback прокси не +проверяет сгенерированную заглушку. :::caution[Сливать, а не заменять] `ocx export` никогда не пишет в ваш реальный клиентский конфиг. Путь назначения лишь diff --git a/docs-site/src/content/docs/zh-cn/reference/cli/agents.md b/docs-site/src/content/docs/zh-cn/reference/cli/agents.md index 04fab65008..6957537ee6 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli/agents.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli/agents.md @@ -152,7 +152,7 @@ ocx export --client opencode --out ~/opencodex-opencode.json | `opencode` | `~/.config/opencode/opencode.json`(设置了 `XDG_CONFIG_HOME` 时以其为准) | `opencode.json` | `OPENCODEX_OPENCODE_API_KEY` | | `pi` | `~/.pi/agent/models.json` | `pi-models.json` | 无 - 块中携带字面值 `opencodex-loopback` | -opencode 会插值 `{env:OPENCODEX_OPENCODE_API_KEY}`。Pi 完全不使用环境变量:它在构建模型列表时会解析 `apiKey`,一旦该值是未设置的环境变量引用,就会隐藏整个 provider,因此导出的块携带的是字面占位值 `opencodex-loopback`,而回环上的代理从不校验这个值。 +opencode 会插值 `{env:OPENCODEX_OPENCODE_API_KEY}`。opencodex 生成的 Pi 导出不需要环境变量,而是携带字面占位值 `opencodex-loopback`。这个值是必需的:Pi 在构建模型列表时会解析 `apiKey`,如果已有配置包含未设置的环境变量引用,它就会隐藏整个 provider。回环上的代理从不校验生成的占位值。 :::caution[合并,不要替换] `ocx export` 从不写入你的真实客户端配置。该命令只会打印目标路径供你手动合并,而 `--out` 在没有 `--force` 的情况下拒绝覆盖已有文件,因为替换配置会破坏其中已有的其他 providers、agents 和 MCP 条目。 From ce89ba5902d4f8b148e2211b81b216ad09b14a5e Mon Sep 17 00:00:00 2001 From: n3wr1ch <40690535+n3wr1ch@users.noreply.github.com> Date: Thu, 6 Aug 2026 09:29:32 +0900 Subject: [PATCH 05/16] fix(export): omit direct native models --- src/server/management/model-rows.ts | 21 ++++++++++------ tests/management-client-config-route.test.ts | 26 ++++++++++++++++++++ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/server/management/model-rows.ts b/src/server/management/model-rows.ts index 71c200574b..c660994e17 100644 --- a/src/server/management/model-rows.ts +++ b/src/server/management/model-rows.ts @@ -13,6 +13,7 @@ import { catalogModelSlug, nativeModelRows, uniqueCatalogModelsForPublicList } f import type { ExportModel } from "../../clients/config-export"; import { providerContextCap } from "../../providers/context-cap"; import { routedSlug, slugEquals } from "../../providers/slug-codec"; +import { providerCodexAccountMode } from "../../providers/registry"; import type { OcxConfig } from "../../types"; import { fetchAllModels } from "./shared"; @@ -102,16 +103,20 @@ export function toExportModel(row: ManagementModelRow): ExportModel { } /** - * Visible (non-disabled) rows as export models — the ONE loader both - * `/api/client-config` and the integration routes use, so the two can never - * disagree about which models a client is told about. + * Visible rows as export models — the ONE loader both `/api/client-config` + * and the integration routes use, so the two can never disagree about which + * models a client is told about. * - * The visibility filter lives HERE rather than at each call site: the export - * core serializes what it is given, so a model the user disabled in the Models - * tab is absent from `/v1/models` and exporting it would hand the client a - * selector the proxy refuses to route. + * Disabled rows are absent because the proxy refuses to route them. Native + * rows are also absent in Codex Direct mode: those routes require the caller's + * real ChatGPT bearer, while file integrations authenticate their provider + * with a generated loopback placeholder or admission credential. Advertising + * native rows there would forward the wrong Authorization value upstream. */ export async function loadExportModels(config: OcxConfig): Promise { const rows = await listManagementModelRows(config); - return rows.filter(row => !row.disabled).map(toExportModel); + const omitNative = providerCodexAccountMode("openai", config.providers?.openai) === "direct"; + return rows + .filter(row => !row.disabled && !(omitNative && row.native === true)) + .map(toExportModel); } diff --git a/tests/management-client-config-route.test.ts b/tests/management-client-config-route.test.ts index 1e20c446ee..efb5a25e3c 100644 --- a/tests/management-client-config-route.test.ts +++ b/tests/management-client-config-route.test.ts @@ -12,6 +12,7 @@ import { type OpencodeGeneratedConfig, type PiGeneratedConfig, } from "../src/clients/config-export"; +import { nativeModelRows } from "../src/codex/catalog"; import type { OcxConfig } from "../src/types"; import { catalogConvergenceFactory } from "./helpers/catalog-convergence"; @@ -157,6 +158,31 @@ describe("GET /api/client-config", () => { expect(provider.models.map(model => model.id)).toContain("a/m1"); }, 15_000); + test("pi omits native rows in Codex Direct mode", async () => { + const poolConfig = baseConfig(); + const nativeIds = new Set(nativeModelRows(poolConfig).map(row => row.slug)); + const pool = await (await clientConfigApi(poolConfig, "?client=pi")).json() as ClientConfigEnvelope; + const poolModels = (pool.config as PiGeneratedConfig).providers[OPENCODE_PROVIDER_ID].models; + expect(poolModels.some(model => nativeIds.has(model.id))).toBe(true); + + const directConfig = baseConfig({ + providers: { + ...poolConfig.providers, + openai: { + adapter: "openai-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + authMode: "forward", + codexAccountMode: "direct", + disabled: true, + }, + }, + }); + const direct = await (await clientConfigApi(directConfig, "?client=pi")).json() as ClientConfigEnvelope; + const directModels = (direct.config as PiGeneratedConfig).providers[OPENCODE_PROVIDER_ID].models; + expect(directModels.some(model => nativeIds.has(model.id))).toBe(false); + expect(directModels.map(model => model.id)).toContain("a/m1"); + }, 15_000); + test("counts describe the emitted document, including models without limits", async () => { const config = baseConfig(); const opencode = await (await clientConfigApi(config, "?client=opencode")).json() as ClientConfigEnvelope; From 75243fd617c8f64f54b7a346673cc08aec2e3693 Mon Sep 17 00:00:00 2001 From: n3wr1ch <40690535+n3wr1ch@users.noreply.github.com> Date: Thu, 6 Aug 2026 09:38:06 +0900 Subject: [PATCH 06/16] docs(pi): clarify placeholder wire contract --- docs-site/src/content/docs/guides/pi.md | 19 ++++++++-------- docs-site/src/content/docs/ja/guides/pi.md | 8 +++---- docs-site/src/content/docs/ko/guides/pi.md | 22 +++++++++---------- docs-site/src/content/docs/ru/guides/pi.md | 16 ++++++++------ docs-site/src/content/docs/zh-cn/guides/pi.md | 6 ++--- 5 files changed, 36 insertions(+), 35 deletions(-) diff --git a/docs-site/src/content/docs/guides/pi.md b/docs-site/src/content/docs/guides/pi.md index 7b96a21ca7..76b0fe40bd 100644 --- a/docs-site/src/content/docs/guides/pi.md +++ b/docs-site/src/content/docs/guides/pi.md @@ -67,18 +67,19 @@ ocx export --client pi --json > ~/opencodex-pi-models.json # or redirect the b The exported block is a static snapshot, not a live view. Re-run `ocx export` after adding a provider or changing model visibility, and merge the new block over the old one. -## The admission key +## The Pi `apiKey` placeholder -**A loopback proxy needs no key at all.** opencodex binds `127.0.0.1` by default and authenticates -nothing there, so the exported block carries the literal placeholder `opencodex-loopback` rather -than a real credential — no environment variable is involved. +Pi normally calls `/chat/completions` and sends its configured `apiKey` as a Bearer authorization +value. The generated block therefore includes the non-secret literal `opencodex-loopback` in Pi's +normal `apiKey` field. -The placeholder is load-bearing, not cosmetic: Pi resolves `apiKey` while building its model list -and hides the whole provider when the value is an env reference that is not set. A literal keeps -every routed model visible, and the proxy never checks it on loopback. +That literal is neither a proxy admission credential nor an upstream provider key. The loopback +proxy ignores it and requires no credential at all. It is still load-bearing for discovery: Pi +resolves `apiKey` while building its model list and hides the whole provider when the value is an +unset env reference, so a literal keeps every routed model visible. -Your provider keys are a different matter — the Anthropic / OpenAI / OpenRouter key lives in -opencodex's own config, per [Providers](/guides/providers/), and never appears in this file. +Your provider keys are separate — the Anthropic / OpenAI / OpenRouter key lives in opencodex's own +config, per [Providers](/guides/providers/), and never appears in this file. ## Model metadata diff --git a/docs-site/src/content/docs/ja/guides/pi.md b/docs-site/src/content/docs/ja/guides/pi.md index 865c981796..4dfb17dcf1 100644 --- a/docs-site/src/content/docs/ja/guides/pi.md +++ b/docs-site/src/content/docs/ja/guides/pi.md @@ -58,13 +58,13 @@ ocx export --client pi --json > ~/opencodex-pi-models.json # or redirect the b エクスポートされたブロックは静的なスナップショットであり、ライブ ビューではありません。プロバイダーを追加するかモデルの可視性を変更した後、`ocx export` を再実行し、新しいブロックを古いブロックにマージします。 -## アドミッションキー +## Pi の `apiKey` プレースホルダー -**ループバック プロキシにはキーはまったく必要ありません。** opencodex はデフォルトで `127.0.0.1` をバインドし、そこでは何も認証しないため、エクスポートされたブロックには実際の認証情報ではなくリテラルのプレースホルダー `opencodex-loopback` が入ります。環境変数は一切関与しません。 +Pi は通常 `/chat/completions` を呼び出し、設定された `apiKey` を Bearer 認証値として送信します。そのため、生成されるブロックでは、Pi の通常の `apiKey` フィールドに非シークレットのリテラル `opencodex-loopback` を入れます。 -このプレースホルダーは飾りではなく必須です。Pi はモデル リストを構築する際に `apiKey` を解決し、その値が未設定の環境変数参照である場合はプロバイダー全体を隠します。リテラルであればルーティングされたすべてのモデルが表示され、ループバックではプロキシがこの値を検査することはありません。 +このリテラルは、プロキシのアドミッション認証情報でも上流プロバイダーのキーでもありません。ループバック プロキシはこの値を無視し、認証情報を一切要求しません。ただしモデル検出には必須です。Pi はモデル リストを構築する際に `apiKey` を解決し、その値が未設定の環境変数参照である場合はプロバイダー全体を隠すため、リテラルであればルーティングされたすべてのモデルが表示されます。 -プロバイダー キーは別の話です。Anthropic / OpenAI / OpenRouter のキーは opencodex 自身の設定にあり ([プロバイダー](/guides/providers/) を参照)、このファイルには決して現れません。 +プロバイダー キーは別のものです。Anthropic / OpenAI / OpenRouter のキーは opencodex 自身の設定にあり ([プロバイダー](/guides/providers/) を参照)、このファイルには決して現れません。 ## モデルのメタデータ diff --git a/docs-site/src/content/docs/ko/guides/pi.md b/docs-site/src/content/docs/ko/guides/pi.md index 7fa8222640..5b044f875c 100644 --- a/docs-site/src/content/docs/ko/guides/pi.md +++ b/docs-site/src/content/docs/ko/guides/pi.md @@ -68,21 +68,19 @@ ocx export --client pi --json > ~/opencodex-pi-models.json # or redirect the b 내보낸 블록은 실시간 뷰가 아니라 고정 스냅샷입니다. provider를 추가하거나 모델 가시성을 바꾼 뒤에는 `ocx export`를 다시 실행하고, 새 블록을 옛 블록 위에 병합하세요. -## 인증 키 +## Pi의 `apiKey` placeholder -**루프백 프록시는 키가 전혀 필요 없습니다.** opencodex는 기본적으로 `127.0.0.1`에 -바인드하고 그곳에서는 아무 것도 인증하지 않으므로, 내보낸 블록에는 실제 인증 정보가 -아니라 리터럴 placeholder인 `opencodex-loopback`이 들어갑니다. 환경 변수는 전혀 -관여하지 않습니다. +Pi는 일반적으로 `/chat/completions`를 호출하며, 설정된 `apiKey`를 Bearer 인증 값으로 +보냅니다. 따라서 생성된 블록은 Pi의 일반 `apiKey` 필드에 비밀이 아닌 리터럴 +`opencodex-loopback`을 넣습니다. -이 placeholder는 겉치레가 아니라 필수입니다. Pi는 모델 목록을 만들 때 `apiKey`를 -해석하는데, 값이 설정되지 않은 환경 변수 참조이면 provider 전체를 숨겨 버립니다. -리터럴이어야 라우팅된 모든 모델이 보이며, 루프백에서 프록시는 이 값을 검사하지 -않습니다. +이 리터럴은 프록시 admission credential도 upstream provider 키도 아닙니다. 루프백 +프록시는 이 값을 무시하며 credential을 전혀 요구하지 않습니다. 다만 모델 탐색에는 +필수입니다. Pi는 모델 목록을 만들 때 `apiKey`를 해석하고, 값이 설정되지 않은 환경 변수 +참조이면 provider 전체를 숨기므로, 리터럴이어야 라우팅된 모든 모델이 보입니다. -Provider 키는 별개의 문제입니다. Anthropic / OpenAI / OpenRouter 키는 opencodex의 -자체 config에 있으며([Providers](/guides/providers/) 참조), 이 파일에는 절대 -나타나지 않습니다. +Provider 키는 별개입니다. Anthropic / OpenAI / OpenRouter 키는 opencodex의 자체 +config에 있으며([Providers](/guides/providers/) 참조), 이 파일에는 절대 나타나지 않습니다. ## 모델 메타데이터 diff --git a/docs-site/src/content/docs/ru/guides/pi.md b/docs-site/src/content/docs/ru/guides/pi.md index 2615064231..a1b7e1dbf0 100644 --- a/docs-site/src/content/docs/ru/guides/pi.md +++ b/docs-site/src/content/docs/ru/guides/pi.md @@ -69,15 +69,17 @@ ocx export --client pi --json > ~/opencodex-pi-models.json # or redirect the b провайдера или изменения видимости моделей заново выполняйте `ocx export`, а новый блок вливайте поверх старого. -## Admission key +## Заглушка Pi `apiKey` -**Прокси на loopback вообще не требует ключа.** По умолчанию opencodex привязывается к -`127.0.0.1` и ничего там не аутентифицирует, поэтому экспортируемый блок несёт не настоящие -учётные данные, а литеральную заглушку `opencodex-loopback`; переменные окружения тут ни при чём. +Pi обычно вызывает `/chat/completions` и отправляет настроенный `apiKey` как значение Bearer- +авторизации. Поэтому сгенерированный блок помещает несекретный литерал `opencodex-loopback` в +обычное поле Pi `apiKey`. -Заглушка нужна не для красоты: Pi разрешает `apiKey`, когда строит список моделей, и прячет -провайдера целиком, если значение — ссылка на незаданную переменную окружения. Литерал -сохраняет видимость всех маршрутизируемых моделей, а на loopback прокси это значение не проверяет. +Этот литерал — не credential допуска прокси и не ключ upstream-провайдера. Loopback-прокси +игнорирует его и вообще не требует credential. При этом значение необходимо для обнаружения +моделей: Pi разрешает `apiKey`, когда строит список, и прячет провайдера целиком, если значение — +ссылка на незаданную переменную окружения. Литерал сохраняет видимость всех маршрутизируемых +моделей. Ключи провайдеров — отдельная история: ключ Anthropic / OpenAI / OpenRouter хранится в конфигурации самого opencodex, см. [Провайдеры](/guides/providers/), и в этом файле не появляется. diff --git a/docs-site/src/content/docs/zh-cn/guides/pi.md b/docs-site/src/content/docs/zh-cn/guides/pi.md index 9b7982ac00..45926460d7 100644 --- a/docs-site/src/content/docs/zh-cn/guides/pi.md +++ b/docs-site/src/content/docs/zh-cn/guides/pi.md @@ -58,11 +58,11 @@ ocx export --client pi --json > ~/opencodex-pi-models.json # or redirect the b 导出的块是静态快照,不是实时视图。新增 provider 或更改模型可见性后,请重新运行 `ocx export`,再用新的块覆盖旧块进行合并。 -## 准入密钥 +## Pi 的 `apiKey` 占位值 -**回环代理根本不需要 key。** opencodex 默认绑定 `127.0.0.1`,在那里不做任何认证,所以导出的块携带的是字面占位值 `opencodex-loopback`,而不是真实凭据 - 完全不涉及环境变量。 +Pi 通常调用 `/chat/completions`,并将配置的 `apiKey` 作为 Bearer 认证值发送。因此,生成的块会在 Pi 的常规 `apiKey` 字段中写入非机密字面值 `opencodex-loopback`。 -这个占位值不是装饰,而是必需的:Pi 在构建模型列表时会解析 `apiKey`,一旦该值是未设置的环境变量引用,它就会隐藏整个 provider。使用字面值才能让所有已路由模型保持可见,而回环上的代理从不校验这个值。 +这个字面值既不是代理准入凭据,也不是上游 provider key。回环代理会忽略它,并且完全不需要凭据。不过它对模型发现是必需的:Pi 在构建模型列表时会解析 `apiKey`,如果该值是未设置的环境变量引用,它就会隐藏整个 provider;使用字面值才能让所有已路由模型保持可见。 Provider key 是另一回事:你的 Anthropic / OpenAI / OpenRouter key 保存在 opencodex 自己的配置中,见 [Providers](/guides/providers/),它绝不会出现在这个文件里。 From eea69391c9db37fbb9bc3db8bc4707f32e8d86e7 Mon Sep 17 00:00:00 2001 From: n3wr1ch <40690535+n3wr1ch@users.noreply.github.com> Date: Thu, 6 Aug 2026 09:43:06 +0900 Subject: [PATCH 07/16] docs(pi): fix Russian Bearer wording --- docs-site/src/content/docs/ru/guides/pi.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs-site/src/content/docs/ru/guides/pi.md b/docs-site/src/content/docs/ru/guides/pi.md index a1b7e1dbf0..ab6407f2c9 100644 --- a/docs-site/src/content/docs/ru/guides/pi.md +++ b/docs-site/src/content/docs/ru/guides/pi.md @@ -71,8 +71,8 @@ ocx export --client pi --json > ~/opencodex-pi-models.json # or redirect the b ## Заглушка Pi `apiKey` -Pi обычно вызывает `/chat/completions` и отправляет настроенный `apiKey` как значение Bearer- -авторизации. Поэтому сгенерированный блок помещает несекретный литерал `opencodex-loopback` в +Pi обычно вызывает `/chat/completions` и отправляет настроенный `apiKey` как значение авторизации +Bearer. Поэтому сгенерированный блок помещает несекретный литерал `opencodex-loopback` в обычное поле Pi `apiKey`. Этот литерал — не credential допуска прокси и не ключ upstream-провайдера. Loopback-прокси From 75b0c5b071a1fe45bebd4d96c8d47358cc9d32f5 Mon Sep 17 00:00:00 2001 From: n3wr1ch <40690535+n3wr1ch@users.noreply.github.com> Date: Thu, 6 Aug 2026 20:21:20 +0900 Subject: [PATCH 08/16] fix(pi): clarify loopback placeholder guidance --- docs-site/src/content/docs/guides/pi.md | 2 +- docs-site/src/content/docs/ja/guides/pi.md | 2 +- docs-site/src/content/docs/ja/reference/cli/agents.md | 2 +- docs-site/src/content/docs/ko/guides/pi.md | 4 ++-- docs-site/src/content/docs/ko/reference/cli/agents.md | 2 +- docs-site/src/content/docs/reference/cli/agents.md | 4 ++-- docs-site/src/content/docs/ru/guides/pi.md | 4 ++-- docs-site/src/content/docs/ru/reference/cli/agents.md | 4 ++-- docs-site/src/content/docs/zh-cn/guides/pi.md | 2 +- docs-site/src/content/docs/zh-cn/reference/cli/agents.md | 2 +- .../components/apikeys-workspace/ClientConfigDialog.tsx | 4 ++-- gui/src/i18n/de.ts | 2 +- gui/src/i18n/en.ts | 2 +- gui/src/i18n/ja.ts | 2 +- gui/src/i18n/ko.ts | 2 +- gui/src/i18n/ru.ts | 2 +- gui/src/i18n/zh.ts | 2 +- gui/tests/client-config-panel.test.tsx | 8 ++++++-- src/cli/export-command.ts | 8 ++++---- src/clients/config-export.ts | 2 +- 20 files changed, 33 insertions(+), 29 deletions(-) diff --git a/docs-site/src/content/docs/guides/pi.md b/docs-site/src/content/docs/guides/pi.md index 76b0fe40bd..5cb3d6e3e9 100644 --- a/docs-site/src/content/docs/guides/pi.md +++ b/docs-site/src/content/docs/guides/pi.md @@ -5,7 +5,7 @@ description: Use any routed model from Pi — ocx export writes a custom provide Pi reads its providers from a single global JSON file rather than environment variables, so opencodex does not launch it. Instead, `ocx export` serializes the `opencodex` provider block — -base URL, model list, and a placeholder admission key — and you merge it into your own config. +base URL, model list, and a non-secret literal `apiKey` placeholder — and you merge it into your own config. ## Quickstart diff --git a/docs-site/src/content/docs/ja/guides/pi.md b/docs-site/src/content/docs/ja/guides/pi.md index 4dfb17dcf1..3c1585e2da 100644 --- a/docs-site/src/content/docs/ja/guides/pi.md +++ b/docs-site/src/content/docs/ja/guides/pi.md @@ -3,7 +3,7 @@ title: 円周率 description: Pi からルーティングされたモデルを使用します。ocx エクスポートは、実行中のプロキシに接続された Pi の models.json のカスタム プロバイダー ブロックを書き込みます。 --- -Pi は環境変数ではなく単一のグローバル JSON ファイルからプロバイダーを読み取るため、opencodex はそれを起動しません。代わりに、`ocx export` は `opencodex` プロバイダー ブロック (ベース URL、モデル リスト、プレースホルダーのアドミッション キー) をシリアル化し、それを独自の設定にマージします。 +Pi は環境変数ではなく単一のグローバル JSON ファイルからプロバイダーを読み取るため、opencodex はそれを起動しません。代わりに、`ocx export` は `opencodex` プロバイダー ブロック (ベース URL、モデル リスト、秘密ではないリテラル `apiKey` プレースホルダー) をシリアル化し、それを独自の設定にマージします。 ## クイックスタート diff --git a/docs-site/src/content/docs/ja/reference/cli/agents.md b/docs-site/src/content/docs/ja/reference/cli/agents.md index eb9f97fae6..9446edf44a 100644 --- a/docs-site/src/content/docs/ja/reference/cli/agents.md +++ b/docs-site/src/content/docs/ja/reference/cli/agents.md @@ -127,7 +127,7 @@ Grok Build モデル フェンスを管理および適用します。 ### `ocx export --client ` -実行中のプロキシに接続されているクライアント設定を出力します。 opencode と [円周率](/guides/pi/) は環境変数ではなく独自の JSON 設定からプロバイダーを読み取るため、このコマンドは `opencodex` プロバイダー ブロック (ベース URL、モデル リスト、クライアントの環境参照) をシリアル化し、そのファイルにマージできるようにします。 +実行中のプロキシに接続されているクライアント設定を出力します。 opencode と [円周率](/guides/pi/) は環境変数ではなく独自の JSON 設定からプロバイダーを読み取るため、このコマンドは `opencodex` プロバイダー ブロック (ベース URL、モデル リスト、クライアント固有の認証情報参照またはループバック プレースホルダー) をシリアル化し、そのファイルにマージできるようにします。 プロキシが実行されている必要があります。このコマンドはライブ ポートを解決し、`/api/models` を読み取り、Codex が現在認識できるモデルのみを出力します。 diff --git a/docs-site/src/content/docs/ko/guides/pi.md b/docs-site/src/content/docs/ko/guides/pi.md index 5b044f875c..23d4a58086 100644 --- a/docs-site/src/content/docs/ko/guides/pi.md +++ b/docs-site/src/content/docs/ko/guides/pi.md @@ -5,8 +5,8 @@ description: Pi에서 라우팅된 모델을 그대로 쓸 수 있습니다. `oc Pi는 provider를 환경 변수 대신 하나의 전역 JSON 파일에서 읽기 때문에, opencodex가 Pi를 직접 실행하지 않습니다. 대신 `ocx export`가 `opencodex` provider 블록, -즉 base URL, 모델 목록, 그리고 placeholder 인증 키를 직렬화해서 사용자가 -자신의 설정에 병합하도록 합니다. +즉 base URL, 모델 목록, 그리고 비밀이 아닌 리터럴 `apiKey` placeholder를 직렬화해서 +사용자가 자신의 설정에 병합하도록 합니다. ## 빠른 시작 diff --git a/docs-site/src/content/docs/ko/reference/cli/agents.md b/docs-site/src/content/docs/ko/reference/cli/agents.md index f3b109a492..ffb46ff16c 100644 --- a/docs-site/src/content/docs/ko/reference/cli/agents.md +++ b/docs-site/src/content/docs/ko/reference/cli/agents.md @@ -133,7 +133,7 @@ Grok Build model fence를 관리하고 적용합니다. ### `ocx export --client ` -실행 중인 프록시에 연결된 client config를 출력합니다. opencode와 [Pi](/guides/pi/)는 environment variable이 아니라 각자의 JSON config에서 provider를 읽으므로, 이 명령은 `opencodex` provider block, 즉 base URL, model list, 그리고 client의 env reference를 직렬화해서 해당 파일에 병합할 수 있게 해줍니다. +실행 중인 프록시에 연결된 client config를 출력합니다. opencode와 [Pi](/guides/pi/)는 environment variable이 아니라 각자의 JSON config에서 provider를 읽으므로, 이 명령은 `opencodex` provider block, 즉 base URL, model list, 그리고 client별 credential reference 또는 loopback placeholder를 직렬화해서 해당 파일에 병합할 수 있게 해줍니다. 프록시는 실행 중이어야 합니다. 이 명령은 실제 포트를 확인하고, `/api/models`를 읽고, 현재 Codex가 볼 수 있는 model만 내보냅니다. diff --git a/docs-site/src/content/docs/reference/cli/agents.md b/docs-site/src/content/docs/reference/cli/agents.md index 1515bd13bc..fdd9c4b600 100644 --- a/docs-site/src/content/docs/reference/cli/agents.md +++ b/docs-site/src/content/docs/reference/cli/agents.md @@ -150,8 +150,8 @@ Manage and apply the Grok Build model fence. Print a client config wired to the running proxy. opencode and [Pi](/guides/pi/) read providers from their own JSON config rather than environment variables, so this command serializes the -`opencodex` provider block — base URL, model list, and the client's env reference — for you to -merge into that file. +`opencodex` provider block — base URL, model list, and the client's credential reference or +loopback placeholder — for you to merge into that file. The proxy must be running; the command resolves its live port, reads `/api/models`, and emits only models Codex can currently see. diff --git a/docs-site/src/content/docs/ru/guides/pi.md b/docs-site/src/content/docs/ru/guides/pi.md index ab6407f2c9..7dca6e5bb5 100644 --- a/docs-site/src/content/docs/ru/guides/pi.md +++ b/docs-site/src/content/docs/ru/guides/pi.md @@ -5,8 +5,8 @@ description: Используйте любую маршрутизируемую Pi читает провайдеров из одного глобального JSON-файла, а не из переменных окружения, поэтому opencodex не запускает его сам. Вместо этого `ocx export` сериализует блок провайдера -`opencodex` — base URL, список моделей и ключ-заглушку допуска, — а вы сливаете его в свою -конфигурацию. +`opencodex` — base URL, список моделей и несекретный литеральный плейсхолдер `apiKey`, — а вы +сливаете его в свою конфигурацию. ## Быстрый старт diff --git a/docs-site/src/content/docs/ru/reference/cli/agents.md b/docs-site/src/content/docs/ru/reference/cli/agents.md index b3c08052bf..2f62e6b60a 100644 --- a/docs-site/src/content/docs/ru/reference/cli/agents.md +++ b/docs-site/src/content/docs/ru/reference/cli/agents.md @@ -156,8 +156,8 @@ override, но файлы на диске никогда не меняются. Печатает client config, направленный на работающий прокси. opencode и [Pi](/guides/pi/) читают провайдеров из собственных JSON-конфигов, а не из переменных окружения, поэтому команда -сериализует для вас блок провайдера `opencodex` — base URL, список моделей и env-reference -конкретного клиента. +сериализует для вас блок провайдера `opencodex` — base URL, список моделей и клиентскую +ссылку на credential либо loopback-заглушку. Прокси должен быть запущен; команда определяет его живой порт, читает `/api/models` и выводит только те модели, которые сейчас видит Codex. diff --git a/docs-site/src/content/docs/zh-cn/guides/pi.md b/docs-site/src/content/docs/zh-cn/guides/pi.md index 45926460d7..088ad71461 100644 --- a/docs-site/src/content/docs/zh-cn/guides/pi.md +++ b/docs-site/src/content/docs/zh-cn/guides/pi.md @@ -3,7 +3,7 @@ title: Pi description: 在 Pi 中使用任意已路由模型 - `ocx export` 会为 Pi 的 `models.json` 写入一个自定义 provider 块,并连接到正在运行的代理。 --- -Pi 从一个全局 JSON 文件而不是环境变量中读取 providers,所以 opencodex 不会启动它。相反,`ocx export` 会序列化 `opencodex` provider 块 - 基础 URL、模型列表,以及一个占位准入 key - 然后你把它合并到自己的配置中。 +Pi 从一个全局 JSON 文件而不是环境变量中读取 providers,所以 opencodex 不会启动它。相反,`ocx export` 会序列化 `opencodex` provider 块——基础 URL、模型列表,以及一个非秘密的字面量 `apiKey` 占位值——然后你把它合并到自己的配置中。 ## 快速开始 diff --git a/docs-site/src/content/docs/zh-cn/reference/cli/agents.md b/docs-site/src/content/docs/zh-cn/reference/cli/agents.md index 6957537ee6..e0651b6859 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli/agents.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli/agents.md @@ -128,7 +128,7 @@ ocx claude desktop import [--apply] Validate and import JSON ### `ocx export --client ` -输出连接到正在运行代理的客户端配置。opencode 和 [Pi](/guides/pi/) 不是从环境变量,而是从各自的 JSON 配置中读取 providers,因此此命令会序列化 `opencodex` provider 块——基础 URL、模型列表以及客户端的环境引用——供你合并进那个文件。 +输出连接到正在运行代理的客户端配置。opencode 和 [Pi](/guides/pi/) 不是从环境变量,而是从各自的 JSON 配置中读取 providers,因此此命令会序列化 `opencodex` provider 块——基础 URL、模型列表以及客户端对应的凭据引用或回环占位值——供你合并进那个文件。 代理必须正在运行;该命令会解析其当前端口,读取 `/api/models`,并且只输出 Codex 当前可见的模型。 diff --git a/gui/src/components/apikeys-workspace/ClientConfigDialog.tsx b/gui/src/components/apikeys-workspace/ClientConfigDialog.tsx index cad0dbcd25..1a0026b21d 100644 --- a/gui/src/components/apikeys-workspace/ClientConfigDialog.tsx +++ b/gui/src/components/apikeys-workspace/ClientConfigDialog.tsx @@ -81,8 +81,8 @@ export default function ClientConfigDialog({ {!hasKeys && envelope.apiKeyEnv !== "" && ( // Informational, never blocking: an agent may legitimately want the shape // first, so both actions stay enabled. A client with no env var at all - // (Pi, Kimi) reads its credential from its own file, so naming a - // variable there would render an empty name. + // (Pi, Kimi) carries its non-secret loopback placeholder in its own file, + // so naming a variable here would render an empty name.

{t("api.clientConfig.noKeyYet", { env: envelope.apiKeyEnv })}

diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 7703f6c09f..d0f298914d 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -1137,7 +1137,7 @@ export const de: Record = { "api.clientConfig.loading": "Client-Konfiguration wird erstellt…", "api.clientConfig.jsonLabel": "{client}-Konfigurations-JSON", "api.clientConfig.destination": "Zieldatei", - "api.clientConfig.envHint": "Schlüssel vor dem Start setzen", + "api.clientConfig.envHint": "Vor dem Start", "api.clientConfig.mergeWarning": "Führe dies in die Zieldatei ein. Ein Ersetzen würde deine anderen Provider und MCP-Einstellungen entfernen.", "api.clientConfig.modelCount": "{count} Modell(e) exportiert", "api.clientConfig.missingLimits": "{count} von {total} Modell(en) haben kein Kontextlimit; der Client verwendet dafür seine eigenen Vorgaben.", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 9ae8861249..3f7e2dd56c 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -1600,7 +1600,7 @@ export const en = { "api.clientConfig.loading": "Building client config…", "api.clientConfig.jsonLabel": "{client} config JSON", "api.clientConfig.destination": "Destination file", - "api.clientConfig.envHint": "Set the key before launching", + "api.clientConfig.envHint": "Before launching", "api.clientConfig.mergeWarning": "Merge this into the destination file. Replacing it would drop your other providers and MCP settings.", "api.clientConfig.modelCount": "{count} model(s) exported", "api.clientConfig.missingLimits": "{count} of {total} model(s) ship without a context limit; the client applies its own defaults.", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index d5f7a915b4..92f630e58f 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -1545,7 +1545,7 @@ export const ja: Record = { "api.clientConfig.loading": "クライアント設定を生成中…", "api.clientConfig.jsonLabel": "{client} 設定 JSON", "api.clientConfig.destination": "配置先ファイル", - "api.clientConfig.envHint": "起動前にキーを設定", + "api.clientConfig.envHint": "起動前の案内", "api.clientConfig.mergeWarning": "配置先ファイルにマージしてください。置き換えると既存のプロバイダーや MCP 設定が失われます。", "api.clientConfig.modelCount": "{count} 件のモデルを書き出しました", "api.clientConfig.missingLimits": "{total} 件中 {count} 件のモデルにコンテキスト上限がないため、クライアント側の既定値が使われます。", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index e0ee1af6a4..5e0db973ad 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -1164,7 +1164,7 @@ export const ko: Record = { "api.clientConfig.loading": "클라이언트 설정 생성 중…", "api.clientConfig.jsonLabel": "{client} 설정 JSON", "api.clientConfig.destination": "대상 파일", - "api.clientConfig.envHint": "실행 전 키 설정", + "api.clientConfig.envHint": "실행 전 안내", "api.clientConfig.mergeWarning": "대상 파일에 병합하세요. 덮어쓰면 기존 프로바이더와 MCP 설정이 사라집니다.", "api.clientConfig.modelCount": "모델 {count}개 내보냄", "api.clientConfig.missingLimits": "{total}개 중 {count}개 모델에 컨텍스트 한도가 없어 클라이언트 기본값이 적용됩니다.", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index f707bb5f5e..120dd23ed8 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -1587,7 +1587,7 @@ export const ru: Record = { "api.clientConfig.loading": "Формируется конфигурация клиента…", "api.clientConfig.jsonLabel": "JSON конфигурации {client}", "api.clientConfig.destination": "Целевой файл", - "api.clientConfig.envHint": "Задайте ключ перед запуском", + "api.clientConfig.envHint": "Перед запуском", "api.clientConfig.mergeWarning": "Объедините это с целевым файлом. Замена удалит ваши другие провайдеры и настройки MCP.", "api.clientConfig.modelCount": "Экспортировано моделей: {count}", "api.clientConfig.missingLimits": "У {count} из {total} моделей нет лимита контекста; клиент применит свои значения по умолчанию.", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 14113c6110..6f2b4b6cd6 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -1157,7 +1157,7 @@ export const zh: Record = { "api.clientConfig.loading": "正在生成客户端配置…", "api.clientConfig.jsonLabel": "{client} 配置 JSON", "api.clientConfig.destination": "目标文件", - "api.clientConfig.envHint": "启动前设置密钥", + "api.clientConfig.envHint": "启动前提示", "api.clientConfig.mergeWarning": "请合并到目标文件中。直接替换会丢失你已有的其他提供商和 MCP 配置。", "api.clientConfig.modelCount": "已导出 {count} 个模型", "api.clientConfig.missingLimits": "{total} 个模型中有 {count} 个没有上下文上限,客户端将使用自己的默认值。", diff --git a/gui/tests/client-config-panel.test.tsx b/gui/tests/client-config-panel.test.tsx index 1dd7287f8d..d037715260 100644 --- a/gui/tests/client-config-panel.test.tsx +++ b/gui/tests/client-config-panel.test.tsx @@ -48,7 +48,7 @@ const PI_ENVELOPE_BASE = { filename: "pi-models.json", destination: "/home/dev/.pi/agent/models.json", apiKeyEnv: "", - exportHint: "Pi reads credentials from its models.json; loopback needs no key.", + exportHint: "Pi reads a non-secret placeholder from models.json; loopback needs no key.", modelCount: 2, modelsWithoutLimits: 1, format: "json", @@ -476,7 +476,11 @@ test("no-key state is informational and leaves copy and download enabled", async .toContain("OPENCODEX_OPENCODE_API_KEY has no key behind it yet"); await act(async () => { button(container, "Close").click(); }); await act(async () => { rowButton(container, "Pi", "Details").click(); }); - expect(container.querySelector(".awi-clientconfig-nokey")).toBeNull(); + const piDialog = container.querySelector("dialog")!; + expect(piDialog.querySelector(".awi-clientconfig-nokey")).toBeNull(); + expect(piDialog.textContent).toContain("Before launching"); + expect(piDialog.textContent).toContain("Pi reads a non-secret placeholder from models.json; loopback needs no key."); + expect(piDialog.textContent).not.toContain("Set the key before launching"); await act(async () => { root.unmount(); }); }); diff --git a/src/cli/export-command.ts b/src/cli/export-command.ts index d7d074fb5f..0b38e89046 100644 --- a/src/cli/export-command.ts +++ b/src/cli/export-command.ts @@ -12,7 +12,7 @@ * readability, not the client's native format. Every diagnostic — including the * `--out` write note — goes to stderr. * - **Human** (no flag) and `--out`: the client's NATIVE serialization leads, then the - * destination path, the merge warning, the env export line, and the model/degraded + * destination path, merge warning, client-specific pre-launch guidance, and model/degraded * counts. * * The command never writes the user's real config path. `--out` is an explicit target and @@ -20,9 +20,9 @@ * (`--out ~/.config/opencode/opencode.json`) would silently destroy other providers. * * Serialization itself belongs to src/clients/config-export.ts; this module only resolves - * the base URL, filters the catalog, and renders. No secret is ever serialized: the config - * carries the client's documented env reference — or, for Kimi, which cannot hold one, a - * loopback placeholder — and the real key stays in the environment. + * the base URL, filters the catalog, and renders. No secret is serialized: the config carries + * the client's documented env reference or, for loopback-only file clients, a non-secret + * placeholder. Real keys stay outside the generated file. */ import { writeFileSync } from "node:fs"; import { loadConfig } from "../config"; diff --git a/src/clients/config-export.ts b/src/clients/config-export.ts index 0853ea85c6..10d8cd3cc2 100644 --- a/src/clients/config-export.ts +++ b/src/clients/config-export.ts @@ -951,7 +951,7 @@ export const EXPORT_CLIENTS: Record = { // Pi carries the placeholder literal in models.json (see // LOOPBACK_API_KEY_PLACEHOLDER), so there is no env var to export. apiKeyEnv: "", - exportHint: "Pi reads credentials from its models.json; loopback needs no key.", + exportHint: "Pi reads a non-secret placeholder from models.json; loopback needs no key.", build: buildPiClientConfig, format: "json", summarize: summarizePi, From 8bc35cb99b4774023e42fc8f72b0724abedf9467 Mon Sep 17 00:00:00 2001 From: n3wr1ch <40690535+n3wr1ch@users.noreply.github.com> Date: Thu, 6 Aug 2026 20:36:24 +0900 Subject: [PATCH 09/16] docs(pi): qualify exported model visibility --- docs-site/src/content/docs/guides/pi.md | 4 ++-- docs-site/src/content/docs/ja/guides/pi.md | 2 +- docs-site/src/content/docs/ko/guides/pi.md | 4 ++-- docs-site/src/content/docs/ru/guides/pi.md | 5 +++-- docs-site/src/content/docs/zh-cn/guides/pi.md | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs-site/src/content/docs/guides/pi.md b/docs-site/src/content/docs/guides/pi.md index 5cb3d6e3e9..d72ce6f995 100644 --- a/docs-site/src/content/docs/guides/pi.md +++ b/docs-site/src/content/docs/guides/pi.md @@ -100,8 +100,8 @@ a guess. :::note[Verified against a real install] The shape above has been verified against Pi 0.83.x on a real `~/.pi/agent/models.json`: the block -validates and every routed model appears in Pi's picker. If a newer Pi rejects the exported -block, the mismatch is on our side — please +validates, and every exported routed model with a Pi-supported input modality appears in Pi's +picker. If a newer Pi rejects the exported block, the mismatch is on our side — please [open an issue](https://github.com/lidge-jun/opencodex/issues) with what Pi reported. ::: diff --git a/docs-site/src/content/docs/ja/guides/pi.md b/docs-site/src/content/docs/ja/guides/pi.md index 3c1585e2da..c5aab97cd6 100644 --- a/docs-site/src/content/docs/ja/guides/pi.md +++ b/docs-site/src/content/docs/ja/guides/pi.md @@ -77,7 +77,7 @@ Pi は通常 `/chat/completions` を呼び出し、設定された `apiKey` を ## スキーマのステータス :::note[実際のインストールで検証済み] -上の形状は、Pi 0.83.x がインストールされたマシン上の実際の `~/.pi/agent/models.json` に対して検証されています。ブロックは検証を通過し、ルーティングされたすべてのモデルが Pi のピッカーに表示されます。より新しい Pi がエクスポートされたブロックを拒否した場合、不一致は私たちの側にあります。Pi が報告した内容を添えて [問題を開く](https://github.com/lidge-jun/opencodex/issues) してください。 +上の形状は、Pi 0.83.x がインストールされたマシン上の実際の `~/.pi/agent/models.json` に対して検証されています。ブロックは検証を通過し、Pi がサポートする入力モダリティを持つ、エクスポートされたすべてのルーティング モデルが Pi のピッカーに表示されます。より新しい Pi がエクスポートされたブロックを拒否した場合、不一致は私たちの側にあります。Pi が報告した内容を添えて [問題を開く](https://github.com/lidge-jun/opencodex/issues) してください。 ::: ## 要件 diff --git a/docs-site/src/content/docs/ko/guides/pi.md b/docs-site/src/content/docs/ko/guides/pi.md index 23d4a58086..87c5dbbd8f 100644 --- a/docs-site/src/content/docs/ko/guides/pi.md +++ b/docs-site/src/content/docs/ko/guides/pi.md @@ -101,8 +101,8 @@ config에 있으며([Providers](/guides/providers/) 참조), 이 파일에는 :::note[실제 설치에서 검증됨] 위의 형태는 Pi 0.83.x가 설치된 머신의 실제 `~/.pi/agent/models.json`으로 검증했습니다. -블록이 유효하고 라우팅된 모든 모델이 Pi 선택기에 표시됩니다. 더 새로운 Pi가 이 블록을 -거부하면 문제는 우리 쪽에 있습니다. Pi가 무엇을 보고했는지와 함께 +블록이 유효하고 Pi가 지원하는 입력 modality를 가진 내보낸 라우팅 모델이 모두 선택기에 +표시됩니다. 더 새로운 Pi가 이 블록을 거부하면 문제는 우리 쪽에 있습니다. Pi가 무엇을 보고했는지와 함께 [issue를 열어주세요](https://github.com/lidge-jun/opencodex/issues). ::: diff --git a/docs-site/src/content/docs/ru/guides/pi.md b/docs-site/src/content/docs/ru/guides/pi.md index 7dca6e5bb5..d47e7e8bee 100644 --- a/docs-site/src/content/docs/ru/guides/pi.md +++ b/docs-site/src/content/docs/ru/guides/pi.md @@ -103,8 +103,9 @@ Bearer. Поэтому сгенерированный блок помещает :::note[Проверено на реальной установке] Форма выше проверена на реальном `~/.pi/agent/models.json` с установленным Pi 0.83.x: блок -проходит валидацию, и все маршрутизируемые модели видны в выборе моделей Pi. Если более новый Pi -отвергнет экспортированный блок, несоответствие на нашей стороне — пожалуйста, +проходит валидацию, и все экспортированные маршрутизируемые модели с поддерживаемой Pi входной +модальностью видны в выборе моделей. Если более новый Pi отвергнет экспортированный блок, +несоответствие на нашей стороне — пожалуйста, [создайте issue](https://github.com/lidge-jun/opencodex/issues) и приложите то, что сообщил Pi. ::: diff --git a/docs-site/src/content/docs/zh-cn/guides/pi.md b/docs-site/src/content/docs/zh-cn/guides/pi.md index 088ad71461..e502d557dc 100644 --- a/docs-site/src/content/docs/zh-cn/guides/pi.md +++ b/docs-site/src/content/docs/zh-cn/guides/pi.md @@ -77,7 +77,7 @@ Provider key 是另一回事:你的 Anthropic / OpenAI / OpenRouter key 保存 ## Schema 状态 :::note[已在真实安装上验证] -上面的结构已在安装了 Pi 0.83.x 的机器上、针对真实的 `~/.pi/agent/models.json` 完成验证:该块通过校验,并且所有已路由模型都会出现在 Pi 的模型选择器中。如果更新版本的 Pi 拒绝这个导出块,问题在我们这边 - 请带上 Pi 的报错信息[提交 issue](https://github.com/lidge-jun/opencodex/issues)。 +上面的结构已在安装了 Pi 0.83.x 的机器上、针对真实的 `~/.pi/agent/models.json` 完成验证:该块通过校验,并且所有具有 Pi 所支持输入模态的已导出路由模型都会出现在 Pi 的模型选择器中。如果更新版本的 Pi 拒绝这个导出块,问题在我们这边 - 请带上 Pi 的报错信息[提交 issue](https://github.com/lidge-jun/opencodex/issues)。 ::: ## 需求 From eb6d7849efa44f1044dbfc5c5e3733c3a5487366 Mon Sep 17 00:00:00 2001 From: n3wr1ch <40690535+n3wr1ch@users.noreply.github.com> Date: Thu, 6 Aug 2026 20:38:56 +0900 Subject: [PATCH 10/16] docs(pi): describe export summary counts --- docs-site/src/content/docs/guides/pi.md | 4 ++-- docs-site/src/content/docs/ja/guides/pi.md | 2 +- docs-site/src/content/docs/ko/guides/pi.md | 4 ++-- docs-site/src/content/docs/ru/guides/pi.md | 4 ++-- docs-site/src/content/docs/zh-cn/guides/pi.md | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs-site/src/content/docs/guides/pi.md b/docs-site/src/content/docs/guides/pi.md index d72ce6f995..5f76f0aa9c 100644 --- a/docs-site/src/content/docs/guides/pi.md +++ b/docs-site/src/content/docs/guides/pi.md @@ -16,8 +16,8 @@ ocx start ocx export --client pi ``` -The output leads with the JSON, then prints the destination path, the merge warning, and how many -models carry authoritative context limits. +The output leads with the JSON, then prints the destination path, the merge warning, the total +model count, and how many rows omit context limits. ```json { diff --git a/docs-site/src/content/docs/ja/guides/pi.md b/docs-site/src/content/docs/ja/guides/pi.md index c5aab97cd6..b323d59b20 100644 --- a/docs-site/src/content/docs/ja/guides/pi.md +++ b/docs-site/src/content/docs/ja/guides/pi.md @@ -14,7 +14,7 @@ ocx start ocx export --client pi ``` -出力は JSON で始まり、宛先パス、マージ警告、および権威コンテキスト制限を持つモデルの数を出力します。 +出力は JSON で始まり、宛先パス、マージ警告、モデルの総数、およびコンテキスト制限を省略した行数を出力します。 ```json { diff --git a/docs-site/src/content/docs/ko/guides/pi.md b/docs-site/src/content/docs/ko/guides/pi.md index 87c5dbbd8f..45978337fc 100644 --- a/docs-site/src/content/docs/ko/guides/pi.md +++ b/docs-site/src/content/docs/ko/guides/pi.md @@ -17,8 +17,8 @@ ocx start ocx export --client pi ``` -출력은 JSON으로 시작하고, 이어서 대상 경로, 병합 경고, 그리고 공식 context limit이 -있는 모델 수를 보여줍니다. +출력은 JSON으로 시작하고, 이어서 대상 경로, 병합 경고, 전체 모델 수, 그리고 context +limit을 생략한 row 수를 보여줍니다. ```json { diff --git a/docs-site/src/content/docs/ru/guides/pi.md b/docs-site/src/content/docs/ru/guides/pi.md index d47e7e8bee..840aa1da40 100644 --- a/docs-site/src/content/docs/ru/guides/pi.md +++ b/docs-site/src/content/docs/ru/guides/pi.md @@ -17,8 +17,8 @@ ocx start ocx export --client pi ``` -Сначала выводится JSON, затем путь назначения, предупреждение о merge и число моделей, для -которых есть авторитетные контекстные лимиты. +Сначала выводится JSON, затем путь назначения, предупреждение о merge, общее число моделей и +число строк без контекстных лимитов. ```json { diff --git a/docs-site/src/content/docs/zh-cn/guides/pi.md b/docs-site/src/content/docs/zh-cn/guides/pi.md index e502d557dc..6a2dfa0d56 100644 --- a/docs-site/src/content/docs/zh-cn/guides/pi.md +++ b/docs-site/src/content/docs/zh-cn/guides/pi.md @@ -14,7 +14,7 @@ ocx start ocx export --client pi ``` -输出会先显示 JSON,然后打印目标路径、合并警告,以及有多少模型带有权威上下文窗口限制。 +输出会先显示 JSON,然后打印目标路径、合并警告、模型总数,以及省略上下文限制的行数。 ```json { From a5401106fa88f768df90c77f319af315de77dc7b Mon Sep 17 00:00:00 2001 From: n3wr1ch <40690535+n3wr1ch@users.noreply.github.com> Date: Thu, 6 Aug 2026 20:45:11 +0900 Subject: [PATCH 11/16] docs(pi): name validated Pi version --- docs-site/src/content/docs/guides/pi.md | 2 +- docs-site/src/content/docs/ja/guides/pi.md | 2 +- docs-site/src/content/docs/ko/guides/pi.md | 2 +- docs-site/src/content/docs/ru/guides/pi.md | 2 +- docs-site/src/content/docs/zh-cn/guides/pi.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs-site/src/content/docs/guides/pi.md b/docs-site/src/content/docs/guides/pi.md index 5f76f0aa9c..c3bf047f46 100644 --- a/docs-site/src/content/docs/guides/pi.md +++ b/docs-site/src/content/docs/guides/pi.md @@ -99,7 +99,7 @@ a guess. ## Schema status :::note[Verified against a real install] -The shape above has been verified against Pi 0.83.x on a real `~/.pi/agent/models.json`: the block +The shape above has been verified against Pi 0.83.0 on a real `~/.pi/agent/models.json`: the block validates, and every exported routed model with a Pi-supported input modality appears in Pi's picker. If a newer Pi rejects the exported block, the mismatch is on our side — please [open an issue](https://github.com/lidge-jun/opencodex/issues) with what Pi reported. diff --git a/docs-site/src/content/docs/ja/guides/pi.md b/docs-site/src/content/docs/ja/guides/pi.md index b323d59b20..e6d4fece17 100644 --- a/docs-site/src/content/docs/ja/guides/pi.md +++ b/docs-site/src/content/docs/ja/guides/pi.md @@ -77,7 +77,7 @@ Pi は通常 `/chat/completions` を呼び出し、設定された `apiKey` を ## スキーマのステータス :::note[実際のインストールで検証済み] -上の形状は、Pi 0.83.x がインストールされたマシン上の実際の `~/.pi/agent/models.json` に対して検証されています。ブロックは検証を通過し、Pi がサポートする入力モダリティを持つ、エクスポートされたすべてのルーティング モデルが Pi のピッカーに表示されます。より新しい Pi がエクスポートされたブロックを拒否した場合、不一致は私たちの側にあります。Pi が報告した内容を添えて [問題を開く](https://github.com/lidge-jun/opencodex/issues) してください。 +上の形状は、Pi 0.83.0 がインストールされたマシン上の実際の `~/.pi/agent/models.json` に対して検証されています。ブロックは検証を通過し、Pi がサポートする入力モダリティを持つ、エクスポートされたすべてのルーティング モデルが Pi のピッカーに表示されます。より新しい Pi がエクスポートされたブロックを拒否した場合、不一致は私たちの側にあります。Pi が報告した内容を添えて [問題を開く](https://github.com/lidge-jun/opencodex/issues) してください。 ::: ## 要件 diff --git a/docs-site/src/content/docs/ko/guides/pi.md b/docs-site/src/content/docs/ko/guides/pi.md index 45978337fc..d7de2e4fd9 100644 --- a/docs-site/src/content/docs/ko/guides/pi.md +++ b/docs-site/src/content/docs/ko/guides/pi.md @@ -100,7 +100,7 @@ config에 있으며([Providers](/guides/providers/) 참조), 이 파일에는 ## 스키마 상태 :::note[실제 설치에서 검증됨] -위의 형태는 Pi 0.83.x가 설치된 머신의 실제 `~/.pi/agent/models.json`으로 검증했습니다. +위의 형태는 Pi 0.83.0이 설치된 머신의 실제 `~/.pi/agent/models.json`으로 검증했습니다. 블록이 유효하고 Pi가 지원하는 입력 modality를 가진 내보낸 라우팅 모델이 모두 선택기에 표시됩니다. 더 새로운 Pi가 이 블록을 거부하면 문제는 우리 쪽에 있습니다. Pi가 무엇을 보고했는지와 함께 [issue를 열어주세요](https://github.com/lidge-jun/opencodex/issues). diff --git a/docs-site/src/content/docs/ru/guides/pi.md b/docs-site/src/content/docs/ru/guides/pi.md index 840aa1da40..4f20eb6753 100644 --- a/docs-site/src/content/docs/ru/guides/pi.md +++ b/docs-site/src/content/docs/ru/guides/pi.md @@ -102,7 +102,7 @@ Bearer. Поэтому сгенерированный блок помещает ## Статус схемы :::note[Проверено на реальной установке] -Форма выше проверена на реальном `~/.pi/agent/models.json` с установленным Pi 0.83.x: блок +Форма выше проверена на реальном `~/.pi/agent/models.json` с установленным Pi 0.83.0: блок проходит валидацию, и все экспортированные маршрутизируемые модели с поддерживаемой Pi входной модальностью видны в выборе моделей. Если более новый Pi отвергнет экспортированный блок, несоответствие на нашей стороне — пожалуйста, diff --git a/docs-site/src/content/docs/zh-cn/guides/pi.md b/docs-site/src/content/docs/zh-cn/guides/pi.md index 6a2dfa0d56..2ee51ac7c9 100644 --- a/docs-site/src/content/docs/zh-cn/guides/pi.md +++ b/docs-site/src/content/docs/zh-cn/guides/pi.md @@ -77,7 +77,7 @@ Provider key 是另一回事:你的 Anthropic / OpenAI / OpenRouter key 保存 ## Schema 状态 :::note[已在真实安装上验证] -上面的结构已在安装了 Pi 0.83.x 的机器上、针对真实的 `~/.pi/agent/models.json` 完成验证:该块通过校验,并且所有具有 Pi 所支持输入模态的已导出路由模型都会出现在 Pi 的模型选择器中。如果更新版本的 Pi 拒绝这个导出块,问题在我们这边 - 请带上 Pi 的报错信息[提交 issue](https://github.com/lidge-jun/opencodex/issues)。 +上面的结构已在安装了 Pi 0.83.0 的机器上、针对真实的 `~/.pi/agent/models.json` 完成验证:该块通过校验,并且所有具有 Pi 所支持输入模态的已导出路由模型都会出现在 Pi 的模型选择器中。如果更新版本的 Pi 拒绝这个导出块,问题在我们这边 - 请带上 Pi 的报错信息[提交 issue](https://github.com/lidge-jun/opencodex/issues)。 ::: ## 需求 From dc118d8674d5a0e6effc062bead8b278b4b99c0d Mon Sep 17 00:00:00 2001 From: n3wr1ch <40690535+n3wr1ch@users.noreply.github.com> Date: Thu, 6 Aug 2026 20:49:04 +0900 Subject: [PATCH 12/16] docs(pi): include pre-launch output guidance --- docs-site/src/content/docs/guides/pi.md | 4 ++-- docs-site/src/content/docs/ja/guides/pi.md | 2 +- docs-site/src/content/docs/ko/guides/pi.md | 4 ++-- docs-site/src/content/docs/ru/guides/pi.md | 4 ++-- docs-site/src/content/docs/zh-cn/guides/pi.md | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs-site/src/content/docs/guides/pi.md b/docs-site/src/content/docs/guides/pi.md index c3bf047f46..cfdfbc9d4d 100644 --- a/docs-site/src/content/docs/guides/pi.md +++ b/docs-site/src/content/docs/guides/pi.md @@ -16,8 +16,8 @@ ocx start ocx export --client pi ``` -The output leads with the JSON, then prints the destination path, the merge warning, the total -model count, and how many rows omit context limits. +The output leads with the JSON, then prints the destination path, the merge warning, Pi-specific +pre-launch guidance, the total model count, and how many rows omit context limits. ```json { diff --git a/docs-site/src/content/docs/ja/guides/pi.md b/docs-site/src/content/docs/ja/guides/pi.md index e6d4fece17..b7b1dbd3d3 100644 --- a/docs-site/src/content/docs/ja/guides/pi.md +++ b/docs-site/src/content/docs/ja/guides/pi.md @@ -14,7 +14,7 @@ ocx start ocx export --client pi ``` -出力は JSON で始まり、宛先パス、マージ警告、モデルの総数、およびコンテキスト制限を省略した行数を出力します。 +出力は JSON で始まり、宛先パス、マージ警告、Pi 固有の起動前ガイダンス、モデルの総数、およびコンテキスト制限を省略した行数を出力します。 ```json { diff --git a/docs-site/src/content/docs/ko/guides/pi.md b/docs-site/src/content/docs/ko/guides/pi.md index d7de2e4fd9..8afe4f17ab 100644 --- a/docs-site/src/content/docs/ko/guides/pi.md +++ b/docs-site/src/content/docs/ko/guides/pi.md @@ -17,8 +17,8 @@ ocx start ocx export --client pi ``` -출력은 JSON으로 시작하고, 이어서 대상 경로, 병합 경고, 전체 모델 수, 그리고 context -limit을 생략한 row 수를 보여줍니다. +출력은 JSON으로 시작하고, 이어서 대상 경로, 병합 경고, Pi 전용 실행 전 안내, 전체 모델 +수, 그리고 context limit을 생략한 row 수를 보여줍니다. ```json { diff --git a/docs-site/src/content/docs/ru/guides/pi.md b/docs-site/src/content/docs/ru/guides/pi.md index 4f20eb6753..6ed6cc9c2f 100644 --- a/docs-site/src/content/docs/ru/guides/pi.md +++ b/docs-site/src/content/docs/ru/guides/pi.md @@ -17,8 +17,8 @@ ocx start ocx export --client pi ``` -Сначала выводится JSON, затем путь назначения, предупреждение о merge, общее число моделей и -число строк без контекстных лимитов. +Сначала выводится JSON, затем путь назначения, предупреждение о merge, специальные инструкции +для Pi перед запуском, общее число моделей и число строк без контекстных лимитов. ```json { diff --git a/docs-site/src/content/docs/zh-cn/guides/pi.md b/docs-site/src/content/docs/zh-cn/guides/pi.md index 2ee51ac7c9..d5e08ce836 100644 --- a/docs-site/src/content/docs/zh-cn/guides/pi.md +++ b/docs-site/src/content/docs/zh-cn/guides/pi.md @@ -14,7 +14,7 @@ ocx start ocx export --client pi ``` -输出会先显示 JSON,然后打印目标路径、合并警告、模型总数,以及省略上下文限制的行数。 +输出会先显示 JSON,然后打印目标路径、合并警告、Pi 专用的启动前提示、模型总数,以及省略上下文限制的行数。 ```json { From 712816cd45c04e0851d92673032711523654e891 Mon Sep 17 00:00:00 2001 From: n3wr1ch <40690535+n3wr1ch@users.noreply.github.com> Date: Thu, 6 Aug 2026 21:21:20 +0900 Subject: [PATCH 13/16] fix(export): align file-client model contracts --- .../apikeys-workspace/ClientConfigDialog.tsx | 4 +- gui/src/i18n/de.ts | 1 + gui/src/i18n/en.ts | 1 + gui/src/i18n/ja.ts | 1 + gui/src/i18n/ko.ts | 1 + gui/src/i18n/ru.ts | 1 + gui/src/i18n/zh.ts | 1 + gui/tests/client-config-panel.test.tsx | 4 ++ src/cli/export-command.ts | 10 +---- src/cli/opencode.ts | 9 ++-- src/server/management/model-rows.ts | 15 +++---- tests/cli-export-command.test.ts | 42 +++++++++++++++++++ tests/management-client-config-route.test.ts | 11 ++++- 13 files changed, 79 insertions(+), 22 deletions(-) diff --git a/gui/src/components/apikeys-workspace/ClientConfigDialog.tsx b/gui/src/components/apikeys-workspace/ClientConfigDialog.tsx index 1a0026b21d..689266c392 100644 --- a/gui/src/components/apikeys-workspace/ClientConfigDialog.tsx +++ b/gui/src/components/apikeys-workspace/ClientConfigDialog.tsx @@ -100,7 +100,9 @@ export default function ClientConfigDialog({ {/* The old
is gone — this is the one place the answer lives now, so it is a labelled paragraph rather than a fold. */}

{t("api.clientConfig.whereDisclosure")}

-

{t("api.clientConfig.whereBody")}

+

+ {t(envelope.apiKeyEnv === "" ? "api.clientConfig.whereBodyLoopback" : "api.clientConfig.whereBody")} +