Skip to content

Commit 0b7fa13

Browse files
committed
Apply provider context window settings to occupancy lookups
The settings field was parsed but occupancy and compaction still used models.dev metadata and family heuristics. An override at config load is the one place that can beat both without being wiped by a later pricing refresh.
1 parent d498552 commit 0b7fa13

9 files changed

Lines changed: 239 additions & 15 deletions

File tree

docs/IMPLEMENTATION.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,8 @@ Provider and model configuration lives in JSON settings files. The global file h
253253

254254
`models` is always an array (single- and multi-model providers are uniform). `defaultModel` (or the first entry) is used when no model is selected. With exactly one provider configured, `defaultProvider` may be omitted.
255255

256+
Optional `contextWindow` (positive number, tokens) overrides the models.dev / heuristic window for that provider. `loadConfig` applies it after `resolveProvider` via `setProviderContextWindowOverrides`, keyed as `<provider>:<model>` for every model on a provider that sets the field, plus the bare model id for the resolved provider so occupancy lookups that only have `source.model` still hit. It takes precedence over models.dev metadata and family heuristics. OAuth-projected Codex/xAI providers still drop the field: the synthetic `ProviderSettings` written by the projection overwrites the settings entry and does not copy `contextWindow`, so a hand-edited value on `codex/...` or `xai/...` is ignored. API-key providers are unaffected.
257+
256258
Optional `tools` block to arm the outer per-tool wall-clock budget (unset leaves the watchdog unarmed):
257259

258260
```json
@@ -463,7 +465,7 @@ Mid-run queue/steer/interrupt state is a pure state machine in `src/tui/session-
463465

464466
- Pricing fetched from models.dev, cached, refreshed on a background interval
465467
- `faremeter` converts `inference.usage` counts into a formatted `$X.XXXX` cost
466-
- The same models.dev payload also yields per-model context windows (`limit.context`), captured into the pricing cache (`contextWindows`) and loaded into `src/provider/context-window.ts`. `compactionThresholdFor(model)` returns ~60% of that window (falling back to per-family heuristics, then 128k) to size proactive compaction. Unknown/family-only models still get a sane default.
468+
- The same models.dev payload also yields per-model context windows (`limit.context`), captured into the pricing cache (`contextWindows`) and loaded into `src/provider/context-window.ts`. A provider-level `contextWindow` settings override, when present, beats that metadata. `compactionThresholdFor(model)` returns ~60% of that window (falling back to per-family heuristics, then 128k) to size proactive compaction. Unknown/family-only models still get a sane default.
467469

468470
### Plugin system
469471

src/config.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import { saveState } from "./session/state.js";
4646
import { filterMcpServersForConnect } from "./trust/project-trust.js";
4747
import { createExaMCPServerConfig } from "./mcp/exa.js";
4848
import { withFileLogSink } from "../tests/helpers/file-log-sink.js";
49+
import { setProviderContextWindowOverrides } from "./provider/context-window.js";
4950

