Skip to content
37 changes: 32 additions & 5 deletions docs-site/src/content/docs/guides/codex-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,11 +266,38 @@ name.

### External provider managers

If `config.toml` already selects a provider other than `openai` or `opencodex`, OpenCodex leaves the
file unchanged and skips profile writes, catalog/cache refresh, and both immediate and background
Codex history migration. Tools that manage a custom provider often tag existing sessions with that
provider id; replacing the active id can make those intact sessions disappear from Codex's history
view. The same protection applies to an external provider selected by a legacy root profile.
In the default `full` integration mode, if `config.toml` already selects a provider other than
`openai` or `opencodex`, OpenCodex leaves the file unchanged and skips profile writes, catalog/cache
refresh, and both immediate and background Codex history migration. Tools that manage a custom
provider often tag existing sessions with that provider id; replacing the active id can make those
intact sessions disappear from Codex's history view. The same protection applies to an external
provider selected by a legacy root profile.

Use `catalog-only` when the external provider id must remain stable but OpenCodex should populate the
standard Codex model picker:

```bash
ocx config set clientIntegrations.codex catalog-only
ocx sync
```

```toml
model = "gpt-5.6-sol"
model_provider = "global-infra"
model_catalog_json = "/absolute/path/to/.codex/opencodex-catalog.json"

[model_providers.global-infra]
name = "global-infra"
base_url = "http://127.0.0.1:10100/v1"
wire_api = "responses"
```

In this mode OpenCodex updates the catalog selected by `model_catalog_json` and
`$CODEX_HOME/models_cache.json`, then returns before config injection. It does not change
`config.toml`, `opencodex.config.toml`, the OpenCodex journal, history databases, or session JSONL
files. After configuring a provider such as DeepSeek, run `ocx sync`; generated routes such as
`deepseek/deepseek-v4-flash` and `deepseek/deepseek-v4-pro` then enter the picker without changing
`model_provider = "global-infra"`.

Keep one tool as the owner of Codex provider configuration. To use OpenCodex behind an existing
provider manager, point that provider at `http://127.0.0.1:10100/v1` with Responses passthrough
Expand Down
4 changes: 3 additions & 1 deletion docs-site/src/content/docs/ja/guides/codex-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,9 @@ ocx sync-cache

### 外部プロバイダーマネージャー

`config.toml` がすでに `openai` または `opencodex` 以外のプロバイダーを選択している場合、OpenCodex はファイルを変更しないままにし、プロファイルの書き込み、カタログ/キャッシュの更新、および即時およびバックグラウンドの両方の Codex 履歴の移行をスキップします。カスタム プロバイダーを管理するツールは、多くの場合、既存のセッションにそのプロバイダー ID をタグ付けします。アクティブな ID を置き換えると、それらの無傷のセッションが Codex の履歴ビューから消える可能性があります。同じ保護が、レガシー ルート プロファイルによって選択された外部プロバイダーにも適用されます。
デフォルトの `full` モードで `config.toml` がすでに `openai` または `opencodex` 以外のプロバイダーを選択している場合、OpenCodex はファイルを変更しないままにし、プロファイルの書き込み、カタログ/キャッシュの更新、および即時およびバックグラウンドの両方の Codex 履歴の移行をスキップします。カスタム プロバイダーを管理するツールは、多くの場合、既存のセッションにそのプロバイダー ID をタグ付けします。アクティブな ID を置き換えると、それらの無傷のセッションが Codex の履歴ビューから消える可能性があります。同じ保護が、レガシー ルート プロファイルによって選択された外部プロバイダーにも適用されます。

外部 provider id を維持しながら標準モデル選択を更新するには、`ocx config set clientIntegrations.codex catalog-only` を実行してから `ocx sync` を実行します。このモードは `model_catalog_json` のカタログと `models_cache.json` だけを更新し、`config.toml`、`opencodex.config.toml`、journal、履歴データベース、session JSONL を変更しません。外部 provider は Responses passthrough (`wire_api = "responses"`) でローカルプロキシを参照させてください。

1 つのツールを Codex プロバイダー設定の所有者として保持します。既存のプロバイダー マネージャーの背後で OpenCodex を使用するには、チャット完了変換ではなく、応答パススルー (Codex TOML では `wire_api = "responses"`) を使用して、そのプロバイダーを `http://127.0.0.1:10100/v1` に指定します。プロキシ API 認証が有効な場合は、上記の非ループバック プロバイダー フォームと一致して、`OPENCODEX_API_AUTH_TOKEN` から `x-opencodex-api-key` も渡します。 OpenCodex にルーティングを直接挿入させるには、まず Codex を組み込みの `openai` プロバイダーに戻し、ユーザー所有のルート `openai_base_url` を削除してから、`ocx start` を再実行します。

