Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion docs-site/src/content/docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ network. Only do this on trusted networks, and always set a strong `OPENCODEX_AP
| Field | Type | Meaning |
| --- | --- | --- |
| `adapter` | `string` | One of `openai-chat`, `openai-responses`, `anthropic`, `google`, `kiro`, `cursor`, `azure-openai` (or alias `azure`). |
| `baseUrl` | `string` | Upstream API base URL. |
| `baseUrl` | `string` | Upstream API base URL. Built-in providers with a fixed endpoint ignore it — see [Fixed provider endpoints](#fixed-provider-endpoints). |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
| `responsesPath?` | `string` | Optional relative resource path for key-auth `openai-responses` requests. It must start with `/` and contain no URL scheme, query, or fragment. When omitted, the adapter keeps its legacy `/v1/responses` URL construction. |
| `disabled?` | `boolean` | Keep the provider on disk but exclude it from routing and model/catalog listings. |
| `apiKey?` | `string` | API key, or an `${ENV_VAR}` / `$ENV_VAR` reference resolved at request time. |
Expand Down Expand Up @@ -193,6 +193,31 @@ network. Only do this on trusted networks, and always set a strong `OPENCODEX_AP
| `unsafeAllowNativeLocalExec?` | `boolean` | **Cursor adapter only.** Legacy compatibility boolean for the Cursor server-driven local `read` / `write` / `delete` / `ls` / `grep` / `shell` / `fetch` executor. Equivalent to `nativeLocalExec: "on"` when `nativeLocalExec` is unset; an explicit `nativeLocalExec` value always wins. Defaults to `false`. Prefer `nativeLocalExec` for new configs. See [Cursor provider](#cursor-provider-adapter-cursor) below. |
| `nativeLocalExec?` | `"off" \| "codex-sandbox" \| "on"` | **Cursor adapter only.** Native local exec policy for the Cursor server-driven executor. `"off"` (default) rejects it; `"on"` is the trusted-local opt-in; `"codex-sandbox"` is accepted for backwards compatibility but is fail-closed like `"off"`. See [Cursor provider](#cursor-provider-adapter-cursor) below. |

### Fixed provider endpoints

Routing resolves a provider's endpoint before any adapter sees it, and for most built-in
providers the registry's own endpoint wins over a `baseUrl` in your config. Three kinds of entry
keep the configured URL at this stage:

- providers that opt into an override — `ollama`, `vllm`, `lm-studio`, `litellm`, `qwen-cloud`
and `alibaba-token-plan-intl`;
- providers whose registry endpoint is a template you fill in, such as `azure-openai` and
`cloudflare-ai-gateway`;
- providers you define yourself, which are not in the registry at all.

Adapters may adjust the resolved URL afterward. The `kiro` adapter, for example, follows the API
region of the imported credential for a canonical `runtime.{region}.kiro.dev` host. See
[Adapters](/reference/adapters/) for per-adapter rules.

When routing discards a configured `baseUrl`, opencodex logs a warning. It names the registry
endpoint in full and your configured one by origin only, shown as `https://host/…` when it had a
path — a configured path can itself be a credential, so none of it is logged. Either drop the
`baseUrl` — the registry endpoint is what routing will use regardless — or switch to the provider
whose endpoint matches the URL you wanted.
Picking the right entry matters when a vendor runs one product in several regions:
`alibaba-token-plan` is pinned to Beijing while `alibaba-token-plan-intl` covers the
international endpoints, and a key issued for one is rejected by the other.

Comment on lines +196 to +220

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f -i 'configuration.md' docs-site/src/content/docs \
  | rg '/(ja|ko|ru|zh-cn)/' \
  | xargs -r rg -n 'Fixed provider endpoints|baseUrl|alibaba-token-plan|Credentials'

Repository: lidge-jun/opencodex

Length of output: 2782


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in \
  docs-site/src/content/docs/ja/reference/configuration.md \
  docs-site/src/content/docs/ko/reference/configuration.md \
  docs-site/src/content/docs/ru/reference/configuration.md \
  docs-site/src/content/docs/zh-cn/reference/configuration.md
do
  echo "===== $f ====="
  # show the section around the provider endpoint guidance if present
  rg -n -C 8 'Fixed provider endpoints|固定|固定された|고정|поставляем|baseUrl|alibaba-token-plan|cloudflare-ai-gateway|ollama|vllm|lm-studio|litellm|qwen-cloud' "$f" || true
  echo
done

Repository: lidge-jun/opencodex

Length of output: 19921


Add the new fixed-endpoint guidance to the translated config docs. The ja, ko, ru, and zh-cn copies of docs-site/src/content/docs/reference/configuration.md still move from the OcxProviderConfig table into later sections (ja 129-158, ko 132-165, ru 151-164, zh-cn 124-138) without the new “Fixed provider endpoints” block, so readers miss the pinned-endpoint precedence, the six override-capable providers, the template-based exceptions, and the masked warning behavior. Mirror reference/configuration.md:196-211 into each locale to keep the routing rules aligned.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs-site/src/content/docs/reference/configuration.md` around lines 196 -
211, Add the “Fixed provider endpoints” guidance to the Japanese, Korean,
Russian, and Simplified Chinese configuration documents immediately after their
OcxProviderConfig tables, mirroring the English reference content. Preserve all
details about pinned endpoint precedence, the six override-capable providers,
template-based exceptions, custom providers, masked warning URLs, and Alibaba
regional endpoint selection.

Source: Path instructions

For broken `openai-responses` compatibility gateways, `responsesItemIdRepair` belongs on the
provider object itself, for example:

Expand Down
22 changes: 21 additions & 1 deletion docs-site/src/content/docs/zh-cn/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ x-opencodex-api-key: your-secret-token
| Field | Type | 含义 |
| --- | --- | --- |
| `adapter` | `string` | `openai-chat`、`openai-responses`、`anthropic`、`google`、`kiro`、`cursor`、`azure-openai`(或别名 `azure`)之一。 |
| `baseUrl` | `string` | 上游 API base URL。 |
| `baseUrl` | `string` | 上游 API base URL。端点固定的内置 provider 会忽略它 —— 见[固定的 provider 端点](#固定的-provider-端点)。 |
| `responsesPath?` | `string` | `key` 认证的 `openai-responses` 请求可选相对 resource path。必须以 `/` 开头,且不得包含 URL scheme、query 或 fragment。省略时保留原有的 `/v1/responses` URL 构造。 |
| `disabled?` | `boolean` | 配置保留在磁盘上,但从路由和模型/目录列表排除。 |
| `apiKey?` | `string` | API key,或在请求时解析的 `${ENV_VAR}` / `$ENV_VAR` 引用。 |
Expand Down Expand Up @@ -167,6 +167,26 @@ x-opencodex-api-key: your-secret-token
| `desktopExecutor?` | `DesktopExecutorConfig` | **仅 Cursor。** 外部 computer-use/record-screen 命令;字段见下文。 |
| `unsafeAllowNativeLocalExec?` | `boolean` | **仅 Cursor adapter。** 允许 Cursor server 驱动本地 `read` / `write` / `delete` / `ls` / `grep` / `shell` / `fetch` 的 opt-in escape hatch。默认 `false`,防止远程 Cursor message 绕过 Codex 审批与 sandbox。见下文 [Cursor provider](#cursor-provideradapter-cursor)。 |

### 固定的 provider 端点

路由会在任何 adapter 介入之前解析 provider 的端点;对大多数内置 provider 而言,registry 自带的端点
优先于你在配置里写的 `baseUrl`。在这一步保留配置 URL 的只有三类:

- 显式开启覆盖的 provider —— `ollama`、`vllm`、`lm-studio`、`litellm`、`qwen-cloud` 和
`alibaba-token-plan-intl`;
- registry 端点本身是待填模板的 provider,例如 `azure-openai` 和 `cloudflare-ai-gateway`;
- 你自己定义的 provider,它们根本不在 registry 中。

之后 adapter 仍可能调整已解析的 URL。例如 `kiro` adapter 在 host 为标准
`runtime.{region}.kiro.dev` 时,会改用导入凭据所属的 API region。逐个 adapter 的规则见
[Adapters](/zh-cn/reference/adapters/)。

当路由丢弃配置的 `baseUrl` 时,opencodex 会打印一条警告:registry 端点会完整列出,而你配置的那个
只列出 origin —— 原本带路径时显示为 `https://host/…`。配置的路径本身可能就是凭据,因此一段都不会记录。
此时要么删掉 `baseUrl`(路由本来就只会使用 registry 端点),要么改用端点与目标 URL 相符的 provider。
当同一产品分区域运营时,选对条目尤其重要:`alibaba-token-plan` 固定指向北京,而
`alibaba-token-plan-intl` 覆盖国际端点,为其中一个签发的 key 在另一个上会被拒绝。

## Cursor provider(`adapter: "cursor"`)

Cursor bridge 仍属实验功能。运行 `ocx login cursor` 后,在
Expand Down
69 changes: 69 additions & 0 deletions src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "./types";
import { preservesPhysicalComboProvider, tryPickComboModel, type ComboPick } from "./combos";
import { hasOwnProvider, resolveEnvValue } from "./config";
import { assertProviderDestinationAllowed } from "./lib/destination-policy";
import { redactSecretString, redactUrlForLog } from "./lib/redact";
import { PROVIDER_REGISTRY, providerCodexAccountMode } from "./providers/registry";
import { LEGACY_CHATGPT_PROVIDER_ID, LEGACY_OPENAI_MULTI_PROVIDER_ID, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "./providers/openai-tiers";
import { decodeRoutedModelId, encodeRoutedModelId } from "./providers/slug-codec";
Expand Down Expand Up @@ -117,6 +118,73 @@ function mergeStringArrayRecord(
return out;
}

/** Same endpoint modulo surrounding space and trailing slashes — matches `matchBaseUrlChoice`. */
function isSameEndpoint(a: string, b: string): boolean {
return a.trim().replace(/\/+$/, "") === b.trim().replace(/\/+$/, "");
}

/**
* Origin of a user-configured URL, with the path withheld.
*
* A configured `baseUrl` is user-controlled and its path may itself be the credential — an
* account-scoped route token such as `https://proxy.example/v1/8fK2mP7qR4nV6x` is opaque and
* high-entropy, so it matches none of the prefix patterns in `redactSecretString`. Pattern
* redaction cannot be trusted for this value, so no path segment is logged at all. `URL.origin`
* also excludes userinfo, query and fragment.
*
* `…/…` marks that a path was present without revealing it, so a reader can tell an origin-only
* config apart from one whose path was dropped.
*/
function configuredOriginForLog(url: string): string {
try {
const parsed = new URL(url.trim());
// "null" is what URL.origin yields for non-special schemes; treat it as unusable.
if (!parsed.origin || parsed.origin === "null") return "(unloggable URL)";
const hasPath = parsed.pathname !== "" && parsed.pathname !== "/";
return hasPath ? `${parsed.origin}/…` : parsed.origin;
} catch {
return "(unparseable URL)";
}
}

// `routedProviderConfig` runs per request, so warn once per (provider, discarded, effective) triple.
// Keyed by the URLs too: editing config.json to a different wrong value warns again.
const discardedBaseUrlWarnings = new Set<string>();

/**
* A pinned registry entry — non-template `baseUrl`, no `allowBaseUrlOverride` — outranks a saved
* `baseUrl`. Dropping it silently is a footgun: requests go to an endpoint the user never
* configured, and a wrong-region or wrong-account URL then surfaces only as a 401 with nothing
* pointing back at the discarded setting.
*
* Warns rather than throws. The effective route is exactly what it was before, so a hard error
* here would break configs that route fine today (a stale `baseUrl` left over from an earlier
* provider is harmless whenever it names the same endpoint the registry pins).
*/
function warnIfBaseUrlDiscarded(providerName: string, userBaseUrl: string, effectiveBaseUrl: string): void {
if (isSameEndpoint(userBaseUrl, effectiveBaseUrl)) return;
// Asymmetric on purpose. Past the guard above, `effectiveBaseUrl` is necessarily
// `registryEntry.baseUrl`: the caller passes the resolved URL, and whenever that resolution
// kept the user's value the two are equal and we have already returned. So the effective side
// is a constant from this repo's registry and safe to print in full — it is also the useful
// half, naming the endpoint requests will actually use. The configured side is untrusted.
const discarded = configuredOriginForLog(userBaseUrl);
const effective = redactSecretString(redactUrlForLog(effectiveBaseUrl));
// Key off the logged forms: no raw credential is retained for the process lifetime, and
// rotating a key embedded in the URL no longer re-warns about the same endpoint mismatch.
// Coarser than the raw URLs — two bad paths on one host warn once, which is the right grain.
const key = `${providerName} | ${discarded} | ${effective}`;
if (discardedBaseUrlWarnings.has(key)) return;
discardedBaseUrlWarnings.add(key);
console.warn(
// Routing is what this warning speaks for: an adapter may adjust the endpoint again
// downstream (kiro re-derives the region), so do not promise where the request lands.
`⚠️ config.json provider "${providerName}": configured baseUrl ${discarded} is ignored`
+ ` because this provider's endpoint is fixed at ${effective}. A URL saved for a different`
+ ` account or region is a common cause of 401s here — drop it, or use the provider whose endpoint matches.`,
);
}

function routedProviderConfig(providerName: string, provider: OcxProviderConfig): OcxProviderConfig {
const registryEntry = PROVIDER_REGISTRY.find(entry => entry.id === providerName);
if (!registryEntry) {
Expand Down Expand Up @@ -163,6 +231,7 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig)
const baseUrl = (registryBaseUrlIsTemplate || registryEntry.allowBaseUrlOverride) && userBaseUrlIsResolved
? userBaseUrl
: registryEntry.baseUrl;
if (userBaseUrlIsResolved) warnIfBaseUrlDiscarded(providerName, userBaseUrl, baseUrl);
assertProviderDestinationAllowed(providerName, { baseUrl, allowPrivateNetwork: provider.allowPrivateNetwork });

return {
Expand Down
Loading
Loading