From 1586f59fb4e3eb6f03216f4e9cfa82ec4c253781 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Sat, 29 Aug 2026 19:13:48 -0700 Subject: [PATCH 01/32] fix: clamp the output-token reservation against the context window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ProviderTransform.maxOutputTokens` is a per-model ceiling that never reads `limit.context`, so on a model where the prompt and the completion share one window a large system prompt pushed `input + reservation` past that window and the provider rejected the request with a hard 400 before generating anything. Measured case: a ~52,180-token prompt plus the 16,384-token reservation on a 65,536-token window. Compaction cannot cover this. `Compaction.isOverflow` runs off the previous assistant message's token counts, so there is nothing to check on the first request of a session, and compaction only shortens conversation messages — never the system prompt, which is where the whole overflow lives here. The failure was also not classified as an overflow: the provider's wording ("maximum context length **of** 65536 tokens") matches no pattern in `OVERFLOW_PATTERNS`, the status is 400 rather than 413, and the body carries no `context_length_exceeded` code. It surfaced as a generic non-retryable `APIError` and the session died showing raw provider text. - add `ProviderTransform.clampOutputTokens`, which shrinks the reservation so `input + reservation` fits the window, and `estimateInputTokens` to size the prompt about to be sent - throw `OutputTokenBudgetError` before the request when clamping cannot leave `OUTPUT_TOKEN_FLOOR` (1,024) tokens, naming the prompt size, the requested reservation, the window, and the three ways to fix it - keep a 2% (minimum 512-token) margin, since the input count is a character-ratio estimate rather than the provider's tokenizer - apply the clamp in `session/llm.ts` after the `chat.params` hook so plugin overrides are checked too, and in the `session/llm/request.ts` twin - leave untouched: configs that already fit, models that budget input separately via `limit.input`, models declaring no window, and windows too small to hold a floor-sized completion (those limits are not credible enough to fail a request on) 11 tests cover the reported case, the floor, and the unchanged paths. --- packages/opencode/src/provider/transform.ts | 109 ++++++++++ packages/opencode/src/session/llm.ts | 17 +- packages/opencode/src/session/llm/request.ts | 18 +- .../opencode/test/provider/transform.test.ts | 199 ++++++++++++++++++ 4 files changed, 340 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 446b1a5036..a6d4a795eb 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -6,6 +6,10 @@ import type { Provider } from "./provider" import type { ModelsDev } from "./models" import { iife } from "@/util/iife" import { Flag } from "@/flag/flag" +// altimate_change start — output-token clamp needs prompt sizing and a logger +import { Token } from "@/util/token" +import { Log } from "@/util/log" +// altimate_change end type Modality = NonNullable["input"][number] @@ -19,6 +23,9 @@ function mimeToModality(mime: string): Modality | undefined { export namespace ProviderTransform { export const OUTPUT_TOKEN_MAX = Flag.OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX || 32_000 + // altimate_change start — logger for the output-token clamp below + const log = Log.create({ service: "provider.transform" }) + // altimate_change end // altimate_change start — keep OpenAI encrypted reasoning include values consistent across transforms const INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"] as const @@ -1278,6 +1285,108 @@ export namespace ProviderTransform { // altimate_change end } + // altimate_change start — clamp the reserved completion budget against the real prompt size. + // + // `maxOutputTokens` above is a per-model ceiling. It never reads `limit.context`, so on a + // model where input and completion share one window a large system prompt can push + // `input + reservation` past that window and the provider rejects the request with a hard + // 400 before generating anything. The client already knows all three numbers, so it can + // either shrink the reservation to fit or refuse with an actionable message. + + /** + * Smallest completion budget worth sending. Below this an agent turn cannot reliably emit + * even one tool call, so a request that would clamp this far is refused instead of being + * sent to come back as a silently truncated response. + */ + export const OUTPUT_TOKEN_FLOOR = 1_024 + + // `inputTokens` is a character-ratio estimate, not the provider's tokenizer. Measured drift + // against a provider's own accounting on a ~52K prompt was under 1%, so a clamped budget + // keeps a 2% (minimum 512-token) margin rather than filling the window exactly. + const CLAMP_MARGIN_FRACTION = 0.02 + const CLAMP_MARGIN_MIN = 512 + + /** Thrown before the request is sent when no usable completion budget fits in the window. */ + export class OutputTokenBudgetError extends Error { + constructor( + readonly info: { + modelID: string + providerID: string + inputTokens: number + requested: number + context: number + floor: number + }, + ) { + super( + [ + `Context budget exceeded before the request was sent.`, + `${info.providerID}/${info.modelID} declares a ${info.context}-token context window,`, + `the prompt is ~${info.inputTokens} tokens, and ${info.requested} tokens are reserved for`, + `the completion — ${info.inputTokens + info.requested} in total.`, + `Even after clamping, fewer than ${info.floor} tokens would remain for the response.`, + `Reduce the system prompt (fewer instructions, skills, or AGENTS.md content), lower the`, + `output reservation via OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX, or use a model with a`, + `larger context window.`, + ].join(" "), + ) + this.name = "OutputTokenBudgetError" + } + } + + /** Rough token count for the prompt about to be sent, used only for the clamp decision. */ + export function estimateInputTokens(system: string[], messages: unknown[]): number { + return Token.estimate(system.join("\n")) + Token.estimate(JSON.stringify(messages)) + } + + /** + * Shrink `requested` so `inputTokens + result` fits `model.limit.context`. + * + * Returns `requested` unchanged whenever it already fits, when the caller omitted it, when the + * model declares no context window, when the declared window is too small to hold even a + * floor-sized completion (those limits are not credible enough to fail a request on — the + * provider stays the authority), or when the model budgets input separately via `limit.input` + * (there the two budgets are not shared and clamping would be wrong). + * Throws `OutputTokenBudgetError` when the remaining budget is below `OUTPUT_TOKEN_FLOOR`. + */ + export function clampOutputTokens(input: { + model: Provider.Model + requested: number | undefined + inputTokens: number + }): number | undefined { + const requested = input.requested + if (requested === undefined) return undefined + + const context = input.model.limit.context + if (!context || context <= OUTPUT_TOKEN_FLOOR) return requested + if (input.model.limit.input) return requested + if (input.inputTokens <= 0) return requested + if (input.inputTokens + requested <= context) return requested + + const margin = Math.max(CLAMP_MARGIN_MIN, Math.ceil(input.inputTokens * CLAMP_MARGIN_FRACTION)) + const clamped = context - input.inputTokens - margin + if (clamped < OUTPUT_TOKEN_FLOOR) { + throw new OutputTokenBudgetError({ + modelID: input.model.id, + providerID: input.model.providerID, + inputTokens: input.inputTokens, + requested, + context, + floor: OUTPUT_TOKEN_FLOOR, + }) + } + log.warn("clamped output token reservation to fit context window", { + providerID: input.model.providerID, + modelID: input.model.id, + context, + inputTokens: input.inputTokens, + requested, + clamped, + }) + return clamped + } + // altimate_change end + // altimate_change start — lower MCP/tool JSON Schema to provider-compatible subsets type JsonRecord = Record diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 893f4dda4d..62eddc836a 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -150,6 +150,18 @@ export namespace LLM { ) // altimate_change end + // altimate_change start — clamp the reserved completion budget against the real prompt size. + // `maxOutputTokens` is a per-model ceiling that ignores `limit.context`, so a large system + // prompt could push `input + reservation` past the window and the provider rejected the + // request with a hard 400 before generating anything. Clamped after the chat.params hook so + // a plugin override is checked too. Throws when no usable budget is left. + const maxOutputTokens = ProviderTransform.clampOutputTokens({ + model: input.model, + requested: params.maxOutputTokens, + inputTokens: ProviderTransform.estimateInputTokens(system, input.messages), + }) + // altimate_change end + const { headers } = await Plugin.trigger( "chat.headers", { @@ -249,8 +261,9 @@ export namespace LLM { activeTools: Object.keys(tools).filter((x) => x !== "invalid"), tools, toolChoice: input.toolChoice, - // altimate_change start — read maxOutputTokens from params (now plumbed through chat.params hook) - maxOutputTokens: params.maxOutputTokens, + // altimate_change start — read maxOutputTokens from params (now plumbed through chat.params + // hook), clamped above so it cannot exceed the model's context window + maxOutputTokens, // altimate_change end abortSignal: input.abort, headers: { diff --git a/packages/opencode/src/session/llm/request.ts b/packages/opencode/src/session/llm/request.ts index 2785d98526..07c43f1237 100644 --- a/packages/opencode/src/session/llm/request.ts +++ b/packages/opencode/src/session/llm/request.ts @@ -168,11 +168,27 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre ? (yield* InstanceState.context).project.id : undefined + // altimate_change start — clamp the reserved completion budget against the real prompt size. + // `maxOutputTokens` is a per-model ceiling that ignores `limit.context`, so a large system + // prompt could push `input + reservation` past the window and the provider rejected the request + // with a hard 400 before generating anything. `input.messages` is used rather than the merged + // `messages` because the latter can already carry `system` as leading system messages, which + // would double-count the prompt. Throws when no usable budget is left. + const clampedParams = { + ...params, + maxOutputTokens: ProviderTransform.clampOutputTokens({ + model: input.model, + requested: params.maxOutputTokens, + inputTokens: ProviderTransform.estimateInputTokens(system, input.messages), + }), + } + // altimate_change end + return { system, messages, tools: Object.fromEntries(Object.entries(tools).toSorted(([a], [b]) => a.localeCompare(b))), - params, + params: clampedParams, messageTransformOptions: options, headers: { ...(input.model.providerID.startsWith("opencode") diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 001aab94f3..28921f99ef 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test" import { Effect } from "effect" +import type { ModelMessage } from "ai" import { ProviderTransform } from "@/provider/transform" import { LLMRequestPrep } from "@/session/llm/request" // ProviderTransform.message expects a Provider.Model with the fork's ModelID/ProviderID brands. @@ -4650,3 +4651,201 @@ describe("ProviderTransform.providerOptions - ai-gateway-provider", () => { expect(result).toEqual({ openaiCompatible: { reasoningEffort: "high" } }) }) }) + +// Regression suite for the output-token reservation exceeding the context window. +// +// Reported failure: with a large system prompt on a model declaring a 65,536-token window, the +// shipped 16,384-token reservation produced a hard provider 400 before any model work — +// "You requested a total of 68564 tokens: 52180 tokens from the input messages and 16384 tokens +// for the completion". The reservation was applied without ever being compared to the input size. +describe("ProviderTransform.clampOutputTokens", () => { + const createWindowModel = (limit: { context: number; input?: number; output: number }) => + ({ + id: "large-window-model", + providerID: "openai-compatible", + api: { id: "large-window-model", url: "https://example.invalid/v1", npm: "@ai-sdk/openai-compatible" }, + name: "Large Window Model", + capabilities: { + temperature: true, + reasoning: false, + attachment: false, + toolcall: true, + input: { text: true, audio: false, image: false, video: false, pdf: false }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + cost: { input: 1, output: 1, cache: { read: 0, write: 0 } }, + limit, + status: "active", + options: {}, + headers: {}, + }) as any + + // The exact reported case. + const REPORTED = { inputTokens: 52_180, requested: 16_384, context: 65_536 } + + test("the reported case is clamped instead of being sent as-is", () => { + const model = createWindowModel({ context: REPORTED.context, output: 16_384 }) + const result = ProviderTransform.clampOutputTokens({ + model, + requested: REPORTED.requested, + inputTokens: REPORTED.inputTokens, + })! + + // Unclamped, this is exactly the request the provider rejected. + expect(REPORTED.inputTokens + REPORTED.requested).toBeGreaterThan(REPORTED.context) + expect(result).toBeLessThan(REPORTED.requested) + // The clamped request fits, with the estimator margin (2%, min 512) still spare. + expect(REPORTED.inputTokens + result).toBeLessThanOrEqual(REPORTED.context) + // 65536 - 52180 - ceil(52180 * 0.02) = 12312 + expect(result).toBe(12_312) + // Still a usable completion budget, not a stub. + expect(result).toBeGreaterThanOrEqual(ProviderTransform.OUTPUT_TOKEN_FLOOR) + }) + + test("throws with the actual numbers when even the floor does not fit", () => { + const model = createWindowModel({ context: 65_536, output: 16_384 }) + let thrown: unknown + try { + ProviderTransform.clampOutputTokens({ model, requested: 16_384, inputTokens: 65_000 }) + } catch (e) { + thrown = e + } + expect(thrown).toBeInstanceOf(ProviderTransform.OutputTokenBudgetError) + const message = (thrown as Error).message + // The message must name input tokens, the requested reservation and the window. + expect(message).toContain("65000") + expect(message).toContain("16384") + expect(message).toContain("65536") + expect(message).toContain("OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX") + }) + + test("throws rather than clamping to an unusable budget just above zero", () => { + const model = createWindowModel({ context: 65_536, output: 16_384 }) + // 65536 - 65000 = 536 would "fit" arithmetically but is below the floor. + expect(536).toBeLessThan(ProviderTransform.OUTPUT_TOKEN_FLOOR) + expect(() => ProviderTransform.clampOutputTokens({ model, requested: 16_384, inputTokens: 65_000 })).toThrow() + }) + + test("leaves a config that already fits completely unchanged", () => { + const model = createWindowModel({ context: 65_536, output: 16_384 }) + expect(ProviderTransform.clampOutputTokens({ model, requested: 16_384, inputTokens: 10_000 })).toBe(16_384) + // Exactly filling the window is still a valid request and must not be touched. + expect(ProviderTransform.clampOutputTokens({ model, requested: 16_384, inputTokens: 49_152 })).toBe(16_384) + }) + + test("leaves large-window models unchanged", () => { + const model = createWindowModel({ context: 200_000, output: 8_192 }) + expect(ProviderTransform.clampOutputTokens({ model, requested: 8_192, inputTokens: 150_000 })).toBe(8_192) + }) + + test("does not clamp models that budget input separately from output", () => { + // limit.input means the two budgets are not shared, so input + output can exceed context. + const model = createWindowModel({ context: 65_536, input: 65_536, output: 16_384 }) + expect(ProviderTransform.clampOutputTokens({ model, requested: 16_384, inputTokens: 60_000 })).toBe(16_384) + }) + + test("does not clamp when the model declares no context window", () => { + const model = createWindowModel({ context: 0, output: 16_384 }) + expect(ProviderTransform.clampOutputTokens({ model, requested: 16_384, inputTokens: 60_000 })).toBe(16_384) + }) + + test("does not clamp a window too small to hold even a floor-sized completion", () => { + // A declared window this small is a placeholder or a test fixture, not a real limit. Failing + // the request client-side on numbers we do not believe would be worse than letting the + // provider answer, so the guard stays out of the way. + const model = createWindowModel({ context: 20, output: 10 }) + expect(ProviderTransform.clampOutputTokens({ model, requested: 10, inputTokens: 11 })).toBe(10) + }) + + test("passes an omitted reservation through untouched", () => { + // Codex and GitHub Copilot deliberately send no maxOutputTokens. + const model = createWindowModel({ context: 65_536, output: 16_384 }) + expect(ProviderTransform.clampOutputTokens({ model, requested: undefined, inputTokens: 60_000 })).toBeUndefined() + }) +}) + +describe("LLMRequestPrep.prepare - output token reservation", () => { + const sessionID = "ses_clamp-test" + + const model = { + id: "large-window-model", + providerID: "openai-compatible", + api: { id: "large-window-model", url: "https://example.invalid/v1", npm: "@ai-sdk/openai-compatible" }, + name: "Large Window Model", + capabilities: { + temperature: true, + reasoning: false, + attachment: false, + toolcall: true, + input: { text: true, audio: false, image: false, video: false, pdf: false }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + cost: { input: 1, output: 1, cache: { read: 0, write: 0 } }, + limit: { context: 65_536, output: 16_384 }, + status: "active", + options: {}, + headers: {}, + } as any + + const messages: ModelMessage[] = [{ role: "user", content: "Hello" }] + + const run = (systemPrompt: string) => + Effect.runPromise( + LLMRequestPrep.prepare({ + user: { + id: "msg_user-clamp", + sessionID, + role: "user", + time: { created: Date.now() }, + agent: "test", + model: { providerID: "openai-compatible", modelID: "large-window-model" }, + } as any, + sessionID, + model, + agent: { name: "test", mode: "primary", options: {}, permission: [], prompt: systemPrompt } as any, + system: [], + messages, + tools: {}, + provider: { id: "openai-compatible", options: {} } as any, + auth: undefined, + plugin: { + trigger: (_name: string, _input: unknown, output: unknown) => Effect.succeed(output), + list: () => Effect.succeed([]), + init: () => Effect.void, + } as any, + flags: { outputTokenMax: 32_000, client: "test" } as any, + isWorkflow: false, + }), + ) + + // Prose with no code or JSON characters, so Token.estimate uses its default 3.7 chars/token. + const PROSE = "the quick brown fox jumps over the lazy dog " + const largePrompt = PROSE.repeat(Math.ceil((52_180 * 3.7) / PROSE.length)) + + test("a ~52K-token system prompt does not produce an unclamped request", async () => { + const estimated = ProviderTransform.estimateInputTokens([largePrompt], messages) + // Sized to reproduce the reported 52,180-token prompt. + expect(estimated).toBeGreaterThan(51_500) + expect(estimated).toBeLessThan(53_500) + // Unclamped this is the request the provider rejected: 52,180 + 16,384 > 65,536. + expect(estimated + 16_384).toBeGreaterThan(65_536) + + const result = await run(largePrompt) + const maxOutputTokens = result.params.maxOutputTokens! + expect(maxOutputTokens).toBeLessThan(16_384) + expect(estimated + maxOutputTokens).toBeLessThanOrEqual(65_536) + expect(maxOutputTokens).toBeGreaterThanOrEqual(ProviderTransform.OUTPUT_TOKEN_FLOOR) + }) + + test("a small system prompt keeps the full model reservation", async () => { + const result = await run("You are a helpful assistant.") + expect(result.params.maxOutputTokens).toBe(16_384) + }) + + test("a system prompt that leaves no usable budget fails before the request is built", async () => { + const hugePrompt = PROSE.repeat(Math.ceil((65_000 * 3.7) / PROSE.length)) + await expect(run(hugePrompt)).rejects.toThrow(/Context budget exceeded/) + }) +}) From 8c289cb513096ee8f07d413846a6ffb4e326daef Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Sat, 29 Aug 2026 19:18:35 -0700 Subject: [PATCH 02/32] fix: mark the clamped-params return line in the upstream-shared request builder Marker Guard diffs against `origin/main`, which treats `session/llm/request.ts` as upstream-shared. The changed `params:` line in the returned object sat outside any `altimate_change` block, so the strict check failed. The local run missed it because a stale `main` ref put the file outside the shared set. No behaviour change. --- packages/opencode/src/session/llm/request.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/opencode/src/session/llm/request.ts b/packages/opencode/src/session/llm/request.ts index 07c43f1237..9c94febdd4 100644 --- a/packages/opencode/src/session/llm/request.ts +++ b/packages/opencode/src/session/llm/request.ts @@ -188,7 +188,9 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre system, messages, tools: Object.fromEntries(Object.entries(tools).toSorted(([a], [b]) => a.localeCompare(b))), + // altimate_change start — return the context-window-clamped params built above params: clampedParams, + // altimate_change end messageTransformOptions: options, headers: { ...(input.model.providerID.startsWith("opencode") From ac3d78290cc44d9c27d4398f77add0e3cebadc7c Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Sun, 30 Aug 2026 00:19:46 -0700 Subject: [PATCH 03/32] fix(provider): harden output-token budgeting --- .../src/provider/output-token-budget.ts | 350 ++++++++++++++++++ packages/opencode/src/provider/transform.ts | 161 ++------ packages/opencode/src/session/llm.ts | 54 ++- packages/opencode/src/session/llm/request.ts | 43 ++- .../opencode/test/provider/transform.test.ts | 225 +++++++++-- packages/opencode/test/session/llm.test.ts | 103 +++++- .../test/upstream/bridge-merge-e2e.test.ts | 13 +- 7 files changed, 753 insertions(+), 196 deletions(-) create mode 100644 packages/opencode/src/provider/output-token-budget.ts diff --git a/packages/opencode/src/provider/output-token-budget.ts b/packages/opencode/src/provider/output-token-budget.ts new file mode 100644 index 0000000000..7696c83048 --- /dev/null +++ b/packages/opencode/src/provider/output-token-budget.ts @@ -0,0 +1,350 @@ +// altimate_change start — keep output reservations inside the provider context window +import type { Provider } from "./provider" +import { Log } from "@/util/log" +import { Token } from "@/util/token" + +const log = Log.create({ service: "provider.output-token-budget" }) + +/** Smallest completion budget worth sending for an agent turn. */ +export const OUTPUT_TOKEN_FLOOR = 1_024 + +const CLAMP_MARGIN_FRACTION = 0.02 +const CLAMP_MARGIN_MIN = 512 +const ESTIMATE_CHUNK_SIZE = 400 +const MEDIA_TOKEN_ALLOWANCE = 2_048 +const MIN_REASONING_BUDGET = 1_024 +const MEDIA_DATA_URL = /^data:(?:image\/|audio\/|video\/|application\/pdf(?:;|,))[^,]*,/i +const EMOJI = /\p{Extended_Pictographic}/u +const REASONING_BUDGET_KEYS = new Set(["budgetTokens", "thinkingBudget", "budget_tokens"]) +const CONTEXT_WINDOW_BETAS = new Map([["context-1m-2025-08-07", 1_000_000]]) + +type JsonRecord = Record + +/** Numbers captured when a prompt cannot leave a usable completion budget. */ +export type OutputTokenBudgetInfo = { + readonly modelID: string + readonly providerID: string + readonly inputTokens: number + readonly requested: number + readonly context: number + readonly floor: number +} + +/** Numbers captured when a prompt exceeds a model's dedicated input ceiling. */ +export type InputTokenBudgetInfo = { + readonly modelID: string + readonly providerID: string + readonly inputTokens: number + readonly inputLimit: number + readonly margin: number +} + +/** Thrown before transport when the prompt leaves no usable completion budget. */ +export class OutputTokenBudgetError extends Error { + constructor(readonly info: OutputTokenBudgetInfo) { + super( + [ + "Context budget exceeded before the request was sent.", + `${info.providerID}/${info.modelID} declares a ${info.context}-token context window,`, + `the prompt is ~${info.inputTokens} tokens, and ${info.requested} tokens are reserved for`, + `the completion — ${info.inputTokens + info.requested} in total.`, + `Even after clamping, fewer than ${info.floor} tokens would remain for the response.`, + "Reduce the system prompt (fewer instructions, skills, or AGENTS.md content), lower the", + "output reservation via OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX, or use a model with a", + "larger context window.", + ].join(" "), + ) + this.name = "OutputTokenBudgetError" + } +} + +/** Thrown before transport when the estimated prompt exceeds a dedicated input limit. */ +export class InputTokenBudgetError extends Error { + constructor(readonly info: InputTokenBudgetInfo) { + super( + [ + "Input budget exceeded before the request was sent.", + `${info.providerID}/${info.modelID} declares a ${info.inputLimit}-token input limit,`, + `the prompt is ~${info.inputTokens} tokens, and ${info.margin} safety tokens are required.`, + "Reduce the prompt or use a model with a larger input limit.", + ].join(" "), + ) + this.name = "InputTokenBudgetError" + } +} + +/** Thrown when preserving enabled reasoning would leave no useful visible response. */ +export class ReasoningTokenBudgetError extends Error { + constructor( + readonly info: { + readonly path: string + readonly configured: number + readonly maxOutputTokens: number + }, + ) { + super( + [ + "The context-window clamp cannot preserve the configured reasoning budget.", + `${info.path} is ${info.configured} tokens while maxOutputTokens is ${info.maxOutputTokens},`, + `which cannot leave ${OUTPUT_TOKEN_FLOOR} tokens for the visible response.`, + "Use a larger-context model, shorten the prompt, or select a lower reasoning effort.", + ].join(" "), + ) + this.name = "ReasoningTokenBudgetError" + } +} + +/** Return true for plain record-like values used in request payloads. */ +function isRecord(value: unknown): value is JsonRecord { + return typeof value === "object" && value !== null && !Array.isArray(value) +} + +/** Estimate heterogeneous text in small chunks and conservatively count non-ASCII scripts. */ +function estimateTextTokens(input: string): number { + let total = 0 + for (let offset = 0; offset < input.length; offset += ESTIMATE_CHUNK_SIZE) { + const chunk = input.slice(offset, offset + ESTIMATE_CHUNK_SIZE) + let ascii = "" + let nonAscii = 0 + let emoji = 0 + for (const character of chunk) { + if (character.codePointAt(0)! <= 0x7f) { + ascii += character + } else { + nonAscii++ + if (EMOJI.test(character)) emoji++ + } + } + const multilingualFloor = Token.estimate(ascii) + nonAscii + emoji + total += Math.max(Token.estimate(chunk), multilingualFloor) + } + return total +} + +/** Detect message containers whose data/image fields carry media rather than prompt text. */ +function isMediaContainer(value: unknown): boolean { + if (!isRecord(value)) return false + if (value.type === "Buffer") return true + if (["image", "audio", "video", "file"].includes(String(value.type))) return true + const mediaType = value.mediaType ?? value.mimeType + return typeof mediaType === "string" && /^(?:image|audio|video)\//.test(mediaType) +} + +/** Serialize request structures without expanding encoded media bytes into fake text tokens. */ +function serializeForEstimate(value: unknown): { readonly text: string; readonly mediaParts: number } { + let mediaParts = 0 + const ancestors: object[] = [] + const text = + JSON.stringify(value, function (key, child) { + while (ancestors.length > 0 && ancestors.at(-1) !== this) ancestors.pop() + + const mediaField = ["data", "image", "audio", "video", "file"].includes(key) && isMediaContainer(this) + if (mediaField && child !== undefined && child !== null) { + mediaParts++ + return "[media omitted]" + } + if (typeof child === "string") { + if (MEDIA_DATA_URL.test(child)) { + mediaParts++ + return "[encoded media omitted]" + } + return child + } + if (typeof child === "object" && child !== null) { + if (ArrayBuffer.isView(child) || child instanceof ArrayBuffer) { + mediaParts++ + return "[binary media omitted]" + } + if (ancestors.includes(child)) return "[circular value omitted]" + ancestors.push(child) + } + return child + }) ?? "" + return { text, mediaParts } +} + +/** Collect Anthropic beta values only from header-shaped records. */ +function anthropicBetaValues(source: unknown, depth = 0): string[] { + if (!isRecord(source) || depth > 4) return [] + const result: string[] = [] + for (const [key, value] of Object.entries(source)) { + const normalized = key.toLowerCase() + if (normalized === "anthropic-beta") { + if (typeof value === "string") result.push(value) + if (Array.isArray(value)) result.push(...value.filter((item): item is string => typeof item === "string")) + continue + } + if (normalized === "headers" || normalized.endsWith("headers")) { + result.push(...anthropicBetaValues(value, depth + 1)) + } + } + return result +} + +/** Resolve catalog context limits with known provider beta headers applied. */ +export function effectiveContextWindow(input: { + readonly model: Provider.Model + readonly headerSources?: readonly unknown[] +}): number { + let context = input.model.limit.context + for (const source of input.headerSources ?? []) { + for (const value of anthropicBetaValues(source)) { + for (const beta of value.split(/[\s,]+/)) { + context = Math.max(context, CONTEXT_WINDOW_BETAS.get(beta) ?? 0) + } + } + } + return context +} + +/** Estimate the text, finalized tools, instructions, and media allowance sent in one request. */ +export function estimateInputTokens(input: { + readonly system: readonly string[] + readonly messages: readonly unknown[] + readonly tools?: Readonly> + readonly instructions?: unknown +}): number { + const system = input.system.join("\n") + let total = estimateTextTokens(system) + + for (const value of [input.messages, input.tools]) { + if (value === undefined) continue + const serialized = serializeForEstimate(value) + total += estimateTextTokens(serialized.text) + serialized.mediaParts * MEDIA_TOKEN_ALLOWANCE + } + + if (input.instructions !== undefined && input.instructions !== system) { + const serialized = serializeForEstimate(input.instructions) + total += estimateTextTokens(serialized.text) + serialized.mediaParts * MEDIA_TOKEN_ALLOWANCE + } + return total +} + +/** Resolve a direct or lazy input estimate after cheap no-op checks have passed. */ +function resolveInputTokens(value: number | (() => number)): number { + return typeof value === "function" ? value() : value +} + +/** Clamp a completion reservation so estimated input, margin, and output fit the effective window. */ +export function clampOutputTokens(input: { + readonly model: Provider.Model + readonly requested: number | undefined + readonly inputTokens: number | (() => number) + readonly context?: number +}): number | undefined { + const requested = input.requested + if (requested === undefined) return undefined + + const context = input.context ?? input.model.limit.context + const inputLimit = input.model.limit.input + if ((!context || context <= OUTPUT_TOKEN_FLOOR) && (!inputLimit || inputLimit <= 0)) return requested + + const inputTokens = resolveInputTokens(input.inputTokens) + if (!Number.isFinite(inputTokens) || inputTokens <= 0) return requested + + const margin = Math.max(CLAMP_MARGIN_MIN, Math.ceil(inputTokens * CLAMP_MARGIN_FRACTION)) + if (inputLimit && inputLimit > 0 && inputTokens + margin > inputLimit) { + throw new InputTokenBudgetError({ + modelID: input.model.id, + providerID: input.model.providerID, + inputTokens, + inputLimit, + margin, + }) + } + if (!context || context <= OUTPUT_TOKEN_FLOOR) return requested + if (inputTokens + requested + margin <= context) return requested + + const clamped = Math.floor(context - inputTokens - margin) + if (clamped < OUTPUT_TOKEN_FLOOR) { + throw new OutputTokenBudgetError({ + modelID: input.model.id, + providerID: input.model.providerID, + inputTokens, + requested, + context, + floor: OUTPUT_TOKEN_FLOOR, + }) + } + log.warn("clamped output token reservation to fit context window", { + providerID: input.model.providerID, + modelID: input.model.id, + context, + inputTokens, + requested, + clamped, + }) + return clamped +} + +/** Recursively copy and clamp explicit reasoning-token fields in provider options. */ +function transformReasoningBudgets( + value: JsonRecord, + ceiling: number, + maxOutputTokens: number, + path?: readonly string[], +): JsonRecord +function transformReasoningBudgets( + value: unknown, + ceiling: number, + maxOutputTokens: number, + path?: readonly string[], +): unknown +function transformReasoningBudgets( + value: unknown, + ceiling: number, + maxOutputTokens: number, + path: readonly string[] = [], +): unknown { + if (Array.isArray(value)) { + let changed = false + const result = value.map((item, index) => { + const next = transformReasoningBudgets(item, ceiling, maxOutputTokens, [...path, String(index)]) + changed ||= next !== item + return next + }) + return changed ? result : value + } + if (!isRecord(value)) return value + + let result = value + for (const [key, child] of Object.entries(value)) { + let next = child + if (REASONING_BUDGET_KEYS.has(key) && typeof child === "number" && child > 0 && child > ceiling) { + if (ceiling < MIN_REASONING_BUDGET) { + throw new ReasoningTokenBudgetError({ + path: [...path, key].join("."), + configured: child, + maxOutputTokens, + }) + } + next = ceiling + log.warn("clamped reasoning token budget with output reservation", { + path: [...path, key].join("."), + configured: child, + clamped: ceiling, + maxOutputTokens, + }) + } else { + next = transformReasoningBudgets(child, ceiling, maxOutputTokens, [...path, key]) + } + if (next !== child) { + if (result === value) result = { ...value } + result[key] = next + } + } + return result +} + +/** Clamp explicit thinking budgets while preserving room for a visible response. */ +export function clampReasoningBudget( + options: Record, + maxOutputTokens: number | undefined, +): Record { + if (maxOutputTokens === undefined) return options + const ceiling = Math.floor(maxOutputTokens - OUTPUT_TOKEN_FLOOR) + return transformReasoningBudgets(options, ceiling, maxOutputTokens) +} + +export * as OutputTokenBudget from "./output-token-budget" +// altimate_change end diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index a6d4a795eb..c71a986386 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -6,10 +6,6 @@ import type { Provider } from "./provider" import type { ModelsDev } from "./models" import { iife } from "@/util/iife" import { Flag } from "@/flag/flag" -// altimate_change start — output-token clamp needs prompt sizing and a logger -import { Token } from "@/util/token" -import { Log } from "@/util/log" -// altimate_change end type Modality = NonNullable["input"][number] @@ -23,9 +19,6 @@ function mimeToModality(mime: string): Modality | undefined { export namespace ProviderTransform { export const OUTPUT_TOKEN_MAX = Flag.OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX || 32_000 - // altimate_change start — logger for the output-token clamp below - const log = Log.create({ service: "provider.transform" }) - // altimate_change end // altimate_change start — keep OpenAI encrypted reasoning include values consistent across transforms const INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"] as const @@ -383,7 +376,10 @@ export namespace ProviderTransform { content: msg.content.map((part) => { const partType = (part as { type?: string }).type if (partType === "tool-approval-request" || partType === "tool-approval-response") return part - return { ...part, providerOptions: transform((part as { providerOptions?: Record }).providerOptions) } + return { + ...part, + providerOptions: transform((part as { providerOptions?: Record }).providerOptions), + } }), } as typeof msg }) @@ -405,9 +401,8 @@ export namespace ProviderTransform { model.id.includes("claude") || model.api.npm === "@ai-sdk/anthropic" || // altimate_change start — Alibaba Anthropic-compatible cache-control namespace - model.api.npm === "@ai-sdk/alibaba" - // altimate_change end - ) && + model.api.npm === "@ai-sdk/alibaba") && + // altimate_change end model.api.npm !== "@ai-sdk/gateway" ) { msgs = applyCaching(msgs, model) @@ -569,16 +564,9 @@ export namespace ProviderTransform { return ["low", "medium", "high", "xhigh", "max"] } if ( - [ - "opus-4-6", - "opus-4.6", - "4-6-opus", - "4.6-opus", - "sonnet-4-6", - "sonnet-4.6", - "4-6-sonnet", - "4.6-sonnet", - ].some((v) => apiId.includes(v)) + ["opus-4-6", "opus-4.6", "4-6-opus", "4.6-opus", "sonnet-4-6", "sonnet-4.6", "4-6-sonnet", "4.6-sonnet"].some( + (v) => apiId.includes(v), + ) ) { return ["low", "medium", "high", "max"] } @@ -819,7 +807,9 @@ export namespace ProviderTransform { return Object.fromEntries(["none", "high"].map((effort) => [effort, { reasoningEffort: effort }])) } if (model.api.id.toLowerCase().includes("deepseek-v4")) { - return Object.fromEntries([...WIDELY_SUPPORTED_EFFORTS, "max"].map((effort) => [effort, { reasoningEffort: effort }])) + return Object.fromEntries( + [...WIDELY_SUPPORTED_EFFORTS, "max"].map((effort) => [effort, { reasoningEffort: effort }]), + ) } // altimate_change end return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }])) @@ -954,15 +944,13 @@ export namespace ProviderTransform { // https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai return googleThinkingVariants(model) - case "@ai-sdk/mistral": - // https://v5.ai-sdk.dev/providers/ai-sdk-providers/mistral - // altimate_change start — only Mistral Small 4 and Medium 3.5 expose adjustable reasoning - { - const mistralId = model.api.id.toLowerCase() - const ids = ["mistral-small-2603", "mistral-small-latest", "mistral-medium-3.5", "mistral-medium-2604"] - if (!ids.some((item) => mistralId.includes(item))) return {} - return { high: { reasoningEffort: "high" } } - } + case "@ai-sdk/mistral": { + // altimate_change start — only Mistral Small 4 and Medium 3.5 expose adjustable reasoning // https://v5.ai-sdk.dev/providers/ai-sdk-providers/mistral + const mistralId = model.api.id.toLowerCase() + const ids = ["mistral-small-2603", "mistral-small-latest", "mistral-medium-3.5", "mistral-medium-2604"] + if (!ids.some((item) => mistralId.includes(item))) return {} + return { high: { reasoningEffort: "high" } } + } // altimate_change end case "@ai-sdk/cohere": @@ -1011,7 +999,9 @@ export namespace ProviderTransform { } if (apiId.includes("gpt") || /\bo[1-9]/.test(apiId)) { const efforts = openaiReasoningEfforts(apiId, model.release_date) - return wrapInSapModelParams(Object.fromEntries(efforts.map((effort) => [effort, { reasoning_effort: effort }]))) + return wrapInSapModelParams( + Object.fromEntries(efforts.map((effort) => [effort, { reasoning_effort: effort }])), + ) } return wrapInSapModelParams( Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoning_effort: effort }])), @@ -1047,7 +1037,10 @@ export namespace ProviderTransform { } // altimate_change end - if (input.model.api.npm === "@openrouter/ai-sdk-provider" || input.model.api.npm === "@llmgateway/ai-sdk-provider") { + if ( + input.model.api.npm === "@openrouter/ai-sdk-provider" || + input.model.api.npm === "@llmgateway/ai-sdk-provider" + ) { result["usage"] = { include: true, } @@ -1285,108 +1278,6 @@ export namespace ProviderTransform { // altimate_change end } - // altimate_change start — clamp the reserved completion budget against the real prompt size. - // - // `maxOutputTokens` above is a per-model ceiling. It never reads `limit.context`, so on a - // model where input and completion share one window a large system prompt can push - // `input + reservation` past that window and the provider rejects the request with a hard - // 400 before generating anything. The client already knows all three numbers, so it can - // either shrink the reservation to fit or refuse with an actionable message. - - /** - * Smallest completion budget worth sending. Below this an agent turn cannot reliably emit - * even one tool call, so a request that would clamp this far is refused instead of being - * sent to come back as a silently truncated response. - */ - export const OUTPUT_TOKEN_FLOOR = 1_024 - - // `inputTokens` is a character-ratio estimate, not the provider's tokenizer. Measured drift - // against a provider's own accounting on a ~52K prompt was under 1%, so a clamped budget - // keeps a 2% (minimum 512-token) margin rather than filling the window exactly. - const CLAMP_MARGIN_FRACTION = 0.02 - const CLAMP_MARGIN_MIN = 512 - - /** Thrown before the request is sent when no usable completion budget fits in the window. */ - export class OutputTokenBudgetError extends Error { - constructor( - readonly info: { - modelID: string - providerID: string - inputTokens: number - requested: number - context: number - floor: number - }, - ) { - super( - [ - `Context budget exceeded before the request was sent.`, - `${info.providerID}/${info.modelID} declares a ${info.context}-token context window,`, - `the prompt is ~${info.inputTokens} tokens, and ${info.requested} tokens are reserved for`, - `the completion — ${info.inputTokens + info.requested} in total.`, - `Even after clamping, fewer than ${info.floor} tokens would remain for the response.`, - `Reduce the system prompt (fewer instructions, skills, or AGENTS.md content), lower the`, - `output reservation via OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX, or use a model with a`, - `larger context window.`, - ].join(" "), - ) - this.name = "OutputTokenBudgetError" - } - } - - /** Rough token count for the prompt about to be sent, used only for the clamp decision. */ - export function estimateInputTokens(system: string[], messages: unknown[]): number { - return Token.estimate(system.join("\n")) + Token.estimate(JSON.stringify(messages)) - } - - /** - * Shrink `requested` so `inputTokens + result` fits `model.limit.context`. - * - * Returns `requested` unchanged whenever it already fits, when the caller omitted it, when the - * model declares no context window, when the declared window is too small to hold even a - * floor-sized completion (those limits are not credible enough to fail a request on — the - * provider stays the authority), or when the model budgets input separately via `limit.input` - * (there the two budgets are not shared and clamping would be wrong). - * Throws `OutputTokenBudgetError` when the remaining budget is below `OUTPUT_TOKEN_FLOOR`. - */ - export function clampOutputTokens(input: { - model: Provider.Model - requested: number | undefined - inputTokens: number - }): number | undefined { - const requested = input.requested - if (requested === undefined) return undefined - - const context = input.model.limit.context - if (!context || context <= OUTPUT_TOKEN_FLOOR) return requested - if (input.model.limit.input) return requested - if (input.inputTokens <= 0) return requested - if (input.inputTokens + requested <= context) return requested - - const margin = Math.max(CLAMP_MARGIN_MIN, Math.ceil(input.inputTokens * CLAMP_MARGIN_FRACTION)) - const clamped = context - input.inputTokens - margin - if (clamped < OUTPUT_TOKEN_FLOOR) { - throw new OutputTokenBudgetError({ - modelID: input.model.id, - providerID: input.model.providerID, - inputTokens: input.inputTokens, - requested, - context, - floor: OUTPUT_TOKEN_FLOOR, - }) - } - log.warn("clamped output token reservation to fit context window", { - providerID: input.model.providerID, - modelID: input.model.id, - context, - inputTokens: input.inputTokens, - requested, - clamped, - }) - return clamped - } - // altimate_change end - // altimate_change start — lower MCP/tool JSON Schema to provider-compatible subsets type JsonRecord = Record diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 62eddc836a..cdcbd1e02d 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -13,6 +13,14 @@ import { } from "ai" import { mergeDeep, pipe } from "remeda" import { ProviderTransform } from "@/provider/transform" +// altimate_change start — size and clamp the finalized provider request +import { + clampOutputTokens, + clampReasoningBudget, + effectiveContextWindow, + estimateInputTokens, +} from "@/provider/output-token-budget" +// altimate_change end // altimate_change start — tool retrieval import { Retrieval } from "@/tool/retrieval" // altimate_change end @@ -73,7 +81,9 @@ export namespace LLM { ]) const isCodex = provider.id === "openai" && auth?.type === "oauth" - const system = [] + // altimate_change start — keep the request-budget input typed before the first push + const system: string[] = [] + // altimate_change end system.push( [ // use agent prompt otherwise provider prompt @@ -150,18 +160,6 @@ export namespace LLM { ) // altimate_change end - // altimate_change start — clamp the reserved completion budget against the real prompt size. - // `maxOutputTokens` is a per-model ceiling that ignores `limit.context`, so a large system - // prompt could push `input + reservation` past the window and the provider rejected the - // request with a hard 400 before generating anything. Clamped after the chat.params hook so - // a plugin override is checked too. Throws when no usable budget is left. - const maxOutputTokens = ProviderTransform.clampOutputTokens({ - model: input.model, - requested: params.maxOutputTokens, - inputTokens: ProviderTransform.estimateInputTokens(system, input.messages), - }) - // altimate_change end - const { headers } = await Plugin.trigger( "chat.headers", { @@ -223,6 +221,29 @@ export namespace LLM { } // altimate_change end + // altimate_change start — clamp after every context-affecting request field is finalized. + // Tool schemas and provider instructions consume the shared context window, while encoded + // media bytes do not count as literal text. The estimator runs lazily so providers that omit + // maxOutputTokens pay no serialization cost. Known context beta headers widen the catalog + // limit before the clamp. Fixed reasoning budgets are reconciled with the final reservation. + const maxOutputTokens = clampOutputTokens({ + model: input.model, + requested: params.maxOutputTokens, + context: effectiveContextWindow({ + model: input.model, + headerSources: [input.model.headers, headers, provider.options], + }), + inputTokens: () => + estimateInputTokens({ + system, + messages: input.messages, + tools, + instructions: params.options.instructions, + }), + }) + const requestOptions = clampReasoningBudget(params.options, maxOutputTokens) + // altimate_change end + return streamText({ onError(error) { l.error("stream error", { @@ -257,12 +278,11 @@ export namespace LLM { temperature: params.temperature, topP: params.topP, topK: params.topK, - providerOptions: ProviderTransform.providerOptions(input.model, params.options), + providerOptions: ProviderTransform.providerOptions(input.model, requestOptions), activeTools: Object.keys(tools).filter((x) => x !== "invalid"), tools, toolChoice: input.toolChoice, - // altimate_change start — read maxOutputTokens from params (now plumbed through chat.params - // hook), clamped above so it cannot exceed the model's context window + // altimate_change start — use the plugin-selected reservation after context clamping maxOutputTokens, // altimate_change end abortSignal: input.abort, @@ -302,7 +322,7 @@ export namespace LLM { async transformParams(args) { if (args.type === "stream") { // @ts-expect-error - args.params.prompt = ProviderTransform.message(args.params.prompt, input.model, options) + args.params.prompt = ProviderTransform.message(args.params.prompt, input.model, requestOptions) } return args.params }, diff --git a/packages/opencode/src/session/llm/request.ts b/packages/opencode/src/session/llm/request.ts index 9c94febdd4..af2db413cd 100644 --- a/packages/opencode/src/session/llm/request.ts +++ b/packages/opencode/src/session/llm/request.ts @@ -8,6 +8,14 @@ import type { Agent } from "@/agent/agent" import type { MessageV2 } from "../message-v2" import type { Provider } from "@/provider/provider" import { ProviderTransform } from "@/provider/transform" +// altimate_change start — size and clamp the finalized provider request +import { + clampOutputTokens, + clampReasoningBudget, + effectiveContextWindow, + estimateInputTokens, +} from "@/provider/output-token-budget" +// altimate_change end import { SystemPrompt } from "../system" import { InstallationVersion } from "@opencode-ai/core/installation/version" import { Effect, Record } from "effect" @@ -168,30 +176,39 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre ? (yield* InstanceState.context).project.id : undefined - // altimate_change start — clamp the reserved completion budget against the real prompt size. - // `maxOutputTokens` is a per-model ceiling that ignores `limit.context`, so a large system - // prompt could push `input + reservation` past the window and the provider rejected the request - // with a hard 400 before generating anything. `input.messages` is used rather than the merged - // `messages` because the latter can already carry `system` as leading system messages, which - // would double-count the prompt. Throws when no usable budget is left. - const clampedParams = { - ...params, - maxOutputTokens: ProviderTransform.clampOutputTokens({ + // altimate_change start — clamp after tools, headers, and plugin options are finalized. + const sortedTools = Object.fromEntries(Object.entries(tools).toSorted(([a], [b]) => a.localeCompare(b))) + const maxOutputTokens = clampOutputTokens({ + model: input.model, + requested: params.maxOutputTokens, + context: effectiveContextWindow({ model: input.model, - requested: params.maxOutputTokens, - inputTokens: ProviderTransform.estimateInputTokens(system, input.messages), + headerSources: [input.model.headers, headers, input.provider.options], }), + inputTokens: () => + estimateInputTokens({ + system, + messages: input.messages, + tools: sortedTools, + instructions: params.options.instructions, + }), + }) + const requestOptions = clampReasoningBudget(params.options, maxOutputTokens) + const clampedParams = { + ...params, + maxOutputTokens, + options: requestOptions, } // altimate_change end return { system, messages, - tools: Object.fromEntries(Object.entries(tools).toSorted(([a], [b]) => a.localeCompare(b))), + tools: sortedTools, // altimate_change start — return the context-window-clamped params built above params: clampedParams, // altimate_change end - messageTransformOptions: options, + messageTransformOptions: requestOptions, headers: { ...(input.model.providerID.startsWith("opencode") ? { diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 28921f99ef..6e09b622a7 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -1,7 +1,16 @@ import { describe, expect, test } from "bun:test" import { Effect } from "effect" -import type { ModelMessage } from "ai" +import { jsonSchema, tool, type ModelMessage, type Tool } from "ai" import { ProviderTransform } from "@/provider/transform" +import { + clampOutputTokens, + clampReasoningBudget, + effectiveContextWindow, + estimateInputTokens, + InputTokenBudgetError, + OUTPUT_TOKEN_FLOOR, + OutputTokenBudgetError, +} from "@/provider/output-token-budget" import { LLMRequestPrep } from "@/session/llm/request" // ProviderTransform.message expects a Provider.Model with the fork's ModelID/ProviderID brands. import { ModelID, ProviderID } from "@/provider/schema" @@ -4658,7 +4667,7 @@ describe("ProviderTransform.providerOptions - ai-gateway-provider", () => { // shipped 16,384-token reservation produced a hard provider 400 before any model work — // "You requested a total of 68564 tokens: 52180 tokens from the input messages and 16384 tokens // for the completion". The reservation was applied without ever being compared to the input size. -describe("ProviderTransform.clampOutputTokens", () => { +describe("output token budget", () => { const createWindowModel = (limit: { context: number; input?: number; output: number }) => ({ id: "large-window-model", @@ -4686,7 +4695,7 @@ describe("ProviderTransform.clampOutputTokens", () => { test("the reported case is clamped instead of being sent as-is", () => { const model = createWindowModel({ context: REPORTED.context, output: 16_384 }) - const result = ProviderTransform.clampOutputTokens({ + const result = clampOutputTokens({ model, requested: REPORTED.requested, inputTokens: REPORTED.inputTokens, @@ -4700,18 +4709,18 @@ describe("ProviderTransform.clampOutputTokens", () => { // 65536 - 52180 - ceil(52180 * 0.02) = 12312 expect(result).toBe(12_312) // Still a usable completion budget, not a stub. - expect(result).toBeGreaterThanOrEqual(ProviderTransform.OUTPUT_TOKEN_FLOOR) + expect(result).toBeGreaterThanOrEqual(OUTPUT_TOKEN_FLOOR) }) test("throws with the actual numbers when even the floor does not fit", () => { const model = createWindowModel({ context: 65_536, output: 16_384 }) let thrown: unknown try { - ProviderTransform.clampOutputTokens({ model, requested: 16_384, inputTokens: 65_000 }) + clampOutputTokens({ model, requested: 16_384, inputTokens: 65_000 }) } catch (e) { thrown = e } - expect(thrown).toBeInstanceOf(ProviderTransform.OutputTokenBudgetError) + expect(thrown).toBeInstanceOf(OutputTokenBudgetError) const message = (thrown as Error).message // The message must name input tokens, the requested reservation and the window. expect(message).toContain("65000") @@ -4723,31 +4732,36 @@ describe("ProviderTransform.clampOutputTokens", () => { test("throws rather than clamping to an unusable budget just above zero", () => { const model = createWindowModel({ context: 65_536, output: 16_384 }) // 65536 - 65000 = 536 would "fit" arithmetically but is below the floor. - expect(536).toBeLessThan(ProviderTransform.OUTPUT_TOKEN_FLOOR) - expect(() => ProviderTransform.clampOutputTokens({ model, requested: 16_384, inputTokens: 65_000 })).toThrow() + expect(536).toBeLessThan(OUTPUT_TOKEN_FLOOR) + expect(() => clampOutputTokens({ model, requested: 16_384, inputTokens: 65_000 })).toThrow() }) test("leaves a config that already fits completely unchanged", () => { const model = createWindowModel({ context: 65_536, output: 16_384 }) - expect(ProviderTransform.clampOutputTokens({ model, requested: 16_384, inputTokens: 10_000 })).toBe(16_384) - // Exactly filling the window is still a valid request and must not be touched. - expect(ProviderTransform.clampOutputTokens({ model, requested: 16_384, inputTokens: 49_152 })).toBe(16_384) + expect(clampOutputTokens({ model, requested: 16_384, inputTokens: 10_000 })).toBe(16_384) + // An exact estimated fit still needs room for estimator drift. + expect(clampOutputTokens({ model, requested: 16_384, inputTokens: 49_152 })).toBe(15_400) }) test("leaves large-window models unchanged", () => { const model = createWindowModel({ context: 200_000, output: 8_192 }) - expect(ProviderTransform.clampOutputTokens({ model, requested: 8_192, inputTokens: 150_000 })).toBe(8_192) + expect(clampOutputTokens({ model, requested: 8_192, inputTokens: 150_000 })).toBe(8_192) }) - test("does not clamp models that budget input separately from output", () => { - // limit.input means the two budgets are not shared, so input + output can exceed context. + test("still clamps models that also declare an input ceiling", () => { + // limit.input is an input ceiling, not evidence that completion tokens use a separate window. const model = createWindowModel({ context: 65_536, input: 65_536, output: 16_384 }) - expect(ProviderTransform.clampOutputTokens({ model, requested: 16_384, inputTokens: 60_000 })).toBe(16_384) + expect(clampOutputTokens({ model, requested: 16_384, inputTokens: 60_000 })).toBe(4_336) + }) + + test("rejects a prompt that exceeds a dedicated input ceiling", () => { + const model = createWindowModel({ context: 200_000, input: 65_536, output: 16_384 }) + expect(() => clampOutputTokens({ model, requested: 16_384, inputTokens: 70_000 })).toThrow(InputTokenBudgetError) }) test("does not clamp when the model declares no context window", () => { const model = createWindowModel({ context: 0, output: 16_384 }) - expect(ProviderTransform.clampOutputTokens({ model, requested: 16_384, inputTokens: 60_000 })).toBe(16_384) + expect(clampOutputTokens({ model, requested: 16_384, inputTokens: 60_000 })).toBe(16_384) }) test("does not clamp a window too small to hold even a floor-sized completion", () => { @@ -4755,13 +4769,125 @@ describe("ProviderTransform.clampOutputTokens", () => { // the request client-side on numbers we do not believe would be worse than letting the // provider answer, so the guard stays out of the way. const model = createWindowModel({ context: 20, output: 10 }) - expect(ProviderTransform.clampOutputTokens({ model, requested: 10, inputTokens: 11 })).toBe(10) + expect(clampOutputTokens({ model, requested: 10, inputTokens: 11 })).toBe(10) }) test("passes an omitted reservation through untouched", () => { // Codex and GitHub Copilot deliberately send no maxOutputTokens. const model = createWindowModel({ context: 65_536, output: 16_384 }) - expect(ProviderTransform.clampOutputTokens({ model, requested: undefined, inputTokens: 60_000 })).toBeUndefined() + expect(clampOutputTokens({ model, requested: undefined, inputTokens: 60_000 })).toBeUndefined() + }) + + test("does not evaluate a lazy estimate when the reservation is omitted", () => { + const model = createWindowModel({ context: 65_536, output: 16_384 }) + let evaluated = false + expect( + clampOutputTokens({ + model, + requested: undefined, + inputTokens: () => { + evaluated = true + return 60_000 + }, + }), + ).toBeUndefined() + expect(evaluated).toBeFalse() + }) + + test("counts tool schemas and provider instructions", () => { + const base = estimateInputTokens({ system: ["system"], messages: [{ role: "user", content: "hello" }] }) + const complete = estimateInputTokens({ + system: ["system"], + messages: [{ role: "user", content: "hello" }], + instructions: "provider instruction ".repeat(400), + tools: { + search: { + description: "search fields ".repeat(400), + inputSchema: { type: "object", properties: { query: { type: "string" } } }, + }, + }, + }) + expect(complete).toBeGreaterThan(base + 1_000) + }) + + test("does not tokenize encoded media bytes as literal prompt text", () => { + const estimated = estimateInputTokens({ + system: [], + messages: [ + { + role: "user", + content: [{ type: "image", image: `data:image/png;base64,${"A".repeat(1_048_576)}` }], + }, + ], + }) + expect(estimated).toBeGreaterThan(2_000) + expect(estimated).toBeLessThan(10_000) + }) + + test("charges the same fixed allowance for URL-backed media", () => { + const base = estimateInputTokens({ system: [], messages: [{ role: "user", content: "show this" }] }) + const estimated = estimateInputTokens({ + system: [], + messages: [ + { + role: "user", + content: [{ type: "image", image: new URL("https://example.com/tiny.png") }], + }, + ], + }) + expect(estimated).toBeGreaterThan(base + 2_000) + expect(estimated).toBeLessThan(base + 3_000) + }) + + test("counts repeated shared tool objects while terminating true cycles", () => { + const sharedTool = tool({ + description: "shared schema documentation ".repeat(1_200), + inputSchema: jsonSchema({ + type: "object", + properties: { query: { type: "string", description: "query details ".repeat(1_200) } }, + }), + }) + const once = estimateInputTokens({ system: [], messages: [], tools: { first: sharedTool } }) + const twice = estimateInputTokens({ system: [], messages: [], tools: { first: sharedTool, second: sharedTool } }) + expect(twice).toBeGreaterThan(once * 1.8) + + const circular: Record = { text: "still counted" } + circular.self = circular + expect(estimateInputTokens({ system: [], messages: [circular] })).toBeGreaterThan(0) + }) + + test("uses a conservative multilingual floor instead of the ASCII ratio", () => { + const text = "漢".repeat(10_000) + expect(estimateInputTokens({ system: [text], messages: [] })).toBeGreaterThanOrEqual(10_000) + }) + + test("honors the known one-million-token Anthropic beta header", () => { + const model = createWindowModel({ context: 200_000, output: 16_384 }) + expect( + effectiveContextWindow({ + model, + headerSources: [{ aiGatewayHeaders: { "anthropic-beta": "context-1m-2025-08-07" } }], + }), + ).toBe(1_000_000) + }) + + test("clamps fixed reasoning budgets with the output reservation without mutating inputs", () => { + const options = { + thinking: { type: "enabled", budgetTokens: 16_000 }, + thinkingConfig: { thinkingBudget: 16_000 }, + reasoningConfig: { budgetTokens: 31_999 }, + } + const result = clampReasoningBudget(options, 12_312) + expect(result.thinking.budgetTokens).toBe(11_288) + expect(result.thinkingConfig.thinkingBudget).toBe(11_288) + expect(result.reasoningConfig.budgetTokens).toBe(11_288) + expect(options.thinking.budgetTokens).toBe(16_000) + }) + + test("fails clearly when reasoning and visible output cannot both fit", () => { + expect(() => clampReasoningBudget({ thinking: { budgetTokens: 16_000 } }, 1_500)).toThrow( + /cannot preserve the configured reasoning budget/, + ) }) }) @@ -4791,7 +4917,14 @@ describe("LLMRequestPrep.prepare - output token reservation", () => { const messages: ModelMessage[] = [{ role: "user", content: "Hello" }] - const run = (systemPrompt: string) => + const run = ( + systemPrompt: string, + overrides: { + readonly tools?: Record + readonly agentOptions?: Record + readonly outputTokenMax?: number + } = {}, + ) => Effect.runPromise( LLMRequestPrep.prepare({ user: { @@ -4804,14 +4937,25 @@ describe("LLMRequestPrep.prepare - output token reservation", () => { } as any, sessionID, model, - agent: { name: "test", mode: "primary", options: {}, permission: [], prompt: systemPrompt } as any, + agent: { + name: "test", + mode: "primary", + options: overrides.agentOptions ?? {}, + permission: [], + prompt: systemPrompt, + } as any, system: [], messages, - tools: {}, + tools: overrides.tools ?? {}, provider: { id: "openai-compatible", options: {} } as any, auth: undefined, plugin: { - trigger: (_name: string, _input: unknown, output: unknown) => Effect.succeed(output), + trigger: (name: string, _input: unknown, output: unknown) => + Effect.succeed( + name === "chat.params" && overrides.outputTokenMax !== undefined + ? { ...(output as Record), maxOutputTokens: overrides.outputTokenMax } + : output, + ), list: () => Effect.succeed([]), init: () => Effect.void, } as any, @@ -4825,7 +4969,7 @@ describe("LLMRequestPrep.prepare - output token reservation", () => { const largePrompt = PROSE.repeat(Math.ceil((52_180 * 3.7) / PROSE.length)) test("a ~52K-token system prompt does not produce an unclamped request", async () => { - const estimated = ProviderTransform.estimateInputTokens([largePrompt], messages) + const estimated = estimateInputTokens({ system: [largePrompt], messages }) // Sized to reproduce the reported 52,180-token prompt. expect(estimated).toBeGreaterThan(51_500) expect(estimated).toBeLessThan(53_500) @@ -4836,7 +4980,7 @@ describe("LLMRequestPrep.prepare - output token reservation", () => { const maxOutputTokens = result.params.maxOutputTokens! expect(maxOutputTokens).toBeLessThan(16_384) expect(estimated + maxOutputTokens).toBeLessThanOrEqual(65_536) - expect(maxOutputTokens).toBeGreaterThanOrEqual(ProviderTransform.OUTPUT_TOKEN_FLOOR) + expect(maxOutputTokens).toBeGreaterThanOrEqual(OUTPUT_TOKEN_FLOOR) }) test("a small system prompt keeps the full model reservation", async () => { @@ -4844,6 +4988,39 @@ describe("LLMRequestPrep.prepare - output token reservation", () => { expect(result.params.maxOutputTokens).toBe(16_384) }) + test("large finalized tool schemas participate in the request-builder clamp", async () => { + const mediumPrompt = PROSE.repeat(Math.ceil((47_500 * 3.7) / PROSE.length)) + expect((await run(mediumPrompt)).params.maxOutputTokens).toBe(16_384) + + const schemaHeavyTool = tool({ + description: "search parameter documentation ".repeat(1_200), + inputSchema: jsonSchema({ + type: "object", + properties: { query: { type: "string" } }, + required: ["query"], + }), + }) + const result = await run(mediumPrompt, { tools: { schema_heavy: schemaHeavyTool } }) + expect(result.params.maxOutputTokens).toBeLessThan(16_384) + }) + + test("a clamped request also clamps a fixed reasoning budget", async () => { + const result = await run(largePrompt, { + agentOptions: { thinking: { type: "enabled", budgetTokens: 16_000 } }, + }) + expect(result.params.options.thinking.budgetTokens).toBeLessThan(result.params.maxOutputTokens!) + expect(result.params.options.thinking.budgetTokens + OUTPUT_TOKEN_FLOOR).toBe(result.params.maxOutputTokens!) + }) + + test("a plugin-selected output budget always reconciles fixed reasoning", async () => { + const result = await run("You are a helpful assistant.", { + outputTokenMax: 8_192, + agentOptions: { thinking: { type: "enabled", budgetTokens: 16_000 } }, + }) + expect(result.params.maxOutputTokens).toBe(8_192) + expect(result.params.options.thinking.budgetTokens).toBe(8_192 - OUTPUT_TOKEN_FLOOR) + }) + test("a system prompt that leaves no usable budget fails before the request is built", async () => { const hugePrompt = PROSE.repeat(Math.ceil((65_000 * 3.7) / PROSE.length)) await expect(run(hugePrompt)).rejects.toThrow(/Context budget exceeded/) diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index 148529ad64..539585df58 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -1,6 +1,6 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test" import path from "path" -import type { ModelMessage } from "ai" +import { jsonSchema, tool, type ModelMessage } from "ai" import { LLM } from "../../src/session/llm" import { Global } from "../../src/global" import { Instance } from "../../src/project/instance" @@ -669,4 +669,105 @@ describe("session.llm.stream", () => { }, }) }, 30_000) + + test("clamps finalized tools and reasoning in the Google stream request", async () => { + const server = state.server + if (!server) throw new Error("Server not initialized") + + const providerID = "google" + const modelID = "gemini-2.5-flash" + const fixture = await loadFixture(providerID, modelID) + const pathSuffix = `/v1beta/models/${fixture.model.id}:streamGenerateContent` + const request = waitRequest( + pathSuffix, + createEventResponse([ + { + candidates: [{ content: { parts: [{ text: "Hello" }] }, finishReason: "STOP" }], + usageMetadata: { promptTokenCount: 1, candidatesTokenCount: 1, totalTokenCount: 2 }, + }, + ]), + ) + + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://altimate.ai/config.json", + enabled_providers: [providerID], + provider: { + [providerID]: { + options: { apiKey: "test-google-key", baseURL: `${server.url.origin}/v1beta` }, + }, + }, + }), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const resolved = await Provider.getModel(ProviderID.make(providerID), ModelID.make(fixture.model.id)) + const budgeted = { + ...resolved, + limit: { ...resolved.limit, context: 65_536, output: 16_384 }, + } + const sessionID = SessionID.make("session-budget-stream") + const agent = { + name: "test", + mode: "primary", + options: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16_000 } }, + permission: [{ permission: "*", pattern: "*", action: "allow" }], + } satisfies Agent.Info + const user = { + id: MessageID.make("msg_user_budget_stream"), + sessionID, + role: "user", + time: { created: Date.now() }, + agent: agent.name, + model: { providerID: ProviderID.make(providerID), modelID: budgeted.id }, + } satisfies MessageV2.User + const prose = "the quick brown fox jumps over the lazy dog " + const largeSystem = prose.repeat(Math.ceil((45_000 * 3.7) / prose.length)) + const schemaMarker = "finalized-tool-schema-marker" + const tools = { + schema_heavy: tool({ + description: `${schemaMarker} ${"search parameter documentation ".repeat(1_000)}`, + inputSchema: jsonSchema({ + type: "object", + properties: { + query: { type: "string", description: "query details ".repeat(1_000) }, + }, + required: ["query"], + }), + }), + } + + const stream = await LLM.stream({ + user, + sessionID, + model: budgeted, + agent, + system: [largeSystem], + abort: new AbortController().signal, + messages: [{ role: "user", content: "Hello" }], + tools, + }) + for await (const _ of stream.fullStream) { + } + + const capture = await request + const config = capture.body.generationConfig as + | { maxOutputTokens?: number; thinkingConfig?: { thinkingBudget?: number } } + | undefined + const maxOutputTokens = config?.maxOutputTokens + expect(maxOutputTokens).toBeDefined() + expect(maxOutputTokens!).toBeLessThan(16_384) + expect(maxOutputTokens!).toBeGreaterThanOrEqual(1_024) + expect(config?.thinkingConfig?.thinkingBudget).toBe(maxOutputTokens! - 1_024) + expect(JSON.stringify(capture.body.tools)).toContain(schemaMarker) + }, + }) + }, 30_000) }) diff --git a/packages/opencode/test/upstream/bridge-merge-e2e.test.ts b/packages/opencode/test/upstream/bridge-merge-e2e.test.ts index 034b3c56c5..7a5d8b4a0d 100644 --- a/packages/opencode/test/upstream/bridge-merge-e2e.test.ts +++ b/packages/opencode/test/upstream/bridge-merge-e2e.test.ts @@ -378,9 +378,7 @@ describe("E2E: OAuth callback XSS prevention (cycle 1 + 2)", () => { // util (src/util/html.ts); oauth-callback.ts now imports it. Accept either an // inline `function escapeHtml` (legacy) or the shared-util import — the XSS // property below is what actually matters and is asserted unchanged. - expect(content).toMatch( - /function escapeHtml|import\s*\{\s*escapeHtml\s*\}\s*from\s*["']@\/util\/html["']/, - ) + expect(content).toMatch(/function escapeHtml|import\s*\{\s*escapeHtml\s*\}\s*from\s*["']@\/util\/html["']/) // Every ${error} or ${error_description} interpolation must go through escapeHtml const errorInterps = content.match(/\$\{(error[A-Za-z_]*?)\}/g) ?? [] for (const interp of errorInterps) { @@ -526,10 +524,13 @@ describe("E2E: chat.params maxOutputTokens hook (cycle 6)", () => { expect(hookBlock).toMatch(/maxOutputTokens/) }) - test("session/llm.ts reads params.maxOutputTokens (not the local var) for streamText", async () => { + test("session/llm.ts routes params.maxOutputTokens through the context clamp", async () => { const content = readFileSync(path.join(srcDir, "session", "llm.ts"), "utf-8") - // streamText config must reference params.maxOutputTokens - expect(content).toMatch(/maxOutputTokens:\s*params\.maxOutputTokens/) + // altimate_change start — the plugin result is now clamped before streamText receives it + expect(content).toMatch(/requested:\s*params\.maxOutputTokens/) + expect(content).toMatch(/const maxOutputTokens = clampOutputTokens/) + expect(content).toMatch(/return streamText\([\s\S]*?maxOutputTokens,/) + // altimate_change end }) test("plugin/codex.ts chat.params hook still sets output.maxOutputTokens = undefined", async () => { From 12180d1dd57a7efd5f9706928289a4936ebc8f07 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Sun, 30 Aug 2026 00:41:44 -0700 Subject: [PATCH 04/32] chore(provider): align upstream markers after main sync --- packages/opencode/src/provider/transform.ts | 52 ++++++++++---------- packages/opencode/src/session/llm.ts | 2 + packages/opencode/src/session/llm/request.ts | 4 +- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index c71a986386..446b1a5036 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -376,10 +376,7 @@ export namespace ProviderTransform { content: msg.content.map((part) => { const partType = (part as { type?: string }).type if (partType === "tool-approval-request" || partType === "tool-approval-response") return part - return { - ...part, - providerOptions: transform((part as { providerOptions?: Record }).providerOptions), - } + return { ...part, providerOptions: transform((part as { providerOptions?: Record }).providerOptions) } }), } as typeof msg }) @@ -401,8 +398,9 @@ export namespace ProviderTransform { model.id.includes("claude") || model.api.npm === "@ai-sdk/anthropic" || // altimate_change start — Alibaba Anthropic-compatible cache-control namespace - model.api.npm === "@ai-sdk/alibaba") && - // altimate_change end + model.api.npm === "@ai-sdk/alibaba" + // altimate_change end + ) && model.api.npm !== "@ai-sdk/gateway" ) { msgs = applyCaching(msgs, model) @@ -564,9 +562,16 @@ export namespace ProviderTransform { return ["low", "medium", "high", "xhigh", "max"] } if ( - ["opus-4-6", "opus-4.6", "4-6-opus", "4.6-opus", "sonnet-4-6", "sonnet-4.6", "4-6-sonnet", "4.6-sonnet"].some( - (v) => apiId.includes(v), - ) + [ + "opus-4-6", + "opus-4.6", + "4-6-opus", + "4.6-opus", + "sonnet-4-6", + "sonnet-4.6", + "4-6-sonnet", + "4.6-sonnet", + ].some((v) => apiId.includes(v)) ) { return ["low", "medium", "high", "max"] } @@ -807,9 +812,7 @@ export namespace ProviderTransform { return Object.fromEntries(["none", "high"].map((effort) => [effort, { reasoningEffort: effort }])) } if (model.api.id.toLowerCase().includes("deepseek-v4")) { - return Object.fromEntries( - [...WIDELY_SUPPORTED_EFFORTS, "max"].map((effort) => [effort, { reasoningEffort: effort }]), - ) + return Object.fromEntries([...WIDELY_SUPPORTED_EFFORTS, "max"].map((effort) => [effort, { reasoningEffort: effort }])) } // altimate_change end return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }])) @@ -944,13 +947,15 @@ export namespace ProviderTransform { // https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai return googleThinkingVariants(model) - case "@ai-sdk/mistral": { - // altimate_change start — only Mistral Small 4 and Medium 3.5 expose adjustable reasoning // https://v5.ai-sdk.dev/providers/ai-sdk-providers/mistral - const mistralId = model.api.id.toLowerCase() - const ids = ["mistral-small-2603", "mistral-small-latest", "mistral-medium-3.5", "mistral-medium-2604"] - if (!ids.some((item) => mistralId.includes(item))) return {} - return { high: { reasoningEffort: "high" } } - } + case "@ai-sdk/mistral": + // https://v5.ai-sdk.dev/providers/ai-sdk-providers/mistral + // altimate_change start — only Mistral Small 4 and Medium 3.5 expose adjustable reasoning + { + const mistralId = model.api.id.toLowerCase() + const ids = ["mistral-small-2603", "mistral-small-latest", "mistral-medium-3.5", "mistral-medium-2604"] + if (!ids.some((item) => mistralId.includes(item))) return {} + return { high: { reasoningEffort: "high" } } + } // altimate_change end case "@ai-sdk/cohere": @@ -999,9 +1004,7 @@ export namespace ProviderTransform { } if (apiId.includes("gpt") || /\bo[1-9]/.test(apiId)) { const efforts = openaiReasoningEfforts(apiId, model.release_date) - return wrapInSapModelParams( - Object.fromEntries(efforts.map((effort) => [effort, { reasoning_effort: effort }])), - ) + return wrapInSapModelParams(Object.fromEntries(efforts.map((effort) => [effort, { reasoning_effort: effort }]))) } return wrapInSapModelParams( Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoning_effort: effort }])), @@ -1037,10 +1040,7 @@ export namespace ProviderTransform { } // altimate_change end - if ( - input.model.api.npm === "@openrouter/ai-sdk-provider" || - input.model.api.npm === "@llmgateway/ai-sdk-provider" - ) { + if (input.model.api.npm === "@openrouter/ai-sdk-provider" || input.model.api.npm === "@llmgateway/ai-sdk-provider") { result["usage"] = { include: true, } diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index cdcbd1e02d..4cd1d9cb3b 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -278,7 +278,9 @@ export namespace LLM { temperature: params.temperature, topP: params.topP, topK: params.topK, + // altimate_change start — use the reasoning options reconciled with the final output reservation providerOptions: ProviderTransform.providerOptions(input.model, requestOptions), + // altimate_change end activeTools: Object.keys(tools).filter((x) => x !== "invalid"), tools, toolChoice: input.toolChoice, diff --git a/packages/opencode/src/session/llm/request.ts b/packages/opencode/src/session/llm/request.ts index af2db413cd..6ce880eb50 100644 --- a/packages/opencode/src/session/llm/request.ts +++ b/packages/opencode/src/session/llm/request.ts @@ -204,11 +204,11 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre return { system, messages, + // altimate_change start — return the finalized tools and context-window-clamped request values tools: sortedTools, - // altimate_change start — return the context-window-clamped params built above params: clampedParams, - // altimate_change end messageTransformOptions: requestOptions, + // altimate_change end headers: { ...(input.model.providerID.startsWith("opencode") ? { From b05887606952d3ce651ffeedf0610b8f284edfea Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Sun, 30 Aug 2026 00:42:56 -0700 Subject: [PATCH 05/32] chore(provider): protect message option integration --- packages/opencode/src/session/llm.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 4cd1d9cb3b..178e9ad0d7 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -323,8 +323,10 @@ export namespace LLM { specificationVersion: "v3", async transformParams(args) { if (args.type === "stream") { + // altimate_change start — transform messages with the reconciled reasoning options // @ts-expect-error args.params.prompt = ProviderTransform.message(args.params.prompt, input.model, requestOptions) + // altimate_change end } return args.params }, From b06207af536dd2ec28f972b83b0d476116d61d30 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Sun, 30 Aug 2026 00:56:05 -0700 Subject: [PATCH 06/32] fix: count what is actually sent when clamping the output reservation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review of the clamp found five ways the guard read the wrong numbers. Two were regressions the clamp itself introduced, so they are fixed here rather than deferred. - count tool definitions in the estimate. Providers bill tool schemas as prompt tokens, so omitting them under-counted exactly the tool-heavy sessions most likely to overflow. In `session/llm.ts` the clamp moves below `resolveTools` and the retrieval filter — still after the `chat.params` hook — so it sees the tools actually sent; `session/llm/request.ts` already had them in scope. - strip media before estimating. Attachments arrive as base64 payloads or byte arrays on the model messages, and `JSON.stringify` counted a 1 MiB screenshot as hundreds of thousands of text tokens, refusing a multimodal request the provider would have accepted. `Compaction.estimate` already does this via `stripMedia`; this follows the same trade-off. - apply the estimator margin to the fit check, not just to the clamp. The boundary was discontinuous: one token over clamped to a ~1K cushion, one token under was sent with no protection at all, even though the input count is a character-ratio estimate. - clamp models that declare `limit.input`. That field is an input ceiling inside the shared window, not a separate budget: `Session.Overflow.usable` subtracts the reserved completion from it, and catalog-shaped fixtures pair it with an equal or larger `context` (context 200K / input 200K / output 32K). The early return left the exact 400 this guard prevents. - clamp the configured reasoning budget alongside `maxOutputTokens`. Anthropic rejects a request whose thinking budget is not below `max_tokens`, and this repository configures fixed 16,000/31,999-token budgets, so a clamp on its own turned one provider 400 into another. The floor also rises when a reasoning variant is configured so thinking and an answer both fit. Also honours a context window widened by a request header — the GitLab AI-gateway loader always sends `anthropic-beta: context-1m-2025-08-07` while its catalog entries still declare 200K — and takes the prompt estimate lazily so the history is not serialized on paths that return before it is needed. 13 new tests; the two that pinned the old exact-fill and `limit.input` behaviour are rewritten to assert the corrected outcome. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VqnuBDGkh1ZT65Ti7e6DHZ --- packages/opencode/src/provider/transform.ts | 147 +++++++++++++--- packages/opencode/src/session/llm.ts | 37 ++-- packages/opencode/src/session/llm/request.ts | 18 +- .../opencode/test/provider/transform.test.ts | 159 +++++++++++++++++- 4 files changed, 314 insertions(+), 47 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index a6d4a795eb..11fc34c8c0 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -1334,52 +1334,155 @@ export namespace ProviderTransform { } } - /** Rough token count for the prompt about to be sent, used only for the clamp decision. */ - export function estimateInputTokens(system: string[], messages: unknown[]): number { - return Token.estimate(system.join("\n")) + Token.estimate(JSON.stringify(messages)) + /** + * Smallest reasoning budget the Anthropic API accepts. When a reasoning variant is configured + * the clamp has to leave room for both the thinking budget and an actual answer, so the floor + * rises by this much. + */ + const MIN_REASONING_BUDGET = 1_024 + + // Attachments reach this point as base64 payloads or byte arrays on the model messages. + // Serializing them whole would count a 1 MiB screenshot as hundreds of thousands of text + // tokens and refuse a request the provider would have accepted, so they are replaced with a + // marker before estimating — the same trade-off `Compaction.estimate` makes via `stripMedia`. + const MEDIA_PLACEHOLDER = "[media]" + + function stringifyWithoutMedia(value: unknown): string { + return JSON.stringify(value, (key, val) => { + if (ArrayBuffer.isView(val) || val instanceof ArrayBuffer) return MEDIA_PLACEHOLDER + if (typeof val !== "string") return val + if (key === "data" || key === "image") return MEDIA_PLACEHOLDER + if (key === "url" && val.startsWith("data:")) return MEDIA_PLACEHOLDER + return val + }) + } + + /** + * Rough token count for the prompt about to be sent, used only for the clamp decision. + * + * Counts the system text, the messages with media stripped, and the tool definitions — tools + * are sent with the request and providers bill them as prompt tokens, so leaving them out + * would under-count exactly the tool-heavy sessions most likely to overflow. + */ + export function estimateInputTokens(system: string[], messages: unknown[], tools?: unknown): number { + return ( + Token.estimate(system.join("\n")) + + Token.estimate(stringifyWithoutMedia(messages)) + + (tools ? Token.estimate(stringifyWithoutMedia(tools)) : 0) + ) + } + + /** + * The context window actually in force for a request. + * + * The catalog value is a static declaration, but a request header can enable a larger window: + * the GitLab AI-gateway loader always sends `anthropic-beta: context-1m-2025-08-07` while its + * catalog entries still declare 200K. Clamping against the catalog number there would shrink + * or refuse prompts the provider route accepts. + */ + export function effectiveContext(model: Provider.Model, headers?: Record): number { + const context = model.limit.context + if (headers?.["anthropic-beta"]?.includes("context-1m-2025-08-07")) return Math.max(context, 1_000_000) + return context + } + + /** Largest reasoning budget configured anywhere in the provider options, or 0 if none. */ + export function configuredReasoningBudget(options: unknown): number { + let budget = 0 + const visit = (value: unknown) => { + if (Array.isArray(value)) return value.forEach(visit) + if (typeof value !== "object" || value === null) return + for (const [key, val] of Object.entries(value)) { + if ((key === "budgetTokens" || key === "thinkingBudget") && typeof val === "number") { + budget = Math.max(budget, val) + } else visit(val) + } + } + visit(options) + return budget + } + + /** + * Lower a configured reasoning budget so it stays below the (possibly clamped) completion + * budget. Anthropic rejects a request whose `thinking.budget_tokens` is not less than + * `max_tokens`, and this repository configures fixed 16,000/31,999-token budgets, so clamping + * `maxOutputTokens` on its own can turn one provider 400 into another. + */ + export function clampReasoningBudget(options: T, maxOutputTokens: number | undefined): T { + if (maxOutputTokens === undefined) return options + const ceiling = maxOutputTokens - OUTPUT_TOKEN_FLOOR + if (ceiling < MIN_REASONING_BUDGET) return options + + let changed = false + const next = JSON.parse(JSON.stringify(options ?? null), (key, val) => { + if ((key === "budgetTokens" || key === "thinkingBudget") && typeof val === "number" && val > ceiling) { + changed = true + return ceiling + } + return val + }) + return changed ? next : options } /** - * Shrink `requested` so `inputTokens + result` fits `model.limit.context`. + * Shrink `requested` so `inputTokens + result` fits the context window. + * + * Returns `requested` unchanged whenever it already fits with the estimator margin to spare, + * when the caller omitted it, when the model declares no context window, or when the declared + * window is too small to hold even a floor-sized completion (those limits are not credible + * enough to fail a request on — the provider stays the authority). + * Throws `OutputTokenBudgetError` when the remaining budget is below the floor. * - * Returns `requested` unchanged whenever it already fits, when the caller omitted it, when the - * model declares no context window, when the declared window is too small to hold even a - * floor-sized completion (those limits are not credible enough to fail a request on — the - * provider stays the authority), or when the model budgets input separately via `limit.input` - * (there the two budgets are not shared and clamping would be wrong). - * Throws `OutputTokenBudgetError` when the remaining budget is below `OUTPUT_TOKEN_FLOOR`. + * `inputTokens` may be a thunk: serializing the whole history is wasted work on the paths that + * return before the estimate is needed (Codex and GitHub Copilot send no reservation at all). */ export function clampOutputTokens(input: { model: Provider.Model requested: number | undefined - inputTokens: number + inputTokens: number | (() => number) + /** Overrides `model.limit.context`; see `effectiveContext`. */ + context?: number + /** Configured reasoning budget, if any — raises the floor so thinking still fits. */ + reasoningBudget?: number }): number | undefined { const requested = input.requested if (requested === undefined) return undefined - const context = input.model.limit.context - if (!context || context <= OUTPUT_TOKEN_FLOOR) return requested - if (input.model.limit.input) return requested - if (input.inputTokens <= 0) return requested - if (input.inputTokens + requested <= context) return requested + // A configured reasoning budget has to fit under the clamped value alongside a real answer. + const floor = + input.reasoningBudget && input.reasoningBudget > 0 + ? OUTPUT_TOKEN_FLOOR + MIN_REASONING_BUDGET + : OUTPUT_TOKEN_FLOOR + + const context = input.context ?? input.model.limit.context + if (!context || context <= floor) return requested + + const inputTokens = typeof input.inputTokens === "function" ? input.inputTokens() : input.inputTokens + if (inputTokens <= 0) return requested + + // The margin applies to the fit check as well as the clamp. `inputTokens` is a character-ratio + // estimate, so a prompt that appears to fill the window exactly can still overflow it; without + // this the boundary is discontinuous — one token over clamps to a ~1K cushion, one token under + // gets no protection at all. + const margin = Math.max(CLAMP_MARGIN_MIN, Math.ceil(inputTokens * CLAMP_MARGIN_FRACTION)) + if (inputTokens + requested + margin <= context) return requested - const margin = Math.max(CLAMP_MARGIN_MIN, Math.ceil(input.inputTokens * CLAMP_MARGIN_FRACTION)) - const clamped = context - input.inputTokens - margin - if (clamped < OUTPUT_TOKEN_FLOOR) { + const clamped = context - inputTokens - margin + if (clamped < floor) { throw new OutputTokenBudgetError({ modelID: input.model.id, providerID: input.model.providerID, - inputTokens: input.inputTokens, + inputTokens, requested, context, - floor: OUTPUT_TOKEN_FLOOR, + floor, }) } log.warn("clamped output token reservation to fit context window", { providerID: input.model.providerID, modelID: input.model.id, context, - inputTokens: input.inputTokens, + inputTokens, requested, clamped, }) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 62eddc836a..eec15e6743 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -150,18 +150,6 @@ export namespace LLM { ) // altimate_change end - // altimate_change start — clamp the reserved completion budget against the real prompt size. - // `maxOutputTokens` is a per-model ceiling that ignores `limit.context`, so a large system - // prompt could push `input + reservation` past the window and the provider rejected the - // request with a hard 400 before generating anything. Clamped after the chat.params hook so - // a plugin override is checked too. Throws when no usable budget is left. - const maxOutputTokens = ProviderTransform.clampOutputTokens({ - model: input.model, - requested: params.maxOutputTokens, - inputTokens: ProviderTransform.estimateInputTokens(system, input.messages), - }) - // altimate_change end - const { headers } = await Plugin.trigger( "chat.headers", { @@ -223,6 +211,27 @@ export namespace LLM { } // altimate_change end + // altimate_change start — clamp the reserved completion budget against the real prompt size. + // `maxOutputTokens` is a per-model ceiling that ignores `limit.context`, so a large system + // prompt could push `input + reservation` past the window and the provider rejected the + // request with a hard 400 before generating anything. Placed after the chat.params hook so a + // plugin override is checked too, and after tool resolution and retrieval filtering so the + // estimate covers the tool schemas actually sent. Throws when no usable budget is left. + // Read outside the closure below: `system` is declared as an untyped array upstream and its + // evolved element type is not visible from inside a callback. + const systemParts: string[] = system + const reasoningBudget = ProviderTransform.configuredReasoningBudget(params.options) + const maxOutputTokens = ProviderTransform.clampOutputTokens({ + model: input.model, + requested: params.maxOutputTokens, + context: ProviderTransform.effectiveContext(input.model, headers), + reasoningBudget, + inputTokens: () => ProviderTransform.estimateInputTokens(systemParts, input.messages, tools), + }) + // A clamped completion budget must still leave room for the configured thinking budget. + const clampedOptions = ProviderTransform.clampReasoningBudget(params.options, maxOutputTokens) + // altimate_change end + return streamText({ onError(error) { l.error("stream error", { @@ -257,7 +266,9 @@ export namespace LLM { temperature: params.temperature, topP: params.topP, topK: params.topK, - providerOptions: ProviderTransform.providerOptions(input.model, params.options), + // altimate_change start — use the reasoning-budget-clamped options built above + providerOptions: ProviderTransform.providerOptions(input.model, clampedOptions), + // altimate_change end activeTools: Object.keys(tools).filter((x) => x !== "invalid"), tools, toolChoice: input.toolChoice, diff --git a/packages/opencode/src/session/llm/request.ts b/packages/opencode/src/session/llm/request.ts index 9c94febdd4..d253cb95a1 100644 --- a/packages/opencode/src/session/llm/request.ts +++ b/packages/opencode/src/session/llm/request.ts @@ -173,14 +173,20 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre // prompt could push `input + reservation` past the window and the provider rejected the request // with a hard 400 before generating anything. `input.messages` is used rather than the merged // `messages` because the latter can already carry `system` as leading system messages, which - // would double-count the prompt. Throws when no usable budget is left. + // would double-count the prompt. Tool schemas are counted too — providers bill them as prompt + // tokens. Throws when no usable budget is left. + const clampedMaxOutputTokens = ProviderTransform.clampOutputTokens({ + model: input.model, + requested: params.maxOutputTokens, + context: ProviderTransform.effectiveContext(input.model, headers), + reasoningBudget: ProviderTransform.configuredReasoningBudget(params.options), + inputTokens: () => ProviderTransform.estimateInputTokens(system, input.messages, tools), + }) const clampedParams = { ...params, - maxOutputTokens: ProviderTransform.clampOutputTokens({ - model: input.model, - requested: params.maxOutputTokens, - inputTokens: ProviderTransform.estimateInputTokens(system, input.messages), - }), + maxOutputTokens: clampedMaxOutputTokens, + // A clamped completion budget must still leave room for the configured thinking budget. + options: ProviderTransform.clampReasoningBudget(params.options, clampedMaxOutputTokens), } // altimate_change end diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 28921f99ef..cb34b1d42d 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -4727,11 +4727,22 @@ describe("ProviderTransform.clampOutputTokens", () => { expect(() => ProviderTransform.clampOutputTokens({ model, requested: 16_384, inputTokens: 65_000 })).toThrow() }) - test("leaves a config that already fits completely unchanged", () => { + test("leaves a config with room to spare unchanged", () => { const model = createWindowModel({ context: 65_536, output: 16_384 }) expect(ProviderTransform.clampOutputTokens({ model, requested: 16_384, inputTokens: 10_000 })).toBe(16_384) - // Exactly filling the window is still a valid request and must not be touched. - expect(ProviderTransform.clampOutputTokens({ model, requested: 16_384, inputTokens: 49_152 })).toBe(16_384) + }) + + test("applies the estimator margin to a request that only just fits", () => { + // `inputTokens` is a character-ratio estimate, so a prompt that appears to fill the window + // exactly can still overflow it at the provider's tokenizer. Without the margin on this path + // the boundary is discontinuous: one token over clamps to a ~1K cushion, one token under is + // sent with no protection at all and reproduces the reported 400. + const model = createWindowModel({ context: 65_536, output: 16_384 }) + const exactFill = ProviderTransform.clampOutputTokens({ model, requested: 16_384, inputTokens: 49_152 })! + expect(exactFill).toBeLessThan(16_384) + // 65536 - 49152 - ceil(49152 * 0.02) = 15400 + expect(exactFill).toBe(15_400) + expect(49_152 + exactFill).toBeLessThan(65_536) }) test("leaves large-window models unchanged", () => { @@ -4739,10 +4750,15 @@ describe("ProviderTransform.clampOutputTokens", () => { expect(ProviderTransform.clampOutputTokens({ model, requested: 8_192, inputTokens: 150_000 })).toBe(8_192) }) - test("does not clamp models that budget input separately from output", () => { - // limit.input means the two budgets are not shared, so input + output can exceed context. + test("clamps models that declare limit.input, which is an input ceiling inside the same window", () => { + // `limit.input` is not a separate budget. `Session.Overflow.usable` subtracts the reserved + // completion from it, and catalog-shaped fixtures pair `input` with an equal or larger + // `context` (e.g. context 200K / input 200K / output 32K), so input plus the reservation + // still has to fit `context`. Skipping these models left the exact 400 this guard prevents. const model = createWindowModel({ context: 65_536, input: 65_536, output: 16_384 }) - expect(ProviderTransform.clampOutputTokens({ model, requested: 16_384, inputTokens: 60_000 })).toBe(16_384) + const clamped = ProviderTransform.clampOutputTokens({ model, requested: 16_384, inputTokens: 60_000 })! + expect(clamped).toBeLessThan(16_384) + expect(60_000 + clamped).toBeLessThan(65_536) }) test("does not clamp when the model declares no context window", () => { @@ -4763,6 +4779,137 @@ describe("ProviderTransform.clampOutputTokens", () => { const model = createWindowModel({ context: 65_536, output: 16_384 }) expect(ProviderTransform.clampOutputTokens({ model, requested: undefined, inputTokens: 60_000 })).toBeUndefined() }) + + test("does not estimate the prompt on paths that return before the estimate is needed", () => { + // Serializing the whole history costs real time and allocation on long sessions. Codex and + // GitHub Copilot send no reservation at all, so the estimate must never run for them. + const model = createWindowModel({ context: 65_536, output: 16_384 }) + let calls = 0 + const inputTokens = () => { + calls++ + return 60_000 + } + expect(ProviderTransform.clampOutputTokens({ model, requested: undefined, inputTokens })).toBeUndefined() + expect(calls).toBe(0) + + ProviderTransform.clampOutputTokens({ + model: createWindowModel({ context: 0, output: 16_384 }), + requested: 16_384, + inputTokens, + }) + expect(calls).toBe(0) + + ProviderTransform.clampOutputTokens({ model, requested: 16_384, inputTokens }) + expect(calls).toBe(1) + }) + + test("respects a context window widened by a request header", () => { + // The GitLab AI-gateway loader always sends `anthropic-beta: context-1m-2025-08-07` while its + // catalog entries still declare 200K. Clamping against the catalog value would refuse prompts + // the provider route accepts. + const model = createWindowModel({ context: 200_000, output: 16_384 }) + expect(ProviderTransform.effectiveContext(model, {})).toBe(200_000) + expect(ProviderTransform.effectiveContext(model, { "anthropic-beta": "context-1m-2025-08-07" })).toBe(1_000_000) + + // Without the widened window this 300K prompt would be refused client-side. + expect(() => ProviderTransform.clampOutputTokens({ model, requested: 16_384, inputTokens: 300_000 })).toThrow( + ProviderTransform.OutputTokenBudgetError, + ) + expect( + ProviderTransform.clampOutputTokens({ + model, + requested: 16_384, + inputTokens: 300_000, + context: ProviderTransform.effectiveContext(model, { "anthropic-beta": "context-1m-2025-08-07" }), + }), + ).toBe(16_384) + }) + + test("keeps room for a configured reasoning budget", () => { + // Anthropic rejects a request whose thinking budget is not below max_tokens, and this repo + // configures fixed 16,000/31,999-token budgets. A clamp that ignores them turns one provider + // 400 into another. + const model = createWindowModel({ context: 65_536, output: 16_384 }) + const clamped = ProviderTransform.clampOutputTokens({ + model, + requested: 16_384, + inputTokens: 52_180, + reasoningBudget: 16_000, + })! + // The floor rises to OUTPUT_TOKEN_FLOOR + 1024 so thinking and an answer both fit. + expect(clamped).toBeGreaterThanOrEqual(ProviderTransform.OUTPUT_TOKEN_FLOOR + 1_024) + + const options = { anthropic: { thinking: { type: "enabled", budgetTokens: 16_000 } } } + const adjusted = ProviderTransform.clampReasoningBudget(options, clamped) as typeof options + expect(adjusted.anthropic.thinking.budgetTokens).toBeLessThan(clamped) + expect(adjusted.anthropic.thinking.budgetTokens).toBe(clamped - ProviderTransform.OUTPUT_TOKEN_FLOOR) + // The original object is not mutated. + expect(options.anthropic.thinking.budgetTokens).toBe(16_000) + + // A budget that already fits is left exactly as it was. + const small = { google: { thinkingConfig: { thinkingBudget: 512 } } } + expect(ProviderTransform.clampReasoningBudget(small, 16_384)).toBe(small) + }) + + test("reads the configured reasoning budget out of nested provider options", () => { + expect(ProviderTransform.configuredReasoningBudget({ anthropic: { thinking: { budgetTokens: 31_999 } } })).toBe( + 31_999, + ) + expect( + ProviderTransform.configuredReasoningBudget({ google: { thinkingConfig: { thinkingBudget: 16_000 } } }), + ).toBe(16_000) + expect(ProviderTransform.configuredReasoningBudget({ openai: { reasoningEffort: "high" } })).toBe(0) + expect(ProviderTransform.configuredReasoningBudget(undefined)).toBe(0) + }) +}) + +describe("ProviderTransform.estimateInputTokens", () => { + test("does not count base64 attachment bytes as prompt text", () => { + // A ~1 MiB screenshot arrives as a base64 payload on the model message. Counted as text it + // reads as hundreds of thousands of tokens and would refuse a request the provider accepts, + // so media is stripped first — the same trade-off Compaction.estimate makes. + const image = "A".repeat(1_000_000) + const withMedia = [{ role: "user", content: [{ type: "image", image }] }] + const withoutMedia = [{ role: "user", content: [{ type: "image", image: "" }] }] + expect(ProviderTransform.estimateInputTokens([], withMedia)).toBeLessThan(100) + expect(ProviderTransform.estimateInputTokens([], withMedia)).toBeCloseTo( + ProviderTransform.estimateInputTokens([], withoutMedia), + -1, + ) + + // File parts and data: URLs go the same way, as do raw byte arrays. + const file = [ + { role: "user", content: [{ type: "file", mediaType: "application/pdf", data: "B".repeat(500_000) }] }, + ] + expect(ProviderTransform.estimateInputTokens([], file)).toBeLessThan(100) + const dataUrl = [ + { role: "user", content: [{ type: "image", url: `data:image/png;base64,${"C".repeat(500_000)}` }] }, + ] + expect(ProviderTransform.estimateInputTokens([], dataUrl)).toBeLessThan(100) + const bytes = [{ role: "user", content: [{ type: "image", image: new Uint8Array(200_000) }] }] + expect(ProviderTransform.estimateInputTokens([], bytes)).toBeLessThan(100) + }) + + test("still counts ordinary message text", () => { + const text = "the quick brown fox jumps over the lazy dog ".repeat(500) + const messages = [{ role: "user", content: [{ type: "text", text }] }] + expect(ProviderTransform.estimateInputTokens([], messages)).toBeGreaterThan(4_000) + }) + + test("counts tool definitions, which providers bill as prompt tokens", () => { + // The clamp decides on this number, so omitting the tool schemas under-counts exactly the + // tool-heavy sessions most likely to overflow the window. + const messages = [{ role: "user", content: [{ type: "text", text: "hi" }] }] + const bare = ProviderTransform.estimateInputTokens([], messages) + const tools = Object.fromEntries( + Array.from({ length: 40 }, (_, i) => [ + `tool_${i}`, + { description: "does a thing with several parameters ".repeat(20), inputSchema: { type: "object" } }, + ]), + ) + const withTools = ProviderTransform.estimateInputTokens([], messages, tools) + expect(withTools).toBeGreaterThan(bare + 5_000) + }) }) describe("LLMRequestPrep.prepare - output token reservation", () => { From 3b01975251ae06ca84d0c52bde154ee0c784b1d2 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Sun, 30 Aug 2026 01:14:25 -0700 Subject: [PATCH 07/32] fix: close the gaps the second review pass found in the clamp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five follow-ups, four of them defects in the code the previous commit added. - charge stripped media a token allowance instead of nothing. Replacing an attachment with a marker fixed the over-count, but assigned it effectively zero, so a near-window multimodal request could still be waved through. Each stripped payload now costs a flat 1,600 tokens, roughly Anthropic's cap for a full-size image. - strip only fields that belong to a real media part. The replacer matched on the key names `data` and `image` alone, so an ordinary tool argument with either name — plausible for MCP tools taking document data — was dropped from the estimate, which is the failure direction that leaves a request unclamped. It now checks the containing part's `type` via the replacer's receiver. - never demand more headroom than the model itself offers. Catalog entries such as `alibaba/qwen-plus-character-ja` (8,192 context, 512 output) cannot reach the 1,024 floor at all, so a viable request was refused on a threshold that model can never satisfy. The floor is capped at the requested reservation, and when honouring the margin would force a hard failure the request now falls back to the largest budget that fits without it rather than throwing. - find the 1M context beta wherever it is actually set. The first version read only the `chat.headers` hook result, which is the one place GitLab does not put it: the loader stores it in `provider.options.aiGatewayHeaders` (`provider/provider.ts:742-755`) and the SDK sends it directly. All three sources are now checked — `model.headers`, the hook result, and the gateway headers — with case-insensitive header names. - stop deep-cloning provider options on every request. `clampReasoningBudget` round-tripped the options through JSON even when nothing needed lowering, and the clone silently dropped `undefined` and non-JSON values and would throw on a BigInt. It now does a cheap read-only check first and rebuilds only the branch it changes. Six new tests, including the tool-argument false-strip, the multi-attachment allowance, the 512-token output model, the gateway-header source, and a case asserting a lowered budget preserves values a JSON round-trip would lose. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VqnuBDGkh1ZT65Ti7e6DHZ --- packages/opencode/src/provider/transform.ts | 111 ++++++++++++++---- packages/opencode/src/session/llm.ts | 2 +- packages/opencode/src/session/llm/request.ts | 2 +- .../opencode/test/provider/transform.test.ts | 111 ++++++++++++++++-- 4 files changed, 190 insertions(+), 36 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 11fc34c8c0..2637d604f0 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -1347,28 +1347,56 @@ export namespace ProviderTransform { // marker before estimating — the same trade-off `Compaction.estimate` makes via `stripMedia`. const MEDIA_PLACEHOLDER = "[media]" - function stringifyWithoutMedia(value: unknown): string { - return JSON.stringify(value, (key, val) => { - if (ArrayBuffer.isView(val) || val instanceof ArrayBuffer) return MEDIA_PLACEHOLDER + // A stripped attachment is not free — the provider still bills the decoded media. Anthropic + // charges roughly `width * height / 750` tokens and caps a full-size image near 1,600, so each + // stripped payload is charged this flat allowance rather than nothing. Deliberately an + // over-estimate: under-counting here would let a near-window multimodal request through. + const MEDIA_TOKEN_ALLOWANCE = 1_600 + + /** + * Serialize for estimation with encoded media replaced by a marker, and report how many + * payloads were replaced so the caller can charge them an allowance. + * + * Only fields on an actual media part are stripped. A tool argument that happens to be named + * `data` or `image` is ordinary textual JSON that the provider bills as text, so a key-name + * check alone would silently drop it from the estimate. + */ + function stringifyWithoutMedia(value: unknown): { text: string; media: number } { + let media = 0 + // `this` is the object holding `key`, which is how a media part is told apart from an + // ordinary field that happens to share its name. + const text = JSON.stringify(value, function (this: { type?: unknown } | undefined, key: string, val: unknown) { + if (ArrayBuffer.isView(val) || val instanceof ArrayBuffer) { + media++ + return MEDIA_PLACEHOLDER + } if (typeof val !== "string") return val - if (key === "data" || key === "image") return MEDIA_PLACEHOLDER - if (key === "url" && val.startsWith("data:")) return MEDIA_PLACEHOLDER + const type = this?.type + if (type !== "image" && type !== "file") return val + if (key === "data" || key === "image" || (key === "url" && val.startsWith("data:"))) { + media++ + return MEDIA_PLACEHOLDER + } return val }) + return { text, media } } /** * Rough token count for the prompt about to be sent, used only for the clamp decision. * - * Counts the system text, the messages with media stripped, and the tool definitions — tools - * are sent with the request and providers bill them as prompt tokens, so leaving them out - * would under-count exactly the tool-heavy sessions most likely to overflow. + * Counts the system text, the messages with media stripped but charged a flat allowance, and + * the tool definitions — tools are sent with the request and providers bill them as prompt + * tokens, so leaving them out would under-count exactly the tool-heavy sessions most likely to + * overflow. */ export function estimateInputTokens(system: string[], messages: unknown[], tools?: unknown): number { + const stripped = stringifyWithoutMedia(messages) return ( Token.estimate(system.join("\n")) + - Token.estimate(stringifyWithoutMedia(messages)) + - (tools ? Token.estimate(stringifyWithoutMedia(tools)) : 0) + Token.estimate(stripped.text) + + stripped.media * MEDIA_TOKEN_ALLOWANCE + + (tools ? Token.estimate(stringifyWithoutMedia(tools).text) : 0) ) } @@ -1379,10 +1407,28 @@ export namespace ProviderTransform { * the GitLab AI-gateway loader always sends `anthropic-beta: context-1m-2025-08-07` while its * catalog entries still declare 200K. Clamping against the catalog number there would shrink * or refuse prompts the provider route accepts. + * + * Three sources can carry the flag and all are checked: `model.headers` and the `chat.headers` + * hook result, which both request paths spread into the outgoing headers, and the GitLab + * loader's `provider.options.aiGatewayHeaders`, which the SDK sends without ever passing + * through either. Header names are matched case-insensitively because HTTP header names are. */ - export function effectiveContext(model: Provider.Model, headers?: Record): number { + export function effectiveContext( + model: Provider.Model, + headers?: Record, + providerOptions?: Record, + ): number { const context = model.limit.context - if (headers?.["anthropic-beta"]?.includes("context-1m-2025-08-07")) return Math.max(context, 1_000_000) + const sources = [model.headers, headers, providerOptions?.["aiGatewayHeaders"]] + for (const source of sources) { + if (!source || typeof source !== "object") continue + for (const [name, value] of Object.entries(source)) { + if (name.toLowerCase() !== "anthropic-beta") continue + if (typeof value === "string" && value.includes("context-1m-2025-08-07")) { + return Math.max(context, 1_000_000) + } + } + } return context } @@ -1408,20 +1454,27 @@ export namespace ProviderTransform { * `max_tokens`, and this repository configures fixed 16,000/31,999-token budgets, so clamping * `maxOutputTokens` on its own can turn one provider 400 into another. */ + function lowerReasoningBudgets(value: any, ceiling: number): any { + if (Array.isArray(value)) return value.map((item) => lowerReasoningBudgets(item, ceiling)) + if (typeof value !== "object" || value === null) return value + const next: Record = {} + for (const [key, val] of Object.entries(value)) { + const isBudget = key === "budgetTokens" || key === "thinkingBudget" + next[key] = isBudget && typeof val === "number" && val > ceiling ? ceiling : lowerReasoningBudgets(val, ceiling) + } + return next + } + export function clampReasoningBudget(options: T, maxOutputTokens: number | undefined): T { if (maxOutputTokens === undefined) return options const ceiling = maxOutputTokens - OUTPUT_TOKEN_FLOOR if (ceiling < MIN_REASONING_BUDGET) return options - - let changed = false - const next = JSON.parse(JSON.stringify(options ?? null), (key, val) => { - if ((key === "budgetTokens" || key === "thinkingBudget") && typeof val === "number" && val > ceiling) { - changed = true - return ceiling - } - return val - }) - return changed ? next : options + // Cheap read-only walk first, so the common case — no reasoning configured, or a budget that + // already fits — returns the caller's own object without copying anything. + if (configuredReasoningBudget(options) <= ceiling) return options + // Rebuilt rather than JSON round-tripped: a clone through JSON silently drops `undefined` and + // non-JSON values out of the provider options and throws outright on a BigInt. + return lowerReasoningBudgets(options, ceiling) } /** @@ -1449,13 +1502,13 @@ export namespace ProviderTransform { if (requested === undefined) return undefined // A configured reasoning budget has to fit under the clamped value alongside a real answer. - const floor = + const baseFloor = input.reasoningBudget && input.reasoningBudget > 0 ? OUTPUT_TOKEN_FLOOR + MIN_REASONING_BUDGET : OUTPUT_TOKEN_FLOOR const context = input.context ?? input.model.limit.context - if (!context || context <= floor) return requested + if (!context || context <= baseFloor) return requested const inputTokens = typeof input.inputTokens === "function" ? input.inputTokens() : input.inputTokens if (inputTokens <= 0) return requested @@ -1467,8 +1520,18 @@ export namespace ProviderTransform { const margin = Math.max(CLAMP_MARGIN_MIN, Math.ceil(inputTokens * CLAMP_MARGIN_FRACTION)) if (inputTokens + requested + margin <= context) return requested + // Never demand more headroom than the model itself offers. Some catalog entries cap output + // below the floor (`alibaba/qwen-plus-character-ja` declares 8,192 context / 512 output), and + // failing those requests on a threshold the model can never reach would be wrong. + const floor = Math.min(baseFloor, requested) + const clamped = context - inputTokens - margin if (clamped < floor) { + // Honouring the margin would force a hard client-side failure. If the request still fits the + // window without it, prefer sending a marginal request over refusing a viable one — the + // provider stays the authority on its own tokenizer. + const withoutMargin = context - inputTokens + if (withoutMargin >= floor) return Math.min(requested, withoutMargin) throw new OutputTokenBudgetError({ modelID: input.model.id, providerID: input.model.providerID, diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index eec15e6743..862d414753 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -224,7 +224,7 @@ export namespace LLM { const maxOutputTokens = ProviderTransform.clampOutputTokens({ model: input.model, requested: params.maxOutputTokens, - context: ProviderTransform.effectiveContext(input.model, headers), + context: ProviderTransform.effectiveContext(input.model, headers, provider.options), reasoningBudget, inputTokens: () => ProviderTransform.estimateInputTokens(systemParts, input.messages, tools), }) diff --git a/packages/opencode/src/session/llm/request.ts b/packages/opencode/src/session/llm/request.ts index d253cb95a1..aad48bda5b 100644 --- a/packages/opencode/src/session/llm/request.ts +++ b/packages/opencode/src/session/llm/request.ts @@ -178,7 +178,7 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre const clampedMaxOutputTokens = ProviderTransform.clampOutputTokens({ model: input.model, requested: params.maxOutputTokens, - context: ProviderTransform.effectiveContext(input.model, headers), + context: ProviderTransform.effectiveContext(input.model, headers, input.provider.options), reasoningBudget: ProviderTransform.configuredReasoningBudget(params.options), inputTokens: () => ProviderTransform.estimateInputTokens(system, input.messages, tools), }) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index cb34b1d42d..0053eca421 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -4766,6 +4766,20 @@ describe("ProviderTransform.clampOutputTokens", () => { expect(ProviderTransform.clampOutputTokens({ model, requested: 16_384, inputTokens: 60_000 })).toBe(16_384) }) + test("never demands more headroom than the model's own output limit offers", () => { + // `alibaba/qwen-plus-character-ja` declares an 8,192-token context and a 512-token output + // limit. At 7,169 estimated input the requested 512 still fits (7,681 of 8,192), but the + // 512-token minimum margin computes a 511-token clamp. Failing the request on the unrelated + // 1,024 floor would refuse a viable request on a threshold this model can never reach. + const model = createWindowModel({ context: 8_192, output: 512 }) + expect(ProviderTransform.clampOutputTokens({ model, requested: 512, inputTokens: 7_169 })).toBe(512) + + // The guard still fires when even the model's own reservation cannot fit. + expect(() => ProviderTransform.clampOutputTokens({ model, requested: 512, inputTokens: 8_000 })).toThrow( + ProviderTransform.OutputTokenBudgetError, + ) + }) + test("does not clamp a window too small to hold even a floor-sized completion", () => { // A declared window this small is a placeholder or a test fixture, not a real limit. Failing // the request client-side on numbers we do not believe would be worse than letting the @@ -4811,6 +4825,25 @@ describe("ProviderTransform.clampOutputTokens", () => { expect(ProviderTransform.effectiveContext(model, {})).toBe(200_000) expect(ProviderTransform.effectiveContext(model, { "anthropic-beta": "context-1m-2025-08-07" })).toBe(1_000_000) + // Both request paths spread `model.headers` under the chat.headers result, so the flag counts + // from either source, and HTTP header names are case-insensitive. + const betaModel = createWindowModel({ context: 200_000, output: 16_384 }) + betaModel.headers = { "anthropic-beta": "context-1m-2025-08-07" } + expect(ProviderTransform.effectiveContext(betaModel)).toBe(1_000_000) + expect(ProviderTransform.effectiveContext(betaModel, {})).toBe(1_000_000) + expect(ProviderTransform.effectiveContext(model, { "Anthropic-Beta": "context-1m-2025-08-07" })).toBe(1_000_000) + // A different beta flag must not widen the window. + expect(ProviderTransform.effectiveContext(model, { "anthropic-beta": "interleaved-thinking-2025-05-14" })).toBe( + 200_000, + ) + + // The GitLab loader never routes the flag through model.headers or the chat.headers hook — it + // sits in provider.options.aiGatewayHeaders and the SDK sends it directly. + const gitlabOptions = { aiGatewayHeaders: { "anthropic-beta": "context-1m-2025-08-07" } } + expect(ProviderTransform.effectiveContext(model, {}, gitlabOptions)).toBe(1_000_000) + expect(ProviderTransform.effectiveContext(model, {}, { aiGatewayHeaders: {} })).toBe(200_000) + expect(ProviderTransform.effectiveContext(model, {}, {})).toBe(200_000) + // Without the widened window this 300K prompt would be refused client-side. expect(() => ProviderTransform.clampOutputTokens({ model, requested: 16_384, inputTokens: 300_000 })).toThrow( ProviderTransform.OutputTokenBudgetError, @@ -4846,9 +4879,35 @@ describe("ProviderTransform.clampOutputTokens", () => { // The original object is not mutated. expect(options.anthropic.thinking.budgetTokens).toBe(16_000) - // A budget that already fits is left exactly as it was. + // A budget that already fits is left exactly as it was — the same object, not a copy. const small = { google: { thinkingConfig: { thinkingBudget: 512 } } } expect(ProviderTransform.clampReasoningBudget(small, 16_384)).toBe(small) + // So are options with no reasoning configured at all, which is the common case. + const none = { openai: { reasoningEffort: "high" } } + expect(ProviderTransform.clampReasoningBudget(none, 16_384)).toBe(none) + }) + + test("lowering a reasoning budget preserves values a JSON round-trip would lose", () => { + // The options object is rebuilt field by field rather than cloned through JSON, which would + // drop `undefined` and function values and throw outright on a BigInt. + const noop = () => {} + const options = { + anthropic: { thinking: { type: "enabled", budgetTokens: 31_999 } }, + keepUndefined: undefined, + keepFunction: noop, + keepBigInt: 10n, + nested: [{ thinkingBudget: 31_999 }, { untouched: "value" }], + } + const adjusted = ProviderTransform.clampReasoningBudget(options, 8_192) + + expect(adjusted.anthropic.thinking.budgetTokens).toBe(8_192 - ProviderTransform.OUTPUT_TOKEN_FLOOR) + expect(adjusted.nested[0].thinkingBudget).toBe(8_192 - ProviderTransform.OUTPUT_TOKEN_FLOOR) + expect(adjusted.nested[1].untouched).toBe("value") + expect("keepUndefined" in adjusted).toBe(true) + expect(adjusted.keepFunction).toBe(noop) + expect(adjusted.keepBigInt).toBe(10n) + // The caller's object is untouched. + expect(options.anthropic.thinking.budgetTokens).toBe(31_999) }) test("reads the configured reasoning budget out of nested provider options", () => { @@ -4868,26 +4927,58 @@ describe("ProviderTransform.estimateInputTokens", () => { // A ~1 MiB screenshot arrives as a base64 payload on the model message. Counted as text it // reads as hundreds of thousands of tokens and would refuse a request the provider accepts, // so media is stripped first — the same trade-off Compaction.estimate makes. + // Each stripped payload is charged a flat allowance rather than nothing, so the estimate lands + // in the low thousands instead of the ~270,000 a raw serialization would produce. const image = "A".repeat(1_000_000) const withMedia = [{ role: "user", content: [{ type: "image", image }] }] - const withoutMedia = [{ role: "user", content: [{ type: "image", image: "" }] }] - expect(ProviderTransform.estimateInputTokens([], withMedia)).toBeLessThan(100) - expect(ProviderTransform.estimateInputTokens([], withMedia)).toBeCloseTo( - ProviderTransform.estimateInputTokens([], withoutMedia), - -1, - ) + expect(ProviderTransform.estimateInputTokens([], withMedia)).toBeLessThan(2_000) + expect(ProviderTransform.estimateInputTokens([], withMedia)).toBeGreaterThan(1_000) // File parts and data: URLs go the same way, as do raw byte arrays. const file = [ { role: "user", content: [{ type: "file", mediaType: "application/pdf", data: "B".repeat(500_000) }] }, ] - expect(ProviderTransform.estimateInputTokens([], file)).toBeLessThan(100) + expect(ProviderTransform.estimateInputTokens([], file)).toBeLessThan(2_000) const dataUrl = [ { role: "user", content: [{ type: "image", url: `data:image/png;base64,${"C".repeat(500_000)}` }] }, ] - expect(ProviderTransform.estimateInputTokens([], dataUrl)).toBeLessThan(100) + expect(ProviderTransform.estimateInputTokens([], dataUrl)).toBeLessThan(2_000) const bytes = [{ role: "user", content: [{ type: "image", image: new Uint8Array(200_000) }] }] - expect(ProviderTransform.estimateInputTokens([], bytes)).toBeLessThan(100) + expect(ProviderTransform.estimateInputTokens([], bytes)).toBeLessThan(2_000) + + // Several attachments cost several allowances, so a near-window multimodal request is not + // waved through as though the media were free. + const many = [ + { + role: "user", + content: [ + { type: "image", image }, + { type: "image", image }, + { type: "image", image }, + ], + }, + ] + expect(ProviderTransform.estimateInputTokens([], many)).toBeGreaterThan( + ProviderTransform.estimateInputTokens([], withMedia) * 2, + ) + }) + + test("only strips fields that belong to an actual media part", () => { + // A tool argument named `data` or `image` is ordinary textual JSON that the provider bills as + // text. Stripping it on the key name alone would drop it from the estimate and leave the + // reservation unclamped on exactly the large-argument requests that need clamping. + const payload = "x".repeat(40_000) + const toolArgs = [ + { + role: "assistant", + content: [{ type: "tool-call", toolName: "ingest", input: { data: payload, image: payload } }], + }, + ] + expect(ProviderTransform.estimateInputTokens([], toolArgs)).toBeGreaterThan(15_000) + + // The same field names on a real media part are still stripped. + const mediaPart = [{ role: "user", content: [{ type: "image", image: payload }] }] + expect(ProviderTransform.estimateInputTokens([], mediaPart)).toBeLessThan(2_000) }) test("still counts ordinary message text", () => { From 4304de852ca53ba6005606938eb10eea40c2bf4d Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Sun, 30 Aug 2026 01:17:43 -0700 Subject: [PATCH 08/32] fix(provider): close output budget media bypasses --- .../src/provider/output-token-budget.ts | 78 ++++--- packages/opencode/src/provider/transform.ts | 20 +- packages/opencode/src/session/llm.ts | 2 +- packages/opencode/src/session/llm/request.ts | 2 +- .../opencode/test/provider/transform.test.ts | 215 +++++++++++++++++- packages/opencode/test/session/llm.test.ts | 92 ++++++++ 6 files changed, 374 insertions(+), 35 deletions(-) diff --git a/packages/opencode/src/provider/output-token-budget.ts b/packages/opencode/src/provider/output-token-budget.ts index 7696c83048..4c7a5d49f5 100644 --- a/packages/opencode/src/provider/output-token-budget.ts +++ b/packages/opencode/src/provider/output-token-budget.ts @@ -13,10 +13,23 @@ const CLAMP_MARGIN_MIN = 512 const ESTIMATE_CHUNK_SIZE = 400 const MEDIA_TOKEN_ALLOWANCE = 2_048 const MIN_REASONING_BUDGET = 1_024 -const MEDIA_DATA_URL = /^data:(?:image\/|audio\/|video\/|application\/pdf(?:;|,))[^,]*,/i const EMOJI = /\p{Extended_Pictographic}/u const REASONING_BUDGET_KEYS = new Set(["budgetTokens", "thinkingBudget", "budget_tokens"]) const CONTEXT_WINDOW_BETAS = new Map([["context-1m-2025-08-07", 1_000_000]]) +const MEDIA_PART_TYPES = new Set([ + "image", + "file", + "media", + "audio", + "video", + "file-data", + "file-url", + "file-id", + "image-data", + "image-url", + "image-file-id", +]) +const MEDIA_PAYLOAD_KEYS = new Set(["data", "image", "file", "audio", "video", "url", "fileId"]) type JsonRecord = Record @@ -121,41 +134,50 @@ function estimateTextTokens(input: string): number { return total } -/** Detect message containers whose data/image fields carry media rather than prompt text. */ -function isMediaContainer(value: unknown): boolean { - if (!isRecord(value)) return false - if (value.type === "Buffer") return true - if (["image", "audio", "video", "file"].includes(String(value.type))) return true - const mediaType = value.mediaType ?? value.mimeType - return typeof mediaType === "string" && /^(?:image|audio|video)\//.test(mediaType) +/** Mark only actual ModelMessage content parts whose payload is provider media. */ +function messageMediaContainers(messages: readonly unknown[]): WeakSet { + const result = new WeakSet() + const visited = new WeakSet() + + const visitContent = (content: unknown) => { + if (!Array.isArray(content) || visited.has(content)) return + visited.add(content) + for (const part of content) { + if (!isRecord(part)) continue + if (MEDIA_PART_TYPES.has(String(part.type))) result.add(part) + + // Tool-result media is nested in the AI SDK's typed content output. + if (part.type !== "tool-result" || !isRecord(part.output)) continue + if (part.output.type === "content") visitContent(part.output.value) + } + } + + for (const message of messages) { + if (isRecord(message)) visitContent(message.content) + } + return result } -/** Serialize request structures without expanding encoded media bytes into fake text tokens. */ -function serializeForEstimate(value: unknown): { readonly text: string; readonly mediaParts: number } { +/** Serialize request structures without expanding semantic media payloads into fake text tokens. */ +function serializeForEstimate( + value: unknown, + mediaContainers?: WeakSet, +): { readonly text: string; readonly mediaParts: number } { let mediaParts = 0 const ancestors: object[] = [] const text = JSON.stringify(value, function (key, child) { while (ancestors.length > 0 && ancestors.at(-1) !== this) ancestors.pop() - const mediaField = ["data", "image", "audio", "video", "file"].includes(key) && isMediaContainer(this) + const mediaField = + typeof this === "object" && this !== null && mediaContainers?.has(this) && MEDIA_PAYLOAD_KEYS.has(key) if (mediaField && child !== undefined && child !== null) { - mediaParts++ return "[media omitted]" } - if (typeof child === "string") { - if (MEDIA_DATA_URL.test(child)) { - mediaParts++ - return "[encoded media omitted]" - } - return child - } if (typeof child === "object" && child !== null) { - if (ArrayBuffer.isView(child) || child instanceof ArrayBuffer) { - mediaParts++ - return "[binary media omitted]" - } if (ancestors.includes(child)) return "[circular value omitted]" + // Count transport occurrences, not object identities: JSON duplicates shared aliases. + if (mediaContainers?.has(child)) mediaParts++ ancestors.push(child) } return child @@ -207,10 +229,12 @@ export function estimateInputTokens(input: { const system = input.system.join("\n") let total = estimateTextTokens(system) - for (const value of [input.messages, input.tools]) { - if (value === undefined) continue - const serialized = serializeForEstimate(value) - total += estimateTextTokens(serialized.text) + serialized.mediaParts * MEDIA_TOKEN_ALLOWANCE + const messages = serializeForEstimate(input.messages, messageMediaContainers(input.messages)) + total += estimateTextTokens(messages.text) + messages.mediaParts * MEDIA_TOKEN_ALLOWANCE + + if (input.tools !== undefined) { + const tools = serializeForEstimate(input.tools) + total += estimateTextTokens(tools.text) } if (input.instructions !== undefined && input.instructions !== system) { diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 446b1a5036..6e4d3cba8b 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -334,9 +334,10 @@ export namespace ProviderTransform { // Check for empty base64 image data if (part.type === "image") { - const imageStr = part.image.toString() - if (imageStr.startsWith("data:")) { - const match = imageStr.match(/^data:([^;]+);base64,(.*)$/) + // altimate_change start — support every valid image payload form and case + const imageStr = typeof part.image === "string" ? part.image : undefined + if (imageStr && /^data:/i.test(imageStr)) { + const match = imageStr.match(/^data:([^;]+);base64,(.*)$/i) if (match && (!match[2] || match[2].length === 0)) { return { type: "text" as const, @@ -344,11 +345,14 @@ export namespace ProviderTransform { } } } + // altimate_change end } - const mime = part.type === "image" ? part.image.toString().split(";")[0].replace("data:", "") : part.mediaType + // altimate_change start — classify semantic images independently of their payload representation const filename = part.type === "file" ? part.filename : undefined - const modality = mimeToModality(mime) + const modality = + part.type === "image" ? "image" : mimeToModality(part.mediaType.split(";", 1)[0]!.trim().toLowerCase()) + // altimate_change end if (!modality) return part if (model.capabilities.input[modality]) return part @@ -363,6 +367,12 @@ export namespace ProviderTransform { }) } + // altimate_change start — expose the pure media projection used before input-budget estimation + export function messagesForInputEstimate(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] { + return unsupportedParts(msgs, model) + } + // altimate_change end + // altimate_change start — shared providerOptions transform used before request signing function mapProviderOptions( msgs: ModelMessage[], diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 178e9ad0d7..6b47b98761 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -236,7 +236,7 @@ export namespace LLM { inputTokens: () => estimateInputTokens({ system, - messages: input.messages, + messages: ProviderTransform.messagesForInputEstimate(input.messages, input.model), tools, instructions: params.options.instructions, }), diff --git a/packages/opencode/src/session/llm/request.ts b/packages/opencode/src/session/llm/request.ts index 6ce880eb50..8d8c6be3fe 100644 --- a/packages/opencode/src/session/llm/request.ts +++ b/packages/opencode/src/session/llm/request.ts @@ -188,7 +188,7 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre inputTokens: () => estimateInputTokens({ system, - messages: input.messages, + messages: ProviderTransform.messagesForInputEstimate(input.messages, input.model), tools: sortedTools, instructions: params.options.instructions, }), diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 6e09b622a7..b5b5c5e655 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -4824,6 +4824,61 @@ describe("output token budget", () => { expect(estimated).toBeLessThan(10_000) }) + test("counts data-URL-shaped text in every textual request field", () => { + const prefixes = [ + "data:image/png;base64,", + "data:audio/wav;base64,", + "data:video/mp4;base64,", + "data:application/pdf;base64,", + ] + for (const prefix of prefixes) { + const text = prefix + "漢".repeat(70_000) + expect( + estimateInputTokens({ + system: [], + messages: [{ role: "user", content: [{ type: "text", text }] }], + }), + ).toBeGreaterThan(70_000) + } + + const text = prefixes[0] + "漢".repeat(70_000) + const estimates = [ + estimateInputTokens({ system: [], messages: [{ role: "user", content: text }] }), + estimateInputTokens({ system: [], messages: [], instructions: text }), + estimateInputTokens({ + system: [], + messages: [], + tools: { inspect: { description: text, inputSchema: { type: "object" } } }, + }), + estimateInputTokens({ + system: [], + messages: [ + { + role: "assistant", + content: [ + { type: "tool-call", toolCallId: "call-1", toolName: "inspect", input: { type: "file", data: text } }, + ], + }, + ], + }), + estimateInputTokens({ + system: [], + messages: [ + { + role: "user", + content: [{ type: "image", image: "data:image/png;base64,AQ==", filename: text }], + }, + ], + }), + ] + for (const estimated of estimates) expect(estimated).toBeGreaterThan(70_000) + + const model = createWindowModel({ context: 65_536, output: 16_384 }) + expect(() => clampOutputTokens({ model, requested: 16_384, inputTokens: estimates[0] })).toThrow( + OutputTokenBudgetError, + ) + }) + test("charges the same fixed allowance for URL-backed media", () => { const base = estimateInputTokens({ system: [], messages: [{ role: "user", content: "show this" }] }) const estimated = estimateInputTokens({ @@ -4839,6 +4894,148 @@ describe("output token budget", () => { expect(estimated).toBeLessThan(base + 3_000) }) + test("keeps binary image and PDF tool-result payloads on the fixed media allowance", () => { + const binaryImage = estimateInputTokens({ + system: [], + messages: [ + { + role: "user", + content: [{ type: "image", image: new Uint8Array(1_048_576) }], + }, + ], + }) + const pdfToolResult = estimateInputTokens({ + system: [], + messages: [ + { + role: "tool", + content: [ + { + type: "tool-result", + toolCallId: "call-1", + toolName: "read", + output: { + type: "content", + value: [{ type: "media", mediaType: "application/pdf", data: "A".repeat(1_048_576) }], + }, + }, + ], + }, + ], + }) + for (const estimated of [binaryImage, pdfToolResult]) { + expect(estimated).toBeGreaterThan(2_000) + expect(estimated).toBeLessThan(10_000) + } + }) + + test("counts every AI SDK v6 tool-result media variant once per transport occurrence", () => { + const payload = "A".repeat(1_048_576) + const variants = [ + { type: "media" as const, mediaType: "application/pdf", data: payload }, + { type: "file-data" as const, mediaType: "application/pdf", data: payload }, + { type: "file-url" as const, url: `https://example.invalid/${payload}` }, + { type: "file-id" as const, fileId: { openai: payload } }, + { type: "image-data" as const, mediaType: "image/png", data: payload }, + { type: "image-url" as const, url: `https://example.invalid/${payload}` }, + { type: "image-file-id" as const, fileId: { openai: payload } }, + ] + + for (const variant of variants) { + const estimated = estimateInputTokens({ + system: [], + messages: [ + { + role: "tool", + content: [ + { + type: "tool-result", + toolCallId: "call-1", + toolName: "read", + output: { type: "content", value: [variant] }, + }, + ], + }, + ], + }) + expect(estimated).toBeGreaterThan(2_000) + expect(estimated).toBeLessThan(10_000) + } + + const shared = { type: "image-data" as const, mediaType: "image/png", data: "AQ==" } + const repeated = estimateInputTokens({ + system: [], + messages: [ + { + role: "tool", + content: [ + { + type: "tool-result", + toolCallId: "call-1", + toolName: "read", + output: { type: "content", value: Array.from({ length: 64 }, () => shared) }, + }, + ], + }, + ], + }) + expect(repeated).toBeGreaterThan(64 * 2_000) + }) + + test("projects unsupported media before estimation without discounting supported media", () => { + const unsupported = createWindowModel({ context: 65_536, output: 16_384 }) + const messages = [ + { + role: "user", + content: Array.from({ length: 64 }, () => ({ + type: "image" as const, + image: "data:image/png;base64,AQ==", + })), + }, + ] satisfies ModelMessage[] + + const projected = ProviderTransform.messagesForInputEstimate(messages, unsupported) + expect((messages[0].content[0] as { type: string }).type).toBe("image") + expect((projected[0]!.content[0] as { type: string }).type).toBe("text") + const projectedEstimate = estimateInputTokens({ system: [], messages: projected }) + expect(projectedEstimate).toBeLessThan(10_000) + expect(clampOutputTokens({ model: unsupported, requested: 16_384, inputTokens: projectedEstimate })).toBe(16_384) + + const supported = { + ...unsupported, + capabilities: { + ...unsupported.capabilities, + input: { ...unsupported.capabilities.input, image: true }, + }, + } + const preserved = ProviderTransform.messagesForInputEstimate(messages, supported) + expect((preserved[0]!.content[0] as { type: string }).type).toBe("image") + expect(estimateInputTokens({ system: [], messages: preserved })).toBeGreaterThan(64 * 2_000) + }) + + test("projects every valid unsupported image payload and case-normalized file media type", () => { + const unsupported = createWindowModel({ context: 65_536, output: 16_384 }) + const messages = [ + { + role: "user", + content: [ + { type: "image" as const, image: new URL("https://example.invalid/image.png") }, + { type: "image" as const, image: "AQ==" }, + { type: "image" as const, image: new Uint8Array([1]) }, + { type: "image" as const, image: new Uint8Array([1]).buffer }, + { type: "image" as const, image: "DATA:IMAGE/PNG;BASE64,AQ==" }, + { type: "file" as const, data: "AQ==", mediaType: "IMAGE/PNG" }, + { type: "file" as const, data: "AQ==", mediaType: "APPLICATION/PDF; VERSION=1.7" }, + ], + }, + ] satisfies ModelMessage[] + + const projected = ProviderTransform.messagesForInputEstimate(messages, unsupported) + expect((projected[0]!.content as Array<{ type: string }>).every((part) => part.type === "text")).toBeTrue() + expect(estimateInputTokens({ system: [], messages: projected })).toBeLessThan(10_000) + expect((messages[0].content as Array<{ type: string }>).every((part) => part.type !== "text")).toBeTrue() + }) + test("counts repeated shared tool objects while terminating true cycles", () => { const sharedTool = tool({ description: "shared schema documentation ".repeat(1_200), @@ -4923,6 +5120,7 @@ describe("LLMRequestPrep.prepare - output token reservation", () => { readonly tools?: Record readonly agentOptions?: Record readonly outputTokenMax?: number + readonly messages?: ModelMessage[] } = {}, ) => Effect.runPromise( @@ -4945,7 +5143,7 @@ describe("LLMRequestPrep.prepare - output token reservation", () => { prompt: systemPrompt, } as any, system: [], - messages, + messages: overrides.messages ?? messages, tools: overrides.tools ?? {}, provider: { id: "openai-compatible", options: {} } as any, auth: undefined, @@ -4988,6 +5186,21 @@ describe("LLMRequestPrep.prepare - output token reservation", () => { expect(result.params.maxOutputTokens).toBe(16_384) }) + test("unsupported media is normalized before the request-builder estimate", async () => { + const result = await run("You are a helpful assistant.", { + messages: [ + { + role: "user", + content: Array.from({ length: 64 }, () => ({ + type: "image" as const, + image: "data:image/png;base64,AQ==", + })), + }, + ], + }) + expect(result.params.maxOutputTokens).toBe(16_384) + }) + test("large finalized tool schemas participate in the request-builder clamp", async () => { const mediumPrompt = PROSE.repeat(Math.ceil((47_500 * 3.7) / PROSE.length)) expect((await run(mediumPrompt)).params.maxOutputTokens).toBe(16_384) diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index 539585df58..74101d9bcb 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -770,4 +770,96 @@ describe("session.llm.stream", () => { }, }) }, 30_000) + + test("normalizes unsupported media before the Google stream budget is enforced", async () => { + const server = state.server + if (!server) throw new Error("Server not initialized") + + const providerID = "google" + const modelID = "gemini-2.5-flash" + const fixture = await loadFixture(providerID, modelID) + const pathSuffix = `/v1beta/models/${fixture.model.id}:streamGenerateContent` + const request = waitRequest( + pathSuffix, + createEventResponse([ + { + candidates: [{ content: { parts: [{ text: "Hello" }] }, finishReason: "STOP" }], + usageMetadata: { promptTokenCount: 1, candidatesTokenCount: 1, totalTokenCount: 2 }, + }, + ]), + ) + + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://altimate.ai/config.json", + enabled_providers: [providerID], + provider: { + [providerID]: { + options: { apiKey: "test-google-key", baseURL: `${server.url.origin}/v1beta` }, + }, + }, + }), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const resolved = await Provider.getModel(ProviderID.make(providerID), ModelID.make(fixture.model.id)) + const textOnly = { + ...resolved, + capabilities: { + ...resolved.capabilities, + input: { ...resolved.capabilities.input, image: false }, + }, + limit: { ...resolved.limit, context: 65_536, output: 16_384 }, + } + const sessionID = SessionID.make("session-budget-unsupported-media") + const agent = { + name: "test", + mode: "primary", + options: {}, + permission: [{ permission: "*", pattern: "*", action: "allow" }], + } satisfies Agent.Info + const user = { + id: MessageID.make("msg_user_budget_unsupported_media"), + sessionID, + role: "user", + time: { created: Date.now() }, + agent: agent.name, + model: { providerID: ProviderID.make(providerID), modelID: textOnly.id }, + } satisfies MessageV2.User + + const stream = await LLM.stream({ + user, + sessionID, + model: textOnly, + agent, + system: ["You are a helpful assistant."], + abort: new AbortController().signal, + messages: [ + { + role: "user", + content: Array.from({ length: 64 }, () => ({ + type: "image" as const, + image: "data:image/png;base64,AQ==", + })), + }, + ], + tools: {}, + }) + for await (const _ of stream.fullStream) { + } + + const capture = await request + const config = capture.body.generationConfig as { maxOutputTokens?: number } | undefined + expect(config?.maxOutputTokens).toBe(16_384) + expect(JSON.stringify(capture.body.contents)).toContain("Cannot read image") + }, + }) + }, 30_000) }) From 9039a178c0dc312d84ad6e74ed6bc675ec0968f5 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Sun, 30 Aug 2026 01:20:15 -0700 Subject: [PATCH 09/32] fix(provider): preserve small-model output budgets --- .../src/provider/output-token-budget.ts | 10 ++++- .../opencode/test/provider/transform.test.ts | 40 +++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/provider/output-token-budget.ts b/packages/opencode/src/provider/output-token-budget.ts index 4c7a5d49f5..0460a6ae48 100644 --- a/packages/opencode/src/provider/output-token-budget.ts +++ b/packages/opencode/src/provider/output-token-budget.ts @@ -279,15 +279,21 @@ export function clampOutputTokens(input: { if (!context || context <= OUTPUT_TOKEN_FLOOR) return requested if (inputTokens + requested + margin <= context) return requested + // Do not reject a model for failing to reach a floor above its own reservation. + const floor = Math.min(OUTPUT_TOKEN_FLOOR, requested) const clamped = Math.floor(context - inputTokens - margin) - if (clamped < OUTPUT_TOKEN_FLOOR) { + if (clamped < floor) { + // If only the estimator margin causes the failure, preserve a reservation that still fits + // the declared window. The provider remains authoritative for its exact tokenizer. + const withoutMargin = Math.floor(context - inputTokens) + if (withoutMargin >= floor) return Math.min(requested, withoutMargin) throw new OutputTokenBudgetError({ modelID: input.model.id, providerID: input.model.providerID, inputTokens, requested, context, - floor: OUTPUT_TOKEN_FLOOR, + floor, }) } log.warn("clamped output token reservation to fit context window", { diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index b5b5c5e655..4c02030db7 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -4764,6 +4764,12 @@ describe("output token budget", () => { expect(clampOutputTokens({ model, requested: 16_384, inputTokens: 60_000 })).toBe(16_384) }) + test("never demands more headroom than the model's own output reservation", () => { + const model = createWindowModel({ context: 8_192, output: 512 }) + expect(clampOutputTokens({ model, requested: 512, inputTokens: 7_169 })).toBe(512) + expect(() => clampOutputTokens({ model, requested: 512, inputTokens: 8_000 })).toThrow(OutputTokenBudgetError) + }) + test("does not clamp a window too small to hold even a floor-sized completion", () => { // A declared window this small is a placeholder or a test fixture, not a real limit. Failing // the request client-side on numbers we do not believe would be worse than letting the @@ -5066,6 +5072,18 @@ describe("output token budget", () => { headerSources: [{ aiGatewayHeaders: { "anthropic-beta": "context-1m-2025-08-07" } }], }), ).toBe(1_000_000) + expect( + effectiveContextWindow({ + model, + headerSources: [{ "Anthropic-Beta": "context-1m-2025-08-07" }], + }), + ).toBe(1_000_000) + expect( + effectiveContextWindow({ + model, + headerSources: [{ aiGatewayHeaders: { "anthropic-beta": "interleaved-thinking-2025-05-14" } }], + }), + ).toBe(200_000) }) test("clamps fixed reasoning budgets with the output reservation without mutating inputs", () => { @@ -5081,6 +5099,28 @@ describe("output token budget", () => { expect(options.thinking.budgetTokens).toBe(16_000) }) + test("preserves non-JSON provider options while lowering reasoning budgets", () => { + const callback = () => undefined + const options = { + thinking: { budgetTokens: 31_999 }, + keepUndefined: undefined, + keepFunction: callback, + keepBigInt: 10n, + nested: [{ thinkingBudget: 31_999 }, { untouched: "value" }], + } + const result = clampReasoningBudget(options, 8_192) + expect(result.thinking.budgetTokens).toBe(7_168) + expect(result.nested[0]!.thinkingBudget).toBe(7_168) + expect(result.nested[1]!.untouched).toBe("value") + expect("keepUndefined" in result).toBeTrue() + expect(result.keepFunction).toBe(callback) + expect(result.keepBigInt).toBe(10n) + expect(options.thinking.budgetTokens).toBe(31_999) + + const unchanged = { thinking: { budgetTokens: 512 } } + expect(clampReasoningBudget(unchanged, 8_192)).toBe(unchanged) + }) + test("fails clearly when reasoning and visible output cannot both fit", () => { expect(() => clampReasoningBudget({ thinking: { budgetTokens: 16_000 } }, 1_500)).toThrow( /cannot preserve the configured reasoning budget/, From ab7c90731b6ede1b77ed463e8b31efe5ed89d8b7 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Sun, 30 Aug 2026 01:43:46 -0700 Subject: [PATCH 10/32] fix: close final output budget review gaps --- .../src/provider/output-token-budget.ts | 117 +++++++++++++++--- .../opencode/test/provider/transform.test.ts | 63 ++++++++-- 2 files changed, 153 insertions(+), 27 deletions(-) diff --git a/packages/opencode/src/provider/output-token-budget.ts b/packages/opencode/src/provider/output-token-budget.ts index 0460a6ae48..5116cadbca 100644 --- a/packages/opencode/src/provider/output-token-budget.ts +++ b/packages/opencode/src/provider/output-token-budget.ts @@ -12,6 +12,11 @@ const CLAMP_MARGIN_FRACTION = 0.02 const CLAMP_MARGIN_MIN = 512 const ESTIMATE_CHUNK_SIZE = 400 const MEDIA_TOKEN_ALLOWANCE = 2_048 +const FILE_TOKEN_ALLOWANCE = 16_384 +const PDF_TOKEN_ALLOWANCE = 32_768 +const PDF_TOKENS_PER_PAGE = 5_000 +const PDF_FALLBACK_BYTES_PER_TOKEN = 4 +const PDF_PAGE_LIMIT = 600 const MIN_REASONING_BUDGET = 1_024 const EMOJI = /\p{Extended_Pictographic}/u const REASONING_BUDGET_KEYS = new Set(["budgetTokens", "thinkingBudget", "budget_tokens"]) @@ -30,6 +35,7 @@ const MEDIA_PART_TYPES = new Set([ "image-file-id", ]) const MEDIA_PAYLOAD_KEYS = new Set(["data", "image", "file", "audio", "video", "url", "fileId"]) +const FILE_PART_TYPES = new Set(["file", "media", "file-data", "file-url", "file-id"]) type JsonRecord = Record @@ -134,9 +140,83 @@ function estimateTextTokens(input: string): number { return total } +/** Return the first transport payload carried by a semantic media part. */ +function mediaPayload(part: JsonRecord): unknown { + for (const key of MEDIA_PAYLOAD_KEYS) { + if (part[key] !== undefined && part[key] !== null) return part[key] + } + return undefined +} + +/** Resolve a media part's MIME type from its declared type, data URL, or URL suffix. */ +function mediaType(part: JsonRecord, payload: unknown): string | undefined { + const declared = + typeof part.mediaType === "string" ? part.mediaType : typeof part.mime === "string" ? part.mime : undefined + if (declared) return declared.split(";", 1)[0].trim().toLowerCase() + + const value = payload instanceof URL ? payload.href : typeof payload === "string" ? payload : undefined + const dataType = value?.match(/^data:([^;,]+)/i)?.[1] + if (dataType) return dataType.toLowerCase() + if (value && /\.pdf(?:[?#]|$)/i.test(value)) return "application/pdf" + if (String(part.type).startsWith("image")) return "image/*" + return undefined +} + +/** Decode inline PDF bytes for page-tree inspection; remote URLs and file IDs stay unknown. */ +function inlinePdfBytes(payload: unknown): Uint8Array | undefined { + if (ArrayBuffer.isView(payload)) { + return new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength) + } + if (payload instanceof ArrayBuffer) return new Uint8Array(payload) + + const value = payload instanceof URL ? payload.href : typeof payload === "string" ? payload : undefined + if (!value || /^https?:/i.test(value)) return undefined + try { + if (/^data:/i.test(value)) { + const comma = value.indexOf(",") + if (comma === -1) return undefined + const header = value.slice(0, comma) + const body = value.slice(comma + 1) + return /;base64(?:;|$)/i.test(header) + ? Buffer.from(body, "base64") + : Buffer.from(decodeURIComponent(body), "latin1") + } + return Buffer.from(value, "base64") + } catch { + return undefined + } +} + +/** Estimate PDF pages from the standard page tree, including conservative object-stream fallback. */ +function pdfTokenAllowance(payload: unknown): number { + const bytes = inlinePdfBytes(payload) + if (!bytes) return PDF_TOKEN_ALLOWANCE + + const source = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("latin1") + const leafPages = source.match(/\/Type\s*\/Page\b/g)?.length ?? 0 + let pages = leafPages + for (const match of source.matchAll(/\/Count\s+(\d+)/g)) { + pages = Math.max(pages, Number(match[1])) + } + pages = Math.min(pages, PDF_PAGE_LIMIT) + if (pages > 0) return Math.max(MEDIA_TOKEN_ALLOWANCE, pages * PDF_TOKENS_PER_PAGE) + + return Math.max(PDF_TOKEN_ALLOWANCE, Math.ceil(bytes.byteLength / PDF_FALLBACK_BYTES_PER_TOKEN)) +} + +/** Assign an allowance that matches the semantic media kind rather than its encoded bytes. */ +function mediaTokenAllowance(part: JsonRecord): number { + const payload = mediaPayload(part) + const mime = mediaType(part, payload) + if (mime?.startsWith("image/") || String(part.type).startsWith("image")) return MEDIA_TOKEN_ALLOWANCE + if (mime === "application/pdf") return pdfTokenAllowance(payload) + if (FILE_PART_TYPES.has(String(part.type))) return FILE_TOKEN_ALLOWANCE + return MEDIA_TOKEN_ALLOWANCE +} + /** Mark only actual ModelMessage content parts whose payload is provider media. */ -function messageMediaContainers(messages: readonly unknown[]): WeakSet { - const result = new WeakSet() +function messageMediaAllowances(messages: readonly unknown[]): WeakMap { + const result = new WeakMap() const visited = new WeakSet() const visitContent = (content: unknown) => { @@ -144,7 +224,7 @@ function messageMediaContainers(messages: readonly unknown[]): WeakSet { visited.add(content) for (const part of content) { if (!isRecord(part)) continue - if (MEDIA_PART_TYPES.has(String(part.type))) result.add(part) + if (MEDIA_PART_TYPES.has(String(part.type))) result.set(part, mediaTokenAllowance(part)) // Tool-result media is nested in the AI SDK's typed content output. if (part.type !== "tool-result" || !isRecord(part.output)) continue @@ -161,9 +241,9 @@ function messageMediaContainers(messages: readonly unknown[]): WeakSet { /** Serialize request structures without expanding semantic media payloads into fake text tokens. */ function serializeForEstimate( value: unknown, - mediaContainers?: WeakSet, -): { readonly text: string; readonly mediaParts: number } { - let mediaParts = 0 + mediaContainers?: WeakMap, +): { readonly text: string; readonly mediaTokens: number } { + let mediaTokens = 0 const ancestors: object[] = [] const text = JSON.stringify(value, function (key, child) { @@ -177,12 +257,12 @@ function serializeForEstimate( if (typeof child === "object" && child !== null) { if (ancestors.includes(child)) return "[circular value omitted]" // Count transport occurrences, not object identities: JSON duplicates shared aliases. - if (mediaContainers?.has(child)) mediaParts++ + mediaTokens += mediaContainers?.get(child) ?? 0 ancestors.push(child) } return child }) ?? "" - return { text, mediaParts } + return { text, mediaTokens } } /** Collect Anthropic beta values only from header-shaped records. */ @@ -209,11 +289,14 @@ export function effectiveContextWindow(input: { readonly headerSources?: readonly unknown[] }): number { let context = input.model.limit.context + let finalValues: string[] = [] for (const source of input.headerSources ?? []) { - for (const value of anthropicBetaValues(source)) { - for (const beta of value.split(/[\s,]+/)) { - context = Math.max(context, CONTEXT_WINDOW_BETAS.get(beta) ?? 0) - } + const values = anthropicBetaValues(source) + if (values.length > 0) finalValues = values + } + for (const value of finalValues) { + for (const beta of value.split(/[\s,]+/)) { + context = Math.max(context, CONTEXT_WINDOW_BETAS.get(beta) ?? 0) } } return context @@ -229,8 +312,8 @@ export function estimateInputTokens(input: { const system = input.system.join("\n") let total = estimateTextTokens(system) - const messages = serializeForEstimate(input.messages, messageMediaContainers(input.messages)) - total += estimateTextTokens(messages.text) + messages.mediaParts * MEDIA_TOKEN_ALLOWANCE + const messages = serializeForEstimate(input.messages, messageMediaAllowances(input.messages)) + total += estimateTextTokens(messages.text) + messages.mediaTokens if (input.tools !== undefined) { const tools = serializeForEstimate(input.tools) @@ -239,7 +322,7 @@ export function estimateInputTokens(input: { if (input.instructions !== undefined && input.instructions !== system) { const serialized = serializeForEstimate(input.instructions) - total += estimateTextTokens(serialized.text) + serialized.mediaParts * MEDIA_TOKEN_ALLOWANCE + total += estimateTextTokens(serialized.text) + serialized.mediaTokens } return total } @@ -283,10 +366,6 @@ export function clampOutputTokens(input: { const floor = Math.min(OUTPUT_TOKEN_FLOOR, requested) const clamped = Math.floor(context - inputTokens - margin) if (clamped < floor) { - // If only the estimator margin causes the failure, preserve a reservation that still fits - // the declared window. The provider remains authoritative for its exact tokenizer. - const withoutMargin = Math.floor(context - inputTokens) - if (withoutMargin >= floor) return Math.min(requested, withoutMargin) throw new OutputTokenBudgetError({ modelID: input.model.id, providerID: input.model.providerID, diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 4c02030db7..4c6c8a42af 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -4766,7 +4766,10 @@ describe("output token budget", () => { test("never demands more headroom than the model's own output reservation", () => { const model = createWindowModel({ context: 8_192, output: 512 }) - expect(clampOutputTokens({ model, requested: 512, inputTokens: 7_169 })).toBe(512) + expect(clampOutputTokens({ model, requested: 512, inputTokens: 7_168 })).toBe(512) + // One token more would require discarding the estimator margin. Refuse instead of sending an + // exact-fill request that is likely to reproduce the provider context error. + expect(() => clampOutputTokens({ model, requested: 512, inputTokens: 7_169 })).toThrow(OutputTokenBudgetError) expect(() => clampOutputTokens({ model, requested: 512, inputTokens: 8_000 })).toThrow(OutputTokenBudgetError) }) @@ -4900,7 +4903,7 @@ describe("output token budget", () => { expect(estimated).toBeLessThan(base + 3_000) }) - test("keeps binary image and PDF tool-result payloads on the fixed media allowance", () => { + test("keeps binary images bounded while scaling PDF estimates with document size", () => { const binaryImage = estimateInputTokens({ system: [], messages: [ @@ -4929,10 +4932,28 @@ describe("output token budget", () => { }, ], }) - for (const estimated of [binaryImage, pdfToolResult]) { - expect(estimated).toBeGreaterThan(2_000) - expect(estimated).toBeLessThan(10_000) - } + expect(binaryImage).toBeGreaterThan(2_000) + expect(binaryImage).toBeLessThan(10_000) + expect(pdfToolResult).toBeGreaterThan(100_000) + }) + + test("charges PDFs by page count when the document page tree is available", () => { + const pdf = [ + "%PDF-1.7", + "1 0 obj << /Type /Pages /Count 12 /Kids [] >> endobj", + "2 0 obj << /Type /Page /Parent 1 0 R >> endobj", + "%%EOF", + ].join("\n") + const estimated = estimateInputTokens({ + system: [], + messages: [ + { + role: "user", + content: [{ type: "file", mediaType: "application/pdf", data: Buffer.from(pdf).toString("base64") }], + }, + ], + }) + expect(estimated).toBeGreaterThanOrEqual(60_000) }) test("counts every AI SDK v6 tool-result media variant once per transport occurrence", () => { @@ -4964,8 +4985,12 @@ describe("output token budget", () => { }, ], }) - expect(estimated).toBeGreaterThan(2_000) - expect(estimated).toBeLessThan(10_000) + if (variant.type.startsWith("image")) { + expect(estimated).toBeGreaterThan(2_000) + expect(estimated).toBeLessThan(10_000) + } else { + expect(estimated).toBeGreaterThan(16_000) + } } const shared = { type: "image-data" as const, mediaType: "image/png", data: "AQ==" } @@ -5086,6 +5111,28 @@ describe("output token budget", () => { ).toBe(200_000) }) + test("uses the final outgoing Anthropic beta header value", () => { + const model = createWindowModel({ context: 200_000, output: 16_384 }) + expect( + effectiveContextWindow({ + model, + headerSources: [ + { "anthropic-beta": "context-1m-2025-08-07" }, + { "Anthropic-Beta": "interleaved-thinking-2025-05-14" }, + ], + }), + ).toBe(200_000) + expect( + effectiveContextWindow({ + model, + headerSources: [ + { "anthropic-beta": "interleaved-thinking-2025-05-14" }, + { aiGatewayHeaders: { "anthropic-beta": "context-1m-2025-08-07" } }, + ], + }), + ).toBe(1_000_000) + }) + test("clamps fixed reasoning budgets with the output reservation without mutating inputs", () => { const options = { thinking: { type: "enabled", budgetTokens: 16_000 }, From e37b7a974d03ecd68d642f93911778e217ab502d Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Sun, 30 Aug 2026 02:18:35 -0700 Subject: [PATCH 11/32] fix: bound document budget estimation --- .../src/provider/output-token-budget.ts | 125 ++++++++++++---- packages/opencode/src/provider/provider.ts | 13 +- packages/opencode/src/session/llm.ts | 42 +++--- .../src/session/llm/native-request.ts | 10 +- .../src/session/llm/native-runtime.ts | 15 +- packages/opencode/src/session/llm/request.ts | 42 +++--- .../opencode/test/provider/transform.test.ts | 141 +++++++++++++++++- .../opencode/test/session/llm-native.test.ts | 20 +++ packages/opencode/test/session/llm.test.ts | 8 +- 9 files changed, 323 insertions(+), 93 deletions(-) diff --git a/packages/opencode/src/provider/output-token-budget.ts b/packages/opencode/src/provider/output-token-budget.ts index 5116cadbca..6e21d64ce2 100644 --- a/packages/opencode/src/provider/output-token-budget.ts +++ b/packages/opencode/src/provider/output-token-budget.ts @@ -15,7 +15,8 @@ const MEDIA_TOKEN_ALLOWANCE = 2_048 const FILE_TOKEN_ALLOWANCE = 16_384 const PDF_TOKEN_ALLOWANCE = 32_768 const PDF_TOKENS_PER_PAGE = 5_000 -const PDF_FALLBACK_BYTES_PER_TOKEN = 4 +const PDF_SCAN_BYTE_LIMIT = 64 * 1_024 +const DATA_URL_HEADER_LIMIT = 1_024 const PDF_PAGE_LIMIT = 600 const MIN_REASONING_BUDGET = 1_024 const EMOJI = /\p{Extended_Pictographic}/u @@ -118,6 +119,31 @@ function isRecord(value: unknown): value is JsonRecord { return typeof value === "object" && value !== null && !Array.isArray(value) } +/** Merge outgoing request headers with HTTP's case-insensitive last-source precedence. */ +export function mergeRequestHeaders(...sources: readonly unknown[]): Record { + const result: Record = {} + const set = (name: unknown, value: unknown) => { + if (typeof name !== "string" || typeof value !== "string") return + result[name.toLowerCase()] = value + } + for (const source of sources) { + if (!source) continue + if (source instanceof Headers) { + source.forEach((value, name) => set(name, value)) + continue + } + if (Array.isArray(source)) { + for (const entry of source) { + if (Array.isArray(entry)) set(entry[0], entry[1]) + } + continue + } + if (!isRecord(source)) continue + for (const [name, value] of Object.entries(source)) set(name, value) + } + return result +} + /** Estimate heterogeneous text in small chunks and conservatively count non-ASCII scripts. */ function estimateTextTokens(input: string): number { let total = 0 @@ -155,53 +181,88 @@ function mediaType(part: JsonRecord, payload: unknown): string | undefined { if (declared) return declared.split(";", 1)[0].trim().toLowerCase() const value = payload instanceof URL ? payload.href : typeof payload === "string" ? payload : undefined - const dataType = value?.match(/^data:([^;,]+)/i)?.[1] + const prefix = value?.slice(0, DATA_URL_HEADER_LIMIT) + const dataType = prefix?.match(/^data:([^;,]+)/i)?.[1] if (dataType) return dataType.toLowerCase() - if (value && /\.pdf(?:[?#]|$)/i.test(value)) return "application/pdf" + if (value && /\.pdf(?:[?#]|$)/i.test(value.slice(-DATA_URL_HEADER_LIMIT))) return "application/pdf" if (String(part.type).startsWith("image")) return "image/*" return undefined } -/** Decode inline PDF bytes for page-tree inspection; remote URLs and file IDs stay unknown. */ -function inlinePdfBytes(payload: unknown): Uint8Array | undefined { +/** Return an inline payload's decoded byte size without allocating its encoded contents. */ +function inlinePayloadSize(payload: unknown): number | undefined { + if (ArrayBuffer.isView(payload)) return payload.byteLength + if (payload instanceof ArrayBuffer) return payload.byteLength + const value = payload instanceof URL ? payload.href : typeof payload === "string" ? payload : undefined + if (!value || /^https?:/i.test(value)) return undefined + + const dataURL = /^data:/i.test(value) + const prefix = dataURL ? value.slice(0, DATA_URL_HEADER_LIMIT) : "" + const comma = dataURL ? prefix.indexOf(",") : -1 + // A delimiter outside the bounded header prefix is malformed for admission purposes. Charge + // its complete encoded length without scanning or copying the attacker-controlled payload. + if (dataURL && comma === -1) return value.length + const bodyOffset = comma === -1 ? 0 : comma + 1 + const bodyLength = value.length - bodyOffset + if (comma !== -1 && !/;base64(?:;|$)/i.test(prefix.slice(0, comma))) return bodyLength + + const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0 + return Math.max(0, Math.floor((bodyLength * 3) / 4) - padding) +} + +/** Decode at most one bounded prefix for optional PDF page-tree evidence. */ +function inlinePdfPrefix(payload: unknown): string | undefined { if (ArrayBuffer.isView(payload)) { - return new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength) + const length = Math.min(payload.byteLength, PDF_SCAN_BYTE_LIMIT) + return Buffer.from(payload.buffer, payload.byteOffset, length).toString("latin1") + } + if (payload instanceof ArrayBuffer) { + return Buffer.from(payload, 0, Math.min(payload.byteLength, PDF_SCAN_BYTE_LIMIT)).toString("latin1") } - if (payload instanceof ArrayBuffer) return new Uint8Array(payload) const value = payload instanceof URL ? payload.href : typeof payload === "string" ? payload : undefined if (!value || /^https?:/i.test(value)) return undefined + const dataURL = /^data:/i.test(value) + const prefix = dataURL ? value.slice(0, DATA_URL_HEADER_LIMIT) : "" + const comma = dataURL ? prefix.indexOf(",") : -1 + if (dataURL && comma === -1) return undefined + const bodyOffset = comma === -1 ? 0 : comma + 1 + if (comma !== -1 && !/;base64(?:;|$)/i.test(prefix.slice(0, comma))) { + return value.slice(bodyOffset, bodyOffset + PDF_SCAN_BYTE_LIMIT) + } + try { - if (/^data:/i.test(value)) { - const comma = value.indexOf(",") - if (comma === -1) return undefined - const header = value.slice(0, comma) - const body = value.slice(comma + 1) - return /;base64(?:;|$)/i.test(header) - ? Buffer.from(body, "base64") - : Buffer.from(decodeURIComponent(body), "latin1") - } - return Buffer.from(value, "base64") + const encodedLimit = Math.ceil(PDF_SCAN_BYTE_LIMIT / 3) * 4 + return Buffer.from(value.slice(bodyOffset, bodyOffset + encodedLimit), "base64") + .subarray(0, PDF_SCAN_BYTE_LIMIT) + .toString("latin1") } catch { return undefined } } -/** Estimate PDF pages from the standard page tree, including conservative object-stream fallback. */ -function pdfTokenAllowance(payload: unknown): number { - const bytes = inlinePdfBytes(payload) - if (!bytes) return PDF_TOKEN_ALLOWANCE - - const source = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("latin1") - const leafPages = source.match(/\/Type\s*\/Page\b/g)?.length ?? 0 - let pages = leafPages - for (const match of source.matchAll(/\/Count\s+(\d+)/g)) { - pages = Math.max(pages, Number(match[1])) +/** Estimate page count only from the bounded PDF prefix. */ +function pdfPageCount(source: string | undefined): number { + if (!source) return 0 + let pages = 0 + const leaf = /\/Type\s*\/Page\b/g + while (leaf.exec(source) && pages < PDF_PAGE_LIMIT) pages++ + + const trees = [/\/Type\s*\/Pages\b[^>]{0,512}?\/Count\s+(\d+)/g, /\/Count\s+(\d+)[^>]{0,512}?\/Type\s*\/Pages\b/g] + for (const tree of trees) { + for (let match = tree.exec(source); match; match = tree.exec(source)) { + pages = Math.max(pages, Number(match[1])) + } } - pages = Math.min(pages, PDF_PAGE_LIMIT) - if (pages > 0) return Math.max(MEDIA_TOKEN_ALLOWANCE, pages * PDF_TOKENS_PER_PAGE) + return Math.min(pages, PDF_PAGE_LIMIT) +} - return Math.max(PDF_TOKEN_ALLOWANCE, Math.ceil(bytes.byteLength / PDF_FALLBACK_BYTES_PER_TOKEN)) +/** Combine fixed, monotonic byte-size, and bounded page evidence for inline PDFs. */ +function pdfTokenAllowance(payload: unknown): number { + const bytes = inlinePayloadSize(payload) ?? 0 + const pages = pdfPageCount(inlinePdfPrefix(payload)) + // One decoded byte per token is deliberately conservative for compressed and multilingual files. + return Math.max(PDF_TOKEN_ALLOWANCE, bytes, pages * PDF_TOKENS_PER_PAGE) } /** Assign an allowance that matches the semantic media kind rather than its encoded bytes. */ @@ -210,7 +271,9 @@ function mediaTokenAllowance(part: JsonRecord): number { const mime = mediaType(part, payload) if (mime?.startsWith("image/") || String(part.type).startsWith("image")) return MEDIA_TOKEN_ALLOWANCE if (mime === "application/pdf") return pdfTokenAllowance(payload) - if (FILE_PART_TYPES.has(String(part.type))) return FILE_TOKEN_ALLOWANCE + if (FILE_PART_TYPES.has(String(part.type))) { + return Math.max(FILE_TOKEN_ALLOWANCE, inlinePayloadSize(payload) ?? 0) + } return MEDIA_TOKEN_ALLOWANCE } diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 5850bb82f3..a956a38559 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -58,6 +58,9 @@ import { createGitLab, VERSION as GITLAB_PROVIDER_VERSION } from "gitlab-ai-prov import { fromNodeProviderChain } from "@aws-sdk/credential-providers" import { GoogleAuth } from "google-auth-library" import { ProviderTransform } from "./transform" +// altimate_change start — make provider/model headers obey HTTP's case-insensitive precedence +import { mergeRequestHeaders } from "./output-token-budget" +// altimate_change end // altimate_change start — provider fetch timeout errors use typed ProviderError classes import { ProviderError } from "./error" // altimate_change end @@ -1817,11 +1820,11 @@ export namespace Provider { if (baseURL !== undefined) options["baseURL"] = baseURL if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key - if (model.headers) - options["headers"] = { - ...options["headers"], - ...model.headers, - } + // altimate_change start — canonical names ensure later per-request headers replace provider defaults + if (options["headers"] !== undefined || model.headers) { + options["headers"] = mergeRequestHeaders(options["headers"], model.headers) + } + // altimate_change end const key = Hash.fast(JSON.stringify({ providerID: model.providerID, npm: model.api.npm, options })) const existing = s.sdk.get(key) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 6b47b98761..c1f59c3e8c 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -19,6 +19,7 @@ import { clampReasoningBudget, effectiveContextWindow, estimateInputTokens, + mergeRequestHeaders, } from "@/provider/output-token-budget" // altimate_change end // altimate_change start — tool retrieval @@ -174,6 +175,25 @@ export namespace LLM { }, ) + const requestHeaders = mergeRequestHeaders( + input.model.providerID.startsWith("opencode") + ? { + "x-opencode-project": Instance.project.id, + "x-opencode-session": input.sessionID, + "x-opencode-request": input.user.id, + "x-opencode-client": Flag.OPENCODE_CLIENT, + } + : input.model.providerID !== "anthropic" + ? { + // altimate_change start — upstream_fix: UA brand + "User-Agent": `altimate-code/${Installation.VERSION}`, + // altimate_change end + } + : undefined, + input.model.headers, + headers, + ) + const tools = await resolveTools(input) // altimate_change start — ensure tool definitions exist for all tool_use blocks in history @@ -231,7 +251,8 @@ export namespace LLM { requested: params.maxOutputTokens, context: effectiveContextWindow({ model: input.model, - headerSources: [input.model.headers, headers, provider.options], + // Provider defaults are lower precedence than the exact case-normalized outgoing record. + headerSources: [provider.options, requestHeaders], }), inputTokens: () => estimateInputTokens({ @@ -288,24 +309,7 @@ export namespace LLM { maxOutputTokens, // altimate_change end abortSignal: input.abort, - headers: { - ...(input.model.providerID.startsWith("opencode") - ? { - "x-opencode-project": Instance.project.id, - "x-opencode-session": input.sessionID, - "x-opencode-request": input.user.id, - "x-opencode-client": Flag.OPENCODE_CLIENT, - } - : input.model.providerID !== "anthropic" - ? { - // altimate_change start — upstream_fix: UA brand - "User-Agent": `altimate-code/${Installation.VERSION}`, - // altimate_change end - } - : undefined), - ...input.model.headers, - ...headers, - }, + headers: requestHeaders, maxRetries: input.retries ?? 0, messages: [ ...system.map( diff --git a/packages/opencode/src/session/llm/native-request.ts b/packages/opencode/src/session/llm/native-request.ts index b7f30e24c3..7e649ded89 100644 --- a/packages/opencode/src/session/llm/native-request.ts +++ b/packages/opencode/src/session/llm/native-request.ts @@ -11,6 +11,9 @@ import { } from "@opencode-ai/llm/providers" import type { ModelMessage } from "ai" import type { Provider } from "@/provider/provider" +// altimate_change start — preserve canonical header precedence through the native route adapter +import { mergeRequestHeaders } from "@/provider/output-token-budget" +// altimate_change end import { isRecord } from "@/util/record" type ToolInput = { @@ -153,10 +156,15 @@ const requireBaseURL = (model: Provider.Model, url: string | undefined) => { export const model = (input: Provider.Model | RequestInput, headers?: Record) => { const model = "model" in input ? input.model : input const url = baseURL(input) + // altimate_change start — avoid recreating differently-cased duplicates after request canonicalization + const requestHeaders = mergeRequestHeaders(model.headers, headers) + // altimate_change end const options = { ...("model" in input && input.apiKey ? { apiKey: input.apiKey } : {}), ...(url ? { baseURL: url } : {}), - headers: Object.keys({ ...model.headers, ...headers }).length === 0 ? undefined : { ...model.headers, ...headers }, + // altimate_change start — the later request value wins case-insensitively at the final native boundary + headers: Object.keys(requestHeaders).length === 0 ? undefined : requestHeaders, + // altimate_change end limits: { context: model.limit.context, output: model.limit.output, diff --git a/packages/opencode/src/session/llm/native-runtime.ts b/packages/opencode/src/session/llm/native-runtime.ts index bac385c591..f67c73cb6f 100644 --- a/packages/opencode/src/session/llm/native-runtime.ts +++ b/packages/opencode/src/session/llm/native-runtime.ts @@ -1,8 +1,10 @@ import type { Auth } from "@/auth" import type { Provider } from "@/provider/provider" import { ProviderTransform } from "@/provider/transform" +// altimate_change start — share case-insensitive provider-to-request header precedence +import { mergeRequestHeaders } from "@/provider/output-token-budget" +// altimate_change end import { errorMessage } from "@/util/error" -import { isRecord } from "@/util/record" import { asSchema, type ModelMessage, type Tool } from "ai" import { Cause, Effect, FiberSet, Queue } from "effect" import * as Stream from "effect/Stream" @@ -98,7 +100,9 @@ export function stream(input: StreamInput): StreamResult { topK: input.topK, maxOutputTokens: input.maxOutputTokens, providerOptions: ProviderTransform.providerOptions(input.model, input.providerOptions ?? {}), - headers: { ...providerHeaders(input.provider.options.headers), ...input.headers }, + // altimate_change start — canonical names keep request headers authoritative regardless of casing + headers: mergeRequestHeaders(input.provider.options.headers, input.headers), + // altimate_change end }) const stream = Stream.scoped( Stream.unwrap( @@ -152,13 +156,6 @@ function providerFetch(input: Pick): typeof gl return value as typeof globalThis.fetch } -function providerHeaders(value: unknown): Record | undefined { - if (!isRecord(value)) return undefined - return Object.fromEntries( - Object.entries(value).filter((entry): entry is [string, string] => typeof entry[1] === "string"), - ) -} - function nativeSchema(value: unknown): JsonSchema { if (!value || typeof value !== "object") return { type: "object", properties: {} } if ("jsonSchema" in value && value.jsonSchema && typeof value.jsonSchema === "object") diff --git a/packages/opencode/src/session/llm/request.ts b/packages/opencode/src/session/llm/request.ts index 8d8c6be3fe..f8d4c40680 100644 --- a/packages/opencode/src/session/llm/request.ts +++ b/packages/opencode/src/session/llm/request.ts @@ -14,6 +14,7 @@ import { clampReasoningBudget, effectiveContextWindow, estimateInputTokens, + mergeRequestHeaders, } from "@/provider/output-token-budget" // altimate_change end import { SystemPrompt } from "../system" @@ -176,6 +177,25 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre ? (yield* InstanceState.context).project.id : undefined + const requestHeaders = mergeRequestHeaders( + input.model.providerID.startsWith("opencode") + ? { + ...(opencodeProjectID ? { "x-opencode-project": opencodeProjectID } : {}), + "x-opencode-session": input.sessionID, + "x-opencode-request": input.user.id, + "x-opencode-client": input.flags.client, + "User-Agent": USER_AGENT, + } + : { + "x-session-affinity": input.sessionID, + "X-Session-Id": input.sessionID, + ...(input.parentSessionID ? { "x-parent-session-id": input.parentSessionID } : {}), + "User-Agent": USER_AGENT, + }, + input.model.headers, + headers, + ) + // altimate_change start — clamp after tools, headers, and plugin options are finalized. const sortedTools = Object.fromEntries(Object.entries(tools).toSorted(([a], [b]) => a.localeCompare(b))) const maxOutputTokens = clampOutputTokens({ @@ -183,7 +203,8 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre requested: params.maxOutputTokens, context: effectiveContextWindow({ model: input.model, - headerSources: [input.model.headers, headers, input.provider.options], + // Provider defaults are lower precedence than the exact case-normalized outgoing record. + headerSources: [input.provider.options, requestHeaders], }), inputTokens: () => estimateInputTokens({ @@ -209,24 +230,7 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre params: clampedParams, messageTransformOptions: requestOptions, // altimate_change end - headers: { - ...(input.model.providerID.startsWith("opencode") - ? { - ...(opencodeProjectID ? { "x-opencode-project": opencodeProjectID } : {}), - "x-opencode-session": input.sessionID, - "x-opencode-request": input.user.id, - "x-opencode-client": input.flags.client, - "User-Agent": USER_AGENT, - } - : { - "x-session-affinity": input.sessionID, - "X-Session-Id": input.sessionID, - ...(input.parentSessionID ? { "x-parent-session-id": input.parentSessionID } : {}), - "User-Agent": USER_AGENT, - }), - ...input.model.headers, - ...headers, - }, + headers: requestHeaders, } }) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 4c6c8a42af..6a79df8aa5 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -4956,6 +4956,100 @@ describe("output token budget", () => { expect(estimated).toBeGreaterThanOrEqual(60_000) }) + test("never lets a page marker suppress the PDF byte-size floor", () => { + const pdf = ["%PDF-1.7", "1 0 obj << /Type /Page >> endobj", "x".repeat(200_000), "%%EOF"].join("\n") + const estimated = estimateInputTokens({ + system: [], + messages: [ + { + role: "user", + content: [{ type: "file", mediaType: "application/pdf", data: Buffer.from(pdf).toString("base64") }], + }, + ], + }) + expect(estimated).toBeGreaterThan(200_000) + }) + + test("bounds PDF page-tree scanning to a fixed prefix", () => { + const pdf = ["%PDF-1.7", "x".repeat(70 * 1_024), "<< /Type /Pages /Count 600 >>", "%%EOF"].join("\n") + const estimated = estimateInputTokens({ + system: [], + messages: [ + { + role: "user", + content: [{ type: "file", mediaType: "application/pdf", data: Buffer.from(pdf).toString("base64") }], + }, + ], + }) + // The late page tree is outside the 64 KiB inspection prefix. Size still participates, but + // the estimator does not scan the full payload and inflate this to 600 * 5,000 tokens. + expect(estimated).toBeGreaterThan(70_000) + expect(estimated).toBeLessThan(100_000) + }) + + test("bounds malformed data-URL header inspection and keeps its size floor", () => { + const lateDelimiter = [ + "data:application/pdf", + "x".repeat(70 * 1_024), + ",%PDF-1.7 << /Type /Pages /Count 600 >> %%EOF", + ].join("") + const lateEstimated = estimateInputTokens({ + system: [], + messages: [ + { + role: "user", + content: [{ type: "file", mediaType: "application/pdf", data: lateDelimiter }], + }, + ], + }) + // The comma and page tree are outside the bounded data-URL header prefix. The full string + // still establishes a conservative size floor, but the late page marker is never scanned. + expect(lateEstimated).toBeGreaterThan(70_000) + expect(lateEstimated).toBeLessThan(100_000) + + const missingDelimiter = `data:application/pdf${"A".repeat(100_000)}` + const missingEstimated = estimateInputTokens({ + system: [], + messages: [ + { + role: "user", + content: [{ type: "file", mediaType: "application/pdf", data: missingDelimiter }], + }, + ], + }) + expect(missingEstimated).toBeGreaterThan(100_000) + }) + + test("scales inline non-PDF files with decoded payload size", () => { + const text = "漢".repeat(50_000) + const estimated = estimateInputTokens({ + system: [], + messages: [ + { + role: "user", + content: [{ type: "file", mediaType: "text/plain", data: Buffer.from(text, "utf8").toString("base64") }], + }, + ], + }) + expect(estimated).toBeGreaterThanOrEqual(150_000) + }) + + test("uses a conservative fixed fallback for remote PDFs", () => { + const estimated = estimateInputTokens({ + system: [], + messages: [ + { + role: "user", + content: [ + { type: "file", mediaType: "application/pdf", data: new URL("https://example.invalid/report.pdf") }, + ], + }, + ], + }) + expect(estimated).toBeGreaterThan(32_000) + expect(estimated).toBeLessThan(40_000) + }) + test("counts every AI SDK v6 tool-result media variant once per transport occurrence", () => { const payload = "A".repeat(1_048_576) const variants = [ @@ -5208,6 +5302,9 @@ describe("LLMRequestPrep.prepare - output token reservation", () => { readonly agentOptions?: Record readonly outputTokenMax?: number readonly messages?: ModelMessage[] + readonly providerOptions?: Record + readonly modelHeaders?: Record + readonly chatHeaders?: Record } = {}, ) => Effect.runPromise( @@ -5221,7 +5318,7 @@ describe("LLMRequestPrep.prepare - output token reservation", () => { model: { providerID: "openai-compatible", modelID: "large-window-model" }, } as any, sessionID, - model, + model: { ...model, headers: overrides.modelHeaders ?? model.headers }, agent: { name: "test", mode: "primary", @@ -5232,15 +5329,21 @@ describe("LLMRequestPrep.prepare - output token reservation", () => { system: [], messages: overrides.messages ?? messages, tools: overrides.tools ?? {}, - provider: { id: "openai-compatible", options: {} } as any, + provider: { id: "openai-compatible", options: overrides.providerOptions ?? {} } as any, auth: undefined, plugin: { - trigger: (name: string, _input: unknown, output: unknown) => - Effect.succeed( - name === "chat.params" && overrides.outputTokenMax !== undefined - ? { ...(output as Record), maxOutputTokens: overrides.outputTokenMax } - : output, - ), + trigger: (name: string, _input: unknown, output: unknown) => { + if (name === "chat.params" && overrides.outputTokenMax !== undefined) { + return Effect.succeed({ + ...(output as Record), + maxOutputTokens: overrides.outputTokenMax, + }) + } + if (name === "chat.headers" && overrides.chatHeaders) { + return Effect.succeed({ headers: overrides.chatHeaders }) + } + return Effect.succeed(output) + }, list: () => Effect.succeed([]), init: () => Effect.void, } as any, @@ -5273,6 +5376,28 @@ describe("LLMRequestPrep.prepare - output token reservation", () => { expect(result.params.maxOutputTokens).toBe(16_384) }) + test("uses provider then model then chat header precedence at the request boundary", async () => { + const beta = "context-1m-2025-08-07" + const disabled = "interleaved-thinking-2025-05-14" + const clamped = await run(largePrompt, { + providerOptions: { headers: { "Anthropic-Beta": beta } }, + modelHeaders: { "Anthropic-Beta": beta }, + chatHeaders: { "anthropic-beta": disabled }, + }) + expect(clamped.params.maxOutputTokens).toBeLessThan(16_384) + expect(new Headers(clamped.headers).get("anthropic-beta")).toBe(disabled) + expect(Object.keys(clamped.headers).filter((key) => key.toLowerCase() === "anthropic-beta")).toHaveLength(1) + + const widened = await run(largePrompt, { + providerOptions: { headers: { "Anthropic-Beta": disabled } }, + modelHeaders: { "Anthropic-Beta": disabled }, + chatHeaders: { "anthropic-beta": beta }, + }) + expect(widened.params.maxOutputTokens).toBe(16_384) + expect(new Headers(widened.headers).get("anthropic-beta")).toBe(beta) + expect(Object.keys(widened.headers).filter((key) => key.toLowerCase() === "anthropic-beta")).toHaveLength(1) + }) + test("unsupported media is normalized before the request-builder estimate", async () => { const result = await run("You are a helpful assistant.", { messages: [ diff --git a/packages/opencode/test/session/llm-native.test.ts b/packages/opencode/test/session/llm-native.test.ts index 54fd097ec1..17ee289a42 100644 --- a/packages/opencode/test/session/llm-native.test.ts +++ b/packages/opencode/test/session/llm-native.test.ts @@ -344,6 +344,26 @@ describe("session.llm-native.request", () => { expect(anthropic.route.id).toBe("anthropic-messages") expect(anthropic.route.endpoint.baseURL).toBe("https://api.anthropic.com/v1") + const disabledBeta = "interleaved-thinking-2025-05-14" + const anthropicWithCanonicalHeaders = LLMNative.model( + { + model: { + ...baseModel, + api: { ...baseModel.api, url: "", npm: "@ai-sdk/anthropic" }, + headers: { "Anthropic-Beta": "context-1m-2025-08-07" }, + }, + apiKey: "test-key", + messages: [], + }, + { "anthropic-beta": disabledBeta }, + ) + expect(anthropicWithCanonicalHeaders.route.defaults.headers?.["anthropic-beta"]).toBe(disabledBeta) + expect( + Object.keys(anthropicWithCanonicalHeaders.route.defaults.headers ?? {}).filter( + (key) => key.toLowerCase() === "anthropic-beta", + ), + ).toHaveLength(1) + const google = LLMNative.model({ model: { ...baseModel, api: { ...baseModel.api, url: "", npm: "@ai-sdk/google" } }, apiKey: "test-key", diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index 74101d9bcb..fe95d43b97 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -697,7 +697,11 @@ describe("session.llm.stream", () => { enabled_providers: [providerID], provider: { [providerID]: { - options: { apiKey: "test-google-key", baseURL: `${server.url.origin}/v1beta` }, + options: { + apiKey: "test-google-key", + baseURL: `${server.url.origin}/v1beta`, + headers: { "Anthropic-Beta": "context-1m-2025-08-07" }, + }, }, }, }), @@ -711,6 +715,7 @@ describe("session.llm.stream", () => { const resolved = await Provider.getModel(ProviderID.make(providerID), ModelID.make(fixture.model.id)) const budgeted = { ...resolved, + headers: { "anthropic-beta": "interleaved-thinking-2025-05-14" }, limit: { ...resolved.limit, context: 65_536, output: 16_384 }, } const sessionID = SessionID.make("session-budget-stream") @@ -767,6 +772,7 @@ describe("session.llm.stream", () => { expect(maxOutputTokens!).toBeGreaterThanOrEqual(1_024) expect(config?.thinkingConfig?.thinkingBudget).toBe(maxOutputTokens! - 1_024) expect(JSON.stringify(capture.body.tools)).toContain(schemaMarker) + expect(capture.headers.get("anthropic-beta")).toBe("interleaved-thinking-2025-05-14") }, }) }, 30_000) From ac7346e767116afdd2fb24b76820d0563a944782 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Sun, 30 Aug 2026 10:09:59 -0700 Subject: [PATCH 12/32] fix: reject untrusted PDF metadata amplification --- .../src/provider/output-token-budget.ts | 59 ++------------- packages/opencode/src/session/llm.ts | 6 +- packages/opencode/src/session/llm/request.ts | 4 + .../opencode/test/provider/transform.test.ts | 73 ++++++++++--------- 4 files changed, 50 insertions(+), 92 deletions(-) diff --git a/packages/opencode/src/provider/output-token-budget.ts b/packages/opencode/src/provider/output-token-budget.ts index 6e21d64ce2..03b63a1595 100644 --- a/packages/opencode/src/provider/output-token-budget.ts +++ b/packages/opencode/src/provider/output-token-budget.ts @@ -14,10 +14,7 @@ const ESTIMATE_CHUNK_SIZE = 400 const MEDIA_TOKEN_ALLOWANCE = 2_048 const FILE_TOKEN_ALLOWANCE = 16_384 const PDF_TOKEN_ALLOWANCE = 32_768 -const PDF_TOKENS_PER_PAGE = 5_000 -const PDF_SCAN_BYTE_LIMIT = 64 * 1_024 const DATA_URL_HEADER_LIMIT = 1_024 -const PDF_PAGE_LIMIT = 600 const MIN_REASONING_BUDGET = 1_024 const EMOJI = /\p{Extended_Pictographic}/u const REASONING_BUDGET_KEYS = new Set(["budgetTokens", "thinkingBudget", "budget_tokens"]) @@ -210,59 +207,13 @@ function inlinePayloadSize(payload: unknown): number | undefined { return Math.max(0, Math.floor((bodyLength * 3) / 4) - padding) } -/** Decode at most one bounded prefix for optional PDF page-tree evidence. */ -function inlinePdfPrefix(payload: unknown): string | undefined { - if (ArrayBuffer.isView(payload)) { - const length = Math.min(payload.byteLength, PDF_SCAN_BYTE_LIMIT) - return Buffer.from(payload.buffer, payload.byteOffset, length).toString("latin1") - } - if (payload instanceof ArrayBuffer) { - return Buffer.from(payload, 0, Math.min(payload.byteLength, PDF_SCAN_BYTE_LIMIT)).toString("latin1") - } - - const value = payload instanceof URL ? payload.href : typeof payload === "string" ? payload : undefined - if (!value || /^https?:/i.test(value)) return undefined - const dataURL = /^data:/i.test(value) - const prefix = dataURL ? value.slice(0, DATA_URL_HEADER_LIMIT) : "" - const comma = dataURL ? prefix.indexOf(",") : -1 - if (dataURL && comma === -1) return undefined - const bodyOffset = comma === -1 ? 0 : comma + 1 - if (comma !== -1 && !/;base64(?:;|$)/i.test(prefix.slice(0, comma))) { - return value.slice(bodyOffset, bodyOffset + PDF_SCAN_BYTE_LIMIT) - } - - try { - const encodedLimit = Math.ceil(PDF_SCAN_BYTE_LIMIT / 3) * 4 - return Buffer.from(value.slice(bodyOffset, bodyOffset + encodedLimit), "base64") - .subarray(0, PDF_SCAN_BYTE_LIMIT) - .toString("latin1") - } catch { - return undefined - } -} - -/** Estimate page count only from the bounded PDF prefix. */ -function pdfPageCount(source: string | undefined): number { - if (!source) return 0 - let pages = 0 - const leaf = /\/Type\s*\/Page\b/g - while (leaf.exec(source) && pages < PDF_PAGE_LIMIT) pages++ - - const trees = [/\/Type\s*\/Pages\b[^>]{0,512}?\/Count\s+(\d+)/g, /\/Count\s+(\d+)[^>]{0,512}?\/Type\s*\/Pages\b/g] - for (const tree of trees) { - for (let match = tree.exec(source); match; match = tree.exec(source)) { - pages = Math.max(pages, Number(match[1])) - } - } - return Math.min(pages, PDF_PAGE_LIMIT) -} - -/** Combine fixed, monotonic byte-size, and bounded page evidence for inline PDFs. */ +/** Combine fixed and monotonic byte-size evidence without trusting unparsed PDF metadata. */ function pdfTokenAllowance(payload: unknown): number { const bytes = inlinePayloadSize(payload) ?? 0 - const pages = pdfPageCount(inlinePdfPrefix(payload)) - // One decoded byte per token is deliberately conservative for compressed and multilingual files. - return Math.max(PDF_TOKEN_ALLOWANCE, bytes, pages * PDF_TOKENS_PER_PAGE) + // Raw PDF bytes cannot authenticate page-tree metadata: comments, strings, streams, stale + // objects, and incremental revisions may all contain convincing but unreachable markers. + // One decoded byte per token plus the fixed floor stays conservative without amplification. + return Math.max(PDF_TOKEN_ALLOWANCE, bytes) } /** Assign an allowance that matches the semantic media kind rather than its encoded bytes. */ diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index c1f59c3e8c..6005c2f623 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -175,6 +175,7 @@ export namespace LLM { }, ) + // altimate_change start — canonicalize the exact outgoing header precedence before budgeting const requestHeaders = mergeRequestHeaders( input.model.providerID.startsWith("opencode") ? { @@ -185,14 +186,13 @@ export namespace LLM { } : input.model.providerID !== "anthropic" ? { - // altimate_change start — upstream_fix: UA brand "User-Agent": `altimate-code/${Installation.VERSION}`, - // altimate_change end } : undefined, input.model.headers, headers, ) + // altimate_change end const tools = await resolveTools(input) @@ -309,7 +309,9 @@ export namespace LLM { maxOutputTokens, // altimate_change end abortSignal: input.abort, + // altimate_change start — send the canonical headers used by the budget estimator headers: requestHeaders, + // altimate_change end maxRetries: input.retries ?? 0, messages: [ ...system.map( diff --git a/packages/opencode/src/session/llm/request.ts b/packages/opencode/src/session/llm/request.ts index f8d4c40680..edbd7befe8 100644 --- a/packages/opencode/src/session/llm/request.ts +++ b/packages/opencode/src/session/llm/request.ts @@ -177,6 +177,7 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre ? (yield* InstanceState.context).project.id : undefined + // altimate_change start — canonicalize the exact outgoing header precedence before budgeting const requestHeaders = mergeRequestHeaders( input.model.providerID.startsWith("opencode") ? { @@ -195,6 +196,7 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre input.model.headers, headers, ) + // altimate_change end // altimate_change start — clamp after tools, headers, and plugin options are finalized. const sortedTools = Object.fromEntries(Object.entries(tools).toSorted(([a], [b]) => a.localeCompare(b))) @@ -230,7 +232,9 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre params: clampedParams, messageTransformOptions: requestOptions, // altimate_change end + // altimate_change start — return the canonical headers used by the budget estimator headers: requestHeaders, + // altimate_change end } }) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 6a79df8aa5..f642b0c929 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -4937,23 +4937,41 @@ describe("output token budget", () => { expect(pdfToolResult).toBeGreaterThan(100_000) }) - test("charges PDFs by page count when the document page tree is available", () => { - const pdf = [ - "%PDF-1.7", - "1 0 obj << /Type /Pages /Count 12 /Kids [] >> endobj", - "2 0 obj << /Type /Page /Parent 1 0 R >> endobj", - "%%EOF", - ].join("\n") - const estimated = estimateInputTokens({ - system: [], - messages: [ - { - role: "user", - content: [{ type: "file", mediaType: "application/pdf", data: Buffer.from(pdf).toString("base64") }], - }, - ], - }) - expect(estimated).toBeGreaterThanOrEqual(60_000) + test("does not trust unparsed PDF metadata as page-count evidence", () => { + const marker = "/Type /Pages /Count 600" + const variants = [ + ["%PDF-1.4", `% ${marker}`, "1 0 obj << /Type /Pages /Count 1 >> endobj", "%%EOF"].join("\n"), + ["%PDF-1.4", `1 0 obj << /Note (${marker}) >> endobj`, "%%EOF"].join("\n"), + ["%PDF-1.4", "1 0 obj << /Length 24 >> stream", marker, "endstream endobj", "%%EOF"].join("\n"), + ["%PDF-1.4", `99 0 obj << ${marker} >> endobj`, "%%EOF"].join("\n"), + ] + const estimate = (data: string | Uint8Array | ArrayBuffer) => + estimateInputTokens({ + system: [], + messages: [{ role: "user", content: [{ type: "file", mediaType: "application/pdf", data }] }], + }) + + for (const pdf of variants) { + const bytes = new Uint8Array(Buffer.from(pdf, "latin1")) + const payloads = [ + Buffer.from(bytes).toString("base64"), + `data:application/pdf;base64,${Buffer.from(bytes).toString("base64")}`, + bytes, + bytes.buffer, + ] + for (const payload of payloads) { + const estimated = estimate(payload) + expect(estimated).toBeGreaterThan(32_000) + expect(estimated).toBeLessThan(40_000) + expect( + clampOutputTokens({ + model: createWindowModel({ context: 200_000, input: 180_000, output: 16_384 }), + requested: 16_384, + inputTokens: estimated, + }), + ).toBe(16_384) + } + } }) test("never lets a page marker suppress the PDF byte-size floor", () => { @@ -4970,23 +4988,6 @@ describe("output token budget", () => { expect(estimated).toBeGreaterThan(200_000) }) - test("bounds PDF page-tree scanning to a fixed prefix", () => { - const pdf = ["%PDF-1.7", "x".repeat(70 * 1_024), "<< /Type /Pages /Count 600 >>", "%%EOF"].join("\n") - const estimated = estimateInputTokens({ - system: [], - messages: [ - { - role: "user", - content: [{ type: "file", mediaType: "application/pdf", data: Buffer.from(pdf).toString("base64") }], - }, - ], - }) - // The late page tree is outside the 64 KiB inspection prefix. Size still participates, but - // the estimator does not scan the full payload and inflate this to 600 * 5,000 tokens. - expect(estimated).toBeGreaterThan(70_000) - expect(estimated).toBeLessThan(100_000) - }) - test("bounds malformed data-URL header inspection and keeps its size floor", () => { const lateDelimiter = [ "data:application/pdf", @@ -5002,8 +5003,8 @@ describe("output token budget", () => { }, ], }) - // The comma and page tree are outside the bounded data-URL header prefix. The full string - // still establishes a conservative size floor, but the late page marker is never scanned. + // The comma is outside the bounded data-URL header prefix. The full string still establishes + // a conservative size floor without inspecting attacker-controlled PDF metadata. expect(lateEstimated).toBeGreaterThan(70_000) expect(lateEstimated).toBeLessThan(100_000) From 858c7b1dabc492dd59e603d76f0ac76e2823f9da Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Sun, 30 Aug 2026 10:33:50 -0700 Subject: [PATCH 13/32] fix: parse PDF page counts safely --- bun.lock | 11 + packages/opencode/package.json | 1 + .../src/provider/output-token-budget.ts | 93 ++++++-- packages/opencode/src/session/llm.ts | 21 +- packages/opencode/src/session/llm/request.ts | 19 +- .../opencode/test/provider/transform.test.ts | 199 +++++++++++++----- 6 files changed, 255 insertions(+), 89 deletions(-) diff --git a/bun.lock b/bun.lock index 0a711c43de..3c43b48f16 100644 --- a/bun.lock +++ b/bun.lock @@ -337,6 +337,7 @@ "opencode-poe-auth": "0.0.1", "opentui-spinner": "catalog:", "partial-json": "0.1.7", + "pdf-lib": "1.17.1", "remeda": "catalog:", "semver": "^7.6.3", "solid-js": "catalog:", @@ -1293,6 +1294,10 @@ "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="], + "@pdf-lib/standard-fonts": ["@pdf-lib/standard-fonts@1.0.0", "", { "dependencies": { "pako": "^1.0.6" } }, "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA=="], + + "@pdf-lib/upng": ["@pdf-lib/upng@1.0.1", "", { "dependencies": { "pako": "^1.0.10" } }, "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ=="], + "@pierre/diffs": ["@pierre/diffs@1.2.10", "", { "dependencies": { "@pierre/theme": "1.0.3", "@pierre/theming": "0.0.1", "@shikijs/transformers": "^3.0.0 || ^4.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0 || ^4.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-rPeAmDWarxFVTQpaf4y6wTxjZxU44xKJKoJti2zU21P06DVd9nRHZX+xSIObLB307Qjpaesyb1x/j0z94t7vLw=="], "@pierre/theme": ["@pierre/theme@1.0.3", "", {}, "sha512-sWHv11TMoqKxKDgTIk5VbhQjdPhs8DCcBxbjh3mRlS3YOM/OcrWoGX6MM8eBGn9cUu3M46Py0JnxsG2nJaFTuA=="], @@ -2459,6 +2464,8 @@ "pad-left": ["pad-left@2.1.0", "", { "dependencies": { "repeat-string": "^1.5.4" } }, "sha512-HJxs9K9AztdIQIAIa/OIazRAUW/L6B9hbQDxO4X07roW3eo9XqZc2ur9bn1StH9CnbbI9EgvejHQX7CBpCF1QA=="], + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + "parse-conflict-json": ["parse-conflict-json@5.0.1", "", { "dependencies": { "json-parse-even-better-errors": "^5.0.0", "just-diff": "^6.0.0", "just-diff-apply": "^5.2.0" } }, "sha512-ZHEmNKMq1wyJXNwLxyHnluPfRAFSIliBvbK/UiOceROt4Xh9Pz0fq49NytIaeaCUf5VR86hwQ/34FCcNU5/LKQ=="], "parse-passwd": ["parse-passwd@1.0.0", "", {}, "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q=="], @@ -2487,6 +2494,8 @@ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "pdf-lib": ["pdf-lib@1.17.1", "", { "dependencies": { "@pdf-lib/standard-fonts": "^1.0.0", "@pdf-lib/upng": "^1.0.1", "pako": "^1.0.11", "tslib": "^1.11.1" } }, "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw=="], + "perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="], "pg": ["pg@8.22.0", "", { "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.15.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA=="], @@ -3347,6 +3356,8 @@ "patch-package/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + "pdf-lib/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + "proxy-addr/ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "proxy-agent/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 1714dafeaf..323949100a 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -143,6 +143,7 @@ "opencode-poe-auth": "0.0.1", "opentui-spinner": "catalog:", "partial-json": "0.1.7", + "pdf-lib": "1.17.1", "remeda": "catalog:", "semver": "^7.6.3", "solid-js": "catalog:", diff --git a/packages/opencode/src/provider/output-token-budget.ts b/packages/opencode/src/provider/output-token-budget.ts index 03b63a1595..5aeffb0f5b 100644 --- a/packages/opencode/src/provider/output-token-budget.ts +++ b/packages/opencode/src/provider/output-token-budget.ts @@ -14,6 +14,11 @@ const ESTIMATE_CHUNK_SIZE = 400 const MEDIA_TOKEN_ALLOWANCE = 2_048 const FILE_TOKEN_ALLOWANCE = 16_384 const PDF_TOKEN_ALLOWANCE = 32_768 +const PDF_TOKENS_PER_PAGE = 5_000 +const PDF_UNINSPECTABLE_TOKEN_ALLOWANCE = 100 * PDF_TOKENS_PER_PAGE +// Above this decoded size, the one-byte/one-token floor already exceeds the maximum +// documented 100-page allowance, so structural parsing cannot raise the estimate. +const PDF_PARSE_BYTE_LIMIT = PDF_UNINSPECTABLE_TOKEN_ALLOWANCE const DATA_URL_HEADER_LIMIT = 1_024 const MIN_REASONING_BUDGET = 1_024 const EMOJI = /\p{Extended_Pictographic}/u @@ -207,21 +212,73 @@ function inlinePayloadSize(payload: unknown): number | undefined { return Math.max(0, Math.floor((bodyLength * 3) / 4) - padding) } -/** Combine fixed and monotonic byte-size evidence without trusting unparsed PDF metadata. */ -function pdfTokenAllowance(payload: unknown): number { - const bytes = inlinePayloadSize(payload) ?? 0 - // Raw PDF bytes cannot authenticate page-tree metadata: comments, strings, streams, stale - // objects, and incremental revisions may all contain convincing but unreachable markers. - // One decoded byte per token plus the fixed floor stays conservative without amplification. - return Math.max(PDF_TOKEN_ALLOWANCE, bytes) +/** Decode a bounded inline payload for structural PDF parsing. */ +function inlinePayloadBytes(payload: unknown, size: number): Uint8Array | undefined { + if (size > PDF_PARSE_BYTE_LIMIT) return undefined + if (ArrayBuffer.isView(payload)) { + return new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength) + } + if (payload instanceof ArrayBuffer) return new Uint8Array(payload) + + const value = payload instanceof URL ? payload.href : typeof payload === "string" ? payload : undefined + if (!value || /^https?:/i.test(value)) return undefined + const dataURL = /^data:/i.test(value) + const prefix = dataURL ? value.slice(0, DATA_URL_HEADER_LIMIT) : "" + const comma = dataURL ? prefix.indexOf(",") : -1 + if (dataURL && comma === -1) return undefined + const bodyOffset = comma === -1 ? 0 : comma + 1 + const body = value.slice(bodyOffset) + + try { + if (comma !== -1 && !/;base64(?:;|$)/i.test(prefix.slice(0, comma))) { + return Buffer.from(decodeURIComponent(body), "latin1") + } + return Buffer.from(body, "base64") + } catch { + return undefined + } +} + +/** Combine byte size with a page count obtained from a real PDF object graph. */ +async function pdfTokenAllowance(payload: unknown): Promise { + const size = inlinePayloadSize(payload) + if (size === undefined) { + // Remote URLs and provider file IDs cannot be inspected locally. Reserve the documented + // 100-page request maximum at the same deliberately conservative per-page rate. + return PDF_UNINSPECTABLE_TOKEN_ALLOWANCE + } + + const baseline = Math.max(PDF_TOKEN_ALLOWANCE, size) + const bytes = inlinePayloadBytes(payload, size) + // Inputs above the parser cap already carry a decoded-byte allowance at least as large as the + // maximum page allowance. Smaller inputs that cannot be inspected use the remote/file-ID + // fallback rather than pretending their page expansion is known. + if (!bytes) return size > PDF_PARSE_BYTE_LIMIT ? baseline : Math.max(baseline, PDF_UNINSPECTABLE_TOKEN_ALLOWANCE) + + try { + const { PDFDocument, ParseSpeeds } = await import("pdf-lib") + const document = await PDFDocument.load(bytes, { + parseSpeed: ParseSpeeds.Fastest, + throwOnInvalidObject: true, + updateMetadata: false, + capNumbers: true, + }) + const pages = document.getPageCount() + if (!Number.isSafeInteger(pages) || pages <= 0) { + return Math.max(baseline, PDF_UNINSPECTABLE_TOKEN_ALLOWANCE) + } + return Math.max(baseline, pages * PDF_TOKENS_PER_PAGE) + } catch { + return Math.max(baseline, PDF_UNINSPECTABLE_TOKEN_ALLOWANCE) + } } /** Assign an allowance that matches the semantic media kind rather than its encoded bytes. */ -function mediaTokenAllowance(part: JsonRecord): number { +async function mediaTokenAllowance(part: JsonRecord): Promise { const payload = mediaPayload(part) const mime = mediaType(part, payload) if (mime?.startsWith("image/") || String(part.type).startsWith("image")) return MEDIA_TOKEN_ALLOWANCE - if (mime === "application/pdf") return pdfTokenAllowance(payload) + if (mime === "application/pdf") return await pdfTokenAllowance(payload) if (FILE_PART_TYPES.has(String(part.type))) { return Math.max(FILE_TOKEN_ALLOWANCE, inlinePayloadSize(payload) ?? 0) } @@ -229,25 +286,27 @@ function mediaTokenAllowance(part: JsonRecord): number { } /** Mark only actual ModelMessage content parts whose payload is provider media. */ -function messageMediaAllowances(messages: readonly unknown[]): WeakMap { +async function messageMediaAllowances(messages: readonly unknown[]): Promise> { const result = new WeakMap() const visited = new WeakSet() - const visitContent = (content: unknown) => { + const visitContent = async (content: unknown): Promise => { if (!Array.isArray(content) || visited.has(content)) return visited.add(content) for (const part of content) { if (!isRecord(part)) continue - if (MEDIA_PART_TYPES.has(String(part.type))) result.set(part, mediaTokenAllowance(part)) + if (MEDIA_PART_TYPES.has(String(part.type)) && !result.has(part)) { + result.set(part, await mediaTokenAllowance(part)) + } // Tool-result media is nested in the AI SDK's typed content output. if (part.type !== "tool-result" || !isRecord(part.output)) continue - if (part.output.type === "content") visitContent(part.output.value) + if (part.output.type === "content") await visitContent(part.output.value) } } for (const message of messages) { - if (isRecord(message)) visitContent(message.content) + if (isRecord(message)) await visitContent(message.content) } return result } @@ -317,16 +376,16 @@ export function effectiveContextWindow(input: { } /** Estimate the text, finalized tools, instructions, and media allowance sent in one request. */ -export function estimateInputTokens(input: { +export async function estimateInputTokens(input: { readonly system: readonly string[] readonly messages: readonly unknown[] readonly tools?: Readonly> readonly instructions?: unknown -}): number { +}): Promise { const system = input.system.join("\n") let total = estimateTextTokens(system) - const messages = serializeForEstimate(input.messages, messageMediaAllowances(input.messages)) + const messages = serializeForEstimate(input.messages, await messageMediaAllowances(input.messages)) total += estimateTextTokens(messages.text) + messages.mediaTokens if (input.tools !== undefined) { diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 6005c2f623..96ff9a33ac 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -243,9 +243,18 @@ export namespace LLM { // altimate_change start — clamp after every context-affecting request field is finalized. // Tool schemas and provider instructions consume the shared context window, while encoded - // media bytes do not count as literal text. The estimator runs lazily so providers that omit - // maxOutputTokens pay no serialization cost. Known context beta headers widen the catalog + // media bytes do not count as literal text. Providers that omit maxOutputTokens skip the + // estimator. Known context beta headers widen the catalog // limit before the clamp. Fixed reasoning budgets are reconciled with the final reservation. + const inputTokens = + params.maxOutputTokens === undefined + ? 0 + : await estimateInputTokens({ + system, + messages: ProviderTransform.messagesForInputEstimate(input.messages, input.model), + tools, + instructions: params.options.instructions, + }) const maxOutputTokens = clampOutputTokens({ model: input.model, requested: params.maxOutputTokens, @@ -254,13 +263,7 @@ export namespace LLM { // Provider defaults are lower precedence than the exact case-normalized outgoing record. headerSources: [provider.options, requestHeaders], }), - inputTokens: () => - estimateInputTokens({ - system, - messages: ProviderTransform.messagesForInputEstimate(input.messages, input.model), - tools, - instructions: params.options.instructions, - }), + inputTokens, }) const requestOptions = clampReasoningBudget(params.options, maxOutputTokens) // altimate_change end diff --git a/packages/opencode/src/session/llm/request.ts b/packages/opencode/src/session/llm/request.ts index edbd7befe8..dc97bc0b70 100644 --- a/packages/opencode/src/session/llm/request.ts +++ b/packages/opencode/src/session/llm/request.ts @@ -200,6 +200,17 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre // altimate_change start — clamp after tools, headers, and plugin options are finalized. const sortedTools = Object.fromEntries(Object.entries(tools).toSorted(([a], [b]) => a.localeCompare(b))) + const inputTokens = + params.maxOutputTokens === undefined + ? 0 + : yield* Effect.promise(() => + estimateInputTokens({ + system, + messages: ProviderTransform.messagesForInputEstimate(input.messages, input.model), + tools: sortedTools, + instructions: params.options.instructions, + }), + ) const maxOutputTokens = clampOutputTokens({ model: input.model, requested: params.maxOutputTokens, @@ -208,13 +219,7 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre // Provider defaults are lower precedence than the exact case-normalized outgoing record. headerSources: [input.provider.options, requestHeaders], }), - inputTokens: () => - estimateInputTokens({ - system, - messages: ProviderTransform.messagesForInputEstimate(input.messages, input.model), - tools: sortedTools, - instructions: params.options.instructions, - }), + inputTokens, }) const requestOptions = clampReasoningBudget(params.options, maxOutputTokens) const clampedParams = { diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index f642b0c929..5ef9ad1cb0 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import { Effect } from "effect" -import { jsonSchema, tool, type ModelMessage, type Tool } from "ai" +import { convertToModelMessages, jsonSchema, tool, type ModelMessage, type Tool } from "ai" import { ProviderTransform } from "@/provider/transform" import { clampOutputTokens, @@ -4803,9 +4803,9 @@ describe("output token budget", () => { expect(evaluated).toBeFalse() }) - test("counts tool schemas and provider instructions", () => { - const base = estimateInputTokens({ system: ["system"], messages: [{ role: "user", content: "hello" }] }) - const complete = estimateInputTokens({ + test("counts tool schemas and provider instructions", async () => { + const base = await estimateInputTokens({ system: ["system"], messages: [{ role: "user", content: "hello" }] }) + const complete = await estimateInputTokens({ system: ["system"], messages: [{ role: "user", content: "hello" }], instructions: "provider instruction ".repeat(400), @@ -4819,8 +4819,8 @@ describe("output token budget", () => { expect(complete).toBeGreaterThan(base + 1_000) }) - test("does not tokenize encoded media bytes as literal prompt text", () => { - const estimated = estimateInputTokens({ + test("does not tokenize encoded media bytes as literal prompt text", async () => { + const estimated = await estimateInputTokens({ system: [], messages: [ { @@ -4833,7 +4833,7 @@ describe("output token budget", () => { expect(estimated).toBeLessThan(10_000) }) - test("counts data-URL-shaped text in every textual request field", () => { + test("counts data-URL-shaped text in every textual request field", async () => { const prefixes = [ "data:image/png;base64,", "data:audio/wav;base64,", @@ -4843,7 +4843,7 @@ describe("output token budget", () => { for (const prefix of prefixes) { const text = prefix + "漢".repeat(70_000) expect( - estimateInputTokens({ + await estimateInputTokens({ system: [], messages: [{ role: "user", content: [{ type: "text", text }] }], }), @@ -4852,14 +4852,14 @@ describe("output token budget", () => { const text = prefixes[0] + "漢".repeat(70_000) const estimates = [ - estimateInputTokens({ system: [], messages: [{ role: "user", content: text }] }), - estimateInputTokens({ system: [], messages: [], instructions: text }), - estimateInputTokens({ + await estimateInputTokens({ system: [], messages: [{ role: "user", content: text }] }), + await estimateInputTokens({ system: [], messages: [], instructions: text }), + await estimateInputTokens({ system: [], messages: [], tools: { inspect: { description: text, inputSchema: { type: "object" } } }, }), - estimateInputTokens({ + await estimateInputTokens({ system: [], messages: [ { @@ -4870,7 +4870,7 @@ describe("output token budget", () => { }, ], }), - estimateInputTokens({ + await estimateInputTokens({ system: [], messages: [ { @@ -4888,9 +4888,9 @@ describe("output token budget", () => { ) }) - test("charges the same fixed allowance for URL-backed media", () => { - const base = estimateInputTokens({ system: [], messages: [{ role: "user", content: "show this" }] }) - const estimated = estimateInputTokens({ + test("charges the same fixed allowance for URL-backed media", async () => { + const base = await estimateInputTokens({ system: [], messages: [{ role: "user", content: "show this" }] }) + const estimated = await estimateInputTokens({ system: [], messages: [ { @@ -4903,8 +4903,8 @@ describe("output token budget", () => { expect(estimated).toBeLessThan(base + 3_000) }) - test("keeps binary images bounded while scaling PDF estimates with document size", () => { - const binaryImage = estimateInputTokens({ + test("keeps binary images bounded while scaling PDF estimates with document size", async () => { + const binaryImage = await estimateInputTokens({ system: [], messages: [ { @@ -4913,7 +4913,7 @@ describe("output token budget", () => { }, ], }) - const pdfToolResult = estimateInputTokens({ + const pdfToolResult = await estimateInputTokens({ system: [], messages: [ { @@ -4937,13 +4937,36 @@ describe("output token budget", () => { expect(pdfToolResult).toBeGreaterThan(100_000) }) - test("does not trust unparsed PDF metadata as page-count evidence", () => { + test("does not trust unparsed PDF metadata as page-count evidence", async () => { const marker = "/Type /Pages /Count 600" + const buildPdf = (input: { comment?: string; note?: string; stream?: string; unreachable?: string }) => { + const content = input.stream ?? "" + const objects = [ + `<< /Type /Catalog /Pages 2 0 R${input.note ? ` /Note (${input.note})` : ""} >>`, + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Contents 4 0 R >>", + `<< /Length ${Buffer.byteLength(content)} >>\nstream\n${content}\nendstream`, + ...(input.unreachable ? [`<< ${input.unreachable} >>`] : []), + ] + let source = `%PDF-1.4\n% ${input.comment ?? "control"}\n` + const offsets = [0] + for (const [index, object] of objects.entries()) { + offsets[index + 1] = Buffer.byteLength(source) + source += `${index + 1} 0 obj\n${object}\nendobj\n` + } + const xref = Buffer.byteLength(source) + source += `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n` + for (const offset of offsets.slice(1)) { + source += `${String(offset).padStart(10, "0")} 00000 n \n` + } + source += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n` + return source + } const variants = [ - ["%PDF-1.4", `% ${marker}`, "1 0 obj << /Type /Pages /Count 1 >> endobj", "%%EOF"].join("\n"), - ["%PDF-1.4", `1 0 obj << /Note (${marker}) >> endobj`, "%%EOF"].join("\n"), - ["%PDF-1.4", "1 0 obj << /Length 24 >> stream", marker, "endstream endobj", "%%EOF"].join("\n"), - ["%PDF-1.4", `99 0 obj << ${marker} >> endobj`, "%%EOF"].join("\n"), + buildPdf({ comment: marker }), + buildPdf({ note: marker }), + buildPdf({ stream: `% ${marker}` }), + buildPdf({ unreachable: marker }), ] const estimate = (data: string | Uint8Array | ArrayBuffer) => estimateInputTokens({ @@ -4960,7 +4983,7 @@ describe("output token budget", () => { bytes.buffer, ] for (const payload of payloads) { - const estimated = estimate(payload) + const estimated = await estimate(payload) expect(estimated).toBeGreaterThan(32_000) expect(estimated).toBeLessThan(40_000) expect( @@ -4974,9 +4997,68 @@ describe("output token budget", () => { } }) - test("never lets a page marker suppress the PDF byte-size floor", () => { + test("charges compact valid PDFs by their parsed reachable page count", async () => { + const pages = 100 + const line = "word ".repeat(20) + const content = + "BT /F1 6 Tf 7 TL 10 780 Td\n" + Array.from({ length: 100 }, () => `(${line}) Tj T*\n`).join("") + "ET" + const objects: string[] = [] + objects[1] = "<< /Type /Catalog /Pages 2 0 R >>" + objects[2] = + `<< /Type /Pages /Count ${pages} /Kids [` + + Array.from({ length: pages }, (_, index) => `${index + 3} 0 R`).join(" ") + + "] >>" + for (let index = 0; index < pages; index++) { + objects[index + 3] = + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] " + + "/Resources << /Font << /F1 104 0 R >> >> /Contents 103 0 R >>" + } + objects[103] = `<< /Length ${Buffer.byteLength(content)} >>\nstream\n${content}\nendstream` + objects[104] = "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>" + + let source = "%PDF-1.4\n" + const offsets = [0] + for (let index = 1; index <= 104; index++) { + offsets[index] = Buffer.byteLength(source) + source += `${index} 0 obj\n${objects[index]}\nendobj\n` + } + const xref = Buffer.byteLength(source) + source += "xref\n0 105\n0000000000 65535 f \n" + for (let index = 1; index <= 104; index++) { + source += `${String(offsets[index]).padStart(10, "0")} 00000 n \n` + } + source += `trailer\n<< /Size 105 /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n` + const pdf = Buffer.from(source) + + const messages = await convertToModelMessages([ + { + role: "user", + parts: [ + { + type: "file", + url: `data:application/pdf;base64,${pdf.toString("base64")}`, + mediaType: "application/pdf", + filename: "reused-content.pdf", + }, + ], + }, + ]) + const estimated = await estimateInputTokens({ system: [], messages }) + + expect(pdf.byteLength).toBeLessThan(32_000) + expect(estimated).toBeGreaterThanOrEqual(pages * 5_000) + expect(() => + clampOutputTokens({ + model: createWindowModel({ context: 200_000, input: 200_000, output: 64_000 }), + requested: 64_000, + inputTokens: estimated, + }), + ).toThrow(InputTokenBudgetError) + }) + + test("never lets a page marker suppress the PDF byte-size floor", async () => { const pdf = ["%PDF-1.7", "1 0 obj << /Type /Page >> endobj", "x".repeat(200_000), "%%EOF"].join("\n") - const estimated = estimateInputTokens({ + const estimated = await estimateInputTokens({ system: [], messages: [ { @@ -4988,13 +5070,13 @@ describe("output token budget", () => { expect(estimated).toBeGreaterThan(200_000) }) - test("bounds malformed data-URL header inspection and keeps its size floor", () => { + test("bounds malformed data-URL header inspection and keeps its size floor", async () => { const lateDelimiter = [ "data:application/pdf", "x".repeat(70 * 1_024), ",%PDF-1.7 << /Type /Pages /Count 600 >> %%EOF", ].join("") - const lateEstimated = estimateInputTokens({ + const lateEstimated = await estimateInputTokens({ system: [], messages: [ { @@ -5003,13 +5085,13 @@ describe("output token budget", () => { }, ], }) - // The comma is outside the bounded data-URL header prefix. The full string still establishes - // a conservative size floor without inspecting attacker-controlled PDF metadata. - expect(lateEstimated).toBeGreaterThan(70_000) - expect(lateEstimated).toBeLessThan(100_000) + // The comma is outside the bounded data-URL header prefix, so the document is not safely + // inspectable and receives the same conservative allowance as a remote PDF. + expect(lateEstimated).toBeGreaterThan(499_000) + expect(lateEstimated).toBeLessThan(510_000) const missingDelimiter = `data:application/pdf${"A".repeat(100_000)}` - const missingEstimated = estimateInputTokens({ + const missingEstimated = await estimateInputTokens({ system: [], messages: [ { @@ -5018,12 +5100,13 @@ describe("output token budget", () => { }, ], }) - expect(missingEstimated).toBeGreaterThan(100_000) + expect(missingEstimated).toBeGreaterThan(499_000) + expect(missingEstimated).toBeLessThan(510_000) }) - test("scales inline non-PDF files with decoded payload size", () => { + test("scales inline non-PDF files with decoded payload size", async () => { const text = "漢".repeat(50_000) - const estimated = estimateInputTokens({ + const estimated = await estimateInputTokens({ system: [], messages: [ { @@ -5035,8 +5118,8 @@ describe("output token budget", () => { expect(estimated).toBeGreaterThanOrEqual(150_000) }) - test("uses a conservative fixed fallback for remote PDFs", () => { - const estimated = estimateInputTokens({ + test("uses a conservative fixed fallback for remote PDFs", async () => { + const estimated = await estimateInputTokens({ system: [], messages: [ { @@ -5047,11 +5130,11 @@ describe("output token budget", () => { }, ], }) - expect(estimated).toBeGreaterThan(32_000) - expect(estimated).toBeLessThan(40_000) + expect(estimated).toBeGreaterThan(499_000) + expect(estimated).toBeLessThan(510_000) }) - test("counts every AI SDK v6 tool-result media variant once per transport occurrence", () => { + test("counts every AI SDK v6 tool-result media variant once per transport occurrence", async () => { const payload = "A".repeat(1_048_576) const variants = [ { type: "media" as const, mediaType: "application/pdf", data: payload }, @@ -5064,7 +5147,7 @@ describe("output token budget", () => { ] for (const variant of variants) { - const estimated = estimateInputTokens({ + const estimated = await estimateInputTokens({ system: [], messages: [ { @@ -5089,7 +5172,7 @@ describe("output token budget", () => { } const shared = { type: "image-data" as const, mediaType: "image/png", data: "AQ==" } - const repeated = estimateInputTokens({ + const repeated = await estimateInputTokens({ system: [], messages: [ { @@ -5108,7 +5191,7 @@ describe("output token budget", () => { expect(repeated).toBeGreaterThan(64 * 2_000) }) - test("projects unsupported media before estimation without discounting supported media", () => { + test("projects unsupported media before estimation without discounting supported media", async () => { const unsupported = createWindowModel({ context: 65_536, output: 16_384 }) const messages = [ { @@ -5123,7 +5206,7 @@ describe("output token budget", () => { const projected = ProviderTransform.messagesForInputEstimate(messages, unsupported) expect((messages[0].content[0] as { type: string }).type).toBe("image") expect((projected[0]!.content[0] as { type: string }).type).toBe("text") - const projectedEstimate = estimateInputTokens({ system: [], messages: projected }) + const projectedEstimate = await estimateInputTokens({ system: [], messages: projected }) expect(projectedEstimate).toBeLessThan(10_000) expect(clampOutputTokens({ model: unsupported, requested: 16_384, inputTokens: projectedEstimate })).toBe(16_384) @@ -5136,10 +5219,10 @@ describe("output token budget", () => { } const preserved = ProviderTransform.messagesForInputEstimate(messages, supported) expect((preserved[0]!.content[0] as { type: string }).type).toBe("image") - expect(estimateInputTokens({ system: [], messages: preserved })).toBeGreaterThan(64 * 2_000) + expect(await estimateInputTokens({ system: [], messages: preserved })).toBeGreaterThan(64 * 2_000) }) - test("projects every valid unsupported image payload and case-normalized file media type", () => { + test("projects every valid unsupported image payload and case-normalized file media type", async () => { const unsupported = createWindowModel({ context: 65_536, output: 16_384 }) const messages = [ { @@ -5158,11 +5241,11 @@ describe("output token budget", () => { const projected = ProviderTransform.messagesForInputEstimate(messages, unsupported) expect((projected[0]!.content as Array<{ type: string }>).every((part) => part.type === "text")).toBeTrue() - expect(estimateInputTokens({ system: [], messages: projected })).toBeLessThan(10_000) + expect(await estimateInputTokens({ system: [], messages: projected })).toBeLessThan(10_000) expect((messages[0].content as Array<{ type: string }>).every((part) => part.type !== "text")).toBeTrue() }) - test("counts repeated shared tool objects while terminating true cycles", () => { + test("counts repeated shared tool objects while terminating true cycles", async () => { const sharedTool = tool({ description: "shared schema documentation ".repeat(1_200), inputSchema: jsonSchema({ @@ -5170,18 +5253,22 @@ describe("output token budget", () => { properties: { query: { type: "string", description: "query details ".repeat(1_200) } }, }), }) - const once = estimateInputTokens({ system: [], messages: [], tools: { first: sharedTool } }) - const twice = estimateInputTokens({ system: [], messages: [], tools: { first: sharedTool, second: sharedTool } }) + const once = await estimateInputTokens({ system: [], messages: [], tools: { first: sharedTool } }) + const twice = await estimateInputTokens({ + system: [], + messages: [], + tools: { first: sharedTool, second: sharedTool }, + }) expect(twice).toBeGreaterThan(once * 1.8) const circular: Record = { text: "still counted" } circular.self = circular - expect(estimateInputTokens({ system: [], messages: [circular] })).toBeGreaterThan(0) + expect(await estimateInputTokens({ system: [], messages: [circular] })).toBeGreaterThan(0) }) - test("uses a conservative multilingual floor instead of the ASCII ratio", () => { + test("uses a conservative multilingual floor instead of the ASCII ratio", async () => { const text = "漢".repeat(10_000) - expect(estimateInputTokens({ system: [text], messages: [] })).toBeGreaterThanOrEqual(10_000) + expect(await estimateInputTokens({ system: [text], messages: [] })).toBeGreaterThanOrEqual(10_000) }) test("honors the known one-million-token Anthropic beta header", () => { @@ -5358,7 +5445,7 @@ describe("LLMRequestPrep.prepare - output token reservation", () => { const largePrompt = PROSE.repeat(Math.ceil((52_180 * 3.7) / PROSE.length)) test("a ~52K-token system prompt does not produce an unclamped request", async () => { - const estimated = estimateInputTokens({ system: [largePrompt], messages }) + const estimated = await estimateInputTokens({ system: [largePrompt], messages }) // Sized to reproduce the reported 52,180-token prompt. expect(estimated).toBeGreaterThan(51_500) expect(estimated).toBeLessThan(53_500) From 0eba8f8d249285ab5c8e5a9763f1dba5d1bf386a Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Mon, 31 Aug 2026 11:29:57 -0700 Subject: [PATCH 14/32] Revert "fix: parse PDF page counts safely" This reverts commit 858c7b1dabc492dd59e603d76f0ac76e2823f9da. --- bun.lock | 11 - packages/opencode/package.json | 1 - .../src/provider/output-token-budget.ts | 93 ++------ packages/opencode/src/session/llm.ts | 21 +- packages/opencode/src/session/llm/request.ts | 19 +- .../opencode/test/provider/transform.test.ts | 199 +++++------------- 6 files changed, 89 insertions(+), 255 deletions(-) diff --git a/bun.lock b/bun.lock index 3c43b48f16..0a711c43de 100644 --- a/bun.lock +++ b/bun.lock @@ -337,7 +337,6 @@ "opencode-poe-auth": "0.0.1", "opentui-spinner": "catalog:", "partial-json": "0.1.7", - "pdf-lib": "1.17.1", "remeda": "catalog:", "semver": "^7.6.3", "solid-js": "catalog:", @@ -1294,10 +1293,6 @@ "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="], - "@pdf-lib/standard-fonts": ["@pdf-lib/standard-fonts@1.0.0", "", { "dependencies": { "pako": "^1.0.6" } }, "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA=="], - - "@pdf-lib/upng": ["@pdf-lib/upng@1.0.1", "", { "dependencies": { "pako": "^1.0.10" } }, "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ=="], - "@pierre/diffs": ["@pierre/diffs@1.2.10", "", { "dependencies": { "@pierre/theme": "1.0.3", "@pierre/theming": "0.0.1", "@shikijs/transformers": "^3.0.0 || ^4.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0 || ^4.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-rPeAmDWarxFVTQpaf4y6wTxjZxU44xKJKoJti2zU21P06DVd9nRHZX+xSIObLB307Qjpaesyb1x/j0z94t7vLw=="], "@pierre/theme": ["@pierre/theme@1.0.3", "", {}, "sha512-sWHv11TMoqKxKDgTIk5VbhQjdPhs8DCcBxbjh3mRlS3YOM/OcrWoGX6MM8eBGn9cUu3M46Py0JnxsG2nJaFTuA=="], @@ -2464,8 +2459,6 @@ "pad-left": ["pad-left@2.1.0", "", { "dependencies": { "repeat-string": "^1.5.4" } }, "sha512-HJxs9K9AztdIQIAIa/OIazRAUW/L6B9hbQDxO4X07roW3eo9XqZc2ur9bn1StH9CnbbI9EgvejHQX7CBpCF1QA=="], - "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], - "parse-conflict-json": ["parse-conflict-json@5.0.1", "", { "dependencies": { "json-parse-even-better-errors": "^5.0.0", "just-diff": "^6.0.0", "just-diff-apply": "^5.2.0" } }, "sha512-ZHEmNKMq1wyJXNwLxyHnluPfRAFSIliBvbK/UiOceROt4Xh9Pz0fq49NytIaeaCUf5VR86hwQ/34FCcNU5/LKQ=="], "parse-passwd": ["parse-passwd@1.0.0", "", {}, "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q=="], @@ -2494,8 +2487,6 @@ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "pdf-lib": ["pdf-lib@1.17.1", "", { "dependencies": { "@pdf-lib/standard-fonts": "^1.0.0", "@pdf-lib/upng": "^1.0.1", "pako": "^1.0.11", "tslib": "^1.11.1" } }, "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw=="], - "perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="], "pg": ["pg@8.22.0", "", { "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.15.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA=="], @@ -3356,8 +3347,6 @@ "patch-package/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], - "pdf-lib/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], - "proxy-addr/ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "proxy-agent/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 323949100a..1714dafeaf 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -143,7 +143,6 @@ "opencode-poe-auth": "0.0.1", "opentui-spinner": "catalog:", "partial-json": "0.1.7", - "pdf-lib": "1.17.1", "remeda": "catalog:", "semver": "^7.6.3", "solid-js": "catalog:", diff --git a/packages/opencode/src/provider/output-token-budget.ts b/packages/opencode/src/provider/output-token-budget.ts index 5aeffb0f5b..03b63a1595 100644 --- a/packages/opencode/src/provider/output-token-budget.ts +++ b/packages/opencode/src/provider/output-token-budget.ts @@ -14,11 +14,6 @@ const ESTIMATE_CHUNK_SIZE = 400 const MEDIA_TOKEN_ALLOWANCE = 2_048 const FILE_TOKEN_ALLOWANCE = 16_384 const PDF_TOKEN_ALLOWANCE = 32_768 -const PDF_TOKENS_PER_PAGE = 5_000 -const PDF_UNINSPECTABLE_TOKEN_ALLOWANCE = 100 * PDF_TOKENS_PER_PAGE -// Above this decoded size, the one-byte/one-token floor already exceeds the maximum -// documented 100-page allowance, so structural parsing cannot raise the estimate. -const PDF_PARSE_BYTE_LIMIT = PDF_UNINSPECTABLE_TOKEN_ALLOWANCE const DATA_URL_HEADER_LIMIT = 1_024 const MIN_REASONING_BUDGET = 1_024 const EMOJI = /\p{Extended_Pictographic}/u @@ -212,73 +207,21 @@ function inlinePayloadSize(payload: unknown): number | undefined { return Math.max(0, Math.floor((bodyLength * 3) / 4) - padding) } -/** Decode a bounded inline payload for structural PDF parsing. */ -function inlinePayloadBytes(payload: unknown, size: number): Uint8Array | undefined { - if (size > PDF_PARSE_BYTE_LIMIT) return undefined - if (ArrayBuffer.isView(payload)) { - return new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength) - } - if (payload instanceof ArrayBuffer) return new Uint8Array(payload) - - const value = payload instanceof URL ? payload.href : typeof payload === "string" ? payload : undefined - if (!value || /^https?:/i.test(value)) return undefined - const dataURL = /^data:/i.test(value) - const prefix = dataURL ? value.slice(0, DATA_URL_HEADER_LIMIT) : "" - const comma = dataURL ? prefix.indexOf(",") : -1 - if (dataURL && comma === -1) return undefined - const bodyOffset = comma === -1 ? 0 : comma + 1 - const body = value.slice(bodyOffset) - - try { - if (comma !== -1 && !/;base64(?:;|$)/i.test(prefix.slice(0, comma))) { - return Buffer.from(decodeURIComponent(body), "latin1") - } - return Buffer.from(body, "base64") - } catch { - return undefined - } -} - -/** Combine byte size with a page count obtained from a real PDF object graph. */ -async function pdfTokenAllowance(payload: unknown): Promise { - const size = inlinePayloadSize(payload) - if (size === undefined) { - // Remote URLs and provider file IDs cannot be inspected locally. Reserve the documented - // 100-page request maximum at the same deliberately conservative per-page rate. - return PDF_UNINSPECTABLE_TOKEN_ALLOWANCE - } - - const baseline = Math.max(PDF_TOKEN_ALLOWANCE, size) - const bytes = inlinePayloadBytes(payload, size) - // Inputs above the parser cap already carry a decoded-byte allowance at least as large as the - // maximum page allowance. Smaller inputs that cannot be inspected use the remote/file-ID - // fallback rather than pretending their page expansion is known. - if (!bytes) return size > PDF_PARSE_BYTE_LIMIT ? baseline : Math.max(baseline, PDF_UNINSPECTABLE_TOKEN_ALLOWANCE) - - try { - const { PDFDocument, ParseSpeeds } = await import("pdf-lib") - const document = await PDFDocument.load(bytes, { - parseSpeed: ParseSpeeds.Fastest, - throwOnInvalidObject: true, - updateMetadata: false, - capNumbers: true, - }) - const pages = document.getPageCount() - if (!Number.isSafeInteger(pages) || pages <= 0) { - return Math.max(baseline, PDF_UNINSPECTABLE_TOKEN_ALLOWANCE) - } - return Math.max(baseline, pages * PDF_TOKENS_PER_PAGE) - } catch { - return Math.max(baseline, PDF_UNINSPECTABLE_TOKEN_ALLOWANCE) - } +/** Combine fixed and monotonic byte-size evidence without trusting unparsed PDF metadata. */ +function pdfTokenAllowance(payload: unknown): number { + const bytes = inlinePayloadSize(payload) ?? 0 + // Raw PDF bytes cannot authenticate page-tree metadata: comments, strings, streams, stale + // objects, and incremental revisions may all contain convincing but unreachable markers. + // One decoded byte per token plus the fixed floor stays conservative without amplification. + return Math.max(PDF_TOKEN_ALLOWANCE, bytes) } /** Assign an allowance that matches the semantic media kind rather than its encoded bytes. */ -async function mediaTokenAllowance(part: JsonRecord): Promise { +function mediaTokenAllowance(part: JsonRecord): number { const payload = mediaPayload(part) const mime = mediaType(part, payload) if (mime?.startsWith("image/") || String(part.type).startsWith("image")) return MEDIA_TOKEN_ALLOWANCE - if (mime === "application/pdf") return await pdfTokenAllowance(payload) + if (mime === "application/pdf") return pdfTokenAllowance(payload) if (FILE_PART_TYPES.has(String(part.type))) { return Math.max(FILE_TOKEN_ALLOWANCE, inlinePayloadSize(payload) ?? 0) } @@ -286,27 +229,25 @@ async function mediaTokenAllowance(part: JsonRecord): Promise { } /** Mark only actual ModelMessage content parts whose payload is provider media. */ -async function messageMediaAllowances(messages: readonly unknown[]): Promise> { +function messageMediaAllowances(messages: readonly unknown[]): WeakMap { const result = new WeakMap() const visited = new WeakSet() - const visitContent = async (content: unknown): Promise => { + const visitContent = (content: unknown) => { if (!Array.isArray(content) || visited.has(content)) return visited.add(content) for (const part of content) { if (!isRecord(part)) continue - if (MEDIA_PART_TYPES.has(String(part.type)) && !result.has(part)) { - result.set(part, await mediaTokenAllowance(part)) - } + if (MEDIA_PART_TYPES.has(String(part.type))) result.set(part, mediaTokenAllowance(part)) // Tool-result media is nested in the AI SDK's typed content output. if (part.type !== "tool-result" || !isRecord(part.output)) continue - if (part.output.type === "content") await visitContent(part.output.value) + if (part.output.type === "content") visitContent(part.output.value) } } for (const message of messages) { - if (isRecord(message)) await visitContent(message.content) + if (isRecord(message)) visitContent(message.content) } return result } @@ -376,16 +317,16 @@ export function effectiveContextWindow(input: { } /** Estimate the text, finalized tools, instructions, and media allowance sent in one request. */ -export async function estimateInputTokens(input: { +export function estimateInputTokens(input: { readonly system: readonly string[] readonly messages: readonly unknown[] readonly tools?: Readonly> readonly instructions?: unknown -}): Promise { +}): number { const system = input.system.join("\n") let total = estimateTextTokens(system) - const messages = serializeForEstimate(input.messages, await messageMediaAllowances(input.messages)) + const messages = serializeForEstimate(input.messages, messageMediaAllowances(input.messages)) total += estimateTextTokens(messages.text) + messages.mediaTokens if (input.tools !== undefined) { diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 96ff9a33ac..6005c2f623 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -243,18 +243,9 @@ export namespace LLM { // altimate_change start — clamp after every context-affecting request field is finalized. // Tool schemas and provider instructions consume the shared context window, while encoded - // media bytes do not count as literal text. Providers that omit maxOutputTokens skip the - // estimator. Known context beta headers widen the catalog + // media bytes do not count as literal text. The estimator runs lazily so providers that omit + // maxOutputTokens pay no serialization cost. Known context beta headers widen the catalog // limit before the clamp. Fixed reasoning budgets are reconciled with the final reservation. - const inputTokens = - params.maxOutputTokens === undefined - ? 0 - : await estimateInputTokens({ - system, - messages: ProviderTransform.messagesForInputEstimate(input.messages, input.model), - tools, - instructions: params.options.instructions, - }) const maxOutputTokens = clampOutputTokens({ model: input.model, requested: params.maxOutputTokens, @@ -263,7 +254,13 @@ export namespace LLM { // Provider defaults are lower precedence than the exact case-normalized outgoing record. headerSources: [provider.options, requestHeaders], }), - inputTokens, + inputTokens: () => + estimateInputTokens({ + system, + messages: ProviderTransform.messagesForInputEstimate(input.messages, input.model), + tools, + instructions: params.options.instructions, + }), }) const requestOptions = clampReasoningBudget(params.options, maxOutputTokens) // altimate_change end diff --git a/packages/opencode/src/session/llm/request.ts b/packages/opencode/src/session/llm/request.ts index dc97bc0b70..edbd7befe8 100644 --- a/packages/opencode/src/session/llm/request.ts +++ b/packages/opencode/src/session/llm/request.ts @@ -200,17 +200,6 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre // altimate_change start — clamp after tools, headers, and plugin options are finalized. const sortedTools = Object.fromEntries(Object.entries(tools).toSorted(([a], [b]) => a.localeCompare(b))) - const inputTokens = - params.maxOutputTokens === undefined - ? 0 - : yield* Effect.promise(() => - estimateInputTokens({ - system, - messages: ProviderTransform.messagesForInputEstimate(input.messages, input.model), - tools: sortedTools, - instructions: params.options.instructions, - }), - ) const maxOutputTokens = clampOutputTokens({ model: input.model, requested: params.maxOutputTokens, @@ -219,7 +208,13 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre // Provider defaults are lower precedence than the exact case-normalized outgoing record. headerSources: [input.provider.options, requestHeaders], }), - inputTokens, + inputTokens: () => + estimateInputTokens({ + system, + messages: ProviderTransform.messagesForInputEstimate(input.messages, input.model), + tools: sortedTools, + instructions: params.options.instructions, + }), }) const requestOptions = clampReasoningBudget(params.options, maxOutputTokens) const clampedParams = { diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 5ef9ad1cb0..f642b0c929 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import { Effect } from "effect" -import { convertToModelMessages, jsonSchema, tool, type ModelMessage, type Tool } from "ai" +import { jsonSchema, tool, type ModelMessage, type Tool } from "ai" import { ProviderTransform } from "@/provider/transform" import { clampOutputTokens, @@ -4803,9 +4803,9 @@ describe("output token budget", () => { expect(evaluated).toBeFalse() }) - test("counts tool schemas and provider instructions", async () => { - const base = await estimateInputTokens({ system: ["system"], messages: [{ role: "user", content: "hello" }] }) - const complete = await estimateInputTokens({ + test("counts tool schemas and provider instructions", () => { + const base = estimateInputTokens({ system: ["system"], messages: [{ role: "user", content: "hello" }] }) + const complete = estimateInputTokens({ system: ["system"], messages: [{ role: "user", content: "hello" }], instructions: "provider instruction ".repeat(400), @@ -4819,8 +4819,8 @@ describe("output token budget", () => { expect(complete).toBeGreaterThan(base + 1_000) }) - test("does not tokenize encoded media bytes as literal prompt text", async () => { - const estimated = await estimateInputTokens({ + test("does not tokenize encoded media bytes as literal prompt text", () => { + const estimated = estimateInputTokens({ system: [], messages: [ { @@ -4833,7 +4833,7 @@ describe("output token budget", () => { expect(estimated).toBeLessThan(10_000) }) - test("counts data-URL-shaped text in every textual request field", async () => { + test("counts data-URL-shaped text in every textual request field", () => { const prefixes = [ "data:image/png;base64,", "data:audio/wav;base64,", @@ -4843,7 +4843,7 @@ describe("output token budget", () => { for (const prefix of prefixes) { const text = prefix + "漢".repeat(70_000) expect( - await estimateInputTokens({ + estimateInputTokens({ system: [], messages: [{ role: "user", content: [{ type: "text", text }] }], }), @@ -4852,14 +4852,14 @@ describe("output token budget", () => { const text = prefixes[0] + "漢".repeat(70_000) const estimates = [ - await estimateInputTokens({ system: [], messages: [{ role: "user", content: text }] }), - await estimateInputTokens({ system: [], messages: [], instructions: text }), - await estimateInputTokens({ + estimateInputTokens({ system: [], messages: [{ role: "user", content: text }] }), + estimateInputTokens({ system: [], messages: [], instructions: text }), + estimateInputTokens({ system: [], messages: [], tools: { inspect: { description: text, inputSchema: { type: "object" } } }, }), - await estimateInputTokens({ + estimateInputTokens({ system: [], messages: [ { @@ -4870,7 +4870,7 @@ describe("output token budget", () => { }, ], }), - await estimateInputTokens({ + estimateInputTokens({ system: [], messages: [ { @@ -4888,9 +4888,9 @@ describe("output token budget", () => { ) }) - test("charges the same fixed allowance for URL-backed media", async () => { - const base = await estimateInputTokens({ system: [], messages: [{ role: "user", content: "show this" }] }) - const estimated = await estimateInputTokens({ + test("charges the same fixed allowance for URL-backed media", () => { + const base = estimateInputTokens({ system: [], messages: [{ role: "user", content: "show this" }] }) + const estimated = estimateInputTokens({ system: [], messages: [ { @@ -4903,8 +4903,8 @@ describe("output token budget", () => { expect(estimated).toBeLessThan(base + 3_000) }) - test("keeps binary images bounded while scaling PDF estimates with document size", async () => { - const binaryImage = await estimateInputTokens({ + test("keeps binary images bounded while scaling PDF estimates with document size", () => { + const binaryImage = estimateInputTokens({ system: [], messages: [ { @@ -4913,7 +4913,7 @@ describe("output token budget", () => { }, ], }) - const pdfToolResult = await estimateInputTokens({ + const pdfToolResult = estimateInputTokens({ system: [], messages: [ { @@ -4937,36 +4937,13 @@ describe("output token budget", () => { expect(pdfToolResult).toBeGreaterThan(100_000) }) - test("does not trust unparsed PDF metadata as page-count evidence", async () => { + test("does not trust unparsed PDF metadata as page-count evidence", () => { const marker = "/Type /Pages /Count 600" - const buildPdf = (input: { comment?: string; note?: string; stream?: string; unreachable?: string }) => { - const content = input.stream ?? "" - const objects = [ - `<< /Type /Catalog /Pages 2 0 R${input.note ? ` /Note (${input.note})` : ""} >>`, - "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", - "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Contents 4 0 R >>", - `<< /Length ${Buffer.byteLength(content)} >>\nstream\n${content}\nendstream`, - ...(input.unreachable ? [`<< ${input.unreachable} >>`] : []), - ] - let source = `%PDF-1.4\n% ${input.comment ?? "control"}\n` - const offsets = [0] - for (const [index, object] of objects.entries()) { - offsets[index + 1] = Buffer.byteLength(source) - source += `${index + 1} 0 obj\n${object}\nendobj\n` - } - const xref = Buffer.byteLength(source) - source += `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n` - for (const offset of offsets.slice(1)) { - source += `${String(offset).padStart(10, "0")} 00000 n \n` - } - source += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n` - return source - } const variants = [ - buildPdf({ comment: marker }), - buildPdf({ note: marker }), - buildPdf({ stream: `% ${marker}` }), - buildPdf({ unreachable: marker }), + ["%PDF-1.4", `% ${marker}`, "1 0 obj << /Type /Pages /Count 1 >> endobj", "%%EOF"].join("\n"), + ["%PDF-1.4", `1 0 obj << /Note (${marker}) >> endobj`, "%%EOF"].join("\n"), + ["%PDF-1.4", "1 0 obj << /Length 24 >> stream", marker, "endstream endobj", "%%EOF"].join("\n"), + ["%PDF-1.4", `99 0 obj << ${marker} >> endobj`, "%%EOF"].join("\n"), ] const estimate = (data: string | Uint8Array | ArrayBuffer) => estimateInputTokens({ @@ -4983,7 +4960,7 @@ describe("output token budget", () => { bytes.buffer, ] for (const payload of payloads) { - const estimated = await estimate(payload) + const estimated = estimate(payload) expect(estimated).toBeGreaterThan(32_000) expect(estimated).toBeLessThan(40_000) expect( @@ -4997,68 +4974,9 @@ describe("output token budget", () => { } }) - test("charges compact valid PDFs by their parsed reachable page count", async () => { - const pages = 100 - const line = "word ".repeat(20) - const content = - "BT /F1 6 Tf 7 TL 10 780 Td\n" + Array.from({ length: 100 }, () => `(${line}) Tj T*\n`).join("") + "ET" - const objects: string[] = [] - objects[1] = "<< /Type /Catalog /Pages 2 0 R >>" - objects[2] = - `<< /Type /Pages /Count ${pages} /Kids [` + - Array.from({ length: pages }, (_, index) => `${index + 3} 0 R`).join(" ") + - "] >>" - for (let index = 0; index < pages; index++) { - objects[index + 3] = - "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] " + - "/Resources << /Font << /F1 104 0 R >> >> /Contents 103 0 R >>" - } - objects[103] = `<< /Length ${Buffer.byteLength(content)} >>\nstream\n${content}\nendstream` - objects[104] = "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>" - - let source = "%PDF-1.4\n" - const offsets = [0] - for (let index = 1; index <= 104; index++) { - offsets[index] = Buffer.byteLength(source) - source += `${index} 0 obj\n${objects[index]}\nendobj\n` - } - const xref = Buffer.byteLength(source) - source += "xref\n0 105\n0000000000 65535 f \n" - for (let index = 1; index <= 104; index++) { - source += `${String(offsets[index]).padStart(10, "0")} 00000 n \n` - } - source += `trailer\n<< /Size 105 /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n` - const pdf = Buffer.from(source) - - const messages = await convertToModelMessages([ - { - role: "user", - parts: [ - { - type: "file", - url: `data:application/pdf;base64,${pdf.toString("base64")}`, - mediaType: "application/pdf", - filename: "reused-content.pdf", - }, - ], - }, - ]) - const estimated = await estimateInputTokens({ system: [], messages }) - - expect(pdf.byteLength).toBeLessThan(32_000) - expect(estimated).toBeGreaterThanOrEqual(pages * 5_000) - expect(() => - clampOutputTokens({ - model: createWindowModel({ context: 200_000, input: 200_000, output: 64_000 }), - requested: 64_000, - inputTokens: estimated, - }), - ).toThrow(InputTokenBudgetError) - }) - - test("never lets a page marker suppress the PDF byte-size floor", async () => { + test("never lets a page marker suppress the PDF byte-size floor", () => { const pdf = ["%PDF-1.7", "1 0 obj << /Type /Page >> endobj", "x".repeat(200_000), "%%EOF"].join("\n") - const estimated = await estimateInputTokens({ + const estimated = estimateInputTokens({ system: [], messages: [ { @@ -5070,13 +4988,13 @@ describe("output token budget", () => { expect(estimated).toBeGreaterThan(200_000) }) - test("bounds malformed data-URL header inspection and keeps its size floor", async () => { + test("bounds malformed data-URL header inspection and keeps its size floor", () => { const lateDelimiter = [ "data:application/pdf", "x".repeat(70 * 1_024), ",%PDF-1.7 << /Type /Pages /Count 600 >> %%EOF", ].join("") - const lateEstimated = await estimateInputTokens({ + const lateEstimated = estimateInputTokens({ system: [], messages: [ { @@ -5085,13 +5003,13 @@ describe("output token budget", () => { }, ], }) - // The comma is outside the bounded data-URL header prefix, so the document is not safely - // inspectable and receives the same conservative allowance as a remote PDF. - expect(lateEstimated).toBeGreaterThan(499_000) - expect(lateEstimated).toBeLessThan(510_000) + // The comma is outside the bounded data-URL header prefix. The full string still establishes + // a conservative size floor without inspecting attacker-controlled PDF metadata. + expect(lateEstimated).toBeGreaterThan(70_000) + expect(lateEstimated).toBeLessThan(100_000) const missingDelimiter = `data:application/pdf${"A".repeat(100_000)}` - const missingEstimated = await estimateInputTokens({ + const missingEstimated = estimateInputTokens({ system: [], messages: [ { @@ -5100,13 +5018,12 @@ describe("output token budget", () => { }, ], }) - expect(missingEstimated).toBeGreaterThan(499_000) - expect(missingEstimated).toBeLessThan(510_000) + expect(missingEstimated).toBeGreaterThan(100_000) }) - test("scales inline non-PDF files with decoded payload size", async () => { + test("scales inline non-PDF files with decoded payload size", () => { const text = "漢".repeat(50_000) - const estimated = await estimateInputTokens({ + const estimated = estimateInputTokens({ system: [], messages: [ { @@ -5118,8 +5035,8 @@ describe("output token budget", () => { expect(estimated).toBeGreaterThanOrEqual(150_000) }) - test("uses a conservative fixed fallback for remote PDFs", async () => { - const estimated = await estimateInputTokens({ + test("uses a conservative fixed fallback for remote PDFs", () => { + const estimated = estimateInputTokens({ system: [], messages: [ { @@ -5130,11 +5047,11 @@ describe("output token budget", () => { }, ], }) - expect(estimated).toBeGreaterThan(499_000) - expect(estimated).toBeLessThan(510_000) + expect(estimated).toBeGreaterThan(32_000) + expect(estimated).toBeLessThan(40_000) }) - test("counts every AI SDK v6 tool-result media variant once per transport occurrence", async () => { + test("counts every AI SDK v6 tool-result media variant once per transport occurrence", () => { const payload = "A".repeat(1_048_576) const variants = [ { type: "media" as const, mediaType: "application/pdf", data: payload }, @@ -5147,7 +5064,7 @@ describe("output token budget", () => { ] for (const variant of variants) { - const estimated = await estimateInputTokens({ + const estimated = estimateInputTokens({ system: [], messages: [ { @@ -5172,7 +5089,7 @@ describe("output token budget", () => { } const shared = { type: "image-data" as const, mediaType: "image/png", data: "AQ==" } - const repeated = await estimateInputTokens({ + const repeated = estimateInputTokens({ system: [], messages: [ { @@ -5191,7 +5108,7 @@ describe("output token budget", () => { expect(repeated).toBeGreaterThan(64 * 2_000) }) - test("projects unsupported media before estimation without discounting supported media", async () => { + test("projects unsupported media before estimation without discounting supported media", () => { const unsupported = createWindowModel({ context: 65_536, output: 16_384 }) const messages = [ { @@ -5206,7 +5123,7 @@ describe("output token budget", () => { const projected = ProviderTransform.messagesForInputEstimate(messages, unsupported) expect((messages[0].content[0] as { type: string }).type).toBe("image") expect((projected[0]!.content[0] as { type: string }).type).toBe("text") - const projectedEstimate = await estimateInputTokens({ system: [], messages: projected }) + const projectedEstimate = estimateInputTokens({ system: [], messages: projected }) expect(projectedEstimate).toBeLessThan(10_000) expect(clampOutputTokens({ model: unsupported, requested: 16_384, inputTokens: projectedEstimate })).toBe(16_384) @@ -5219,10 +5136,10 @@ describe("output token budget", () => { } const preserved = ProviderTransform.messagesForInputEstimate(messages, supported) expect((preserved[0]!.content[0] as { type: string }).type).toBe("image") - expect(await estimateInputTokens({ system: [], messages: preserved })).toBeGreaterThan(64 * 2_000) + expect(estimateInputTokens({ system: [], messages: preserved })).toBeGreaterThan(64 * 2_000) }) - test("projects every valid unsupported image payload and case-normalized file media type", async () => { + test("projects every valid unsupported image payload and case-normalized file media type", () => { const unsupported = createWindowModel({ context: 65_536, output: 16_384 }) const messages = [ { @@ -5241,11 +5158,11 @@ describe("output token budget", () => { const projected = ProviderTransform.messagesForInputEstimate(messages, unsupported) expect((projected[0]!.content as Array<{ type: string }>).every((part) => part.type === "text")).toBeTrue() - expect(await estimateInputTokens({ system: [], messages: projected })).toBeLessThan(10_000) + expect(estimateInputTokens({ system: [], messages: projected })).toBeLessThan(10_000) expect((messages[0].content as Array<{ type: string }>).every((part) => part.type !== "text")).toBeTrue() }) - test("counts repeated shared tool objects while terminating true cycles", async () => { + test("counts repeated shared tool objects while terminating true cycles", () => { const sharedTool = tool({ description: "shared schema documentation ".repeat(1_200), inputSchema: jsonSchema({ @@ -5253,22 +5170,18 @@ describe("output token budget", () => { properties: { query: { type: "string", description: "query details ".repeat(1_200) } }, }), }) - const once = await estimateInputTokens({ system: [], messages: [], tools: { first: sharedTool } }) - const twice = await estimateInputTokens({ - system: [], - messages: [], - tools: { first: sharedTool, second: sharedTool }, - }) + const once = estimateInputTokens({ system: [], messages: [], tools: { first: sharedTool } }) + const twice = estimateInputTokens({ system: [], messages: [], tools: { first: sharedTool, second: sharedTool } }) expect(twice).toBeGreaterThan(once * 1.8) const circular: Record = { text: "still counted" } circular.self = circular - expect(await estimateInputTokens({ system: [], messages: [circular] })).toBeGreaterThan(0) + expect(estimateInputTokens({ system: [], messages: [circular] })).toBeGreaterThan(0) }) - test("uses a conservative multilingual floor instead of the ASCII ratio", async () => { + test("uses a conservative multilingual floor instead of the ASCII ratio", () => { const text = "漢".repeat(10_000) - expect(await estimateInputTokens({ system: [text], messages: [] })).toBeGreaterThanOrEqual(10_000) + expect(estimateInputTokens({ system: [text], messages: [] })).toBeGreaterThanOrEqual(10_000) }) test("honors the known one-million-token Anthropic beta header", () => { @@ -5445,7 +5358,7 @@ describe("LLMRequestPrep.prepare - output token reservation", () => { const largePrompt = PROSE.repeat(Math.ceil((52_180 * 3.7) / PROSE.length)) test("a ~52K-token system prompt does not produce an unclamped request", async () => { - const estimated = await estimateInputTokens({ system: [largePrompt], messages }) + const estimated = estimateInputTokens({ system: [largePrompt], messages }) // Sized to reproduce the reported 52,180-token prompt. expect(estimated).toBeGreaterThan(51_500) expect(estimated).toBeLessThan(53_500) From 908be9cabb2b552c56cbd86537fbccb86ea5e0b2 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Mon, 31 Aug 2026 11:30:59 -0700 Subject: [PATCH 15/32] fix: keep PDF budgeting parser-free --- packages/opencode/src/provider/output-token-budget.ts | 9 +++++---- packages/opencode/test/provider/transform.test.ts | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/provider/output-token-budget.ts b/packages/opencode/src/provider/output-token-budget.ts index 03b63a1595..1de35047e9 100644 --- a/packages/opencode/src/provider/output-token-budget.ts +++ b/packages/opencode/src/provider/output-token-budget.ts @@ -207,12 +207,13 @@ function inlinePayloadSize(payload: unknown): number | undefined { return Math.max(0, Math.floor((bodyLength * 3) / 4) - padding) } -/** Combine fixed and monotonic byte-size evidence without trusting unparsed PDF metadata. */ +/** Apply a bounded, parser-free PDF heuristic without trusting document metadata. */ function pdfTokenAllowance(payload: unknown): number { const bytes = inlinePayloadSize(payload) ?? 0 - // Raw PDF bytes cannot authenticate page-tree metadata: comments, strings, streams, stale - // objects, and incremental revisions may all contain convincing but unreachable markers. - // One decoded byte per token plus the fixed floor stays conservative without amplification. + // Page expansion cannot be derived safely from raw bytes: lexical page markers are spoofable, + // while structural parsing adds decompression and traversal risks to the request path. Keep the + // local estimate best-effort and monotonic; the configured provider remains authoritative for + // unusually compact or dense documents. return Math.max(PDF_TOKEN_ALLOWANCE, bytes) } diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index f642b0c929..c7262964a0 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -5035,7 +5035,7 @@ describe("output token budget", () => { expect(estimated).toBeGreaterThanOrEqual(150_000) }) - test("uses a conservative fixed fallback for remote PDFs", () => { + test("uses a fixed parser-free fallback for remote PDFs", () => { const estimated = estimateInputTokens({ system: [], messages: [ From 5e04c7885de23dcb9806bf30e30a9b4f5c7858c5 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Mon, 31 Aug 2026 11:53:15 -0700 Subject: [PATCH 16/32] docs: record PR 1196 final review --- .../code-reviews/PR 1196 Consensus Review.md | 134 ++++++++++++++++++ .../PR 1196 Security Review.md | 88 ++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 research/code-reviews/PR 1196 Consensus Review.md create mode 100644 research/security-reviews/PR 1196 Security Review.md diff --git a/research/code-reviews/PR 1196 Consensus Review.md b/research/code-reviews/PR 1196 Consensus Review.md new file mode 100644 index 0000000000..1fd8b2c79b --- /dev/null +++ b/research/code-reviews/PR 1196 Consensus Review.md @@ -0,0 +1,134 @@ +# PR 1196 Consensus Review + +- Repository: `AltimateAI/altimate-code` +- Pull request: [#1196](https://github.com/AltimateAI/altimate-code/pull/1196) +- Review dates: 2026-08-30 through 2026-08-31 +- Final code candidate: `908be9cabb2b552c56cbd86537fbccb86ea5e0b2` +- Mode: full Council review plus final independent remediation pass +- Local verdict: **PASS** +- Remote status: final commits still need to be pushed and fresh CI/bot review must finish + +## Decision + +PR #1196 is locally merge-ready under the selected product scope. + +The feature prevents avoidable provider failures by reserving output only after the real request shape is known. The final code budgets system text, messages, finalized tool schemas, provider instructions, semantic media, dedicated input ceilings, context-expansion headers, and fixed reasoning options at both production request boundaries. + +PDF accounting is intentionally approximate. The user selected a parser-free policy: + +```text +PDF allowance = max(32,768, decoded inline payload bytes) +``` + +Remote URLs and provider file IDs receive the fixed allowance. Exact page expansion and tokenization remain provider-authoritative. The Council did not require exact PDF parsing because adding a parser would create a new document-processing/resource boundary without making provider token accounting exact. + +## Original Council gate + +The first three-round Council unanimously rejected the pre-repair candidate and required five concrete fixes: + +1. Enforce `model.limit.input` as a hard estimated-input ceiling. +2. Count every semantic media occurrence conservatively, including URL-backed and AI SDK v6 tool-result variants. +3. Reconcile fixed reasoning budgets with every defined final output reservation. +4. Distinguish repeated aliases from true cycles so repeated schemas/media are counted per transport occurrence. +5. Add behavioral proof that both AI SDK and native request paths send finalized tools, clamped output, and reconciled reasoning together. + +All five are present in the final branch with focused regressions. + +## Additional review findings + +Post-Council review found and repaired: + +- request-header precedence drift around the Anthropic 1M context flag; +- unsupported-media estimation before canonical projection; +- media-looking ordinary text being discounted as binary content; +- small-model output floors above the model's own reservation; +- safety-margin loss near the context boundary; +- non-JSON provider-option loss during reasoning reconciliation; +- raw PDF page-marker trust. + +Each repair was centralized in the shared output-budget module or the existing pure media projection, then exercised through both request boundaries. + +## Why the PDF parser experiment was rejected + +An intermediate branch attempted structural page counting with `pdf-lib`. Independent reviewers found two blockers: + +- the fixed 100-page/500 KB policy still undercounted valid many-page requests on supported 1M-context models; +- compressed object streams could expand substantially in-process before request admission, while the input gate bounded only compressed bytes. + +The parser experiment was reverted. The dependency and transitive lock entries are absent from the final tree, estimator APIs are synchronous again, and tests describe the fixed allowance as parser-free. + +Batching the same parser would not remove its decompression/traversal boundary. A subagent is useful for development review, but it is not a runtime memory, CPU, or trust boundary. The smallest safe scope is therefore the crude local estimate plus provider enforcement. + +## Final independent remediation review + +Two independent live Council seats re-reviewed exact head `908be9cabb` after the parser removal. + +### Feynman seat — PASS + +- Verified exact final head. +- Confirmed `pdf-lib`, async parsing, page regex/policy, and transitive lock entries are absent. +- Confirmed lazy estimation is not evaluated when `maxOutputTokens` is omitted. +- Confirmed both request boundaries clamp after headers, tools, instructions, and media projection are finalized. +- Re-ran provider, native, stream, typecheck, diff, and strict marker checks successfully. + +### Musashi seat — PASS + +- Confirmed the parser experiment is cleanly reverted. +- Confirmed the final code differs from the last pre-parser safe candidate only in the explicit parser-free comment and test naming. +- Confirmed synchronous lazy estimation and parity across both callers. +- Confirmed dependency cleanup, focused tests, typecheck, and diff checks pass. + +### Degraded seat + +The original chairman seat was rejected twice by the service safety filter because its retained conversation context included the earlier parser stress case. It produced no contrary code finding on the final head. The thread limit prevented replacing that retained seat with a new fourth thread. + +Consensus therefore rests on two independent live PASS votes, the primary review, the complete changed-file inspection, and a sealed zero-finding Codex Security remediation scan. The degraded seat is disclosed rather than silently counted as agreement. + +## Final verification + +- Focused provider, AI-SDK stream, native request, and upstream bridge suites: **413 passed, 11 skipped, 1 existing todo, 0 failed**. +- Repository typecheck: **13/13 successful**. +- Strict changed-file marker validation: passed. +- Required-marker inventory: **35/35**. +- Frozen lockfile install: **1,295 installs across 1,390 packages, no changes**. +- Targeted oxlint: **212 warnings, 0 errors**. +- Prettier: all changed files pass except `provider/provider.ts` and `provider/transform.ts`; both fail identically on `origin/main`. +- `git diff --check origin/main...HEAD`: passed. +- Final Codex Security remediation scan: complete coverage, **0 findings**. + +## Residual limitations + +- Compact or dense PDFs can be underestimated locally and rejected by the provider. +- Exact tokenizer parity across providers is not claimed. +- No live request was made against every provider/context combination. +- Broader tokenizer calibration and document-ingestion architecture belong in follow-up work, not this PR. + +## Merge gate + +The local recommendation is **merge after remote completion**, provided: + +1. the final commits are pushed without overwriting concurrent remote work; +2. every review thread is answered or resolved against the new head; +3. fresh required CI and bot reviews are green; and +4. no new critical finding appears on the pushed head. + +Do not merge merely on the strength of the stale green checks attached to `9039a178c0`. + +## Execution reliability + +- Intended panel seats: 3 +- Final live PASS seats: 2 +- Degraded seats: 1 (service filter) +- Offline seats: 0 +- Final retries of degraded seat: 1 +- Fallback: primary reviewer completed the full diff and security reconciliation +- Code-discovery source: repository knowledge graph plus exact immutable Git diffs + +--- + +- `schema_version: 1` +- `mode: full` +- `panel_size: 3` +- `final_live_votes: 2` +- `final_pass_votes: 2` +- `degraded_seats: 1` diff --git a/research/security-reviews/PR 1196 Security Review.md b/research/security-reviews/PR 1196 Security Review.md new file mode 100644 index 0000000000..6799ffa154 --- /dev/null +++ b/research/security-reviews/PR 1196 Security Review.md @@ -0,0 +1,88 @@ +# PR 1196 Security Review + +- Repository: `AltimateAI/altimate-code` +- Pull request: [#1196](https://github.com/AltimateAI/altimate-code/pull/1196) +- Review dates: 2026-08-30 through 2026-08-31 +- Final code candidate: `908be9cabb2b552c56cbd86537fbccb86ea5e0b2` +- Scan mode: chained immutable branch-diff reviews +- Final coverage: complete +- Findings remaining on the final candidate: **0** + +## Outcome + +The final candidate removes local PDF parsing entirely. PDF media now uses a deliberately crude, parser-free estimate: + +```text +max(32,768 tokens, decoded inline payload bytes) +``` + +Remote URLs and provider file IDs receive the fixed 32,768-token allowance. The estimate is monotonic in locally observable payload size, ignores untrusted page metadata, and does not decompress or traverse PDF structure. The configured provider remains authoritative for exact tokenization and for unusually compact or dense documents. + +This is the selected product boundary, not an attempt at exact PDF accounting. A compact or dense PDF can still be underestimated locally and rejected by the provider. That residual is an acknowledged reliability limitation; it is not a local parser, authorization, confidentiality, integrity, or shared-service vulnerability. + +## Scan chain + +The review used immutable ranges so every material repair was independently reconciled. + +| Stage | Immutable range/candidate | Result | +| ---------------------------- | -------------------------------------------------- | ------------------------------------------------------------ | +| Initial PR review | current `main` through remote PR head `9039a178c0` | Complete coverage; no remaining finding in that candidate | +| Media hardening supplement | `9039a178c0..e37b7a974d` | One validated Low finding: raw PDF page-marker amplification | +| Marker fix | `ac7346e767` | Removed lexical page-marker trust | +| Structural parser experiment | `ac7346e767..858c7b1dab` | Two validated Low findings; experiment rejected | +| Parser-free remediation | `858c7b1dab..908be9cabb` | Complete coverage; **0 findings** | + +The final remediation scan was sealed once as scan `80591880-0a17-454c-b312-92c32a38f5ff`. Its authoritative result contains six reviewed surfaces, no deferred work, no open question, and zero findings. + +## Findings discovered and resolved + +### 1. Raw page markers could poison local admission + +An intermediate estimator scanned PDF bytes for lexical `/Type /Pages /Count` text. A marker in a comment, literal string, stream, or unreachable object could therefore inflate the local estimate and reject a valid user turn before transport. + +The fix removed page-marker scanning. Regression tests cover comment, string, stream, and unreachable-object variants across string, base64/data URL, `Uint8Array`, and `ArrayBuffer` payload shapes. + +### 2. In-process parsing could amplify compressed object streams + +The structural-parser experiment passed attacker-influenced PDF bytes to `pdf-lib` inside the synchronous request path. A bounded reproduction used a 51,430-byte PDF whose unreachable compressed object stream expanded to 50 MiB while loading and increased process RSS by roughly 88 MiB. The byte gate limited compressed input, not decompressed output, traversal work, or memory. + +The final candidate removes `pdf-lib`, `PDFDocument.load`, all structural page traversal, and the parser's transitive lockfile entries. Graph-augmented source search found no remaining runtime parser reference. + +### 3. The parser policy still disagreed with supported long-context requests + +The experiment coupled a 100-page fallback to a 500 KB parser ceiling. A valid 600-page PDF on a 1M-context request could sit just above that byte ceiling, receive only a byte-sized estimate, and preserve a large output reservation. Structural parsing therefore did not make the local estimate authoritative; it merely added a new resource boundary. + +The final policy removes the 100-page claim instead of replacing it with a larger parser. Exact page expansion is explicitly delegated to the provider. + +## Final trust and data flow + +Both production request paths use the same sequence: + +1. Finalize messages, tools, provider instructions, headers, and plugin-selected output reservation. +2. If no output reservation or credible limit exists, return without serializing the prompt. +3. Lazily estimate text, schemas, semantic media allowances, and decoded inline payload size. +4. Enforce a dedicated input limit when declared. +5. Clamp the output reservation against the shared context window with a safety margin. +6. Reconcile fixed reasoning budgets with the final reservation. +7. Send through the AI SDK or native transport. + +Codebase graph tracing found exactly two production callers of the centralized clamp: `session/llm.ts` and `session/llm/request.ts`. + +## Verification + +- Focused provider, AI-SDK stream, native request, and upstream bridge suites: **413 passed, 11 skipped, 1 existing todo, 0 failed**. +- Repository typecheck: **13/13 tasks successful**. +- Strict changed-file marker validation: passed. +- Required-marker inventory: **35/35**. +- Frozen lockfile install: **1,295 installs across 1,390 packages, no changes**. +- Targeted oxlint: **0 errors**; warnings remain repository debt. +- Prettier: all changed files pass except `provider/provider.ts` and `provider/transform.ts`; both fail identically at `origin/main`, so no unrelated whole-file rewrite was introduced. +- `git diff --check origin/main...HEAD`: passed. +- Final Codex Security remediation scan: complete, **0 findings**. + +## Operational caveats + +- TAC advisory was attempted once earlier in the PR workflow and was unavailable; it was not retried. +- No live provider request or full interactive UI replay was performed. +- Provider-side rejection remains possible for compact or unusually dense PDFs because the local estimate is intentionally crude. +- The remote PR still needs the final commits pushed, current review threads reconciled, and fresh CI/bot results before a merge recommendation. From c78e1a61b69bc42f05d600a185120127290c3683 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Mon, 31 Aug 2026 12:08:55 -0700 Subject: [PATCH 17/32] fix: align budgeting with final request shape --- packages/opencode/src/provider/transform.ts | 22 ++++++++++++-- packages/opencode/src/session/llm/request.ts | 4 ++- .../opencode/test/provider/transform.test.ts | 30 ++++++++++++++++++- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 6e4d3cba8b..0734a5eeeb 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -367,9 +367,27 @@ export namespace ProviderTransform { }) } - // altimate_change start — expose the pure media projection used before input-budget estimation + // altimate_change start — expose the pure request projection used before input-budget estimation export function messagesForInputEstimate(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] { - return unsupportedParts(msgs, model) + const projected = unsupportedParts(msgs, model) + const mistral = + model.providerID === "mistral" || + model.api.id.toLowerCase().includes("mistral") || + model.api.id.toLowerCase().includes("devstral") + if (!mistral) return projected + + // normalizeMessages inserts this bridge before transport because Mistral rejects a tool + // message followed directly by a user message. Estimate the same synthetic messages without + // mutating the history that the real transform will process later. + const result: ModelMessage[] = [] + for (let index = 0; index < projected.length; index++) { + const message = projected[index] + result.push(message) + if (message.role === "tool" && projected[index + 1]?.role === "user") { + result.push({ role: "assistant", content: [{ type: "text", text: "Done." }] }) + } + } + return result } // altimate_change end diff --git a/packages/opencode/src/session/llm/request.ts b/packages/opencode/src/session/llm/request.ts index edbd7befe8..992a4ef1ac 100644 --- a/packages/opencode/src/session/llm/request.ts +++ b/packages/opencode/src/session/llm/request.ts @@ -210,7 +210,9 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre }), inputTokens: () => estimateInputTokens({ - system, + // OAuth carries this prompt in instructions; workflows deliberately omit it. Count the + // generated system prompt only when this request prepends it to the outgoing messages. + system: isOpenaiOauth || input.isWorkflow ? [] : system, messages: ProviderTransform.messagesForInputEstimate(input.messages, input.model), tools: sortedTools, instructions: params.options.instructions, diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index c7262964a0..ef452966c7 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -5162,6 +5162,27 @@ describe("output token budget", () => { expect((messages[0].content as Array<{ type: string }>).every((part) => part.type !== "text")).toBeTrue() }) + test("projects Mistral's synthetic tool-to-user bridge before estimation", () => { + const model = { + ...createWindowModel({ context: 65_536, output: 16_384 }), + providerID: "mistral", + api: { id: "mistral-large", url: "https://example.invalid/v1", npm: "@ai-sdk/mistral" }, + } + const messages = Array.from({ length: 64 }, (_, index) => [ + { role: "tool" as const, content: [] }, + { role: "user" as const, content: `continue ${index}` }, + ]).flat() as ModelMessage[] + + const projected = ProviderTransform.messagesForInputEstimate(messages, model) + const normalized = ProviderTransform.message(structuredClone(messages), model, {}) + + expect(projected).toEqual(normalized) + expect(projected).toHaveLength(messages.length + 64) + expect(estimateInputTokens({ system: [], messages: projected })).toBeGreaterThan( + estimateInputTokens({ system: [], messages }) + 1_000, + ) + }) + test("counts repeated shared tool objects while terminating true cycles", () => { const sharedTool = tool({ description: "shared schema documentation ".repeat(1_200), @@ -5306,6 +5327,7 @@ describe("LLMRequestPrep.prepare - output token reservation", () => { readonly providerOptions?: Record readonly modelHeaders?: Record readonly chatHeaders?: Record + readonly isWorkflow?: boolean } = {}, ) => Effect.runPromise( @@ -5349,7 +5371,7 @@ describe("LLMRequestPrep.prepare - output token reservation", () => { init: () => Effect.void, } as any, flags: { outputTokenMax: 32_000, client: "test" } as any, - isWorkflow: false, + isWorkflow: overrides.isWorkflow ?? false, }), ) @@ -5377,6 +5399,12 @@ describe("LLMRequestPrep.prepare - output token reservation", () => { expect(result.params.maxOutputTokens).toBe(16_384) }) + test("does not budget a generated system prompt omitted from workflow requests", async () => { + const result = await run(largePrompt, { isWorkflow: true }) + expect(result.messages).toEqual(messages) + expect(result.params.maxOutputTokens).toBe(16_384) + }) + test("uses provider then model then chat header precedence at the request boundary", async () => { const beta = "context-1m-2025-08-07" const disabled = "interleaved-thinking-2025-05-14" From 071f4dc782e70bb4a0f63397902a4285d0156903 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Mon, 31 Aug 2026 12:21:10 -0700 Subject: [PATCH 18/32] fix: count duplicate instruction fields --- packages/opencode/src/provider/output-token-budget.ts | 2 +- packages/opencode/test/provider/transform.test.ts | 8 ++++++++ packages/opencode/test/upstream/bridge-merge-e2e.test.ts | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/provider/output-token-budget.ts b/packages/opencode/src/provider/output-token-budget.ts index 1de35047e9..e878908861 100644 --- a/packages/opencode/src/provider/output-token-budget.ts +++ b/packages/opencode/src/provider/output-token-budget.ts @@ -335,7 +335,7 @@ export function estimateInputTokens(input: { total += estimateTextTokens(tools.text) } - if (input.instructions !== undefined && input.instructions !== system) { + if (input.instructions !== undefined) { const serialized = serializeForEstimate(input.instructions) total += estimateTextTokens(serialized.text) + serialized.mediaTokens } diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index ef452966c7..485a5b8a8b 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -4819,6 +4819,14 @@ describe("output token budget", () => { expect(complete).toBeGreaterThan(base + 1_000) }) + test("counts identical system and instructions as separate wire occurrences", () => { + const prompt = "same provider instruction ".repeat(1_000) + const systemOnly = estimateInputTokens({ system: [prompt], messages: [] }) + const both = estimateInputTokens({ system: [prompt], messages: [], instructions: prompt }) + + expect(both).toBeGreaterThan(systemOnly * 1.8) + }) + test("does not tokenize encoded media bytes as literal prompt text", () => { const estimated = estimateInputTokens({ system: [], diff --git a/packages/opencode/test/upstream/bridge-merge-e2e.test.ts b/packages/opencode/test/upstream/bridge-merge-e2e.test.ts index 7a5d8b4a0d..d493c3eba6 100644 --- a/packages/opencode/test/upstream/bridge-merge-e2e.test.ts +++ b/packages/opencode/test/upstream/bridge-merge-e2e.test.ts @@ -529,7 +529,7 @@ describe("E2E: chat.params maxOutputTokens hook (cycle 6)", () => { // altimate_change start — the plugin result is now clamped before streamText receives it expect(content).toMatch(/requested:\s*params\.maxOutputTokens/) expect(content).toMatch(/const maxOutputTokens = clampOutputTokens/) - expect(content).toMatch(/return streamText\([\s\S]*?maxOutputTokens,/) + expect(content).toMatch(/return streamText\([\s\S]*?(? Date: Mon, 31 Aug 2026 12:28:13 -0700 Subject: [PATCH 19/32] docs: finalize PR 1196 review record --- .../code-reviews/PR 1196 Consensus Review.md | 41 +++++++++++-------- .../PR 1196 Security Review.md | 16 ++++---- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/research/code-reviews/PR 1196 Consensus Review.md b/research/code-reviews/PR 1196 Consensus Review.md index 1fd8b2c79b..e58230e599 100644 --- a/research/code-reviews/PR 1196 Consensus Review.md +++ b/research/code-reviews/PR 1196 Consensus Review.md @@ -3,10 +3,10 @@ - Repository: `AltimateAI/altimate-code` - Pull request: [#1196](https://github.com/AltimateAI/altimate-code/pull/1196) - Review dates: 2026-08-30 through 2026-08-31 -- Final code candidate: `908be9cabb2b552c56cbd86537fbccb86ea5e0b2` +- Final code candidate: `071f4dc782e70bb4a0f63397902a4285d0156903` - Mode: full Council review plus final independent remediation pass - Local verdict: **PASS** -- Remote status: final commits still need to be pushed and fresh CI/bot review must finish +- Remote gate: the final candidate must be pushed and fresh CI/bot review must finish ## Decision @@ -20,7 +20,7 @@ PDF accounting is intentionally approximate. The user selected a parser-free pol PDF allowance = max(32,768, decoded inline payload bytes) ``` -Remote URLs and provider file IDs receive the fixed allowance. Exact page expansion and tokenization remain provider-authoritative. The Council did not require exact PDF parsing because adding a parser would create a new document-processing/resource boundary without making provider token accounting exact. +Remote references and provider file IDs receive the fixed allowance when the part is identifiable as a PDF. An untyped provider file ID cannot be classified locally and receives the generic 16,384-token file allowance instead. Exact page expansion and tokenization remain provider-authoritative. The Council did not require exact PDF parsing because adding a parser would create a new document-processing/resource boundary without making provider token accounting exact. ## Original Council gate @@ -44,7 +44,11 @@ Post-Council review found and repaired: - small-model output floors above the model's own reservation; - safety-margin loss near the context boundary; - non-JSON provider-option loss during reasoning reconciliation; -- raw PDF page-marker trust. +- raw PDF page-marker trust; +- provider-normalized Mistral/Devstral bridge messages missing from the estimate; +- generated system prompts counted even when workflows omit them or OAuth routes them through instructions; +- identical system and instruction values being deduplicated despite occupying two wire fields; and +- a static bridge assertion that could match the unclamped property-access form. Each repair was centralized in the shared output-budget module or the existing pure media projection, then exercised through both request boundaries. @@ -61,40 +65,45 @@ Batching the same parser would not remove its decompression/traversal boundary. ## Final independent remediation review -Two independent live Council seats re-reviewed exact head `908be9cabb` after the parser removal. +Two independent live Council seats re-reviewed the request-shape correction at `c78e1a61b6`, then received the final `c78e1a61b6..071f4dc782` delta after fresh bot findings. The final delta makes the estimator more conservative and does not change PDF behavior. ### Feynman seat — PASS -- Verified exact final head. +- Verified exact final code head `071f4dc782`. - Confirmed `pdf-lib`, async parsing, page regex/policy, and transitive lock entries are absent. - Confirmed lazy estimation is not evaluated when `maxOutputTokens` is omitted. -- Confirmed both request boundaries clamp after headers, tools, instructions, and media projection are finalized. -- Re-ran provider, native, stream, typecheck, diff, and strict marker checks successfully. +- Confirmed Mistral/Devstral projection matches the transport normalizer without mutating history. +- Confirmed workflow/OAuth prompt routing matches the fields actually sent. +- Confirmed identical system/instruction values are counted as two wire occurrences while omitted system fields remain excluded. +- Confirmed the tightened bridge regex rejects `params.maxOutputTokens` and accepts the clamped shorthand. +- Re-ran provider, typecheck, diff, and bridge checks successfully. ### Musashi seat — PASS -- Confirmed the parser experiment is cleanly reverted. -- Confirmed the final code differs from the last pre-parser safe candidate only in the explicit parser-free comment and test naming. -- Confirmed synchronous lazy estimation and parity across both callers. -- Confirmed dependency cleanup, focused tests, typecheck, and diff checks pass. +- Verified exact final code head `071f4dc782`. +- Confirmed the parser experiment remains cleanly reverted and no PDF dependency returned. +- Confirmed synchronous lazy estimation, linear Mistral projection, and parity across both callers. +- Confirmed identical wire fields are counted separately while OAuth/workflow omissions are not double-counted. +- Confirmed the fixed-width bridge lookbehind runs correctly under Bun. +- Confirmed focused tests, typecheck, and diff checks pass. ### Degraded seat The original chairman seat was rejected twice by the service safety filter because its retained conversation context included the earlier parser stress case. It produced no contrary code finding on the final head. The thread limit prevented replacing that retained seat with a new fourth thread. -Consensus therefore rests on two independent live PASS votes, the primary review, the complete changed-file inspection, and a sealed zero-finding Codex Security remediation scan. The degraded seat is disclosed rather than silently counted as agreement. +Consensus therefore rests on two independent live PASS votes on exact final code head `071f4dc782`, the primary review, the complete changed-file inspection, and sealed zero-finding Codex Security scans. The degraded seat is disclosed rather than silently counted as agreement. ## Final verification -- Focused provider, AI-SDK stream, native request, and upstream bridge suites: **413 passed, 11 skipped, 1 existing todo, 0 failed**. +- Focused provider, AI-SDK stream, native request, and upstream bridge suites: **416 passed, 11 skipped, 1 existing todo, 0 failed**. - Repository typecheck: **13/13 successful**. - Strict changed-file marker validation: passed. - Required-marker inventory: **35/35**. - Frozen lockfile install: **1,295 installs across 1,390 packages, no changes**. -- Targeted oxlint: **212 warnings, 0 errors**. +- Targeted oxlint on the final supplemental files: **161 warnings, 0 errors**; warnings are existing repository debt. - Prettier: all changed files pass except `provider/provider.ts` and `provider/transform.ts`; both fail identically on `origin/main`. - `git diff --check origin/main...HEAD`: passed. -- Final Codex Security remediation scan: complete coverage, **0 findings**. +- Final request-shape Codex Security scans `1dcbce9e-587e-4495-94ff-6d3291e5e1d6` and `bfa1cc4a-7d87-463d-8fc9-822e1624f8b1`: complete coverage, **0 findings**. ## Residual limitations diff --git a/research/security-reviews/PR 1196 Security Review.md b/research/security-reviews/PR 1196 Security Review.md index 6799ffa154..e71ab93f18 100644 --- a/research/security-reviews/PR 1196 Security Review.md +++ b/research/security-reviews/PR 1196 Security Review.md @@ -3,7 +3,7 @@ - Repository: `AltimateAI/altimate-code` - Pull request: [#1196](https://github.com/AltimateAI/altimate-code/pull/1196) - Review dates: 2026-08-30 through 2026-08-31 -- Final code candidate: `908be9cabb2b552c56cbd86537fbccb86ea5e0b2` +- Final code candidate: `071f4dc782e70bb4a0f63397902a4285d0156903` - Scan mode: chained immutable branch-diff reviews - Final coverage: complete - Findings remaining on the final candidate: **0** @@ -16,7 +16,7 @@ The final candidate removes local PDF parsing entirely. PDF media now uses a del max(32,768 tokens, decoded inline payload bytes) ``` -Remote URLs and provider file IDs receive the fixed 32,768-token allowance. The estimate is monotonic in locally observable payload size, ignores untrusted page metadata, and does not decompress or traverse PDF structure. The configured provider remains authoritative for exact tokenization and for unusually compact or dense documents. +Remote references and provider file IDs receive the fixed 32,768-token allowance when the part is identifiable as a PDF. An untyped provider file ID cannot be classified locally and receives the generic 16,384-token file allowance instead. The estimate is monotonic in locally observable payload size, ignores untrusted page metadata, and does not decompress or traverse PDF structure. The configured provider remains authoritative for exact tokenization and for unusually compact or dense documents. This is the selected product boundary, not an attempt at exact PDF accounting. A compact or dense PDF can still be underestimated locally and rejected by the provider. That residual is an acknowledged reliability limitation; it is not a local parser, authorization, confidentiality, integrity, or shared-service vulnerability. @@ -31,8 +31,10 @@ The review used immutable ranges so every material repair was independently reco | Marker fix | `ac7346e767` | Removed lexical page-marker trust | | Structural parser experiment | `ac7346e767..858c7b1dab` | Two validated Low findings; experiment rejected | | Parser-free remediation | `858c7b1dab..908be9cabb` | Complete coverage; **0 findings** | +| Final request-shape fixes | `5e04c7885d..c78e1a61b6` | Complete coverage; **0 findings** | +| Instruction occurrence fix | `c78e1a61b6..071f4dc782` | Complete coverage; **0 findings** | -The final remediation scan was sealed once as scan `80591880-0a17-454c-b312-92c32a38f5ff`. Its authoritative result contains six reviewed surfaces, no deferred work, no open question, and zero findings. +The parser-free remediation scan was sealed once as scan `80591880-0a17-454c-b312-92c32a38f5ff`. The final request-shape scans were sealed once each as `1dcbce9e-587e-4495-94ff-6d3291e5e1d6` and `bfa1cc4a-7d87-463d-8fc9-822e1624f8b1`. Their authoritative results contain no deferred work, no open question, and zero findings. ## Findings discovered and resolved @@ -60,7 +62,7 @@ Both production request paths use the same sequence: 1. Finalize messages, tools, provider instructions, headers, and plugin-selected output reservation. 2. If no output reservation or credible limit exists, return without serializing the prompt. -3. Lazily estimate text, schemas, semantic media allowances, and decoded inline payload size. +3. Lazily estimate text, schemas, semantic media allowances, decoded inline payload size, and every system/instruction wire occurrence. 4. Enforce a dedicated input limit when declared. 5. Clamp the output reservation against the shared context window with a safety margin. 6. Reconcile fixed reasoning budgets with the final reservation. @@ -70,7 +72,7 @@ Codebase graph tracing found exactly two production callers of the centralized c ## Verification -- Focused provider, AI-SDK stream, native request, and upstream bridge suites: **413 passed, 11 skipped, 1 existing todo, 0 failed**. +- Focused provider, AI-SDK stream, native request, and upstream bridge suites: **416 passed, 11 skipped, 1 existing todo, 0 failed**. - Repository typecheck: **13/13 tasks successful**. - Strict changed-file marker validation: passed. - Required-marker inventory: **35/35**. @@ -78,11 +80,11 @@ Codebase graph tracing found exactly two production callers of the centralized c - Targeted oxlint: **0 errors**; warnings remain repository debt. - Prettier: all changed files pass except `provider/provider.ts` and `provider/transform.ts`; both fail identically at `origin/main`, so no unrelated whole-file rewrite was introduced. - `git diff --check origin/main...HEAD`: passed. -- Final Codex Security remediation scan: complete, **0 findings**. +- Final request-shape Codex Security scans: complete coverage, **0 findings**. ## Operational caveats - TAC advisory was attempted once earlier in the PR workflow and was unavailable; it was not retried. - No live provider request or full interactive UI replay was performed. - Provider-side rejection remains possible for compact or unusually dense PDFs because the local estimate is intentionally crude. -- The remote PR still needs the final commits pushed, current review threads reconciled, and fresh CI/bot results before a merge recommendation. +- The remote PR must receive the final commits, reconcile every current review thread, and pass fresh CI/bot review before merge. From aab3dac85008fd9a21919741021d3be3848cefbe Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Mon, 31 Aug 2026 12:46:18 -0700 Subject: [PATCH 20/32] fix: enforce final edge-case budgets --- .../src/provider/output-token-budget.ts | 4 +-- packages/opencode/src/provider/transform.ts | 3 ++- .../opencode/test/provider/transform.test.ts | 25 ++++++++++++++----- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/provider/output-token-budget.ts b/packages/opencode/src/provider/output-token-budget.ts index e878908861..2965dbdc7c 100644 --- a/packages/opencode/src/provider/output-token-budget.ts +++ b/packages/opencode/src/provider/output-token-budget.ts @@ -359,7 +359,7 @@ export function clampOutputTokens(input: { const context = input.context ?? input.model.limit.context const inputLimit = input.model.limit.input - if ((!context || context <= OUTPUT_TOKEN_FLOOR) && (!inputLimit || inputLimit <= 0)) return requested + if ((!context || context <= 0) && (!inputLimit || inputLimit <= 0)) return requested const inputTokens = resolveInputTokens(input.inputTokens) if (!Number.isFinite(inputTokens) || inputTokens <= 0) return requested @@ -374,7 +374,7 @@ export function clampOutputTokens(input: { margin, }) } - if (!context || context <= OUTPUT_TOKEN_FLOOR) return requested + if (!context || context <= 0) return requested if (inputTokens + requested + margin <= context) return requested // Do not reject a model for failing to reach a floor above its own reservation. diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 0734a5eeeb..dc3c6e7863 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -335,7 +335,8 @@ export namespace ProviderTransform { // Check for empty base64 image data if (part.type === "image") { // altimate_change start — support every valid image payload form and case - const imageStr = typeof part.image === "string" ? part.image : undefined + const imageStr = + typeof part.image === "string" ? part.image : part.image instanceof URL ? part.image.href : undefined if (imageStr && /^data:/i.test(imageStr)) { const match = imageStr.match(/^data:([^;]+);base64,(.*)$/i) if (match && (!match[2] || match[2].length === 0)) { diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 485a5b8a8b..fa30640c4b 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -1937,6 +1937,22 @@ describe("ProviderTransform.message - empty image handling", () => { }) }) + test("should replace an empty base64 image wrapped in a URL object", () => { + const msgs = [ + { + role: "user", + content: [{ type: "image", image: new URL("data:image/png;base64,") }], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, mockModel, {}) + + expect(result[0].content[0]).toEqual({ + type: "text", + text: "ERROR: Image file is empty or corrupted. Please provide a valid image.", + }) + }) + test("should keep valid base64 images unchanged", () => { const validBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" @@ -4773,12 +4789,9 @@ describe("output token budget", () => { expect(() => clampOutputTokens({ model, requested: 512, inputTokens: 8_000 })).toThrow(OutputTokenBudgetError) }) - test("does not clamp a window too small to hold even a floor-sized completion", () => { - // A declared window this small is a placeholder or a test fixture, not a real limit. Failing - // the request client-side on numbers we do not believe would be worse than letting the - // provider answer, so the guard stays out of the way. - const model = createWindowModel({ context: 20, output: 10 }) - expect(clampOutputTokens({ model, requested: 10, inputTokens: 11 })).toBe(10) + test("enforces real context windows at or below the default output floor", () => { + const model = createWindowModel({ context: 512, output: 512 }) + expect(() => clampOutputTokens({ model, requested: 512, inputTokens: 1 })).toThrow(OutputTokenBudgetError) }) test("passes an omitted reservation through untouched", () => { From e3ca59741af3575189a3c0c350286ebef1ca40e2 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Mon, 31 Aug 2026 12:55:36 -0700 Subject: [PATCH 21/32] docs: record final PR 1196 edge-case review --- .../code-reviews/PR 1196 Consensus Review.md | 25 ++++++++++++------- .../PR 1196 Security Review.md | 11 +++++--- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/research/code-reviews/PR 1196 Consensus Review.md b/research/code-reviews/PR 1196 Consensus Review.md index e58230e599..40b7023411 100644 --- a/research/code-reviews/PR 1196 Consensus Review.md +++ b/research/code-reviews/PR 1196 Consensus Review.md @@ -3,7 +3,7 @@ - Repository: `AltimateAI/altimate-code` - Pull request: [#1196](https://github.com/AltimateAI/altimate-code/pull/1196) - Review dates: 2026-08-30 through 2026-08-31 -- Final code candidate: `071f4dc782e70bb4a0f63397902a4285d0156903` +- Final code candidate: `aab3dac85008fd9a21919741021d3be3848cefbe` - Mode: full Council review plus final independent remediation pass - Local verdict: **PASS** - Remote gate: the final candidate must be pushed and fresh CI/bot review must finish @@ -48,7 +48,9 @@ Post-Council review found and repaired: - provider-normalized Mistral/Devstral bridge messages missing from the estimate; - generated system prompts counted even when workflows omit them or OAuth routes them through instructions; - identical system and instruction values being deduplicated despite occupying two wire fields; and -- a static bridge assertion that could match the unclamped property-access form. +- a static bridge assertion that could match the unclamped property-access form; +- empty base64 image data wrapped in a `URL` object escaping unsupported-media projection; and +- positive sub-floor context windows being treated as placeholder metadata. Each repair was centralized in the shared output-budget module or the existing pure media projection, then exercised through both request boundaries. @@ -65,45 +67,50 @@ Batching the same parser would not remove its decompression/traversal boundary. ## Final independent remediation review -Two independent live Council seats re-reviewed the request-shape correction at `c78e1a61b6`, then received the final `c78e1a61b6..071f4dc782` delta after fresh bot findings. The final delta makes the estimator more conservative and does not change PDF behavior. +Two independent live Council seats re-reviewed the request-shape correction at `c78e1a61b6`, then reviewed both follow-up deltas through exact final head `aab3dac850`. The final deltas make the estimator more conservative and do not change PDF behavior. ### Feynman seat — PASS -- Verified exact final code head `071f4dc782`. +- Verified exact final code head `aab3dac850`. - Confirmed `pdf-lib`, async parsing, page regex/policy, and transitive lock entries are absent. - Confirmed lazy estimation is not evaluated when `maxOutputTokens` is omitted. - Confirmed Mistral/Devstral projection matches the transport normalizer without mutating history. - Confirmed workflow/OAuth prompt routing matches the fields actually sent. - Confirmed identical system/instruction values are counted as two wire occurrences while omitted system fields remain excluded. - Confirmed the tightened bridge regex rejects `params.maxOutputTokens` and accepts the clamped shorthand. +- Confirmed empty base64 images wrapped in `URL` objects become explanatory text while valid strings, byte buffers, and ordinary URLs remain unchanged. +- Confirmed undefined and zero context limits bypass lazily, while every positive limit—including 1, 512, and 1,024—is enforced. +- Confirmed the requested-capped floor accepts the exact 523-token boundary and rejects 522 tokens for the reproduced request. - Re-ran provider, typecheck, diff, and bridge checks successfully. ### Musashi seat — PASS -- Verified exact final code head `071f4dc782`. +- Verified exact final code head `aab3dac850`. - Confirmed the parser experiment remains cleanly reverted and no PDF dependency returned. - Confirmed synchronous lazy estimation, linear Mistral projection, and parity across both callers. - Confirmed identical wire fields are counted separately while OAuth/workflow omissions are not double-counted. - Confirmed the fixed-width bridge lookbehind runs correctly under Bun. +- Confirmed `URL.href` inspection matches the AI SDK URL contract without changing binary media handling. +- Confirmed every positive context window is enforced while absent/non-positive contexts retain lazy bypass behavior. - Confirmed focused tests, typecheck, and diff checks pass. ### Degraded seat The original chairman seat was rejected twice by the service safety filter because its retained conversation context included the earlier parser stress case. It produced no contrary code finding on the final head. The thread limit prevented replacing that retained seat with a new fourth thread. -Consensus therefore rests on two independent live PASS votes on exact final code head `071f4dc782`, the primary review, the complete changed-file inspection, and sealed zero-finding Codex Security scans. The degraded seat is disclosed rather than silently counted as agreement. +Consensus therefore rests on two independent live PASS votes on exact final code head `aab3dac850`, the primary review, the complete changed-file inspection, and sealed zero-finding Codex Security scans. The degraded seat is disclosed rather than silently counted as agreement. ## Final verification -- Focused provider, AI-SDK stream, native request, and upstream bridge suites: **416 passed, 11 skipped, 1 existing todo, 0 failed**. +- Focused provider, AI-SDK stream, native request, and upstream bridge suites: **417 passed, 11 skipped, 1 existing todo, 0 failed**. - Repository typecheck: **13/13 successful**. - Strict changed-file marker validation: passed. - Required-marker inventory: **35/35**. - Frozen lockfile install: **1,295 installs across 1,390 packages, no changes**. -- Targeted oxlint on the final supplemental files: **161 warnings, 0 errors**; warnings are existing repository debt. +- Targeted oxlint on the final supplemental files: **173 warnings, 0 errors**; warnings are existing repository debt. - Prettier: all changed files pass except `provider/provider.ts` and `provider/transform.ts`; both fail identically on `origin/main`. - `git diff --check origin/main...HEAD`: passed. -- Final request-shape Codex Security scans `1dcbce9e-587e-4495-94ff-6d3291e5e1d6` and `bfa1cc4a-7d87-463d-8fc9-822e1624f8b1`: complete coverage, **0 findings**. +- Final request-shape and edge-case Codex Security scans `1dcbce9e-587e-4495-94ff-6d3291e5e1d6`, `bfa1cc4a-7d87-463d-8fc9-822e1624f8b1`, and `19f139b0-8c4c-48cc-adca-a60d41920664`: complete coverage, **0 findings**. ## Residual limitations diff --git a/research/security-reviews/PR 1196 Security Review.md b/research/security-reviews/PR 1196 Security Review.md index e71ab93f18..1c63f0d862 100644 --- a/research/security-reviews/PR 1196 Security Review.md +++ b/research/security-reviews/PR 1196 Security Review.md @@ -3,7 +3,7 @@ - Repository: `AltimateAI/altimate-code` - Pull request: [#1196](https://github.com/AltimateAI/altimate-code/pull/1196) - Review dates: 2026-08-30 through 2026-08-31 -- Final code candidate: `071f4dc782e70bb4a0f63397902a4285d0156903` +- Final code candidate: `aab3dac85008fd9a21919741021d3be3848cefbe` - Scan mode: chained immutable branch-diff reviews - Final coverage: complete - Findings remaining on the final candidate: **0** @@ -33,8 +33,9 @@ The review used immutable ranges so every material repair was independently reco | Parser-free remediation | `858c7b1dab..908be9cabb` | Complete coverage; **0 findings** | | Final request-shape fixes | `5e04c7885d..c78e1a61b6` | Complete coverage; **0 findings** | | Instruction occurrence fix | `c78e1a61b6..071f4dc782` | Complete coverage; **0 findings** | +| Final edge-case hardening | `a279303720..aab3dac850` | Complete coverage; **0 findings** | -The parser-free remediation scan was sealed once as scan `80591880-0a17-454c-b312-92c32a38f5ff`. The final request-shape scans were sealed once each as `1dcbce9e-587e-4495-94ff-6d3291e5e1d6` and `bfa1cc4a-7d87-463d-8fc9-822e1624f8b1`. Their authoritative results contain no deferred work, no open question, and zero findings. +The parser-free remediation scan was sealed once as scan `80591880-0a17-454c-b312-92c32a38f5ff`. The final request-shape and edge-case scans were sealed once each as `1dcbce9e-587e-4495-94ff-6d3291e5e1d6`, `bfa1cc4a-7d87-463d-8fc9-822e1624f8b1`, and `19f139b0-8c4c-48cc-adca-a60d41920664`. Their authoritative results contain no deferred work, no open question, and zero findings. ## Findings discovered and resolved @@ -56,6 +57,10 @@ The experiment coupled a 100-page fallback to a 500 KB parser ceiling. A valid 6 The final policy removes the 100-page claim instead of replacing it with a larger parser. Exact page expansion is explicitly delegated to the provider. +### 4. Final boundary cases were made conservative + +The final supplemental review found that an empty base64 image represented as a `URL` object could escape unsupported-media projection, and that positive context windows at or below 1,024 tokens were treated as placeholder metadata. The final candidate inspects `URL.href` using the same data-URL rule as strings and enforces every positive context limit. Absent and non-positive context metadata still bypasses lazily, so estimates are not evaluated when no credible limit exists. + ## Final trust and data flow Both production request paths use the same sequence: @@ -72,7 +77,7 @@ Codebase graph tracing found exactly two production callers of the centralized c ## Verification -- Focused provider, AI-SDK stream, native request, and upstream bridge suites: **416 passed, 11 skipped, 1 existing todo, 0 failed**. +- Focused provider, AI-SDK stream, native request, and upstream bridge suites: **417 passed, 11 skipped, 1 existing todo, 0 failed**. - Repository typecheck: **13/13 tasks successful**. - Strict changed-file marker validation: passed. - Required-marker inventory: **35/35**. From 56dbc7e9b1f81fa226821754bf44005d8d08715b Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Mon, 31 Aug 2026 13:09:37 -0700 Subject: [PATCH 22/32] fix: scale safety margins for small limits --- .../src/provider/output-token-budget.ts | 27 ++++++++++++------- .../opencode/test/provider/transform.test.ts | 11 ++++++-- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/packages/opencode/src/provider/output-token-budget.ts b/packages/opencode/src/provider/output-token-budget.ts index 2965dbdc7c..5638715e26 100644 --- a/packages/opencode/src/provider/output-token-budget.ts +++ b/packages/opencode/src/provider/output-token-budget.ts @@ -347,6 +347,12 @@ function resolveInputTokens(value: number | (() => number)): number { return typeof value === "function" ? value() : value } +/** Keep estimator drift proportional when a credible limit is smaller than the default margin. */ +function safetyMargin(inputTokens: number, limit: number): number { + const proportionalMinimum = Math.max(1, Math.ceil(limit * CLAMP_MARGIN_FRACTION)) + return Math.max(Math.ceil(inputTokens * CLAMP_MARGIN_FRACTION), Math.min(CLAMP_MARGIN_MIN, proportionalMinimum)) +} + /** Clamp a completion reservation so estimated input, margin, and output fit the effective window. */ export function clampOutputTokens(input: { readonly model: Provider.Model @@ -364,17 +370,20 @@ export function clampOutputTokens(input: { const inputTokens = resolveInputTokens(input.inputTokens) if (!Number.isFinite(inputTokens) || inputTokens <= 0) return requested - const margin = Math.max(CLAMP_MARGIN_MIN, Math.ceil(inputTokens * CLAMP_MARGIN_FRACTION)) - if (inputLimit && inputLimit > 0 && inputTokens + margin > inputLimit) { - throw new InputTokenBudgetError({ - modelID: input.model.id, - providerID: input.model.providerID, - inputTokens, - inputLimit, - margin, - }) + if (inputLimit && inputLimit > 0) { + const margin = safetyMargin(inputTokens, inputLimit) + if (inputTokens + margin > inputLimit) { + throw new InputTokenBudgetError({ + modelID: input.model.id, + providerID: input.model.providerID, + inputTokens, + inputLimit, + margin, + }) + } } if (!context || context <= 0) return requested + const margin = safetyMargin(inputTokens, context) if (inputTokens + requested + margin <= context) return requested // Do not reject a model for failing to reach a floor above its own reservation. diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index fa30640c4b..c59bb28b34 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -4782,18 +4782,25 @@ describe("output token budget", () => { test("never demands more headroom than the model's own output reservation", () => { const model = createWindowModel({ context: 8_192, output: 512 }) - expect(clampOutputTokens({ model, requested: 512, inputTokens: 7_168 })).toBe(512) + expect(clampOutputTokens({ model, requested: 512, inputTokens: 7_516 })).toBe(512) // One token more would require discarding the estimator margin. Refuse instead of sending an // exact-fill request that is likely to reproduce the provider context error. - expect(() => clampOutputTokens({ model, requested: 512, inputTokens: 7_169 })).toThrow(OutputTokenBudgetError) + expect(() => clampOutputTokens({ model, requested: 512, inputTokens: 7_517 })).toThrow(OutputTokenBudgetError) expect(() => clampOutputTokens({ model, requested: 512, inputTokens: 8_000 })).toThrow(OutputTokenBudgetError) }) test("enforces real context windows at or below the default output floor", () => { const model = createWindowModel({ context: 512, output: 512 }) + expect(clampOutputTokens({ model, requested: 1, inputTokens: 1 })).toBe(1) expect(() => clampOutputTokens({ model, requested: 512, inputTokens: 1 })).toThrow(OutputTokenBudgetError) }) + test("scales the safety margin for a small dedicated input ceiling", () => { + const model = createWindowModel({ context: 200_000, input: 512, output: 1 }) + expect(clampOutputTokens({ model, requested: 1, inputTokens: 501 })).toBe(1) + expect(() => clampOutputTokens({ model, requested: 1, inputTokens: 502 })).toThrow(InputTokenBudgetError) + }) + test("passes an omitted reservation through untouched", () => { // Codex and GitHub Copilot deliberately send no maxOutputTokens. const model = createWindowModel({ context: 65_536, output: 16_384 }) From d663c49b74165bf90a4e0e20fcd155efd2a253ac Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Mon, 31 Aug 2026 13:16:14 -0700 Subject: [PATCH 23/32] docs: record final small-limit review --- .../code-reviews/PR 1196 Consensus Review.md | 29 ++++++++++++------- .../PR 1196 Security Review.md | 11 +++++-- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/research/code-reviews/PR 1196 Consensus Review.md b/research/code-reviews/PR 1196 Consensus Review.md index 40b7023411..faac23a7b6 100644 --- a/research/code-reviews/PR 1196 Consensus Review.md +++ b/research/code-reviews/PR 1196 Consensus Review.md @@ -3,7 +3,7 @@ - Repository: `AltimateAI/altimate-code` - Pull request: [#1196](https://github.com/AltimateAI/altimate-code/pull/1196) - Review dates: 2026-08-30 through 2026-08-31 -- Final code candidate: `aab3dac85008fd9a21919741021d3be3848cefbe` +- Final code candidate: `56dbc7e9b1f81fa226821754bf44005d8d08715b` - Mode: full Council review plus final independent remediation pass - Local verdict: **PASS** - Remote gate: the final candidate must be pushed and fresh CI/bot review must finish @@ -47,10 +47,11 @@ Post-Council review found and repaired: - raw PDF page-marker trust; - provider-normalized Mistral/Devstral bridge messages missing from the estimate; - generated system prompts counted even when workflows omit them or OAuth routes them through instructions; -- identical system and instruction values being deduplicated despite occupying two wire fields; and +- identical system and instruction values being deduplicated despite occupying two wire fields; - a static bridge assertion that could match the unclamped property-access form; -- empty base64 image data wrapped in a `URL` object escaping unsupported-media projection; and -- positive sub-floor context windows being treated as placeholder metadata. +- empty base64 image data wrapped in a `URL` object escaping unsupported-media projection; +- positive sub-floor context windows being treated as placeholder metadata; and +- the fixed 512-token safety margin consuming an entire small but valid context or input limit. Each repair was centralized in the shared output-budget module or the existing pure media projection, then exercised through both request boundaries. @@ -67,11 +68,11 @@ Batching the same parser would not remove its decompression/traversal boundary. ## Final independent remediation review -Two independent live Council seats re-reviewed the request-shape correction at `c78e1a61b6`, then reviewed both follow-up deltas through exact final head `aab3dac850`. The final deltas make the estimator more conservative and do not change PDF behavior. +Two independent live Council seats re-reviewed the request-shape correction at `c78e1a61b6`, then reviewed all follow-up deltas through exact final head `56dbc7e9b1`. The final delta scales the safety margin against each authoritative small limit while retaining the 512-token minimum for normal windows. It does not change PDF behavior. ### Feynman seat — PASS -- Verified exact final code head `aab3dac850`. +- Verified exact final code head `56dbc7e9b1`. - Confirmed `pdf-lib`, async parsing, page regex/policy, and transitive lock entries are absent. - Confirmed lazy estimation is not evaluated when `maxOutputTokens` is omitted. - Confirmed Mistral/Devstral projection matches the transport normalizer without mutating history. @@ -80,29 +81,35 @@ Two independent live Council seats re-reviewed the request-shape correction at ` - Confirmed the tightened bridge regex rejects `params.maxOutputTokens` and accepts the clamped shorthand. - Confirmed empty base64 images wrapped in `URL` objects become explanatory text while valid strings, byte buffers, and ordinary URLs remain unchanged. - Confirmed undefined and zero context limits bypass lazily, while every positive limit—including 1, 512, and 1,024—is enforced. -- Confirmed the requested-capped floor accepts the exact 523-token boundary and rejects 522 tokens for the reproduced request. +- Confirmed the final margin is the larger of 2% of estimated input and the limit-scaled minimum: 2% of the authoritative limit capped at 512 tokens, with a one-token floor. +- Confirmed the exact 8,192-token context boundary: input 7,516 fits with output 512 and margin 164, while input 7,517 is rejected. +- Confirmed a one-token input/output request fits a 512-token context, while reserving the full 512-token output does not. +- Confirmed a 512-token dedicated input ceiling accepts input 501 with margin 11 and rejects input 502. - Re-ran provider, typecheck, diff, and bridge checks successfully. ### Musashi seat — PASS -- Verified exact final code head `aab3dac850`. +- Verified exact final code head `56dbc7e9b1`. - Confirmed the parser experiment remains cleanly reverted and no PDF dependency returned. - Confirmed synchronous lazy estimation, linear Mistral projection, and parity across both callers. - Confirmed identical wire fields are counted separately while OAuth/workflow omissions are not double-counted. - Confirmed the fixed-width bridge lookbehind runs correctly under Bun. - Confirmed `URL.href` inspection matches the AI SDK URL contract without changing binary media handling. - Confirmed every positive context window is enforced while absent/non-positive contexts retain lazy bypass behavior. +- Confirmed the exact 8,192-token context boundary: input 7,516 plus output 512 plus margin 164 fits, while one additional input token is rejected. +- Confirmed the exact 512-token input-limit boundary: input 501 plus margin 11 fits, while input 502 is rejected. +- Confirmed context and dedicated input limits receive independent margins while normal-window behavior remains unchanged. - Confirmed focused tests, typecheck, and diff checks pass. ### Degraded seat The original chairman seat was rejected twice by the service safety filter because its retained conversation context included the earlier parser stress case. It produced no contrary code finding on the final head. The thread limit prevented replacing that retained seat with a new fourth thread. -Consensus therefore rests on two independent live PASS votes on exact final code head `aab3dac850`, the primary review, the complete changed-file inspection, and sealed zero-finding Codex Security scans. The degraded seat is disclosed rather than silently counted as agreement. +Consensus therefore rests on two independent live PASS votes on exact final code head `56dbc7e9b1`, the primary review, the complete changed-file inspection, and sealed zero-finding Codex Security scans. The degraded seat is disclosed rather than silently counted as agreement. ## Final verification -- Focused provider, AI-SDK stream, native request, and upstream bridge suites: **417 passed, 11 skipped, 1 existing todo, 0 failed**. +- Focused provider, AI-SDK stream, native request, and upstream bridge suites: **418 passed, 11 skipped, 1 existing todo, 0 failed**. - Repository typecheck: **13/13 successful**. - Strict changed-file marker validation: passed. - Required-marker inventory: **35/35**. @@ -110,7 +117,7 @@ Consensus therefore rests on two independent live PASS votes on exact final code - Targeted oxlint on the final supplemental files: **173 warnings, 0 errors**; warnings are existing repository debt. - Prettier: all changed files pass except `provider/provider.ts` and `provider/transform.ts`; both fail identically on `origin/main`. - `git diff --check origin/main...HEAD`: passed. -- Final request-shape and edge-case Codex Security scans `1dcbce9e-587e-4495-94ff-6d3291e5e1d6`, `bfa1cc4a-7d87-463d-8fc9-822e1624f8b1`, and `19f139b0-8c4c-48cc-adca-a60d41920664`: complete coverage, **0 findings**. +- Final request-shape and edge-case Codex Security scans `1dcbce9e-587e-4495-94ff-6d3291e5e1d6`, `bfa1cc4a-7d87-463d-8fc9-822e1624f8b1`, `19f139b0-8c4c-48cc-adca-a60d41920664`, and `5111b689-e4ad-4243-a7cb-86845b30ca6d`: complete coverage, **0 findings**. ## Residual limitations diff --git a/research/security-reviews/PR 1196 Security Review.md b/research/security-reviews/PR 1196 Security Review.md index 1c63f0d862..e086157731 100644 --- a/research/security-reviews/PR 1196 Security Review.md +++ b/research/security-reviews/PR 1196 Security Review.md @@ -3,7 +3,7 @@ - Repository: `AltimateAI/altimate-code` - Pull request: [#1196](https://github.com/AltimateAI/altimate-code/pull/1196) - Review dates: 2026-08-30 through 2026-08-31 -- Final code candidate: `aab3dac85008fd9a21919741021d3be3848cefbe` +- Final code candidate: `56dbc7e9b1f81fa226821754bf44005d8d08715b` - Scan mode: chained immutable branch-diff reviews - Final coverage: complete - Findings remaining on the final candidate: **0** @@ -34,8 +34,9 @@ The review used immutable ranges so every material repair was independently reco | Final request-shape fixes | `5e04c7885d..c78e1a61b6` | Complete coverage; **0 findings** | | Instruction occurrence fix | `c78e1a61b6..071f4dc782` | Complete coverage; **0 findings** | | Final edge-case hardening | `a279303720..aab3dac850` | Complete coverage; **0 findings** | +| Small-limit margin fix | `e3ca59741a..56dbc7e9b1` | Complete coverage; **0 findings** | -The parser-free remediation scan was sealed once as scan `80591880-0a17-454c-b312-92c32a38f5ff`. The final request-shape and edge-case scans were sealed once each as `1dcbce9e-587e-4495-94ff-6d3291e5e1d6`, `bfa1cc4a-7d87-463d-8fc9-822e1624f8b1`, and `19f139b0-8c4c-48cc-adca-a60d41920664`. Their authoritative results contain no deferred work, no open question, and zero findings. +The parser-free remediation scan was sealed once as scan `80591880-0a17-454c-b312-92c32a38f5ff`. The final request-shape and edge-case scans were sealed once each as `1dcbce9e-587e-4495-94ff-6d3291e5e1d6`, `bfa1cc4a-7d87-463d-8fc9-822e1624f8b1`, `19f139b0-8c4c-48cc-adca-a60d41920664`, and `5111b689-e4ad-4243-a7cb-86845b30ca6d`. Their authoritative results contain no deferred work, no open question, and zero findings. ## Findings discovered and resolved @@ -61,6 +62,10 @@ The final policy removes the 100-page claim instead of replacing it with a large The final supplemental review found that an empty base64 image represented as a `URL` object could escape unsupported-media projection, and that positive context windows at or below 1,024 tokens were treated as placeholder metadata. The final candidate inspects `URL.href` using the same data-URL rule as strings and enforces every positive context limit. Absent and non-positive context metadata still bypasses lazily, so estimates are not evaluated when no credible limit exists. +### 5. Small authoritative limits retain usable capacity + +A follow-up review correctly found that enforcing every positive limit with an unconditional 512-token minimum margin would consume an entire 512-token window. The final candidate retains the 512-token minimum for normal windows but caps that minimum at 2% of each smaller authoritative limit, never below one token. Context and dedicated input ceilings receive separate margins. This preserves local enforcement without rejecting every otherwise valid request on small models. + ## Final trust and data flow Both production request paths use the same sequence: @@ -77,7 +82,7 @@ Codebase graph tracing found exactly two production callers of the centralized c ## Verification -- Focused provider, AI-SDK stream, native request, and upstream bridge suites: **417 passed, 11 skipped, 1 existing todo, 0 failed**. +- Focused provider, AI-SDK stream, native request, and upstream bridge suites: **418 passed, 11 skipped, 1 existing todo, 0 failed**. - Repository typecheck: **13/13 tasks successful**. - Strict changed-file marker validation: passed. - Required-marker inventory: **35/35**. From b7cfd659fac99f533d44fff506977a98e28af437 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Mon, 31 Aug 2026 13:33:56 -0700 Subject: [PATCH 24/32] test: keep processor fixture within request budget --- packages/opencode/test/session/processor-effect.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/opencode/test/session/processor-effect.test.ts b/packages/opencode/test/session/processor-effect.test.ts index 8922baf247..bb68e31364 100644 --- a/packages/opencode/test/session/processor-effect.test.ts +++ b/packages/opencode/test/session/processor-effect.test.ts @@ -392,8 +392,8 @@ it.live("session.processor effect tests preserve text start time", () => { config: (url) => providerCfg(url) }, ), ) -// ACCEPT: the fork's overflow guard intentionally leaves this context:20 fixture -// below the compaction threshold, so processor.process should continue. +// ACCEPT: the fork's overflow guard intentionally leaves this context:20_000 fixture +// at the compaction threshold, so processor.process should continue. it.live("session.processor effect tests continue when guarded token fixture does not request compaction", () => provideTmpdirServerLegacy( ({ dir, llm }) => @@ -407,7 +407,7 @@ it.live("session.processor effect tests continue when guarded token fixture does const parent = yield* user(chat.id, "compact") const msg = yield* assistant(chat.id, parent.id, path.resolve(dir)) const base = yield* refModel(provider) - const mdl = { ...base, limit: { context: 20, output: 10 } } + const mdl = { ...base, limit: { context: 20_000, output: 10 } } const controller = new AbortController() const handle = yield* processors.create({ assistantMessage: msg as unknown as MessageV2.Assistant, From cedd33a8669389a38b7cdba16e3165982ec9e45f Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Mon, 31 Aug 2026 13:38:47 -0700 Subject: [PATCH 25/32] docs: record final CI fixture review --- research/code-reviews/PR 1196 Consensus Review.md | 14 ++++++++------ .../security-reviews/PR 1196 Security Review.md | 7 +++++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/research/code-reviews/PR 1196 Consensus Review.md b/research/code-reviews/PR 1196 Consensus Review.md index faac23a7b6..f973996bc9 100644 --- a/research/code-reviews/PR 1196 Consensus Review.md +++ b/research/code-reviews/PR 1196 Consensus Review.md @@ -3,7 +3,7 @@ - Repository: `AltimateAI/altimate-code` - Pull request: [#1196](https://github.com/AltimateAI/altimate-code/pull/1196) - Review dates: 2026-08-30 through 2026-08-31 -- Final code candidate: `56dbc7e9b1f81fa226821754bf44005d8d08715b` +- Final code candidate: `b7cfd659fac99f533d44fff506977a98e28af437` - Mode: full Council review plus final independent remediation pass - Local verdict: **PASS** - Remote gate: the final candidate must be pushed and fresh CI/bot review must finish @@ -68,11 +68,11 @@ Batching the same parser would not remove its decompression/traversal boundary. ## Final independent remediation review -Two independent live Council seats re-reviewed the request-shape correction at `c78e1a61b6`, then reviewed all follow-up deltas through exact final head `56dbc7e9b1`. The final delta scales the safety margin against each authoritative small limit while retaining the 512-token minimum for normal windows. It does not change PDF behavior. +Two independent live Council seats re-reviewed the request-shape correction at `c78e1a61b6`, then reviewed all production deltas through `56dbc7e9b1` and the final test-fixture delta through exact branch head `b7cfd659fa`. The final production delta scales the safety margin against each authoritative small limit while retaining the 512-token minimum for normal windows. The branch-head delta only updates a processor test's artificial context window to remain compatible with that production enforcement. Neither changes PDF behavior. ### Feynman seat — PASS -- Verified exact final code head `56dbc7e9b1`. +- Verified exact final production code head `56dbc7e9b1` and test-only branch head `b7cfd659fa`. - Confirmed `pdf-lib`, async parsing, page regex/policy, and transitive lock entries are absent. - Confirmed lazy estimation is not evaluated when `maxOutputTokens` is omitted. - Confirmed Mistral/Devstral projection matches the transport normalizer without mutating history. @@ -85,11 +85,12 @@ Two independent live Council seats re-reviewed the request-shape correction at ` - Confirmed the exact 8,192-token context boundary: input 7,516 fits with output 512 and margin 164, while input 7,517 is rejected. - Confirmed a one-token input/output request fits a 512-token context, while reserving the full 512-token output does not. - Confirmed a 512-token dedicated input ceiling accepts input 501 with margin 11 and rejects input 502. +- Confirmed the processor fixture now uses context 20,000, exactly its default compaction headroom, while the former context 20 still raises `OutputTokenBudgetError`. - Re-ran provider, typecheck, diff, and bridge checks successfully. ### Musashi seat — PASS -- Verified exact final code head `56dbc7e9b1`. +- Verified exact final production code head `56dbc7e9b1` and test-only branch head `b7cfd659fa`. - Confirmed the parser experiment remains cleanly reverted and no PDF dependency returned. - Confirmed synchronous lazy estimation, linear Mistral projection, and parity across both callers. - Confirmed identical wire fields are counted separately while OAuth/workflow omissions are not double-counted. @@ -99,17 +100,18 @@ Two independent live Council seats re-reviewed the request-shape correction at ` - Confirmed the exact 8,192-token context boundary: input 7,516 plus output 512 plus margin 164 fits, while one additional input token is rejected. - Confirmed the exact 512-token input-limit boundary: input 501 plus margin 11 fits, while input 502 is rejected. - Confirmed context and dedicated input limits receive independent margins while normal-window behavior remains unchanged. +- Confirmed the final branch delta changes only the processor fixture and comment, preserves the intended `base <= headroom` compaction guard, and does not mask the separate small-context admission regressions. - Confirmed focused tests, typecheck, and diff checks pass. ### Degraded seat The original chairman seat was rejected twice by the service safety filter because its retained conversation context included the earlier parser stress case. It produced no contrary code finding on the final head. The thread limit prevented replacing that retained seat with a new fourth thread. -Consensus therefore rests on two independent live PASS votes on exact final code head `56dbc7e9b1`, the primary review, the complete changed-file inspection, and sealed zero-finding Codex Security scans. The degraded seat is disclosed rather than silently counted as agreement. +Consensus therefore rests on two independent live PASS votes on exact final branch head `b7cfd659fa`, the primary review, the complete changed-file inspection, and sealed zero-finding Codex Security scans through the final production change. The degraded seat is disclosed rather than silently counted as agreement. ## Final verification -- Focused provider, AI-SDK stream, native request, and upstream bridge suites: **418 passed, 11 skipped, 1 existing todo, 0 failed**. +- Focused provider, AI-SDK stream, native request, processor, and upstream bridge suites: **427 passed, 11 skipped, 7 existing todos, 0 failed**. - Repository typecheck: **13/13 successful**. - Strict changed-file marker validation: passed. - Required-marker inventory: **35/35**. diff --git a/research/security-reviews/PR 1196 Security Review.md b/research/security-reviews/PR 1196 Security Review.md index e086157731..ad4e893614 100644 --- a/research/security-reviews/PR 1196 Security Review.md +++ b/research/security-reviews/PR 1196 Security Review.md @@ -3,7 +3,7 @@ - Repository: `AltimateAI/altimate-code` - Pull request: [#1196](https://github.com/AltimateAI/altimate-code/pull/1196) - Review dates: 2026-08-30 through 2026-08-31 -- Final code candidate: `56dbc7e9b1f81fa226821754bf44005d8d08715b` +- Final code candidate: `b7cfd659fac99f533d44fff506977a98e28af437` - Scan mode: chained immutable branch-diff reviews - Final coverage: complete - Findings remaining on the final candidate: **0** @@ -35,6 +35,7 @@ The review used immutable ranges so every material repair was independently reco | Instruction occurrence fix | `c78e1a61b6..071f4dc782` | Complete coverage; **0 findings** | | Final edge-case hardening | `a279303720..aab3dac850` | Complete coverage; **0 findings** | | Small-limit margin fix | `e3ca59741a..56dbc7e9b1` | Complete coverage; **0 findings** | +| Final fixture compatibility | `d663c49b74..b7cfd659fa` | Test-only; no production attack-surface change | The parser-free remediation scan was sealed once as scan `80591880-0a17-454c-b312-92c32a38f5ff`. The final request-shape and edge-case scans were sealed once each as `1dcbce9e-587e-4495-94ff-6d3291e5e1d6`, `bfa1cc4a-7d87-463d-8fc9-822e1624f8b1`, `19f139b0-8c4c-48cc-adca-a60d41920664`, and `5111b689-e4ad-4243-a7cb-86845b30ca6d`. Their authoritative results contain no deferred work, no open question, and zero findings. @@ -66,6 +67,8 @@ The final supplemental review found that an empty base64 image represented as a A follow-up review correctly found that enforcing every positive limit with an unconditional 512-token minimum margin would consume an entire 512-token window. The final candidate retains the 512-token minimum for normal windows but caps that minimum at 2% of each smaller authoritative limit, never below one token. Context and dedicated input ceilings receive separate margins. This preserves local enforcement without rejecting every otherwise valid request on small models. +The subsequent branch-head delta changes only an artificial processor test context from 20 to 20,000 tokens. That value exactly equals the existing default compaction headroom and preserves the fixture's intended guard path; the separate production admission regressions continue to exercise 1-, 512-, and 1,024-token contexts. + ## Final trust and data flow Both production request paths use the same sequence: @@ -82,7 +85,7 @@ Codebase graph tracing found exactly two production callers of the centralized c ## Verification -- Focused provider, AI-SDK stream, native request, and upstream bridge suites: **418 passed, 11 skipped, 1 existing todo, 0 failed**. +- Focused provider, AI-SDK stream, native request, processor, and upstream bridge suites: **427 passed, 11 skipped, 7 existing todos, 0 failed**. - Repository typecheck: **13/13 tasks successful**. - Strict changed-file marker validation: passed. - Required-marker inventory: **35/35**. From d13f784786d03908b0147ac93c9ca7effdd19e78 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Mon, 31 Aug 2026 13:53:04 -0700 Subject: [PATCH 26/32] fix: count system message framing --- packages/opencode/src/provider/output-token-budget.ts | 7 +++++-- packages/opencode/test/provider/transform.test.ts | 8 ++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/provider/output-token-budget.ts b/packages/opencode/src/provider/output-token-budget.ts index 5638715e26..822592b390 100644 --- a/packages/opencode/src/provider/output-token-budget.ts +++ b/packages/opencode/src/provider/output-token-budget.ts @@ -324,8 +324,11 @@ export function estimateInputTokens(input: { readonly tools?: Readonly> readonly instructions?: unknown }): number { - const system = input.system.join("\n") - let total = estimateTextTokens(system) + let total = 0 + if (input.system.length > 0) { + const system = serializeForEstimate(input.system.map((content) => ({ role: "system", content }))) + total += estimateTextTokens(system.text) + } const messages = serializeForEstimate(input.messages, messageMediaAllowances(input.messages)) total += estimateTextTokens(messages.text) + messages.mediaTokens diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index c59bb28b34..fbdb749ca3 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -4847,6 +4847,14 @@ describe("output token budget", () => { expect(both).toBeGreaterThan(systemOnly * 1.8) }) + test("counts framing for every separately transmitted system message", () => { + const entries = Array.from({ length: 2_000 }, () => "x") + const flattened = estimateInputTokens({ system: [entries.join("\n")], messages: [] }) + const framed = estimateInputTokens({ system: entries, messages: [] }) + + expect(framed).toBeGreaterThan(flattened + entries.length) + }) + test("does not tokenize encoded media bytes as literal prompt text", () => { const estimated = estimateInputTokens({ system: [], From a0d7a4aed2fc620fcaee44d7e05bd123bb73170a Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Mon, 31 Aug 2026 14:01:35 -0700 Subject: [PATCH 27/32] docs: record final system-framing review --- .../code-reviews/PR 1196 Consensus Review.md | 23 +++++++++++-------- .../PR 1196 Security Review.md | 17 ++++++++++---- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/research/code-reviews/PR 1196 Consensus Review.md b/research/code-reviews/PR 1196 Consensus Review.md index f973996bc9..024fef9593 100644 --- a/research/code-reviews/PR 1196 Consensus Review.md +++ b/research/code-reviews/PR 1196 Consensus Review.md @@ -3,7 +3,7 @@ - Repository: `AltimateAI/altimate-code` - Pull request: [#1196](https://github.com/AltimateAI/altimate-code/pull/1196) - Review dates: 2026-08-30 through 2026-08-31 -- Final code candidate: `b7cfd659fac99f533d44fff506977a98e28af437` +- Final code candidate: `d13f784786d03908b0147ac93c9ca7effdd19e78` - Mode: full Council review plus final independent remediation pass - Local verdict: **PASS** - Remote gate: the final candidate must be pushed and fresh CI/bot review must finish @@ -51,7 +51,8 @@ Post-Council review found and repaired: - a static bridge assertion that could match the unclamped property-access form; - empty base64 image data wrapped in a `URL` object escaping unsupported-media projection; - positive sub-floor context windows being treated as placeholder metadata; and -- the fixed 512-token safety margin consuming an entire small but valid context or input limit. +- the fixed 512-token safety margin consuming an entire small but valid context or input limit; and +- separately transmitted system messages being flattened before estimation, omitting each entry's wire framing. Each repair was centralized in the shared output-budget module or the existing pure media projection, then exercised through both request boundaries. @@ -68,11 +69,11 @@ Batching the same parser would not remove its decompression/traversal boundary. ## Final independent remediation review -Two independent live Council seats re-reviewed the request-shape correction at `c78e1a61b6`, then reviewed all production deltas through `56dbc7e9b1` and the final test-fixture delta through exact branch head `b7cfd659fa`. The final production delta scales the safety margin against each authoritative small limit while retaining the 512-token minimum for normal windows. The branch-head delta only updates a processor test's artificial context window to remain compatible with that production enforcement. Neither changes PDF behavior. +Two independent live Council seats re-reviewed the request-shape correction at `c78e1a61b6`, the small-limit production delta through `56dbc7e9b1`, the test-fixture correction through `b7cfd659fa`, and the final system-framing correction at exact production head `d13f784786`. The last correction serializes non-empty system entries as the same array of `{ role: "system", content }` records sent by both applicable request paths. Empty arrays remain free, while OAuth and workflow paths continue to omit generated-system framing. None of these changes alter PDF behavior. ### Feynman seat — PASS -- Verified exact final production code head `56dbc7e9b1` and test-only branch head `b7cfd659fa`. +- Verified exact final production code head `d13f784786`. - Confirmed `pdf-lib`, async parsing, page regex/policy, and transitive lock entries are absent. - Confirmed lazy estimation is not evaluated when `maxOutputTokens` is omitted. - Confirmed Mistral/Devstral projection matches the transport normalizer without mutating history. @@ -86,11 +87,13 @@ Two independent live Council seats re-reviewed the request-shape correction at ` - Confirmed a one-token input/output request fits a 512-token context, while reserving the full 512-token output does not. - Confirmed a 512-token dedicated input ceiling accepts input 501 with margin 11 and rejects input 502. - Confirmed the processor fixture now uses context 20,000, exactly its default compaction headroom, while the former context 20 still raises `OutputTokenBudgetError`. +- Confirmed each system entry receives its own role/content framing in both the AI SDK and native request shapes, while OAuth/workflow callers still pass an empty system array. +- Reproduced the framing regression with 2,000 entries: the old flattened shape estimated 1,629 tokens and the corrected framed shape estimated 17,298 tokens, with linear bounded runtime. - Re-ran provider, typecheck, diff, and bridge checks successfully. ### Musashi seat — PASS -- Verified exact final production code head `56dbc7e9b1` and test-only branch head `b7cfd659fa`. +- Verified exact final production code head `d13f784786`. - Confirmed the parser experiment remains cleanly reverted and no PDF dependency returned. - Confirmed synchronous lazy estimation, linear Mistral projection, and parity across both callers. - Confirmed identical wire fields are counted separately while OAuth/workflow omissions are not double-counted. @@ -100,18 +103,20 @@ Two independent live Council seats re-reviewed the request-shape correction at ` - Confirmed the exact 8,192-token context boundary: input 7,516 plus output 512 plus margin 164 fits, while one additional input token is rejected. - Confirmed the exact 512-token input-limit boundary: input 501 plus margin 11 fits, while input 502 is rejected. - Confirmed context and dedicated input limits receive independent margins while normal-window behavior remains unchanged. -- Confirmed the final branch delta changes only the processor fixture and comment, preserves the intended `base <= headroom` compaction guard, and does not mask the separate small-context admission regressions. +- Confirmed the processor-fixture delta changes only that fixture and comment, preserves the intended `base <= headroom` compaction guard, and does not mask the separate small-context admission regressions. +- Confirmed discrete system-entry framing matches both request lowerings, empty arrays remain free, and OAuth/workflow caller projections are unchanged. +- Reproduced a 15,669-token increase over flattened text for 2,000 tiny entries in roughly 2.4 ms. - Confirmed focused tests, typecheck, and diff checks pass. ### Degraded seat The original chairman seat was rejected twice by the service safety filter because its retained conversation context included the earlier parser stress case. It produced no contrary code finding on the final head. The thread limit prevented replacing that retained seat with a new fourth thread. -Consensus therefore rests on two independent live PASS votes on exact final branch head `b7cfd659fa`, the primary review, the complete changed-file inspection, and sealed zero-finding Codex Security scans through the final production change. The degraded seat is disclosed rather than silently counted as agreement. +Consensus therefore rests on two independent live PASS votes on exact final production head `d13f784786`, the primary review, the complete changed-file inspection, and sealed zero-finding Codex Security scans through the final production change. The degraded seat is disclosed rather than silently counted as agreement. ## Final verification -- Focused provider, AI-SDK stream, native request, processor, and upstream bridge suites: **427 passed, 11 skipped, 7 existing todos, 0 failed**. +- Focused provider, AI-SDK stream, native request, processor, and upstream bridge suites: **428 passed, 11 skipped, 7 existing todos, 0 failed**. - Repository typecheck: **13/13 successful**. - Strict changed-file marker validation: passed. - Required-marker inventory: **35/35**. @@ -119,7 +124,7 @@ Consensus therefore rests on two independent live PASS votes on exact final bran - Targeted oxlint on the final supplemental files: **173 warnings, 0 errors**; warnings are existing repository debt. - Prettier: all changed files pass except `provider/provider.ts` and `provider/transform.ts`; both fail identically on `origin/main`. - `git diff --check origin/main...HEAD`: passed. -- Final request-shape and edge-case Codex Security scans `1dcbce9e-587e-4495-94ff-6d3291e5e1d6`, `bfa1cc4a-7d87-463d-8fc9-822e1624f8b1`, `19f139b0-8c4c-48cc-adca-a60d41920664`, and `5111b689-e4ad-4243-a7cb-86845b30ca6d`: complete coverage, **0 findings**. +- Final request-shape and edge-case Codex Security scans `1dcbce9e-587e-4495-94ff-6d3291e5e1d6`, `bfa1cc4a-7d87-463d-8fc9-822e1624f8b1`, `19f139b0-8c4c-48cc-adca-a60d41920664`, `5111b689-e4ad-4243-a7cb-86845b30ca6d`, and `33ec5c89-fce8-434d-b8a6-2baba941ff27`: complete coverage, **0 findings**. ## Residual limitations diff --git a/research/security-reviews/PR 1196 Security Review.md b/research/security-reviews/PR 1196 Security Review.md index ad4e893614..e6afc6a72b 100644 --- a/research/security-reviews/PR 1196 Security Review.md +++ b/research/security-reviews/PR 1196 Security Review.md @@ -3,7 +3,7 @@ - Repository: `AltimateAI/altimate-code` - Pull request: [#1196](https://github.com/AltimateAI/altimate-code/pull/1196) - Review dates: 2026-08-30 through 2026-08-31 -- Final code candidate: `b7cfd659fac99f533d44fff506977a98e28af437` +- Final code candidate: `d13f784786d03908b0147ac93c9ca7effdd19e78` - Scan mode: chained immutable branch-diff reviews - Final coverage: complete - Findings remaining on the final candidate: **0** @@ -36,8 +36,9 @@ The review used immutable ranges so every material repair was independently reco | Final edge-case hardening | `a279303720..aab3dac850` | Complete coverage; **0 findings** | | Small-limit margin fix | `e3ca59741a..56dbc7e9b1` | Complete coverage; **0 findings** | | Final fixture compatibility | `d663c49b74..b7cfd659fa` | Test-only; no production attack-surface change | +| System-message framing fix | `cedd33a866..d13f784786` | Complete coverage; **0 findings** | -The parser-free remediation scan was sealed once as scan `80591880-0a17-454c-b312-92c32a38f5ff`. The final request-shape and edge-case scans were sealed once each as `1dcbce9e-587e-4495-94ff-6d3291e5e1d6`, `bfa1cc4a-7d87-463d-8fc9-822e1624f8b1`, `19f139b0-8c4c-48cc-adca-a60d41920664`, and `5111b689-e4ad-4243-a7cb-86845b30ca6d`. Their authoritative results contain no deferred work, no open question, and zero findings. +The parser-free remediation scan was sealed once as scan `80591880-0a17-454c-b312-92c32a38f5ff`. The final request-shape and edge-case scans were sealed once each as `1dcbce9e-587e-4495-94ff-6d3291e5e1d6`, `bfa1cc4a-7d87-463d-8fc9-822e1624f8b1`, `19f139b0-8c4c-48cc-adca-a60d41920664`, `5111b689-e4ad-4243-a7cb-86845b30ca6d`, and `33ec5c89-fce8-434d-b8a6-2baba941ff27`. Their authoritative results contain no deferred work, no open question, and zero findings. ## Findings discovered and resolved @@ -69,13 +70,19 @@ A follow-up review correctly found that enforcing every positive limit with an u The subsequent branch-head delta changes only an artificial processor test context from 20 to 20,000 tokens. That value exactly equals the existing default compaction headroom and preserves the fixture's intended guard path; the separate production admission regressions continue to exercise 1-, 512-, and 1,024-token contexts. +### 6. System-message framing matches the request transport + +A final bot review found that the estimator joined separately transmitted system strings with newlines. Both applicable request paths instead lower each entry to its own `{ role: "system", content }` record, so a plugin emitting many short entries could omit substantial role/content and JSON framing from the estimate. + +The final candidate serializes the exact framed array before token estimation. Empty arrays still contribute zero, and OAuth/workflow paths still pass an empty system array while counting their provider instructions separately. A 2,000-entry regression fails under the old flattening and confirms linear, bounded execution under the corrected shape. The exact production delta was sealed as a complete zero-finding security scan. + ## Final trust and data flow Both production request paths use the same sequence: 1. Finalize messages, tools, provider instructions, headers, and plugin-selected output reservation. 2. If no output reservation or credible limit exists, return without serializing the prompt. -3. Lazily estimate text, schemas, semantic media allowances, decoded inline payload size, and every system/instruction wire occurrence. +3. Lazily estimate text, schemas, semantic media allowances, decoded inline payload size, each separately framed system entry, and every provider-instruction wire occurrence. 4. Enforce a dedicated input limit when declared. 5. Clamp the output reservation against the shared context window with a safety margin. 6. Reconcile fixed reasoning budgets with the final reservation. @@ -85,7 +92,7 @@ Codebase graph tracing found exactly two production callers of the centralized c ## Verification -- Focused provider, AI-SDK stream, native request, processor, and upstream bridge suites: **427 passed, 11 skipped, 7 existing todos, 0 failed**. +- Focused provider, AI-SDK stream, native request, processor, and upstream bridge suites: **428 passed, 11 skipped, 7 existing todos, 0 failed**. - Repository typecheck: **13/13 tasks successful**. - Strict changed-file marker validation: passed. - Required-marker inventory: **35/35**. @@ -93,7 +100,7 @@ Codebase graph tracing found exactly two production callers of the centralized c - Targeted oxlint: **0 errors**; warnings remain repository debt. - Prettier: all changed files pass except `provider/provider.ts` and `provider/transform.ts`; both fail identically at `origin/main`, so no unrelated whole-file rewrite was introduced. - `git diff --check origin/main...HEAD`: passed. -- Final request-shape Codex Security scans: complete coverage, **0 findings**. +- Final request-shape Codex Security scans, including `cedd33a866..d13f784786`: complete coverage, **0 findings**. ## Operational caveats From e475a2d1dfe093303a707520ec5fa46276876b13 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Mon, 31 Aug 2026 17:33:56 -0700 Subject: [PATCH 28/32] fix: harden text and media token estimates --- .../src/provider/output-token-budget.ts | 28 ++++++++++++--- .../opencode/test/provider/transform.test.ts | 34 +++++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/provider/output-token-budget.ts b/packages/opencode/src/provider/output-token-budget.ts index 822592b390..fd1fe5a9eb 100644 --- a/packages/opencode/src/provider/output-token-budget.ts +++ b/packages/opencode/src/provider/output-token-budget.ts @@ -14,9 +14,13 @@ const ESTIMATE_CHUNK_SIZE = 400 const MEDIA_TOKEN_ALLOWANCE = 2_048 const FILE_TOKEN_ALLOWANCE = 16_384 const PDF_TOKEN_ALLOWANCE = 32_768 +const AUDIO_TOKEN_ALLOWANCE = 32_768 +const VIDEO_TOKEN_ALLOWANCE = 131_072 const DATA_URL_HEADER_LIMIT = 1_024 const MIN_REASONING_BUDGET = 1_024 const EMOJI = /\p{Extended_Pictographic}/u +const DENSE_ASCII_RUN = /[A-Za-z0-9+/_=-]{32,}/g +const DENSE_ASCII_MIN_UNIQUE = 6 const REASONING_BUDGET_KEYS = new Set(["budgetTokens", "thinkingBudget", "budget_tokens"]) const CONTEXT_WINDOW_BETAS = new Map([["context-1m-2025-08-07", 1_000_000]]) const MEDIA_PART_TYPES = new Set([ @@ -141,7 +145,20 @@ export function mergeRequestHeaders(...sources: readonly unknown[]): Record { expect(framed).toBeGreaterThan(flattened + entries.length) }) + test("charges dense high-entropy ASCII more conservatively than repetitive text", () => { + const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + let state = 0x12345678 + const dense = Array.from({ length: 8_192 }, () => { + state = (Math.imul(state, 1_664_525) + 1_013_904_223) >>> 0 + return alphabet[state & 63] + }).join("") + const repetitive = "x".repeat(dense.length) + const estimate = (content: string) => estimateInputTokens({ system: [], messages: [{ role: "user", content }] }) + + expect(estimate(dense)).toBeGreaterThan(8_000) + expect(estimate(dense)).toBeGreaterThan(estimate(repetitive) * 3) + }) + test("does not tokenize encoded media bytes as literal prompt text", () => { const estimated = estimateInputTokens({ system: [], @@ -5071,6 +5085,26 @@ describe("output token budget", () => { expect(estimated).toBeGreaterThanOrEqual(150_000) }) + test("uses semantic allowances instead of decoded byte size for audio and video", () => { + const estimate = (mediaType: string) => + estimateInputTokens({ + system: [], + messages: [ + { + role: "user", + content: [{ type: "file", mediaType, data: "A".repeat(1_398_104) }], + }, + ], + }) + + const audio = estimate("audio/wav") + const video = estimate("video/mp4") + expect(audio).toBeGreaterThan(32_000) + expect(audio).toBeLessThan(40_000) + expect(video).toBeGreaterThan(131_000) + expect(video).toBeLessThan(140_000) + }) + test("uses a fixed parser-free fallback for remote PDFs", () => { const estimated = estimateInputTokens({ system: [], From dc24ed05559d85a5a358d116e97b9b66d536cea4 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Mon, 31 Aug 2026 17:38:23 -0700 Subject: [PATCH 29/32] fix: preserve dense token floors across chunks --- .../src/provider/output-token-budget.ts | 37 +++++++++++++------ .../opencode/test/provider/transform.test.ts | 6 +++ 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/packages/opencode/src/provider/output-token-budget.ts b/packages/opencode/src/provider/output-token-budget.ts index fd1fe5a9eb..18f253dd71 100644 --- a/packages/opencode/src/provider/output-token-budget.ts +++ b/packages/opencode/src/provider/output-token-budget.ts @@ -19,8 +19,10 @@ const VIDEO_TOKEN_ALLOWANCE = 131_072 const DATA_URL_HEADER_LIMIT = 1_024 const MIN_REASONING_BUDGET = 1_024 const EMOJI = /\p{Extended_Pictographic}/u -const DENSE_ASCII_RUN = /[A-Za-z0-9+/_=-]{32,}/g +const DENSE_ASCII_CHARACTER = /[A-Za-z0-9+/_=-]/ +const DENSE_ASCII_MIN_LENGTH = 32 const DENSE_ASCII_MIN_UNIQUE = 6 +const DENSE_ASCII_EXTRA_FRACTION = 0.75 const REASONING_BUDGET_KEYS = new Set(["budgetTokens", "thinkingBudget", "budget_tokens"]) const CONTEXT_WINDOW_BETAS = new Map([["context-1m-2025-08-07", 1_000_000]]) const MEDIA_PART_TYPES = new Set([ @@ -145,17 +147,26 @@ export function mergeRequestHeaders(...sources: readonly unknown[]): Record() + const flush = () => { + if (length >= DENSE_ASCII_MIN_LENGTH && unique.size >= DENSE_ASCII_MIN_UNIQUE) total += length + length = 0 + unique.clear() } - return total + Token.estimate(input.slice(offset)) + for (const character of input) { + if (!DENSE_ASCII_CHARACTER.test(character)) { + flush() + continue + } + length++ + if (unique.size < DENSE_ASCII_MIN_UNIQUE) unique.add(character) + } + flush() + return total } /** Estimate heterogeneous text in small chunks and conservatively count dense or non-ASCII text. */ @@ -174,10 +185,12 @@ function estimateTextTokens(input: string): number { if (EMOJI.test(character)) emoji++ } } - const multilingualFloor = estimateAsciiTokens(ascii) + nonAscii + emoji + const multilingualFloor = Token.estimate(ascii) + nonAscii + emoji total += Math.max(Token.estimate(chunk), multilingualFloor) } - return total + // Token.estimate already charges at least one token per 3.7 characters. Adding three quarters + // of each dense run establishes a conservative one-token-per-character floor. + return total + Math.ceil(denseAsciiCharacters(input) * DENSE_ASCII_EXTRA_FRACTION) } /** Return the first transport payload carried by a semantic media part. */ diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index ec808750e3..bbd8f15712 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -4867,6 +4867,12 @@ describe("output token budget", () => { expect(estimate(dense)).toBeGreaterThan(8_000) expect(estimate(dense)).toBeGreaterThan(estimate(repetitive) * 3) + + const shortDense = alphabet.slice(0, 32) + for (let padding = 0; padding < 400; padding++) { + const prefix = " ".repeat(padding) + expect(estimate(prefix + shortDense)).toBeGreaterThan(estimate(prefix + "x".repeat(32)) + 15) + } }) test("does not tokenize encoded media bytes as literal prompt text", () => { From 800e3b102fafb2f9e72ef7a39c562c55194a0f37 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Mon, 31 Aug 2026 17:47:35 -0700 Subject: [PATCH 30/32] docs: record post-merge review fixes --- .../code-reviews/PR 1196 Consensus Review.md | 37 ++++++++++++++----- .../PR 1196 Security Review.md | 22 ++++++++--- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/research/code-reviews/PR 1196 Consensus Review.md b/research/code-reviews/PR 1196 Consensus Review.md index 024fef9593..e30468e9a9 100644 --- a/research/code-reviews/PR 1196 Consensus Review.md +++ b/research/code-reviews/PR 1196 Consensus Review.md @@ -3,7 +3,7 @@ - Repository: `AltimateAI/altimate-code` - Pull request: [#1196](https://github.com/AltimateAI/altimate-code/pull/1196) - Review dates: 2026-08-30 through 2026-08-31 -- Final code candidate: `d13f784786d03908b0147ac93c9ca7effdd19e78` +- Final code candidate: `dc24ed05559d85a5a358d116e97b9b66d536cea4` - Mode: full Council review plus final independent remediation pass - Local verdict: **PASS** - Remote gate: the final candidate must be pushed and fresh CI/bot review must finish @@ -50,12 +50,25 @@ Post-Council review found and repaired: - identical system and instruction values being deduplicated despite occupying two wire fields; - a static bridge assertion that could match the unclamped property-access form; - empty base64 image data wrapped in a `URL` object escaping unsupported-media projection; -- positive sub-floor context windows being treated as placeholder metadata; and -- the fixed 512-token safety margin consuming an entire small but valid context or input limit; and +- positive sub-floor context windows being treated as placeholder metadata; +- the fixed 512-token safety margin consuming an entire small but valid context or input limit; - separately transmitted system messages being flattened before estimation, omitting each entry's wire framing. +- high-entropy ASCII being estimated like repetitive prose, which could leave too much output reserved for opaque identifiers or encoded text; +- audio and video payloads being charged by decoded bytes rather than a semantic media allowance; and +- a first-pass dense-text detector resetting at 400-character estimator boundaries, allowing short opaque runs to evade the conservative floor at particular alignments. Each repair was centralized in the shared output-budget module or the existing pure media projection, then exercised through both request boundaries. +## Main reconciliation and final bot-comment repairs + +Current `main` (`7f07b7d3b6`) was merged into the PR branch in `b2c3a3480c`. The only textual conflict was the import in `packages/opencode/test/session/llm.test.ts`; the resolution preserves the PR's `jsonSchema` and `tool` runtime imports and `main`'s `Tool` type import. The focused file passes with 14 tests and 2 intentional skips. + +The two remaining bot comments were addressed in `e475a2d1df` and `dc24ed0555`: + +- Dense ASCII runs of at least 32 characters and six distinct opaque characters receive an added conservative floor. Detection is one forward pass with scalar counters and a `Set` capped at six entries, so it is linear time, constant auxiliary memory, and independent of the estimator's 400-character chunk boundaries. +- Audio and video use fixed semantic allowances of 32,768 and 131,072 tokens respectively. Generic files still scale with decoded payload size, images retain the existing fixed allowance, and PDF remains parser-free at `max(32,768, decoded inline bytes)`. +- A regression sweeps all 400 possible chunk alignments for a 32-character dense token. Both independent reviewers reproduced the same conservative delta at every offset. + ## Why the PDF parser experiment was rejected An intermediate branch attempted structural page counting with `pdf-lib`. Independent reviewers found two blockers: @@ -71,8 +84,11 @@ Batching the same parser would not remove its decompression/traversal boundary. Two independent live Council seats re-reviewed the request-shape correction at `c78e1a61b6`, the small-limit production delta through `56dbc7e9b1`, the test-fixture correction through `b7cfd659fa`, and the final system-framing correction at exact production head `d13f784786`. The last correction serializes non-empty system entries as the same array of `{ role: "system", content }` records sent by both applicable request paths. Empty arrays remain free, while OAuth and workflow paths continue to omit generated-system framing. None of these changes alter PDF behavior. +After reconciling `main`, the same two seats reviewed the exact final range `a0d7a4aed2..dc24ed0555`. Both returned **PASS**. Feynman independently measured a 1 MiB dense-text pass at roughly 38 ms and confirmed all 400 alignments. Musashi independently observed the same +24-token delta at every alignment and verified that equal-size generic/PDF payloads still scale by bytes while audio/video remain fixed at their semantic allowances. + ### Feynman seat — PASS +- Re-reviewed exact final head `dc24ed0555` after the `main` merge and final bot-comment repairs. - Verified exact final production code head `d13f784786`. - Confirmed `pdf-lib`, async parsing, page regex/policy, and transitive lock entries are absent. - Confirmed lazy estimation is not evaluated when `maxOutputTokens` is omitted. @@ -93,6 +109,7 @@ Two independent live Council seats re-reviewed the request-shape correction at ` ### Musashi seat — PASS +- Re-reviewed exact final head `dc24ed0555` after the `main` merge and final bot-comment repairs. - Verified exact final production code head `d13f784786`. - Confirmed the parser experiment remains cleanly reverted and no PDF dependency returned. - Confirmed synchronous lazy estimation, linear Mistral projection, and parity across both callers. @@ -112,19 +129,19 @@ Two independent live Council seats re-reviewed the request-shape correction at ` The original chairman seat was rejected twice by the service safety filter because its retained conversation context included the earlier parser stress case. It produced no contrary code finding on the final head. The thread limit prevented replacing that retained seat with a new fourth thread. -Consensus therefore rests on two independent live PASS votes on exact final production head `d13f784786`, the primary review, the complete changed-file inspection, and sealed zero-finding Codex Security scans through the final production change. The degraded seat is disclosed rather than silently counted as agreement. +Consensus therefore rests on two independent live PASS votes on exact final production head `dc24ed0555`, the primary review, the complete changed-file inspection, and sealed zero-finding Codex Security scans through the final production change. The degraded seat is disclosed rather than silently counted as agreement. ## Final verification -- Focused provider, AI-SDK stream, native request, processor, and upstream bridge suites: **428 passed, 11 skipped, 7 existing todos, 0 failed**. +- Focused provider, AI-SDK stream, native request, processor, and upstream bridge suites: **435 passed, 11 skipped, 7 existing todos, 0 failed**. - Repository typecheck: **13/13 successful**. - Strict changed-file marker validation: passed. - Required-marker inventory: **35/35**. - Frozen lockfile install: **1,295 installs across 1,390 packages, no changes**. -- Targeted oxlint on the final supplemental files: **173 warnings, 0 errors**; warnings are existing repository debt. -- Prettier: all changed files pass except `provider/provider.ts` and `provider/transform.ts`; both fail identically on `origin/main`. -- `git diff --check origin/main...HEAD`: passed. -- Final request-shape and edge-case Codex Security scans `1dcbce9e-587e-4495-94ff-6d3291e5e1d6`, `bfa1cc4a-7d87-463d-8fc9-822e1624f8b1`, `19f139b0-8c4c-48cc-adca-a60d41920664`, `5111b689-e4ad-4243-a7cb-86845b30ca6d`, and `33ec5c89-fce8-434d-b8a6-2baba941ff27`: complete coverage, **0 findings**. +- Targeted oxlint on the final supplemental production and provider-test files: **161 warnings, 0 errors**; warnings are existing test-file debt. +- Prettier: the final supplemental files and resolved `llm.test.ts` pass. +- `git diff --check`: passed. +- Post-main and final boundary-hardening Codex Security scans `86b98822-70df-446e-bd56-47d7169ef98a` and `37afdb50-204e-462a-85ab-7deeafdbb2ab`: complete coverage, **0 findings**. Earlier request-shape and edge-case scans remain sealed with zero findings. ## Residual limitations @@ -142,7 +159,7 @@ The local recommendation is **merge after remote completion**, provided: 3. fresh required CI and bot reviews are green; and 4. no new critical finding appears on the pushed head. -Do not merge merely on the strength of the stale green checks attached to `9039a178c0`. +Do not merge merely on the strength of checks attached to an earlier head; fresh checks must complete on `dc24ed0555` after push. ## Execution reliability diff --git a/research/security-reviews/PR 1196 Security Review.md b/research/security-reviews/PR 1196 Security Review.md index e6afc6a72b..7916135800 100644 --- a/research/security-reviews/PR 1196 Security Review.md +++ b/research/security-reviews/PR 1196 Security Review.md @@ -3,7 +3,7 @@ - Repository: `AltimateAI/altimate-code` - Pull request: [#1196](https://github.com/AltimateAI/altimate-code/pull/1196) - Review dates: 2026-08-30 through 2026-08-31 -- Final code candidate: `d13f784786d03908b0147ac93c9ca7effdd19e78` +- Final code candidate: `dc24ed05559d85a5a358d116e97b9b66d536cea4` - Scan mode: chained immutable branch-diff reviews - Final coverage: complete - Findings remaining on the final candidate: **0** @@ -37,8 +37,10 @@ The review used immutable ranges so every material repair was independently reco | Small-limit margin fix | `e3ca59741a..56dbc7e9b1` | Complete coverage; **0 findings** | | Final fixture compatibility | `d663c49b74..b7cfd659fa` | Test-only; no production attack-surface change | | System-message framing fix | `cedd33a866..d13f784786` | Complete coverage; **0 findings** | +| Post-main estimator repairs | `b2c3a3480c..e475a2d1df` | Complete coverage; **0 findings** | +| Dense-boundary hardening | `e475a2d1df..dc24ed0555` | Complete coverage; **0 findings** | -The parser-free remediation scan was sealed once as scan `80591880-0a17-454c-b312-92c32a38f5ff`. The final request-shape and edge-case scans were sealed once each as `1dcbce9e-587e-4495-94ff-6d3291e5e1d6`, `bfa1cc4a-7d87-463d-8fc9-822e1624f8b1`, `19f139b0-8c4c-48cc-adca-a60d41920664`, `5111b689-e4ad-4243-a7cb-86845b30ca6d`, and `33ec5c89-fce8-434d-b8a6-2baba941ff27`. Their authoritative results contain no deferred work, no open question, and zero findings. +The parser-free remediation scan was sealed once as scan `80591880-0a17-454c-b312-92c32a38f5ff`. The final request-shape and edge-case scans were sealed once each as `1dcbce9e-587e-4495-94ff-6d3291e5e1d6`, `bfa1cc4a-7d87-463d-8fc9-822e1624f8b1`, `19f139b0-8c4c-48cc-adca-a60d41920664`, `5111b689-e4ad-4243-a7cb-86845b30ca6d`, and `33ec5c89-fce8-434d-b8a6-2baba941ff27`. The post-main bot-comment repair and chunk-boundary hardening were sealed once each as `86b98822-70df-446e-bd56-47d7169ef98a` and `37afdb50-204e-462a-85ab-7deeafdbb2ab`. Their authoritative results contain no deferred work, no open question, and zero findings. ## Findings discovered and resolved @@ -76,6 +78,14 @@ A final bot review found that the estimator joined separately transmitted system The final candidate serializes the exact framed array before token estimation. Empty arrays still contribute zero, and OAuth/workflow paths still pass an empty system array while counting their provider instructions separately. A 2,000-entry regression fails under the old flattening and confirms linear, bounded execution under the corrected shape. The exact production delta was sealed as a complete zero-finding security scan. +### 7. Dense ASCII and semantic media accounting + +Final bot review identified two estimator mismatches. High-entropy ASCII was receiving the same low character-ratio floor as repetitive prose, while audio and video were charged according to decoded bytes even though provider accounting is duration/semantic based. + +Dense ASCII classification now makes one forward pass over the complete serialized text, preserving run state across the 400-character token-estimator chunks. It uses fixed counters and a unique-character `Set` capped at six entries. The final security review found no superlinear scan, unbounded allocation, backtracking, execution, logging, or chunk-boundary bypass. A regression exercises every possible chunk alignment. + +Audio and video now receive fixed conservative semantic allowances of 32,768 and 131,072 tokens. Generic files retain decoded-byte scaling, and PDF remains the selected parser-free `max(32,768, decoded inline bytes)` policy. This avoids treating a long recording's encoded byte count as if every byte were a model token while preserving conservative request admission. + ## Final trust and data flow Both production request paths use the same sequence: @@ -92,15 +102,15 @@ Codebase graph tracing found exactly two production callers of the centralized c ## Verification -- Focused provider, AI-SDK stream, native request, processor, and upstream bridge suites: **428 passed, 11 skipped, 7 existing todos, 0 failed**. +- Focused provider, AI-SDK stream, native request, processor, and upstream bridge suites: **435 passed, 11 skipped, 7 existing todos, 0 failed**. - Repository typecheck: **13/13 tasks successful**. - Strict changed-file marker validation: passed. - Required-marker inventory: **35/35**. - Frozen lockfile install: **1,295 installs across 1,390 packages, no changes**. - Targeted oxlint: **0 errors**; warnings remain repository debt. -- Prettier: all changed files pass except `provider/provider.ts` and `provider/transform.ts`; both fail identically at `origin/main`, so no unrelated whole-file rewrite was introduced. -- `git diff --check origin/main...HEAD`: passed. -- Final request-shape Codex Security scans, including `cedd33a866..d13f784786`: complete coverage, **0 findings**. +- Prettier: final supplemental files and the resolved merge-conflict test pass. +- `git diff --check`: passed. +- Final post-main scans through `dc24ed0555`: complete coverage, **0 findings**. ## Operational caveats From 856f428981ddc970880cb525e82aee4f974e5e5f Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Mon, 31 Aug 2026 17:56:58 -0700 Subject: [PATCH 31/32] fix: calibrate semantic media estimates --- .../src/provider/output-token-budget.ts | 23 ++++++++++--- .../opencode/test/provider/transform.test.ts | 33 ++++++++++++++----- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/packages/opencode/src/provider/output-token-budget.ts b/packages/opencode/src/provider/output-token-budget.ts index 18f253dd71..24700e9731 100644 --- a/packages/opencode/src/provider/output-token-budget.ts +++ b/packages/opencode/src/provider/output-token-budget.ts @@ -14,8 +14,9 @@ const ESTIMATE_CHUNK_SIZE = 400 const MEDIA_TOKEN_ALLOWANCE = 2_048 const FILE_TOKEN_ALLOWANCE = 16_384 const PDF_TOKEN_ALLOWANCE = 32_768 -const AUDIO_TOKEN_ALLOWANCE = 32_768 -const VIDEO_TOKEN_ALLOWANCE = 131_072 +const AUDIO_TOKEN_ALLOWANCE = 8_192 +const VIDEO_TOKEN_ALLOWANCE = 8_192 +const SEMANTIC_MEDIA_BYTES_PER_TOKEN = 64 const DATA_URL_HEADER_LIMIT = 1_024 const MIN_REASONING_BUDGET = 1_024 const EMOJI = /\p{Extended_Pictographic}/u @@ -247,15 +248,27 @@ function pdfTokenAllowance(payload: unknown): number { return Math.max(PDF_TOKEN_ALLOWANCE, bytes) } -/** Assign an allowance that matches the semantic media kind rather than its encoded bytes. */ +/** Keep semantic media usable on small contexts while making very large inline payloads monotonic. */ +function semanticMediaTokenAllowance(payload: unknown, baseline: number): number { + const bytes = inlinePayloadSize(payload) ?? 0 + // Encoded bytes do not map directly to provider tokens, but a coarse size floor prevents an + // arbitrarily large inline recording from receiving the same allowance as a tiny or remote one. + return Math.max(baseline, Math.ceil(bytes / SEMANTIC_MEDIA_BYTES_PER_TOKEN)) +} + +/** Assign an allowance that matches the semantic media kind rather than charging every byte. */ function mediaTokenAllowance(part: JsonRecord): number { const payload = mediaPayload(part) const mime = mediaType(part, payload) const type = String(part.type) if (mime?.startsWith("image/") || type.startsWith("image")) return MEDIA_TOKEN_ALLOWANCE if (mime === "application/pdf") return pdfTokenAllowance(payload) - if (mime?.startsWith("audio/") || type === "audio") return AUDIO_TOKEN_ALLOWANCE - if (mime?.startsWith("video/") || type === "video") return VIDEO_TOKEN_ALLOWANCE + if (mime?.startsWith("audio/") || type === "audio") { + return semanticMediaTokenAllowance(payload, AUDIO_TOKEN_ALLOWANCE) + } + if (mime?.startsWith("video/") || type === "video") { + return semanticMediaTokenAllowance(payload, VIDEO_TOKEN_ALLOWANCE) + } if (FILE_PART_TYPES.has(type)) { return Math.max(FILE_TOKEN_ALLOWANCE, inlinePayloadSize(payload) ?? 0) } diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index bbd8f15712..c4dce5d413 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -5091,24 +5091,39 @@ describe("output token budget", () => { expect(estimated).toBeGreaterThanOrEqual(150_000) }) - test("uses semantic allowances instead of decoded byte size for audio and video", () => { - const estimate = (mediaType: string) => + test("uses semantic baselines plus a coarse size floor for audio and video", () => { + const estimate = (mediaType: string, data: string) => estimateInputTokens({ system: [], messages: [ { role: "user", - content: [{ type: "file", mediaType, data: "A".repeat(1_398_104) }], + content: [{ type: "file", mediaType, data }], }, ], }) - const audio = estimate("audio/wav") - const video = estimate("video/mp4") - expect(audio).toBeGreaterThan(32_000) - expect(audio).toBeLessThan(40_000) - expect(video).toBeGreaterThan(131_000) - expect(video).toBeLessThan(140_000) + const tinyAudio = estimate("audio/wav", "AQ==") + const tinyVideo = estimate("video/mp4", "AQ==") + expect(tinyAudio).toBeGreaterThan(8_000) + expect(tinyAudio).toBeLessThan(10_000) + expect(tinyVideo).toBeGreaterThan(8_000) + expect(tinyVideo).toBeLessThan(10_000) + + const smallContext = createWindowModel({ context: 16_384, output: 8_192 }) + expect(clampOutputTokens({ model: smallContext, requested: 8_192, inputTokens: tinyVideo })).toBeGreaterThan( + OUTPUT_TOKEN_FLOOR, + ) + + const oneMiB = "A".repeat(1_398_104) + expect(estimate("audio/wav", oneMiB)).toBeGreaterThan(16_000) + expect(estimate("audio/wav", oneMiB)).toBeLessThan(20_000) + expect(estimate("video/mp4", oneMiB)).toBeGreaterThan(16_000) + expect(estimate("video/mp4", oneMiB)).toBeLessThan(20_000) + + const fourMiB = "A".repeat(5_592_408) + expect(estimate("audio/wav", fourMiB)).toBeGreaterThan(65_000) + expect(estimate("video/mp4", fourMiB)).toBeGreaterThan(65_000) }) test("uses a fixed parser-free fallback for remote PDFs", () => { From f8552be280e6d263436a54b9543c944c248be86e Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Mon, 31 Aug 2026 18:03:03 -0700 Subject: [PATCH 32/32] docs: record final media calibration review --- .../code-reviews/PR 1196 Consensus Review.md | 20 +++++++++++-------- .../PR 1196 Security Review.md | 11 ++++++---- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/research/code-reviews/PR 1196 Consensus Review.md b/research/code-reviews/PR 1196 Consensus Review.md index e30468e9a9..fc5302e80c 100644 --- a/research/code-reviews/PR 1196 Consensus Review.md +++ b/research/code-reviews/PR 1196 Consensus Review.md @@ -3,7 +3,7 @@ - Repository: `AltimateAI/altimate-code` - Pull request: [#1196](https://github.com/AltimateAI/altimate-code/pull/1196) - Review dates: 2026-08-30 through 2026-08-31 -- Final code candidate: `dc24ed05559d85a5a358d116e97b9b66d536cea4` +- Final code candidate: `856f428981ddc970880cb525e82aee4f974e5e5f` - Mode: full Council review plus final independent remediation pass - Local verdict: **PASS** - Remote gate: the final candidate must be pushed and fresh CI/bot review must finish @@ -66,9 +66,11 @@ Current `main` (`7f07b7d3b6`) was merged into the PR branch in `b2c3a3480c`. The The two remaining bot comments were addressed in `e475a2d1df` and `dc24ed0555`: - Dense ASCII runs of at least 32 characters and six distinct opaque characters receive an added conservative floor. Detection is one forward pass with scalar counters and a `Set` capped at six entries, so it is linear time, constant auxiliary memory, and independent of the estimator's 400-character chunk boundaries. -- Audio and video use fixed semantic allowances of 32,768 and 131,072 tokens respectively. Generic files still scale with decoded payload size, images retain the existing fixed allowance, and PDF remains parser-free at `max(32,768, decoded inline bytes)`. +- Audio and video use the crude hybrid `max(8,192, ceil(decoded inline bytes / 64))`. Tiny and remote media remain usable on 16K contexts, while very large inline payloads grow monotonically instead of receiving an unbounded fixed estimate. Generic files still scale one-for-one with decoded payload size, images retain the existing fixed allowance, and PDF remains parser-free at `max(32,768, decoded inline bytes)`. - A regression sweeps all 400 possible chunk alignments for a 32-character dense token. Both independent reviewers reproduced the same conservative delta at every offset. +After the first push, fresh Cubic and Codex reviews exposed opposing problems with the initial fixed media constants: 131,072 tokens rejected even tiny video on supported 16K/32K models, while any fixed constant could undercount arbitrarily large inline media. Commit `856f428981` replaces those constants with the hybrid above without parsing codecs, duration, frames, or PDF structure. + ## Why the PDF parser experiment was rejected An intermediate branch attempted structural page counting with `pdf-lib`. Independent reviewers found two blockers: @@ -84,11 +86,13 @@ Batching the same parser would not remove its decompression/traversal boundary. Two independent live Council seats re-reviewed the request-shape correction at `c78e1a61b6`, the small-limit production delta through `56dbc7e9b1`, the test-fixture correction through `b7cfd659fa`, and the final system-framing correction at exact production head `d13f784786`. The last correction serializes non-empty system entries as the same array of `{ role: "system", content }` records sent by both applicable request paths. Empty arrays remain free, while OAuth and workflow paths continue to omit generated-system framing. None of these changes alter PDF behavior. -After reconciling `main`, the same two seats reviewed the exact final range `a0d7a4aed2..dc24ed0555`. Both returned **PASS**. Feynman independently measured a 1 MiB dense-text pass at roughly 38 ms and confirmed all 400 alignments. Musashi independently observed the same +24-token delta at every alignment and verified that equal-size generic/PDF payloads still scale by bytes while audio/video remain fixed at their semantic allowances. +After reconciling `main`, the same two seats reviewed the intermediate range `a0d7a4aed2..dc24ed0555`. Both returned **PASS**. Feynman independently measured a 1 MiB dense-text pass at roughly 38 ms and confirmed all 400 alignments. Musashi independently observed the same +24-token delta at every alignment and verified that equal-size generic/PDF payloads still scaled by bytes while audio/video used the intermediate fixed allowances. + +Both seats then reviewed `800e3b102f..856f428981` and returned **PASS** on the final hybrid media curve. They independently reproduced 8,221 tokens for tiny/remote media, 16,413 for 1 MiB, and 65,565 for 4 MiB; verified usable output remains on a 16K context; and confirmed bounded header inspection with no decoding, parsing, copying, dependency, or PDF/generic-file change. Both explicitly classified codec-dependent over/underestimation as the documented heuristic limitation rather than a blocker. ### Feynman seat — PASS -- Re-reviewed exact final head `dc24ed0555` after the `main` merge and final bot-comment repairs. +- Re-reviewed post-main head `dc24ed0555` and final media-calibration head `856f428981`. - Verified exact final production code head `d13f784786`. - Confirmed `pdf-lib`, async parsing, page regex/policy, and transitive lock entries are absent. - Confirmed lazy estimation is not evaluated when `maxOutputTokens` is omitted. @@ -109,7 +113,7 @@ After reconciling `main`, the same two seats reviewed the exact final range `a0d ### Musashi seat — PASS -- Re-reviewed exact final head `dc24ed0555` after the `main` merge and final bot-comment repairs. +- Re-reviewed post-main head `dc24ed0555` and final media-calibration head `856f428981`. - Verified exact final production code head `d13f784786`. - Confirmed the parser experiment remains cleanly reverted and no PDF dependency returned. - Confirmed synchronous lazy estimation, linear Mistral projection, and parity across both callers. @@ -129,7 +133,7 @@ After reconciling `main`, the same two seats reviewed the exact final range `a0d The original chairman seat was rejected twice by the service safety filter because its retained conversation context included the earlier parser stress case. It produced no contrary code finding on the final head. The thread limit prevented replacing that retained seat with a new fourth thread. -Consensus therefore rests on two independent live PASS votes on exact final production head `dc24ed0555`, the primary review, the complete changed-file inspection, and sealed zero-finding Codex Security scans through the final production change. The degraded seat is disclosed rather than silently counted as agreement. +Consensus therefore rests on two independent live PASS votes on exact final production head `856f428981`, the primary review, the complete changed-file inspection, and sealed zero-finding Codex Security scans through the final production change. The degraded seat is disclosed rather than silently counted as agreement. ## Final verification @@ -141,7 +145,7 @@ Consensus therefore rests on two independent live PASS votes on exact final prod - Targeted oxlint on the final supplemental production and provider-test files: **161 warnings, 0 errors**; warnings are existing test-file debt. - Prettier: the final supplemental files and resolved `llm.test.ts` pass. - `git diff --check`: passed. -- Post-main and final boundary-hardening Codex Security scans `86b98822-70df-446e-bd56-47d7169ef98a` and `37afdb50-204e-462a-85ab-7deeafdbb2ab`: complete coverage, **0 findings**. Earlier request-shape and edge-case scans remain sealed with zero findings. +- Post-main, dense-boundary, and final media-calibration Codex Security scans `86b98822-70df-446e-bd56-47d7169ef98a`, `37afdb50-204e-462a-85ab-7deeafdbb2ab`, and `5b969965-638d-42a6-bc38-d2f5d23b7dc2`: complete coverage, **0 findings**. Earlier request-shape and edge-case scans remain sealed with zero findings. ## Residual limitations @@ -159,7 +163,7 @@ The local recommendation is **merge after remote completion**, provided: 3. fresh required CI and bot reviews are green; and 4. no new critical finding appears on the pushed head. -Do not merge merely on the strength of checks attached to an earlier head; fresh checks must complete on `dc24ed0555` after push. +Do not merge merely on the strength of checks attached to an earlier head; fresh checks must complete after the final documentation commit is pushed. ## Execution reliability diff --git a/research/security-reviews/PR 1196 Security Review.md b/research/security-reviews/PR 1196 Security Review.md index 7916135800..26e2ff6dfd 100644 --- a/research/security-reviews/PR 1196 Security Review.md +++ b/research/security-reviews/PR 1196 Security Review.md @@ -3,7 +3,7 @@ - Repository: `AltimateAI/altimate-code` - Pull request: [#1196](https://github.com/AltimateAI/altimate-code/pull/1196) - Review dates: 2026-08-30 through 2026-08-31 -- Final code candidate: `dc24ed05559d85a5a358d116e97b9b66d536cea4` +- Final code candidate: `856f428981ddc970880cb525e82aee4f974e5e5f` - Scan mode: chained immutable branch-diff reviews - Final coverage: complete - Findings remaining on the final candidate: **0** @@ -39,8 +39,9 @@ The review used immutable ranges so every material repair was independently reco | System-message framing fix | `cedd33a866..d13f784786` | Complete coverage; **0 findings** | | Post-main estimator repairs | `b2c3a3480c..e475a2d1df` | Complete coverage; **0 findings** | | Dense-boundary hardening | `e475a2d1df..dc24ed0555` | Complete coverage; **0 findings** | +| Final media calibration | `800e3b102f..856f428981` | Complete coverage; **0 findings** | -The parser-free remediation scan was sealed once as scan `80591880-0a17-454c-b312-92c32a38f5ff`. The final request-shape and edge-case scans were sealed once each as `1dcbce9e-587e-4495-94ff-6d3291e5e1d6`, `bfa1cc4a-7d87-463d-8fc9-822e1624f8b1`, `19f139b0-8c4c-48cc-adca-a60d41920664`, `5111b689-e4ad-4243-a7cb-86845b30ca6d`, and `33ec5c89-fce8-434d-b8a6-2baba941ff27`. The post-main bot-comment repair and chunk-boundary hardening were sealed once each as `86b98822-70df-446e-bd56-47d7169ef98a` and `37afdb50-204e-462a-85ab-7deeafdbb2ab`. Their authoritative results contain no deferred work, no open question, and zero findings. +The parser-free remediation scan was sealed once as scan `80591880-0a17-454c-b312-92c32a38f5ff`. The final request-shape and edge-case scans were sealed once each as `1dcbce9e-587e-4495-94ff-6d3291e5e1d6`, `bfa1cc4a-7d87-463d-8fc9-822e1624f8b1`, `19f139b0-8c4c-48cc-adca-a60d41920664`, `5111b689-e4ad-4243-a7cb-86845b30ca6d`, and `33ec5c89-fce8-434d-b8a6-2baba941ff27`. The post-main bot-comment repair, chunk-boundary hardening, and final media calibration were sealed once each as `86b98822-70df-446e-bd56-47d7169ef98a`, `37afdb50-204e-462a-85ab-7deeafdbb2ab`, and `5b969965-638d-42a6-bc38-d2f5d23b7dc2`. Their authoritative results contain no deferred work, no open question, and zero findings. ## Findings discovered and resolved @@ -84,7 +85,9 @@ Final bot review identified two estimator mismatches. High-entropy ASCII was rec Dense ASCII classification now makes one forward pass over the complete serialized text, preserving run state across the 400-character token-estimator chunks. It uses fixed counters and a unique-character `Set` capped at six entries. The final security review found no superlinear scan, unbounded allocation, backtracking, execution, logging, or chunk-boundary bypass. A regression exercises every possible chunk alignment. -Audio and video now receive fixed conservative semantic allowances of 32,768 and 131,072 tokens. Generic files retain decoded-byte scaling, and PDF remains the selected parser-free `max(32,768, decoded inline bytes)` policy. This avoids treating a long recording's encoded byte count as if every byte were a model token while preserving conservative request admission. +The first repair gave audio and video fixed semantic allowances of 32,768 and 131,072 tokens. Fresh review then found that 131,072 unconditionally rejects tiny video on supported 16K/32K contexts, while a constant can undercount arbitrarily large inline media. + +The final candidate uses `max(8,192, ceil(decoded inline bytes / 64))` for both modalities. Tiny and remote media therefore retain an 8,192-token baseline; a 16K context still leaves a usable clamped output budget. One MiB grows to roughly 16K tokens and four MiB to roughly 65K. The calculation reuses bounded payload-size inspection and introduces no decoding, codec/duration/frame parsing, traversal, proportional allocation, fetch, logging, or dependency. Generic files retain one-token-per-decoded-byte scaling, and PDF remains the selected parser-free `max(32,768, decoded inline bytes)` policy. ## Final trust and data flow @@ -110,7 +113,7 @@ Codebase graph tracing found exactly two production callers of the centralized c - Targeted oxlint: **0 errors**; warnings remain repository debt. - Prettier: final supplemental files and the resolved merge-conflict test pass. - `git diff --check`: passed. -- Final post-main scans through `dc24ed0555`: complete coverage, **0 findings**. +- Final post-main scans through `856f428981`: complete coverage, **0 findings**. ## Operational caveats