From f46aa6ae9e3a9fc85e7b143974e847f83e7f37f2 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 31 Jul 2026 14:24:39 +0200 Subject: [PATCH 1/2] fix(cli): preserve Kilo prompt cache keys --- .changeset/gpt-56-kilo-prompt-cache.md | 5 ++ .../opencode/src/kilocode/provider-options.ts | 2 + packages/opencode/src/provider/transform.ts | 1 + .../kilocode/provider/prompt-cache.test.ts | 63 +++++++++++++++++++ 4 files changed, 71 insertions(+) create mode 100644 .changeset/gpt-56-kilo-prompt-cache.md create mode 100644 packages/opencode/test/kilocode/provider/prompt-cache.test.ts diff --git a/.changeset/gpt-56-kilo-prompt-cache.md b/.changeset/gpt-56-kilo-prompt-cache.md new file mode 100644 index 00000000000..f40ab5c8c74 --- /dev/null +++ b/.changeset/gpt-56-kilo-prompt-cache.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Preserve Kilo session prompt cache keys for GPT-5.6 Responses requests. diff --git a/packages/opencode/src/kilocode/provider-options.ts b/packages/opencode/src/kilocode/provider-options.ts index 6c929def90f..a9b49ad6d29 100644 --- a/packages/opencode/src/kilocode/provider-options.ts +++ b/packages/opencode/src/kilocode/provider-options.ts @@ -9,6 +9,7 @@ export function kiloProviderOptions(options: { [x: string]: any }) { const result: Record = {} const openrouter = options as OpenRouterProviderOptions & { verbosity?: "high" | "medium" | "low" + promptCacheKey?: string } result.openrouter = openrouter result.openai = { @@ -17,6 +18,7 @@ export function kiloProviderOptions(options: { [x: string]: any }) { textVerbosity: openrouter.verbosity, store: false, forceReasoning: openrouter.reasoning?.enabled, + promptCacheKey: openrouter.promptCacheKey, } satisfies OpenAIResponsesProviderOptions result.anthropic = { thinking: { type: openrouter.reasoning?.enabled ? "adaptive" : "disabled" }, diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index c0b14184d08..f085c9d21e8 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -1218,6 +1218,7 @@ export function options(input: { input.providerOptions?.setCacheKey !== false && (input.model.providerID === "openai" || input.model.api.npm === "@ai-sdk/xai" || + input.model.api.npm === "@kilocode/kilo-gateway" || input.providerOptions?.setCacheKey) ) { result["promptCacheKey"] = input.sessionID diff --git a/packages/opencode/test/kilocode/provider/prompt-cache.test.ts b/packages/opencode/test/kilocode/provider/prompt-cache.test.ts new file mode 100644 index 00000000000..7e9817c770a --- /dev/null +++ b/packages/opencode/test/kilocode/provider/prompt-cache.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, test } from "bun:test" +import { streamText } from "ai" +import { createKilo } from "@kilocode/kilo-gateway" +import { ProviderTransform } from "@/provider/transform" + +const model = { + id: "openai/gpt-5.6-sol", + providerID: "kilo", + api: { + id: "openai/gpt-5.6-sol", + url: "https://api.kilo.ai", + npm: "@kilocode/kilo-gateway", + }, + capabilities: { reasoning: true }, + limit: { output: 128_000 }, +} as any + +describe("Kilo GPT-5.6 prompt cache options", () => { + test("preserves the session key through Kilo provider options", () => { + const sessionID = "ses_cache_probe" + const options = ProviderTransform.options({ model, sessionID, providerOptions: {} }) + const providerOptions = ProviderTransform.providerOptions(model, options) + + expect(options.promptCacheKey).toBe(sessionID) + expect(providerOptions.openai.promptCacheKey).toBe(sessionID) + }) + + test("allows Kilo provider configuration to disable the session key", () => { + const options = ProviderTransform.options({ + model, + sessionID: "ses_cache_probe", + providerOptions: { setCacheKey: false }, + }) + + expect(options.promptCacheKey).toBeUndefined() + }) + + test("sends the session key in the Kilo Responses body", async () => { + let body: Record | undefined + const sdk = createKilo({ + kilocodeToken: "test", + fetch: (async (_input, init) => { + body = JSON.parse(String(init?.body)) as Record + return new Response("data: [DONE]\n\n", { headers: { "content-type": "text/event-stream" } }) + }) as typeof fetch, + }) + const sessionID = "ses_cache_probe" + const options = ProviderTransform.options({ model, sessionID, providerOptions: {} }) + const providerOptions = ProviderTransform.providerOptions(model, options) + + for await (const _ of ( + await streamText({ + model: sdk.openai("openai/gpt-5.6-sol"), + prompt: "Hi", + providerOptions, + }) + ).fullStream) { + // Consume the stream so the lazy request executes. + } + + expect(body?.prompt_cache_key).toBe(sessionID) + }) +}) From 0cf3648f8ab8cf602fe2d0116f7ba0501ffacb18 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Mon, 3 Aug 2026 11:11:06 +0200 Subject: [PATCH 2/2] fix(cli): defer threshold compaction during tool loops --- .changeset/compaction-threshold-boundary.md | 5 ++ .changeset/gpt-56-kilo-prompt-cache.md | 5 -- .../opencode/src/kilocode/provider-options.ts | 2 - packages/opencode/src/provider/transform.ts | 1 - packages/opencode/src/session/overflow.ts | 5 +- .../kilocode/provider/prompt-cache.test.ts | 63 ------------------- .../test/kilocode/session-overflow.test.ts | 17 ++--- 7 files changed, 16 insertions(+), 82 deletions(-) create mode 100644 .changeset/compaction-threshold-boundary.md delete mode 100644 .changeset/gpt-56-kilo-prompt-cache.md delete mode 100644 packages/opencode/test/kilocode/provider/prompt-cache.test.ts diff --git a/.changeset/compaction-threshold-boundary.md b/.changeset/compaction-threshold-boundary.md new file mode 100644 index 00000000000..2e610dbefb0 --- /dev/null +++ b/.changeset/compaction-threshold-boundary.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Prevent configured compaction thresholds from interrupting active tool sequences. diff --git a/.changeset/gpt-56-kilo-prompt-cache.md b/.changeset/gpt-56-kilo-prompt-cache.md deleted file mode 100644 index f40ab5c8c74..00000000000 --- a/.changeset/gpt-56-kilo-prompt-cache.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Preserve Kilo session prompt cache keys for GPT-5.6 Responses requests. diff --git a/packages/opencode/src/kilocode/provider-options.ts b/packages/opencode/src/kilocode/provider-options.ts index a9b49ad6d29..6c929def90f 100644 --- a/packages/opencode/src/kilocode/provider-options.ts +++ b/packages/opencode/src/kilocode/provider-options.ts @@ -9,7 +9,6 @@ export function kiloProviderOptions(options: { [x: string]: any }) { const result: Record = {} const openrouter = options as OpenRouterProviderOptions & { verbosity?: "high" | "medium" | "low" - promptCacheKey?: string } result.openrouter = openrouter result.openai = { @@ -18,7 +17,6 @@ export function kiloProviderOptions(options: { [x: string]: any }) { textVerbosity: openrouter.verbosity, store: false, forceReasoning: openrouter.reasoning?.enabled, - promptCacheKey: openrouter.promptCacheKey, } satisfies OpenAIResponsesProviderOptions result.anthropic = { thinking: { type: openrouter.reasoning?.enabled ? "adaptive" : "disabled" }, diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index f085c9d21e8..c0b14184d08 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -1218,7 +1218,6 @@ export function options(input: { input.providerOptions?.setCacheKey !== false && (input.model.providerID === "openai" || input.model.api.npm === "@ai-sdk/xai" || - input.model.api.npm === "@kilocode/kilo-gateway" || input.providerOptions?.setCacheKey) ) { result["promptCacheKey"] = input.sessionID diff --git a/packages/opencode/src/session/overflow.ts b/packages/opencode/src/session/overflow.ts index 64b2b795531..bd462f11b99 100644 --- a/packages/opencode/src/session/overflow.ts +++ b/packages/opencode/src/session/overflow.ts @@ -30,8 +30,7 @@ export function isOverflow(input: { if (input.model.limit.context === 0) return false const count = KiloSessionOverflow.count(input.tokens) // kilocode_change - // kilocode_change start - const cap = KiloSessionOverflow.limit({ cfg: input.cfg, model: input.model, usable: usable(input) }) - return count >= cap + // kilocode_change start - post-step checks are safety-only; economic thresholds run in preflight + return count >= usable(input) // kilocode_change end } diff --git a/packages/opencode/test/kilocode/provider/prompt-cache.test.ts b/packages/opencode/test/kilocode/provider/prompt-cache.test.ts deleted file mode 100644 index 7e9817c770a..00000000000 --- a/packages/opencode/test/kilocode/provider/prompt-cache.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { streamText } from "ai" -import { createKilo } from "@kilocode/kilo-gateway" -import { ProviderTransform } from "@/provider/transform" - -const model = { - id: "openai/gpt-5.6-sol", - providerID: "kilo", - api: { - id: "openai/gpt-5.6-sol", - url: "https://api.kilo.ai", - npm: "@kilocode/kilo-gateway", - }, - capabilities: { reasoning: true }, - limit: { output: 128_000 }, -} as any - -describe("Kilo GPT-5.6 prompt cache options", () => { - test("preserves the session key through Kilo provider options", () => { - const sessionID = "ses_cache_probe" - const options = ProviderTransform.options({ model, sessionID, providerOptions: {} }) - const providerOptions = ProviderTransform.providerOptions(model, options) - - expect(options.promptCacheKey).toBe(sessionID) - expect(providerOptions.openai.promptCacheKey).toBe(sessionID) - }) - - test("allows Kilo provider configuration to disable the session key", () => { - const options = ProviderTransform.options({ - model, - sessionID: "ses_cache_probe", - providerOptions: { setCacheKey: false }, - }) - - expect(options.promptCacheKey).toBeUndefined() - }) - - test("sends the session key in the Kilo Responses body", async () => { - let body: Record | undefined - const sdk = createKilo({ - kilocodeToken: "test", - fetch: (async (_input, init) => { - body = JSON.parse(String(init?.body)) as Record - return new Response("data: [DONE]\n\n", { headers: { "content-type": "text/event-stream" } }) - }) as typeof fetch, - }) - const sessionID = "ses_cache_probe" - const options = ProviderTransform.options({ model, sessionID, providerOptions: {} }) - const providerOptions = ProviderTransform.providerOptions(model, options) - - for await (const _ of ( - await streamText({ - model: sdk.openai("openai/gpt-5.6-sol"), - prompt: "Hi", - providerOptions, - }) - ).fullStream) { - // Consume the stream so the lazy request executes. - } - - expect(body?.prompt_cache_key).toBe(sessionID) - }) -}) diff --git a/packages/opencode/test/kilocode/session-overflow.test.ts b/packages/opencode/test/kilocode/session-overflow.test.ts index 4cca5369492..7f5c67afa16 100644 --- a/packages/opencode/test/kilocode/session-overflow.test.ts +++ b/packages/opencode/test/kilocode/session-overflow.test.ts @@ -47,16 +47,17 @@ function tokens(count: number): MessageV2.Assistant["tokens"] { return { input: count, output: 0, reasoning: 0, cache: { read: 0, write: 0 } } } -describe("Kilo auto-compaction threshold", () => { - test("triggers at the configured context percentage", () => { +describe("Kilo post-step compaction safety", () => { + test("ignores the configured threshold after a provider step", () => { const conf = cfg({ threshold_percent: 75 }) const mdl = model({ context: 200_000, output: 32_000 }) expect(isOverflow({ cfg: conf, model: mdl, tokens: tokens(149_999) })).toBe(false) - expect(isOverflow({ cfg: conf, model: mdl, tokens: tokens(150_000) })).toBe(true) + expect(isOverflow({ cfg: conf, model: mdl, tokens: tokens(167_999) })).toBe(false) + expect(isOverflow({ cfg: conf, model: mdl, tokens: tokens(168_000) })).toBe(true) }) - test("keeps the reserved safety trigger when it is lower", () => { + test("uses the usable context limit when the threshold is high", () => { const conf = cfg({ threshold_percent: 95 }) const mdl = model({ context: 200_000, output: 32_000 }) @@ -68,8 +69,8 @@ describe("Kilo auto-compaction threshold", () => { const conf = cfg({ threshold_percent: 75 }) const mdl = model({ context: 400_000, input: 200_000, output: 32_000 }) - expect(isOverflow({ cfg: conf, model: mdl, tokens: tokens(149_999) })).toBe(false) - expect(isOverflow({ cfg: conf, model: mdl, tokens: tokens(150_000) })).toBe(true) + expect(isOverflow({ cfg: conf, model: mdl, tokens: tokens(179_999) })).toBe(false) + expect(isOverflow({ cfg: conf, model: mdl, tokens: tokens(180_000) })).toBe(true) }) test("ignores a cleared threshold", () => { @@ -114,14 +115,14 @@ describe("Kilo auto-compaction threshold", () => { const conf = cfg({ threshold_percent: 75 }) const mdl = model({ context: 200_000, output: 32_000 }) - expect(isOverflow({ cfg: conf, model: mdl, tokens: { ...tokens(149_999), reasoning: 1 } })).toBe(true) + expect(isOverflow({ cfg: conf, model: mdl, tokens: { ...tokens(167_999), reasoning: 1 } })).toBe(true) }) test("falls back to provider total when normalized usage is unavailable", () => { const conf = cfg({ threshold_percent: 75 }) const mdl = model({ context: 200_000, output: 32_000 }) - expect(isOverflow({ cfg: conf, model: mdl, tokens: { ...tokens(0), total: 150_000 } })).toBe(true) + expect(isOverflow({ cfg: conf, model: mdl, tokens: { ...tokens(0), total: 168_000 } })).toBe(true) }) test("uses the output cap as the reserve for single-window gateway models", () => {