5051
const BUILTIN_EXA_MCP = createExaMCPServerConfig();
5152
const originalFetch = globalThis.fetch;
@@ -57,6 +58,7 @@ beforeEach(() => {
5758
afterEach(() => {
5859
globalThis.fetch = originalFetch;
5960
resetGoModelDiscoveryForTests();
61+
setProviderContextWindowOverrides(undefined);
6062
});
6163

6264
function assertConfigured(

src/config/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import {
1616
validateEffort,
1717
type ReasoningEffort,
1818
} from "../provider/reasoning-effort.js";
19+
import {
20+
buildProviderContextWindowOverrides,
21+
setProviderContextWindowOverrides,
22+
} from "../provider/context-window.js";
1923
import { bootstrapPricingMetadata } from "../cost/pricing-metadata.js";
2024
import {
2125
defaultPricingCachePath,
@@ -924,6 +928,14 @@ export async function loadConfig(
924928
};
925929
}
926930

931+
setProviderContextWindowOverrides(
932+
buildProviderContextWindowOverrides(
933+
settingsForResolution?.providers ?? {},
934+
resolved.providerName,
935+
resolved.model,
936+
),
937+
);
938+
927939
// Enforce model/effort compatibility at the boundary. The modal only offers
928940
// supported levels, but a hand-edited local settings file can pair an effort
929941
// with a model that does not accept it; reject it here rather than shipping an

src/config/settings.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ export interface ProviderSettings {
3939
// provider regardless of model pricing — e.g. a prepaid coding plan or a
4040
// gateway whose models.dev prices do not apply.
4141
free?: boolean;
42+
// Token-window override for compaction and the status-bar meter. Applied at
43+
// config load into contextWindowFor. OAuth-projected Codex/xAI entries drop
44+
// this field, so a hand-edited value on those providers is ignored.
4245
contextWindow?: number;
4346
// When true, this provider uses a Bifrost virtual key (sk-bf-...).
4447
// The marker causes the inference source to route through the Bifrost

src/cost/cost-summary.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { afterEach, describe, expect, it } from "bun:test";
22

3-
import { setModelContextWindows } from "../provider/context-window.js";
3+
import {
4+
setModelContextWindows,
5+
setProviderContextWindowOverrides,
6+
} from "../provider/context-window.js";
47
import {
58
buildCostSummary,
69
formatCostCommandOutput,
@@ -9,7 +12,10 @@ import {
912
} from "./cost-summary.js";
1013
import type { CostSummaryInput } from "./cost-summary.js";
1114

12-
afterEach(() => setModelContextWindows(undefined));
15+
afterEach(() => {
16+
setModelContextWindows(undefined);
17+
setProviderContextWindowOverrides(undefined);
18+
});
1319

1420
const baseInput: CostSummaryInput = {
1521
modelId: "test-model",

src/provider/context-window.test.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import { describe, expect, it, afterEach } from "bun:test";
2+
import { mkdtemp, writeFile } from "node:fs/promises";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
import { loadConfig } from "../config/index.js";
26
import {
7+
buildProviderContextWindowOverrides,
38
contextWindowFor,
49
hasContextWindowFor,
510
setModelContextWindows,
11+
setProviderContextWindowOverrides,
612
} from "./context-window.js";
713

814
describe("contextWindowFor", () => {
915
afterEach(() => {
1016
setModelContextWindows(undefined);
17+
setProviderContextWindowOverrides(undefined);
1118
});
1219

1320
it("resolves a custom-provider-prefixed id against the bare model registry entry", () => {
@@ -34,4 +41,126 @@ describe("contextWindowFor", () => {
3441
setModelContextWindows({ "grok-4.5": 500_000 });
3542
expect(hasContextWindowFor("xai/thegreataxios:grok-4.5")).toBe(true);
3643
});
44+
45+
it("lets a provider override beat models.dev registry metadata", () => {
46+
setModelContextWindows({ "fp-large": 200_000 });
47+
setProviderContextWindowOverrides({ "fp-large": 32_000 });
48+
expect(contextWindowFor("fp-large")).toBe(32_000);
49+
expect(contextWindowFor("firepass:fp-large")).toBe(32_000);
50+
});
51+
52+
it("lets a provider override beat the family heuristic", () => {
53+
setProviderContextWindowOverrides({ "claude-sonnet": 8_000 });
54+
expect(contextWindowFor("claude-sonnet")).toBe(8_000);
55+
});
56+
57+
it("keeps the override across a later models.dev registry replace", () => {
58+
setProviderContextWindowOverrides({ "fp-large": 32_000 });
59+
setModelContextWindows({ "fp-large": 200_000 });
60+
expect(contextWindowFor("fp-large")).toBe(32_000);
61+
});
62+
63+
it("reports confidence for an override even when the registry is empty", () => {
64+
setProviderContextWindowOverrides({ "fp-large": 32_000 });
65+
expect(hasContextWindowFor("fp-large")).toBe(true);
66+
expect(hasContextWindowFor("firepass:fp-large")).toBe(true);
67+
});
68+
});
69+
70+
describe("buildProviderContextWindowOverrides", () => {
71+
it("keys <provider>:<model> for every model and the bare id only for the resolved provider", () => {
72+
const overrides = buildProviderContextWindowOverrides(
73+
{
74+
firepass: {
75+
models: ["fp-large", "fp-small"],
76+
contextWindow: 32_000,
77+
},
78+
other: {
79+
models: ["fp-large", "other-model"],
80+
contextWindow: 64_000,
81+
},
82+
skipped: {
83+
models: ["no-window"],
84+
},
85+
},
86+
"firepass",
87+
"fp-large",
88+
);
89+
expect(overrides).toEqual({
90+
"firepass:fp-large": 32_000,
91+
"firepass:fp-small": 32_000,
92+
"fp-large": 32_000,
93+
"fp-small": 32_000,
94+
"other:fp-large": 64_000,
95+
"other:other-model": 64_000,
96+
});
97+
});
98+
99+
it("includes the resolved model even when it is not in the provider model list", () => {
100+
const overrides = buildProviderContextWindowOverrides(
101+
{
102+
firepass: { models: ["fp-large"], contextWindow: 32_000 },
103+
},
104+
"firepass",
105+
"fp-cli",
106+
);
107+
expect(overrides["firepass:fp-cli"]).toBe(32_000);
108+
expect(overrides["fp-cli"]).toBe(32_000);
109+
});
110+
111+
it("skips non-positive windows", () => {
112+
expect(
113+
buildProviderContextWindowOverrides(
114+
{
115+
firepass: { models: ["fp-large"], contextWindow: 0 },
116+
other: { models: ["m"], contextWindow: -1 },
117+
},
118+
"firepass",
119+
"fp-large",
120+
),
121+
).toEqual({});
122+
});
123+
});
124+
125+
describe("loadConfig provider contextWindow", () => {
126+
afterEach(() => {
127+
setModelContextWindows(undefined);
128+
setProviderContextWindowOverrides(undefined);
129+
});
130+
131+
it("applies providers.<name>.contextWindow after resolveProvider", async () => {
132+
const cwd = await mkdtemp(join(tmpdir(), "ic-cw-"));
133+
const globalPath = join(cwd, "global.json");
134+
await writeFile(
135+
globalPath,
136+
JSON.stringify({
137+
defaultProvider: "firepass",
138+
providers: {
139+
firepass: {
140+
baseURL: "https://firepass.example/v1",
141+
apiKey: "test-key",
142+
models: ["fp-large", "fp-small"],
143+
defaultModel: "fp-large",
144+
contextWindow: 32_000,
145+
},
146+
other: {
147+
baseURL: "https://other.example/v1",
148+
apiKey: "other-key",
149+
models: ["fp-large"],
150+
contextWindow: 64_000,
151+
},
152+
},
153+
}),
154+
);
155+
156+
setModelContextWindows({ "fp-large": 200_000 });
157+
await loadConfig(["--cwd", cwd, "hello"], {
158+
globalSettingsPath: globalPath,
159+
});
160+
161+
expect(contextWindowFor("firepass:fp-large")).toBe(32_000);
162+
expect(contextWindowFor("fp-large")).toBe(32_000);
163+
expect(contextWindowFor("firepass:fp-small")).toBe(32_000);
164+
expect(contextWindowFor("other:fp-large")).toBe(64_000);
165+
});
37166
});

src/provider/context-window.ts

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Approximate total context window (tokens) per model, used to render
22
// context-window occupancy in the status bar and to size compaction. When
3-
// models.dev metadata is loaded at startup it takes priority; otherwise we fall
3+
// a provider settings override is applied at config load it takes priority;
4+
// otherwise models.dev metadata loaded at startup wins; otherwise we fall
45
// back to conservative per-family floors, and finally a common 128k window.
56

67
import type { TokenUsage } from "@intx/types/runtime";
@@ -23,12 +24,59 @@ export function contextTokensFromUsage(usage: TokenUsage | undefined): number {
2324
// Exact model-id match wins over the family heuristics below.
2425
let contextWindowRegistry: Record<string, number> = {};
2526

27+
// Populated at config load from providers.<name>.contextWindow. Survives a
28+
// later models.dev refresh because it lives beside the registry, not in it.
29+
let contextWindowOverrides: Record<string, number> = {};
30+
2631
export function setModelContextWindows(
2732
windows: Record<string, number> | undefined,
2833
): void {
2934
contextWindowRegistry = windows ?? {};
3035
}
3136

37+
export function setProviderContextWindowOverrides(
38+
windows: Record<string, number> | undefined,
39+
): void {
40+
contextWindowOverrides = windows ?? {};
41+
}
42+
43+
export type ProviderContextWindowSource = {
44+
models: readonly string[];
45+
contextWindow?: number;
46+
};
47+
48+
function isPositiveWindow(window: number | undefined): window is number {
49+
return window !== undefined && Number.isFinite(window) && window > 0;
50+
}
51+
52+
// Key `<provider>:<model>` for every model on a provider that sets the knob.
53+
// Bare model ids are added only for the resolved provider so occupancy
54+
// lookups that only have `source.model` still hit, without letting another
55+
// provider's same model id steal the bare slot.
56+
export function buildProviderContextWindowOverrides(
57+
providers: Record<string, ProviderContextWindowSource>,
58+
resolvedProviderName: string,
59+
resolvedModel: string,
60+
): Record<string, number> {
61+
const overrides: Record<string, number> = {};
62+
for (const [name, provider] of Object.entries(providers)) {
63+
const window = provider.contextWindow;
64+
if (!isPositiveWindow(window)) continue;
65+
const models = new Set(provider.models);
66+
if (name === resolvedProviderName && resolvedModel.length > 0) {
67+
models.add(resolvedModel);
68+
}
69+
for (const model of models) {
70+
if (model.length === 0) continue;
71+
overrides[`${name}:${model}`] = window;
72+
if (name === resolvedProviderName) {
73+
overrides[model] = window;
74+
}
75+
}
76+
}
77+
return overrides;
78+
}
79+
3280
function heuristicWindow(model: string): number {
3381
const m = model.toLowerCase();
3482
if (m.includes("gpt-6")) return 1_000_000;
@@ -60,20 +108,33 @@ function lookupCandidates(model: string): string[] {
60108
return [model, bareModel, `${canonicalProvider}/${bareModel}`];
61109
}
62110

63-
/** True when the registry has an entry for `model` under any known form, so a
64-
* caller can distinguish a confident lookup from the heuristic fallback. */
111+
function lookupWindow(
112+
table: Record<string, number>,
113+
model: string,
114+
): number | undefined {
115+
for (const candidate of lookupCandidates(model)) {
116+
const exact = table[candidate];
117+
if (exact !== undefined) return exact;
118+
}
119+
return undefined;
120+
}
121+
122+
/** True when an override or the registry has an entry for `model` under any
123+
* known form, so a caller can distinguish a confident lookup from the
124+
* heuristic fallback. */
65125
export function hasContextWindowFor(model: string): boolean {
66-
return lookupCandidates(model).some(
67-
(candidate) => contextWindowRegistry[candidate] !== undefined,
126+
return (
127+
lookupWindow(contextWindowOverrides, model) !== undefined ||
128+
lookupWindow(contextWindowRegistry, model) !== undefined
68129
);
69130
}
70131

71132
export function contextWindowFor(model: string): number {
72-
for (const candidate of lookupCandidates(model)) {
73-
const exact = contextWindowRegistry[candidate];
74-
if (exact !== undefined) return exact;
75-
}
76-
return heuristicWindow(model);
133+
return (
134+
lookupWindow(contextWindowOverrides, model) ??
135+
lookupWindow(contextWindowRegistry, model) ??
136+
heuristicWindow(model)
137+
);
77138
}
78139

79140
// Fraction of the window at which proactive compaction should fire. Kept well

tests/unit/config.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { test, expect } from "bun:test";
1+
import { afterEach, test, expect } from "bun:test";
22
import {
33
mkdtemp,
44
mkdir,
@@ -11,8 +11,13 @@ import { tmpdir } from "node:os";
1111
import { join } from "node:path";
1212
import { loadConfig } from "../../src/config/index.js";
1313
import { resetPricingMetadataRefreshForTests } from "../../src/cost/pricing-metadata.js";
14+
import { setProviderContextWindowOverrides } from "../../src/provider/context-window.js";
1415
import { withMockedModuleDuring } from "../helpers/mock-module.js";
1516

17+
afterEach(() => {
18+
setProviderContextWindowOverrides(undefined);
19+
});
20+
1621
// Rejects immediately instead of touching the network. loadConfig's pricing
1722
// refresh is fire-and-forget, so a resolved run proves only that the injected
1823
// impl was reached — which is exactly the regression this file guards against:

tests/unit/context-window.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
COMPACTION_RESUME_FRACTION,
1111
CONTEXT_METER_DANGER_FRACTION,
1212
setModelContextWindows,
13+
setProviderContextWindowOverrides,
1314
} from "../../src/provider/context-window.js";
1415

1516
function usage(overrides: Partial<TokenUsage>): TokenUsage {
@@ -23,7 +24,10 @@ function usage(overrides: Partial<TokenUsage>): TokenUsage {
2324
};
2425
}
2526

26-
afterEach(() => setModelContextWindows(undefined));
27+
afterEach(() => {
28+
setModelContextWindows(undefined);
29+
setProviderContextWindowOverrides(undefined);
30+
});
2731

2832
describe("contextWindowFor", () => {
2933
test("returns the gpt-5 family window for codex models", () => {

0 commit comments

Comments
 (0)