From cafbe6dec0164e892f8c22e239981da4b96968b1 Mon Sep 17 00:00:00 2001 From: roboomp Date: Thu, 30 Jul 2026 15:40:34 +0000 Subject: [PATCH] fix(catalog): refreshed endpoint-migration caches Fold provider cache-drop policies into the catalog fingerprint and force online-if-uncached discovery when an affected cached model is present. Seed the Copilot migration regression with the real bundled fingerprint and verify stale Grok and MAI completion routes are rewritten through Responses. Fixes #7096 --- packages/catalog/src/model-manager.ts | 20 +++++++++++++++--- .../test/github-copilot-model-limits.test.ts | 21 ++++++++++++------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/packages/catalog/src/model-manager.ts b/packages/catalog/src/model-manager.ts index ca7230aac..4c48908c8 100644 --- a/packages/catalog/src/model-manager.ts +++ b/packages/catalog/src/model-manager.ts @@ -39,7 +39,7 @@ export interface ModelManagerOptions !restoredCache.unresolvedModelIds.has(model.id)); const cacheHasUnresolvedHeaders = restoredCache.unresolvedModelIds.size > 0; const dynamicModelsAuthoritative = options.dynamicModelsAuthoritative ?? false; - const staticFingerprint = fingerprintStatic(staticModels, dynamicModelsAuthoritative); + const cacheDropIds = options.dropCachedModelIdsOnStaticMismatch; + const staticCatalogFingerprint = fingerprintStatic(staticModels, dynamicModelsAuthoritative); + // Endpoint-migration policy is cache identity: adding an id must invalidate + // matching-static-catalog caches written by the prior resolver. + const staticFingerprint = + cacheDropIds && cacheDropIds.length > 0 + ? `${staticCatalogFingerprint}:drop:${Bun.hash(cacheDropIds.join("\0")).toString(36)}` + : staticCatalogFingerprint; const cacheFingerprintMatches = cache?.staticFingerprint === staticFingerprint && staticFingerprint.length > 0; + const cacheNeedsModelMigration = + !cacheFingerprintMatches && + cacheDropIds !== undefined && + usableCachedModels.some(model => cacheDropIds.includes(model.id)); const hasUsableFreshCache = - (cache?.fresh ?? false) && !cacheHasUnresolvedHeaders && (!dynamicModelsAuthoritative || cacheFingerprintMatches); + (cache?.fresh ?? false) && + !cacheHasUnresolvedHeaders && + !cacheNeedsModelMigration && + (!dynamicModelsAuthoritative || cacheFingerprintMatches); const dynamicFetcher = options.fetchDynamicModels; const hasDynamicFetcher = typeof dynamicFetcher === "function"; const hasAuthoritativeCache = ((cache?.authoritative ?? false) && hasUsableFreshCache) || !hasDynamicFetcher; diff --git a/packages/catalog/test/github-copilot-model-limits.test.ts b/packages/catalog/test/github-copilot-model-limits.test.ts index 992258afe..f0cccf5b4 100644 --- a/packages/catalog/test/github-copilot-model-limits.test.ts +++ b/packages/catalog/test/github-copilot-model-limits.test.ts @@ -334,10 +334,10 @@ describe("github copilot model limits mapping", () => { expect(model?.api).toBe("openai-responses"); }); for (const migration of [ - { id: "mai-code-1-flash-picker", name: "MAI-Code-1-Flash", expectedApi: "openai-responses" }, - { id: "grok-4.5", name: "Grok 4.5", expectedApi: undefined }, + { id: "mai-code-1-flash-picker", name: "MAI-Code-1-Flash" }, + { id: "grok-4.5", name: "Grok 4.5" }, ]) { - it(`invalidates a cached ${migration.name} completion route after the endpoint migration`, async () => { + it(`refreshes a cached ${migration.name} completion route after the endpoint migration`, async () => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), `pi-ai-copilot-${migration.id}-cache-`)); const cacheDbPath = path.join(tempDir, "models.db"); const cacheProviderId = `github-copilot-${migration.id}-cache-test`; @@ -346,7 +346,6 @@ describe("github copilot model limits mapping", () => { providerId: "github-copilot", cacheProviderId, cacheDbPath, - staticModels: [], fetchDynamicModels: async () => [ { id: migration.id, @@ -365,7 +364,15 @@ describe("github copilot model limits mapping", () => { await oldManager.refresh("online"); const fetchMock = vi.fn(async () => { - throw new Error("a fresh cache must avoid discovery"); + return new Response( + JSON.stringify({ + data: [{ id: migration.id, name: migration.name }], + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ); }); const manager = createModelManager({ ...githubCopilotModelManagerOptions({ apiKey: "copilot-test-key", fetch: fetchMock }), @@ -375,8 +382,8 @@ describe("github copilot model limits mapping", () => { const { models } = await manager.refresh("online-if-uncached"); const model = models.find(candidate => candidate.id === migration.id); - expect(fetchMock).not.toHaveBeenCalled(); - expect(model?.api).toBe(migration.expectedApi); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(model?.api).toBe("openai-responses"); } finally { await fs.rm(tempDir, { recursive: true, force: true }); }