Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions .changeset/openai-explicit-prompt-cache-breakpoints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
Comment thread
chrarnoldus marked this conversation as resolved.
Resolved
---

Set explicit prompt cache breakpoints on stable prefixes for OpenAI GPT-5.6+ models.
14 changes: 10 additions & 4 deletions packages/llm/src/cache-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Comment thread
chrarnoldus marked this conversation as resolved.
Resolved
])
// kilocode_change end

const makeHint = (ttlSeconds: number | undefined): CacheHint =>
ttlSeconds !== undefined ? new CacheHint({ type: "ephemeral", ttlSeconds }) : new CacheHint({ type: "ephemeral" })
Expand Down
87 changes: 76 additions & 11 deletions packages/llm/src/protocols/openai-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,24 @@ const IMAGE_MIMES = new Set<string>(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
// =============================================================================
Expand Down Expand Up @@ -57,15 +75,29 @@ const OpenAIChatAssistantToolCall = Schema.Struct({
type OpenAIChatAssistantToolCall = Schema.Schema.Type<typeof OpenAIChatAssistantToolCall>

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)]),
Expand Down Expand Up @@ -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<Schema.Schema.Type<typeof OpenAIChatUserContent>> = []
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 }
})

Expand Down Expand Up @@ -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<Schema.Schema.Type<typeof OpenAIChatUserContent>> = []
const flushImages = () => {
Expand Down Expand Up @@ -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
Expand Down
69 changes: 64 additions & 5 deletions packages/llm/src/protocols/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Comment thread
chrarnoldus marked this conversation as resolved.
Resolved
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<typeof OpenAIResponsesInputContent>
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<string>(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"])
})

Expand Down Expand Up @@ -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)

Expand All @@ -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
}

Expand Down
Loading
Loading