Skip to content

Commit db34bc6

Browse files
devin-ai-integration[bot]TylerBarnesMastra Code (openai/gpt-5.5)
authored
fix(core, mastracode): silence provider cache corruption warning and consolidate gateway sync (#16332)
## Description Two related fixes for the noisy `[GatewayRegistry] Detected invalid provider-types in global cache …` / `Detected corrupted global cache …` warnings that show up when running `mastracode` and `mastra dev` against the same `~/.cache/mastra/` directory. ### 1. Stop logging on auto-recoverable cache corruption (`@mastra/core`) In `packages/core/src/llm/model/provider-registry.ts > syncGlobalCacheToLocal()`: - Removed the three `console.warn` calls that fired on corrupted JSON, corrupted `.d.ts`, or any thrown error during sync. - Behavior is otherwise unchanged — the corrupted file is still deleted so it can't propagate into a project's `dist/`, and the next gateway sync writes a valid file. The warnings were just noise (most commonly triggered by an older `@mastra/core` in another project's `node_modules` writing a cache file without the digit-quoting fix). - Added an explicit `expect(warnSpy).not.toHaveBeenCalled()` assertion to the existing "delete corrupted JSON" test to lock this in. ### 2. Consolidate gateway sync (`mastracode`) `mastracode/src/utils/gateway-sync.ts` was a near-duplicate of `@mastra/core`'s `GatewayRegistry.syncGateways` — it had its own `fetchProvidersFromGateways`, `generateTypesContent`, `atomicWriteFile`, and a separate `~/.cache/mastra/gateway-refresh-time` timestamp file. Two independent code paths writing to the same global cache made it harder to reason about and made it possible for the two implementations to drift (e.g. only one having the digit-quoting fix). `gateway-sync.ts` is now a thin wrapper that: - Calls `GatewayRegistry.getInstance({ useDynamicLoading: true }).syncGateways(true)` for the actual fetch/generate/write. - Keeps the existing public surface (`syncGateways`, `startGatewaySync`, `stopGatewaySync`) and the 5-minute skip-if-recently-synced behavior, but uses `getLastRefreshTime()` from the registry instead of a separate timestamp file. Tests under `mastracode/src/utils/__tests__/gateway-sync.test.ts` were rewritten to cover the wrapper behavior (delegation, force flag, skip-if-recent, error swallowing, periodic timer). ## Related Issue(s) Internal Slack thread; no GitHub issue. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [x] Code refactoring - [x] Test update ## Reviewer notes / risks to look at - **Behavioral subtlety in mastracode:** the old code persisted last-sync time to disk (`~/.cache/mastra/gateway-refresh-time`), so the 5-minute skip survived process restarts. The new wrapper uses `GatewayRegistry.getLastRefreshTime()` which is in-memory per process. In practice this means a fresh `mastracode` start will always do one sync, then back off for 5 minutes. This matches `mastra dev`'s behavior and seems desirable, but flagging it explicitly. - **`MastraGateway` enable check:** the previous mastracode code passed an explicit gateway list. Core's `GatewayRegistry.syncGateways` constructs the same default set (`ModelsDevGateway`, `NetlifyGateway`, `MastraGateway`) and each gateway's own `shouldEnable()` still gates whether it runs, so the `MASTRA_GATEWAY_API_KEY` skip is preserved — but worth double-checking against `packages/core/src/llm/model/provider-registry.ts > GatewayRegistry.syncGateways`. - **No end-to-end verification:** I ran the focused unit tests (`provider-registry.test.ts`, `registry-generator.test.ts`, new `gateway-sync.test.ts`) and `pnpm --filter @mastra/core check`, but did not reproduce the original "corruption" scenario with both `mastracode` and `mastra dev` running side-by-side. The repro depends on having an older `@mastra/core` write to the shared cache, which is awkward to set up locally. - **Pre-existing failures unrelated to this PR:** `mastracode/src/__tests__/index.test.ts` and `pnpm --filter mastracode check` fail on `main` too (unbuilt sibling packages like `@mastra/duckdb`); not introduced here. ## Checklist - [x] I have made corresponding changes to the documentation (if applicable — changeset added) - [x] I have added tests that prove my fix is effective or that my feature works - [ ] I have addressed all Coderabbit comments on this PR Link to Devin session: https://app.devin.ai/sessions/45161f2995e142368556aad18f290a9d Requested by: @TylerBarnes <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## ELI5 This PR stops noisy warning messages when old cache files are auto-recovered and makes mastracode reuse the core gateway-sync logic so there’s one canonical implementation for syncing gateways. --- ## Changes ### packages/core - Silence cache corruption warnings - packages/core/src/llm/model/provider-registry.ts: Removed three `console.warn` calls in syncGlobalCacheToLocal() that previously logged cache-corruption warnings for corrupted JSON, corrupted `.d.ts`, or any thrown error during sync. Corrupted cache files are still deleted so they can't propagate; the next gateway sync rewrites valid files. - packages/core/src/llm/model/provider-registry.test.ts: Updated test to assert no warning is emitted when corrupted JSON is deleted (`expect(warnSpy).not.toHaveBeenCalled()`). ### mastracode - Consolidate gateway sync - mastracode/src/utils/gateway-sync.ts: Replaced the near-duplicate standalone implementation with a thin wrapper that delegates to GatewayRegistry.getInstance({ useDynamicLoading: true }).syncGateways(true). Preserves public API (`syncGateways`, `startGatewaySync`, `stopGatewaySync`) and the 5-minute skip-if-recent behavior, but now uses the registry’s in-memory getLastRefreshTime() instead of a ~/.cache/mastra/gateway-refresh-time file. Removed duplicated filesystem, provider-aggregation, and type-generation logic. - mastracode/src/utils/__tests__/gateway-sync.test.ts: Rewrote tests to cover delegation, force flag, skip-if-recent behavior, error swallowing (logged with `[GatewaySync]`), and periodic timer start/stop. Removed prior tests that duplicated provider-fetch/type-generation behavior now handled by core. ### Changesets - .changeset/silence-provider-cache-corruption-warning.md and .changeset/mastracode-gateway-sync-delegation.md: Patch changesets documenting the warning silencing and delegation. --- ## Reviewer notes / behavioral impacts - The 5-minute cooldown is now in-memory per process (no longer persisted across restarts). - MastraGateway enablement remains governed by core’s shouldEnable logic. - Corrupted cache files are still deleted automatically; warnings are silenced for auto-recoverable cases. - Focused unit tests were added; no end-to-end concurrent mastracode + mastra dev reproduction was performed. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: tyler <tylerdbarnes@gmail.com> Co-authored-by: Mastra Code (openai/gpt-5.5) <noreply@mastra.ai>
1 parent 9260e01 commit db34bc6

6 files changed

Lines changed: 145 additions & 310 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'mastracode': patch
3+
---
4+
5+
Delegate gateway sync to `@mastra/core`'s `GatewayRegistry.syncGateways`, removing duplicated provider-fetch, type-generation, and atomic-write logic so mastracode stays in sync with core registry behavior.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@mastra/core': patch
3+
---
4+
5+
Stop logging auto-recoverable provider cache corruption warnings when `~/.cache/mastra/` contains stale content from another Mastra version. Corrupted cache files are still deleted on read so they cannot propagate into a project's `dist/`, and the next gateway sync rewrites valid files.
Lines changed: 94 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,121 +1,106 @@
1-
import type { MastraModelGateway, ProviderConfig } from '@mastra/core/llm';
2-
import { afterEach, describe, expect, it, vi } from 'vitest';
3-
4-
import { fetchProvidersFromGateways, generateTypesContent } from '../gateway-sync.js';
5-
6-
function createGateway(id: string, providers: Record<string, ProviderConfig>, shouldEnable = true): MastraModelGateway {
7-
return {
8-
id,
9-
name: id,
10-
shouldEnable: vi.fn().mockReturnValue(shouldEnable),
11-
fetchProviders: vi.fn().mockResolvedValue(providers),
12-
buildUrl: vi.fn().mockReturnValue(undefined),
13-
getApiKey: vi.fn().mockResolvedValue('test-key'),
14-
resolveLanguageModel: vi.fn(),
15-
} as unknown as MastraModelGateway;
16-
}
17-
18-
describe('gateway-sync', () => {
19-
const originalMastraGatewayApiKey = process.env.MASTRA_GATEWAY_API_KEY;
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
3+
const registrySyncGateways = vi.fn();
4+
const registryGetLastRefreshTime = vi.fn<() => Date | null>();
5+
const registryGetInstance = vi.fn(() => ({
6+
syncGateways: registrySyncGateways,
7+
getLastRefreshTime: registryGetLastRefreshTime,
8+
}));
9+
10+
vi.mock('@mastra/core/llm', () => ({
11+
GatewayRegistry: {
12+
getInstance: registryGetInstance,
13+
},
14+
}));
15+
16+
describe('gateway-sync wrapper', () => {
17+
beforeEach(() => {
18+
vi.resetModules();
19+
registrySyncGateways.mockReset();
20+
registrySyncGateways.mockResolvedValue(undefined);
21+
registryGetLastRefreshTime.mockReset();
22+
registryGetLastRefreshTime.mockReturnValue(null);
23+
registryGetInstance.mockClear();
24+
});
2025

2126
afterEach(() => {
22-
if (originalMastraGatewayApiKey === undefined) {
23-
delete process.env.MASTRA_GATEWAY_API_KEY;
24-
return;
25-
}
27+
vi.useRealTimers();
28+
});
29+
30+
it('delegates to GatewayRegistry.syncGateways with dynamic loading enabled', async () => {
31+
const { syncGateways } = await import('../gateway-sync.js');
32+
33+
await syncGateways(true);
34+
35+
expect(registryGetInstance).toHaveBeenCalledWith({ useDynamicLoading: true });
36+
expect(registrySyncGateways).toHaveBeenCalledWith(true);
37+
});
38+
39+
it('skips the network sync when the global cache was refreshed recently', async () => {
40+
registryGetLastRefreshTime.mockReturnValue(new Date(Date.now() - 60_000));
41+
const { syncGateways } = await import('../gateway-sync.js');
42+
43+
await syncGateways();
44+
45+
expect(registrySyncGateways).not.toHaveBeenCalled();
46+
});
47+
48+
it('runs the sync when the last refresh is older than the interval', async () => {
49+
registryGetLastRefreshTime.mockReturnValue(new Date(Date.now() - 10 * 60_000));
50+
const { syncGateways } = await import('../gateway-sync.js');
2651

27-
process.env.MASTRA_GATEWAY_API_KEY = originalMastraGatewayApiKey;
52+
await syncGateways();
53+
54+
expect(registrySyncGateways).toHaveBeenCalledWith(true);
2855
});
2956

30-
it('prefixes gateway providers and includes mastra gateway models', async () => {
31-
process.env.MASTRA_GATEWAY_API_KEY = 'test-key';
32-
const modelsDev = createGateway('models.dev', {
33-
openai: {
34-
name: 'OpenAI',
35-
gateway: 'models.dev',
36-
apiKeyEnvVar: 'OPENAI_API_KEY',
37-
models: ['gpt-4.1'],
38-
},
39-
});
40-
const netlify = createGateway('netlify', {
41-
xai: {
42-
name: 'xAI',
43-
gateway: 'netlify',
44-
apiKeyEnvVar: 'NETLIFY_API_KEY',
45-
models: ['grok-3-mini'],
46-
},
47-
});
48-
const mastra = createGateway('mastra', {
49-
google: {
50-
name: 'Google',
51-
gateway: 'mastra',
52-
apiKeyEnvVar: 'MASTRA_GATEWAY_API_KEY',
53-
models: ['gemini-2.5-flash'],
54-
},
55-
});
56-
57-
const { providers, models } = await fetchProvidersFromGateways([modelsDev, netlify, mastra]);
58-
59-
expect(providers.openai).toBeDefined();
60-
expect(providers['netlify/xai']).toBeDefined();
61-
expect(providers['mastra/google']).toBeDefined();
62-
expect(models['mastra/google']).toEqual(['gemini-2.5-flash']);
57+
it('runs the sync when no previous refresh time is recorded', async () => {
58+
registryGetLastRefreshTime.mockReturnValue(null);
59+
const { syncGateways } = await import('../gateway-sync.js');
60+
61+
await syncGateways();
62+
63+
expect(registrySyncGateways).toHaveBeenCalledWith(true);
6364
});
6465

65-
it('skips mastra gateway providers when MASTRA_GATEWAY_API_KEY is not set', async () => {
66-
delete process.env.MASTRA_GATEWAY_API_KEY;
67-
68-
const modelsDev = createGateway('models.dev', {
69-
openai: {
70-
name: 'OpenAI',
71-
gateway: 'models.dev',
72-
apiKeyEnvVar: 'OPENAI_API_KEY',
73-
models: ['gpt-4.1'],
74-
},
75-
});
76-
const netlify = createGateway('netlify', {
77-
xai: {
78-
name: 'xAI',
79-
gateway: 'netlify',
80-
apiKeyEnvVar: 'NETLIFY_API_KEY',
81-
models: ['grok-3-mini'],
82-
},
83-
});
84-
85-
// Use a gateway with shouldEnable=true and real providers so skipping
86-
// is driven by MastraGateway's own shouldEnable() checking the env var,
87-
// not by the mock hardcoding false.
88-
const mastra = createGateway(
89-
'mastra',
90-
{
91-
myMastra: {
92-
name: 'Mastra',
93-
gateway: 'mastra',
94-
apiKeyEnvVar: 'MASTRA_GATEWAY_API_KEY',
95-
models: ['gemini-2.5-flash'],
96-
},
97-
},
98-
true,
99-
);
100-
101-
// Override shouldEnable to use the real env-var check
102-
(mastra.shouldEnable as ReturnType<typeof vi.fn>).mockImplementation(() => !!process.env['MASTRA_GATEWAY_API_KEY']);
103-
104-
const { providers } = await fetchProvidersFromGateways([modelsDev, netlify, mastra]);
105-
106-
expect(providers.openai).toBeDefined();
107-
expect(providers['netlify/xai']).toBeDefined();
108-
// Mastra providers are skipped because MASTRA_GATEWAY_API_KEY is not set
109-
expect(providers['mastra/myMastra']).toBeUndefined();
110-
expect(providers['mastra']).toBeUndefined();
66+
it('always syncs when force=true even if recently refreshed', async () => {
67+
registryGetLastRefreshTime.mockReturnValue(new Date(Date.now() - 1_000));
68+
const { syncGateways } = await import('../gateway-sync.js');
69+
70+
await syncGateways(true);
71+
72+
expect(registrySyncGateways).toHaveBeenCalledWith(true);
11173
});
11274

113-
it('generates types for quoted gateway provider ids', () => {
114-
const content = generateTypesContent({
115-
'mastra/google': ['gemini-2.5-flash'],
116-
});
75+
it('does not throw when the registry sync rejects', async () => {
76+
const error = new Error('boom');
77+
registrySyncGateways.mockRejectedValueOnce(error);
78+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
79+
const { syncGateways } = await import('../gateway-sync.js');
80+
81+
await expect(syncGateways(true)).resolves.toBeUndefined();
82+
83+
expect(errorSpy).toHaveBeenCalledWith('[GatewaySync] Sync failed:', error);
84+
errorSpy.mockRestore();
85+
});
86+
87+
it('startGatewaySync schedules a periodic sync that clears on stop', async () => {
88+
vi.useFakeTimers();
89+
registryGetLastRefreshTime.mockReturnValue(null);
90+
const { startGatewaySync, stopGatewaySync } = await import('../gateway-sync.js');
91+
92+
startGatewaySync(1_000);
93+
// Initial sync fires immediately (skip-if-recent passed because no prior refresh)
94+
await vi.advanceTimersByTimeAsync(0);
95+
expect(registrySyncGateways).toHaveBeenCalledTimes(1);
96+
97+
// Mark "recently synced" so the next interval tick should skip
98+
registryGetLastRefreshTime.mockReturnValue(new Date());
99+
await vi.advanceTimersByTimeAsync(1_000);
100+
expect(registrySyncGateways).toHaveBeenCalledTimes(1);
117101

118-
expect(content).toContain("readonly 'mastra/google': readonly ['gemini-2.5-flash'];");
119-
expect(content).toContain('[P in Provider]: `${P}/${ProviderModelsMap[P][number]}`;');
102+
stopGatewaySync();
103+
await vi.advanceTimersByTimeAsync(5_000);
104+
expect(registrySyncGateways).toHaveBeenCalledTimes(1);
120105
});
121106
});

0 commit comments

Comments
 (0)