Skip to content
Draft
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
23 changes: 22 additions & 1 deletion docs-site/src/content/docs/reference/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,28 @@ An explicitly configured provider `User-Agent` remains authoritative regardless
if the caller sends none, OpenCodex does not invent one. Additional caller metadata can be selected
with `forwardClientHeaders`; provider `headers` win for this option, and credential or transport-owned
names are refused. Canonical ChatGPT forward auth retains its separate fixed header allowlist.
Only `originator`, `x-client-request-id`, `x-codex-app-version`, and `user-agent` are supported by `forwardClientHeaders`; arbitrary names are rejected on load/write and ignored at runtime.
Only `originator`, `x-client-request-id`, `x-codex-app-version`, `user-agent`,
`x-codex-window-id`, and `x-codex-installation-id` are supported by `forwardClientHeaders`;
arbitrary names and wildcard prefixes are rejected on load/write and ignored at runtime.

For a custom gateway that requires original Codex client metadata (for example, a sub2api
account with `codex_cli_only` enabled), opt in on that provider:

```json
{
"forwardClientHeaders": [
"originator", "user-agent", "x-codex-app-version", "x-client-request-id",
"x-codex-window-id", "x-codex-installation-id"
]
}
```

This preserves only values the client actually sends, including across native compact fallback.
WebSocket clients retain their original `User-Agent` and app-version metadata through the
handshake, with the same provider forwarding policy as HTTP clients.
The identifiers may correlate requests from the same client; enable them only for a trusted
destination that needs them. Missing identifiers are never generated, and connection tests or
model discovery do not acquire a Codex identity through this option.

