diff --git a/devlog/_plan/260806_omp_integration/evidence/omp-dashboard.png b/devlog/_plan/260806_omp_integration/evidence/omp-dashboard.png new file mode 100644 index 0000000000..b132a5334b Binary files /dev/null and b/devlog/_plan/260806_omp_integration/evidence/omp-dashboard.png differ diff --git a/devlog/_plan/260806_omp_integration/evidence/omp-live-prompt.png b/devlog/_plan/260806_omp_integration/evidence/omp-live-prompt.png new file mode 100644 index 0000000000..b131f7d9f3 Binary files /dev/null and b/devlog/_plan/260806_omp_integration/evidence/omp-live-prompt.png differ diff --git a/devlog/_plan/260806_omp_integration/evidence/omp-model-catalog.png b/devlog/_plan/260806_omp_integration/evidence/omp-model-catalog.png new file mode 100644 index 0000000000..4292a63ce2 Binary files /dev/null and b/devlog/_plan/260806_omp_integration/evidence/omp-model-catalog.png differ diff --git a/docs-site/src/content/docs/guides/integrations.md b/docs-site/src/content/docs/guides/integrations.md index c917fd273a..73214bb379 100644 --- a/docs-site/src/content/docs/guides/integrations.md +++ b/docs-site/src/content/docs/guides/integrations.md @@ -1,23 +1,28 @@ --- title: Integrations -description: Connect opencodex to OpenCode, Pi, Hermes, OpenClaw, Kimi Code and Gajae Code from the dashboard — one switch per client, with a backup taken before every write. +description: Connect opencodex to OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code and Gajae Code from the dashboard — one switch per client, with a backup taken before every write. --- The **Integrations** tab writes opencodex's provider block into a client's own config -file, and removes it again. Six clients work this way, each with a switch: +file, and removes it again. Seven 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 | +| OMP | `~/.omp/agent/models.yml` | YAML | after restarting OMP | `opencodex-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 | | Gajae Code | `~/.gjc/agent/models.yml` | YAML | new sessions, or when you open `/model` |`OPENCODEX_GAJAE_API_KEY` | -Paths honor each client's own environment override where it has one, so a relocated -`HERMES_HOME`, `KIMI_CODE_HOME` or `XDG_CONFIG_HOME` is followed rather than guessed -at. The table lists each client's default; an override always wins. +Paths honor each client's own environment override where it has one. For OMP, +`OMP_PROFILE` wins over `PI_PROFILE` by presence, even when explicitly empty. A named profile +uses `PI_CONFIG_DIR` as a directory name relative to the user's home and ignores `PI_CODING_AGENT_DIR`; without a named profile, +`PI_CODING_AGENT_DIR` wins. OMP supports provider-level headers, but this initial integration +is deliberately loopback-only; remote `x-opencodex-api-key` wiring is deferred. Relocated +`HERMES_HOME`, `KIMI_CODE_HOME`, and `XDG_CONFIG_HOME` paths are likewise followed rather than +guessed at. The table lists each client's default. OpenClaw has several, and they do different jobs. `OPENCLAW_CONFIG_PATH` selects the file; `OPENCLAW_STATE_DIR`, `OPENCLAW_PROFILE` and `OPENCLAW_HOME` select the state @@ -75,12 +80,15 @@ changed value and calling it success. You will see the file named and nothing on disk will have moved. Editing that file by hand still works; it is only our automatic rewrite that declines. -**Pi, Kimi Code and Gajae Code only work against a loopback bind.** None of their config -schemas has a place for the `x-opencodex-api-key` header that a non-loopback bind -requires, so a generated config would simply be rejected — and writing one by hand does -not help, because there is nowhere in the file to put the header either. Reaching a -remote opencodex from these clients is not supported directly; give them loopback access -instead, through an SSH tunnel or a local forwarder that adds the header. +**Pi, Kimi Code and Gajae Code only work against a loopback bind.** Their config schemas +have no place for the `x-opencodex-api-key` header that a non-loopback bind requires, so a +generated config would simply be rejected. Give them loopback access instead, through an +SSH tunnel or a local forwarder that adds the header. + +**The generated OMP integration is also deliberately loopback-only.** OMP does support +provider-level headers, but this initial integration does not emit remote +`x-opencodex-api-key` credential wiring. Manual remote OMP configuration is outside the +managed integration for now. **Kimi Code cannot hold an environment reference,** so its config carries an `opencodex-loopback` placeholder rather than a key. No real credential is ever written 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..203644d96a 100644 --- a/docs-site/src/content/docs/ja/reference/cli/agents.md +++ b/docs-site/src/content/docs/ja/reference/cli/agents.md @@ -125,39 +125,45 @@ Grok Build モデル フェンスを管理および適用します。 ## クライアント設定のエクスポート -### `ocx export --client ` +### `ocx export --client ` -実行中のプロキシに接続されているクライアント設定を出力します。 opencode と [円周率](/guides/pi/) は環境変数ではなく独自の JSON 設定からプロバイダーを読み取るため、このコマンドは `opencodex` プロバイダー ブロック (ベース URL、モデル リスト、クライアントの環境参照) をシリアル化し、そのファイルにマージできるようにします。 +実行中のプロキシに接続するクライアント設定を出力します。このコマンドは、ベース URL、モデル一覧、およびクライアントに応じた認証情報参照または `opencodex-loopback` プレースホルダーを含む `opencodex` プロバイダーブロックを、選択したクライアントのネイティブ形式でシリアル化します。 プロキシが実行されている必要があります。このコマンドはライブ ポートを解決し、`/api/models` を読み取り、Codex が現在認識できるモデルのみを出力します。 |旗 |アクション | | --- | --- | -| `--client ` |必須。クライアント言語を選択します: opencode のキー付き `provider` オブジェクトまたは Pi の `providers` 配列。 | +| `--client ` |必須。クライアントの設定形式を選択します。 | | `--json` |構成 JSON のみを標準出力に出力するため、リダイレクトはバイト正確な出力をキャプチャします。 `--out` 書き込みメモを含むすべての診断は stderr に送られます。 | | `--out ` |設定を `` に書き込みます。既存のファイルの置き換えを拒否します。 | | `--force` | `--out` が既存のファイルを置き換えることを許可します。 | ```bash ocx export --client opencode # config plus destination, merge warning, and counts -ocx export --client pi --json > pi-models.json # byte-exact JSON for a pipe or a diff +ocx export --client pi --json > pi-models.json # JSON document for a pipe or a diff +ocx export --client omp --out ./omp-models.yml # native OMP YAML ocx export --client opencode --out ~/opencodex-opencode.json ``` -`--json` がない場合、JSON が先頭に続き、正規の宛先パス、マージ警告、環境エクスポート行、およびコンテキスト制限を省略する行数を含むモデル数が続きます (クライアントはこれらに対して独自のデフォルトを適用します)。 +`--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` が入ります | +| `omp` | `~/.omp/agent/models.yml` (デフォルト。空の場合も `OMP_PROFILE` が `PI_PROFILE` より優先されます) | `omp-models.yaml` | なし - リテラル `opencodex-loopback` | +| `hermes` | `~/.hermes/config.yaml` | `hermes-config.yaml` | `OPENCODEX_HERMES_API_KEY` | +| `openclaw` | `~/.openclaw/openclaw.json` | `openclaw.json5` | `OPENCODEX_OPENCLAW_API_KEY` | +| `kimi` | `~/.kimi-code/config.toml` | `kimi-config.toml` | なし - loopback placeholder | +| `gajae` | `~/.gjc/agent/models.yml` | `gajae-models.yaml` | `OPENCODEX_GAJAE_API_KEY` | -2 つの環境変数名は異なり、各クライアントは独自の名前のみを補間します。 opencode は `{env:OPENCODEX_OPENCODE_API_KEY}` を読み取ります。 Pi は `$OPENCODEX_API_KEY` を読み取ります。 +opencode は `{env:OPENCODEX_OPENCODE_API_KEY}` を補間します。opencodex が生成する Pi のエクスポートには環境変数が不要で、リテラルのプレースホルダー `opencodex-loopback` が入ります。この値は必須です。Pi はモデル リストを構築する際に `apiKey` を解決し、既存の設定に未設定の環境変数参照がある場合はプロバイダー全体を隠すためです。ループバックでは、生成されたプレースホルダーをプロキシが検査することはありません。 :::caution[マージし、決して置き換えないでください] `ocx export` は実際のクライアント設定を書き込むことはありません。宛先は手動でマージできるように出力されます。`--out` は、`--force` なしで既存のファイルを上書きすることを拒否します。これは、設定を置き換えると、その中にすでに含まれている他のプロバイダー、エージェント、および MCP エントリが破壊されるためです。 ::: -キーはシリアル化されません。設定にはクライアントの環境参照のみが含まれるため、シークレットは環境内に残ります。ループバック プロキシ (`127.0.0.1`、デフォルト) にはアドミッション キーはまったく必要ありません。参照は単に使用されないだけです。プロキシがループバックを超えてバインドする場合にのみ変数を設定します。アドミッションキーの発行方法については、[リモートアクセス](/reference/configuration/#remote-access) を参照してください。上流プロバイダー自体のキーは完全に別のものであり、[プロバイダー](/guides/providers/) ごとに構成されます。 +キーはシリアル化されません。opencode、Hermes、OpenClaw の設定には環境参照のみが含まれるためシークレットは環境内に残り、Pi、OMP、Kimi の設定には認証情報ではなくループバック用プレースホルダーが入ります。ループバック プロキシ (`127.0.0.1`、デフォルト) にはアドミッション キーはまったく必要ありません。プロキシがループバックを超えてバインドする場合は、対応する `OPENCODEX_OPENCODE_API_KEY`、`OPENCODEX_HERMES_API_KEY`、または `OPENCODEX_OPENCLAW_API_KEY` を設定します。生成される Pi、OMP、Kimi、Gajae 統合はループバック専用です。Gajae の設定には `OPENCODEX_GAJAE_API_KEY` への環境参照が含まれますが、厳格なスキーマには header フィールドがないため、この統合は loopback-only のままです。アドミッションキーの発行方法については、[リモートアクセス](/reference/configuration/#remote-access) を参照してください。上流プロバイダー自体のキーは完全に別のものであり、[プロバイダー](/guides/providers/) ごとに構成されます。 同じペイロードが `GET /api/client-config` によって提供され、ダッシュボードの [API] タブにレンダリングされるため、CLI、API、および GUI は同じバイトを使用します。 diff --git a/docs-site/src/content/docs/ja/reference/management-api.md b/docs-site/src/content/docs/ja/reference/management-api.md index a2a8c1ddac..7e903d8749 100644 --- a/docs-site/src/content/docs/ja/reference/management-api.md +++ b/docs-site/src/content/docs/ja/reference/management-api.md @@ -130,7 +130,7 @@ Authorization: Bearer | --- | --- | --- | | `GET /api/catalog` |インストールされている Codex カタログ ドキュメントを返します | 404 カタログが見つかりません | | `GET /api/models` |ダッシュボード/CLI モデルの行を返す |収集が飽和したときの `catalog_busy` | -| `GET /api/client-config?client=...` |読み取り専用の OpenCode または Pi クライアント構成ドキュメントを作成する | 400 クライアントがサポートされていません。 503 カタログは利用できません | +| `GET /api/client-config?client=...` |サポートされているファイル連携の読み取り専用クライアント設定を作成する | 400 クライアントがサポートされていません。 503 カタログは利用できません | | `PUT /api/disabled-models` |共有の無効モデル リストを置き換える | 400 無効な JSON | | `PUT /api/model-visibility` |プロバイダーレベルまたはモデルレベルの可視性をアトミックに変更 | 400 プロバイダー、スコープ、ターゲット、または本文が無効です。 | `GET, POST /api/custom-models` |カスタム モデルをリストするか追加する | 400 個の無効なフィールド。 404 プロバイダーがありません。 409 複製モデル | 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..5a692cad10 100644 --- a/docs-site/src/content/docs/ko/reference/cli/agents.md +++ b/docs-site/src/content/docs/ko/reference/cli/agents.md @@ -131,39 +131,45 @@ Grok Build model fence를 관리하고 적용합니다. ## 클라이언트 설정 내보내기 -### `ocx export --client ` +### `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를 출력합니다. 이 명령은 base URL, model list, 그리고 client에 따라 credential reference 또는 `opencodex-loopback` placeholder를 포함한 `opencodex` provider block을 선택한 client의 네이티브 형식으로 직렬화합니다. 프록시는 실행 중이어야 합니다. 이 명령은 실제 포트를 확인하고, `/api/models`를 읽고, 현재 Codex가 볼 수 있는 model만 내보냅니다. | 플래그 | 동작 | | --- | --- | -| `--client ` | 필수입니다. 클라이언트 방식을 선택합니다. opencode의 keyed `provider` object 또는 Pi의 `providers` array입니다. | +| `--client ` | 필수입니다. 클라이언트 설정 형식을 선택합니다. | | `--json` | config JSON만 stdout에 출력하므로, redirect가 byte-exact 출력을 캡처합니다. `--out` write note를 포함한 모든 진단 메시지는 stderr로 갑니다. | | `--out ` | config를 ``에 씁니다. 기존 파일이 있으면 덮어쓰지 않습니다. | | `--force` | `--out`이 기존 파일을 덮어쓰도록 허용합니다. | ```bash ocx export --client opencode # config plus destination, merge warning, and counts -ocx export --client pi --json > pi-models.json # byte-exact JSON for a pipe or a diff +ocx export --client pi --json > pi-models.json # JSON document for a pipe or a diff +ocx export --client omp --out ./omp-models.yml # native OMP YAML ocx export --client opencode --out ~/opencodex-opencode.json ``` -`--json`이 없으면 JSON이 먼저 나오고, 그다음 표준 대상 경로, merge 경고, env export 줄, 그리고 context limit을 생략한 row 수를 포함한 model count가 이어집니다(이 경우 client는 자체 기본값을 적용합니다). +`--json`이 없으면 선택한 client의 네이티브 형식으로 생성된 config가 먼저 나오고, 그다음 표준 대상 경로, 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`이 들어갑니다 | +| `omp` | `~/.omp/agent/models.yml` (기본값. 빈 값이어도 `OMP_PROFILE`이 `PI_PROFILE`보다 우선합니다) | `omp-models.yaml` | 없음 - 리터럴 `opencodex-loopback` | +| `hermes` | `~/.hermes/config.yaml` | `hermes-config.yaml` | `OPENCODEX_HERMES_API_KEY` | +| `openclaw` | `~/.openclaw/openclaw.json` | `openclaw.json5` | `OPENCODEX_OPENCLAW_API_KEY` | +| `kimi` | `~/.kimi-code/config.toml` | `kimi-config.toml` | 없음 - loopback placeholder | +| `gajae` | `~/.gjc/agent/models.yml` | `gajae-models.yaml` | `OPENCODEX_GAJAE_API_KEY` | -두 환경 변수 이름은 서로 다르며, 각 client는 자기 것만 보간합니다. opencode는 `{env:OPENCODEX_OPENCODE_API_KEY}`를 읽고, Pi는 `$OPENCODEX_API_KEY`를 읽습니다. +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가 사라지기 때문입니다. ::: -어떤 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, Hermes, OpenClaw config에는 env reference만 들어가므로 secret은 환경 변수에 남고, Pi, OMP, Kimi config에는 인증 정보가 아니라 loopback placeholder가 들어갑니다. loopback proxy(`127.0.0.1`, 기본값)는 admission key가 전혀 필요하지 않습니다. proxy가 loopback을 넘어 바인딩할 때는 해당하는 `OPENCODEX_OPENCODE_API_KEY`, `OPENCODEX_HERMES_API_KEY`, `OPENCODEX_OPENCLAW_API_KEY`를 설정하십시오. 생성되는 Pi, OMP, Kimi, Gajae 통합은 loopback 전용입니다. Gajae config에는 `OPENCODEX_GAJAE_API_KEY`에 대한 env reference가 들어가지만 엄격한 schema에 header 필드가 없어 이 통합은 loopback-only로 유지됩니다. 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/ko/reference/management-api.md b/docs-site/src/content/docs/ko/reference/management-api.md index a0bcc78e1c..da0b170854 100644 --- a/docs-site/src/content/docs/ko/reference/management-api.md +++ b/docs-site/src/content/docs/ko/reference/management-api.md @@ -130,7 +130,7 @@ Authorization: Bearer | --- | --- | --- | | `GET /api/catalog` | 설치된 Codex catalog 문서를 반환합니다 | 404 catalog 없음 | | `GET /api/models` | 대시보드/CLI model 행을 반환합니다 | 수집이 포화 상태이면 `catalog_busy` | -| `GET /api/client-config?client=...` | 읽기 전용 OpenCode 또는 Pi client-config 문서를 만듭니다 | 400 지원되지 않는 client; 503 catalog 사용 불가 | +| `GET /api/client-config?client=...` | 지원되는 파일 연동의 읽기 전용 client config를 만듭니다 | 400 지원되지 않는 client; 503 catalog 사용 불가 | | `PUT /api/disabled-models` | 공유 disabled-model 목록을 교체합니다 | 400 잘못된 JSON | | `PUT /api/model-visibility` | provider 또는 model 수준의 visibility를 원자적으로 변경합니다 | 400 잘못된 provider, scope, target, 또는 본문 | | `GET, POST /api/custom-models` | custom model을 나열하거나 하나를 추가합니다 | 400 잘못된 필드; 404 provider 없음; 409 중복 model | diff --git a/docs-site/src/content/docs/reference/cli/agents.md b/docs-site/src/content/docs/reference/cli/agents.md index 4944a483d2..da33134f3b 100644 --- a/docs-site/src/content/docs/reference/cli/agents.md +++ b/docs-site/src/content/docs/reference/cli/agents.md @@ -146,40 +146,49 @@ Manage and apply the Grok Build model fence. ## Client config export -### `ocx export --client ` +### `ocx export --client ` -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. +Print a client config wired to the running proxy. The command serializes the +`opencodex` provider block — base URL, model list, and the client's credential +reference or loopback placeholder — in the selected client's native format. The proxy must be running; the command resolves its live port, reads `/api/models`, and emits only models Codex can currently see. | Flag | Action | | --- | --- | -| `--client ` | Required. Selects the client dialect: opencode's keyed `provider` object or Pi's `providers` array. | -| `--json` | Print only the config JSON on stdout, so a redirect captures byte-exact output. Every diagnostic, including the `--out` write note, goes to stderr. | -| `--out ` | Write the config to ``. Refuses to replace an existing file. | +| `--client ` | Required. Selects the client config dialect. | +| `--json` | Print the generated document as JSON on stdout for scripts. This is JSON even when the selected client's native format is YAML, TOML, or JSON5. | +| `--out ` | Write the client's native config format to ``. Refuses to replace an existing file. | | `--force` | Allow `--out` to replace an existing file. | ```bash ocx export --client opencode # config plus destination, merge warning, and counts -ocx export --client pi --json > pi-models.json # byte-exact JSON for a pipe or a diff +ocx export --client pi --json > pi-models.json # JSON document for a pipe or a diff +ocx export --client omp --out ./omp-models.yml # native OMP YAML 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). +Without `--json` the generated config leads, then the canonical destination path, the merge warning, the env +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` | - -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`. +| `pi` | `~/.pi/agent/models.json` | `pi-models.json` | none — the block carries the literal `opencodex-loopback` | +| `omp` | `~/.omp/agent/models.yml` (`OMP_PROFILE` wins over `PI_PROFILE`, even when empty; named profiles use the home-relative `PI_CONFIG_DIR` directory name and ignore `PI_CODING_AGENT_DIR`, while the default profile lets `PI_CODING_AGENT_DIR` win) | `omp-models.yaml` | none — loopback placeholder | +| `hermes` | `~/.hermes/config.yaml` | `hermes-config.yaml` | `OPENCODEX_HERMES_API_KEY` | +| `openclaw` | `~/.openclaw/openclaw.json` | `openclaw.json5` | `OPENCODEX_OPENCLAW_API_KEY` | +| `kimi` | `~/.kimi-code/config.toml` | `kimi-config.toml` | none — loopback placeholder | +| `gajae` | `~/.gjc/agent/models.yml` | `gajae-models.yaml` | `OPENCODEX_GAJAE_API_KEY` | + +opencode interpolates `{env:OPENCODEX_OPENCODE_API_KEY}`. The generated Pi and OMP exports do +not require an environment variable: each carries the literal `opencodex-loopback` placeholder. +This is load-bearing because both clients resolve `apiKey` while building their model lists and +hide the whole provider when an existing config contains an unset env reference. The proxy never +checks the generated placeholder on loopback. OMP supports provider-level headers, but this initial +integration deliberately remains loopback-only; remote `x-opencodex-api-key` wiring is deferred. :::caution[Merge, never replace] `ocx export` never writes your real client config. The destination is printed for you to merge by @@ -187,9 +196,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. Configs carry either a documented environment reference or a +non-secret loopback placeholder. A loopback proxy (`127.0.0.1`, the default) requires no +admission key at all. Set a referenced variable only when the client schema supports it and +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/reference/management-api.md b/docs-site/src/content/docs/reference/management-api.md index e6a7e385e8..dde75b2703 100644 --- a/docs-site/src/content/docs/reference/management-api.md +++ b/docs-site/src/content/docs/reference/management-api.md @@ -146,7 +146,7 @@ first and submit the returned digest. Prefer quarantine when recovery may be nee | --- | --- | --- | | `GET /api/catalog` | Return the installed Codex catalog document | 404 catalog not found | | `GET /api/models` | Return the dashboard/CLI model rows | `catalog_busy` when gathering is saturated | -| `GET /api/client-config?client=...` | Build a read-only OpenCode or Pi client-config document | 400 unsupported client; 503 catalog unavailable | +| `GET /api/client-config?client=...` | Build a read-only client config for any supported file integration | 400 unsupported client; 503 catalog unavailable | | `PUT /api/disabled-models` | Replace the shared disabled-model list | 400 invalid JSON | | `PUT /api/model-visibility` | Atomically change provider- or model-level visibility | 400 invalid provider, scope, target, or body | | `GET, POST /api/custom-models` | List custom models or add one | 400 invalid fields; 404 provider missing; 409 duplicate model | 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..91333443c1 100644 --- a/docs-site/src/content/docs/ru/reference/cli/agents.md +++ b/docs-site/src/content/docs/ru/reference/cli/agents.md @@ -152,40 +152,49 @@ override, но файлы на диске никогда не меняются. ## Экспорт client config -### `ocx export --client ` +### `ocx export --client ` -Печатает client config, направленный на работающий прокси. opencode и [Pi](/guides/pi/) читают -провайдеров из собственных JSON-конфигов, а не из переменных окружения, поэтому команда -сериализует для вас блок провайдера `opencodex` — base URL, список моделей и env-reference -конкретного клиента. +Печатает client config, направленный на работающий прокси. Команда сериализует блок +провайдера `opencodex` в нативном формате выбранного клиента: base URL, список моделей и, +в зависимости от клиента, credential reference либо заглушку `opencodex-loopback`. Прокси должен быть запущен; команда определяет его живой порт, читает `/api/models` и выводит только те модели, которые сейчас видит Codex. | Флаг | Действие | | --- | --- | -| `--client ` | Обязателен. Выбирает клиентский диалект: keyed-объект `provider` у opencode или массив `providers` у Pi. | +| `--client ` | Обязателен. Выбирает формат конфигурации клиента. | | `--json` | Печатать только JSON-конфиг в stdout, чтобы redirect сохранял побайтно точный вывод. Вся диагностика, включая заметку о записи через `--out`, идёт в stderr. | | `--out ` | Записать конфиг в ``. Перезаписывать существующий файл не позволит. | | `--force` | Разрешить `--out` заменить существующий файл. | ```bash ocx export --client opencode # config plus destination, merge warning, and counts -ocx export --client pi --json > pi-models.json # byte-exact JSON for a pipe or a diff +ocx export --client pi --json > pi-models.json # JSON document for a pipe or a diff +ocx export --client omp --out ./omp-models.yml # native OMP YAML ocx export --client opencode --out ~/opencodex-opencode.json ``` -Без `--json` сначала идёт JSON, затем канонический путь назначения, предупреждение о merge, строка -экспорта переменной окружения и количество моделей с указанием, сколько строк не имеют context -limit'а (для них клиент применяет собственные default'ы). +Без `--json` сначала идёт сгенерированная конфигурация в нативном формате выбранного клиента, +затем канонический путь назначения, предупреждение о merge, клиентская подсказка перед запуском +и количество моделей с указанием, сколько строк не имеют context limit'а (для них клиент применяет +собственные default'ы). | Клиент | Канонический путь | Имя скачиваемого файла | Переменная окружения | | --- | --- | --- | --- | | `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` | - -Имена этих двух env-переменных различаются, и каждый клиент интерполирует только свою. opencode -читает `{env:OPENCODEX_OPENCODE_API_KEY}`; Pi читает `$OPENCODEX_API_KEY`. +| `pi` | `~/.pi/agent/models.json` | `pi-models.json` | нет — блок несёт литерал `opencodex-loopback` | +| `omp` | `~/.omp/agent/models.yml` (по умолчанию; `OMP_PROFILE` имеет приоритет над `PI_PROFILE`, даже если пуст) | `omp-models.yaml` | нет — литерал `opencodex-loopback` | +| `hermes` | `~/.hermes/config.yaml` | `hermes-config.yaml` | `OPENCODEX_HERMES_API_KEY` | +| `openclaw` | `~/.openclaw/openclaw.json` | `openclaw.json5` | `OPENCODEX_OPENCLAW_API_KEY` | +| `kimi` | `~/.kimi-code/config.toml` | `kimi-config.toml` | нет — loopback placeholder | +| `gajae` | `~/.gjc/agent/models.yml` | `gajae-models.yaml` | `OPENCODEX_GAJAE_API_KEY` | + +opencode интерполирует `{env:OPENCODEX_OPENCODE_API_KEY}`. Сгенерированный opencodex экспорт для +Pi не требует переменной окружения и несёт литеральную заглушку `opencodex-loopback`. Это значение +обязательно: Pi разрешает `apiKey`, когда строит список моделей, и прячет провайдера целиком, если +существующий конфиг содержит ссылку на незаданную переменную окружения. На loopback прокси не +проверяет сгенерированную заглушку. :::caution[Сливать, а не заменять] `ocx export` никогда не пишет в ваш реальный клиентский конфиг. Путь назначения лишь @@ -194,10 +203,14 @@ limit'а (для них клиент применяет собственные d MCP-записи. ::: -Никакой ключ никогда не сериализуется. Конфиг несёт только env-reference клиента, так что секрет -остаётся в вашем окружении. Loopback-прокси (`127.0.0.1`, по умолчанию) вообще не требует -admission key — ссылка просто остаётся неиспользованной. Задавайте переменную только если прокси -слушает не на loopback; как выдаются admission key, описано в +Никакой ключ никогда не сериализуется. Конфиги opencode, Hermes и OpenClaw несут только +env-reference, так что секрет остаётся в вашем окружении, а конфиги Pi, OMP и Kimi несут +loopback-заглушку вместо учётных данных. Loopback-прокси (`127.0.0.1`, по умолчанию) вообще не +требует admission key. Если прокси слушает не на loopback, задайте соответствующую переменную +`OPENCODEX_OPENCODE_API_KEY`, `OPENCODEX_HERMES_API_KEY` или `OPENCODEX_OPENCLAW_API_KEY`. +Сгенерированные интеграции Pi, OMP, Kimi и Gajae работают только через loopback. Gajae несёт +env-reference на `OPENCODEX_GAJAE_API_KEY`, но строгая схема не имеет поля header, поэтому +интеграция остаётся loopback-only. Как выдаются admission key, описано в [Удалённом доступе](/reference/configuration/#remote-access). Ключи upstream-провайдеров — это совсем отдельная история и настраиваются в [Провайдерах](/guides/providers/). diff --git a/docs-site/src/content/docs/ru/reference/management-api.md b/docs-site/src/content/docs/ru/reference/management-api.md index bac4b5de46..f6e8c74ae1 100644 --- a/docs-site/src/content/docs/ru/reference/management-api.md +++ b/docs-site/src/content/docs/ru/reference/management-api.md @@ -148,7 +148,7 @@ Endpoint'ы storage cleanup могут перемещать или навсег | --- | --- | --- | | `GET /api/catalog` | Вернуть установленный документ каталога Codex | 404 catalog not found | | `GET /api/models` | Вернуть model-row'ы для дашборда и CLI | `catalog_busy`, когда сборка перегружена | -| `GET /api/client-config?client=...` | Собрать read-only client-config document для OpenCode или Pi | 400 unsupported client; 503 catalog unavailable | +| `GET /api/client-config?client=...` | Собрать read-only client config для любой поддерживаемой файловой интеграции | 400 unsupported client; 503 catalog unavailable | | `PUT /api/disabled-models` | Полностью заменить общий список disabled-models | 400 invalid JSON | | `PUT /api/model-visibility` | Атомарно изменить видимость на уровне провайдера или модели | 400 invalid provider, scope, target or body | | `GET, POST /api/custom-models` | Показать список custom-моделей или добавить одну | 400 invalid fields; 404 provider missing; 409 duplicate model | 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..b0d12b470b 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 @@ -126,39 +126,45 @@ ocx claude desktop import [--apply] Validate and import JSON ## Client config export -### `ocx export --client ` +### `ocx export --client ` -输出连接到正在运行代理的客户端配置。opencode 和 [Pi](/guides/pi/) 不是从环境变量,而是从各自的 JSON 配置中读取 providers,因此此命令会序列化 `opencodex` provider 块——基础 URL、模型列表以及客户端的环境引用——供你合并进那个文件。 +输出连接到正在运行代理的客户端配置。此命令会以所选客户端的原生格式序列化 `opencodex` provider 块,其中包含基础 URL、模型列表,以及该客户端适用的凭据引用或 `opencodex-loopback` 占位值。 代理必须正在运行;该命令会解析其当前端口,读取 `/api/models`,并且只输出 Codex 当前可见的模型。 | 标志 | 动作 | | --- | --- | -| `--client ` | 必需。选择客户端方言:opencode 的带键 `provider` 对象或 Pi 的 `providers` 数组。 | +| `--client ` | 必需。选择客户端配置格式。 | | `--json` | 仅在 stdout 打印配置 JSON,这样重定向即可捕获字节级精确输出。包括 `--out` 写入提示在内的所有诊断信息都会输出到 stderr。 | | `--out ` | 将配置写入 ``。拒绝替换已存在的文件。 | | `--force` | 允许 `--out` 替换已存在的文件。 | ```bash ocx export --client opencode # config plus destination, merge warning, and counts -ocx export --client pi --json > pi-models.json # byte-exact JSON for a pipe or a diff +ocx export --client pi --json > pi-models.json # JSON document for a pipe or a diff +ocx export --client omp --out ./omp-models.yml # native OMP YAML ocx export --client opencode --out ~/opencodex-opencode.json ``` -不使用 `--json` 时,JSON 会先输出,随后是规范目标路径、合并警告、环境变量导出行,以及一个模型计数,并标明有多少行省略了上下文限制(客户端会对这些项应用自己的默认值)。 +不使用 `--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` | +| `omp` | `~/.omp/agent/models.yml`(默认路径;即使为空,`OMP_PROFILE` 也优先于 `PI_PROFILE`) | `omp-models.yaml` | 无 - 字面值 `opencodex-loopback` | +| `hermes` | `~/.hermes/config.yaml` | `hermes-config.yaml` | `OPENCODEX_HERMES_API_KEY` | +| `openclaw` | `~/.openclaw/openclaw.json` | `openclaw.json5` | `OPENCODEX_OPENCLAW_API_KEY` | +| `kimi` | `~/.kimi-code/config.toml` | `kimi-config.toml` | 无 - loopback placeholder | +| `gajae` | `~/.gjc/agent/models.yml` | `gajae-models.yaml` | `OPENCODEX_GAJAE_API_KEY` | -这两个环境变量名称不同,而且每个客户端只会插入自己的那个。opencode 读取 `{env:OPENCODEX_OPENCODE_API_KEY}`;Pi 读取 `$OPENCODEX_API_KEY`。 +opencode 会插值 `{env:OPENCODEX_OPENCODE_API_KEY}`。opencodex 生成的 Pi 导出不需要环境变量,而是携带字面占位值 `opencodex-loopback`。这个值是必需的:Pi 在构建模型列表时会解析 `apiKey`,如果已有配置包含未设置的环境变量引用,它就会隐藏整个 provider。回环上的代理从不校验生成的占位值。 :::caution[合并,不要替换] `ocx export` 从不写入你的真实客户端配置。该命令只会打印目标路径供你手动合并,而 `--out` 在没有 `--force` 的情况下拒绝覆盖已有文件,因为替换配置会破坏其中已有的其他 providers、agents 和 MCP 条目。 ::: -任何密钥都不会被序列化。配置里只包含客户端的环境引用,因此密钥仍保留在你的环境中。环回代理(`127.0.0.1`,默认值)根本不需要准入密钥——该引用只是不会被使用。只有当代理绑定到环回地址之外时才设置该变量;关于准入密钥如何签发,请参见 [远程访问](/reference/configuration/#remote-access)。上游 providers 自身的密钥则完全是另一回事,需要按 [Providers](/guides/providers/) 单独配置。 +任何密钥都不会被序列化。opencode、Hermes 和 OpenClaw 配置里只包含环境引用,因此密钥仍保留在你的环境中;Pi、OMP 和 Kimi 配置里携带的是环回占位值而不是任何凭据。环回代理(`127.0.0.1`,默认值)根本不需要准入密钥。当代理绑定到环回地址之外时,请设置对应的 `OPENCODEX_OPENCODE_API_KEY`、`OPENCODEX_HERMES_API_KEY` 或 `OPENCODEX_OPENCLAW_API_KEY`。生成的 Pi、OMP、Kimi 和 Gajae 集成仅支持环回。Gajae 配置里包含对 `OPENCODEX_GAJAE_API_KEY` 的环境引用,但其严格的 schema 没有 header 字段,因此该集成仍保持 loopback-only。关于准入密钥如何签发,请参见 [远程访问](/reference/configuration/#remote-access)。上游 providers 自身的密钥则完全是另一回事,需要按 [Providers](/guides/providers/) 单独配置。 同一份负载会通过 `GET /api/client-config` 提供,并在仪表盘的 API 选项卡中渲染,因此 CLI、API 和 GUI 使用的是同一字节内容。 diff --git a/docs-site/src/content/docs/zh-cn/reference/management-api.md b/docs-site/src/content/docs/zh-cn/reference/management-api.md index eb28876408..4e611356e4 100644 --- a/docs-site/src/content/docs/zh-cn/reference/management-api.md +++ b/docs-site/src/content/docs/zh-cn/reference/management-api.md @@ -130,7 +130,7 @@ Authorization: Bearer | --- | --- | --- | | `GET /api/catalog` | 返回已安装的 Codex 目录文档 | 404 未找到目录 | | `GET /api/models` | 返回仪表板/CLI 模型行 | 收集饱和时返回 `catalog_busy` | -| `GET /api/client-config?client=...` | 构建只读的 OpenCode 或 Pi client-config 文档 | 400 不支持的客户端;503 目录不可用 | +| `GET /api/client-config?client=...` | 为任意支持的文件集成构建只读客户端配置 | 400 不支持的客户端;503 目录不可用 | | `PUT /api/disabled-models` | 替换共享的禁用模型列表 | 400 无效 JSON | | `PUT /api/model-visibility` | 原子性地更改 provider 级或 model 级可见性 | 400 provider、scope、target 或请求体无效 | | `GET, POST /api/custom-models` | 列出自定义模型或添加一个 | 400 字段无效;404 provider 缺失;409 模型重复 | diff --git a/gui/src/app-routing.ts b/gui/src/app-routing.ts index 47a5362a08..55b59af85b 100644 --- a/gui/src/app-routing.ts +++ b/gui/src/app-routing.ts @@ -81,6 +81,7 @@ export const INTEGRATION_TAB_HASHES = [ "integrations/grok", "integrations/opencode", "integrations/pi", + "integrations/omp", "integrations/hermes", "integrations/openclaw", "integrations/kimi", diff --git a/gui/src/components/apikeys-workspace/client-config-clients.ts b/gui/src/components/apikeys-workspace/client-config-clients.ts index dca09d8566..dc98871dbf 100644 --- a/gui/src/components/apikeys-workspace/client-config-clients.ts +++ b/gui/src/components/apikeys-workspace/client-config-clients.ts @@ -8,12 +8,13 @@ * with EXPORT_CLIENT_IDS by hand; adding a client server-side renders no row * until this tuple changes. */ -export const CLIENTS = ["opencode", "pi", "hermes", "openclaw", "kimi", "gajae"] as const; +export const CLIENTS = ["opencode", "pi", "omp", "hermes", "openclaw", "kimi", "gajae"] as const; export type ExportClientId = (typeof CLIENTS)[number]; export const CLIENT_LABEL_KEYS = { opencode: "api.clientConfig.clientOpencode", pi: "api.clientConfig.clientPi", + omp: "api.clientConfig.clientOmp", hermes: "api.clientConfig.clientHermes", openclaw: "api.clientConfig.clientOpenclaw", kimi: "api.clientConfig.clientKimi", diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 2e535c0c93..61654250f4 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -764,6 +764,7 @@ export const de: Record = { "integrations.tab.grok": "Grok Build", "integrations.tab.opencode": "OpenCode", "integrations.tab.pi": "Pi", + "integrations.tab.omp": "OMP", "integrations.tab.hermes": "Hermes", "integrations.tab.openclaw": "OpenClaw", "integrations.tab.kimi": "Kimi Code", @@ -853,7 +854,7 @@ export const de: Record = { "integrations.error.conflict": "Die Konfiguration wurde geändert, nachdem opencodex sie geschrieben hatte. Es wurde nichts entfernt.", "integrations.error.unsafe": "Die Konfiguration kann nicht sicher geändert werden.", "integrations.error.generic": "Die Integrationsänderung ist fehlgeschlagen. Der vorherige Zustand wurde beibehalten.", - "integrations.error.nonLoopback": "{client} erreicht nur einen Proxy auf localhost. In seiner Konfiguration ist kein Platz für den Header, den eine Remote-Bindung verlangt — von Hand geschrieben hilft es also ebenso wenig. Ermöglichen Sie stattdessen Loopback-Zugriff, etwa über einen Tunnel oder lokalen Forwarder.", + "integrations.error.nonLoopback": "Die generierte {client}-Integration funktioniert nur über Loopback. Ermöglichen Sie Loopback-Zugriff über einen Tunnel oder lokalen Forwarder.", "integrations.status.installed": "Installiert", "integrations.status.notInstalled": "Nicht installiert", "integrations.status.appliedAt": "Angewendet", @@ -873,6 +874,7 @@ export const de: Record = { "integrations.kind.restore": "Wiederhergestellt", "integrations.semantics.opencode": "Gilt nur für direkte Starts von der Festplatte; die Umgebungsinjektion von ocx opencode hat Vorrang.", "integrations.semantics.pi": "Gilt für neue Sitzungen.", + "integrations.semantics.omp": "Starten Sie OMP neu, um den Katalog zu laden.", "integrations.semantics.hermes": "Gilt für neue Sitzungen.", "integrations.semantics.openclaw": "Wird sofort auf ein laufendes Gateway angewendet.", "integrations.semantics.kimi": "Zum Anwenden neu starten oder /reload ausführen (v2 überwacht die Datei).", @@ -1143,21 +1145,22 @@ export const de: Record = { "api.clientConfig.rowsLabel": "Client verbinden", "api.clientConfig.details": "Details", "api.clientConfig.detailsAria": "Details zur {client}-Konfiguration", - "api.clientConfig.copyAria": "{client}-Konfigurations-JSON kopieren", + "api.clientConfig.copyAria": "{client}-Konfiguration kopieren", "api.clientConfig.downloadAria": "{client}-Konfiguration herunterladen", "api.clientConfig.rowMeta": "{destination} · {count} Modell(e)", "api.clientConfig.rowError": "Die {client}-Konfiguration konnte nicht erstellt werden.", - "api.clientConfig.copiedAnnounceClient": "{client}-Konfigurations-JSON in die Zwischenablage kopiert.", + "api.clientConfig.copiedAnnounceClient": "{client}-Konfiguration in die Zwischenablage kopiert.", "api.clientConfig.clientOpencode": "OpenCode", "api.clientConfig.clientPi": "Pi", + "api.clientConfig.clientOmp": "OMP", "api.clientConfig.clientHermes": "Hermes", "api.clientConfig.clientOpenclaw": "OpenClaw", "api.clientConfig.clientKimi": "Kimi Code", "api.clientConfig.clientGajae": "Gajae Code", - "api.clientConfig.copy": "JSON kopieren", + "api.clientConfig.copy": "Konfiguration kopieren", "api.clientConfig.download": "Herunterladen", "api.clientConfig.loading": "Client-Konfiguration wird erstellt…", - "api.clientConfig.jsonLabel": "{client}-Konfigurations-JSON", + "api.clientConfig.jsonLabel": "{client}-Konfiguration", "api.clientConfig.destination": "Zieldatei", "api.clientConfig.envHint": "Schlüssel vor dem Start setzen", "api.clientConfig.mergeWarning": "Führe dies in die Zieldatei ein. Ein Ersetzen würde deine anderen Provider und MCP-Einstellungen entfernen.", @@ -1165,8 +1168,8 @@ export const de: Record = { "api.clientConfig.missingLimits": "{count} von {total} Modell(en) haben kein Kontextlimit; der Client verwendet dafür seine eigenen Vorgaben.", "api.clientConfig.noKeyYet": "Für {env} existiert noch kein Schlüssel. Erzeuge oben einen Schlüssel, bevor du diese Konfiguration außerhalb von Loopback nutzt.", "api.clientConfig.loadFailed": "Die Modellliste konnte nicht gelesen werden, daher wurde keine Client-Konfiguration erzeugt.", - "api.clientConfig.copiedAnnounce": "Client-Konfigurations-JSON in die Zwischenablage kopiert.", - "api.clientConfig.copyFailed": "Client-Konfigurations-JSON konnte nicht kopiert werden.", + "api.clientConfig.copiedAnnounce": "Client-Konfiguration in die Zwischenablage kopiert.", + "api.clientConfig.copyFailed": "Client-Konfiguration konnte nicht kopiert werden.", "api.clientConfig.downloadedAnnounce": "{filename} heruntergeladen. Es hat sich noch nichts geändert — führe die Datei selbst in {destination} ein.", "api.clientConfig.whereDisclosure": "Wohin diese Datei gehört", "api.clientConfig.whereBody": "Der Pfad oben ist der globale Speicherort. Eine projektlokale Konfigurationsdatei im Arbeitsverzeichnis hat Vorrang, und der Schlüssel wird aus der in der Konfiguration genannten Umgebungsvariable gelesen — nie aus dieser Datei.", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 9ad5d576d5..9ad07b2852 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -1219,6 +1219,7 @@ export const en = { "integrations.tab.grok": "Grok Build", "integrations.tab.opencode": "OpenCode", "integrations.tab.pi": "Pi", + "integrations.tab.omp": "OMP", "integrations.tab.hermes": "Hermes", "integrations.tab.openclaw": "OpenClaw", "integrations.tab.kimi": "Kimi Code", @@ -1308,7 +1309,7 @@ export const en = { "integrations.error.conflict": "The config changed after opencodex wrote it. Nothing was removed.", "integrations.error.unsafe": "The config cannot be changed safely.", "integrations.error.generic": "The integration change failed. Your previous state was kept.", - "integrations.error.nonLoopback": "{client} can only reach a proxy on localhost — its config has nowhere to put the admission header a remote bind requires, so writing one by hand would not help either. Give it loopback access instead, through a tunnel or a local forwarder.", + "integrations.error.nonLoopback": "The generated {client} integration is loopback-only. Give it loopback access through a tunnel or a local forwarder.", "integrations.status.installed": "Installed", "integrations.status.notInstalled": "Not installed", "integrations.status.appliedAt": "Applied", @@ -1328,6 +1329,7 @@ export const en = { "integrations.kind.restore": "Restored", "integrations.semantics.opencode": "Direct disk launches only; ocx opencode environment injection takes precedence.", "integrations.semantics.pi": "Applies to new sessions.", + "integrations.semantics.omp": "Restart OMP to load the catalog.", "integrations.semantics.hermes": "Applies to new sessions.", "integrations.semantics.openclaw": "Applies immediately to a running gateway.", "integrations.semantics.kimi": "Restart or run /reload to apply it (v2 watches the file).", @@ -1606,21 +1608,22 @@ export const en = { "api.clientConfig.rowsLabel": "Connect a client", "api.clientConfig.details": "Details", "api.clientConfig.detailsAria": "{client} config details", - "api.clientConfig.copyAria": "Copy {client} config JSON", + "api.clientConfig.copyAria": "Copy {client} config", "api.clientConfig.downloadAria": "Download {client} config", "api.clientConfig.rowMeta": "{destination} · {count} model(s)", "api.clientConfig.rowError": "Could not build the {client} config.", - "api.clientConfig.copiedAnnounceClient": "{client} config JSON copied to the clipboard.", + "api.clientConfig.copiedAnnounceClient": "{client} config copied to the clipboard.", "api.clientConfig.clientOpencode": "OpenCode", "api.clientConfig.clientPi": "Pi", + "api.clientConfig.clientOmp": "OMP", "api.clientConfig.clientHermes": "Hermes", "api.clientConfig.clientOpenclaw": "OpenClaw", "api.clientConfig.clientKimi": "Kimi Code", "api.clientConfig.clientGajae": "Gajae Code", - "api.clientConfig.copy": "Copy JSON", + "api.clientConfig.copy": "Copy config", "api.clientConfig.download": "Download", "api.clientConfig.loading": "Building client config…", - "api.clientConfig.jsonLabel": "{client} config JSON", + "api.clientConfig.jsonLabel": "{client} config", "api.clientConfig.destination": "Destination file", "api.clientConfig.envHint": "Set the key before launching", "api.clientConfig.mergeWarning": "Merge this into the destination file. Replacing it would drop your other providers and MCP settings.", @@ -1628,8 +1631,8 @@ export const en = { "api.clientConfig.missingLimits": "{count} of {total} model(s) ship without a context limit; the client applies its own defaults.", "api.clientConfig.noKeyYet": "{env} has no key behind it yet. Generate a key above before using this config off loopback.", "api.clientConfig.loadFailed": "Could not read the model list, so no client config was produced.", - "api.clientConfig.copiedAnnounce": "Client config JSON copied to the clipboard.", - "api.clientConfig.copyFailed": "Could not copy the client config JSON.", + "api.clientConfig.copiedAnnounce": "Client config copied to the clipboard.", + "api.clientConfig.copyFailed": "Could not copy the client config.", "api.clientConfig.downloadedAnnounce": "Downloaded {filename}. Nothing changed yet — merge it into {destination} yourself.", "api.clientConfig.whereDisclosure": "Where this file goes", "api.clientConfig.whereBody": "The destination above is the global path. A project-local config file in the working directory takes precedence over it, and the client reads the key from the environment variable named in the config — never from this file.", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 12e1fb6e7b..d7a194131b 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -1167,6 +1167,7 @@ export const ja: Record = { "integrations.tab.grok": "Grok Build", "integrations.tab.opencode": "OpenCode", "integrations.tab.pi": "Pi", + "integrations.tab.omp": "OMP", "integrations.tab.hermes": "Hermes", "integrations.tab.openclaw": "OpenClaw", "integrations.tab.kimi": "Kimi Code", @@ -1256,7 +1257,7 @@ export const ja: Record = { "integrations.error.conflict": "opencodex の書き込み後に設定が変更されました。何も削除していません。", "integrations.error.unsafe": "設定を安全に変更できません。", "integrations.error.generic": "連携の変更に失敗しました。以前の状態は保持されています。", - "integrations.error.nonLoopback": "{client} は localhost のプロキシにしか接続できません。リモートバインドに必要な認証ヘッダーを置く場所が設定ファイルになく、手動で書いても同じです。トンネルやローカルフォワーダーで loopback 経路を用意してください。", + "integrations.error.nonLoopback": "生成された {client} 連携は loopback 専用です。トンネルやローカルフォワーダーで loopback 経路を用意してください。", "integrations.status.installed": "インストール済み", "integrations.status.notInstalled": "未インストール", "integrations.status.appliedAt": "適用", @@ -1276,6 +1277,7 @@ export const ja: Record = { "integrations.kind.restore": "復元", "integrations.semantics.opencode": "ディスクから直接起動した場合にのみ適用されます。ocx opencode の環境注入が優先されます。", "integrations.semantics.pi": "新しいセッションから適用されます。", + "integrations.semantics.omp": "カタログを読み込むには OMP を再起動してください。", "integrations.semantics.hermes": "新しいセッションから適用されます。", "integrations.semantics.openclaw": "実行中のゲートウェイにすぐ適用されます。", "integrations.semantics.kimi": "再起動するか /reload を実行すると適用されます(v2 はファイルを監視します)。", @@ -1551,21 +1553,22 @@ export const ja: Record = { "api.clientConfig.rowsLabel": "クライアントを接続", "api.clientConfig.details": "詳細", "api.clientConfig.detailsAria": "{client} 設定の詳細", - "api.clientConfig.copyAria": "{client} 設定 JSON をコピー", + "api.clientConfig.copyAria": "{client} 設定をコピー", "api.clientConfig.downloadAria": "{client} 設定をダウンロード", "api.clientConfig.rowMeta": "{destination} · モデル {count} 件", "api.clientConfig.rowError": "{client} の設定を生成できませんでした。", - "api.clientConfig.copiedAnnounceClient": "{client} の設定 JSON をクリップボードにコピーしました。", + "api.clientConfig.copiedAnnounceClient": "{client} の設定をクリップボードにコピーしました。", "api.clientConfig.clientOpencode": "OpenCode", "api.clientConfig.clientPi": "Pi", + "api.clientConfig.clientOmp": "OMP", "api.clientConfig.clientHermes": "Hermes", "api.clientConfig.clientOpenclaw": "OpenClaw", "api.clientConfig.clientKimi": "Kimi Code", "api.clientConfig.clientGajae": "Gajae Code", - "api.clientConfig.copy": "JSON をコピー", + "api.clientConfig.copy": "設定をコピー", "api.clientConfig.download": "ダウンロード", "api.clientConfig.loading": "クライアント設定を生成中…", - "api.clientConfig.jsonLabel": "{client} 設定 JSON", + "api.clientConfig.jsonLabel": "{client} 設定", "api.clientConfig.destination": "配置先ファイル", "api.clientConfig.envHint": "起動前にキーを設定", "api.clientConfig.mergeWarning": "配置先ファイルにマージしてください。置き換えると既存のプロバイダーや MCP 設定が失われます。", @@ -1573,8 +1576,8 @@ export const ja: Record = { "api.clientConfig.missingLimits": "{total} 件中 {count} 件のモデルにコンテキスト上限がないため、クライアント側の既定値が使われます。", "api.clientConfig.noKeyYet": "{env} に対応するキーがまだありません。ループバック外で使う前に上でキーを発行してください。", "api.clientConfig.loadFailed": "モデル一覧を読み取れなかったため、クライアント設定を生成できませんでした。", - "api.clientConfig.copiedAnnounce": "クライアント設定 JSON をクリップボードにコピーしました。", - "api.clientConfig.copyFailed": "クライアント設定 JSON をコピーできませんでした。", + "api.clientConfig.copiedAnnounce": "クライアント設定をクリップボードにコピーしました。", + "api.clientConfig.copyFailed": "クライアント設定をコピーできませんでした。", "api.clientConfig.downloadedAnnounce": "{filename} をダウンロードしました。まだ何も変わっていません。{destination} に自分でマージしてください。", "api.clientConfig.whereDisclosure": "このファイルの置き場所", "api.clientConfig.whereBody": "上のパスはグローバル設定の場所です。作業ディレクトリのプロジェクト設定ファイルが優先され、キーは設定に書かれた環境変数から読み込まれ、このファイルには保存されません。", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index bd17b27419..d8542a4121 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -788,6 +788,7 @@ export const ko: Record = { "integrations.tab.grok": "Grok Build", "integrations.tab.opencode": "OpenCode", "integrations.tab.pi": "Pi", + "integrations.tab.omp": "OMP", "integrations.tab.hermes": "Hermes", "integrations.tab.openclaw": "OpenClaw", "integrations.tab.kimi": "Kimi Code", @@ -877,7 +878,7 @@ export const ko: Record = { "integrations.error.conflict": "opencodex가 쓴 뒤 설정이 변경되었습니다. 아무 내용도 제거하지 않았습니다.", "integrations.error.unsafe": "설정을 안전하게 변경할 수 없습니다.", "integrations.error.generic": "연동 변경에 실패했습니다. 이전 상태는 유지되었습니다.", - "integrations.error.nonLoopback": "{client}은(는) localhost 프록시에만 연결할 수 있습니다. 원격 바인드에 필요한 인증 헤더를 넣을 자리가 설정 파일에 없어 직접 작성해도 마찬가지입니다. 터널이나 로컬 포워더로 loopback 경로를 열어주세요.", + "integrations.error.nonLoopback": "생성된 {client} 연동은 loopback 전용입니다. 터널이나 로컬 포워더로 loopback 경로를 열어주세요.", "integrations.status.installed": "설치 감지됨", "integrations.status.notInstalled": "설치되지 않음", "integrations.status.appliedAt": "적용", @@ -897,6 +898,7 @@ export const ko: Record = { "integrations.kind.restore": "복원", "integrations.semantics.opencode": "디스크에서 직접 실행할 때만 적용됩니다. ocx opencode의 환경 주입이 우선합니다.", "integrations.semantics.pi": "새 세션부터 적용됩니다.", + "integrations.semantics.omp": "카탈로그를 불러오려면 OMP를 재시작하세요.", "integrations.semantics.hermes": "새 세션부터 적용됩니다.", "integrations.semantics.openclaw": "실행 중인 게이트웨이에 즉시 반영됩니다.", "integrations.semantics.kimi": "재시작 또는 /reload 시 적용됩니다 (v2는 파일 변경을 감지합니다).", @@ -1170,21 +1172,22 @@ export const ko: Record = { "api.clientConfig.rowsLabel": "클라이언트 연결", "api.clientConfig.details": "자세히", "api.clientConfig.detailsAria": "{client} 설정 자세히 보기", - "api.clientConfig.copyAria": "{client} 설정 JSON 복사", + "api.clientConfig.copyAria": "{client} 설정 복사", "api.clientConfig.downloadAria": "{client} 설정 다운로드", "api.clientConfig.rowMeta": "{destination} · 모델 {count}개", "api.clientConfig.rowError": "{client} 설정을 만들지 못했습니다.", - "api.clientConfig.copiedAnnounceClient": "{client} 설정 JSON을 클립보드에 복사했습니다.", + "api.clientConfig.copiedAnnounceClient": "{client} 설정을 클립보드에 복사했습니다.", "api.clientConfig.clientOpencode": "OpenCode", "api.clientConfig.clientPi": "Pi", + "api.clientConfig.clientOmp": "OMP", "api.clientConfig.clientHermes": "Hermes", "api.clientConfig.clientOpenclaw": "OpenClaw", "api.clientConfig.clientKimi": "Kimi Code", "api.clientConfig.clientGajae": "Gajae Code", - "api.clientConfig.copy": "JSON 복사", + "api.clientConfig.copy": "설정 복사", "api.clientConfig.download": "다운로드", "api.clientConfig.loading": "클라이언트 설정 생성 중…", - "api.clientConfig.jsonLabel": "{client} 설정 JSON", + "api.clientConfig.jsonLabel": "{client} 설정", "api.clientConfig.destination": "대상 파일", "api.clientConfig.envHint": "실행 전 키 설정", "api.clientConfig.mergeWarning": "대상 파일에 병합하세요. 덮어쓰면 기존 프로바이더와 MCP 설정이 사라집니다.", @@ -1192,8 +1195,8 @@ export const ko: Record = { "api.clientConfig.missingLimits": "{total}개 중 {count}개 모델에 컨텍스트 한도가 없어 클라이언트 기본값이 적용됩니다.", "api.clientConfig.noKeyYet": "{env}에 연결된 키가 아직 없습니다. 루프백 밖에서 쓰려면 위에서 키를 발급하세요.", "api.clientConfig.loadFailed": "모델 목록을 읽지 못해 클라이언트 설정을 만들지 못했습니다.", - "api.clientConfig.copiedAnnounce": "클라이언트 설정 JSON을 클립보드에 복사했습니다.", - "api.clientConfig.copyFailed": "클라이언트 설정 JSON을 복사하지 못했습니다.", + "api.clientConfig.copiedAnnounce": "클라이언트 설정을 클립보드에 복사했습니다.", + "api.clientConfig.copyFailed": "클라이언트 설정을 복사하지 못했습니다.", "api.clientConfig.downloadedAnnounce": "{filename} 파일을 다운로드했습니다. 아직 아무것도 바뀌지 않았으니 {destination}에 직접 병합하세요.", "api.clientConfig.whereDisclosure": "이 파일이 들어갈 위치", "api.clientConfig.whereBody": "위 경로는 전역 설정 경로입니다. 작업 디렉터리의 프로젝트 설정 파일이 우선하며, 키는 설정에 적힌 환경 변수에서 읽고 이 파일에는 저장되지 않습니다.", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index b02cb6e591..0d9c99f4e6 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -1209,6 +1209,7 @@ export const ru: Record = { "integrations.tab.grok": "Grok Build", "integrations.tab.opencode": "OpenCode", "integrations.tab.pi": "Pi", + "integrations.tab.omp": "OMP", "integrations.tab.hermes": "Hermes", "integrations.tab.openclaw": "OpenClaw", "integrations.tab.kimi": "Kimi Code", @@ -1298,7 +1299,7 @@ export const ru: Record = { "integrations.error.conflict": "После записи opencodex конфигурация изменилась. Ничего не было удалено.", "integrations.error.unsafe": "Конфигурацию нельзя изменить безопасно.", "integrations.error.generic": "Изменить интеграцию не удалось. Предыдущее состояние сохранено.", - "integrations.error.nonLoopback": "{client} может обращаться только к прокси на localhost: в его конфигурации негде разместить заголовок, который требуется при удалённой привязке, поэтому ручная настройка тоже не поможет. Обеспечьте доступ через loopback — туннелем или локальным форвардером.", + "integrations.error.nonLoopback": "Сгенерированная интеграция {client} работает только через loopback. Обеспечьте доступ через loopback — туннелем или локальным форвардером.", "integrations.status.installed": "Установлен", "integrations.status.notInstalled": "Не установлен", "integrations.status.appliedAt": "Применено", @@ -1318,6 +1319,7 @@ export const ru: Record = { "integrations.kind.restore": "Восстановлено", "integrations.semantics.opencode": "Действует только при прямом запуске с диска; внедрение окружения через ocx opencode имеет приоритет.", "integrations.semantics.pi": "Применяется к новым сеансам.", + "integrations.semantics.omp": "Перезапустите OMP, чтобы загрузить каталог.", "integrations.semantics.hermes": "Применяется к новым сеансам.", "integrations.semantics.openclaw": "Немедленно применяется к работающему шлюзу.", "integrations.semantics.kimi": "Чтобы применить, перезапустите клиент или выполните /reload (v2 отслеживает файл).", @@ -1593,21 +1595,22 @@ export const ru: Record = { "api.clientConfig.rowsLabel": "Подключение клиента", "api.clientConfig.details": "Подробнее", "api.clientConfig.detailsAria": "Подробности конфигурации {client}", - "api.clientConfig.copyAria": "Скопировать JSON конфигурации {client}", + "api.clientConfig.copyAria": "Скопировать конфигурацию {client}", "api.clientConfig.downloadAria": "Скачать конфигурацию {client}", "api.clientConfig.rowMeta": "{destination} · моделей: {count}", "api.clientConfig.rowError": "Не удалось собрать конфигурацию {client}.", - "api.clientConfig.copiedAnnounceClient": "JSON конфигурации {client} скопирован в буфер обмена.", + "api.clientConfig.copiedAnnounceClient": "Конфигурация {client} скопирована в буфер обмена.", "api.clientConfig.clientOpencode": "OpenCode", "api.clientConfig.clientPi": "Pi", + "api.clientConfig.clientOmp": "OMP", "api.clientConfig.clientHermes": "Hermes", "api.clientConfig.clientOpenclaw": "OpenClaw", "api.clientConfig.clientKimi": "Kimi Code", "api.clientConfig.clientGajae": "Gajae Code", - "api.clientConfig.copy": "Копировать JSON", + "api.clientConfig.copy": "Копировать конфигурацию", "api.clientConfig.download": "Скачать", "api.clientConfig.loading": "Формируется конфигурация клиента…", - "api.clientConfig.jsonLabel": "JSON конфигурации {client}", + "api.clientConfig.jsonLabel": "Конфигурация {client}", "api.clientConfig.destination": "Целевой файл", "api.clientConfig.envHint": "Задайте ключ перед запуском", "api.clientConfig.mergeWarning": "Объедините это с целевым файлом. Замена удалит ваши другие провайдеры и настройки MCP.", @@ -1615,8 +1618,8 @@ export const ru: Record = { "api.clientConfig.missingLimits": "У {count} из {total} моделей нет лимита контекста; клиент применит свои значения по умолчанию.", "api.clientConfig.noKeyYet": "Для {env} пока нет ключа. Создайте ключ выше, прежде чем использовать конфигурацию вне loopback.", "api.clientConfig.loadFailed": "Не удалось прочитать список моделей, поэтому конфигурация клиента не создана.", - "api.clientConfig.copiedAnnounce": "JSON конфигурации клиента скопирован в буфер обмена.", - "api.clientConfig.copyFailed": "Не удалось скопировать JSON конфигурации клиента.", + "api.clientConfig.copiedAnnounce": "Конфигурация клиента скопирована в буфер обмена.", + "api.clientConfig.copyFailed": "Не удалось скопировать конфигурацию клиента.", "api.clientConfig.downloadedAnnounce": "Файл {filename} скачан. Пока ничего не изменилось — объедините его с {destination} самостоятельно.", "api.clientConfig.whereDisclosure": "Куда положить этот файл", "api.clientConfig.whereBody": "Путь выше — глобальное расположение. Файл конфигурации проекта в рабочем каталоге имеет приоритет, а ключ читается из переменной окружения, указанной в конфигурации, и никогда не хранится в этом файле.", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 83d42fb0ed..bf710a75cb 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -781,6 +781,7 @@ export const zh: Record = { "integrations.tab.grok": "Grok Build", "integrations.tab.opencode": "OpenCode", "integrations.tab.pi": "Pi", + "integrations.tab.omp": "OMP", "integrations.tab.hermes": "Hermes", "integrations.tab.openclaw": "OpenClaw", "integrations.tab.kimi": "Kimi Code", @@ -870,7 +871,7 @@ export const zh: Record = { "integrations.error.conflict": "opencodex 写入后配置又发生了更改。未移除任何内容。", "integrations.error.unsafe": "无法安全地更改配置。", "integrations.error.generic": "集成更改失败。已保留之前的状态。", - "integrations.error.nonLoopback": "{client} 只能连接 localhost 上的代理:其配置没有位置放置远程绑定所需的准入标头,手动编写同样无效。请改用隧道或本地转发器提供 loopback 访问。", + "integrations.error.nonLoopback": "生成的 {client} 集成仅支持环回。请通过隧道或本地转发器提供 loopback 访问。", "integrations.status.installed": "已安装", "integrations.status.notInstalled": "未安装", "integrations.status.appliedAt": "已应用", @@ -890,6 +891,7 @@ export const zh: Record = { "integrations.kind.restore": "已恢复", "integrations.semantics.opencode": "仅适用于直接从磁盘启动;ocx opencode 的环境注入优先。", "integrations.semantics.pi": "对新会话生效。", + "integrations.semantics.omp": "重启 OMP 以加载模型目录。", "integrations.semantics.hermes": "对新会话生效。", "integrations.semantics.openclaw": "立即应用到正在运行的网关。", "integrations.semantics.kimi": "重启或运行 /reload 以应用(v2 会监视该文件)。", @@ -1163,21 +1165,22 @@ export const zh: Record = { "api.clientConfig.rowsLabel": "连接客户端", "api.clientConfig.details": "详情", "api.clientConfig.detailsAria": "{client} 配置详情", - "api.clientConfig.copyAria": "复制 {client} 配置 JSON", + "api.clientConfig.copyAria": "复制 {client} 配置", "api.clientConfig.downloadAria": "下载 {client} 配置", "api.clientConfig.rowMeta": "{destination} · {count} 个模型", "api.clientConfig.rowError": "无法生成 {client} 配置。", - "api.clientConfig.copiedAnnounceClient": "已将 {client} 配置 JSON 复制到剪贴板。", + "api.clientConfig.copiedAnnounceClient": "已将 {client} 配置复制到剪贴板。", "api.clientConfig.clientOpencode": "OpenCode", "api.clientConfig.clientPi": "Pi", + "api.clientConfig.clientOmp": "OMP", "api.clientConfig.clientHermes": "Hermes", "api.clientConfig.clientOpenclaw": "OpenClaw", "api.clientConfig.clientKimi": "Kimi Code", "api.clientConfig.clientGajae": "Gajae Code", - "api.clientConfig.copy": "复制 JSON", + "api.clientConfig.copy": "复制配置", "api.clientConfig.download": "下载", "api.clientConfig.loading": "正在生成客户端配置…", - "api.clientConfig.jsonLabel": "{client} 配置 JSON", + "api.clientConfig.jsonLabel": "{client} 配置", "api.clientConfig.destination": "目标文件", "api.clientConfig.envHint": "启动前设置密钥", "api.clientConfig.mergeWarning": "请合并到目标文件中。直接替换会丢失你已有的其他提供商和 MCP 配置。", @@ -1185,8 +1188,8 @@ export const zh: Record = { "api.clientConfig.missingLimits": "{total} 个模型中有 {count} 个没有上下文上限,客户端将使用自己的默认值。", "api.clientConfig.noKeyYet": "{env} 目前还没有对应的密钥。离开回环地址使用前,请先在上方生成密钥。", "api.clientConfig.loadFailed": "无法读取模型列表,因此没有生成客户端配置。", - "api.clientConfig.copiedAnnounce": "客户端配置 JSON 已复制到剪贴板。", - "api.clientConfig.copyFailed": "无法复制客户端配置 JSON。", + "api.clientConfig.copiedAnnounce": "客户端配置已复制到剪贴板。", + "api.clientConfig.copyFailed": "无法复制客户端配置。", "api.clientConfig.downloadedAnnounce": "已下载 {filename}。目前还没有任何改动,请自行将其合并到 {destination}。", "api.clientConfig.whereDisclosure": "该文件应放在哪里", "api.clientConfig.whereBody": "上面的路径是全局配置位置。工作目录中的项目级配置文件优先级更高;密钥由配置中指定的环境变量读取,不会写入该文件。", diff --git a/gui/src/pages/Integrations.tsx b/gui/src/pages/Integrations.tsx index 772598af26..27584595dd 100644 --- a/gui/src/pages/Integrations.tsx +++ b/gui/src/pages/Integrations.tsx @@ -31,6 +31,7 @@ const TABS: readonly TabDefinition[] = [ { id: "grok", hash: "integrations/grok", labelKey: "integrations.tab.grok" }, { id: "opencode", hash: "integrations/opencode", labelKey: "integrations.tab.opencode" }, { id: "pi", hash: "integrations/pi", labelKey: "integrations.tab.pi" }, + { id: "omp", hash: "integrations/omp", labelKey: "integrations.tab.omp" }, { id: "hermes", hash: "integrations/hermes", labelKey: "integrations.tab.hermes" }, { id: "openclaw", hash: "integrations/openclaw", labelKey: "integrations.tab.openclaw" }, { id: "kimi", hash: "integrations/kimi", labelKey: "integrations.tab.kimi" }, @@ -40,6 +41,7 @@ const TABS: readonly TabDefinition[] = [ const FILE_CLIENTS = new Set([ "opencode", "pi", + "omp", "hermes", "openclaw", "kimi", diff --git a/gui/src/pages/integrations/FileIntegrationPage.tsx b/gui/src/pages/integrations/FileIntegrationPage.tsx index 32f408d80f..4783a4ef2e 100644 --- a/gui/src/pages/integrations/FileIntegrationPage.tsx +++ b/gui/src/pages/integrations/FileIntegrationPage.tsx @@ -19,6 +19,7 @@ export type { FileIntegrationClientId }; const SEMANTICS_KEY: Record = { opencode: "integrations.semantics.opencode", pi: "integrations.semantics.pi", + omp: "integrations.semantics.omp", hermes: "integrations.semantics.hermes", openclaw: "integrations.semantics.openclaw", kimi: "integrations.semantics.kimi", @@ -28,6 +29,7 @@ const SEMANTICS_KEY: Record = { const TAB_LABEL_KEY: Record = { opencode: "integrations.tab.opencode", pi: "integrations.tab.pi", + omp: "integrations.tab.omp", hermes: "integrations.tab.hermes", openclaw: "integrations.tab.openclaw", kimi: "integrations.tab.kimi", diff --git a/gui/src/pages/integrations/integration-api.ts b/gui/src/pages/integrations/integration-api.ts index 2d6830da67..0d7b0d3ad5 100644 --- a/gui/src/pages/integrations/integration-api.ts +++ b/gui/src/pages/integrations/integration-api.ts @@ -3,6 +3,7 @@ import { readJsonIfOk } from "../../fetch-json"; export const FILE_INTEGRATION_CLIENTS = [ "opencode", "pi", + "omp", "hermes", "openclaw", "kimi", diff --git a/gui/src/pages/integrations/overview-clients.ts b/gui/src/pages/integrations/overview-clients.ts index 73811976bf..c2894c3cd1 100644 --- a/gui/src/pages/integrations/overview-clients.ts +++ b/gui/src/pages/integrations/overview-clients.ts @@ -2,7 +2,7 @@ * One row model for every client the Integrations page can reach. * * The overview used to read a single route, `/api/client-integrations`, which - * answers only for the six file-toggle clients. A user with Claude Code + * answers only for the seven file-toggle clients. A user with Claude Code * connected and a Grok fence written was told "applied: 0" while three * integrations were live one tab away. Five more sources join the grid here, * each with its own payload shape, mapped to the badge vocabulary the file @@ -136,6 +136,7 @@ export interface OverviewSources { const FILE_LABEL_KEY: Record = { opencode: "integrations.tab.opencode", pi: "integrations.tab.pi", + omp: "integrations.tab.omp", hermes: "integrations.tab.hermes", openclaw: "integrations.tab.openclaw", kimi: "integrations.tab.kimi", diff --git a/gui/tests/client-config-panel.test.tsx b/gui/tests/client-config-panel.test.tsx index ce2336c7d6..c467a20b2e 100644 --- a/gui/tests/client-config-panel.test.tsx +++ b/gui/tests/client-config-panel.test.tsx @@ -190,7 +190,7 @@ test("each row fetches its own client and its dialog renders that client's exact await act(async () => { root.unmount(); }); }); -test("the config JSON is not rendered at rest", async () => { +test("the config bytes are not rendered at rest", async () => { // The core of the request: rows carry the actions, not the payload. stubRoute(client => Response.json(client === "pi" ? PI_ENVELOPE : OPENCODE_ENVELOPE)); const { root, container } = await mountPanel(); @@ -199,7 +199,7 @@ test("the config JSON is not rendered at rest", async () => { expect(container.querySelector(".awi-clientconfig-json")).toBeNull(); expect(container.querySelector("dialog")).toBeNull(); // Both transport actions stay on the surface; only inspection is demoted. - expect(rowButton(container, "OpenCode", "Copy JSON").disabled).toBe(false); + expect(rowButton(container, "OpenCode", "Copy config").disabled).toBe(false); expect(rowButton(container, "OpenCode", "Download").disabled).toBe(false); await act(async () => { rowButton(container, "OpenCode", "Details").click(); }); @@ -233,7 +233,7 @@ test("row actions carry client-qualified accessible names", async () => { stubRoute(client => Response.json(client === "pi" ? PI_ENVELOPE : OPENCODE_ENVELOPE)); const { root, container } = await mountPanel(); - expect(rowButton(container, "Pi", "Copy JSON").getAttribute("aria-label")).toBe("Copy Pi config JSON"); + expect(rowButton(container, "Pi", "Copy config").getAttribute("aria-label")).toBe("Copy Pi config"); expect(rowButton(container, "Pi", "Download").getAttribute("aria-label")).toBe("Download Pi config"); expect(rowButton(container, "Pi", "Details").getAttribute("aria-label")).toBe("Pi config details"); expect(container.querySelector(".awi-clientconfig-rows")?.getAttribute("aria-label")).toBe("Connect a client"); @@ -434,14 +434,14 @@ test("one client's failure isolates to its row, with no partial JSON and the bas .toEqual(["Retry"]); // The sibling row is untouched by its neighbour's 503. - expect(rowButton(container, "OpenCode", "Copy JSON").disabled).toBe(false); + expect(rowButton(container, "OpenCode", "Copy config").disabled).toBe(false); expect(rowButton(container, "OpenCode", "Download").disabled).toBe(false); await act(async () => { rowButton(container, "Pi", "Retry").click(); }); expect(piAttempts).toBe(2); expect(row(container, "Pi").textContent).toContain(PI_ENVELOPE.destination); - expect(rowButton(container, "Pi", "Copy JSON").disabled).toBe(false); + expect(rowButton(container, "Pi", "Copy config").disabled).toBe(false); await act(async () => { root.unmount(); }); }); @@ -468,7 +468,7 @@ test("no-key state is informational and leaves copy and download enabled", async // Row actions never block on a missing key: an agent may legitimately want the // shape first. - expect(rowButton(container, "OpenCode", "Copy JSON").disabled).toBe(false); + expect(rowButton(container, "OpenCode", "Copy config").disabled).toBe(false); expect(rowButton(container, "OpenCode", "Download").disabled).toBe(false); await act(async () => { rowButton(container, "OpenCode", "Details").click(); }); diff --git a/gui/tests/integrations-overview-rows.test.ts b/gui/tests/integrations-overview-rows.test.ts index 5410c9fd8a..92151ff369 100644 --- a/gui/tests/integrations-overview-rows.test.ts +++ b/gui/tests/integrations-overview-rows.test.ts @@ -137,7 +137,7 @@ test("file clients keep their existing badge and applied semantics", () => { expect(counts.stale).toBe(1); }); -test("every client counts toward the summary, not just the file six", () => { +test("every client counts toward the summary, not just the file seven", () => { const rows = buildOverviewRows(sources({ clients: [fileStatus({ clientId: "opencode", state: "current" })], codex: { routingInjected: true, status: "at-risk" }, @@ -178,7 +178,8 @@ test("every client counts toward the summary, not just the file six", () => { test("an unsettled file list renders unknown rows instead of dropping them", () => { const built = buildOverviewRows(sources({ clients: [], clientsSettled: false })); - expect(built.rows).toHaveLength(10); + expect(built.rows).toHaveLength(11); + expect(rowById(built, "omp").state).toBe("unknown"); expect(rowById(built, "kimi").state).toBe("unknown"); // Once settled, a client the server omitted is genuinely gone. @@ -194,4 +195,5 @@ test("each row points at its own tab", () => { expect(rowById(rows, "claudeDesktop").hash).toBe("integrations/claude/desktop"); expect(rowById(rows, "grok").hash).toBe("integrations/grok"); expect(rowById(rows, "hermes").hash).toBe("integrations/hermes"); + expect(rowById(rows, "omp").hash).toBe("integrations/omp"); }); diff --git a/src/cli/export-command.ts b/src/cli/export-command.ts index d7d074fb5f..96efbc4c28 100644 --- a/src/cli/export-command.ts +++ b/src/cli/export-command.ts @@ -1,8 +1,8 @@ /** * `ocx export --client ` — print a client config for the live proxy. * - * Six clients, four formats: opencode and Pi are JSON, Hermes and Gajae YAML, - * OpenClaw JSON5, Kimi TOML. + * Seven clients, four formats: opencode and Pi are JSON, OMP, Hermes and Gajae + * are YAML, OpenClaw JSON5, Kimi TOML. * * Two consumers, one payload (devlog 260731_client_config_export/020): * @@ -179,13 +179,13 @@ export async function handleExportCommand(argv: string[], deps: ExportCommandDep // Every serializer already ends with exactly one newline. if (out !== undefined) writeExport(out, text, force); - // stderr, so `--json` stdout stays byte-exact for a redirect. + // stderr, so `--json` stdout stays a standalone JSON document. if (out !== undefined && wantsJson) console.error(`Wrote ${out}`); const degraded = models.filter(model => !hasContextLimit(model)).length; - // `--json` keeps emitting the DOCUMENT at the top level: a script that - // pipes it into a config file must not have to unwrap an envelope we added - // for our own convenience. Format metadata rides in the human lines below. + // `--json` keeps emitting the DOCUMENT at the top level as JSON for scripts; + // `--out` is the path that writes the selected client's native format. + // Format metadata rides in the human lines below. printData(clientConfig, wantsJson, [ text.trimEnd(), "", diff --git a/src/cli/help.ts b/src/cli/help.ts index f768494478..4737f9515a 100644 --- a/src/cli/help.ts +++ b/src/cli/help.ts @@ -173,12 +173,12 @@ const helpEntries: Record = { }, "api-key": { usage: "ocx api-key ...", summary: "Alias of ocx access key." }, export: { - usage: "ocx export --client [--json] [--out ] [--force]", - summary: "Print a client config (opencode, Pi, Hermes, OpenClaw, Kimi Code, Gajae Code) wired to the running proxy.", + usage: "ocx export --client [--json] [--out ] [--force]", + summary: "Print a client config (opencode, Pi, OMP, Hermes, OpenClaw, Kimi Code, Gajae Code) wired to the running proxy.", details: [ - "--json prints only the config JSON on stdout, so it is safe to redirect to a file.", - "--out writes the config there and refuses to replace an existing file without --force.", - "The config never contains a key; it references the client's env var, which you export before launching. Kimi cannot hold an env reference, so it carries a loopback placeholder instead.", + "--json prints the generated document as JSON on stdout; use --out for the client's native format.", + "--out writes the native config there and refuses to replace an existing file without --force.", + "The config never contains a real key; it carries a documented env reference or a non-secret loopback placeholder.", "The destination path is printed for merging by hand — ocx never writes your real client config.", ], }, @@ -313,7 +313,7 @@ Usage: ocx agent Subagents, injection, effort caps, and sidecars ocx observe Logs, usage, storage, memory, and debug data ocx access External API keys and endpoint information - ocx export --client Print a client config wired to the running proxy (6 clients) + ocx export --client Print a client config wired to the running proxy (7 clients) ocx integration client Enable, disable, inspect or roll back a client integration ocx grok Grok Build model selection and apply ocx system Runtime settings, startup, sync, and updates diff --git a/src/clients/config-export.ts b/src/clients/config-export.ts index 4d8ca366c1..b5a4e386ba 100644 --- a/src/clients/config-export.ts +++ b/src/clients/config-export.ts @@ -170,6 +170,49 @@ export function opencodeGlobalConfigPath( return join(xdg, "opencode", "opencode.json"); } +const OMP_PROFILE_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/; +const OMP_WINDOWS_RESERVED_PROFILE_RE = /^(?:CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])(?:\..*)?$/i; + +function ompProfileName(env: OpencodeLaunchEnv): string | undefined { + // OMP_PROFILE wins by presence, even when explicitly empty; PI_PROFILE is + // only the legacy fallback when the canonical variable is undefined. + const raw = env.OMP_PROFILE !== undefined ? env.OMP_PROFILE : env.PI_PROFILE; + const profile = raw?.trim(); + if (!profile || profile === "default") return undefined; + if ( + profile === "." + || profile === ".." + || profile.endsWith(".") + || !OMP_PROFILE_NAME_RE.test(profile) + || OMP_WINDOWS_RESERVED_PROFILE_RE.test(profile) + ) { + throw new ClientPathError(`Invalid OMP profile "${raw}"`); + } + return profile; +} + +/** Resolve the global Oh My Pi agent directory using OMP's own env precedence. */ +export function ompAgentDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string { + const profile = ompProfileName(env); + if (!profile) { + const override = env.PI_CODING_AGENT_DIR?.trim(); + if (override) return absoluteClientPath(override, home, "PI_CODING_AGENT_DIR"); + } + // OMP treats PI_CONFIG_DIR as a directory name relative to the user's home, + // even when the value starts with `/` or `~`. Mirror that path.join contract + // exactly instead of assigning those values different filesystem semantics. + const root = join(home, env.PI_CONFIG_DIR || ".omp"); + return profile ? join(root, "profiles", profile, "agent") : join(root, "agent"); +} + +/** OMP's canonical custom-provider catalog. */ +export function ompModelsConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string { + const agentDir = ompAgentDir(env, home); + const yamlFallback = join(agentDir, "models.yaml"); + const canonical = join(agentDir, "models.yml"); + return !existsSync(canonical) && existsSync(yamlFallback) ? yamlFallback : canonical; +} + /** Compose the OpenAI-compatible proxy base URL from a live probe result. */ export function opencodeProxyBaseUrl(port: number, hostname?: string): string { return `http://${probeHostname(hostname)}:${port}/v1`; @@ -364,6 +407,7 @@ export interface ExportContext { export type ExportClientId = | "opencode" | "pi" + | "omp" | "hermes" | "openclaw" | "kimi" @@ -399,14 +443,13 @@ export interface ExportClientSpec { */ buildContribution: BuildContribution; /** - * True when this client can only reach a loopback bind. + * True when the generated integration deliberately supports loopback only. * * `/v1/chat/completions` rejects bearer credentials and requires the * dedicated `x-opencodex-api-key` header (AUTH_MATRIX in - * src/server/auth-cors.ts). A client whose schema has no place to put that - * header therefore cannot authenticate against a remote bind at all — so we - * say so rather than exporting a config that 401s. Same reasoning as the - * Grok managed block's non-loopback refusal. + * src/server/auth-cors.ts). If this exporter cannot safely emit that header, + * it refuses a remote bind rather than generating a config that 401s. Same + * reasoning as the Grok managed block's non-loopback refusal. */ loopbackOnly: boolean; } @@ -894,6 +937,11 @@ function buildPiContribution(ctx: ExportContext): ManagedContribution { return singleFragment("pi", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]); } +function buildOmpContribution(ctx: ExportContext): ManagedContribution { + const doc = buildPiClientConfig(ctx); + return singleFragment("omp", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]); +} + function buildHermesContribution(ctx: ExportContext): ManagedContribution { const doc = buildHermesClientConfig(ctx); return singleFragment("hermes", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]); @@ -953,6 +1001,20 @@ export const EXPORT_CLIENTS: Record = { // dedicated admission header a remote bind requires. loopbackOnly: true, }, + omp: { + id: "omp", + filename: "omp-models.yaml", + destination: env => ompModelsConfigPath(env), + apiKeyEnv: "", + exportHint: "OMP reads a non-secret placeholder from models.yml; loopback needs no key.", + build: buildPiClientConfig, + format: "yaml", + summarize: summarizePi, + buildContribution: buildOmpContribution, + // OMP supports provider-level headers, but remote credential wiring is + // intentionally deferred from this initial loopback-only integration. + loopbackOnly: true, + }, hermes: { id: "hermes", filename: "hermes-config.yaml", diff --git a/src/integrations/registry.ts b/src/integrations/registry.ts index b4c086bdd0..889909a5ad 100644 --- a/src/integrations/registry.ts +++ b/src/integrations/registry.ts @@ -18,6 +18,8 @@ import { hermesHomeDir, kimiConfigPath, kimiHomeDir, + ompAgentDir, + ompModelsConfigPath, opencodeGlobalConfigPath, openclawConfigPath, openclawHomeDir, @@ -68,6 +70,11 @@ export const INTEGRATION_CLIENTS: Record join(home, ".pi", "agent", "models.json"), detectDir: (_env = process.env, home = homedir()) => join(home, ".pi"), }, + omp: { + id: "omp", + configPath: (env = process.env, home = homedir()) => ompModelsConfigPath(env, home), + detectDir: (env = process.env, home = homedir()) => ompAgentDir(env, home), + }, hermes: { id: "hermes", configPath: (env = process.env, home = homedir()) => hermesConfigPath(env, home), diff --git a/src/integrations/writer.ts b/src/integrations/writer.ts index 3e9ce1f7dc..bdfd41aa96 100644 --- a/src/integrations/writer.ts +++ b/src/integrations/writer.ts @@ -233,7 +233,7 @@ export function applyIntegration(input: IntegrationWriteInput): WriteOutcome { } if (isLoopbackOnly(clientId) && !isLoopbackHostname(input.config.hostname)) { return refuse(clientId, "non_loopback", classified.state, - `${clientId} has nowhere to put the admission header a non-loopback bind requires, so a generated config would be rejected — and writing one by hand would not help either. Give it loopback access instead, through a tunnel or a local forwarder.`); + `The generated ${clientId} integration is loopback-only and does not emit the admission header a non-loopback bind requires. Give it loopback access instead, through a tunnel or a local forwarder.`); } if (classified.state === "conflict") { return refuse(clientId, "conflict", "conflict", diff --git a/tests/cli-export-command.test.ts b/tests/cli-export-command.test.ts index f793fc7e08..0518362fa7 100644 --- a/tests/cli-export-command.test.ts +++ b/tests/cli-export-command.test.ts @@ -206,7 +206,7 @@ describe("ocx export argument validation (accept criterion 4)", () => { const proxy = fakeProxy(); const result = await run(["--client", "cursor"], { baseUrl: proxy.baseUrl }); expect(result.code).toBe(2); - for (const id of ["opencode", "pi", "hermes", "openclaw", "kimi", "gajae"]) { + for (const id of ["opencode", "pi", "omp", "hermes", "openclaw", "kimi", "gajae"]) { expect(result.stderr).toContain(id); } expect(result.stdout).toBe(""); diff --git a/tests/client-config-export-new-clients.test.ts b/tests/client-config-export-new-clients.test.ts index 8f5173b14c..81003fb5a5 100644 --- a/tests/client-config-export-new-clients.test.ts +++ b/tests/client-config-export-new-clients.test.ts @@ -57,13 +57,12 @@ function ctx(config: OcxConfig = LOOPBACK): ExportContext { } describe("no secret reaches a client config", () => { - test("a client is loopback-only exactly when it has nowhere to put the admission header", () => { - // /v1/chat/completions rejects bearer credentials and requires the - // dedicated x-opencodex-api-key header (AUTH_MATRIX in auth-cors.ts), so a - // client whose schema has no header field cannot authenticate remotely at - // all. Saying so beats exporting a config that 401s. + test("the generated client support policy identifies every loopback-only integration", () => { + // Pi, Kimi and Gajae cannot emit the dedicated admission header. OMP can + // carry provider headers, but remote credential wiring is deliberately + // deferred from this initial generated integration. const loopbackOnly = EXPORT_CLIENT_IDS.filter(id => EXPORT_CLIENTS[id].loopbackOnly); - expect(loopbackOnly).toEqual(["pi", "kimi", "gajae"]); + expect(loopbackOnly).toEqual(["pi", "omp", "kimi", "gajae"]); }); test("every client that is not loopback-only carries the header on a remote bind", () => { @@ -268,7 +267,7 @@ describe("gajae", () => { describe("contributions name every fragment we own", () => { test("single-entry clients own exactly one path", () => { - for (const id of ["opencode", "pi", "hermes", "openclaw", "gajae"] as const) { + for (const id of ["opencode", "pi", "omp", "hermes", "openclaw", "gajae"] as const) { expect(buildClientContribution(id, ctx()).fragments).toHaveLength(1); } }); diff --git a/tests/client-config-export.test.ts b/tests/client-config-export.test.ts index 3e8b8e881e..defce6a8a7 100644 --- a/tests/client-config-export.test.ts +++ b/tests/client-config-export.test.ts @@ -1,7 +1,9 @@ import { describe, expect, test } from "bun:test"; -import { homedir } from "node:os"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; import { + ClientPathError, EXPORT_CLIENTS, EXPORT_CLIENT_IDS, OPENCODE_API_KEY_ENV, @@ -12,6 +14,7 @@ import { buildClientConfigText, isExportClientId, normalizeExportModels, + ompModelsConfigPath, type ExportContext, type ExportModel, type OpencodeGeneratedConfig, @@ -73,6 +76,7 @@ function piConfig(context: ExportContext = ctx()): PiGeneratedConfig { return buildClientConfig("pi", context) as PiGeneratedConfig; } + describe("relocated OpenCode serializer (accept criterion 1)", () => { test("the moved builder reproduces the pre-refactor golden byte-for-byte", () => { const block = buildOpencodeProviderBlockFromCatalog(10100, FIXTURE, "127.0.0.1"); @@ -219,6 +223,23 @@ describe("Pi serializer (accept criterion 2)", () => { }); }); +describe("OMP serializer", () => { + test("writes the full routed catalog in OMP's models.yml provider shape", () => { + const built = buildClientConfigText("omp", ctx()); + expect(built.format).toBe("yaml"); + const document = buildClientConfig("omp", ctx()) as PiGeneratedConfig; + expect(document.providers.opencodex!.models.map(model => model.id)).toEqual([ + "anthropic/claude-opus-5", + "custom/no-context", + "gpt-5.6-luna", + "tiny/small-ctx", + ]); + expect(built.text).toContain("providers:"); + expect(built.text).toContain("anthropic/claude-opus-5"); + expect(built.text).toContain("apiKey: opencodex-loopback"); + }); +}); + describe("no credential ever reaches the output (accept criterion 3)", () => { const LIVE_KEY = "ocx_live_do_not_serialize_0123456789"; @@ -280,8 +301,8 @@ describe("stable ordering (accept criterion 4)", () => { }); describe("EXPORT_CLIENTS registry", () => { - test("covers exactly the six file-toggle clients", () => { - expect(EXPORT_CLIENT_IDS).toEqual(["opencode", "pi", "hermes", "openclaw", "kimi", "gajae"]); + test("covers exactly the seven file-toggle clients", () => { + expect(EXPORT_CLIENT_IDS).toEqual(["opencode", "pi", "omp", "hermes", "openclaw", "kimi", "gajae"]); for (const id of EXPORT_CLIENT_IDS) expect(isExportClientId(id)).toBe(true); // The exception clients keep their own surfaces and are not export clients. expect(isExportClientId("claude-desktop")).toBe(false); @@ -426,6 +447,7 @@ describe("EXPORT_CLIENTS registry", () => { test("filenames name the destination file, not the product", () => { expect(EXPORT_CLIENTS.opencode.filename).toBe("opencode.json"); expect(EXPORT_CLIENTS.pi.filename).toBe("pi-models.json"); + expect(EXPORT_CLIENTS.omp.filename).toBe("omp-models.yaml"); expect(EXPORT_CLIENTS.hermes.filename).toBe("hermes-config.yaml"); expect(EXPORT_CLIENTS.openclaw.filename).toBe("openclaw.json5"); expect(EXPORT_CLIENTS.kimi.filename).toBe("kimi-config.toml"); @@ -444,6 +466,46 @@ describe("EXPORT_CLIENTS registry", () => { expect(EXPORT_CLIENTS.pi.destination({} as NodeJS.ProcessEnv)).toBe(join(homedir(), ".pi", "agent", "models.json")); }); + test("the OMP destination follows its global agent directory and profile selectors", () => { + const home = mkdtempSync(join(tmpdir(), "ocx-omp-destination-")); + try { + const defaultAgentDir = join(home, ".omp", "agent"); + expect(ompModelsConfigPath({} as NodeJS.ProcessEnv, home)).toBe(join(defaultAgentDir, "models.yml")); + + mkdirSync(defaultAgentDir, { recursive: true }); + writeFileSync(join(defaultAgentDir, "models.yaml"), "providers: {}\n"); + expect(ompModelsConfigPath({} as NodeJS.ProcessEnv, home)).toBe(join(defaultAgentDir, "models.yaml")); + writeFileSync(join(defaultAgentDir, "models.yml"), "providers: {}\n"); + expect(ompModelsConfigPath({} as NodeJS.ProcessEnv, home)).toBe(join(defaultAgentDir, "models.yml")); + + const configDir = "custom-omp"; + const configRoot = join(home, configDir); + const profiled = { OMP_PROFILE: "work", PI_CONFIG_DIR: configDir } as NodeJS.ProcessEnv; + expect(ompModelsConfigPath(profiled, home)).toBe( + join(configRoot, "profiles", "work", "agent", "models.yml"), + ); + expect(ompModelsConfigPath({ PI_CONFIG_DIR: configDir } as NodeJS.ProcessEnv, home)).toBe( + join(configRoot, "agent", "models.yml"), + ); + expect(ompModelsConfigPath({ PI_PROFILE: "legacy", PI_CONFIG_DIR: configDir } as NodeJS.ProcessEnv, home)).toBe( + join(configRoot, "profiles", "legacy", "agent", "models.yml"), + ); + // OMP_PROFILE wins by presence, so an explicit blank selects the default + // profile instead of inheriting a legacy PI_PROFILE. + expect(ompModelsConfigPath({ + OMP_PROFILE: " ", + PI_PROFILE: "legacy", + PI_CONFIG_DIR: configDir, + } as NodeJS.ProcessEnv, home)).toBe( + join(configRoot, "agent", "models.yml"), + ); + expect(() => ompModelsConfigPath({ OMP_PROFILE: ".." } as NodeJS.ProcessEnv, home)).toThrow(ClientPathError); + expect(() => ompModelsConfigPath({ OMP_PROFILE: "NUL.txt" } as NodeJS.ProcessEnv, home)).toThrow(ClientPathError); + } finally { + rmSync(home, { recursive: true, force: true }); + } + }); + 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); diff --git a/tests/integrations-invariants.test.ts b/tests/integrations-invariants.test.ts index 4178b908bd..f7524214aa 100644 --- a/tests/integrations-invariants.test.ts +++ b/tests/integrations-invariants.test.ts @@ -66,9 +66,9 @@ afterEach(() => { }); describe("the client registries cannot drift apart", () => { - test("every list of clients holds exactly the same six ids", async () => { + test("every list of clients holds exactly the same seven ids", async () => { /* - * Five lists name the same six clients, and two of them are maintained by + * Five lists name the same seven clients, and two of them are maintained by * hand: the GUI cannot import the backend registry, because that would * pull node:os and node:path into the browser bundle. A client added * server-side renders no row until someone remembers the tuple, and the @@ -78,7 +78,7 @@ describe("the client registries cannot drift apart", () => { const guiIntegrations = await import("../gui/src/pages/integrations/integration-api"); const expected = [...EXPORT_CLIENT_IDS].sort(); - expect(expected).toHaveLength(6); + expect(expected).toHaveLength(7); expect([...INTEGRATION_CLIENT_IDS].sort()).toEqual(expected); expect([...gui.CLIENTS].sort()).toEqual(expected); @@ -125,6 +125,7 @@ describe("every client survives a full lifecycle", () => { const SEED: Record = { opencode: '{\n "provider": {\n "mine": { "npm": "keep-me" }\n }\n}\n', pi: '{\n "providers": {\n "mine": { "api": "http://keep-me" }\n }\n}\n', + omp: "providers:\n mine:\n api: http://keep-me\n", hermes: "providers:\n mine:\n api: http://keep-me\n", openclaw: '{\n models: {\n providers: {\n mine: { api: "http://keep-me" },\n },\n },\n}\n', kimi: '[providers.mine]\napi = "http://keep-me"\n', @@ -675,7 +676,7 @@ describe("the store's own root stays tidy", () => { describe("the CLI names every client it supports", () => { test("export help and the top-level list are not stuck on opencode and Pi", () => { /* - * The command has accepted six clients since WP1, but its help said two. + * The command has accepted several clients since WP1, but its help said two. * A user reading it concluded the feature did not support their client — * the one failure mode a help string has. * diff --git a/tests/integrations-state.test.ts b/tests/integrations-state.test.ts index 8ef851018f..ef45405827 100644 --- a/tests/integrations-state.test.ts +++ b/tests/integrations-state.test.ts @@ -381,9 +381,9 @@ describe("installation detection is independent of config state", () => { * from. Rationale and the per-client table: 020 §1 amendment. */ describe("the loopback-only set is one fact, read through one seam", () => { - test("pi, kimi and gajae are loopback-only and nobody else is", () => { + test("omp, pi, kimi and gajae are loopback-only and nobody else is", () => { const loopbackOnly = INTEGRATION_CLIENT_IDS.filter(id => isLoopbackOnly(id)); - expect(loopbackOnly).toEqual(["pi", "kimi", "gajae"]); + expect(loopbackOnly).toEqual(["pi", "omp", "kimi", "gajae"]); }); test("the registry restates nothing — it reads the export spec", () => { diff --git a/tests/integrations-writer.test.ts b/tests/integrations-writer.test.ts index 8906477000..7e4f84b09c 100644 --- a/tests/integrations-writer.test.ts +++ b/tests/integrations-writer.test.ts @@ -148,14 +148,21 @@ describe("apply", () => { expect(readFileSync(configPath, "utf8")).toBe("{{{ not yaml\n"); }); - test("refuses a loopback-only client on a remote bind", () => { - mkdirSync(join(home, ".gjc"), { recursive: true }); - const result = applyIntegration(input({ - clientId: "gajae", - config: { ...CONFIG, hostname: "0.0.0.0" } as OcxConfig, - })); - expect(result.ok).toBe(false); - if (!result.ok) expect(result.reason).toBe("non_loopback"); + test("refuses a loopback-only client on a remote bind without denying manual OMP headers", () => { + for (const clientId of ["gajae", "omp"] as const) { + const spec = INTEGRATION_CLIENTS[clientId]; + mkdirSync(spec.detectDir(TEST_ENV, home), { recursive: true }); + const result = applyIntegration(input({ + clientId, + config: { ...CONFIG, hostname: "0.0.0.0" } as OcxConfig, + })); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.reason).toBe("non_loopback"); + expect(result.message).toContain(`generated ${clientId} integration is loopback-only`); + expect(result.message).not.toContain("writing one by hand would not help"); + } + } }); test("a write failure reports the snapshot rather than claiming success", () => { diff --git a/tests/management-client-config-route.test.ts b/tests/management-client-config-route.test.ts index 1e20c446ee..93001778bc 100644 --- a/tests/management-client-config-route.test.ts +++ b/tests/management-client-config-route.test.ts @@ -29,6 +29,8 @@ interface ClientConfigEnvelope { exportHint: string; modelCount: number; modelsWithoutLimits: number; + format: string; + text: string; config: unknown; } @@ -157,6 +159,28 @@ describe("GET /api/client-config", () => { expect(provider.models.map(model => model.id)).toContain("a/m1"); }, 15_000); + test("OMP returns the full routed catalog as models.yml YAML", async () => { + const response = await clientConfigApi(baseConfig(), "?client=omp"); + expect(response.status).toBe(200); + const body = await response.json() as ClientConfigEnvelope; + + expect(body.client).toBe("omp"); + expect(body.filename).toBe("omp-models.yaml"); + expect(body.format).toBe("yaml"); + expect(body.text).toContain("providers:"); + expect(body.text).toContain("a/m1"); + const provider = (body.config as PiGeneratedConfig).providers[OPENCODE_PROVIDER_ID]; + const routedIds = provider.models + .map(model => model.id) + .filter(id => id.startsWith("a/") || id.startsWith("b/")); + expect(routedIds).toEqual(["a/m1", "a/m2", "b/no-context"]); + expect(provider.models.find(model => model.id === "a/m1")?.contextWindow).toBe(128_000); + expect(provider.models.find(model => model.id === "b/no-context")?.contextWindow).toBeUndefined(); + expect(provider.apiKey).toBe(LOOPBACK_API_KEY_PLACEHOLDER); + expect(body.text).not.toContain(REAL_LOOKING_KEY); + expect(JSON.stringify(body.config)).not.toContain(REAL_LOOKING_KEY); + }, 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; diff --git a/tests/omp-path-contract.test.ts b/tests/omp-path-contract.test.ts new file mode 100644 index 0000000000..fbff0682a5 --- /dev/null +++ b/tests/omp-path-contract.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, test } from "bun:test"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { ompModelsConfigPath } from "../src/clients/config-export"; + +describe("OMP path contract", () => { + test("keeps PI_CONFIG_DIR home-relative for slash and tilde prefixes", () => { + const home = mkdtempSync(join(tmpdir(), "ocx-omp-path-contract-")); + try { + expect(ompModelsConfigPath({ PI_CONFIG_DIR: "/custom-omp" } as NodeJS.ProcessEnv, home)).toBe( + join(home, "/custom-omp", "agent", "models.yml"), + ); + expect(ompModelsConfigPath({ PI_CONFIG_DIR: "~/.custom-omp" } as NodeJS.ProcessEnv, home)).toBe( + join(home, "~/.custom-omp", "agent", "models.yml"), + ); + } finally { + rmSync(home, { recursive: true, force: true }); + } + }); + + test("ignores PI_CODING_AGENT_DIR when an OMP profile is active", () => { + const home = mkdtempSync(join(tmpdir(), "ocx-omp-profile-contract-")); + try { + expect(ompModelsConfigPath({ + OMP_PROFILE: "work", + PI_CONFIG_DIR: "custom-omp", + PI_CODING_AGENT_DIR: join(home, "wrong-agent"), + } as NodeJS.ProcessEnv, home)).toBe( + join(home, "custom-omp", "profiles", "work", "agent", "models.yml"), + ); + } finally { + rmSync(home, { recursive: true, force: true }); + } + }); +});