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
8 changes: 8 additions & 0 deletions docs-site/src/content/docs/guides/codex-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,14 @@ echo `default`, so request logs show the response tier as an observation with co
`assumed`. For latency-sensitive work, compare observed first-output times across the providers you
actually use rather than assuming any particular channel is faster.

For a gateway forwarding to a backend with the same metadata limitation, explicitly declare
[`responseTierAuthoritative: false`](/reference/configuration/providers/#response-service-tier-authority)
on that provider. The request still sends priority and records the raw echo, while actual Fast
scheduling remains unconfirmed. Undeclared gateways and the official API keep their existing
response-based interpretation. Updating OpenCodex alone does not add this declaration to existing
gateway entries: without it, an eligible priority request followed by a `default` echo still records
`response-declined`.

The proxy listens on port `10100` by default and serves `POST /v1/responses`,
`POST /v1/responses/compact`, `POST /v1/images/generations`, `POST /v1/images/edits`,
`GET /v1/models`, `GET /healthz`, and the `/api/*` management surface.
Expand Down
39 changes: 39 additions & 0 deletions docs-site/src/content/docs/reference/configuration/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity
| `allowEncryptedV2AgentTasks?` | `boolean` | Disabled by default. Trust a direct key-auth `openai-responses` provider to consume or relay opaque encrypted V2 sub-agent tasks unchanged. Eligible routes skip `agentTaskRecovery`; all other routes keep the existing recovery or fail-closed behavior. OpenCodex does not decrypt, translate, or recover tasks sent through this opt-in. |
| `upstreamWebsocket?` | `boolean` | Opt-in upstream Responses WebSocket transport for `openai-responses` requests (default false). Honored only for the first-party `https://api.openai.com/v1` upstream; custom-provider endpoints always use bounded HTTP/SSE because Bun cannot enforce an inbound WebSocket message limit before allocating the complete message. The canonical ChatGPT transport is unaffected. Plain HTTP remains on SSE; non-Responses paths and `openai-chat` requests stay on HTTP. |
| `supportsServiceTier?` | `boolean` | Tri-state canonical Fast capability fallback. `true` publishes Fast in the catalog, satisfies service-tier routing requirements, contributes a supported fingerprint, and lets fast mode inject the provider's canonical wire value on a compatible final adapter. `false` strips the field and never injects, and exact model declarations cannot reopen it. Absent leaves the provider unclassified: fast mode does not inject or normalize a canonical caller value, and caller values obey the final wire's forwarding permission (`chatServiceTier` on Chat; passthrough on Responses). The registry classifies canonical OpenAI (`true`), DeepSeek, and Volcengine Ark (`false`); set it explicitly only for custom gateways that genuinely support tiers. |
| `responseTierAuthoritative?` | `boolean` | Whether the response tier can confirm or deny Fast. Set `false` explicitly for routes with non-authoritative response metadata; omission preserves existing behavior. This does not enable Fast or change outbound parameters. See [Response service-tier authority](#response-service-tier-authority). |
| `modelSupportsServiceTier?` | `Record<string, boolean>` | Exact upstream model capability overrides. Exact `true` enables canonical Fast for that model; exact `false` narrows provider defaults. An explicit provider-level `supportsServiceTier: false` remains fail-closed and cannot be reopened. Exact `true` does not authorize foreign caller-tier forwarding on Chat. Undeclared models fall back to provider-wide behavior. Management `PATCH /api/providers` merges entries and accepts `null` to clear one. |
| `chatServiceTier?` | `boolean` | Provider-wide Chat-wire opt-in for forwarding caller `service_tier` values. On a classified route it governs foreign values such as `flex`, not proxy-owned canonical Fast after capability validation; on an unclassified route it governs every caller value because no Fast capability has been validated. Exact model capability does not authorize foreign forwarding. Responses routes retain their capability-based caller forwarding behavior. |
| `promptCacheKey?` | `boolean` | Provider-wide `openai-chat` opt-in for forwarding a `prompt_cache_key`. The adapter forwards the key it is given and never invents one, but the key is not always the caller's: Claude Messages translation derives one from `metadata.user_id`, or from a model/system/tools cohort when no metadata is sent. Default off. Enable only when the upstream documents support, because strict gateways may reject the unknown field with HTTP 400. |
Expand Down Expand Up @@ -437,6 +438,44 @@ it does not restore the old name. Requests have a 60-second deadline covering th
follow-up list refresh. A timeout does not undo a write: use **Retry** to check the current name
before making another change.

### Response service-tier authority

Set `providers.<name>.responseTierAuthoritative` to `false` in `config.json` when that
provider's response `service_tier` cannot establish whether Fast was granted. This is an
operator declaration about the entire provider route, independent of `supportsServiceTier` and
`fastWire`. It neither enables Fast nor changes the outgoing `service_tier`.

For example, add this field to an existing gateway provider whose Codex backend has
non-authoritative response metadata:

```json
{ "responseTierAuthoritative": false }
```

**Gateway support is opt-in.** Updating OpenCodex alone does not add this declaration to
existing providers. Without it, an eligible priority request followed by `service_tier: "default"`
retains the legacy `response-declined` interpretation. Configure `false` only when the
route's response metadata is known to be non-authoritative.

For an eligible request serialized as `priority`, both a `default` and a `priority` echo remain
observations. Logs preserve `responseServiceTier` and record
`tierOutcome.responseTierAuthoritative: false`, `fastOutcome: "applied"`, and
`confirmation: "assumed"`, without `response-declined`. Here **applied means the request parameter
was sent**, and **assumed means the actual Fast effect is unconfirmed**. The model tooltip shows
the request and raw response separately with that confirmation. Neither this setting nor these
records prove an acceleration or a billed tier.

Cost estimates use the existing requested-tier fallback instead of treating the raw echo as a
confirmed price tier; pricing rules requiring response confirmation cannot use that echo. Historical records
without an authority flag retain their previous interpretation.

The field accepts only booleans. Omission and explicit `true` retain response-based confirmation
for other destinations, including the official API and undeclared gateways. Canonical
`https://chatgpt.com/backend-api/codex` with `authMode: "forward"` remains automatically
non-authoritative, even if `true` is configured. Gateway names and URLs are never inferred.
If one gateway mixes response contracts, use separate provider entries for those routes and
apply the declaration only to the relevant entry.

## Codex catalog and root `config.toml` settings

These settings belong in the root of `$CODEX_HOME/config.toml`, alongside
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ selector,而不是分配一个新名称。
| `chatCompletionsPath?` | `string` | 用于 `openai-chat` 请求的相对资源路径,是 `responsesPath` 的对应项,适用相同的路径规则。当同一上游以不同前缀提供 Chat Completions 和 Responses 时需要此配置:按模型的 wire override 只更换适配器而不改动 `baseUrl`,否则已启用的 Chat 请求会被发送到 Responses base。随附示例为 Z.AI。 |
| `upstreamWebsocket?` | `boolean` | 为 `openai-responses` 请求选择性启用上游 Responses WebSocket 传输(默认 `false`)。当上游支持该协议时,流式 POST 请求会使用配置的 Responses 路径(默认 `/v1/responses`),通过 HTTPS 基础 URL 以 WSS 连接,并重新编码为常规流程使用的 SSE。forward 提供者使用 `{baseUrl}/responses`;key-auth 提供者使用 `responsesPath`,未设置时回退到传统的 `/v1/responses`。普通 HTTP 仍使用 SSE;非 Responses 路径和 `openai-chat` 请求仍使用 HTTP。 |
| `supportsServiceTier?` | `boolean` | `service_tier` 能力的三态。`true`:fast 模式可以注入,调用方提供的值也会被保留。`false`:剥离该字段且绝不注入(已明确不支持的上游不会收到它)。未设置:未分类——调用方提供的值原样保留,fast 模式绝不注入。注册表已对官方 OpenAI(`true`)、DeepSeek 和 Volcengine Ark(`false`)分类;仅对真正支持分层的自定义网关显式设置。 |
| `responseTierAuthoritative?` | `boolean` | 响应等级是否足以确认或否定 Fast。对响应元数据不具权威性的链路显式设为 `false`;未设置时保持原有行为。不会启用 Fast 或修改出站参数。详见[响应服务等级的可信度](#响应服务等级的可信度)。 |
| `preserveResponsesReasoningContent?` | `boolean` | 在重放的 Responses reasoning 项中保留明文 reasoning 内容,而不是清空(清空是 ChatGPT 后端的规则)。对接受 reasoning 重放的上游(如 DeepSeek)启用。代理生成的 `ocxr1` 信封始终会被剥离。 |
| `disabled?` | `boolean` | 将提供者保留在磁盘上,但从路由和模型/目录列表中排除。 |
| `apiKey?` | `string` | API key,或在请求时解析的 `${ENV_VAR}` / `$ENV_VAR` 引用。 |
Expand Down Expand Up @@ -154,6 +155,37 @@ selector,而不是分配一个新名称。

API key 提供者可以持有字面量 key,或环境引用。OAuth 提供者使用由 `ocx login` 填充的凭据存储;基于订阅的 Claude Code 启动行为在 [`claudeCode.authMode`](/reference/configuration/server/#claude-code) 下配置。

### 响应服务等级的可信度

当提供者响应中的 `service_tier` 无法确定 Fast 是否生效时,在 `config.json` 中将
`providers.<name>.responseTierAuthoritative` 设为 `false`。这是运营者对整个提供者链路的声明,
独立于 `supportsServiceTier` 和 `fastWire`;它不会启用 Fast,也不会改变出站 `service_tier`。

例如,对于已知 Codex 后端响应元数据不具权威性的网关,在现有 provider 对象中加入:

```json
{ "responseTierAuthoritative": false }
```

**网关需要显式配置。** 仅升级 OpenCodex 不会为现有 provider 自动添加该声明。未配置时,
符合 Fast 条件且已发送 priority 的请求收到 `service_tier: "default"`,仍会按原逻辑记录为 `response-declined`。
仅对已知响应元数据无法确认实际等级的链路设置 `false`。

对于符合 Fast 条件且已发送 `priority` 的请求,`default` 和 `priority` 响应都只作为观察值。
日志保留 `responseServiceTier`,并记录 `tierOutcome.responseTierAuthoritative: false`、
`fastOutcome: "applied"` 和 `confirmation: "assumed"`,不会仅凭响应回显判为 `response-declined`。
这里 **applied 表示请求参数已发出**,**assumed 表示 Fast 的实际效果尚未确认**。
模型提示会分别显示请求等级、原始响应等级和确认状态。这些记录不能证明实际加速或计费等级。

费用估算沿用请求等级的回退逻辑,不将原始回显当成已确认的价格等级;要求响应确认的定价规则
不能使用该回显作为证据。没有可信度标记的历史记录保留原解释。

该字段只接受布尔值。对于官方 API、未声明的网关等其他目的地,省略或显式设为 `true`
都保留基于响应的判定。标准 `https://chatgpt.com/backend-api/codex` 地址配合
`authMode: "forward"` 始终自动按非权威处理,即使配置为 `true` 也不例外。
OpenCodex 不会依据网关名称或 URL 猜测可信度。如果同一网关混合不同的响应契约,
应拆成独立 provider 条目,只为相关条目配置此声明。

## 提供者诊断出站安全性

仪表板连接测试和实时模型发现使用受限的、仅 GET 传输。没有出站代理时,opencodex 只会解析一次主机名,并仅连接到该已验证地址。HTTPS 仍会保留原始 Host、SNI 和证书验证;提供者配置不能关闭证书检查。
Expand Down
1 change: 1 addition & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,7 @@
"fastwire-characterization-routing.test.ts": "routing",
"fastwire-characterization-wire.test.ts": "routing",
"fastwire-observability.test.ts": "routing",
"fastwire-response-authority.test.ts": "routing",
"fastwire-policy.test.ts": "routing",
"featherless-provider.test.ts": "providers",
"fetch-header-timeout.test.ts": "server",
Expand Down
1 change: 1 addition & 0 deletions src/config/schema/leaf-validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ export const providerConfigSchema = z.object({
annotateEmptyToolOutputs: z.boolean().optional(),
foldDeveloperRoleToSystem: z.boolean().optional(),
fastWire: fastWireSchema.nullable().optional(),
responseTierAuthoritative: z.boolean().optional(),
supportsServiceTier: z.boolean().optional(),
modelSupportsServiceTier: z.record(z.string().min(1), z.boolean()).optional(),
modelSuppressSyntheticMax: z.record(z.string().min(1), z.boolean()).optional(),
Expand Down
3 changes: 3 additions & 0 deletions src/providers/fastwire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,9 @@ export function createAdapterTierMetadata(
const loggedWireValue = wireValue === null ? null : sanitizeLogMetadataString(wireValue);
const outcome: AttemptTierOutcome = {
wireKind,
...(context.responseTierAuthoritative !== undefined
? { responseTierAuthoritative: context.responseTierAuthoritative }
: {}),
...(wireValue === null
? { wireValue: null }
: loggedWireValue ? { wireValue: loggedWireValue } : {}),
Expand Down
1 change: 1 addition & 0 deletions src/providers/model-rename-fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const PROVIDER_MODEL_RENAME_ROLES = {
mcpMaxResultBytes: "none",
modelAdapters: "record",
fastWire: "none",
responseTierAuthoritative: "none",
baseUrl: "none",
responsesPath: "none",
chatCompletionsPath: "none",
Expand Down
9 changes: 9 additions & 0 deletions src/providers/openai-tiers-destination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ export function isCanonicalOpenAiForwardProvider(provider: OcxProviderConfig): b
&& normalizedBaseUrl(provider.baseUrl) === CODEX_FORWARD_BASE_URL;
}

/**
* Response evidence is separate from Fast capability and request serialization. Preserve the
* known Codex exception (#2558); other destinations may declare their response contract without
* a gateway-name or URL heuristic. Absence retains the authoritative legacy default.
*/
export function responseTierAuthorityForProvider(provider: OcxProviderConfig): boolean | undefined {
return isCanonicalOpenAiForwardProvider(provider) ? false : provider.responseTierAuthoritative;
}

const OPENAI_API_ORIGIN = "https://api.openai.com";
const OPENAI_API_BASE_URL = `${OPENAI_API_ORIGIN}/v1`;
const OPENAI_API_RESPONSES_URL = `${OPENAI_API_BASE_URL}/responses`;
Expand Down
1 change: 1 addition & 0 deletions src/server/auth-cors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
mcpMaxResultBytes: "editor",
modelAdapters: "editor",
fastWire: "editor",
responseTierAuthoritative: "editor",
baseUrl: "editor",
responsesPath: "editor",
chatCompletionsPath: "editor",
Expand Down
7 changes: 3 additions & 4 deletions src/server/responses/core-normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { resolveOpenCodeGoTransport } from "../../providers/opencode-go-transpor
import { getOrAllocateRequestSessionLane } from "../request-log-conversation";
import { shouldPreparePlaintextV2AgentMessages } from "../../responses/plaintext-v2-agent-messages";
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
import { responseTierAuthorityForProvider } from "../../providers/openai-tiers-destination";
import { applyOpenAiVirtualModel } from "../../providers/openai-virtual-models";
import {
fastPolicyForModel,
Expand Down Expand Up @@ -209,14 +210,12 @@ export async function applyFinalRouteRequestNormalization(args: {
);
const modelServiceTierSupport = serviceTierSupportFromPolicy(fastPolicy);
const callerTier = parsed.options.serviceTier;
// The ChatGPT-internal Codex backend echoes `service_tier: "default"` even on turns it
// scheduled as priority, so its echo cannot confirm OR deny Fast. Believing it reported every
// Fast request as `response-declined` (#2558). The public API's echo stays authoritative.
// Capture destination evidence policy separately from the unchanged outbound Fast decision.
parsed.options.tierObservation = tierObservationContext(
fastPolicy,
config.fastMode,
callerTier,
isCanonicalOpenAiForwardProvider(route.provider) ? false : undefined,
responseTierAuthorityForProvider(route.provider),
);
parsed.options.tierDecision = decideTier(fastPolicy, config.fastMode, callerTier);
parsed.options.serviceTier = tierValueAfterDecision(parsed.options.tierDecision, callerTier);
Expand Down
9 changes: 9 additions & 0 deletions src/types/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ export interface AttemptTierOutcome {
callerFastSuppressedByConfig?: boolean;
confirmation: "confirmed" | "assumed" | "downgraded" | "unknown";
responseServiceTier?: string;
/** False retains the raw echo as evidence only, including during cost estimation. */
responseTierAuthoritative?: boolean;
}

/**
Expand Down Expand Up @@ -299,6 +301,13 @@ export interface OcxProviderConfig {
* absence derives from the final model adapter.
*/
fastWire?: FastWire | null;
/**
* Whether echoed service_tier can confirm or deny Fast. Set false for a relay whose
* response metadata cannot establish the granted tier. Absence keeps legacy authority;
* canonical ChatGPT Codex forwarding always treats the echo as non-authoritative.
* Observation only: this does not enable Fast or change request serialization.
*/
responseTierAuthoritative?: boolean;
baseUrl: string;
/**
* Optional relative resource path for key-auth openai-responses requests. Must start with `/`
Expand Down
6 changes: 4 additions & 2 deletions src/usage/cost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,10 +412,12 @@ export function serviceTierContext(entry: ServiceTierContext): ServiceTierContex

/** Convert one adapter-observed attempt outcome into the existing pricing provenance shape. */
export function serviceTierContextFromOutcome(outcome: AttemptTierOutcome): ServiceTierContext {
if (outcome.canonical === "priority" && outcome.confirmation === "confirmed") {
const responseAuthoritative = outcome.responseTierAuthoritative !== false;
if (responseAuthoritative && outcome.canonical === "priority" && outcome.confirmation === "confirmed") {
return { responseServiceTier: "priority" };
}
if (outcome.responseServiceTier !== undefined) {
// Non-authoritative echoes remain in the log, but cannot become pricing confirmation either.
if (responseAuthoritative && outcome.responseServiceTier !== undefined) {
return { responseServiceTier: outcome.responseServiceTier };
}
if (outcome.canonical === "priority" && outcome.confirmation === "assumed") {
Expand Down
4 changes: 4 additions & 0 deletions src/usage/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,7 @@ function normalizeAttemptTierOutcome(raw: unknown): AttemptTierOutcome | null {
if ("callerFastSuppressedByConfig" in outcome
&& typeof outcome.callerFastSuppressedByConfig !== "boolean") return null;
if ("responseServiceTier" in outcome && typeof outcome.responseServiceTier !== "string") return null;
if ("responseTierAuthoritative" in outcome && typeof outcome.responseTierAuthoritative !== "boolean") return null;
const wireValue = sanitizeLogMetadataString(outcome.wireValue);
const responseServiceTier = sanitizeLogMetadataString(outcome.responseServiceTier);
return {
Expand All @@ -629,6 +630,9 @@ function normalizeAttemptTierOutcome(raw: unknown): AttemptTierOutcome | null {
? { callerFastSuppressedByConfig: outcome.callerFastSuppressedByConfig }
: {}),
confirmation: outcome.confirmation as AttemptTierOutcome["confirmation"],
...(typeof outcome.responseTierAuthoritative === "boolean"
? { responseTierAuthoritative: outcome.responseTierAuthoritative }
: {}),
...(responseServiceTier ? { responseServiceTier } : {}),
};
}
Expand Down
Loading
Loading