From a2f4f7ff8514b734eeb2f715a4cac436dfa745fa Mon Sep 17 00:00:00 2001 From: galz10 Date: Wed, 13 May 2026 10:24:16 -0700 Subject: [PATCH 1/3] fix(core): ensure stable fallback for restricted preview models Corrects model selection failures for GCP/Google Auth users by: 1. Verifying preview access via quota instead of assuming true for GCP. 2. Preserving original model intent in policyHelpers to ensure correct downgrade to the Gemini 2.5 stable chain. --- packages/core/src/availability/policyHelpers.ts | 11 +++++++---- packages/core/src/config/config.ts | 5 +---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/core/src/availability/policyHelpers.ts b/packages/core/src/availability/policyHelpers.ts index 28447bd8367..411985ea03f 100644 --- a/packages/core/src/availability/policyHelpers.ts +++ b/packages/core/src/availability/policyHelpers.ts @@ -57,6 +57,9 @@ export function resolvePolicyChain( const useCustomToolModel = config.getUseCustomToolModelSync?.() ?? false; const hasAccessToPreview = config.getHasAccessToPreviewModel?.() ?? true; + // Capture the original family intent before any normalization or early downgrade. + const isOriginallyGemini3 = isGemini3Model(modelFromConfig, config); + const resolvedModel = normalizeModelId( resolveModel( modelFromConfig, @@ -78,7 +81,7 @@ export function resolvePolicyChain( wrapsAround || isAutoPreferred || isAutoConfigured || - isGemini3Model(resolvedModel, config); + isOriginallyGemini3; // --- DYNAMIC PATH --- if (config.getExperimentalDynamicModelConfiguration?.() === true) { @@ -91,7 +94,7 @@ export function resolvePolicyChain( if (resolvedModel === DEFAULT_GEMINI_FLASH_LITE_MODEL) { chain = config.modelConfigService.resolveChain('lite', context); } else if ( - isGemini3Model(normalizeModelId(resolvedModel), config) || + isOriginallyGemini3 || isAutoPreferred || isAutoConfigured ) { @@ -132,14 +135,14 @@ export function resolvePolicyChain( if (resolvedModel === DEFAULT_GEMINI_FLASH_LITE_MODEL) { chain = getFlashLitePolicyChain(); } else if ( - isGemini3Model(resolvedModel, config) || + isOriginallyGemini3 || isAutoPreferred || isAutoConfigured ) { const isAutoSelection = isAutoPreferred || isAutoConfigured; if (hasAccessToPreview) { const previewEnabled = - isGemini3Model(resolvedModel, config) || + isOriginallyGemini3 || normalizedPreferredModel === PREVIEW_GEMINI_MODEL_AUTO || configuredModel === PREVIEW_GEMINI_MODEL_AUTO; chain = getModelPolicyChain({ diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index b81737b0ea4..64546362468 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -1630,10 +1630,7 @@ export class Config implements McpContext, AgentLoopContext { this.baseLlmClient = new BaseLlmClient(this.contentGenerator, this); const authType = this.contentGeneratorConfig.authType; - if ( - authType === AuthType.USE_GEMINI || - authType === AuthType.USE_VERTEX_AI - ) { + if (authType === AuthType.USE_GEMINI) { this.setHasAccessToPreviewModel(true); } From 837e3f0ecf662bb8bd1c06a3657fa59b7f7c9afd Mon Sep 17 00:00:00 2001 From: galz10 Date: Wed, 13 May 2026 13:49:44 -0700 Subject: [PATCH 2/3] chore: fix lint and build errors --- .../autoRoutingFallback.integration.test.ts | 3 +++ .../availability/fallbackIntegration.test.ts | 1 + .../src/availability/policyHelpers.test.ts | 1 + .../core/src/availability/policyHelpers.ts | 19 ++++--------------- packages/core/src/code_assist/setup.test.ts | 1 + packages/core/src/config/config.test.ts | 8 ++++---- packages/core/src/config/config.ts | 8 ++++++-- packages/core/src/fallback/handler.test.ts | 2 ++ packages/core/src/fallback/handler.ts | 10 +++++++++- .../src/policy/core-tools-mapping.test.ts | 16 +++++++++++++++- 10 files changed, 46 insertions(+), 23 deletions(-) diff --git a/packages/core/src/availability/autoRoutingFallback.integration.test.ts b/packages/core/src/availability/autoRoutingFallback.integration.test.ts index f4e157503bf..9ea062e1ab0 100644 --- a/packages/core/src/availability/autoRoutingFallback.integration.test.ts +++ b/packages/core/src/availability/autoRoutingFallback.integration.test.ts @@ -29,6 +29,9 @@ describe('Auto Routing Fallback Integration', () => { beforeEach(() => { vi.useFakeTimers(); + vi.spyOn(Config.prototype, 'getHasAccessToPreviewModel').mockReturnValue( + true, + ); // Mock fs to avoid real file system access vi.mocked(fs.existsSync).mockReturnValue(true); diff --git a/packages/core/src/availability/fallbackIntegration.test.ts b/packages/core/src/availability/fallbackIntegration.test.ts index 6c49938ed91..1ee53da37e1 100644 --- a/packages/core/src/availability/fallbackIntegration.test.ts +++ b/packages/core/src/availability/fallbackIntegration.test.ts @@ -28,6 +28,7 @@ describe('Fallback Integration', () => { getActiveModel: () => PREVIEW_GEMINI_MODEL_AUTO, setActiveModel: vi.fn(), getUserTier: () => undefined, + getHasAccessToPreviewModel: () => true, getModelAvailabilityService: () => availabilityService, modelConfigService: undefined as unknown as ModelConfigService, } as unknown as Config; diff --git a/packages/core/src/availability/policyHelpers.test.ts b/packages/core/src/availability/policyHelpers.test.ts index 7db99bb1aa5..2eb6f75e0a0 100644 --- a/packages/core/src/availability/policyHelpers.test.ts +++ b/packages/core/src/availability/policyHelpers.test.ts @@ -36,6 +36,7 @@ const createMockConfig = (overrides: Partial = {}): Config => { const authType = config.getContentGeneratorConfig().authType; return useGemini31 && authType === AuthType.USE_GEMINI; }, + getHasAccessToPreviewModel: () => true, getContentGeneratorConfig: () => ({ authType: undefined }), getMaxAttemptsPerTurn: () => 3, ...overrides, diff --git a/packages/core/src/availability/policyHelpers.ts b/packages/core/src/availability/policyHelpers.ts index 411985ea03f..69ce1996814 100644 --- a/packages/core/src/availability/policyHelpers.ts +++ b/packages/core/src/availability/policyHelpers.ts @@ -55,7 +55,7 @@ export function resolvePolicyChain( const useGemini31FlashLite = config.getGemini31FlashLiteLaunchedSync?.() ?? false; const useCustomToolModel = config.getUseCustomToolModelSync?.() ?? false; - const hasAccessToPreview = config.getHasAccessToPreviewModel?.() ?? true; + const hasAccessToPreview = config.getHasAccessToPreviewModel?.() ?? false; // Capture the original family intent before any normalization or early downgrade. const isOriginallyGemini3 = isGemini3Model(modelFromConfig, config); @@ -78,10 +78,7 @@ export function resolvePolicyChain( // We always wrap around for Gemini 3 chains to ensure maximum availability // between models in the same family (e.g. fallback to Pro if Flash is exhausted). const effectiveWrapsAround = - wrapsAround || - isAutoPreferred || - isAutoConfigured || - isOriginallyGemini3; + wrapsAround || isAutoPreferred || isAutoConfigured || isOriginallyGemini3; // --- DYNAMIC PATH --- if (config.getExperimentalDynamicModelConfiguration?.() === true) { @@ -93,11 +90,7 @@ export function resolvePolicyChain( if (resolvedModel === DEFAULT_GEMINI_FLASH_LITE_MODEL) { chain = config.modelConfigService.resolveChain('lite', context); - } else if ( - isOriginallyGemini3 || - isAutoPreferred || - isAutoConfigured - ) { + } else if (isOriginallyGemini3 || isAutoPreferred || isAutoConfigured) { // 1. Try to find a chain specifically for the current configured alias if ( isAutoConfigured && @@ -134,11 +127,7 @@ export function resolvePolicyChain( if (resolvedModel === DEFAULT_GEMINI_FLASH_LITE_MODEL) { chain = getFlashLitePolicyChain(); - } else if ( - isOriginallyGemini3 || - isAutoPreferred || - isAutoConfigured - ) { + } else if (isOriginallyGemini3 || isAutoPreferred || isAutoConfigured) { const isAutoSelection = isAutoPreferred || isAutoConfigured; if (hasAccessToPreview) { const previewEnabled = diff --git a/packages/core/src/code_assist/setup.test.ts b/packages/core/src/code_assist/setup.test.ts index 6779143b9a2..a76525470f4 100644 --- a/packages/core/src/code_assist/setup.test.ts +++ b/packages/core/src/code_assist/setup.test.ts @@ -228,6 +228,7 @@ describe('setupUser', () => { }); it('should throw InvalidNumericProjectIdError when GOOGLE_CLOUD_PROJECT_ID is numeric', async () => { + vi.stubEnv('GOOGLE_CLOUD_PROJECT', ''); vi.stubEnv('GOOGLE_CLOUD_PROJECT_ID', '1234567890'); await expect(setupUser({} as OAuth2Client, mockConfig)).rejects.toThrow( InvalidNumericProjectIdError, diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index 66965411070..60e76a6cb45 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -3237,8 +3237,8 @@ describe('Config Quota & Preview Model Access', () => { vi.mocked(getCodeAssistServer).mockReturnValue(undefined); const result = await config.refreshUserQuota(); expect(result).toBeUndefined(); - // Never set => stays null (unknown); getter returns true so UI shows preview - expect(config.getHasAccessToPreviewModel()).toBe(true); + // Never set => stays null (unknown); getter returns false by default + expect(config.getHasAccessToPreviewModel()).toBe(false); }); it('should return undefined if retrieveUserQuota fails', async () => { @@ -3247,8 +3247,8 @@ describe('Config Quota & Preview Model Access', () => { ); const result = await config.refreshUserQuota(); expect(result).toBeUndefined(); - // Never set => stays null (unknown); getter returns true so UI shows preview - expect(config.getHasAccessToPreviewModel()).toBe(true); + // Never set => stays null (unknown); getter returns false by default + expect(config.getHasAccessToPreviewModel()).toBe(false); }); it('should derive quota from remainingFraction when remainingAmount is missing', async () => { mockCodeAssistServer.retrieveUserQuota.mockResolvedValue({ diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 64546362468..5e7b6ae3a5e 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -2199,7 +2199,7 @@ export class Config implements McpContext, AgentLoopContext { } getHasAccessToPreviewModel(): boolean { - return this.hasAccessToPreviewModel !== false; + return this.hasAccessToPreviewModel ?? false; } setHasAccessToPreviewModel(hasAccess: boolean | null): void { @@ -2261,7 +2261,6 @@ export class Config implements McpContext, AgentLoopContext { }); } } - this.emitQuotaChangedEvent(); } const hasAccess = @@ -2269,6 +2268,11 @@ export class Config implements McpContext, AgentLoopContext { (b) => b.modelId && isPreviewModel(b.modelId, this), ) ?? false; this.setHasAccessToPreviewModel(hasAccess); + + if (quota.buckets) { + this.emitQuotaChangedEvent(); + } + return quota; } catch (e) { debugLogger.debug('Failed to retrieve user quota', e); diff --git a/packages/core/src/fallback/handler.test.ts b/packages/core/src/fallback/handler.test.ts index 698a5d7cfbf..0bc3096f70f 100644 --- a/packages/core/src/fallback/handler.test.ts +++ b/packages/core/src/fallback/handler.test.ts @@ -77,6 +77,7 @@ const createMockConfig = (overrides: Partial = {}): Config => getModel: vi.fn(() => MOCK_PRO_MODEL), getUserTier: vi.fn(() => undefined), isInteractive: vi.fn(() => false), + getHasAccessToPreviewModel: vi.fn(() => false), ...overrides, }) as unknown as Config; @@ -234,6 +235,7 @@ describe('handleFallback', () => { vi.mocked(policyConfig.getModel).mockReturnValue( PREVIEW_GEMINI_MODEL_AUTO, ); + vi.mocked(policyConfig.getHasAccessToPreviewModel).mockReturnValue(true); const result = await handleFallback( policyConfig, diff --git a/packages/core/src/fallback/handler.ts b/packages/core/src/fallback/handler.ts index f216f9216cd..3e8dcc52670 100644 --- a/packages/core/src/fallback/handler.ts +++ b/packages/core/src/fallback/handler.ts @@ -20,6 +20,8 @@ import { applyAvailabilityTransition, } from '../availability/policyHelpers.js'; +import { isPreviewModel } from '../config/models.js'; + export const UPGRADE_URL_PAGE = 'https://goo.gle/set-up-gemini-code-assist'; export async function handleFallback( @@ -28,13 +30,19 @@ export async function handleFallback( authType?: string, error?: unknown, ): Promise { + const failureKind = classifyFailureKind(error); + + // If a preview model is not found, record that the user lacks preview access. + if (failureKind === 'not_found' && isPreviewModel(failedModel, config)) { + config.setHasAccessToPreviewModel?.(false); + } + const chain = resolvePolicyChain(config); const { failedPolicy, candidates } = buildFallbackPolicyContext( chain, failedModel, ); - const failureKind = classifyFailureKind(error); const availability = config.getModelAvailabilityService(); const getAvailabilityContext = () => { if (!failedPolicy) return undefined; diff --git a/packages/core/src/policy/core-tools-mapping.test.ts b/packages/core/src/policy/core-tools-mapping.test.ts index 95877c6ac44..8ef042d6bba 100644 --- a/packages/core/src/policy/core-tools-mapping.test.ts +++ b/packages/core/src/policy/core-tools-mapping.test.ts @@ -4,12 +4,26 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { createPolicyEngineConfig } from './config.js'; import { PolicyEngine } from './policy-engine.js'; import { PolicyDecision, ApprovalMode } from './types.js'; +import { Storage } from '../config/storage.js'; describe('PolicyEngine - Core Tools Mapping', () => { + beforeEach(() => { + vi.spyOn(Storage, 'getUserPoliciesDir').mockReturnValue( + '/mock/user/policies', + ); + vi.spyOn(Storage, 'getSystemPoliciesDir').mockReturnValue( + '/mock/system/policies', + ); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + it('should allow tools explicitly listed in settings.tools.core', async () => { const settings = { tools: { From b72ce1b9c6839c1f7c81f75060d017e773e8306d Mon Sep 17 00:00:00 2001 From: galz10 Date: Wed, 13 May 2026 14:23:13 -0700 Subject: [PATCH 3/3] chore: resolve duplicate --- packages/core/src/availability/policyHelpers.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/availability/policyHelpers.test.ts b/packages/core/src/availability/policyHelpers.test.ts index aa312669ca5..dae41fe6563 100644 --- a/packages/core/src/availability/policyHelpers.test.ts +++ b/packages/core/src/availability/policyHelpers.test.ts @@ -36,7 +36,6 @@ const createMockConfig = (overrides: Partial = {}): Config => { const authType = config.getContentGeneratorConfig().authType; return useGemini31 && authType === AuthType.USE_GEMINI; }, - getHasAccessToPreviewModel: () => true, getContentGeneratorConfig: () => ({ authType: undefined }), getHasAccessToPreviewModel: () => true, getMaxAttemptsPerTurn: () => 3,