Skip to content

Commit 4a5a1b5

Browse files
isac322claude
andcommitted
fix(hooks): use input budget as percentage base for context limits
DCP previously computed maxContextLimit/minContextLimit percentages from limit.context unconditionally — ignoring limit.input when present and not subtracting limit.output otherwise. This produced thresholds higher than the model's actual safe input window: - OpenAI GPT-5 line (gpt-5.4, gpt-5.4-mini, gpt-5.5, …) defines limit.input separately from limit.context; the percentage base was inflated to limit.context, so nudges fired later than the user's intent. - Shared-pool models (Anthropic, gpt-4o, Gemini, Grok, …) have no explicit limit.input; using limit.context as the base allows input + worst-case output to exceed the context window, risking context_length_exceeded. Match opencode core's session/overflow.ts::usable() convention: limit.input ?? max(0, limit.context - limit.output). Closes #512 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 97ceefe commit 4a5a1b5

3 files changed

Lines changed: 67 additions & 2 deletions

File tree

lib/hooks.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { type HostPermissionSnapshot } from "./host-permissions"
3838
import { compressPermission, syncCompressPermissionState } from "./compress-permission"
3939
import { checkSession, ensureSessionInitialized, saveSessionState, syncToolCache } from "./state"
4040
import { cacheSystemPromptTokens } from "./ui/utils"
41+
import { computeInputBudget } from "./input-budget"
4142

4243
const INTERNAL_AGENT_SIGNATURES = [
4344
"You are a title generator",
@@ -52,11 +53,14 @@ export function createSystemPromptHandler(
5253
prompts: PromptStore,
5354
) {
5455
return async (
55-
input: { sessionID?: string; model: { limit: { context: number } } },
56+
input: {
57+
sessionID?: string
58+
model: { limit: { context: number; input?: number; output?: number } }
59+
},
5660
output: { system: string[] },
5761
) => {
5862
if (input.model?.limit?.context) {
59-
state.modelContextLimit = input.model.limit.context
63+
state.modelContextLimit = computeInputBudget(input.model.limit)
6064
logger.debug("Cached model context limit", { limit: state.modelContextLimit })
6165
}
6266

lib/input-budget.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Compute the input token budget for a model — the value DCP percentage
3+
* thresholds (`maxContextLimit`, `minContextLimit`) should resolve against.
4+
*
5+
* Two cases:
6+
* 1. `limit.input` defined (OpenAI GPT-5 line: gpt-5.4, gpt-5.5, …):
7+
* use it directly. The provider enforces it as a hard input ceiling,
8+
* and `limit.input + limit.output ≈ limit.context` by definition.
9+
* 2. `limit.input` undefined (shared-pool models — all Anthropic, gpt-4o,
10+
* Gemini, Grok, DeepSeek, …): subtract `limit.output` from `limit.context`.
11+
* This guarantees `input + worst-case output ≤ limit.context`, preventing
12+
* `context_length_exceeded` errors when the model fills its output budget.
13+
*
14+
* Mirrors the convention in opencode core (`session/overflow.ts::usable()`).
15+
*/
16+
export function computeInputBudget(limit: {
17+
context: number
18+
input?: number
19+
output?: number
20+
}): number {
21+
if (!limit.context) return 0
22+
return limit.input ?? Math.max(0, limit.context - (limit.output ?? 0))
23+
}

tests/input-budget.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import assert from "node:assert/strict"
2+
import test from "node:test"
3+
import { computeInputBudget } from "../lib/input-budget"
4+
5+
test("computeInputBudget uses limit.input when defined (split-budget OpenAI models)", () => {
6+
// gpt-5.4-mini, gpt-5.5: 400K context, 272K input, 128K output
7+
assert.equal(computeInputBudget({ context: 400000, input: 272000, output: 128000 }), 272000)
8+
// gpt-5.4: 1.05M context, 922K input, 128K output
9+
assert.equal(computeInputBudget({ context: 1050000, input: 922000, output: 128000 }), 922000)
10+
})
11+
12+
test("computeInputBudget subtracts output from context when limit.input is undefined (shared-pool models)", () => {
13+
// claude-opus-4-7: 1M context, 128K output, no explicit input limit
14+
assert.equal(computeInputBudget({ context: 1000000, output: 128000 }), 872000)
15+
// claude-haiku-4-5: 200K context, 64K output
16+
assert.equal(computeInputBudget({ context: 200000, output: 64000 }), 136000)
17+
// gpt-4o: 128K context, 16384 output
18+
assert.equal(computeInputBudget({ context: 128000, output: 16384 }), 111616)
19+
})
20+
21+
test("computeInputBudget treats missing output as 0", () => {
22+
assert.equal(computeInputBudget({ context: 200000 }), 200000)
23+
})
24+
25+
test("computeInputBudget returns 0 when context is 0", () => {
26+
assert.equal(computeInputBudget({ context: 0, input: 100, output: 50 }), 0)
27+
})
28+
29+
test("computeInputBudget never returns negative when output exceeds context", () => {
30+
assert.equal(computeInputBudget({ context: 100, output: 200 }), 0)
31+
})
32+
33+
test("computeInputBudget prefers explicit input over the context-minus-output fallback", () => {
34+
// If both `input` and `output` are present, `input` wins regardless of what
35+
// `context - output` would compute to. Defensive against models where the
36+
// numbers don't satisfy `input + output = context`.
37+
assert.equal(computeInputBudget({ context: 1000, input: 500, output: 200 }), 500)
38+
})

0 commit comments

Comments
 (0)