From 060179729e59951c19e4c04c01322a2c54e6d518 Mon Sep 17 00:00:00 2001 From: roboomp Date: Fri, 10 Jul 2026 18:08:40 +0000 Subject: [PATCH 1/2] fix(codex): sent version header with requests Added Codex version headers to turn requests and model discovery so backend-gated models resolve consistently. Fixes #5105 --- packages/ai/CHANGELOG.md | 1 + packages/ai/src/providers/openai-codex-responses.ts | 1 + packages/ai/test/openai-codex-responses-lite.test.ts | 2 ++ packages/catalog/CHANGELOG.md | 4 ++++ packages/catalog/src/discovery/codex.ts | 5 +++-- packages/catalog/src/wire/codex.ts | 1 + packages/catalog/test/codex-discovery.test.ts | 10 +++++++--- 7 files changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index eda640539..327de2002 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -17,6 +17,7 @@ ### Fixed +- Fixed OpenAI Codex turn requests to include the Codex `version` header, matching upstream Codex request metadata for newly gated models. - Fixed xAI SuperGrok multi-account rotation to correctly treat HTTP 403 credit exhaustion and spending limit errors as usage limits, triggering a credential rotation to a sibling account. - Fixed error classification for AWS credential-resolution failures (AwsCredentialsError) to correctly map them as authentication failures. - Fixed OpenAI-compatible chat-completions streams to preserve vLLM-style trailing cached-token usage chunks, ensuring accurate cacheRead and billable input session statistics. diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index 0fa0f720c..391cad23f 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -3774,6 +3774,7 @@ function createCodexHeaders( headers.delete("openai-beta"); headers.set(OPENAI_HEADERS.BETA, betaHeader); headers.set(OPENAI_HEADERS.ORIGINATOR, OPENAI_HEADER_VALUES.ORIGINATOR_CODEX); + headers.set(OPENAI_HEADERS.VERSION, packageJson.version); headers.set("User-Agent", `pi/${packageJson.version} (${os.platform()} ${os.release()}; ${os.arch()})`); if (sessionId) { headers.set(OPENAI_HEADERS.CONVERSATION_ID, sessionId); diff --git a/packages/ai/test/openai-codex-responses-lite.test.ts b/packages/ai/test/openai-codex-responses-lite.test.ts index 992ee26d3..17eab4c90 100644 --- a/packages/ai/test/openai-codex-responses-lite.test.ts +++ b/packages/ai/test/openai-codex-responses-lite.test.ts @@ -14,6 +14,7 @@ import { isOpenAIResponsesProgressEvent } from "@oh-my-pi/pi-ai/providers/openai import type { CodexCompactionRequestContext, Context, FetchImpl, ProviderSessionState } from "@oh-my-pi/pi-ai/types"; import { buildModel } from "@oh-my-pi/pi-catalog/build"; import * as piUtils from "@oh-my-pi/pi-utils"; +import packageJson from "../package.json" with { type: "json" }; import { createCodexModel } from "./helpers"; const TEST_INSTALLATION_ID = "00000000-0000-4000-8000-000000000001"; @@ -625,6 +626,7 @@ describe("openai-codex Responses Lite and client metadata wire format", () => { expect(result.stopReason).toBe("stop"); expect(captured?.headers.get("x-openai-internal-codex-responses-lite")).toBe("true"); + expect(captured?.headers.get("version")).toBe(packageJson.version); expect(captured?.body.instructions).toBeUndefined(); expect(captured?.body.tools).toBeUndefined(); expect((captured?.body.input as Array>)[0]?.type).toBe("additional_tools"); diff --git a/packages/catalog/CHANGELOG.md b/packages/catalog/CHANGELOG.md index bc7d74949..81eb8b5ce 100644 --- a/packages/catalog/CHANGELOG.md +++ b/packages/catalog/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed OpenAI Codex model discovery to include the Codex `version` header alongside the `client_version` query parameter. + ## [16.4.0] - 2026-07-10 ### Breaking Changes diff --git a/packages/catalog/src/discovery/codex.ts b/packages/catalog/src/discovery/codex.ts index 5784d7088..2a06d526e 100644 --- a/packages/catalog/src/discovery/codex.ts +++ b/packages/catalog/src/discovery/codex.ts @@ -86,12 +86,12 @@ export async function fetchCodexModels(options: CodexModelDiscoveryOptions): Pro const fetchFn = discoveryFetch(options.fetchFn); const baseUrl = normalizeBaseUrl(options.baseUrl); const paths = normalizePaths(options.paths); - const headers = buildCodexHeaders(options); const clientVersion = await resolveCodexClientVersion( options.clientVersion, options.registryFetchFn ?? fetchFn, options.signal, ); + const headers = buildCodexHeaders(options, clientVersion); let sawSuccessfulResponse = false; for (const path of paths) { @@ -156,7 +156,7 @@ function buildModelsUrl(baseUrl: string, path: string, clientVersion: string | u return url.toString(); } -function buildCodexHeaders(options: CodexModelDiscoveryOptions): Headers { +function buildCodexHeaders(options: CodexModelDiscoveryOptions, clientVersion: string): Headers { const headers = new Headers(options.headers); headers.set("Authorization", `Bearer ${options.accessToken}`); if (options.accountId && options.accountId.trim().length > 0) { @@ -164,6 +164,7 @@ function buildCodexHeaders(options: CodexModelDiscoveryOptions): Headers { } headers.set(OPENAI_HEADERS.BETA, OPENAI_HEADER_VALUES.BETA_RESPONSES); headers.set(OPENAI_HEADERS.ORIGINATOR, OPENAI_HEADER_VALUES.ORIGINATOR_CODEX); + headers.set(OPENAI_HEADERS.VERSION, clientVersion); headers.set("accept", "application/json"); return headers; } diff --git a/packages/catalog/src/wire/codex.ts b/packages/catalog/src/wire/codex.ts index 329ace70f..48edeb392 100644 --- a/packages/catalog/src/wire/codex.ts +++ b/packages/catalog/src/wire/codex.ts @@ -8,6 +8,7 @@ export const OPENAI_HEADERS = { BETA: "OpenAI-Beta", ACCOUNT_ID: "chatgpt-account-id", ORIGINATOR: "originator", + VERSION: "version", SESSION_ID: "session_id", CONVERSATION_ID: "conversation_id", SCOPED_SESSION_ID: "session-id", diff --git a/packages/catalog/test/codex-discovery.test.ts b/packages/catalog/test/codex-discovery.test.ts index babff9c28..b647ac895 100644 --- a/packages/catalog/test/codex-discovery.test.ts +++ b/packages/catalog/test/codex-discovery.test.ts @@ -11,9 +11,11 @@ import type { ModelSpec } from "@oh-my-pi/pi-catalog/types"; describe("Codex model discovery", () => { it("marks discovered models for provider-native V2 compaction", async () => { + let capturedHeaders: Headers | undefined; const fetchFn: typeof fetch = Object.assign( - async () => - new Response( + async (_input: string | URL | Request, init?: RequestInit) => { + capturedHeaders = new Headers(init?.headers); + return new Response( JSON.stringify({ models: [ { @@ -28,7 +30,8 @@ describe("Codex model discovery", () => { ], }), { headers: { etag: "models-v1" } }, - ), + ); + }, { preconnect() {} }, ); const result = await fetchCodexModels({ @@ -38,6 +41,7 @@ describe("Codex model discovery", () => { fetchFn, }); + expect(capturedHeaders?.get("version")).toBe("0.99.0"); expect(result?.etag).toBe("models-v1"); expect(result?.models).toHaveLength(1); expect(result?.models[0]).toMatchObject({ From 1624d48dd82b9198862b373ecfc4f79aefa85102 Mon Sep 17 00:00:00 2001 From: roboomp Date: Fri, 10 Jul 2026 18:23:06 +0000 Subject: [PATCH 2/2] fix(codex): used codex version for turns Resolved the Codex client version for turn requests with the same catalog resolver used by discovery. Fixes #5105 --- .../ai/src/providers/openai-codex-responses.ts | 14 +++++++++++++- .../ai/test/openai-codex-responses-lite.test.ts | 6 ++++-- packages/catalog/src/discovery/codex.ts | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index 391cad23f..67b578d3c 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -1,5 +1,6 @@ import * as os from "node:os"; import { scheduler } from "node:timers/promises"; +import { resolveCodexClientVersion } from "@oh-my-pi/pi-catalog/discovery/codex"; import { calculateCost } from "@oh-my-pi/pi-catalog/models"; import { CODEX_BASE_URL, @@ -632,6 +633,7 @@ interface CodexRequestContext { baseUrl: string; url: string; requestHeaders: Record; + codexClientVersion: string; transportSessionId?: string; providerSessionState?: CodexProviderSessionState; isolatedTransportState?: CodexProviderSessionState; @@ -1200,6 +1202,7 @@ async function buildCodexRequestContext( const url = resolveCodexResponsesUrl(baseUrl); const promptCacheKey = normalizeOpenAIPromptCacheKey(options?.promptCacheKey ?? options?.sessionId); const transportSessionId = normalizeOpenAIPromptCacheKey(options?.sessionId); + const codexClientVersion = await resolveCodexClientVersion(undefined, options?.fetch ?? fetch, options?.signal); const transformedBody = await buildTransformedCodexRequestBody(model, context, options, promptCacheKey); const requestHeaders = { ...(model.headers ?? {}), ...(options?.headers ?? {}) }; @@ -1275,6 +1278,7 @@ async function buildCodexRequestContext( websocketState, responsesLite, requestMetadata, + codexClientVersion, transformedBody, rawRequestDump, }; @@ -1427,6 +1431,7 @@ async function openCodexWebSocketTransport( requestContext.requestHeaders, requestContext.accountId, requestContext.apiKey, + requestContext.codexClientVersion, requestContext.transportSessionId, "websocket", websocketState, @@ -1527,6 +1532,7 @@ async function openCodexSseTransport( wireBody, state, requestContext.responsesLite, + requestContext.codexClientVersion, requestContext.requestMetadata, requestSetup.requestSignal, requestSetup.firstEventTimeoutMs, @@ -2496,6 +2502,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses" baseUrl: model.baseUrl || CODEX_BASE_URL, url: "", requestHeaders: {}, + codexClientVersion: packageJson.version, responsesLite: options?.responsesLite === true, transformedBody: { model: model.id }, rawRequestDump: { @@ -2559,6 +2566,7 @@ export async function prewarmOpenAICodexResponses( transportSessionId ?? crypto.randomUUID(), providerSessionState, ); + const codexClientVersion = await resolveCodexClientVersion(undefined, fetch, options?.signal); const requestIdentity = createCodexCompatibilityIdentity(metadataSession); const headers = logger.time( "prewarmCodex:createHeaders", @@ -2566,6 +2574,7 @@ export async function prewarmOpenAICodexResponses( { ...(model.headers ?? {}), ...(options?.headers ?? {}) }, accountId, apiKey, + codexClientVersion, promptCacheKey, "websocket", state, @@ -3685,6 +3694,7 @@ async function openCodexSseEventStream( body: RequestBody, state: CodexWebSocketSessionState | undefined, responsesLite: boolean, + codexClientVersion: string, requestMetadata: CodexRequestMetadata | undefined, signal: AbortSignal | undefined, firstEventTimeoutMs: number | undefined, @@ -3695,6 +3705,7 @@ async function openCodexSseEventStream( requestHeaders, accountId, apiKey, + codexClientVersion, sessionId, "sse", state, @@ -3756,6 +3767,7 @@ function createCodexHeaders( initHeaders: Record | undefined, accountId: string | undefined, accessToken: string, + codexClientVersion: string, sessionId?: string, transport: CodexTransport = "sse", state?: CodexWebSocketSessionState, @@ -3774,7 +3786,7 @@ function createCodexHeaders( headers.delete("openai-beta"); headers.set(OPENAI_HEADERS.BETA, betaHeader); headers.set(OPENAI_HEADERS.ORIGINATOR, OPENAI_HEADER_VALUES.ORIGINATOR_CODEX); - headers.set(OPENAI_HEADERS.VERSION, packageJson.version); + headers.set(OPENAI_HEADERS.VERSION, codexClientVersion); headers.set("User-Agent", `pi/${packageJson.version} (${os.platform()} ${os.release()}; ${os.arch()})`); if (sessionId) { headers.set(OPENAI_HEADERS.CONVERSATION_ID, sessionId); diff --git a/packages/ai/test/openai-codex-responses-lite.test.ts b/packages/ai/test/openai-codex-responses-lite.test.ts index 17eab4c90..3b77febe4 100644 --- a/packages/ai/test/openai-codex-responses-lite.test.ts +++ b/packages/ai/test/openai-codex-responses-lite.test.ts @@ -14,7 +14,6 @@ import { isOpenAIResponsesProgressEvent } from "@oh-my-pi/pi-ai/providers/openai import type { CodexCompactionRequestContext, Context, FetchImpl, ProviderSessionState } from "@oh-my-pi/pi-ai/types"; import { buildModel } from "@oh-my-pi/pi-catalog/build"; import * as piUtils from "@oh-my-pi/pi-utils"; -import packageJson from "../package.json" with { type: "json" }; import { createCodexModel } from "./helpers"; const TEST_INSTALLATION_ID = "00000000-0000-4000-8000-000000000001"; @@ -101,6 +100,9 @@ function createCodexFetchMock(sse: string, onRequest: (captured: CapturedCodexRe if (url === "https://api.github.com/repos/openai/codex/releases/latest") { return new Response(JSON.stringify({ tag_name: "rust-v0.0.0" }), { status: 200 }); } + if (url === "https://registry.npmjs.org/@openai%2Fcodex/latest") { + return new Response(JSON.stringify({ version: "0.144.1" }), { status: 200 }); + } if (url.startsWith("https://raw.githubusercontent.com/openai/codex/")) { return new Response("PROMPT", { status: 200, headers: { etag: '"etag"' } }); } @@ -626,7 +628,7 @@ describe("openai-codex Responses Lite and client metadata wire format", () => { expect(result.stopReason).toBe("stop"); expect(captured?.headers.get("x-openai-internal-codex-responses-lite")).toBe("true"); - expect(captured?.headers.get("version")).toBe(packageJson.version); + expect(captured?.headers.get("version")).toBe("0.144.1"); expect(captured?.body.instructions).toBeUndefined(); expect(captured?.body.tools).toBeUndefined(); expect((captured?.body.input as Array>)[0]?.type).toBe("additional_tools"); diff --git a/packages/catalog/src/discovery/codex.ts b/packages/catalog/src/discovery/codex.ts index 2a06d526e..118e37fde 100644 --- a/packages/catalog/src/discovery/codex.ts +++ b/packages/catalog/src/discovery/codex.ts @@ -169,7 +169,7 @@ function buildCodexHeaders(options: CodexModelDiscoveryOptions, clientVersion: s return headers; } -async function resolveCodexClientVersion( +export async function resolveCodexClientVersion( clientVersion: string | undefined, fetchFn: FetchImpl, signal: AbortSignal | undefined,