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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/keep-reasoning-output-budget.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---

Prevent encrypted reasoning state from incorrectly reducing the output token budget for long-running sessions.
9 changes: 5 additions & 4 deletions packages/opencode/src/kilocode/session/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ export namespace KiloLLM {
* Like opencode, the provider is the source of truth: when the last finished
* turn reported usage, `reported` carries that provider-tokenized context size
* (input + output + cache), which already accounts for image/vision input the
* client cannot see. The client-side media-normalized estimate (encoded bytes
* excluded) is used as a floor so newly added text or tool schemas still cap
* output, and as the sole basis on the first turn before any usage is reported.
* The larger of the two is used so the cap never under-counts.
* client cannot see. The client-side normalized estimate (encoded media and
* opaque reasoning-state bytes excluded) is used as a floor so newly added
* text or tool schemas still cap output, and as the sole basis on the first
* turn before any usage is reported. The larger of the two is used so the cap
* never under-counts.
*
* Many small models (e.g. qwen 7B, 32K context) ship with a default
* max_output of 32K, leaving no room for input once tools are included.
Expand Down
9 changes: 9 additions & 0 deletions packages/opencode/src/kilocode/session/overflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type { ModelMessage } from "ai"
const FACTOR = 1.3
const MEDIA = "[encoded media]"
const MEDIA_TOKENS = Token.estimate(MEDIA)
const OPAQUE = "[opaque reasoning state]"
const OPAQUE_TOKENS = Token.estimate(OPAQUE)

type Payload = {
messages: ModelMessage[]
Expand Down Expand Up @@ -46,6 +48,13 @@ export namespace KiloSessionOverflow {
export function measure(input: Payload) {
let extra = 0
const normalized = JSON.stringify(input.messages, function (this: unknown, key, value: unknown) {
// Providers replay encrypted reasoning state as an opaque continuation value.
// Its encoded byte length is not a token count and can be several times larger
// than the context the provider reports for the same request.
if (key === "reasoningEncryptedContent" && typeof value === "string") {
extra += Math.max(0, Token.estimate(value) - OPAQUE_TOKENS)
return OPAQUE
}
if (!["data", "url", "image"].includes(key)) return value
if (!this || typeof this !== "object" || !("type" in this)) return value
if (!["file", "image", "media"].includes(String(this.type))) return value
Expand Down
47 changes: 47 additions & 0 deletions packages/opencode/test/kilocode/session-overflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,53 @@ describe("Kilo request estimation", () => {
expect(KiloLLM.capOutputTokens({ model: mdl, messages, tools: {}, configured: 32_000, usage })).toBe(32_000)
})

test.each(["providerMetadata", "providerOptions"] as const)(
"does not reduce output for encrypted reasoning in %s",
(field) => {
const mdl = model({ context: 1_050_000, output: 128_000 })
const reasoning = {
type: "reasoning" as const,
text: "Checked the previous tool results.",
[field]: {
openai: {
itemId: "rs_1",
reasoningEncryptedContent: "x".repeat(3_200_000),
},
},
}
const messages = [
{ role: "assistant", content: [reasoning] },
{ role: "user", content: "Continue." },
] satisfies ModelMessage[]
const usage = KiloSessionOverflow.measure({ messages, tools: {} })

expect(usage.raw).toBeGreaterThan(1_000_000)
expect(usage.normalized).toBeLessThan(1_000)
expect(
KiloLLM.capOutputTokens({
model: mdl,
messages,
tools: {},
configured: 32_000,
usage,
reported: 624_205,
}),
).toBe(32_000)
},
)

test("still counts visible reasoning text", () => {
const mdl = model({ context: 200_000, output: 32_000 })
const messages = [
{
role: "assistant",
content: [{ type: "reasoning", text: "x".repeat(600_000) }],
},
] satisfies ModelMessage[]

expect(KiloLLM.capOutputTokens({ model: mdl, messages, tools: {}, configured: 32_000 })).toBeLessThan(32_000)
})

test("still reduces output for oversized text", () => {
const mdl = model({ context: 200_000, output: 32_000 })
const messages = [{ role: "user" as const, content: "x".repeat(600_000) }]
Expand Down
Loading