### Native compaction on custom gateways

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity
| `modelMaxOutputTokens?` | `Record<string, number>` | Positive per-model `openai-chat` fallback budgets; exact/pattern matches beat the provider default. |
| `modelCosts?` | `Record<string, ProviderCostOverlay>` | Per-model display prices (USD per 1M tokens), keyed by that provider's exact upstream model id — not a provider identifier or a routed `provider/model` label, e.g. `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`. Any model id is a valid key — custom providers may target any OpenAI-compatible endpoint through the `openai-chat` adapter, and local or internal provider ids work even when they are absent from the built-in catalogs. User-configured prices win over the built-in catalogs in the Logs `~$` and Usage estimates; historical entries are repriced from the current overlay, so editing a price can move past totals. The fallback order is user `modelCosts` → exact official correction → jawcode catalog → expected-price overlay → model-level vendor fallback, and an explicit all-zero user entry means a known-zero estimate; delete that model entry to restore automatic pricing. All-zero catalog metadata still falls through. Each rate must be a non-negative finite number at most 1,000,000 (USD per 1M tokens); out-of-range rows are rejected by the management boundary and dropped on load. Display-time estimation only: overlays never affect routing, account selection, quotas, or billing. |
| `headers?` | `Record<string, string>` | Extra upstream headers. Authorization, cookies, API-key headers, embedded newlines, and invalid names are rejected. Requests that ride the provider outbound wrapper — model discovery, connection tests, and other proxy-originated diagnostics such as the Ollama show probe — send `User-Agent: opencodex` unless a User-Agent is set here or by the provider preset. An experimental relay may negotiate earlier completed-tool delivery with `x-opencodex-tool-completion: block-stop-v1`; the embedding shell must supply authenticated durable turn membership, and the relay must acknowledge the same response header and implement exact call-bound completion markers. Leave it unset for other providers. Early delivery applies only to eligible structured calls; later errors or cancellation cannot retract a call already delivered. |
| `forwardClientHeaders?` | `string[]` | `openai-responses` only: opt in to copying named inbound client metadata headers onto inference requests when the provider has not already set that header. Credential and transport-owned names such as `Authorization`, cookies, API-key headers, `Content-Type`, `Content-Length`, `Host`, and `x-oai-attestation` are rejected and also filtered at runtime. Header names are case-insensitive and may not repeat. `headers` remains authoritative; the existing caller `User-Agent` fallback still applies when neither surface sets one. Supported names: `originator`, `x-client-request-id`, `x-codex-app-version`, `user-agent`; all other names are refused. |
| `forwardClientHeaders?` | `string[]` | `openai-responses` only: opt in to copying named inbound client metadata headers onto inference requests when the provider has not already set that header. Credential and transport-owned names such as `Authorization`, cookies, API-key headers, `Content-Type`, `Content-Length`, `Host`, and `x-oai-attestation` are rejected and also filtered at runtime. Header names are case-insensitive and may not repeat. `headers` remains authoritative; the existing caller `User-Agent` fallback still applies when neither surface sets one. Supported names: `originator`, `x-client-request-id`, `x-codex-app-version`, `user-agent`, `x-codex-window-id`, `x-codex-installation-id`; all other names are refused. |
| `supportsNativeCompactionTrigger?` | `boolean` | `openai-responses` only: declare support for native v2 `compaction_trigger` input items. Defaults off for noncanonical gateways; preserves native compaction rather than requesting a portable text summary. Independent of the v1 endpoint and blob replay capabilities. |
| `supportsNativeCompactEndpoint?` | `boolean` | `openai-responses` only: declare support for native v1 `POST /responses/compact`. Defaults off for custom gateways. Key/local/forward routes use the provider's Responses URL plus `/compact` and its normal transport authentication; OAuth keeps routed compaction and its existing credential lifecycle. Does not enable v2 triggers or grant official OpenAI identity. |
| `decodesNativeCompactionBlobs?` | `boolean` | Declare that this destination can replay native, non-`ocx1:` compaction blobs. Independent of either generation capability; a known serving-provider mismatch still degrades foreign ciphertext. Enable only after testing compact and a subsequent turn on the same backend. |
Expand Down
3 changes: 3 additions & 0 deletions src/lib/provider-client-headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ const BLOCKED_FORWARDED_CLIENT_HEADERS = new Set([

const ALLOWED_FORWARDED_CLIENT_HEADERS = new Set([
"originator", "x-client-request-id", "x-codex-app-version", "user-agent",
// Preserve caller-supplied identifiers only when the provider explicitly opts in.
// Do not admit arbitrary x-codex-* fields or authentication evidence.
"x-codex-window-id", "x-codex-installation-id",
]);
const MAX_FORWARDED_CLIENT_HEADERS = 64;

Expand Down
7 changes: 7 additions & 0 deletions src/server/ws-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,13 @@ export function selectForwardHeaders(
const value = headers.get(name);
if (value !== null) selected.set(name, value);
}
// WS turns rebuild an HTTP request from this snapshot. Keep original client
// identity available for the same per-provider policy used by HTTP requests.
// These are not credentials and do not extend the outbound provider allowlist.
for (const name of ["user-agent", "x-codex-app-version"]) {
const value = headers.get(name);
if (value !== null) selected.set(name, value);
}
if (codexOverride) {
selected.set("authorization", `Bearer ${codexOverride.accessToken}`);
selected.set("chatgpt-account-id", codexOverride.chatgptAccountId);
Expand Down
2 changes: 1 addition & 1 deletion structure/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ A schema-invalid top-level JSON value is repairable only when it is a non-array
| Spend | `spend.root`, `spend.identity`, `spend.pool`, `spend.retentionDays`, top-level `spendPoolAliases` | Durable token ceilings for the spend-reservation ledger. Absent is the default and means observe-only accounting: spend is still journaled and nothing is refused, so observe-only and enforced servers take the same state-directory writer lease. One live process may write one directory; explicit sibling instances need separate `OPENCODEX_HOME` directories. There is no default figure for any scope — the ledger is on by default, so a shipped ceiling would refuse real traffic on upgrade against a number nobody chose. Strictly validated and positive-integer only, because 0 would read as a budget and refuse everything; a malformed section degrades to no ceiling, which is why the write path rejects it and load diagnostics report it. Top-level `spendPoolAliases` is preserved verbatim on load; malformed mappings leave ceilings intact and refuse pool admission, while candidate writes reject them. Current provider IDs automatically own their ordinary salted pool buckets at read time; other historical labels need explicit mappings or remain conservatively unbound. Neither self-bindings nor explicit mappings persist as journal links. Padded destinations and destinations absent from the provider roster are rejected on write and warn/refuse pool admission on load. `src/lib/spend-pool-alias-validation.ts` checks the existing home salt without creating it and rejects a configured provider’s own pool alias mapped to a different provider, including when providers are added later. Live provider and full-config adoption call `reconcileLiveStateStores` in `src/lib/state-store-registrations.ts` to refresh this policy from the active roster; detached config reads do not change it. It stays outside the legacy strict `spend` object. Resolution and application live in `src/lib/spend-reservation-ledger.ts`; see [historical continuity and rollback](transports/responses-spend.md#historical-pool-continuity-and-rollback). |
| Transport | stream mode, timeouts, proxy settings, `websockets`, `emptyCompletionRetry` | `streamMode` persists in config.json; Windows services need a persisted input, and macOS uses it for explicit eager-relay opt-in. Empty-completion replay is an explicit top-level opt-in because its second upstream request may be billable. |
| Canonical ChatGPT upstream transport and Lite history | `providers.openai.upstreamWebsocket`, per-provider `projectResponsesLiteHostedSearchHistory` | Omitted `upstreamWebsocket` uses upstream WebSocket when eligible; explicit `false` selects HTTP/SSE without changing the canonical provider identity. `true` is rejected on the canonical row. This is independent of the client-facing `websockets` setting. `src/config/schema/leaf-validators.ts` validates `projectResponsesLiteHostedSearchHistory` as an optional strict boolean, default off; only runtime `=== true` enables the ordinary lossy policy, still limited by the exact destination and explicit Lite [hosted-search history boundary](providers-and-adapters.md#hosted-search-continuation-binding). `src/providers/model-rename-fields.ts` classifies it as provider-wide policy with no model-ID rewrite role. |
| Provider egress | `providers.<name>.proxy`, `providers.<name>.noProxy` | An absent `proxy` inherits global egress; `"direct"` or `null` forces direct egress; HTTP(S) and SOCKS5(H) URLs select a provider-owned proxy. `noProxy` uses NO_PROXY syntax and sends a matching destination direct across either a provider-owned or inherited global proxy. `src/lib/provider-egress.ts` owns parsing and request-local resolution. `forwardClientHeaders` opts into `originator`, `x-client-request-id`, `x-codex-app-version`, or `user-agent` on Responses inference only; provider headers win and all other names are rejected on load/write and filtered at runtime. |
| Provider egress | `providers.<name>.proxy`, `providers.<name>.noProxy` | An absent `proxy` inherits global egress; `"direct"` or `null` forces direct egress; HTTP(S) and SOCKS5(H) URLs select a provider-owned proxy. `noProxy` uses NO_PROXY syntax and sends a matching destination direct across either a provider-owned or inherited global proxy. `src/lib/provider-egress.ts` owns parsing and request-local resolution. `forwardClientHeaders` opts into `originator`, `x-client-request-id`, `x-codex-app-version`, `user-agent`, `x-codex-window-id`, or `x-codex-installation-id` on Responses inference only; provider headers win and all other names are rejected on load/write and filtered at runtime. |
| Credentials | `apiKeys` | Data-plane only; never admitted to `/api/*`. |
| Lifecycle | `codexAutoStart`, shim/start behavior, resume-history sync, storage cleanup | Startup safety reads these; see [`gui-and-management-api.md`](gui-and-management-api.md). |

Expand Down
3 changes: 3 additions & 0 deletions structure/providers/codex-installation-identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ apply the same identity to HTTP headers and a cloned client-metadata map. The se
`src/server/responses/codex-ws-request.ts`; WebSocket preparation preserves these existence gates.
`src/server/responses/codex-ws-pool.ts` includes immutable outgoing headers and account in its reuse
identity. `src/server/ws-bridge.ts` isolates explicit Pool header overrides as well.
Its inbound handshake snapshot also retains caller-supplied `user-agent` and
`x-codex-app-version`, so reconstructed turns do not lose the original client identity.
Provider egress still applies its own metadata policy; absent fields are not generated.
Native compact in `src/server/responses/compact.ts` projects the body per physical send, so a quota
failover uses the newly selected account ID. Alternate construction reports identity failures as
503 instead of treating them as absence of an eligible account, while recording the first
Expand Down
2 changes: 1 addition & 1 deletion structure/transports/responses.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Responses-compatible streaming output. Every Responses serializer projects unref

The `openai-responses` adapter preserves caller `User-Agent` unless provider headers own it, in either auth mode.
Final route capability normalization removes rejected reasoning effort and toggle fields from the native passthrough body when those fields were parsed into options, before custom gateway-object serialization.
`forwardClientHeaders` additionally opts into `originator`, `x-client-request-id`, `x-codex-app-version`, or `user-agent`.
`forwardClientHeaders` additionally opts into `originator`, `x-client-request-id`, `x-codex-app-version`, `user-agent`, `x-codex-window-id`, or `x-codex-installation-id`. The Codex identifiers are copied only when present and explicitly selected, including native compact fallback; no identifiers are generated and arbitrary `x-codex-*` names remain refused. The inbound WebSocket handshake retains the original `user-agent` and `x-codex-app-version` in the per-connection header snapshot. Reconstructed Responses requests apply the same provider policy as HTTP ingress; retaining a header does not grant a provider permission to receive it. `tests/responses/ws-endpoint.test.ts` covers identity preservation and key-auth credential isolation across this handoff.
Provider headers win case-insensitively; arbitrary names are rejected on load/write and ignored at runtime.
Canonical forward auth retains its separate fixed credential/metadata allowlist; absent caller identity is not invented. Upstream WebSocket reasoning-inclusion headers follow the [native prelude and connection-signal contract](responses-wire-shapes.md#passthrough-sse-stream-shapes-314).

Expand Down
4 changes: 2 additions & 2 deletions tests/responses/native-compaction-gateway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,10 +359,10 @@ describe("native gateway transport", () => {
const provider = { ...gateway, authMode, supportsNativeCompactEndpoint: true,
headers: { Authorization: "Bearer static-fixture" },
// Runtime must reject credential opt-ins even if an unvalidated row reaches dispatch.
forwardClientHeaders: ["X-Codex-App-Version", "originator", "x-client-request-id", "authorization", "chatgpt-account-id", "x-api-key", "cookie", "x-oai-attestation"],
forwardClientHeaders: ["X-Codex-App-Version", "originator", "x-client-request-id", "x-codex-window-id", "x-codex-installation-id", "authorization", "chatgpt-account-id", "x-api-key", "cookie", "x-oai-attestation"],
};
const config = { ...getDefaultConfig(), defaultProvider: "gw", providers: { gw: provider } };
const fingerprint = { "user-agent": "CodexFixture/1.0", "x-codex-app-version": "fixture-version", originator: "fixture-origin", "x-client-request-id": "fixture-request" };
const fingerprint = { "user-agent": "CodexFixture/1.0", "x-codex-app-version": "fixture-version", originator: "fixture-origin", "x-client-request-id": "fixture-request", "x-codex-window-id": "caller-window-fixture", "x-codex-installation-id": "caller-installation-fixture" };
const response = await handleResponsesCompact(compactionRequest(baseCompactionBody(), undefined, {
...fingerprint, authorization: "Bearer caller-fixture", "chatgpt-account-id": "caller-account-fixture",
"x-api-key": "caller-key-fixture", cookie: "caller-cookie-fixture", "x-oai-attestation": "caller-attestation-fixture",
Expand Down
Loading
Loading