From 41213d87896ba0113217ebd5c5f8698e3c8b3c6d Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 22 Jul 2026 21:05:13 -0500 Subject: [PATCH 1/3] fix(provider): select prompt cache keys by SDK Route cache affinity by npm package instead of logical provider ID so OpenAI-compatible endpoints, Azure opt-out, gateway auto-cache, and Anthropic automatic caching behave correctly. --- packages/opencode/src/provider/transform.ts | 89 ++++++++++----- packages/opencode/src/session/llm/request.ts | 2 +- .../opencode/test/provider/transform.test.ts | 107 ++++++++++++++++++ 3 files changed, 169 insertions(+), 29 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 25dc78bdd0b8..38c792e94882 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -58,6 +58,28 @@ function sdkKey(npm: string): string | undefined { return "vertex" case "@ai-sdk/google": return "google" + case "@ai-sdk/alibaba": + return "alibaba" + case "@ai-sdk/cerebras": + return "cerebras" + case "@ai-sdk/cohere": + return "cohere" + case "@ai-sdk/deepinfra": + return "deepinfra" + case "@ai-sdk/groq": + return "groq" + case "@ai-sdk/mistral": + return "mistral" + case "@ai-sdk/perplexity": + return "perplexity" + case "@ai-sdk/togetherai": + return "togetherai" + case "@ai-sdk/vercel": + return "vercel" + case "@ai-sdk/xai": + return "xai" + case "venice-ai-sdk-provider": + return "venice" case "@ai-sdk/gateway": return "gateway" case "@openrouter/ai-sdk-provider": @@ -442,6 +464,9 @@ function mapProviderOptions( export function message(msgs: ModelMessage[], model: Provider.Model, options: Record) { msgs = unsupportedParts(msgs, model) msgs = normalizeMessages(msgs, model, options) + const usesAnthropicAutomaticCaching = + options.cacheControl !== undefined && + (model.api.npm === "@ai-sdk/anthropic" || model.api.npm === "@ai-sdk/google-vertex/anthropic") if ( (model.providerID === "anthropic" || model.providerID === "google-vertex-anthropic" || @@ -451,7 +476,8 @@ export function message(msgs: ModelMessage[], model: Provider.Model, options: Re model.id.includes("claude") || model.api.npm === "@ai-sdk/anthropic" || model.api.npm === "@ai-sdk/alibaba") && - model.api.npm !== "@ai-sdk/gateway" + model.api.npm !== "@ai-sdk/gateway" && + !usesAnthropicAutomaticCaching ) { msgs = applyCaching(msgs, model) } @@ -1115,7 +1141,7 @@ export function options(input: { sessionID: string providerOptions?: Record }): Record { - const result: Record = {} + const result: Record = cachingOptions(input) if ( input.model.api.npm === "@ai-sdk/google-vertex/anthropic" || @@ -1137,7 +1163,6 @@ export function options(input: { if (input.model.api.npm === "@ai-sdk/azure") { result["store"] = false - result["promptCacheKey"] = input.sessionID } if (input.model.api.npm === "@openrouter/ai-sdk-provider" || input.model.api.npm === "@llmgateway/ai-sdk-provider") { @@ -1166,16 +1191,6 @@ export function options(input: { } } - if ( - input.providerOptions?.setCacheKey !== false && - (input.model.providerID === "openai" || - input.model.api.npm === "@ai-sdk/openai" || - input.model.api.npm === "@ai-sdk/xai" || - input.providerOptions?.setCacheKey) - ) { - result["promptCacheKey"] = input.sessionID - } - if (input.model.providerID === "meta" && input.model.api.npm === "@ai-sdk/openai") { result["reasoningSummary"] = "auto" result["include"] = INCLUDE_ENCRYPTED_REASONING @@ -1256,38 +1271,56 @@ export function options(input: { result["textVerbosity"] = "low" } - if (input.model.providerID.startsWith("opencode")) { + if (input.model.providerID.startsWith("opencode") && input.providerOptions?.setCacheKey !== false) { result["promptCacheKey"] = input.sessionID result["include"] = INCLUDE_ENCRYPTED_REASONING result["reasoningSummary"] = "auto" } } - if (input.model.providerID === "venice") { - result["promptCacheKey"] = input.sessionID + return result +} + +function cachingOptions(input: { + model: Provider.Model + sessionID: string + providerOptions?: Record +}): Record { + if (input.providerOptions?.setCacheKey === false) { + if (input.model.api.npm === "@ai-sdk/gateway") return { gateway: { caching: "auto" } } + return {} } - if (input.model.providerID === "openrouter") { - result["prompt_cache_key"] = input.sessionID + if (input.model.api.npm === "@ai-sdk/deepinfra" || input.model.api.npm === "@ai-sdk/cerebras") { + return { prompt_cache_key: input.sessionID } } - if (input.model.api.npm === "@ai-sdk/gateway") { - result["gateway"] = { - caching: "auto", - } + + if ( + input.model.api.npm === "@ai-sdk/openai" || + (input.model.providerID === "openai" && input.model.api.npm !== "@ai-sdk/openai-compatible") || + input.model.api.npm === "@ai-sdk/azure" || + input.model.api.npm === "@ai-sdk/xai" || + input.model.api.npm === "@ai-sdk/mistral" || + input.model.api.npm === "venice-ai-sdk-provider" || + input.providerOptions?.setCacheKey === true + ) { + return { promptCacheKey: input.sessionID } } - return result + if (input.model.api.npm === "@ai-sdk/gateway") return { gateway: { caching: "auto" } } + return {} } -export function smallOptions(model: Provider.Model) { +export function smallOptions(model: Provider.Model, sessionID?: string, providerOptions?: Record) { const small = Object.values(model.variants ?? {})[0] ?? {} + const caching = sessionID ? cachingOptions({ model, sessionID, providerOptions }) : {} if ( model.providerID === "openai" || model.api.npm === "@ai-sdk/openai" || model.api.npm === "@ai-sdk/github-copilot" || model.api.npm === "@ai-sdk/xai" ) { - const base = { store: false } + const base = { ...caching, store: false } return mergeDeep(base, small) } if (model.providerID === "openrouter" || model.providerID === "llmgateway") { @@ -1297,11 +1330,11 @@ export function smallOptions(model: Provider.Model) { } if (model.providerID === "venice") { - if (Object.keys(small).length > 0) return small - return { veniceParameters: { disableThinking: true } } + if (Object.keys(small).length > 0) return mergeDeep(caching, small) + return mergeDeep(caching, { veniceParameters: { disableThinking: true } }) } - return small + return mergeDeep(caching, small) } // Maps model ID prefix to provider slug used in providerOptions. diff --git a/packages/opencode/src/session/llm/request.ts b/packages/opencode/src/session/llm/request.ts index 4f93411107df..ad737e059cb8 100644 --- a/packages/opencode/src/session/llm/request.ts +++ b/packages/opencode/src/session/llm/request.ts @@ -82,7 +82,7 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre ? input.model.variants[input.user.model.variant] : {} const base = input.small - ? ProviderTransform.smallOptions(input.model) + ? ProviderTransform.smallOptions(input.model, input.sessionID, input.provider.options) : ProviderTransform.options({ model: input.model, sessionID: input.sessionID, diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 68da820ee62c..e1ba7cf80f80 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -88,6 +88,32 @@ describe("ProviderTransform.options - setCacheKey", () => { expect(result.promptCacheKey).toBe(sessionID) }) + test("should set promptCacheKey for the OpenAI SDK regardless of provider ID", () => { + const result = ProviderTransform.options({ + model: { + ...mockModel, + providerID: "custom-openai", + api: { id: "gpt-5", url: "https://example.com", npm: "@ai-sdk/openai" }, + }, + sessionID, + providerOptions: {}, + }) + expect(result.promptCacheKey).toBe(sessionID) + }) + + test("should not set promptCacheKey for the OpenAI-compatible SDK by provider name", () => { + const result = ProviderTransform.options({ + model: { + ...mockModel, + providerID: "openai", + api: { id: "gpt-5", url: "https://example.com", npm: "@ai-sdk/openai-compatible" }, + }, + sessionID, + providerOptions: {}, + }) + expect(result.promptCacheKey).toBeUndefined() + }) + test("should not set promptCacheKey for openai when explicitly disabled", () => { const openaiModel = { ...mockModel, @@ -209,6 +235,55 @@ describe("ProviderTransform.options - setCacheKey", () => { providerOptions: {}, }) expect(result.store).toBe(false) + expect(result.promptCacheKey).toBe(sessionID) + }) + + test("should disable the Azure cache key without disabling store=false", () => { + const result = ProviderTransform.options({ + model: { + ...mockModel, + providerID: "azure", + api: { id: "gpt-5", url: "https://azure.com", npm: "@ai-sdk/azure" }, + }, + sessionID, + providerOptions: { setCacheKey: false }, + }) + expect(result.store).toBe(false) + expect(result.promptCacheKey).toBeUndefined() + }) + + for (const npm of ["@ai-sdk/deepinfra", "@ai-sdk/cerebras"]) { + test(`should set the snake-case cache key for ${npm}`, () => { + const result = ProviderTransform.options({ + model: { ...mockModel, providerID: "custom", api: { ...mockModel.api, npm } }, + sessionID, + providerOptions: {}, + }) + expect(result.prompt_cache_key).toBe(sessionID) + expect(result.promptCacheKey).toBeUndefined() + }) + } + + test("should set promptCacheKey for the Mistral SDK", () => { + const result = ProviderTransform.options({ + model: { ...mockModel, providerID: "custom", api: { ...mockModel.api, npm: "@ai-sdk/mistral" } }, + sessionID, + providerOptions: {}, + }) + expect(result.promptCacheKey).toBe(sessionID) + }) + + test("should not send an undocumented OpenRouter prompt_cache_key", () => { + const result = ProviderTransform.options({ + model: { + ...mockModel, + providerID: "openrouter", + api: { ...mockModel.api, npm: "@openrouter/ai-sdk-provider" }, + }, + sessionID, + providerOptions: {}, + }) + expect(result.prompt_cache_key).toBeUndefined() }) }) @@ -722,6 +797,17 @@ describe("ProviderTransform.providerOptions", () => { }) }) + test("uses canonical sdk key for custom xAI models", () => { + const model = createModel({ + providerID: "my-xai", + api: { id: "grok-4", url: "https://api.x.ai", npm: "@ai-sdk/xai" }, + }) + + expect(ProviderTransform.providerOptions(model, { promptCacheKey: "session" })).toEqual({ + xai: { promptCacheKey: "session" }, + }) + }) + test("forces reasoning for explicit effort even when model is not marked reasoning-capable", () => { const model = createModel({ capabilities: { @@ -3011,6 +3097,20 @@ describe("ProviderTransform.message - cache control on gateway", () => { }) }) + test("does not add explicit breakpoints when Anthropic automatic caching is enabled", () => { + const model = createModel({ + providerID: "anthropic", + api: { id: "claude-sonnet-4", url: "https://api.anthropic.com", npm: "@ai-sdk/anthropic" }, + }) + const msgs = [ + { role: "system", content: "You are a helpful assistant" }, + { role: "user", content: "Hello" }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, { cacheControl: { type: "ephemeral" } }) as any[] + expect(result.every((message) => message.providerOptions === undefined)).toBe(true) + }) + test("google-vertex-anthropic applies cache control", () => { const model = createModel({ providerID: "google-vertex-anthropic", @@ -5116,6 +5216,13 @@ describe("ProviderTransform.smallOptions - gpt-5 chat/search", () => { expect(ProviderTransform.smallOptions(createModel(testCase.id))).toEqual(testCase.options) }) } + + test("includes the OpenAI cache key in small requests", () => { + expect(ProviderTransform.smallOptions(createModel("gpt-5-chat-latest"), "session-123")).toEqual({ + store: false, + promptCacheKey: "session-123", + }) + }) }) test("ProviderTransform.smallOptions preserves the weakest OpenRouter reasoning effort", () => { From 4bae138cbc2b98f6d5167827f9bd5599603fcd35 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 22 Jul 2026 21:42:26 -0500 Subject: [PATCH 2/3] fix(provider): simplify prompt cache key selection Inline cache-key selection in options() by SDK package and drop the smallOptions caching path. --- packages/opencode/src/provider/transform.ts | 56 ++++++++----------- packages/opencode/src/session/llm/request.ts | 2 +- .../opencode/test/provider/transform.test.ts | 7 --- 3 files changed, 23 insertions(+), 42 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 38c792e94882..6505046a0d36 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -1141,7 +1141,7 @@ export function options(input: { sessionID: string providerOptions?: Record }): Record { - const result: Record = cachingOptions(input) + const result: Record = {} if ( input.model.api.npm === "@ai-sdk/google-vertex/anthropic" || @@ -1278,49 +1278,37 @@ export function options(input: { } } - return result -} - -function cachingOptions(input: { - model: Provider.Model - sessionID: string - providerOptions?: Record -}): Record { - if (input.providerOptions?.setCacheKey === false) { - if (input.model.api.npm === "@ai-sdk/gateway") return { gateway: { caching: "auto" } } - return {} - } - - if (input.model.api.npm === "@ai-sdk/deepinfra" || input.model.api.npm === "@ai-sdk/cerebras") { - return { prompt_cache_key: input.sessionID } + if (input.providerOptions?.setCacheKey !== false) { + if (input.model.api.npm === "@ai-sdk/deepinfra" || input.model.api.npm === "@ai-sdk/cerebras") { + result["prompt_cache_key"] = input.sessionID + } else if ( + input.model.api.npm === "@ai-sdk/openai" || + input.model.api.npm === "@ai-sdk/azure" || + input.model.api.npm === "@ai-sdk/xai" || + input.model.api.npm === "@ai-sdk/mistral" || + input.model.api.npm === "venice-ai-sdk-provider" || + input.providerOptions?.setCacheKey === true + ) { + result["promptCacheKey"] = input.sessionID + } } - if ( - input.model.api.npm === "@ai-sdk/openai" || - (input.model.providerID === "openai" && input.model.api.npm !== "@ai-sdk/openai-compatible") || - input.model.api.npm === "@ai-sdk/azure" || - input.model.api.npm === "@ai-sdk/xai" || - input.model.api.npm === "@ai-sdk/mistral" || - input.model.api.npm === "venice-ai-sdk-provider" || - input.providerOptions?.setCacheKey === true - ) { - return { promptCacheKey: input.sessionID } + if (input.model.api.npm === "@ai-sdk/gateway") { + result["gateway"] = { caching: "auto" } } - if (input.model.api.npm === "@ai-sdk/gateway") return { gateway: { caching: "auto" } } - return {} + return result } -export function smallOptions(model: Provider.Model, sessionID?: string, providerOptions?: Record) { +export function smallOptions(model: Provider.Model) { const small = Object.values(model.variants ?? {})[0] ?? {} - const caching = sessionID ? cachingOptions({ model, sessionID, providerOptions }) : {} if ( model.providerID === "openai" || model.api.npm === "@ai-sdk/openai" || model.api.npm === "@ai-sdk/github-copilot" || model.api.npm === "@ai-sdk/xai" ) { - const base = { ...caching, store: false } + const base = { store: false } return mergeDeep(base, small) } if (model.providerID === "openrouter" || model.providerID === "llmgateway") { @@ -1330,11 +1318,11 @@ export function smallOptions(model: Provider.Model, sessionID?: string, provider } if (model.providerID === "venice") { - if (Object.keys(small).length > 0) return mergeDeep(caching, small) - return mergeDeep(caching, { veniceParameters: { disableThinking: true } }) + if (Object.keys(small).length > 0) return small + return { veniceParameters: { disableThinking: true } } } - return mergeDeep(caching, small) + return small } // Maps model ID prefix to provider slug used in providerOptions. diff --git a/packages/opencode/src/session/llm/request.ts b/packages/opencode/src/session/llm/request.ts index ad737e059cb8..4f93411107df 100644 --- a/packages/opencode/src/session/llm/request.ts +++ b/packages/opencode/src/session/llm/request.ts @@ -82,7 +82,7 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre ? input.model.variants[input.user.model.variant] : {} const base = input.small - ? ProviderTransform.smallOptions(input.model, input.sessionID, input.provider.options) + ? ProviderTransform.smallOptions(input.model) : ProviderTransform.options({ model: input.model, sessionID: input.sessionID, diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index e1ba7cf80f80..eff584c125d1 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -5216,13 +5216,6 @@ describe("ProviderTransform.smallOptions - gpt-5 chat/search", () => { expect(ProviderTransform.smallOptions(createModel(testCase.id))).toEqual(testCase.options) }) } - - test("includes the OpenAI cache key in small requests", () => { - expect(ProviderTransform.smallOptions(createModel("gpt-5-chat-latest"), "session-123")).toEqual({ - store: false, - promptCacheKey: "session-123", - }) - }) }) test("ProviderTransform.smallOptions preserves the weakest OpenRouter reasoning effort", () => { From 6af958a34be55b9a2b7f39d3d43b1c2f19d71bec Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 22 Jul 2026 22:24:45 -0500 Subject: [PATCH 3/3] fix(provider): apply cache keys before azure early return Azure gpt-5.5 returned before promptCacheKey was set after the cache block moved to the end of options(). --- packages/opencode/src/provider/transform.ts | 38 +++++++++---------- .../opencode/test/provider/transform.test.ts | 15 ++++++++ 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 6505046a0d36..81759160bfeb 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -1239,6 +1239,25 @@ export function options(input: { result["enable_thinking"] = true } + if (input.providerOptions?.setCacheKey !== false) { + if (input.model.api.npm === "@ai-sdk/deepinfra" || input.model.api.npm === "@ai-sdk/cerebras") { + result["prompt_cache_key"] = input.sessionID + } else if ( + input.model.api.npm === "@ai-sdk/openai" || + input.model.api.npm === "@ai-sdk/azure" || + input.model.api.npm === "@ai-sdk/xai" || + input.model.api.npm === "@ai-sdk/mistral" || + input.model.api.npm === "venice-ai-sdk-provider" || + input.providerOptions?.setCacheKey === true + ) { + result["promptCacheKey"] = input.sessionID + } + } + + if (input.model.api.npm === "@ai-sdk/gateway") { + result["gateway"] = { caching: "auto" } + } + if (input.model.api.npm === "@ai-sdk/azure" && input.model.api.id.includes("gpt-5.5")) { result["reasoningSummary"] = "auto" return result @@ -1278,25 +1297,6 @@ export function options(input: { } } - if (input.providerOptions?.setCacheKey !== false) { - if (input.model.api.npm === "@ai-sdk/deepinfra" || input.model.api.npm === "@ai-sdk/cerebras") { - result["prompt_cache_key"] = input.sessionID - } else if ( - input.model.api.npm === "@ai-sdk/openai" || - input.model.api.npm === "@ai-sdk/azure" || - input.model.api.npm === "@ai-sdk/xai" || - input.model.api.npm === "@ai-sdk/mistral" || - input.model.api.npm === "venice-ai-sdk-provider" || - input.providerOptions?.setCacheKey === true - ) { - result["promptCacheKey"] = input.sessionID - } - } - - if (input.model.api.npm === "@ai-sdk/gateway") { - result["gateway"] = { caching: "auto" } - } - return result } diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index eff584c125d1..ef2b275035e1 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -252,6 +252,21 @@ describe("ProviderTransform.options - setCacheKey", () => { expect(result.promptCacheKey).toBeUndefined() }) + test("should keep the Azure cache key for gpt-5.5 early return", () => { + const result = ProviderTransform.options({ + model: { + ...mockModel, + providerID: "azure", + api: { id: "gpt-5.5", url: "https://azure.com", npm: "@ai-sdk/azure" }, + }, + sessionID, + providerOptions: {}, + }) + expect(result.store).toBe(false) + expect(result.reasoningSummary).toBe("auto") + expect(result.promptCacheKey).toBe(sessionID) + }) + for (const npm of ["@ai-sdk/deepinfra", "@ai-sdk/cerebras"]) { test(`should set the snake-case cache key for ${npm}`, () => { const result = ProviderTransform.options({