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/docs-site/src/content/docs/guides/integrations.md b/docs-site/src/content/docs/guides/integrations.md index c917fd273a..3d473f4bd7 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 | 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 its root 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,7 +80,7 @@ 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 +**Pi, OMP, 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 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..d2da34eab2 100644 --- a/docs-site/src/content/docs/ja/reference/cli/agents.md +++ b/docs-site/src/content/docs/ja/reference/cli/agents.md @@ -125,7 +125,7 @@ Grok Build モデル フェンスを管理および適用します。 ## クライアント設定のエクスポート -### `ocx export --client ` +### `ocx export --client ` 実行中のプロキシに接続されているクライアント設定を出力します。 opencode と [円周率](/guides/pi/) は環境変数ではなく独自の JSON 設定からプロバイダーを読み取るため、このコマンドは `opencodex` プロバイダー ブロック (ベース URL、モデル リスト、クライアントの環境参照) をシリアル化し、そのファイルにマージできるようにします。 @@ -133,31 +133,37 @@ Grok Build モデル フェンスを管理および適用します。 |旗 |アクション | | --- | --- | -| `--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 の設定には環境参照のみが含まれるためシークレットは環境内に残り、Pi の設定には認証情報ではなくプレースホルダーが入ります。ループバック プロキシ (`127.0.0.1`、デフォルト) にはアドミッション キーはまったく必要ありません。opencode の変数は、プロキシがループバックを超えてバインドする場合にのみ設定します。アドミッションキーの発行方法については、[リモートアクセス](/reference/configuration/#remote-access) を参照してください。上流プロバイダー自体のキーは完全に別のものであり、[プロバイダー](/guides/providers/) ごとに構成されます。 同じペイロードが `GET /api/client-config` によって提供され、ダッシュボードの [API] タブにレンダリングされるため、CLI、API、および GUI は同じバイトを使用します。 diff --git a/docs-site/src/content/docs/ja/reference/management-api.md b/docs-site/src/content/docs/ja/reference/management-api.md index 56f23e6146..262f2d0e54 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..f66f151270 100644 --- a/docs-site/src/content/docs/ko/reference/cli/agents.md +++ b/docs-site/src/content/docs/ko/reference/cli/agents.md @@ -131,7 +131,7 @@ 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를 직렬화해서 해당 파일에 병합할 수 있게 해줍니다. @@ -139,31 +139,37 @@ Grok Build model fence를 관리하고 적용합니다. | 플래그 | 동작 | | --- | --- | -| `--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 config에는 env reference만 들어가므로 secret은 환경 변수에 남고, Pi config에는 인증 정보가 아니라 placeholder가 들어갑니다. loopback proxy(`127.0.0.1`, 기본값)는 admission key가 전혀 필요하지 않습니다. opencode 변수는 proxy가 loopback을 넘어 바인딩할 때만 설정하십시오. admission key가 어떻게 발급되는지는 [Remote access](/reference/configuration/#remote-access)를 보십시오. upstream provider 자체의 key는 완전히 별개의 것으로, 각 [Providers](/guides/providers/)에 맞게 설정합니다. 같은 payload는 `GET /api/client-config`로 제공되고 dashboard의 API 탭에도 렌더링되므로, CLI, API, GUI가 모두 같은 바이트를 사용합니다. diff --git a/docs-site/src/content/docs/ko/reference/management-api.md b/docs-site/src/content/docs/ko/reference/management-api.md index 334c73f094..2df6d9ccfc 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..d569d4f46e 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 `PI_CONFIG_DIR` 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 2635b719af..49f299a621 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..1c2d576511 100644 --- a/docs-site/src/content/docs/ru/reference/cli/agents.md +++ b/docs-site/src/content/docs/ru/reference/cli/agents.md @@ -152,7 +152,7 @@ override, но файлы на диске никогда не меняются. ## Экспорт client config -### `ocx export --client ` +### `ocx export --client ` Печатает client config, направленный на работающий прокси. opencode и [Pi](/guides/pi/) читают провайдеров из собственных JSON-конфигов, а не из переменных окружения, поэтому команда @@ -164,28 +164,38 @@ override, но файлы на диске никогда не меняются. | Флаг | Действие | | --- | --- | -| `--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 +204,10 @@ limit'а (для них клиент применяет собственные d MCP-записи. ::: -Никакой ключ никогда не сериализуется. Конфиг несёт только env-reference клиента, так что секрет -остаётся в вашем окружении. Loopback-прокси (`127.0.0.1`, по умолчанию) вообще не требует -admission key — ссылка просто остаётся неиспользованной. Задавайте переменную только если прокси -слушает не на loopback; как выдаются admission key, описано в +Никакой ключ никогда не сериализуется. Конфиг opencode несёт только env-reference, так что секрет +остаётся в вашем окружении, а конфиг Pi несёт заглушку вместо учётных данных. Loopback-прокси +(`127.0.0.1`, по умолчанию) вообще не требует admission key. Задавайте переменную opencode только +если прокси слушает не на loopback; как выдаются admission key, описано в [Удалённом доступе](/reference/configuration/#remote-access). Ключи upstream-провайдеров — это совсем отдельная история и настраиваются в [Провайдерах](/guides/providers/). diff --git a/docs-site/src/content/docs/ru/reference/management-api.md b/docs-site/src/content/docs/ru/reference/management-api.md index dbd13ac09a..1479105715 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..422181c2eb 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,7 +126,7 @@ 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、模型列表以及客户端的环境引用——供你合并进那个文件。 @@ -134,31 +134,37 @@ ocx claude desktop import [--apply] Validate and import JSON | 标志 | 动作 | | --- | --- | -| `--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 配置里只包含环境引用,因此密钥仍保留在你的环境中;Pi 配置里携带的是占位值而不是任何凭据。环回代理(`127.0.0.1`,默认值)根本不需要准入密钥。只有当代理绑定到环回地址之外时才需要设置 opencode 的那个变量;关于准入密钥如何签发,请参见 [远程访问](/reference/configuration/#remote-access)。上游 providers 自身的密钥则完全是另一回事,需要按 [Providers](/guides/providers/) 单独配置。 同一份负载会通过 `GET /api/client-config` 提供,并在仪表盘的 API 选项卡中渲染,因此 CLI、API 和 GUI 使用的是同一字节内容。 diff --git a/docs-site/src/content/docs/zh-cn/reference/management-api.md b/docs-site/src/content/docs/zh-cn/reference/management-api.md index 8f68041943..2822dcbf32 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 bc23cf5a28..db7cfd5ae4 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -752,6 +752,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", @@ -861,6 +862,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).", @@ -1122,21 +1124,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.", @@ -1144,8 +1147,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 c63a334950..314e99456c 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -1207,6 +1207,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", @@ -1316,6 +1317,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).", @@ -1585,21 +1587,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.", @@ -1607,8 +1610,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 8bef10accf..7d78d3b281 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -1155,6 +1155,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", @@ -1264,6 +1265,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 はファイルを監視します)。", @@ -1530,21 +1532,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 設定が失われます。", @@ -1552,8 +1555,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 4112cbd791..06b84c8974 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -776,6 +776,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", @@ -885,6 +886,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는 파일 변경을 감지합니다).", @@ -1149,21 +1151,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 설정이 사라집니다.", @@ -1171,8 +1174,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 d38a236bc2..a9b1898f55 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -1197,6 +1197,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", @@ -1306,6 +1307,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 отслеживает файл).", @@ -1572,21 +1574,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.", @@ -1594,8 +1597,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 ba9ca825e8..c7a9583a41 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -769,6 +769,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", @@ -878,6 +879,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 会监视该文件)。", @@ -1142,21 +1144,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 配置。", @@ -1164,8 +1167,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..d874b0c9ff 100644 --- a/src/clients/config-export.ts +++ b/src/clients/config-export.ts @@ -170,6 +170,51 @@ 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"); + } + const configDir = env.PI_CONFIG_DIR?.trim(); + const root = configDir + ? (isAbsolute(configDir) || configDir === "~" || configDir.startsWith("~/") || configDir.startsWith("~\\") + ? absoluteClientPath(configDir, home, "PI_CONFIG_DIR") + : join(home, configDir)) + : join(home, ".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 +409,7 @@ export interface ExportContext { export type ExportClientId = | "opencode" | "pi" + | "omp" | "hermes" | "openclaw" | "kimi" @@ -399,14 +445,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 +939,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 +1003,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/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..d509338050 100644 --- a/tests/client-config-export-new-clients.test.ts +++ b/tests/client-config-export-new-clients.test.ts @@ -63,7 +63,7 @@ describe("no secret reaches a client config", () => { // client whose schema has no header field cannot authenticate remotely at // all. Saying so beats exporting a config that 401s. 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 +268,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..52fa266fc4 100644 --- a/tests/client-config-export.test.ts +++ b/tests/client-config-export.test.ts @@ -1,5 +1,6 @@ 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 { EXPORT_CLIENTS, @@ -12,6 +13,7 @@ import { buildClientConfigText, isExportClientId, normalizeExportModels, + ompModelsConfigPath, type ExportContext, type ExportModel, type OpencodeGeneratedConfig, @@ -73,6 +75,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 +222,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 +300,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 +446,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 +465,41 @@ 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 configRoot = join(home, "custom-omp"); + const profiled = { OMP_PROFILE: "work", PI_CONFIG_DIR: configRoot } as NodeJS.ProcessEnv; + expect(EXPORT_CLIENTS.omp.destination(profiled)).toBe( + join(configRoot, "profiles", "work", "agent", "models.yml"), + ); + expect(EXPORT_CLIENTS.omp.destination(profiled)).toBe(ompModelsConfigPath(profiled)); + expect(EXPORT_CLIENTS.omp.destination({ PI_CONFIG_DIR: configRoot } as NodeJS.ProcessEnv)).toBe( + join(configRoot, "agent", "models.yml"), + ); + // OMP_PROFILE wins by presence, so an explicit blank selects the default + // profile instead of inheriting a legacy PI_PROFILE. + expect(EXPORT_CLIENTS.omp.destination({ + OMP_PROFILE: " ", + PI_PROFILE: "legacy", + PI_CONFIG_DIR: configRoot, + } as NodeJS.ProcessEnv)).toBe( + join(configRoot, "agent", "models.yml"), + ); + } 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/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;