Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 47 additions & 26 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down Expand Up @@ -442,6 +464,9 @@ function mapProviderOptions(
export function message(msgs: ModelMessage[], model: Provider.Model, options: Record<string, unknown>) {
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" ||
Expand All @@ -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)
}
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1224,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
Expand Down Expand Up @@ -1256,26 +1290,13 @@ 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
}

if (input.model.providerID === "openrouter") {
result["prompt_cache_key"] = input.sessionID
}
if (input.model.api.npm === "@ai-sdk/gateway") {
result["gateway"] = {
caching: "auto",
}
}

return result
}

Expand Down
115 changes: 115 additions & 0 deletions packages/opencode/test/provider/transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -209,6 +235,70 @@ 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()
})

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({
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()
})
})

Expand Down Expand Up @@ -722,6 +812,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: {
Expand Down Expand Up @@ -3011,6 +3112,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",
Expand Down
Loading