From 817d3882092fb515df78a4e927db07905397816a Mon Sep 17 00:00:00 2001 From: gitdevine <210788251+gitdevine@users.noreply.github.com> Date: Fri, 19 Jun 2026 14:49:31 +0200 Subject: [PATCH] fix(gemini-vertex): thread saved-profile-only routing through provider identity The saved-profile-only Vertex route (active gemini-vertex profile, no CLAUDE_CODE_USE_GEMINI_VERTEX flag) was only honoured by the API client, so other env-flag-only "is this Vertex?" checks disagreed with it: - routeMetadata.ts: resolveActiveRouteIdFromEnv() now returns 'gemini-vertex' when activeProfileProvider is gemini-vertex and no provider env flag is set, so /model loads the Vertex catalog/discovery for the same session. - providerConfig.ts: getAdditionalModelOptionsCacheScope() scopes a saved gemini-vertex profile as third-party (null) instead of 'firstParty', so bootstrap/model-option traffic is not fetched through the Anthropic path. - provider.tsx: buildCurrentProviderSummary() reports the effective provider so a saved Vertex profile no longer shows "Anthropic"/api.anthropic.com. Tests cover each: saved-profile route resolution (and precedence when a flag is set), the cache-scope fallback, and the provider summary. Co-Authored-By: Claude Fable 5 --- src/commands/provider/provider.test.tsx | 19 ++++++++++++++ src/commands/provider/provider.tsx | 13 +++++++++- src/integrations/routeMetadata.test.ts | 17 +++++++++++++ src/integrations/routeMetadata.ts | 8 ++++++ src/services/api/providerConfig.ts | 20 +++++++++++++++ src/utils/providerProfiles.test.ts | 33 +++++++++++++++++++++++++ 6 files changed, 109 insertions(+), 1 deletion(-) diff --git a/src/commands/provider/provider.test.tsx b/src/commands/provider/provider.test.tsx index f125dcdc8..8183da136 100644 --- a/src/commands/provider/provider.test.tsx +++ b/src/commands/provider/provider.test.tsx @@ -621,6 +621,25 @@ test('buildCurrentProviderSummary redacts poisoned model and endpoint values', ( expect(summary.endpointLabel).toBe('sk-...678') }) +test('buildCurrentProviderSummary reports Gemini Vertex for a saved profile with no env flag', () => { + // Saved-profile-only routing: no CLAUDE_CODE_USE_GEMINI_VERTEX, but the active + // profile is gemini-vertex, so the summary must match the client's routing + // instead of falling through to the Anthropic branch. + const summary = buildCurrentProviderSummary({ + processEnv: { GEMINI_VERTEX_PROJECT: 'my-proj' } as NodeJS.ProcessEnv, + persisted: { + profile: 'gemini-vertex', + env: { CLAUDE_CODE_USE_GEMINI_VERTEX: '1' }, + } as unknown as NonNullable< + Parameters[0] + >['persisted'], + }) + + expect(summary.providerLabel).toContain('Vertex') + expect(summary.providerLabel).not.toBe('Anthropic') + expect(summary.endpointLabel).toContain('/projects/my-proj/') +}) + test('buildCurrentProviderSummary labels generic local openai-compatible providers', () => { const summary = buildCurrentProviderSummary({ processEnv: { diff --git a/src/commands/provider/provider.tsx b/src/commands/provider/provider.tsx index c02c89283..de0837059 100644 --- a/src/commands/provider/provider.tsx +++ b/src/commands/provider/provider.tsx @@ -20,6 +20,7 @@ import { probeRouteReadiness } from '../../integrations/discoveryService.js' import { getProviderPresetUiMetadata, getRouteLabel, + resolveActiveRouteIdFromEnv, resolveRouteIdFromBaseUrl, } from '../../integrations/index.js' import { @@ -294,7 +295,17 @@ export function buildCurrentProviderSummary(options?: { const persisted = options?.persisted ?? loadProfileFile() const savedProfileLabel = persisted?.profile ?? 'none' - if (isEnvTruthy(processEnv.CLAUDE_CODE_USE_GEMINI_VERTEX)) { + // Honour saved-profile-only Gemini Vertex routing (no env flag) so the summary + // matches getAnthropicClient instead of falling through to the Anthropic + // branch for a profile that will actually drive Vertex requests. + const effectiveRouteId = resolveActiveRouteIdFromEnv(processEnv, { + activeProfileProvider: persisted?.profile, + }) + + if ( + isEnvTruthy(processEnv.CLAUDE_CODE_USE_GEMINI_VERTEX) || + effectiveRouteId === 'gemini-vertex' + ) { const vertexMetadata = getProviderPresetUiMetadata('gemini-vertex', processEnv) const location = getGeminiVertexLocation(processEnv) const project = getGeminiVertexProjectId(processEnv) diff --git a/src/integrations/routeMetadata.test.ts b/src/integrations/routeMetadata.test.ts index 89fe7f28b..49fc6b6c4 100644 --- a/src/integrations/routeMetadata.test.ts +++ b/src/integrations/routeMetadata.test.ts @@ -185,6 +185,23 @@ test('resolveActiveRouteIdFromEnv gives gemini-vertex precedence over gemini and ).toBe('gemini-vertex') }) +test('resolveActiveRouteIdFromEnv resolves a saved-profile-only gemini-vertex route with no env flag', () => { + // Matches getAnthropicClient's saved-profile routing so /model loads the + // Vertex catalog for the same session. + expect( + resolveActiveRouteIdFromEnv({}, { activeProfileProvider: 'gemini-vertex' }), + ).toBe('gemini-vertex') +}) + +test('resolveActiveRouteIdFromEnv ignores a saved gemini-vertex profile when another provider flag is set', () => { + expect( + resolveActiveRouteIdFromEnv( + { CLAUDE_CODE_USE_GEMINI: '1' }, + { activeProfileProvider: 'gemini-vertex' }, + ), + ).toBe('gemini') +}) + test('resolveActiveRouteIdFromEnv treats Xiaomi MiMo credential-only env as Xiaomi MiMo', () => { expect( resolveActiveRouteIdFromEnv({ diff --git a/src/integrations/routeMetadata.ts b/src/integrations/routeMetadata.ts index 992c65eda..79dc9a3c3 100644 --- a/src/integrations/routeMetadata.ts +++ b/src/integrations/routeMetadata.ts @@ -855,6 +855,14 @@ export function resolveActiveRouteIdFromEnv( return 'custom' } + // Saved-profile-only Gemini Vertex: with no provider env flag set, the API + // client still routes to the native Vertex client from the active profile + // (shouldRouteToGeminiVertexFromProfile). Keep route resolution in sync, or + // /model would skip the Vertex catalog/discovery for the same session. + if (options?.activeProfileProvider === 'gemini-vertex') { + return 'gemini-vertex' + } + return 'anthropic' } diff --git a/src/services/api/providerConfig.ts b/src/services/api/providerConfig.ts index 0785d7cdd..28f026a17 100644 --- a/src/services/api/providerConfig.ts +++ b/src/services/api/providerConfig.ts @@ -12,6 +12,7 @@ import { import { logForDebugging } from '../../utils/debug.js' import { isEnvTruthy } from '../../utils/envUtils.js' import { PROVIDER_SELECTION_FLAGS } from '../../utils/providerSelectionFlags.js' +import { getGlobalConfig } from '../../utils/config.js' import { asTrimmedString, parseChatgptAccountId, @@ -972,6 +973,18 @@ export function resolveProviderRequest(options?: { } } +/** + * Provider of the saved active profile, if any. Used to detect saved-profile-only + * routing (no provider env flag) that the API client honours via + * shouldRouteToGeminiVertexFromProfile but env-flag checks would miss. + */ +function getActiveProfileProviderId(): string | undefined { + const config = getGlobalConfig() + return config.providerProfiles?.find( + profile => profile.id === config.activeProviderProfileId, + )?.provider +} + export function getAdditionalModelOptionsCacheScope(): string | null { if (!isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI)) { const anotherProviderSelected = PROVIDER_SELECTION_FLAGS.some( @@ -979,6 +992,13 @@ export function getAdditionalModelOptionsCacheScope(): string | null { flag !== 'CLAUDE_CODE_USE_OPENAI' && isEnvTruthy(process.env[flag]), ) if (!anotherProviderSelected) { + // No provider env flag is set, but a saved Gemini Vertex profile still + // routes the client to the native Vertex API. Scope it as third-party + // (null) so bootstrap/model-option traffic is not treated as first-party + // Anthropic — matching getAnthropicClient's saved-profile routing. + if (getActiveProfileProviderId() === 'gemini-vertex') { + return null + } return 'firstParty' } return null diff --git a/src/utils/providerProfiles.test.ts b/src/utils/providerProfiles.test.ts index c6a4a480b..7a6b49c13 100644 --- a/src/utils/providerProfiles.test.ts +++ b/src/utils/providerProfiles.test.ts @@ -3232,3 +3232,36 @@ describe('isGeminiVertexEffectiveProvider', () => { ).toBe(false) }) }) + +describe('getAdditionalModelOptionsCacheScope — saved-profile Gemini Vertex', () => { + test('is not scoped as first-party when a saved gemini-vertex profile is active without an env flag', async () => { + const { getAdditionalModelOptionsCacheScope } = await import( + '../services/api/providerConfig.js' + ) + // beforeEach clears every CLAUDE_CODE_USE_* flag; the saved profile is what + // routes the client to Vertex, so the cache scope must be third-party (null) + // rather than first-party Anthropic bootstrap traffic. + saveMockGlobalConfig(current => ({ + ...current, + providerProfiles: [ + buildGeminiVertexProfile({ id: 'vertex_prof', baseUrl: 'my-proj' }), + ], + activeProviderProfileId: 'vertex_prof', + })) + + expect(getAdditionalModelOptionsCacheScope()).toBeNull() + }) + + test('remains first-party with no provider flag and no saved provider profile', async () => { + const { getAdditionalModelOptionsCacheScope } = await import( + '../services/api/providerConfig.js' + ) + saveMockGlobalConfig(current => ({ + ...current, + providerProfiles: [], + activeProviderProfileId: undefined, + })) + + expect(getAdditionalModelOptionsCacheScope()).toBe('firstParty') + }) +})