diff --git a/.changeset/preserve-image-output-capacity.md b/.changeset/preserve-image-output-capacity.md index affc86a6b1b..e3c196e64c5 100644 --- a/.changeset/preserve-image-output-capacity.md +++ b/.changeset/preserve-image-output-capacity.md @@ -2,4 +2,4 @@ "@kilocode/cli": patch --- -Preserve model output capacity when requests contain encoded images. +Preserve model output capacity when requests contain encoded images. The output token cap now uses the provider-reported context size from the previous turn, so image and vision input is measured by the provider instead of by encoded payload size. diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 1dbc56cbfb1..0a783ab3c7e 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1685,10 +1685,11 @@ export const layer = Layer.effect( tools, model, toolChoice: format.type === "json_schema" ? "required" : undefined, - // kilocode_change - feed the provider-reported context size from the last finished + // kilocode_change start - feed the provider-reported context size from the last finished // turn into the output-token cap, so image/vision input is measured by the provider // rather than by encoded payload bytes (see KiloLLM.capOutputTokens) reportedContextTokens: lastFinished ? KiloSessionOverflow.count(lastFinished.tokens) : undefined, + // kilocode_change end }) if (structured !== undefined) { diff --git a/packages/opencode/test/kilocode/session-overflow.test.ts b/packages/opencode/test/kilocode/session-overflow.test.ts index 6818cfee443..1c5075c0085 100644 --- a/packages/opencode/test/kilocode/session-overflow.test.ts +++ b/packages/opencode/test/kilocode/session-overflow.test.ts @@ -157,6 +157,42 @@ describe("Kilo request estimation", () => { expect(cap).toBeGreaterThanOrEqual(1_024) expect(cap).toBeLessThan(32_000) }) + + test("prefers provider-reported context over the client estimate for images", () => { + // The client cannot price encoded image bytes, but the provider reported a + // large vision-token cost for the last turn. + const mdl = model({ context: 300_000, output: 32_000 }) + const messages = [ + { + role: "user", + content: [{ type: "image", image: `data:image/png;base64,${"x".repeat(600_000)}` }], + }, + ] satisfies ModelMessage[] + + // Without reported usage the media-normalized estimate leaves output untouched. + expect(KiloLLM.capOutputTokens({ model: mdl, messages, tools: {}, configured: 32_000 })).toBe(32_000) + + // With the provider-reported context size, output is capped to fit real usage. + expect(KiloLLM.capOutputTokens({ model: mdl, messages, tools: {}, configured: 32_000, reported: 280_000 })).toBe( + 17_952, + ) + }) + + test("uses the media-normalized floor when reported usage is smaller", () => { + const mdl = model({ context: 200_000, output: 32_000 }) + const messages = [{ role: "user" as const, content: "x".repeat(600_000) }] + + const withoutReported = KiloLLM.capOutputTokens({ model: mdl, messages, tools: {}, configured: 32_000 }) + const withStaleReported = KiloLLM.capOutputTokens({ + model: mdl, + messages, + tools: {}, + configured: 32_000, + reported: 1_000, + }) + expect(withStaleReported).toBe(withoutReported) + expect(withStaleReported).toBeLessThan(32_000) + }) }) describe("Kilo preflight compaction", () => {