Expand Down
4 changes: 3 additions & 1 deletion docs-site/src/content/docs/ko/guides/codex-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,9 @@ ocx sync-cache

### 외부 provider manager

`config.toml`이 이미 `openai`나 `opencodex`가 아닌 provider를 선택하고 있으면, OpenCodex는 그 파일을 그대로 두고 profile write, catalog/cache refresh, 즉시 및 background Codex history migration을 건너뜁니다. custom provider를 관리하는 도구는 기존 session에 그 provider id를 붙이는 경우가 많고, 활성 id를 바꾸면 그 온전한 session이 Codex의 history view에서 사라질 수 있습니다. 이 보호는 legacy root profile이 선택한 외부 provider에도 동일하게 적용됩니다.
기본 `full` 모드에서 `config.toml`이 이미 `openai`나 `opencodex`가 아닌 provider를 선택하고 있으면, OpenCodex는 그 파일을 그대로 두고 profile write, catalog/cache refresh, 즉시 및 background Codex history migration을 건너뜁니다. custom provider를 관리하는 도구는 기존 session에 그 provider id를 붙이는 경우가 많고, 활성 id를 바꾸면 그 온전한 session이 Codex의 history view에서 사라질 수 있습니다. 이 보호는 legacy root profile이 선택한 외부 provider에도 동일하게 적용됩니다.

외부 provider id를 유지하면서 표준 모델 선택기를 갱신하려면 `ocx config set clientIntegrations.codex catalog-only`를 실행한 다음 `ocx sync`를 실행하세요. 이 모드는 `model_catalog_json`의 catalog와 `models_cache.json`만 갱신하며 `config.toml`, `opencodex.config.toml`, journal, history database 또는 session JSONL을 변경하지 않습니다. 외부 provider는 Responses passthrough(`wire_api = "responses"`)로 로컬 proxy를 가리켜야 합니다.

Codex provider configuration의 소유자는 한 도구만 맡게 하세요. 기존 provider manager 뒤에서 OpenCodex를 쓰려면, 그 provider를 `http://127.0.0.1:10100/v1`로 향하게 하고 Responses passthrough를 쓰세요(`wire_api = "responses"` in Codex TOML). Chat Completions translation은 쓰지 않습니다. proxy API auth가 켜져 있으면, 위의 non-loopback provider 형식과 맞추어 `OPENCODEX_API_AUTH_TOKEN`에서 `x-opencodex-api-key`도 함께 전달하세요. OpenCodex가 routing을 직접 주입하게 하려면 먼저 Codex를 built-in `openai` provider로 되돌리고, 사용자가 소유한 root `openai_base_url`을 지운 다음, `ocx start`를 다시 실행하세요.

Expand Down
22 changes: 22 additions & 0 deletions docs-site/src/content/docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,28 @@ resolve that variable at request time. For outbound proxying, an already-set `HT

Routing has its own ordered resolution rules; see [Routing](/reference/configuration/routing/).

## Codex integration mode

`clientIntegrations.codex` controls how much Codex state OpenCodex owns:

| Value | Mode | Behavior |
| --- | --- | --- |
| missing or `true` | `full` | Manage routing, catalog, cache, and compatibility artifacts. |
| `"catalog-only"` | `catalog-only` | Refresh catalog/cache while preserving provider routing and Codex history. |
| `false` | `off` | Disable Codex integration. |

```json
{
"clientIntegrations": {
"codex": "catalog-only"
}
}
```

The dashboard exposes the same three modes under **Integrations → Codex CLI**. See
[Codex Integration](/guides/codex-integration/#external-provider-managers) for the external-provider
ownership boundary.

## Configuration domains

- [Providers](/reference/configuration/providers/) — provider entries, authentication, endpoints,
Expand Down
9 changes: 8 additions & 1 deletion docs-site/src/content/docs/ru/guides/codex-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,13 +269,20 @@ upstream snapshot и никогда не перекрываются пользо

### Внешние provider manager'ы

Если `config.toml` уже выбирает провайдера, отличного от `openai` или `opencodex`, OpenCodex
Если в режиме `full` (по умолчанию) `config.toml` уже выбирает провайдера, отличного от `openai` или `opencodex`, OpenCodex
оставляет файл без изменений и пропускает запись profile, обновление catalog/cache и как
немедленную, так и фоновую миграцию истории Codex. Инструменты, управляющие custom-провайдером,
часто помечают существующие сессии своим provider id; замена активного id может привести к тому,
что рабочие сессии просто исчезнут из history view Codex. Та же защита действует и для внешнего
провайдера, выбранного через legacy root profile.

Чтобы сохранить внешний provider id и при этом обновлять стандартный picker моделей, выполните
`ocx config set clientIntegrations.codex catalog-only`, затем `ocx sync`. В этом режиме меняются
только каталог из `model_catalog_json` и `models_cache.json`; `config.toml`,
`opencodex.config.toml`, journal, базы истории и session JSONL остаются без изменений. Внешний
провайдер должен указывать на локальный прокси через Responses passthrough
(`wire_api = "responses"`).

Держите владельцем конфигурации провайдера Codex только один инструмент. Если вы хотите
использовать OpenCodex позади уже существующего provider manager'а, направьте этот провайдер на
`http://127.0.0.1:10100/v1` с passthrough Responses (`wire_api = "responses"` в TOML Codex), а
Expand Down
29 changes: 27 additions & 2 deletions docs-site/src/content/docs/zh-cn/guides/codex-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,11 +234,36 @@ display name 是 **仅用于显示且在重新生成时保持稳定的**。每

