Skip to content

Commit f8cf752

Browse files
committed
fix(context): qualified key beats bare min when provider is explicit; bare wins for inferred provider
1 parent 821f823 commit f8cf752

File tree

2 files changed

+66
-32
lines changed

2 files changed

+66
-32
lines changed

src/agents/context.lookup.test.ts

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -195,30 +195,33 @@ describe("lookupContextTokens", () => {
195195
expect(result).toBe(200_000);
196196
});
197197

198-
it("resolveContextTokensForModel: bare key wins over qualified; OpenRouter raw entry not shadowed by Google config", async () => {
199-
// applyConfiguredContextWindows writes "gemini-2.5-pro" → 2M (bare key, Google config).
200-
// Discovery writes "google/gemini-2.5-pro" → 999k (OpenRouter raw qualified entry).
201-
// Lookup order: bare first → finds 2M immediately, never reaching the
202-
// OpenRouter raw "google/gemini-2.5-pro" qualified key.
203-
mockDiscoveryDeps([{ id: "google/gemini-2.5-pro", contextWindow: 999_000 }], {
204-
google: {
205-
models: [{ id: "gemini-2.5-pro", contextWindow: 2_000_000 }],
198+
it("resolveContextTokensForModel: config direct scan prevents OpenRouter qualified key collision for Google provider", async () => {
199+
// When provider is explicitly "google" and cfg has a Google contextWindow
200+
// override, the config direct scan returns it before any cache lookup —
201+
// so the OpenRouter raw "google/gemini-2.5-pro" qualified entry is never hit.
202+
// Real callers (status.summary.ts) always pass cfg when provider is explicit.
203+
mockDiscoveryDeps([{ id: "google/gemini-2.5-pro", contextWindow: 999_000 }]);
204+
205+
const cfg = {
206+
models: {
207+
providers: {
208+
google: { models: [{ id: "gemini-2.5-pro", contextWindow: 2_000_000 }] },
209+
},
206210
},
207-
});
211+
};
208212

209213
const { resolveContextTokensForModel } = await import("./context.js");
210214
await new Promise((r) => setTimeout(r, 0));
211215

212-
// Google provider: bare key "gemini-2.5-pro" → 2M (config-written) is found
213-
// first; OpenRouter's "google/gemini-2.5-pro" qualified entry is never hit.
216+
// Google with explicit cfg: config direct scan wins before any cache lookup.
214217
const googleResult = resolveContextTokensForModel({
218+
cfg: cfg as never,
215219
provider: "google",
216220
model: "gemini-2.5-pro",
217221
});
218222
expect(googleResult).toBe(2_000_000);
219223

220-
// OpenRouter provider with slash model id: bare lookup "google/gemini-2.5-pro"
221-
// → 999k (OpenRouter raw discovery entry, still intact).
224+
// OpenRouter provider with slash model id: bare lookup finds the raw entry.
222225
const openrouterResult = resolveContextTokensForModel({
223226
provider: "openrouter",
224227
model: "google/gemini-2.5-pro",
@@ -297,4 +300,27 @@ describe("lookupContextTokens", () => {
297300
});
298301
expect(explicitResult).toBe(2_000_000);
299302
});
303+
304+
it("resolveContextTokensForModel: qualified key beats bare min when provider is explicit (original #35976 fix)", async () => {
305+
// Regression: when both "gemini-3.1-pro-preview" (bare, min=128k) AND
306+
// "google-gemini-cli/gemini-3.1-pro-preview" (qualified, 1M) are in cache,
307+
// an explicit-provider call must return the provider-specific qualified value,
308+
// not the collided bare minimum.
309+
mockDiscoveryDeps([
310+
{ id: "github-copilot/gemini-3.1-pro-preview", contextWindow: 128_000 },
311+
{ id: "gemini-3.1-pro-preview", contextWindow: 128_000 },
312+
{ id: "google-gemini-cli/gemini-3.1-pro-preview", contextWindow: 1_048_576 },
313+
]);
314+
315+
const { resolveContextTokensForModel } = await import("./context.js");
316+
await new Promise((r) => setTimeout(r, 0));
317+
318+
// Qualified "google-gemini-cli/gemini-3.1-pro-preview" → 1M wins over
319+
// bare "gemini-3.1-pro-preview" → 128k (cross-provider minimum).
320+
const result = resolveContextTokensForModel({
321+
provider: "google-gemini-cli",
322+
model: "gemini-3.1-pro-preview",
323+
});
324+
expect(result).toBe(1_048_576);
325+
});
300326
});

src/agents/context.ts

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -345,30 +345,38 @@ export function resolveContextTokensForModel(params: {
345345
}
346346
}
347347

348-
// Try bare key first. Bare keys contain the most-recently-written value
349-
// (config overrides via applyConfiguredContextWindows run last) and are
350-
// always correct for models registered with bare IDs.
348+
// When provider is explicitly given and the model ID is bare (no slash),
349+
// try the provider-qualified cache key BEFORE the bare key. Discovery
350+
// entries are stored under qualified IDs (e.g. "google-gemini-cli/
351+
// gemini-3.1-pro-preview → 1M"), while the bare key may hold a cross-
352+
// provider minimum (128k). Returning the qualified entry gives the correct
353+
// provider-specific window for /status and session context-token persistence.
354+
//
355+
// Guard: only when params.provider is explicit (not inferred from a slash in
356+
// the model string). For model-only callers (e.g. status.ts log-usage
357+
// fallback with model="google/gemini-2.5-pro"), the inferred provider would
358+
// construct "google/gemini-2.5-pro" as the qualified key which accidentally
359+
// matches OpenRouter's raw discovery entry — the bare lookup is correct there.
360+
if (params.provider && ref && !ref.model.includes("/")) {
361+
const qualifiedResult = lookupContextTokens(
362+
`${normalizeProviderId(ref.provider)}/${ref.model}`,
363+
);
364+
if (qualifiedResult !== undefined) {
365+
return qualifiedResult;
366+
}
367+
}
368+
369+
// Bare key fallback. For model-only calls with slash-containing IDs
370+
// (e.g. "google/gemini-2.5-pro") this IS the raw discovery cache key.
351371
const bareResult = lookupContextTokens(params.model);
352372
if (bareResult !== undefined) {
353373
return bareResult;
354374
}
355375

356-
// Bare key miss. As a last resort, try the provider-qualified key so that
357-
// discovery entries stored under qualified IDs are reachable. The pi-ai
358-
// registry can return entries like "google-gemini-cli/gemini-3.1-pro-preview"
359-
// for the google-gemini-cli provider; without this fallback, resolving
360-
// { provider: "google-gemini-cli", model: "gemini-3.1-pro-preview" } with
361-
// no bare-key hit would always miss.
362-
//
363-
// Collision risk: the same keyspace holds raw slash-containing model IDs
364-
// (e.g. OpenRouter's "google/gemini-2.5-pro"). This fallback is only reached
365-
// when the bare key misses, which means native discovery for the requested
366-
// provider produced no bare entry. In that situation the only available
367-
// context-window data is what another provider stored under the same
368-
// qualified key — in practice identical (OpenRouter mirrors native limits) —
369-
// so returning it is strictly better than falling back to DEFAULT_CONTEXT_TOKENS.
370-
// If native discovery IS running, it writes a bare key, and we never reach here.
371-
if (ref && !ref.model.includes("/")) {
376+
// When provider is implicit, try qualified as a last resort so inferred
377+
// provider/model pairs (e.g. model="google-gemini-cli/gemini-3.1-pro")
378+
// still find discovery entries stored under that qualified ID.
379+
if (!params.provider && ref && !ref.model.includes("/")) {
372380
const qualifiedResult = lookupContextTokens(
373381
`${normalizeProviderId(ref.provider)}/${ref.model}`,
374382
);

0 commit comments

Comments
 (0)