From f8cf752c59708fb388fd200276115277e8b217d6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Mar 2026 09:22:12 +0000 Subject: [PATCH] fix(context): qualified key beats bare min when provider is explicit; bare wins for inferred provider --- src/agents/context.lookup.test.ts | 52 +++++++++++++++++++++++-------- src/agents/context.ts | 46 ++++++++++++++++----------- 2 files changed, 66 insertions(+), 32 deletions(-) diff --git a/src/agents/context.lookup.test.ts b/src/agents/context.lookup.test.ts index 525296a9825..428d47759bc 100644 --- a/src/agents/context.lookup.test.ts +++ b/src/agents/context.lookup.test.ts @@ -195,30 +195,33 @@ describe("lookupContextTokens", () => { expect(result).toBe(200_000); }); - it("resolveContextTokensForModel: bare key wins over qualified; OpenRouter raw entry not shadowed by Google config", async () => { - // applyConfiguredContextWindows writes "gemini-2.5-pro" → 2M (bare key, Google config). - // Discovery writes "google/gemini-2.5-pro" → 999k (OpenRouter raw qualified entry). - // Lookup order: bare first → finds 2M immediately, never reaching the - // OpenRouter raw "google/gemini-2.5-pro" qualified key. - mockDiscoveryDeps([{ id: "google/gemini-2.5-pro", contextWindow: 999_000 }], { - google: { - models: [{ id: "gemini-2.5-pro", contextWindow: 2_000_000 }], + it("resolveContextTokensForModel: config direct scan prevents OpenRouter qualified key collision for Google provider", async () => { + // When provider is explicitly "google" and cfg has a Google contextWindow + // override, the config direct scan returns it before any cache lookup — + // so the OpenRouter raw "google/gemini-2.5-pro" qualified entry is never hit. + // Real callers (status.summary.ts) always pass cfg when provider is explicit. + mockDiscoveryDeps([{ id: "google/gemini-2.5-pro", contextWindow: 999_000 }]); + + const cfg = { + models: { + providers: { + google: { models: [{ id: "gemini-2.5-pro", contextWindow: 2_000_000 }] }, + }, }, - }); + }; const { resolveContextTokensForModel } = await import("./context.js"); await new Promise((r) => setTimeout(r, 0)); - // Google provider: bare key "gemini-2.5-pro" → 2M (config-written) is found - // first; OpenRouter's "google/gemini-2.5-pro" qualified entry is never hit. + // Google with explicit cfg: config direct scan wins before any cache lookup. const googleResult = resolveContextTokensForModel({ + cfg: cfg as never, provider: "google", model: "gemini-2.5-pro", }); expect(googleResult).toBe(2_000_000); - // OpenRouter provider with slash model id: bare lookup "google/gemini-2.5-pro" - // → 999k (OpenRouter raw discovery entry, still intact). + // OpenRouter provider with slash model id: bare lookup finds the raw entry. const openrouterResult = resolveContextTokensForModel({ provider: "openrouter", model: "google/gemini-2.5-pro", @@ -297,4 +300,27 @@ describe("lookupContextTokens", () => { }); expect(explicitResult).toBe(2_000_000); }); + + it("resolveContextTokensForModel: qualified key beats bare min when provider is explicit (original #35976 fix)", async () => { + // Regression: when both "gemini-3.1-pro-preview" (bare, min=128k) AND + // "google-gemini-cli/gemini-3.1-pro-preview" (qualified, 1M) are in cache, + // an explicit-provider call must return the provider-specific qualified value, + // not the collided bare minimum. + mockDiscoveryDeps([ + { id: "github-copilot/gemini-3.1-pro-preview", contextWindow: 128_000 }, + { id: "gemini-3.1-pro-preview", contextWindow: 128_000 }, + { id: "google-gemini-cli/gemini-3.1-pro-preview", contextWindow: 1_048_576 }, + ]); + + const { resolveContextTokensForModel } = await import("./context.js"); + await new Promise((r) => setTimeout(r, 0)); + + // Qualified "google-gemini-cli/gemini-3.1-pro-preview" → 1M wins over + // bare "gemini-3.1-pro-preview" → 128k (cross-provider minimum). + const result = resolveContextTokensForModel({ + provider: "google-gemini-cli", + model: "gemini-3.1-pro-preview", + }); + expect(result).toBe(1_048_576); + }); }); diff --git a/src/agents/context.ts b/src/agents/context.ts index 33ec1b74715..d705438bd50 100644 --- a/src/agents/context.ts +++ b/src/agents/context.ts @@ -345,30 +345,38 @@ export function resolveContextTokensForModel(params: { } } - // Try bare key first. Bare keys contain the most-recently-written value - // (config overrides via applyConfiguredContextWindows run last) and are - // always correct for models registered with bare IDs. + // When provider is explicitly given and the model ID is bare (no slash), + // try the provider-qualified cache key BEFORE the bare key. Discovery + // entries are stored under qualified IDs (e.g. "google-gemini-cli/ + // gemini-3.1-pro-preview → 1M"), while the bare key may hold a cross- + // provider minimum (128k). Returning the qualified entry gives the correct + // provider-specific window for /status and session context-token persistence. + // + // Guard: only when params.provider is explicit (not inferred from a slash in + // the model string). For model-only callers (e.g. status.ts log-usage + // fallback with model="google/gemini-2.5-pro"), the inferred provider would + // construct "google/gemini-2.5-pro" as the qualified key which accidentally + // matches OpenRouter's raw discovery entry — the bare lookup is correct there. + if (params.provider && ref && !ref.model.includes("/")) { + const qualifiedResult = lookupContextTokens( + `${normalizeProviderId(ref.provider)}/${ref.model}`, + ); + if (qualifiedResult !== undefined) { + return qualifiedResult; + } + } + + // Bare key fallback. For model-only calls with slash-containing IDs + // (e.g. "google/gemini-2.5-pro") this IS the raw discovery cache key. const bareResult = lookupContextTokens(params.model); if (bareResult !== undefined) { return bareResult; } - // Bare key miss. As a last resort, try the provider-qualified key so that - // discovery entries stored under qualified IDs are reachable. The pi-ai - // registry can return entries like "google-gemini-cli/gemini-3.1-pro-preview" - // for the google-gemini-cli provider; without this fallback, resolving - // { provider: "google-gemini-cli", model: "gemini-3.1-pro-preview" } with - // no bare-key hit would always miss. - // - // Collision risk: the same keyspace holds raw slash-containing model IDs - // (e.g. OpenRouter's "google/gemini-2.5-pro"). This fallback is only reached - // when the bare key misses, which means native discovery for the requested - // provider produced no bare entry. In that situation the only available - // context-window data is what another provider stored under the same - // qualified key — in practice identical (OpenRouter mirrors native limits) — - // so returning it is strictly better than falling back to DEFAULT_CONTEXT_TOKENS. - // If native discovery IS running, it writes a bare key, and we never reach here. - if (ref && !ref.model.includes("/")) { + // When provider is implicit, try qualified as a last resort so inferred + // provider/model pairs (e.g. model="google-gemini-cli/gemini-3.1-pro") + // still find discovery entries stored under that qualified ID. + if (!params.provider && ref && !ref.model.includes("/")) { const qualifiedResult = lookupContextTokens( `${normalizeProviderId(ref.provider)}/${ref.model}`, );