### 外部 provider 管理器

如果 `config.toml` 已经选择了 `openai` 或 `opencodex` 之外的 provider,OpenCodex 会保持文件不变
并跳过 profile 写入、catalog/cache 刷新,以及立即和后台两种 Codex 历史迁移。管理自定义 provider 的工具
在默认的 `full` 集成模式下,如果 `config.toml` 已经选择了 `openai` 或 `opencodex` 之外的 provider,
OpenCodex 会保持文件不变,并跳过 profile 写入、catalog/cache 刷新,以及立即和后台两种 Codex 历史迁移。管理自定义 provider 的工具
通常会把现有会话标记为那个 provider id;如果替换活动 id,Codex 历史视图里那些完整会话可能会消失。
同样的保护也适用于由旧版 root profile 选择的外部 provider。

如果必须保持外部 provider id 不变,同时希望 OpenCodex 填充标准 Codex 模型选择器,请使用
`catalog-only`:

```bash
ocx config set clientIntegrations.codex catalog-only
ocx sync
```

```toml
model = "gpt-5.6-sol"
model_provider = "global-infra"
model_catalog_json = "/absolute/path/to/.codex/opencodex-catalog.json"

[model_providers.global-infra]
name = "global-infra"
base_url = "http://127.0.0.1:10100/v1"
wire_api = "responses"
```

在此模式下,OpenCodex 会更新 `model_catalog_json` 指向的目录和
`$CODEX_HOME/models_cache.json`,随后在配置注入前返回。它不会修改 `config.toml`、
`opencodex.config.toml`、OpenCodex journal、历史数据库或 session JSONL 文件。配置 DeepSeek
等 provider 后运行 `ocx sync`,`deepseek/deepseek-v4-flash`、`deepseek/deepseek-v4-pro`
等生成路由就会进入模型选择器,同时保持 `model_provider = "global-infra"` 不变。

只保留一个工具作为 Codex provider 配置的 owner。若要在现有 provider 管理器之后使用 OpenCodex,
请把那个 provider 指向 `http://127.0.0.1:10100/v1`,并使用 Responses passthrough(Codex TOML 中的
`wire_api = "responses"`),而不是 Chat Completions translation。当启用 proxy API auth 时,也要像上面的非 loopback
Expand Down
21 changes: 21 additions & 0 deletions docs-site/src/content/docs/zh-cn/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,27 @@ opencodex 会把持久化配置存放在 `$OPENCODEX_HOME/config.json`,通常

路由有自己独立的顺序化解析规则;见 [Routing](/reference/configuration/routing/)。

## Codex 集成模式

`clientIntegrations.codex` 控制 OpenCodex 对 Codex 状态的管理范围:

| 值 | 模式 | 行为 |
| --- | --- | --- |
| 缺失或 `true` | `full` | 管理路由、目录、缓存和兼容性文件。 |
| `"catalog-only"` | `catalog-only` | 刷新目录/缓存,同时保留 provider 路由和 Codex 历史。 |
| `false` | `off` | 关闭 Codex 集成。 |

```json
{
"clientIntegrations": {
"codex": "catalog-only"
}
}
```

