diff --git a/package.json b/package.json index f4e8bbd5c9..178d6a9c7d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.24.2", + "version": "2.25.0", "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code", "type": "module", "main": "./bin/package-main.mjs", diff --git a/src/adapters/openai-responses.ts b/src/adapters/openai-responses.ts index 27268a481b..8ede2e3531 100644 --- a/src/adapters/openai-responses.ts +++ b/src/adapters/openai-responses.ts @@ -815,6 +815,28 @@ function stripUnsupportedForwardParams(body: unknown): unknown { return rest; } +/** + * The ChatGPT codex backend rejects `prompt_cache_retention` with + * `{"detail":"Unsupported parameter: prompt_cache_retention"}` on gpt-5.6 + * models (gpt-5.6-luna / gpt-5.6-sol), aborting the whole agent turn mid-run. + * The parameter is emitted client-side by some Codex App builds - it does not + * exist anywhere in codex-rs - and request bodies are forwarded opaquely, so + * one bad field kills the turn (#2092). + * + * Other models keep the field: the backend's cache handling is account-level + * and has provably varied by deployment (one accepted "24h" and echoed it + * back), so stripping globally would silently drop a parameter a deployment + * honors. Model-scoped, matching the report's invariant: never send it to + * gpt-5.6. + */ +function stripPromptCacheRetentionForGpt56(body: unknown, modelId: string | undefined): unknown { + if (modelId === undefined || !modelId.startsWith("gpt-5.6")) return body; + if (!isPlainObject(body)) return body; + if (!Object.prototype.hasOwnProperty.call(body, "prompt_cache_retention")) return body; + const { prompt_cache_retention: _pcr, ...rest } = body; + return rest; +} + const IMAGE_GEN_NAMESPACE = "image_gen"; const HOSTED_IMAGE_GENERATION_TOOL = "image_generation"; const IMAGE_GEN_DOTTED_PREFIX = `${IMAGE_GEN_NAMESPACE}.`; @@ -1382,6 +1404,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): } if (forward) { outBody = stripUnsupportedForwardParams(outBody); + outBody = stripPromptCacheRetentionForGpt56(outBody, parsed.modelId); } else { outBody = preferConfiguredHostedTools( outBody, diff --git a/tests/openai-responses-passthrough.test.ts b/tests/openai-responses-passthrough.test.ts index c2e9d38a71..504c754a99 100644 --- a/tests/openai-responses-passthrough.test.ts +++ b/tests/openai-responses-passthrough.test.ts @@ -1982,6 +1982,49 @@ describe("OpenAI Responses forward-mode unsupported param stripping", () => { expect(body.model).toBe("gpt-5.6-sol"); }); + + + test("forward mode strips prompt_cache_retention for gpt-5.6 models (#2092)", () => { + const adapter = createResponsesPassthroughAdapter(provider); + for (const modelId of ["gpt-5.6-luna", "gpt-5.6-sol"]) { + const request = adapter.buildRequest({ + modelId, + context: { messages: [] }, + stream: true, + options: {}, + _rawBody: { + model: modelId, + input: [{ role: "user", content: [{ type: "input_text", text: "ping" }] }], + stream: true, + store: false, + prompt_cache_retention: "24h", + }, + }, meta); + const body = JSON.parse(request.body) as Record; + expect(body).not.toHaveProperty("prompt_cache_retention"); + expect(body.model).toBe(modelId); + } + }); + + test("forward mode keeps prompt_cache_retention for non-gpt-5.6 models (#2092)", () => { + const adapter = createResponsesPassthroughAdapter(provider); + const request = adapter.buildRequest({ + modelId: "gpt-5.5", + context: { messages: [] }, + stream: true, + options: {}, + _rawBody: { + model: "gpt-5.5", + input: "hi", + prompt_cache_retention: "24h", + }, + }, meta); + const body = JSON.parse(request.body) as Record; + // The backend's cache handling varies by deployment and has accepted this + // field before; only gpt-5.6 provably rejects it, so it stays. + expect(body.prompt_cache_retention).toBe("24h"); + }); + test("forward mode is a no-op when neither field is present", () => { const adapter = createResponsesPassthroughAdapter(provider); const { max_output_tokens: _m, metadata: _d, ...codexBody } = rawBody;