Merge remote-tracking branch 'origin/farm/10fa7941/fix-openai-codex-luna-model'

This commit is contained in:
can1357
2026-07-11 00:10:21 +02:00
7 changed files with 33 additions and 6 deletions
+1
View File
@@ -23,6 +23,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.
@@ -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<string, string>;
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<string, string> | undefined,
accountId: string | undefined,
accessToken: string,
codexClientVersion: string,
sessionId?: string,
transport: CodexTransport = "sse",
state?: CodexWebSocketSessionState,
@@ -3774,6 +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, codexClientVersion);
headers.set("User-Agent", `pi/${packageJson.version} (${os.platform()} ${os.release()}; ${os.arch()})`);
if (sessionId) {
headers.set(OPENAI_HEADERS.CONVERSATION_ID, sessionId);
@@ -100,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"' } });
}
@@ -666,6 +669,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("0.144.1");
expect(captured?.body.reasoning).toEqual({ context: "all_turns" });
expect(captured?.body.instructions).toBeUndefined();
expect(captured?.body.tools).toBeUndefined();
+3
View File
@@ -19,6 +19,9 @@
### Removed
- Removed the generated GPT-5.6 pro-reasoning aliases (`gpt-5.6-{luna,sol,terra}-pro`) from the `openai-codex` subscription provider — pro reasoning is not offered on subscriptions; the `openai` API-key aliases remain
### Fixed
- Fixed OpenAI Codex model discovery to include the Codex `version` header alongside the `client_version` query parameter.
## [16.4.0] - 2026-07-10
+4 -3
View File
@@ -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,11 +164,12 @@ 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;
}
async function resolveCodexClientVersion(
export async function resolveCodexClientVersion(
clientVersion: string | undefined,
fetchFn: FetchImpl,
signal: AbortSignal | undefined,
+1
View File
@@ -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",
@@ -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({