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 <noreply@anthropic.com>
This commit is contained in:
gitdevine
2026-06-26 11:37:40 +02:00
co-authored by Claude Fable 5
parent 92701ee54f
commit 817d388209
6 changed files with 109 additions and 1 deletions
+19
View File
@@ -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<typeof buildCurrentProviderSummary>[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: {
+12 -1
View File
@@ -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)
+17
View File
@@ -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({
+8
View File
@@ -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'
}
+20
View File
@@ -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
+33
View File
@@ -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')
})
})