仪表盘的 **Integrations → Codex CLI** 提供相同的三种模式。外部 provider 的所有权边界见
[Codex 集成](/zh-cn/guides/codex-integration/#外部-provider-管理器)。

## 配置域

- [Providers](/reference/configuration/providers/) — provider 条目、认证、端点、目录、allowlist、上下文限制、配额和 provider 特定选项。
Expand Down
14 changes: 14 additions & 0 deletions src/adapters/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const FORWARD_HEADERS = [
"session_id",
"session-id",
"thread-id",
"user-agent",
"x-client-request-id",
"x-codex-beta-features",
"x-codex-installation-id",
Expand Down Expand Up @@ -1464,6 +1465,19 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
}
if (provider.apiKey) headers["Authorization"] = `Bearer ${provider.apiKey}`;
if (provider.headers) Object.assign(headers, provider.headers);
// Forward client identity headers to the upstream so providers that
// enforce client validation (e.g. codex_only User-Agent checks) can
// inspect the original caller metadata. Authorization and
// chatgpt-account-id are deliberately excluded — API key mode uses
// the configured apiKey and has no ChatGPT account binding.
// The defensive name-based guard also blocks any future FORWARD_HEADERS
// entry whose name contains "key", "token", or "secret".
for (const h of FORWARD_HEADERS) {
if (h === "authorization" || h === "chatgpt-account-id") continue;
if (/key|token|secret/i.test(h)) continue;
const v = incoming?.headers.get(h);
if (v) headers[h] = v;
}
}

const forward = provider.authMode === "forward";
Expand Down
26 changes: 25 additions & 1 deletion src/cli/opencode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import type {
import { visibleNativeSlugs } from "../codex/catalog";
import { commandInvocation } from "../lib/win-exec";
import { loadServiceTokenFromFile, serviceApiTokenFilePath } from "../lib/service-secrets";
import { configuredAdminToken } from "../lib/admin-secrets";
import { providerCodexAccountMode } from "../providers/registry";
import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
import type { OcxConfig } from "../types";
Expand Down Expand Up @@ -85,6 +86,7 @@ export interface OpencodeProxyModelRow {
disabled?: boolean;
displayName?: string;
contextWindow?: number;
inputModalities?: string[];
}

const PROJECT_CONFIG_FILENAMES = ["opencode.json", "opencode.jsonc"] as const;
Expand Down Expand Up @@ -315,6 +317,9 @@ export function opencodeCatalogFromProxyRows(
id: row.id,
contextWindow: row.contextWindow,
displayName: row.displayName,
...(Array.isArray(row.inputModalities) && row.inputModalities.length > 0
? { inputModalities: row.inputModalities }
: {}),
});
}
return catalog;
Expand Down Expand Up @@ -529,6 +534,14 @@ export function opencodeNotFoundHint(
return platform === "win32" && code === 9009 && !signal ? OPENCODE_INSTALL_HINT : null;
}

export function requireOpencodeManagementToken(
readConfiguredAdminToken: () => string | null = configuredAdminToken,
): string {
const token = readConfiguredAdminToken()?.trim();
if (token) return token;
throw new Error("opencodex admin token is not configured.");
}

export async function cmdOpencode(args: string[]): Promise<number> {
const config = loadConfig();
const live = await ensureProxyForOpencode(config);
Expand All @@ -538,9 +551,20 @@ export async function cmdOpencode(args: string[]): Promise<number> {
}

const apiKey = opencodeApiKey(config);
// `/api/models` is a management endpoint — it requires the admin token, not the
// data-plane admission key. The proxy is already running at this point so
// configuredAdminToken() finds the same token the server initialized with.
let managementToken: string;
try {
managementToken = requireOpencodeManagementToken();
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
console.error(`❌ Could not fetch the model catalog from the proxy: ${reason}`);
return 1;
}
let proxyModels: OpencodeProxyModelRow[];
try {
proxyModels = await fetchOpencodeProxyModels(live, apiKey);
proxyModels = await fetchOpencodeProxyModels(live, managementToken);
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
console.error(`❌ Could not fetch the model catalog from the proxy: ${reason}`);
Expand Down
12 changes: 12 additions & 0 deletions src/clients/config-export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,20 @@ export interface OpencodeCatalogModel {
id?: string;
contextWindow?: number;
displayName?: string;
/** Input modalities declared by the model (e.g. `["text","image"]`). */
inputModalities?: string[];
}

export interface OpencodeModelEntry {
name: string;
limit?: { context: number; output: number };
/**
* Opencode v1 modality declaration. Only emitted when the catalog row
* carries at least one declared modality, so text-only models (which
* have no stored `inputModalities`) do not emit the block and opencode
* keeps its own defaults.
*/
modalities?: { input: string[] };
}

export interface OpencodeProviderBlock {
Expand Down Expand Up @@ -642,6 +651,9 @@ function opencodeProviderBlock(
if (context !== undefined) {
entry.limit = { context, output: outputBudgetFor(context) };
}
if (Array.isArray(model.inputModalities) && model.inputModalities.length > 0) {
entry.modalities = { input: [...model.inputModalities] };
}
models[key] = entry;
}
return {
Expand Down
Loading
Loading