Skip to content

Commit 77078b3

Browse files
authored
fix(core): ensure stable fallback for restricted preview models (google-gemini#26999)
1 parent 1814c7f commit 77078b3

9 files changed

Lines changed: 50 additions & 28 deletions

File tree

packages/core/src/availability/autoRoutingFallback.integration.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ describe('Auto Routing Fallback Integration', () => {
2929

3030
beforeEach(() => {
3131
vi.useFakeTimers();
32+
vi.spyOn(Config.prototype, 'getHasAccessToPreviewModel').mockReturnValue(
33+
true,
34+
);
3235

3336
// Mock fs to avoid real file system access
3437
vi.mocked(fs.existsSync).mockReturnValue(true);

packages/core/src/availability/fallbackIntegration.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ describe('Fallback Integration', () => {
2828
getActiveModel: () => PREVIEW_GEMINI_MODEL_AUTO,
2929
setActiveModel: vi.fn(),
3030
getUserTier: () => undefined,
31+
getHasAccessToPreviewModel: () => true,
3132
getModelAvailabilityService: () => availabilityService,
3233
modelConfigService: undefined as unknown as ModelConfigService,
3334
} as unknown as Config;

packages/core/src/availability/policyHelpers.ts

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ export function resolvePolicyChain(
5555
const useGemini31FlashLite =
5656
config.getGemini31FlashLiteLaunchedSync?.() ?? false;
5757
const useCustomToolModel = config.getUseCustomToolModelSync?.() ?? false;
58-
const hasAccessToPreview = config.getHasAccessToPreviewModel?.() ?? true;
58+
const hasAccessToPreview = config.getHasAccessToPreviewModel?.() ?? false;
59+
60+
// Capture the original family intent before any normalization or early downgrade.
61+
const isOriginallyGemini3 = isGemini3Model(modelFromConfig, config);
5962

6063
const resolvedModel = normalizeModelId(
6164
resolveModel(
@@ -75,10 +78,7 @@ export function resolvePolicyChain(
7578
// We always wrap around for Gemini 3 chains to ensure maximum availability
7679
// between models in the same family (e.g. fallback to Pro if Flash is exhausted).
7780
const effectiveWrapsAround =
78-
wrapsAround ||
79-
isAutoPreferred ||
80-
isAutoConfigured ||
81-
isGemini3Model(resolvedModel, config);
81+
wrapsAround || isAutoPreferred || isAutoConfigured || isOriginallyGemini3;
8282

8383
// --- DYNAMIC PATH ---
8484
if (config.getExperimentalDynamicModelConfiguration?.() === true) {
@@ -91,11 +91,7 @@ export function resolvePolicyChain(
9191

9292
if (resolvedModel === DEFAULT_GEMINI_FLASH_LITE_MODEL) {
9393
chain = config.modelConfigService.resolveChain('lite', context);
94-
} else if (
95-
isGemini3Model(normalizeModelId(resolvedModel), config) ||
96-
isAutoPreferred ||
97-
isAutoConfigured
98-
) {
94+
} else if (isOriginallyGemini3 || isAutoPreferred || isAutoConfigured) {
9995
// 1. Try to find a chain specifically for the current configured alias
10096
if (
10197
isAutoConfigured &&
@@ -132,15 +128,11 @@ export function resolvePolicyChain(
132128

133129
if (resolvedModel === DEFAULT_GEMINI_FLASH_LITE_MODEL) {
134130
chain = getFlashLitePolicyChain();
135-
} else if (
136-
isGemini3Model(resolvedModel, config) ||
137-
isAutoPreferred ||
138-
isAutoConfigured
139-
) {
131+
} else if (isOriginallyGemini3 || isAutoPreferred || isAutoConfigured) {
140132
const isAutoSelection = isAutoPreferred || isAutoConfigured;
141133
if (hasAccessToPreview) {
142134
const previewEnabled =
143-
isGemini3Model(resolvedModel, config) ||
135+
isOriginallyGemini3 ||
144136
normalizedPreferredModel === PREVIEW_GEMINI_MODEL_AUTO ||
145137
configuredModel === PREVIEW_GEMINI_MODEL_AUTO;
146138
chain = getModelPolicyChain({

packages/core/src/code_assist/setup.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ describe('setupUser', () => {
228228
});
229229

230230
it('should throw InvalidNumericProjectIdError when GOOGLE_CLOUD_PROJECT_ID is numeric', async () => {
231+
vi.stubEnv('GOOGLE_CLOUD_PROJECT', '');
231232
vi.stubEnv('GOOGLE_CLOUD_PROJECT_ID', '1234567890');
232233
await expect(setupUser({} as OAuth2Client, mockConfig)).rejects.toThrow(
233234
InvalidNumericProjectIdError,

packages/core/src/config/config.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3251,8 +3251,8 @@ describe('Config Quota & Preview Model Access', () => {
32513251
vi.mocked(getCodeAssistServer).mockReturnValue(undefined);
32523252
const result = await config.refreshUserQuota();
32533253
expect(result).toBeUndefined();
3254-
// Never set => stays null (unknown); getter returns true so UI shows preview
3255-
expect(config.getHasAccessToPreviewModel()).toBe(true);
3254+
// Never set => stays null (unknown); getter returns false by default
3255+
expect(config.getHasAccessToPreviewModel()).toBe(false);
32563256
});
32573257

32583258
it('should return undefined if retrieveUserQuota fails', async () => {
@@ -3261,8 +3261,8 @@ describe('Config Quota & Preview Model Access', () => {
32613261
);
32623262
const result = await config.refreshUserQuota();
32633263
expect(result).toBeUndefined();
3264-
// Never set => stays null (unknown); getter returns true so UI shows preview
3265-
expect(config.getHasAccessToPreviewModel()).toBe(true);
3264+
// Never set => stays null (unknown); getter returns false by default
3265+
expect(config.getHasAccessToPreviewModel()).toBe(false);
32663266
});
32673267
it('should derive quota from remainingFraction when remainingAmount is missing', async () => {
32683268
mockCodeAssistServer.retrieveUserQuota.mockResolvedValue({

packages/core/src/config/config.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1622,10 +1622,7 @@ export class Config implements McpContext, AgentLoopContext {
16221622
this.baseLlmClient = new BaseLlmClient(this.contentGenerator, this);
16231623

16241624
const authType = this.contentGeneratorConfig.authType;
1625-
if (
1626-
authType === AuthType.USE_GEMINI ||
1627-
authType === AuthType.USE_VERTEX_AI
1628-
) {
1625+
if (authType === AuthType.USE_GEMINI) {
16291626
this.setHasAccessToPreviewModel(true);
16301627
}
16311628

@@ -2227,7 +2224,7 @@ export class Config implements McpContext, AgentLoopContext {
22272224
}
22282225

22292226
getHasAccessToPreviewModel(): boolean {
2230-
return this.hasAccessToPreviewModel !== false;
2227+
return this.hasAccessToPreviewModel ?? false;
22312228
}
22322229

22332230
setHasAccessToPreviewModel(hasAccess: boolean | null): void {
@@ -2289,14 +2286,18 @@ export class Config implements McpContext, AgentLoopContext {
22892286
});
22902287
}
22912288
}
2292-
this.emitQuotaChangedEvent();
22932289
}
22942290

22952291
const hasAccess =
22962292
quota.buckets?.some(
22972293
(b) => b.modelId && isPreviewModel(b.modelId, this),
22982294
) ?? false;
22992295
this.setHasAccessToPreviewModel(hasAccess);
2296+
2297+
if (quota.buckets) {
2298+
this.emitQuotaChangedEvent();
2299+
}
2300+
23002301
return quota;
23012302
} catch (e) {
23022303
debugLogger.debug('Failed to retrieve user quota', e);

packages/core/src/fallback/handler.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ const createMockConfig = (overrides: Partial<Config> = {}): Config =>
7777
getModel: vi.fn(() => MOCK_PRO_MODEL),
7878
getUserTier: vi.fn(() => undefined),
7979
isInteractive: vi.fn(() => false),
80+
getHasAccessToPreviewModel: vi.fn(() => false),
8081
...overrides,
8182
}) as unknown as Config;
8283

@@ -234,6 +235,7 @@ describe('handleFallback', () => {
234235
vi.mocked(policyConfig.getModel).mockReturnValue(
235236
PREVIEW_GEMINI_MODEL_AUTO,
236237
);
238+
vi.mocked(policyConfig.getHasAccessToPreviewModel).mockReturnValue(true);
237239

238240
const result = await handleFallback(
239241
policyConfig,

packages/core/src/fallback/handler.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import {
2020
applyAvailabilityTransition,
2121
} from '../availability/policyHelpers.js';
2222

23+
import { isPreviewModel } from '../config/models.js';
24+
2325
export const UPGRADE_URL_PAGE = 'https://goo.gle/set-up-gemini-code-assist';
2426

2527
export async function handleFallback(
@@ -28,13 +30,19 @@ export async function handleFallback(
2830
authType?: string,
2931
error?: unknown,
3032
): Promise<string | boolean | null> {
33+
const failureKind = classifyFailureKind(error);
34+
35+
// If a preview model is not found, record that the user lacks preview access.
36+
if (failureKind === 'not_found' && isPreviewModel(failedModel, config)) {
37+
config.setHasAccessToPreviewModel?.(false);
38+
}
39+
3140
const chain = resolvePolicyChain(config);
3241
const { failedPolicy, candidates } = buildFallbackPolicyContext(
3342
chain,
3443
failedModel,
3544
);
3645

37-
const failureKind = classifyFailureKind(error);
3846
const availability = config.getModelAvailabilityService();
3947
const getAvailabilityContext = () => {
4048
if (!failedPolicy) return undefined;

packages/core/src/policy/core-tools-mapping.test.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,26 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import { describe, it, expect } from 'vitest';
7+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
88
import { createPolicyEngineConfig } from './config.js';
99
import { PolicyEngine } from './policy-engine.js';
1010
import { PolicyDecision, ApprovalMode } from './types.js';
11+
import { Storage } from '../config/storage.js';
1112

1213
describe('PolicyEngine - Core Tools Mapping', () => {
14+
beforeEach(() => {
15+
vi.spyOn(Storage, 'getUserPoliciesDir').mockReturnValue(
16+
'/mock/user/policies',
17+
);
18+
vi.spyOn(Storage, 'getSystemPoliciesDir').mockReturnValue(
19+
'/mock/system/policies',
20+
);
21+
});
22+
23+
afterEach(() => {
24+
vi.restoreAllMocks();
25+
});
26+
1327
it('should allow tools explicitly listed in settings.tools.core', async () => {
1428
const settings = {
1529
tools: {

0 commit comments

Comments
 (0)