Skip to content

Commit 64c86ec

Browse files
committed
Stop sending contextWindow as the request max_tokens
1 parent a61cd76 commit 64c86ec

3 files changed

Lines changed: 90 additions & 23 deletions

File tree

docs/IMPLEMENTATION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ 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.
256+
Optional `contextWindow` (positive number, tokens) overrides the models.dev / heuristic window for that provider. It sizes compaction and the status-bar meter only — it never becomes the request's `max_tokens` output budget, which stays at the shared source default on every provider branch (Codex, xAI, Go, Anthropic, Bifrost, OpenAI-compatible). `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.
257257

258258
Optional `tools` block to arm the outer per-tool wall-clock budget (unset leaves the watchdog unarmed):
259259

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { describe, test, expect, afterEach } from "bun:test";
2+
import type { ConversationTurn, InferenceOptions } from "@intx/types/runtime";
3+
import { SOURCE_MAX_TOKENS, type ProviderCatalogEntry } from "./index.js";
4+
import {
5+
buildInferenceSourceForRef,
6+
type BuildSourceContext,
7+
} from "./inference-sources.js";
8+
import type { Settings } from "./settings.js";
9+
import {
10+
buildProviderContextWindowOverrides,
11+
contextWindowFor,
12+
setProviderContextWindowOverrides,
13+
} from "../provider/context-window.js";
14+
import { createOpenAICompatibleAdapter } from "../provider/openai-compatible-adapter.js";
15+
16+
const WINDOW = 400_000;
17+
18+
function catalog(): ProviderCatalogEntry[] {
19+
return [
20+
{
21+
name: "fp",
22+
baseURL: "https://fp.example/v1",
23+
apiKey: "fp-key",
24+
models: ["fp-large"],
25+
},
26+
];
27+
}
28+
29+
function ctx(): BuildSourceContext {
30+
return { sessionId: "sess-1", catalog: catalog() };
31+
}
32+
33+
function settingsWithWindow(): Settings {
34+
return {
35+
providers: {
36+
fp: {
37+
baseURL: "https://fp.example/v1",
38+
apiKey: "fp-key",
39+
models: ["fp-large"],
40+
contextWindow: WINDOW,
41+
},
42+
},
43+
};
44+
}
45+
46+
afterEach(() => {
47+
setProviderContextWindowOverrides(undefined);
48+
});
49+
50+
describe("contextWindow / maxTokens split (CL-7784)", () => {
51+
test("setting contextWindow does not change the source output budget", () => {
52+
const source = buildInferenceSourceForRef(
53+
{ provider: "fp", model: "fp-large" },
54+
ctx(),
55+
settingsWithWindow(),
56+
);
57+
expect(source?.defaults?.maxTokens).toBe(SOURCE_MAX_TOKENS);
58+
});
59+
60+
test("contextWindow 400000 does not reach the wire as max_tokens 400000", () => {
61+
const source = buildInferenceSourceForRef(
62+
{ provider: "fp", model: "fp-large" },
63+
ctx(),
64+
settingsWithWindow(),
65+
);
66+
const adapter = createOpenAICompatibleAdapter(
67+
source as unknown as Parameters<typeof createOpenAICompatibleAdapter>[0],
68+
);
69+
const messages = [
70+
{ role: "user", content: [{ type: "text", text: "hi" }] },
71+
] as unknown as ConversationTurn[];
72+
const built = adapter.buildRequest(messages, "fp-large", {
73+
maxTokens: source?.defaults?.maxTokens,
74+
} as InferenceOptions);
75+
const body = JSON.parse(built.body) as Record<string, unknown>;
76+
expect(body["max_tokens"]).toBe(SOURCE_MAX_TOKENS);
77+
expect(body["max_tokens"]).not.toBe(WINDOW);
78+
});
79+
80+
test("setting contextWindow still changes contextWindowFor", () => {
81+
const settings = settingsWithWindow();
82+
setProviderContextWindowOverrides(
83+
buildProviderContextWindowOverrides(settings.providers, "fp", "fp-large"),
84+
);
85+
expect(contextWindowFor("fp:fp-large")).toBe(WINDOW);
86+
});
87+
});

src/config/inference-sources.ts

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
resolveSessionEffort,
1515
type ReasoningEffort,
1616
} from "../provider/reasoning-effort.js";
17-
import { SOURCE_MAX_TOKENS } from "./index.js";
1817
import { isOpenCodeGoProvider } from "../../packages/opencode-go/src/index.js";
1918

2019
export interface BuildSourceContext {
@@ -37,16 +36,6 @@ function catalogEntry(
3736
return catalog.find((e) => e.name === provider);
3837
}
3938

40-
function maxTokensFor(
41-
settings: Settings | undefined,
42-
provider: string,
43-
_model: string,
44-
): number {
45-
const cw = settings?.providers[provider]?.contextWindow;
46-
if (typeof cw === "number" && cw > 0) return cw;
47-
return SOURCE_MAX_TOKENS;
48-
}
49-
5039
export function buildInferenceSourceForRef(
5140
ref: ProviderRef,
5241
ctx: BuildSourceContext,
@@ -57,7 +46,6 @@ export function buildInferenceSourceForRef(
5746
const baseURL = entry?.baseURL ?? providerSettings?.baseURL;
5847
if (baseURL === undefined) return null;
5948

60-
const maxTokens = maxTokensFor(settings, ref.provider, ref.model);
6149
const configured = ref.reasoningEffort ?? ctx.reasoningEffort;
6250
const effort =
6351
configured !== undefined
@@ -123,7 +111,7 @@ export function buildInferenceSourceForRef(
123111
});
124112
}
125113
if (entry?.bifrostVirtualKey === true) {
126-
const src = buildBifrostSource({
114+
return buildBifrostSource({
127115
id: ref.provider,
128116
baseURL,
129117
...(entry?.apiKey !== undefined
@@ -134,13 +122,9 @@ export function buildInferenceSourceForRef(
134122
model: ref.model,
135123
...(effort !== undefined ? { reasoningEffort: effort } : {}),
136124
});
137-
return {
138-
...src,
139-
defaults: { ...src.defaults, maxTokens },
140-
};
141125
}
142126

143-
const src = buildOpenAISource({
127+
return buildOpenAISource({
144128
id: ref.provider,
145129
baseURL,
146130
...(entry?.apiKey !== undefined
@@ -151,10 +135,6 @@ export function buildInferenceSourceForRef(
151135
model: ref.model,
152136
...(effort !== undefined ? { reasoningEffort: effort } : {}),
153137
});
154-
return {
155-
...src,
156-
defaults: { ...src.defaults, maxTokens },
157-
};
158138
}
159139

160140
function buildSourceBundle(args: {

0 commit comments

Comments
 (0)