fix(gemini-vertex): saved profile wins over ambient env in profile-only routing

getAnthropicClient's saved-profile-only Vertex branch resolved the project/model
from process.env before falling back to the profile, contradicting its own
comment. A saved gemini-vertex profile targeting one project/model could route
to an ambient GOOGLE_CLOUD_PROJECT / stale GEMINI_VERTEX_MODEL when no
CLAUDE_CODE_USE_GEMINI_VERTEX flag is set. profileProject/profileModel are only
populated when routedFromProfileOnly, so leading the precedence with them makes
the selected profile win in profile-only mode while env still wins for an
explicit Vertex selection. Regression locks the precedence (profile project +
model win over ambient env).

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 a65f9b220c
commit eff1b9c0f2
2 changed files with 68 additions and 3 deletions
+63 -1
View File
@@ -1,10 +1,11 @@
import { afterEach, beforeEach, expect, test } from 'bun:test'
import { afterEach, beforeEach, expect, spyOn, test } from 'bun:test'
import { acquireSharedMutationLock, releaseSharedMutationLock } from '../../test/sharedMutationLock.js'
import {
_clearRegistryForTesting,
ensureIntegrationsLoaded,
registerGateway,
} from '../../integrations/index.js'
import * as providerProfilesModule from '../../utils/providerProfiles.js'
import { getAnthropicClient } from './client.js'
type FetchType = typeof globalThis.fetch
@@ -428,6 +429,67 @@ test('routes Gemini Vertex provider requests through the native client', async (
})
})
test('saved-profile-only Vertex prefers the profile project/model over ambient env', async () => {
let capturedUrl: string | undefined
delete process.env.CLAUDE_CODE_USE_OPENAI
delete process.env.CLAUDE_CODE_USE_GEMINI
delete process.env.CLAUDE_CODE_USE_GEMINI_VERTEX
// Ambient env that must NOT win over the selected profile in profile-only mode.
process.env.GOOGLE_CLOUD_PROJECT = 'ambient-project'
process.env.GEMINI_VERTEX_MODEL = 'ambient-model'
process.env.GEMINI_VERTEX_LOCATION = 'us-central1'
process.env.GEMINI_ACCESS_TOKEN = 'vertex-access-token'
const spy = spyOn(
providerProfilesModule,
'getActiveProviderProfile',
).mockReturnValue({
id: 'vertex_prof',
provider: 'gemini-vertex',
name: 'Vertex',
baseUrl: 'profile-project',
model: 'profile-model',
} as ReturnType<typeof providerProfilesModule.getActiveProviderProfile>)
globalThis.fetch = (async (input, _init) => {
capturedUrl =
typeof input === 'string'
? input
: input instanceof URL
? input.toString()
: input.url
return new Response(
JSON.stringify({
candidates: [{ content: { parts: [{ text: 'vertex ok' }] } }],
}),
{ headers: { 'Content-Type': 'application/json' } },
)
}) as FetchType
try {
const client = (await getAnthropicClient({
maxRetries: 0,
})) as unknown as ShimClient
await client.beta.messages.create({
model: 'profile-model',
messages: [{ role: 'user', content: 'hello' }],
max_tokens: 64,
stream: false,
})
// Profile project + model win; the ambient GOOGLE_CLOUD_PROJECT / stale
// GEMINI_VERTEX_MODEL must not leak into the request.
expect(capturedUrl).toContain('/projects/profile-project/')
expect(capturedUrl).toContain('/models/profile-model:generateContent')
expect(capturedUrl).not.toContain('ambient-project')
expect(capturedUrl).not.toContain('ambient-model')
} finally {
spy.mockRestore()
}
})
test('Gemini Vertex provider uses GEMINI_VERTEX_MODEL instead of generic OPENAI_MODEL', async () => {
let capturedUrl: string | undefined
+5 -2
View File
@@ -555,14 +555,17 @@ export async function getAnthropicClient({
const profileModel = routedFromProfileOnly && activeProfile?.model
? getPrimaryModel(activeProfile.model)
: undefined
const project = getGeminiVertexProjectId(process.env) ?? profileProject
// In profile-only routing the selected profile must win over ambient env;
// profileProject/profileModel are only set when routedFromProfileOnly, so
// leading with them keeps env precedence for an explicit Vertex selection.
const project = profileProject ?? getGeminiVertexProjectId(process.env)
const location = getGeminiVertexLocation(process.env)
// getGeminiVertexModel already falls back to the default model; the trailing
// literal only satisfies its `string | undefined` signature (it never
// actually returns undefined) so geminiVertexModel stays a plain string.
const geminiVertexModel =
process.env.GEMINI_VERTEX_MODEL?.trim() ||
profileModel ||
process.env.GEMINI_VERTEX_MODEL?.trim() ||
model?.trim() ||
getGeminiVertexModel(process.env) ||
DEFAULT_GEMINI_VERTEX_MODEL