diff --git a/.changeset/openai-explicit-prompt-cache-breakpoints.md b/.changeset/openai-explicit-prompt-cache-breakpoints.md new file mode 100644 index 00000000000..4ba3d97b450 --- /dev/null +++ b/.changeset/openai-explicit-prompt-cache-breakpoints.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Set explicit prompt cache breakpoints on stable prefixes for OpenAI GPT-5.6+ models. diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 8112996020f..ff534bf107f 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -328,6 +328,20 @@ function normalizeMessages( return msgs } +// kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+ +function supportsPromptCacheBreakpoint(modelId: string): boolean { + const match = modelId.match(/gpt-(\d+)\.(\d+)/) + if (match) { + const major = Number(match[1]) + const minor = Number(match[2]) + if (major > 5 || (major === 5 && minor >= 6)) return true + } + const majorMatch = modelId.match(/gpt-(\d+)/) + if (majorMatch && Number(majorMatch[1]) >= 6) return true + return false +} +// kilocode_change end + function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] { const system = msgs.filter((msg) => msg.role === "system").slice(0, 2) const final = msgs.filter((msg) => msg.role !== "system").slice(-2) @@ -351,6 +365,18 @@ function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage alibaba: { cacheControl: { type: "ephemeral" }, }, + // kilocode_change start + ...(supportsPromptCacheBreakpoint(model.api.id) + ? { + openai: { + promptCacheBreakpoint: { mode: "explicit" }, + }, + azure: { + promptCacheBreakpoint: { mode: "explicit" }, + }, + } + : {}), + // kilocode_change end } for (const msg of unique([...system, ...final])) { @@ -360,8 +386,24 @@ function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage model.api.npm === "@ai-sdk/amazon-bedrock" const shouldUseContentOptions = !useMessageLevelOptions && Array.isArray(msg.content) && msg.content.length > 0 - if (shouldUseContentOptions) { - const lastContent = msg.content[msg.content.length - 1] + // kilocode_change start - place caching breakpoint on stable content before trailing + if (shouldUseContentOptions && Array.isArray(msg.content)) { + const parts = msg.content + let targetIndex = -1 + for (let i = parts.length - 1; i >= 0; i--) { + const part = parts[i] + if ( + part && + typeof part === "object" && + part.type !== "tool-approval-request" && + part.type !== "tool-approval-response" && + !(part.type === "text" && part.text.startsWith("")) + ) { + targetIndex = i + break + } + } + const lastContent = targetIndex >= 0 ? parts[targetIndex] : parts[parts.length - 1] if ( lastContent && typeof lastContent === "object" && @@ -372,6 +414,7 @@ function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage continue } } + // kilocode_change end msg.providerOptions = mergeDeep(msg.providerOptions ?? {}, providerOptions) } @@ -438,6 +481,7 @@ function mapProviderOptions( export function message(msgs: ModelMessage[], model: Provider.Model, options: Record) { msgs = unsupportedParts(msgs, model) msgs = normalizeMessages(msgs, model, options) + // kilocode_change start - apply caching for anthropic, alibaba, and GPT-5.6+ openai/azure/kilo-gateway if ( (model.providerID === "anthropic" || model.providerID === "google-vertex-anthropic" || @@ -446,11 +490,16 @@ export function message(msgs: ModelMessage[], model: Provider.Model, options: Re model.id.includes("anthropic") || model.id.includes("claude") || model.api.npm === "@ai-sdk/anthropic" || - model.api.npm === "@ai-sdk/alibaba") && + model.api.npm === "@ai-sdk/alibaba" || + ((model.api.npm === "@ai-sdk/openai" || + model.api.npm === "@ai-sdk/azure" || + model.api.npm === "@kilocode/kilo-gateway") && + supportsPromptCacheBreakpoint(model.api.id))) && model.api.npm !== "@ai-sdk/gateway" ) { msgs = applyCaching(msgs, model) } + // kilocode_change end // Remap providerOptions keys from stored providerID to expected SDK key const key = sdkKey(model.api.npm) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index dfa901724b1..74ef174ade8 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -3029,6 +3029,140 @@ describe("ProviderTransform.message - cache control on gateway", () => { }, }) }) + + // kilocode_change start + test("openai gpt-5.6 applies promptCacheBreakpoint", () => { + const model = createModel({ + providerID: "openai", + api: { + id: "gpt-5.6", + url: "https://api.openai.com/v1", + npm: "@ai-sdk/openai", + }, + id: "gpt-5.6", + }) + const msgs = [ + { + role: "system", + content: "You are a helpful assistant", + }, + { + role: "user", + content: "Hello", + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) as any[] + + expect(result[0].providerOptions.openai).toEqual({ + promptCacheBreakpoint: { + mode: "explicit", + }, + }) + expect(result[1].providerOptions.openai).toEqual({ + promptCacheBreakpoint: { + mode: "explicit", + }, + }) + }) + + test("openai pre-5.6 does not apply promptCacheBreakpoint", () => { + const model = createModel({ + providerID: "openai", + api: { + id: "gpt-4o", + url: "https://api.openai.com/v1", + npm: "@ai-sdk/openai", + }, + id: "gpt-4o", + }) + const msgs = [ + { + role: "system", + content: "You are a helpful assistant", + }, + { + role: "user", + content: "Hello", + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) as any[] + + expect(result[0].providerOptions?.openai?.promptCacheBreakpoint).toBeUndefined() + expect(result[1].providerOptions?.openai?.promptCacheBreakpoint).toBeUndefined() + }) + + test("openai gpt-5.6 places promptCacheBreakpoint before trailing environment_details", () => { + const model = createModel({ + providerID: "openai", + api: { + id: "gpt-5.6", + url: "https://api.openai.com/v1", + npm: "@ai-sdk/openai", + }, + id: "gpt-5.6", + }) + const envBlock = "\nCurrent time: 2026-08-08T18:00:00+00:00\n" + const msgs = [ + { + role: "system", + content: "You are a helpful assistant", + }, + { + role: "user", + content: [ + { type: "text", text: "Please review the changes" }, + { type: "text", text: envBlock }, + ], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) as any[] + + expect(result[1].content[0].providerOptions.openai).toEqual({ + promptCacheBreakpoint: { + mode: "explicit", + }, + }) + expect(result[1].content[1].providerOptions?.openai?.promptCacheBreakpoint).toBeUndefined() + }) + + test("kilo gateway with openai gpt-5.6 applies caching options", () => { + const model = createModel({ + providerID: "kilo", + api: { + id: "openai/gpt-5.6", + url: "https://api.kilo.ai/api/gateway", + npm: "@kilocode/kilo-gateway", + }, + id: "openai/gpt-5.6", + }) + const msgs = [ + { + role: "system", + content: "You are a helpful assistant", + }, + { + role: "user", + content: "Hello", + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) as any[] + + expect(result[0].providerOptions.openrouter).toEqual({ + cacheControl: { + type: "ephemeral", + }, + }) + expect(result[1].providerOptions.openrouter).toEqual({ + cacheControl: { + type: "ephemeral", + }, + }) + }) + // kilocode_change end }) describe("ProviderTransform.temperature - Cohere North", () => {