diff --git a/src/adapters/openai-responses.ts b/src/adapters/openai-responses.ts index a8bbab8bfd..1bf407ac6e 100644 --- a/src/adapters/openai-responses.ts +++ b/src/adapters/openai-responses.ts @@ -185,6 +185,20 @@ function stripUnsupportedReasoningParams(body: unknown): unknown { return { ...body, reasoning: Object.keys(rest).length > 0 ? rest : undefined }; } +/** + * GPT-5.6 replaced the legacy 24-hour retention field with `prompt_cache_options.ttl`. + * Do not translate `24h` to the new field: GPT-5.6 currently accepts a different TTL contract, + * and implicit caching remains available when the caller did not send the replacement options. + */ +function stripDeprecatedPromptCacheRetention(body: unknown, modelId: unknown): unknown { + if (!isPlainObject(body)) return body; + if (typeof modelId !== "string") return body; + if (modelId !== "gpt-5.6" && !modelId.startsWith("gpt-5.6-")) return body; + if (!Object.hasOwn(body, "prompt_cache_retention")) return body; + const { prompt_cache_retention: _retention, ...rest } = body; + return rest; +} + /** * A false model capability prevents Codex from emitting summary fields after the catalog refresh. * Strip them here as well so an already-running client with a stale catalog cannot keep sending an @@ -1491,6 +1505,9 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): } if (forward) { outBody = stripUnsupportedForwardParams(outBody); + if (isCanonicalOpenAiForwardProvider(provider)) { + outBody = stripDeprecatedPromptCacheRetention(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..21bab9a07a 100644 --- a/tests/openai-responses-passthrough.test.ts +++ b/tests/openai-responses-passthrough.test.ts @@ -822,6 +822,57 @@ describe("OpenAI Responses passthrough sanitization", () => { expect(body.prompt_cache_retention).toBe("24h"); }); + test.each([ + "gpt-5.6", + "gpt-5.6-sol", + "gpt-5.6-terra", + "gpt-5.6-luna", + ])("drops deprecated prompt_cache_retention for %s without inventing replacement options", modelId => { + const adapter = createResponsesPassthroughAdapter(provider); + const request = adapter.buildRequest({ + modelId, + context: { messages: [] }, + stream: true, + options: {}, + _rawBody: { + model: modelId, + input: "hi", + prompt_cache_retention: "24h", + }, + }, { headers: new Headers({ authorization: "Bearer token" }) }); + const body = JSON.parse(request.body) as { + prompt_cache_retention?: string; + prompt_cache_options?: unknown; + }; + + expect(body.prompt_cache_retention).toBeUndefined(); + expect(body.prompt_cache_options).toBeUndefined(); + }); + + test("preserves caller prompt_cache_options while dropping GPT-5.6 legacy retention", () => { + const adapter = createResponsesPassthroughAdapter(provider); + const promptCacheOptions = { mode: "explicit", ttl: "30m" }; + const request = adapter.buildRequest({ + modelId: "gpt-5.6-sol", + context: { messages: [] }, + stream: true, + options: {}, + _rawBody: { + model: "gpt-5.6-sol", + input: "hi", + prompt_cache_retention: "24h", + prompt_cache_options: promptCacheOptions, + }, + }, { headers: new Headers({ authorization: "Bearer token" }) }); + const body = JSON.parse(request.body) as { + prompt_cache_retention?: string; + prompt_cache_options?: unknown; + }; + + expect(body.prompt_cache_retention).toBeUndefined(); + expect(body.prompt_cache_options).toEqual(promptCacheOptions); + }); + const expandedRawBody = { model: "gpt-5.5", previous_response_id: "resp_1", @@ -2017,6 +2068,52 @@ describe("OpenAI Responses forward-mode unsupported param stripping", () => { expect(body.max_output_tokens).toBe(32000); expect(body.metadata).toEqual({ user_id: "u-1" }); }); + + test("key-auth custom endpoints preserve GPT-5.6 prompt_cache_retention", () => { + const adapter = createResponsesPassthroughAdapter({ + adapter: "openai-responses", + baseUrl: "https://api.openai.example/v1", + authMode: "key", + apiKey: "sk-test", + }); + const request = adapter.buildRequest({ + modelId: "gpt-5.6-sol", + context: { messages: [] }, + stream: true, + options: {}, + _rawBody: { + model: "gpt-5.6-sol", + input: "hi", + prompt_cache_retention: "24h", + }, + }, { headers: new Headers() }); + const body = JSON.parse(request.body) as { prompt_cache_retention?: string }; + + expect(body.prompt_cache_retention).toBe("24h"); + }); + + test("noncanonical forward endpoints preserve GPT-5.6 prompt_cache_retention", () => { + const adapter = createResponsesPassthroughAdapter({ + adapter: "openai-responses", + baseUrl: "https://provider.example/v1", + authMode: "forward", + headers: { authorization: "Bearer provider-static" }, + }); + const request = adapter.buildRequest({ + modelId: "gpt-5.6-sol", + context: { messages: [] }, + stream: true, + options: {}, + _rawBody: { + model: "gpt-5.6-sol", + input: "hi", + prompt_cache_retention: "24h", + }, + }, { headers: new Headers() }); + const body = JSON.parse(request.body) as { prompt_cache_retention?: string }; + + expect(body.prompt_cache_retention).toBe("24h"); + }); }); describe("openaiResponsesUrl", () => {