From 9dbf276adec8a6b88e451eb13422272e16435cbe Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Sat, 8 Aug 2026 17:26:07 +0000 Subject: [PATCH 1/9] feat(llm): set explicit prompt cache breakpoints on stable prefix for GPT-5.6+ --- ...penai-explicit-prompt-cache-breakpoints.md | 5 ++ packages/llm/src/cache-policy.ts | 14 ++- packages/llm/src/protocols/openai-chat.ts | 87 ++++++++++++++++--- .../llm/src/protocols/openai-responses.ts | 69 +++++++++++++-- packages/llm/test/cache-policy.test.ts | 84 +++++++++++++++++- packages/opencode/src/provider/transform.ts | 35 +++++++- .../opencode/test/provider/transform.test.ts | 64 ++++++++++++++ 7 files changed, 335 insertions(+), 23 deletions(-) create mode 100644 .changeset/openai-explicit-prompt-cache-breakpoints.md 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/llm/src/cache-policy.ts b/packages/llm/src/cache-policy.ts index 60f96dc69aa..3387c3a67d0 100644 --- a/packages/llm/src/cache-policy.ts +++ b/packages/llm/src/cache-policy.ts @@ -36,10 +36,16 @@ const resolve = (policy: CachePolicy | undefined): CachePolicyObject => { return policy } -// Protocols whose wire format ignores inline cache markers (OpenAI's implicit -// prefix caching, Gemini's implicit + out-of-band CachedContent). Skip the -// whole policy pass for these — emitting hints would be harmless but pointless. -const RESPECTS_INLINE_HINTS = new Set(["anthropic-messages", "bedrock-converse"]) +// kilocode_change start - Protocols whose wire format supports inline cache markers / explicit breakpoints. +// Gemini uses out-of-band CachedContent. +const RESPECTS_INLINE_HINTS = new Set([ + "anthropic-messages", + "bedrock-converse", + "openai-responses", + "openai-chat", + "openai-compatible-chat", +]) +// kilocode_change end const makeHint = (ttlSeconds: number | undefined): CacheHint => ttlSeconds !== undefined ? new CacheHint({ type: "ephemeral", ttlSeconds }) : new CacheHint({ type: "ephemeral" }) diff --git a/packages/llm/src/protocols/openai-chat.ts b/packages/llm/src/protocols/openai-chat.ts index c6ac16c7b3b..7d672563e5a 100644 --- a/packages/llm/src/protocols/openai-chat.ts +++ b/packages/llm/src/protocols/openai-chat.ts @@ -28,6 +28,24 @@ const IMAGE_MIMES = new Set(ProviderShared.IMAGE_MIMES) export const DEFAULT_BASE_URL = "https://api.openai.com/v1" export const PATH = "/chat/completions" +// kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+ +const supportsBreakpoint = (modelId: string) => { + 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 +} + +const OpenAIChatPromptCacheBreakpoint = Schema.Struct({ + mode: Schema.Literal("explicit"), +}) +// kilocode_change end + // ============================================================================= // Request Body Schema // ============================================================================= @@ -57,15 +75,29 @@ const OpenAIChatAssistantToolCall = Schema.Struct({ type OpenAIChatAssistantToolCall = Schema.Schema.Type const OpenAIChatUserContent = Schema.Union([ - Schema.Struct({ type: Schema.Literal("text"), text: Schema.String }), + Schema.Struct({ + type: Schema.Literal("text"), + text: Schema.String, + prompt_cache_breakpoint: Schema.optional(OpenAIChatPromptCacheBreakpoint), // kilocode_change + }), Schema.Struct({ type: Schema.Literal("image_url"), image_url: Schema.Struct({ url: Schema.String }), + prompt_cache_breakpoint: Schema.optional(OpenAIChatPromptCacheBreakpoint), // kilocode_change }), ]) const OpenAIChatMessage = Schema.Union([ - Schema.Struct({ role: Schema.Literal("system"), content: Schema.String }), + // kilocode_change start - support content block array for system/developer messages + Schema.Struct({ + role: Schema.Literal("system"), + content: Schema.Union([Schema.String, Schema.Array(OpenAIChatUserContent)]), + }), + Schema.Struct({ + role: Schema.Literal("developer"), + content: Schema.Union([Schema.String, Schema.Array(OpenAIChatUserContent)]), + }), + // kilocode_change end Schema.Struct({ role: Schema.Literal("user"), content: Schema.Union([Schema.String, Schema.Array(OpenAIChatUserContent)]), @@ -210,21 +242,33 @@ const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part: MediaPart const openAICompatibleReasoningContent = (native: unknown) => isRecord(native) && typeof native.reasoning_content === "string" ? native.reasoning_content : undefined -const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* (message: OpenAIChatRequestMessage) { +const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* ( + message: OpenAIChatRequestMessage, + modelId: string, // kilocode_change +) { const content: Array> = [] for (const part of message.content) { + // kilocode_change start + const breakpoint = + "cache" in part && part.cache && supportsBreakpoint(modelId) + ? { prompt_cache_breakpoint: { mode: "explicit" as const } } + : {} if (part.type === "text") { - content.push({ type: "text", text: part.text }) + content.push({ type: "text", text: part.text, ...breakpoint }) continue } if (part.type === "media") { - content.push(yield* lowerMedia(part)) + content.push({ ...(yield* lowerMedia(part)), ...breakpoint }) continue } + // kilocode_change end return yield* ProviderShared.unsupportedContent("OpenAI Chat", "user", ["text", "media"]) } - if (content.every((part) => part.type === "text")) - return { role: "user" as const, content: content.map((part) => part.text).join("\n") } // kilocode_change + // kilocode_change start + const hasBreakpoint = content.some((part) => "prompt_cache_breakpoint" in part && part.prompt_cache_breakpoint) + if (!hasBreakpoint && content.every((part) => part.type === "text")) + return { role: "user" as const, content: content.map((part) => (part as { text: string }).text).join("\n") } + // kilocode_change end return { role: "user" as const, content } }) @@ -284,15 +328,36 @@ const lowerToolMessages = Effect.fn("OpenAIChat.lowerToolMessages")(function* (m return { messages, images } }) -const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* (message: OpenAIChatRequestMessage) { - if (message.role === "user") return [yield* lowerUserMessage(message)] +const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* ( + message: OpenAIChatRequestMessage, + modelId: string, // kilocode_change +) { + if (message.role === "user") return [yield* lowerUserMessage(message, modelId)] // kilocode_change if (message.role === "assistant") return [yield* lowerAssistantMessage(message)] return (yield* lowerToolMessages(message)).messages }) const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request: LLMRequest) { + // kilocode_change start + const hasSystemCache = request.system.some((part) => part.cache) && supportsBreakpoint(request.model.id) const system: OpenAIChatMessage[] = - request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }] + request.system.length === 0 + ? [] + : hasSystemCache + ? [ + { + role: "system", + content: [ + { + type: "text", + text: ProviderShared.joinText(request.system), + prompt_cache_breakpoint: { mode: "explicit" }, + }, + ], + }, + ] + : [{ role: "system", content: ProviderShared.joinText(request.system) }] + // kilocode_change end const messages = [...system] const pendingImages: Array> = [] const flushImages = () => { @@ -324,7 +389,7 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request: continue } flushImages() - messages.push(...(yield* lowerMessage(message))) + messages.push(...(yield* lowerMessage(message, request.model.id))) // kilocode_change } flushImages() return messages diff --git a/packages/llm/src/protocols/openai-responses.ts b/packages/llm/src/protocols/openai-responses.ts index 2e6b67aec72..cdbb27965f7 100644 --- a/packages/llm/src/protocols/openai-responses.ts +++ b/packages/llm/src/protocols/openai-responses.ts @@ -29,16 +29,36 @@ const ADAPTER = "openai-responses" export const DEFAULT_BASE_URL = "https://api.openai.com/v1" export const PATH = "/responses" +// kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+ +const supportsBreakpoint = (modelId: string) => { + 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 +} + +const OpenAIResponsesPromptCacheBreakpoint = Schema.Struct({ + mode: Schema.Literal("explicit"), +}) +// kilocode_change end + // ============================================================================= // Request Body Schema // ============================================================================= const OpenAIResponsesInputText = Schema.Struct({ type: Schema.tag("input_text"), text: Schema.String, + prompt_cache_breakpoint: Schema.optional(OpenAIResponsesPromptCacheBreakpoint), // kilocode_change }) const OpenAIResponsesInputImage = Schema.Struct({ type: Schema.tag("input_image"), image_url: Schema.String, + prompt_cache_breakpoint: Schema.optional(OpenAIResponsesPromptCacheBreakpoint), // kilocode_change }) const OpenAIResponsesInputContent = Schema.Union([OpenAIResponsesInputText, OpenAIResponsesInputImage]) type OpenAIResponsesInputContent = Schema.Schema.Type @@ -76,7 +96,16 @@ const OpenAIResponsesFunctionCallOutput = Schema.Union([ ]) const OpenAIResponsesInputItem = Schema.Union([ - Schema.Struct({ role: Schema.tag("system"), content: Schema.String }), + // kilocode_change start - support content block array for system/developer messages + Schema.Struct({ + role: Schema.Literal("system"), + content: Schema.Union([Schema.String, Schema.Array(OpenAIResponsesInputContent)]), + }), + Schema.Struct({ + role: Schema.Literal("developer"), + content: Schema.Union([Schema.String, Schema.Array(OpenAIResponsesInputContent)]), + }), + // kilocode_change end Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenAIResponsesInputContent) }), Schema.Struct({ role: Schema.tag("assistant"), content: Schema.Array(OpenAIResponsesOutputText) }), OpenAIResponsesReasoningItem, @@ -307,16 +336,23 @@ const hostedToolItemID = (part: ToolResultPart) => { const lowerUserContent = Effect.fn("OpenAIResponses.lowerUserContent")(function* ( part: LLMRequest["messages"][number]["content"][number], + modelId: string, // kilocode_change ) { - if (part.type === "text") return { type: "input_text" as const, text: part.text } + // kilocode_change start + const breakpoint = + "cache" in part && part.cache && supportsBreakpoint(modelId) + ? { prompt_cache_breakpoint: { mode: "explicit" as const } } + : {} + if (part.type === "text") return { type: "input_text" as const, text: part.text, ...breakpoint } if (part.type === "media") { const media = yield* ProviderShared.validateMedia( "OpenAI Responses", part, new Set(ProviderShared.IMAGE_MIMES), ) - return { type: "input_image" as const, image_url: media.dataUrl } + return { type: "input_image" as const, image_url: media.dataUrl, ...breakpoint } } + // kilocode_change end return yield* ProviderShared.unsupportedContent("OpenAI Responses", "user", ["text", "media"]) }) @@ -344,8 +380,26 @@ const lowerToolResultOutput = Effect.fn("OpenAIResponses.lowerToolResultOutput") }) const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (request: LLMRequest) { + // kilocode_change start + const hasSystemCache = request.system.some((part) => part.cache) && supportsBreakpoint(request.model.id) const system: OpenAIResponsesInputItem[] = - request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }] + request.system.length === 0 + ? [] + : hasSystemCache + ? [ + { + role: "system", + content: [ + { + type: "input_text", + text: ProviderShared.joinText(request.system), + prompt_cache_breakpoint: { mode: "explicit" }, + }, + ], + }, + ] + : [{ role: "system", content: ProviderShared.joinText(request.system) }] + // kilocode_change end const input: OpenAIResponsesInputItem[] = [...system] const store = OpenAIOptions.store(request) @@ -363,7 +417,12 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ } if (message.role === "user") { - input.push({ role: "user", content: yield* Effect.forEach(message.content, lowerUserContent) }) + // kilocode_change start + input.push({ + role: "user", + content: yield* Effect.forEach(message.content, (part) => lowerUserContent(part, request.model.id)), + }) + // kilocode_change end continue } diff --git a/packages/llm/test/cache-policy.test.ts b/packages/llm/test/cache-policy.test.ts index a126d9502c5..91a6712d108 100644 --- a/packages/llm/test/cache-policy.test.ts +++ b/packages/llm/test/cache-policy.test.ts @@ -6,6 +6,9 @@ import { AmazonBedrock } from "../src/providers" import * as AnthropicMessages from "../src/protocols/anthropic-messages" import * as Gemini from "../src/protocols/gemini" import * as OpenAIChat from "../src/protocols/openai-chat" +// kilocode_change start +import * as OpenAIResponses from "../src/protocols/openai-responses" +// kilocode_change end import { applyCachePolicy } from "../src/cache-policy" import { it } from "./lib/effect" @@ -21,6 +24,16 @@ const openaiModel = OpenAIChat.route .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") }) .model({ id: "gpt-4o-mini" }) +// kilocode_change start +const openaiGpt56ResponsesModel = OpenAIResponses.route + .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") }) + .model({ id: "gpt-5.6" }) + +const openaiGpt56ChatModel = OpenAIChat.route + .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") }) + .model({ id: "gpt-5.6" }) +// kilocode_change end + const geminiModel = Gemini.route .with({ endpoint: { baseURL: "https://generativelanguage.test/v1beta/" }, @@ -79,7 +92,8 @@ describe("applyCachePolicy", () => { }), ) - it.effect("'auto' is a no-op on OpenAI (implicit caching protocol)", () => + // kilocode_change start + it.effect("'auto' does not emit explicit breakpoints on pre-5.6 OpenAI models", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( LLM.request({ @@ -91,13 +105,79 @@ describe("applyCachePolicy", () => { ) const body = prepared.body as { messages: Array<{ content: unknown }> } - // OpenAI doesn't accept cache_control on messages — policy must skip. + // Older OpenAI models reject prompt_cache_breakpoint — policy must skip. const flat = JSON.stringify(body) + expect(flat).not.toContain("prompt_cache_breakpoint") expect(flat).not.toContain("cache_control") expect(flat).not.toContain("cachePoint") }), ) + it.effect("'auto' emits prompt_cache_breakpoint on stable system prefix and latest user on GPT-5.6 Responses", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model: openaiGpt56ResponsesModel, + system: "System instructions", + messages: [ + Message.user("first question"), + Message.assistant("assistant reply"), + Message.user("latest question"), + ], + cache: "auto", + }), + ) + + expect(prepared.body).toMatchObject({ + input: [ + { + role: "system", + content: [{ type: "input_text", text: "System instructions", prompt_cache_breakpoint: { mode: "explicit" } }], + }, + { role: "user", content: [{ type: "input_text", text: "first question" }] }, + { role: "assistant", content: [{ type: "output_text", text: "assistant reply" }] }, + { + role: "user", + content: [{ type: "input_text", text: "latest question", prompt_cache_breakpoint: { mode: "explicit" } }], + }, + ], + }) + }), + ) + + it.effect("'auto' emits prompt_cache_breakpoint on stable system prefix and latest user on GPT-5.6 Chat", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model: openaiGpt56ChatModel, + system: "System instructions", + messages: [ + Message.user("first question"), + Message.assistant("assistant reply"), + Message.user("latest question"), + ], + cache: "auto", + }), + ) + + expect(prepared.body).toMatchObject({ + messages: [ + { + role: "system", + content: [{ type: "text", text: "System instructions", prompt_cache_breakpoint: { mode: "explicit" } }], + }, + { role: "user", content: "first question" }, + { role: "assistant", content: "assistant reply" }, + { + role: "user", + content: [{ type: "text", text: "latest question", prompt_cache_breakpoint: { mode: "explicit" } }], + }, + ], + }) + }), + ) + // kilocode_change end + it.effect("'auto' is a no-op on Gemini (out-of-band caching protocol)", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 8112996020f..4b1c28e6f5f 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 supportsOpenAICacheBreakpoint(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 + ...(supportsOpenAICacheBreakpoint(model.api.id) + ? { + openai: { + promptCacheBreakpoint: { mode: "explicit" }, + }, + azure: { + promptCacheBreakpoint: { mode: "explicit" }, + }, + } + : {}), + // kilocode_change end } for (const msg of unique([...system, ...final])) { @@ -438,6 +464,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 if ( (model.providerID === "anthropic" || model.providerID === "google-vertex-anthropic" || @@ -446,11 +473,17 @@ 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.providerID === "openai" || + model.providerID === "azure") && + supportsOpenAICacheBreakpoint(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..14542cdaa41 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -3029,6 +3029,70 @@ 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() + }) + // kilocode_change end }) describe("ProviderTransform.temperature - Cohere North", () => { From 435c029f8cad49af70be2dd1857a85636d4f406b Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Sat, 8 Aug 2026 17:39:22 +0000 Subject: [PATCH 2/9] refactor(llm): extract shared supportsPromptCacheBreakpoint and restrict inline hints to native openai routes Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- packages/llm/src/cache-policy.ts | 1 - packages/llm/src/index.ts | 1 + packages/llm/src/protocols/openai-chat.ts | 16 ++-------------- .../llm/src/protocols/openai-responses.ts | 16 ++-------------- .../llm/src/protocols/utils/openai-options.ts | 14 ++++++++++++++ packages/opencode/src/provider/transform.ts | 19 +++---------------- 6 files changed, 22 insertions(+), 45 deletions(-) diff --git a/packages/llm/src/cache-policy.ts b/packages/llm/src/cache-policy.ts index 3387c3a67d0..e1caf0d67e5 100644 --- a/packages/llm/src/cache-policy.ts +++ b/packages/llm/src/cache-policy.ts @@ -43,7 +43,6 @@ const RESPECTS_INLINE_HINTS = new Set([ "bedrock-converse", "openai-responses", "openai-chat", - "openai-compatible-chat", ]) // kilocode_change end diff --git a/packages/llm/src/index.ts b/packages/llm/src/index.ts index 735520ff77c..1a9fd1fd3de 100644 --- a/packages/llm/src/index.ts +++ b/packages/llm/src/index.ts @@ -26,6 +26,7 @@ export type { ToolToModelOutput, } from "./tool" export * as LLM from "./llm" +export { supportsPromptCacheBreakpoint } from "./protocols/utils/openai-options" // kilocode_change export type { Definition as ProviderDefinition, ModelFactory as ProviderModelFactory, diff --git a/packages/llm/src/protocols/openai-chat.ts b/packages/llm/src/protocols/openai-chat.ts index 7d672563e5a..9db95915669 100644 --- a/packages/llm/src/protocols/openai-chat.ts +++ b/packages/llm/src/protocols/openai-chat.ts @@ -29,18 +29,6 @@ export const DEFAULT_BASE_URL = "https://api.openai.com/v1" export const PATH = "/chat/completions" // kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+ -const supportsBreakpoint = (modelId: string) => { - 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 -} - const OpenAIChatPromptCacheBreakpoint = Schema.Struct({ mode: Schema.Literal("explicit"), }) @@ -250,7 +238,7 @@ const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* ( for (const part of message.content) { // kilocode_change start const breakpoint = - "cache" in part && part.cache && supportsBreakpoint(modelId) + "cache" in part && part.cache && OpenAIOptions.supportsPromptCacheBreakpoint(modelId) ? { prompt_cache_breakpoint: { mode: "explicit" as const } } : {} if (part.type === "text") { @@ -339,7 +327,7 @@ const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* ( const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request: LLMRequest) { // kilocode_change start - const hasSystemCache = request.system.some((part) => part.cache) && supportsBreakpoint(request.model.id) + const hasSystemCache = request.system.some((part) => part.cache) && OpenAIOptions.supportsPromptCacheBreakpoint(request.model.id) const system: OpenAIChatMessage[] = request.system.length === 0 ? [] diff --git a/packages/llm/src/protocols/openai-responses.ts b/packages/llm/src/protocols/openai-responses.ts index cdbb27965f7..74b4524518d 100644 --- a/packages/llm/src/protocols/openai-responses.ts +++ b/packages/llm/src/protocols/openai-responses.ts @@ -30,18 +30,6 @@ export const DEFAULT_BASE_URL = "https://api.openai.com/v1" export const PATH = "/responses" // kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+ -const supportsBreakpoint = (modelId: string) => { - 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 -} - const OpenAIResponsesPromptCacheBreakpoint = Schema.Struct({ mode: Schema.Literal("explicit"), }) @@ -340,7 +328,7 @@ const lowerUserContent = Effect.fn("OpenAIResponses.lowerUserContent")(function* ) { // kilocode_change start const breakpoint = - "cache" in part && part.cache && supportsBreakpoint(modelId) + "cache" in part && part.cache && OpenAIOptions.supportsPromptCacheBreakpoint(modelId) ? { prompt_cache_breakpoint: { mode: "explicit" as const } } : {} if (part.type === "text") return { type: "input_text" as const, text: part.text, ...breakpoint } @@ -381,7 +369,7 @@ const lowerToolResultOutput = Effect.fn("OpenAIResponses.lowerToolResultOutput") const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (request: LLMRequest) { // kilocode_change start - const hasSystemCache = request.system.some((part) => part.cache) && supportsBreakpoint(request.model.id) + const hasSystemCache = request.system.some((part) => part.cache) && OpenAIOptions.supportsPromptCacheBreakpoint(request.model.id) const system: OpenAIResponsesInputItem[] = request.system.length === 0 ? [] diff --git a/packages/llm/src/protocols/utils/openai-options.ts b/packages/llm/src/protocols/utils/openai-options.ts index 59ac7abc874..708dc07d28c 100644 --- a/packages/llm/src/protocols/utils/openai-options.ts +++ b/packages/llm/src/protocols/utils/openai-options.ts @@ -101,4 +101,18 @@ export const instructions = (request: LLMRequest) => { return typeof value === "string" ? value : undefined } +// kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+ +export const 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 + export * as OpenAIOptions from "./openai-options" diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 4b1c28e6f5f..dbe3f58da25 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -1,6 +1,7 @@ import type { ModelMessage, ToolResultPart } from "ai" import { mergeDeep, unique } from "remeda" import type { JSONSchema7 } from "@ai-sdk/provider" +import { supportsPromptCacheBreakpoint } from "@opencode-ai/llm" // kilocode_change import type * as Provider from "./provider" import type * as ModelsDev from "@opencode-ai/core/models-dev" import { iife } from "@/util/iife" @@ -328,20 +329,6 @@ function normalizeMessages( return msgs } -// kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+ -function supportsOpenAICacheBreakpoint(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) @@ -366,7 +353,7 @@ function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage cacheControl: { type: "ephemeral" }, }, // kilocode_change start - ...(supportsOpenAICacheBreakpoint(model.api.id) + ...(supportsPromptCacheBreakpoint(model.api.id) ? { openai: { promptCacheBreakpoint: { mode: "explicit" }, @@ -478,7 +465,7 @@ export function message(msgs: ModelMessage[], model: Provider.Model, options: Re model.api.npm === "@ai-sdk/azure" || model.providerID === "openai" || model.providerID === "azure") && - supportsOpenAICacheBreakpoint(model.api.id))) && + supportsPromptCacheBreakpoint(model.api.id))) && model.api.npm !== "@ai-sdk/gateway" ) { msgs = applyCaching(msgs, model) From 944ab18304ff4b26574daa546ef745b2c6337efe Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Sat, 8 Aug 2026 18:17:49 +0000 Subject: [PATCH 3/9] fix(llm): place prompt cache breakpoint before trailing environment details Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- packages/llm/src/cache-policy.ts | 21 +++++++-- packages/llm/test/cache-policy.test.ts | 46 +++++++++++++++++++ packages/opencode/src/provider/transform.ts | 28 ++++++++--- .../opencode/test/provider/transform.test.ts | 35 ++++++++++++++ 4 files changed, 120 insertions(+), 10 deletions(-) diff --git a/packages/llm/src/cache-policy.ts b/packages/llm/src/cache-policy.ts index e1caf0d67e5..96cd7b38bdb 100644 --- a/packages/llm/src/cache-policy.ts +++ b/packages/llm/src/cache-policy.ts @@ -66,15 +66,28 @@ const markLastSystem = (system: LLMRequest["system"], hint: CacheHint): LLMReque const lastIndexOfRole = (messages: ReadonlyArray, role: Message["role"]): number => messages.findLastIndex((m) => m.role === role) -// Mark the last text part of `messages[index]`. If no text part exists, mark -// the last content part regardless of type — that's the breakpoint position -// in tool-result-only messages too. +// kilocode_change start - mark the last stable text part of `messages[index]` (skipping trailing ephemeral details) +const isEphemeralPart = (part: ContentPart): boolean => { + if (part.type === "text" && (part.text.startsWith("") || part.text.includes(""))) + return true + if ("metadata" in part && part.metadata && Boolean((part.metadata as Record).synthetic)) + return true + return false +} + const markMessageAt = (messages: ReadonlyArray, index: number, hint: CacheHint): ReadonlyArray => { if (index < 0 || index >= messages.length) return messages const target = messages[index]! if (target.content.length === 0) return messages + const lastStableTextIndex = target.content.findLastIndex((part) => part.type === "text" && !isEphemeralPart(part)) const lastTextIndex = target.content.findLastIndex((part) => part.type === "text") - const markAt = lastTextIndex >= 0 ? lastTextIndex : target.content.length - 1 + const markAt = + lastStableTextIndex >= 0 + ? lastStableTextIndex + : lastTextIndex >= 0 + ? lastTextIndex + : target.content.length - 1 + // kilocode_change end const existing = target.content[markAt]! if ("cache" in existing && existing.cache) return messages const nextContent = target.content.map((part, i) => (i === markAt ? ({ ...part, cache: hint } as ContentPart) : part)) diff --git a/packages/llm/test/cache-policy.test.ts b/packages/llm/test/cache-policy.test.ts index 91a6712d108..b0f3256e988 100644 --- a/packages/llm/test/cache-policy.test.ts +++ b/packages/llm/test/cache-policy.test.ts @@ -176,6 +176,52 @@ describe("applyCachePolicy", () => { }) }), ) + + it.effect("places prompt_cache_breakpoint BEFORE trailing part", () => + Effect.gen(function* () { + const envBlock = "\nCurrent time: 2026-08-08T18:00:00+00:00\n" + const prepared = yield* LLMClient.prepare( + LLM.request({ + model: openaiGpt56ResponsesModel, + system: "System instructions", + messages: [ + Message.user([ + { type: "text", text: "Please inspect the codebase" }, + { type: "text", text: envBlock }, + ]), + ], + cache: "auto", + }), + ) + + expect(prepared.body).toMatchObject({ + input: [ + { + role: "system", + content: [{ type: "input_text", text: "System instructions", prompt_cache_breakpoint: { mode: "explicit" } }], + }, + { + role: "user", + content: [ + { + type: "input_text", + text: "Please inspect the codebase", + prompt_cache_breakpoint: { mode: "explicit" }, + }, + { + type: "input_text", + text: envBlock, + }, + ], + }, + ], + }) + + const userContent = (prepared.body as any).input[1].content + expect(userContent[0].prompt_cache_breakpoint).toEqual({ mode: "explicit" }) + expect(userContent[1].prompt_cache_breakpoint).toBeUndefined() + }), + ) // kilocode_change end it.effect("'auto' is a no-op on Gemini (out-of-band caching protocol)", () => diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index dbe3f58da25..f0173b50303 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -371,20 +371,36 @@ function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage model.providerID === "anthropic" || model.providerID.includes("bedrock") || model.api.npm === "@ai-sdk/amazon-bedrock" + // kilocode_change start - place caching breakpoint on stable content before trailing const shouldUseContentOptions = !useMessageLevelOptions && Array.isArray(msg.content) && msg.content.length > 0 if (shouldUseContentOptions) { - const lastContent = msg.content[msg.content.length - 1] + const parts = msg.content as any[] + const targetIndex = parts.findLastIndex( + (part) => + part && + typeof part === "object" && + part.type !== "tool-approval-request" && + part.type !== "tool-approval-response" && + !( + part.type === "text" && + typeof part.text === "string" && + (part.text.startsWith("") || part.text.includes("")) + ) && + !part.synthetic, + ) + const targetContent = targetIndex >= 0 ? parts[targetIndex] : parts[parts.length - 1] if ( - lastContent && - typeof lastContent === "object" && - lastContent.type !== "tool-approval-request" && - lastContent.type !== "tool-approval-response" + targetContent && + typeof targetContent === "object" && + targetContent.type !== "tool-approval-request" && + targetContent.type !== "tool-approval-response" ) { - lastContent.providerOptions = mergeDeep(lastContent.providerOptions ?? {}, providerOptions) + targetContent.providerOptions = mergeDeep(targetContent.providerOptions ?? {}, providerOptions) continue } } + // kilocode_change end msg.providerOptions = mergeDeep(msg.providerOptions ?? {}, providerOptions) } diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 14542cdaa41..2f8c7d23b18 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -3092,6 +3092,41 @@ describe("ProviderTransform.message - cache control on gateway", () => { 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() + }) // kilocode_change end }) From 2f72a8b72dc16b226f937384c8fd11a140262e1e Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Sat, 8 Aug 2026 18:22:35 +0000 Subject: [PATCH 4/9] refactor(cli): limit explicit prompt cache breakpoint changes to Vercel AI SDK path Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- packages/llm/src/cache-policy.ts | 34 ++--- packages/llm/src/index.ts | 1 - packages/llm/src/protocols/openai-chat.ts | 75 ++-------- .../llm/src/protocols/openai-responses.ts | 57 +------- .../llm/src/protocols/utils/openai-options.ts | 14 -- packages/llm/test/cache-policy.test.ts | 130 +----------------- packages/opencode/src/provider/transform.ts | 15 +- 7 files changed, 40 insertions(+), 286 deletions(-) diff --git a/packages/llm/src/cache-policy.ts b/packages/llm/src/cache-policy.ts index 96cd7b38bdb..60f96dc69aa 100644 --- a/packages/llm/src/cache-policy.ts +++ b/packages/llm/src/cache-policy.ts @@ -36,15 +36,10 @@ const resolve = (policy: CachePolicy | undefined): CachePolicyObject => { return policy } -// kilocode_change start - Protocols whose wire format supports inline cache markers / explicit breakpoints. -// Gemini uses out-of-band CachedContent. -const RESPECTS_INLINE_HINTS = new Set([ - "anthropic-messages", - "bedrock-converse", - "openai-responses", - "openai-chat", -]) -// kilocode_change end +// Protocols whose wire format ignores inline cache markers (OpenAI's implicit +// prefix caching, Gemini's implicit + out-of-band CachedContent). Skip the +// whole policy pass for these — emitting hints would be harmless but pointless. +const RESPECTS_INLINE_HINTS = new Set(["anthropic-messages", "bedrock-converse"]) const makeHint = (ttlSeconds: number | undefined): CacheHint => ttlSeconds !== undefined ? new CacheHint({ type: "ephemeral", ttlSeconds }) : new CacheHint({ type: "ephemeral" }) @@ -66,28 +61,15 @@ const markLastSystem = (system: LLMRequest["system"], hint: CacheHint): LLMReque const lastIndexOfRole = (messages: ReadonlyArray, role: Message["role"]): number => messages.findLastIndex((m) => m.role === role) -// kilocode_change start - mark the last stable text part of `messages[index]` (skipping trailing ephemeral details) -const isEphemeralPart = (part: ContentPart): boolean => { - if (part.type === "text" && (part.text.startsWith("") || part.text.includes(""))) - return true - if ("metadata" in part && part.metadata && Boolean((part.metadata as Record).synthetic)) - return true - return false -} - +// Mark the last text part of `messages[index]`. If no text part exists, mark +// the last content part regardless of type — that's the breakpoint position +// in tool-result-only messages too. const markMessageAt = (messages: ReadonlyArray, index: number, hint: CacheHint): ReadonlyArray => { if (index < 0 || index >= messages.length) return messages const target = messages[index]! if (target.content.length === 0) return messages - const lastStableTextIndex = target.content.findLastIndex((part) => part.type === "text" && !isEphemeralPart(part)) const lastTextIndex = target.content.findLastIndex((part) => part.type === "text") - const markAt = - lastStableTextIndex >= 0 - ? lastStableTextIndex - : lastTextIndex >= 0 - ? lastTextIndex - : target.content.length - 1 - // kilocode_change end + const markAt = lastTextIndex >= 0 ? lastTextIndex : target.content.length - 1 const existing = target.content[markAt]! if ("cache" in existing && existing.cache) return messages const nextContent = target.content.map((part, i) => (i === markAt ? ({ ...part, cache: hint } as ContentPart) : part)) diff --git a/packages/llm/src/index.ts b/packages/llm/src/index.ts index 1a9fd1fd3de..735520ff77c 100644 --- a/packages/llm/src/index.ts +++ b/packages/llm/src/index.ts @@ -26,7 +26,6 @@ export type { ToolToModelOutput, } from "./tool" export * as LLM from "./llm" -export { supportsPromptCacheBreakpoint } from "./protocols/utils/openai-options" // kilocode_change export type { Definition as ProviderDefinition, ModelFactory as ProviderModelFactory, diff --git a/packages/llm/src/protocols/openai-chat.ts b/packages/llm/src/protocols/openai-chat.ts index 9db95915669..c6ac16c7b3b 100644 --- a/packages/llm/src/protocols/openai-chat.ts +++ b/packages/llm/src/protocols/openai-chat.ts @@ -28,12 +28,6 @@ const IMAGE_MIMES = new Set(ProviderShared.IMAGE_MIMES) export const DEFAULT_BASE_URL = "https://api.openai.com/v1" export const PATH = "/chat/completions" -// kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+ -const OpenAIChatPromptCacheBreakpoint = Schema.Struct({ - mode: Schema.Literal("explicit"), -}) -// kilocode_change end - // ============================================================================= // Request Body Schema // ============================================================================= @@ -63,29 +57,15 @@ const OpenAIChatAssistantToolCall = Schema.Struct({ type OpenAIChatAssistantToolCall = Schema.Schema.Type const OpenAIChatUserContent = Schema.Union([ - Schema.Struct({ - type: Schema.Literal("text"), - text: Schema.String, - prompt_cache_breakpoint: Schema.optional(OpenAIChatPromptCacheBreakpoint), // kilocode_change - }), + Schema.Struct({ type: Schema.Literal("text"), text: Schema.String }), Schema.Struct({ type: Schema.Literal("image_url"), image_url: Schema.Struct({ url: Schema.String }), - prompt_cache_breakpoint: Schema.optional(OpenAIChatPromptCacheBreakpoint), // kilocode_change }), ]) const OpenAIChatMessage = Schema.Union([ - // kilocode_change start - support content block array for system/developer messages - Schema.Struct({ - role: Schema.Literal("system"), - content: Schema.Union([Schema.String, Schema.Array(OpenAIChatUserContent)]), - }), - Schema.Struct({ - role: Schema.Literal("developer"), - content: Schema.Union([Schema.String, Schema.Array(OpenAIChatUserContent)]), - }), - // kilocode_change end + Schema.Struct({ role: Schema.Literal("system"), content: Schema.String }), Schema.Struct({ role: Schema.Literal("user"), content: Schema.Union([Schema.String, Schema.Array(OpenAIChatUserContent)]), @@ -230,33 +210,21 @@ const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part: MediaPart const openAICompatibleReasoningContent = (native: unknown) => isRecord(native) && typeof native.reasoning_content === "string" ? native.reasoning_content : undefined -const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* ( - message: OpenAIChatRequestMessage, - modelId: string, // kilocode_change -) { +const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* (message: OpenAIChatRequestMessage) { const content: Array> = [] for (const part of message.content) { - // kilocode_change start - const breakpoint = - "cache" in part && part.cache && OpenAIOptions.supportsPromptCacheBreakpoint(modelId) - ? { prompt_cache_breakpoint: { mode: "explicit" as const } } - : {} if (part.type === "text") { - content.push({ type: "text", text: part.text, ...breakpoint }) + content.push({ type: "text", text: part.text }) continue } if (part.type === "media") { - content.push({ ...(yield* lowerMedia(part)), ...breakpoint }) + content.push(yield* lowerMedia(part)) continue } - // kilocode_change end return yield* ProviderShared.unsupportedContent("OpenAI Chat", "user", ["text", "media"]) } - // kilocode_change start - const hasBreakpoint = content.some((part) => "prompt_cache_breakpoint" in part && part.prompt_cache_breakpoint) - if (!hasBreakpoint && content.every((part) => part.type === "text")) - return { role: "user" as const, content: content.map((part) => (part as { text: string }).text).join("\n") } - // kilocode_change end + if (content.every((part) => part.type === "text")) + return { role: "user" as const, content: content.map((part) => part.text).join("\n") } // kilocode_change return { role: "user" as const, content } }) @@ -316,36 +284,15 @@ const lowerToolMessages = Effect.fn("OpenAIChat.lowerToolMessages")(function* (m return { messages, images } }) -const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* ( - message: OpenAIChatRequestMessage, - modelId: string, // kilocode_change -) { - if (message.role === "user") return [yield* lowerUserMessage(message, modelId)] // kilocode_change +const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* (message: OpenAIChatRequestMessage) { + if (message.role === "user") return [yield* lowerUserMessage(message)] if (message.role === "assistant") return [yield* lowerAssistantMessage(message)] return (yield* lowerToolMessages(message)).messages }) const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request: LLMRequest) { - // kilocode_change start - const hasSystemCache = request.system.some((part) => part.cache) && OpenAIOptions.supportsPromptCacheBreakpoint(request.model.id) const system: OpenAIChatMessage[] = - request.system.length === 0 - ? [] - : hasSystemCache - ? [ - { - role: "system", - content: [ - { - type: "text", - text: ProviderShared.joinText(request.system), - prompt_cache_breakpoint: { mode: "explicit" }, - }, - ], - }, - ] - : [{ role: "system", content: ProviderShared.joinText(request.system) }] - // kilocode_change end + request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }] const messages = [...system] const pendingImages: Array> = [] const flushImages = () => { @@ -377,7 +324,7 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request: continue } flushImages() - messages.push(...(yield* lowerMessage(message, request.model.id))) // kilocode_change + messages.push(...(yield* lowerMessage(message))) } flushImages() return messages diff --git a/packages/llm/src/protocols/openai-responses.ts b/packages/llm/src/protocols/openai-responses.ts index 74b4524518d..2e6b67aec72 100644 --- a/packages/llm/src/protocols/openai-responses.ts +++ b/packages/llm/src/protocols/openai-responses.ts @@ -29,24 +29,16 @@ const ADAPTER = "openai-responses" export const DEFAULT_BASE_URL = "https://api.openai.com/v1" export const PATH = "/responses" -// kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+ -const OpenAIResponsesPromptCacheBreakpoint = Schema.Struct({ - mode: Schema.Literal("explicit"), -}) -// kilocode_change end - // ============================================================================= // Request Body Schema // ============================================================================= const OpenAIResponsesInputText = Schema.Struct({ type: Schema.tag("input_text"), text: Schema.String, - prompt_cache_breakpoint: Schema.optional(OpenAIResponsesPromptCacheBreakpoint), // kilocode_change }) const OpenAIResponsesInputImage = Schema.Struct({ type: Schema.tag("input_image"), image_url: Schema.String, - prompt_cache_breakpoint: Schema.optional(OpenAIResponsesPromptCacheBreakpoint), // kilocode_change }) const OpenAIResponsesInputContent = Schema.Union([OpenAIResponsesInputText, OpenAIResponsesInputImage]) type OpenAIResponsesInputContent = Schema.Schema.Type @@ -84,16 +76,7 @@ const OpenAIResponsesFunctionCallOutput = Schema.Union([ ]) const OpenAIResponsesInputItem = Schema.Union([ - // kilocode_change start - support content block array for system/developer messages - Schema.Struct({ - role: Schema.Literal("system"), - content: Schema.Union([Schema.String, Schema.Array(OpenAIResponsesInputContent)]), - }), - Schema.Struct({ - role: Schema.Literal("developer"), - content: Schema.Union([Schema.String, Schema.Array(OpenAIResponsesInputContent)]), - }), - // kilocode_change end + Schema.Struct({ role: Schema.tag("system"), content: Schema.String }), Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenAIResponsesInputContent) }), Schema.Struct({ role: Schema.tag("assistant"), content: Schema.Array(OpenAIResponsesOutputText) }), OpenAIResponsesReasoningItem, @@ -324,23 +307,16 @@ const hostedToolItemID = (part: ToolResultPart) => { const lowerUserContent = Effect.fn("OpenAIResponses.lowerUserContent")(function* ( part: LLMRequest["messages"][number]["content"][number], - modelId: string, // kilocode_change ) { - // kilocode_change start - const breakpoint = - "cache" in part && part.cache && OpenAIOptions.supportsPromptCacheBreakpoint(modelId) - ? { prompt_cache_breakpoint: { mode: "explicit" as const } } - : {} - if (part.type === "text") return { type: "input_text" as const, text: part.text, ...breakpoint } + if (part.type === "text") return { type: "input_text" as const, text: part.text } if (part.type === "media") { const media = yield* ProviderShared.validateMedia( "OpenAI Responses", part, new Set(ProviderShared.IMAGE_MIMES), ) - return { type: "input_image" as const, image_url: media.dataUrl, ...breakpoint } + return { type: "input_image" as const, image_url: media.dataUrl } } - // kilocode_change end return yield* ProviderShared.unsupportedContent("OpenAI Responses", "user", ["text", "media"]) }) @@ -368,26 +344,8 @@ const lowerToolResultOutput = Effect.fn("OpenAIResponses.lowerToolResultOutput") }) const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (request: LLMRequest) { - // kilocode_change start - const hasSystemCache = request.system.some((part) => part.cache) && OpenAIOptions.supportsPromptCacheBreakpoint(request.model.id) const system: OpenAIResponsesInputItem[] = - request.system.length === 0 - ? [] - : hasSystemCache - ? [ - { - role: "system", - content: [ - { - type: "input_text", - text: ProviderShared.joinText(request.system), - prompt_cache_breakpoint: { mode: "explicit" }, - }, - ], - }, - ] - : [{ role: "system", content: ProviderShared.joinText(request.system) }] - // kilocode_change end + request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }] const input: OpenAIResponsesInputItem[] = [...system] const store = OpenAIOptions.store(request) @@ -405,12 +363,7 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ } if (message.role === "user") { - // kilocode_change start - input.push({ - role: "user", - content: yield* Effect.forEach(message.content, (part) => lowerUserContent(part, request.model.id)), - }) - // kilocode_change end + input.push({ role: "user", content: yield* Effect.forEach(message.content, lowerUserContent) }) continue } diff --git a/packages/llm/src/protocols/utils/openai-options.ts b/packages/llm/src/protocols/utils/openai-options.ts index 708dc07d28c..59ac7abc874 100644 --- a/packages/llm/src/protocols/utils/openai-options.ts +++ b/packages/llm/src/protocols/utils/openai-options.ts @@ -101,18 +101,4 @@ export const instructions = (request: LLMRequest) => { return typeof value === "string" ? value : undefined } -// kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+ -export const 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 - export * as OpenAIOptions from "./openai-options" diff --git a/packages/llm/test/cache-policy.test.ts b/packages/llm/test/cache-policy.test.ts index b0f3256e988..a126d9502c5 100644 --- a/packages/llm/test/cache-policy.test.ts +++ b/packages/llm/test/cache-policy.test.ts @@ -6,9 +6,6 @@ import { AmazonBedrock } from "../src/providers" import * as AnthropicMessages from "../src/protocols/anthropic-messages" import * as Gemini from "../src/protocols/gemini" import * as OpenAIChat from "../src/protocols/openai-chat" -// kilocode_change start -import * as OpenAIResponses from "../src/protocols/openai-responses" -// kilocode_change end import { applyCachePolicy } from "../src/cache-policy" import { it } from "./lib/effect" @@ -24,16 +21,6 @@ const openaiModel = OpenAIChat.route .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") }) .model({ id: "gpt-4o-mini" }) -// kilocode_change start -const openaiGpt56ResponsesModel = OpenAIResponses.route - .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") }) - .model({ id: "gpt-5.6" }) - -const openaiGpt56ChatModel = OpenAIChat.route - .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") }) - .model({ id: "gpt-5.6" }) -// kilocode_change end - const geminiModel = Gemini.route .with({ endpoint: { baseURL: "https://generativelanguage.test/v1beta/" }, @@ -92,8 +79,7 @@ describe("applyCachePolicy", () => { }), ) - // kilocode_change start - it.effect("'auto' does not emit explicit breakpoints on pre-5.6 OpenAI models", () => + it.effect("'auto' is a no-op on OpenAI (implicit caching protocol)", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( LLM.request({ @@ -105,125 +91,13 @@ describe("applyCachePolicy", () => { ) const body = prepared.body as { messages: Array<{ content: unknown }> } - // Older OpenAI models reject prompt_cache_breakpoint — policy must skip. + // OpenAI doesn't accept cache_control on messages — policy must skip. const flat = JSON.stringify(body) - expect(flat).not.toContain("prompt_cache_breakpoint") expect(flat).not.toContain("cache_control") expect(flat).not.toContain("cachePoint") }), ) - it.effect("'auto' emits prompt_cache_breakpoint on stable system prefix and latest user on GPT-5.6 Responses", () => - Effect.gen(function* () { - const prepared = yield* LLMClient.prepare( - LLM.request({ - model: openaiGpt56ResponsesModel, - system: "System instructions", - messages: [ - Message.user("first question"), - Message.assistant("assistant reply"), - Message.user("latest question"), - ], - cache: "auto", - }), - ) - - expect(prepared.body).toMatchObject({ - input: [ - { - role: "system", - content: [{ type: "input_text", text: "System instructions", prompt_cache_breakpoint: { mode: "explicit" } }], - }, - { role: "user", content: [{ type: "input_text", text: "first question" }] }, - { role: "assistant", content: [{ type: "output_text", text: "assistant reply" }] }, - { - role: "user", - content: [{ type: "input_text", text: "latest question", prompt_cache_breakpoint: { mode: "explicit" } }], - }, - ], - }) - }), - ) - - it.effect("'auto' emits prompt_cache_breakpoint on stable system prefix and latest user on GPT-5.6 Chat", () => - Effect.gen(function* () { - const prepared = yield* LLMClient.prepare( - LLM.request({ - model: openaiGpt56ChatModel, - system: "System instructions", - messages: [ - Message.user("first question"), - Message.assistant("assistant reply"), - Message.user("latest question"), - ], - cache: "auto", - }), - ) - - expect(prepared.body).toMatchObject({ - messages: [ - { - role: "system", - content: [{ type: "text", text: "System instructions", prompt_cache_breakpoint: { mode: "explicit" } }], - }, - { role: "user", content: "first question" }, - { role: "assistant", content: "assistant reply" }, - { - role: "user", - content: [{ type: "text", text: "latest question", prompt_cache_breakpoint: { mode: "explicit" } }], - }, - ], - }) - }), - ) - - it.effect("places prompt_cache_breakpoint BEFORE trailing part", () => - Effect.gen(function* () { - const envBlock = "\nCurrent time: 2026-08-08T18:00:00+00:00\n" - const prepared = yield* LLMClient.prepare( - LLM.request({ - model: openaiGpt56ResponsesModel, - system: "System instructions", - messages: [ - Message.user([ - { type: "text", text: "Please inspect the codebase" }, - { type: "text", text: envBlock }, - ]), - ], - cache: "auto", - }), - ) - - expect(prepared.body).toMatchObject({ - input: [ - { - role: "system", - content: [{ type: "input_text", text: "System instructions", prompt_cache_breakpoint: { mode: "explicit" } }], - }, - { - role: "user", - content: [ - { - type: "input_text", - text: "Please inspect the codebase", - prompt_cache_breakpoint: { mode: "explicit" }, - }, - { - type: "input_text", - text: envBlock, - }, - ], - }, - ], - }) - - const userContent = (prepared.body as any).input[1].content - expect(userContent[0].prompt_cache_breakpoint).toEqual({ mode: "explicit" }) - expect(userContent[1].prompt_cache_breakpoint).toBeUndefined() - }), - ) - // kilocode_change end - it.effect("'auto' is a no-op on Gemini (out-of-band caching protocol)", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index f0173b50303..6009891d05e 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -1,7 +1,6 @@ import type { ModelMessage, ToolResultPart } from "ai" import { mergeDeep, unique } from "remeda" import type { JSONSchema7 } from "@ai-sdk/provider" -import { supportsPromptCacheBreakpoint } from "@opencode-ai/llm" // kilocode_change import type * as Provider from "./provider" import type * as ModelsDev from "@opencode-ai/core/models-dev" import { iife } from "@/util/iife" @@ -329,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) From 1b0f94ec74e66331a02bcc06c337ed662e1e6412 Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Sat, 8 Aug 2026 18:25:29 +0000 Subject: [PATCH 5/9] feat(cli): enable explicit prompt cache options for OpenAI models on Kilo Gateway Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- packages/opencode/src/provider/transform.ts | 6 ++-- .../opencode/test/provider/transform.test.ts | 35 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 6009891d05e..efb6ef760ad 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -480,7 +480,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 + // 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" || @@ -492,8 +492,10 @@ export function message(msgs: ModelMessage[], model: Provider.Model, options: Re model.api.npm === "@ai-sdk/alibaba" || ((model.api.npm === "@ai-sdk/openai" || model.api.npm === "@ai-sdk/azure" || + model.api.npm === "@kilocode/kilo-gateway" || model.providerID === "openai" || - model.providerID === "azure") && + model.providerID === "azure" || + model.providerID === "kilo") && supportsPromptCacheBreakpoint(model.api.id))) && model.api.npm !== "@ai-sdk/gateway" ) { diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 2f8c7d23b18..74ef174ade8 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -3127,6 +3127,41 @@ describe("ProviderTransform.message - cache control on gateway", () => { }) 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 }) From bca459832456bdba75e48c675122c324568870ea Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Sat, 8 Aug 2026 18:28:46 +0000 Subject: [PATCH 6/9] refactor(cli): simplify provider checks and narrow kilocode_change markers in transform.ts Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- packages/opencode/src/provider/transform.ts | 31 ++++++++------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index efb6ef760ad..d366119a5f3 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -384,10 +384,10 @@ function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage model.providerID === "anthropic" || model.providerID.includes("bedrock") || model.api.npm === "@ai-sdk/amazon-bedrock" - // kilocode_change start - place caching breakpoint on stable content before trailing const shouldUseContentOptions = !useMessageLevelOptions && Array.isArray(msg.content) && msg.content.length > 0 if (shouldUseContentOptions) { + // kilocode_change start - place caching breakpoint on stable content before trailing const parts = msg.content as any[] const targetIndex = parts.findLastIndex( (part) => @@ -395,25 +395,21 @@ function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage typeof part === "object" && part.type !== "tool-approval-request" && part.type !== "tool-approval-response" && - !( - part.type === "text" && - typeof part.text === "string" && - (part.text.startsWith("") || part.text.includes("")) - ) && + !(part.type === "text" && typeof part.text === "string" && part.text.startsWith("")) && !part.synthetic, ) - const targetContent = targetIndex >= 0 ? parts[targetIndex] : parts[parts.length - 1] + const lastContent = targetIndex >= 0 ? parts[targetIndex] : parts[parts.length - 1] + // kilocode_change end if ( - targetContent && - typeof targetContent === "object" && - targetContent.type !== "tool-approval-request" && - targetContent.type !== "tool-approval-response" + lastContent && + typeof lastContent === "object" && + lastContent.type !== "tool-approval-request" && + lastContent.type !== "tool-approval-response" ) { - targetContent.providerOptions = mergeDeep(targetContent.providerOptions ?? {}, providerOptions) + lastContent.providerOptions = mergeDeep(lastContent.providerOptions ?? {}, providerOptions) continue } } - // kilocode_change end msg.providerOptions = mergeDeep(msg.providerOptions ?? {}, providerOptions) } @@ -480,7 +476,6 @@ 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" || @@ -490,18 +485,16 @@ 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" || + // kilocode_change start ((model.api.npm === "@ai-sdk/openai" || model.api.npm === "@ai-sdk/azure" || - model.api.npm === "@kilocode/kilo-gateway" || - model.providerID === "openai" || - model.providerID === "azure" || - model.providerID === "kilo") && + model.api.npm === "@kilocode/kilo-gateway") && supportsPromptCacheBreakpoint(model.api.id))) && + // kilocode_change end 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) From 7c7c11e33400a4640ad7b6cb031921964c7fe70b Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Sat, 8 Aug 2026 18:34:00 +0000 Subject: [PATCH 7/9] fix(cli): include modified alibaba line in kilocode_change block Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- packages/opencode/src/provider/transform.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index d366119a5f3..cf876f0c56c 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -484,8 +484,8 @@ 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" || // kilocode_change start + model.api.npm === "@ai-sdk/alibaba" || ((model.api.npm === "@ai-sdk/openai" || model.api.npm === "@ai-sdk/azure" || model.api.npm === "@kilocode/kilo-gateway") && From d3548d179bf9814e16cd4acdeb91411bd046aaac Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Sat, 8 Aug 2026 18:52:47 +0000 Subject: [PATCH 8/9] refactor(cli): remove part.synthetic and any-cast from applyCaching Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- packages/opencode/src/provider/transform.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index cf876f0c56c..d8b04db4662 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -388,18 +388,15 @@ function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage if (shouldUseContentOptions) { // kilocode_change start - place caching breakpoint on stable content before trailing - const parts = msg.content as any[] - const targetIndex = parts.findLastIndex( + const target = msg.content.findLast( (part) => - part && typeof part === "object" && + part !== null && part.type !== "tool-approval-request" && part.type !== "tool-approval-response" && - !(part.type === "text" && typeof part.text === "string" && part.text.startsWith("")) && - !part.synthetic, + !(part.type === "text" && part.text.startsWith("")), ) - const lastContent = targetIndex >= 0 ? parts[targetIndex] : parts[parts.length - 1] - // kilocode_change end + const lastContent = target ?? msg.content[msg.content.length - 1] if ( lastContent && typeof lastContent === "object" && @@ -409,6 +406,7 @@ function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage lastContent.providerOptions = mergeDeep(lastContent.providerOptions ?? {}, providerOptions) continue } + // kilocode_change end } msg.providerOptions = mergeDeep(msg.providerOptions ?? {}, providerOptions) @@ -476,6 +474,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" || @@ -484,17 +483,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" || - // kilocode_change start 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))) && - // kilocode_change end 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) From 71abd522b1fd8988046df726f08f9e804e8f45ff Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Sat, 8 Aug 2026 19:06:53 +0000 Subject: [PATCH 9/9] fix(cli): use typed loop and narrow kilocode_change block in applyCaching Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- packages/opencode/src/provider/transform.ts | 25 +++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index d8b04db4662..ff534bf107f 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -386,17 +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) { - // kilocode_change start - place caching breakpoint on stable content before trailing - const target = msg.content.findLast( - (part) => + // 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 !== null && part.type !== "tool-approval-request" && part.type !== "tool-approval-response" && - !(part.type === "text" && part.text.startsWith("")), - ) - const lastContent = target ?? msg.content[msg.content.length - 1] + !(part.type === "text" && part.text.startsWith("")) + ) { + targetIndex = i + break + } + } + const lastContent = targetIndex >= 0 ? parts[targetIndex] : parts[parts.length - 1] if ( lastContent && typeof lastContent === "object" && @@ -406,8 +413,8 @@ function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage lastContent.providerOptions = mergeDeep(lastContent.providerOptions ?? {}, providerOptions) continue } - // kilocode_change end } + // kilocode_change end msg.providerOptions = mergeDeep(msg.providerOptions ?? {}, providerOptions) }