fix(gemini-vertex): address CodeRabbit follow-up — credentials, URL guards, tests

- providerProfiles.ts / providerProfile.ts: carry the active credential forward
  when applying a saved Gemini Vertex profile. buildCompatibilityProcessEnv
  clears every managed key (incl. GEMINI_ACCESS_TOKEN /
  GOOGLE_APPLICATION_CREDENTIALS), so a profile switch could relaunch unauthed;
  re-add the access token (access-token mode) or ADC file (adc mode).
- providerProfiles.ts (startup-profile path) and provider-bootstrap.ts: extend
  the endpoint-URL guard to the remaining sites that fed baseUrl into the Vertex
  project — drop URL-shaped values via geminiVertexProjectFromProfile() and
  reject http(s) --base-url in the bootstrap script.
- Tests: add applyModelFlagFromArgs GEMINI_VERTEX_MODEL coverage; lock
  access-token / ADC credential preservation on profile apply; isolate every
  provider-selection flag in the cache-scope test via PROVIDER_SELECTION_FLAGS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
gitdevine
2026-06-26 11:37:39 +02:00
co-authored by Claude Fable 5
parent 660e6d3845
commit ba2bb11901
6 changed files with 90 additions and 13 deletions
+6 -1
View File
@@ -97,7 +97,12 @@ async function main(): Promise<void> {
} else if (selected === 'gemini-vertex') {
// Reuse the runtime project resolver (same normalization as the provider
// validator) rather than an ad-hoc OR chain.
const project = argBaseUrl?.trim() || getGeminiVertexProjectId(process.env) || null
const projectFromArg = argBaseUrl?.trim()
if (projectFromArg && /^https?:\/\//i.test(projectFromArg)) {
console.error('Gemini Vertex --base-url expects a Google Cloud project id, not an API endpoint URL.')
process.exit(1)
}
const project = projectFromArg || getGeminiVertexProjectId(process.env) || null
if (!project) {
console.error('Gemini Vertex profile requires a project. Use --base-url <project> or set GEMINI_VERTEX_PROJECT/GOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT/GOOGLE_PROJECT_ID.')
process.exit(1)
+17 -7
View File
@@ -8,6 +8,7 @@ import {
resolveProviderRequest,
shouldAttemptLocalToollessRetry,
} from './providerConfig.js'
import { PROVIDER_SELECTION_FLAGS } from '../../utils/providerSelectionFlags.js'
const originalEnv = {
CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI,
@@ -330,17 +331,26 @@ test('does not classify Gemini Vertex sessions as first-party bootstrap traffic'
// Maintainer P2: with only the Gemini Vertex flag set, the cache scope
// must be null (third-party), not 'firstParty' — otherwise bootstrap
// options are fetched through the Anthropic first-party path.
const priorUseOpenAI = process.env.CLAUDE_CODE_USE_OPENAI
// A leaked CLAUDE_CODE_USE_OPENAI from the runner env would take the OpenAI
// branch and make this pass/fail for the wrong reason.
delete process.env.CLAUDE_CODE_USE_OPENAI
//
// Isolate every provider-selection flag (not just CLAUDE_CODE_USE_OPENAI) so a
// leaked flag from the runner env can't take another branch and make this
// pass/fail for the wrong reason — the null result must be attributable to
// Gemini Vertex selection alone.
const priorFlags = new Map<string, string | undefined>()
for (const flag of PROVIDER_SELECTION_FLAGS) {
priorFlags.set(flag, process.env[flag])
delete process.env[flag]
}
process.env.CLAUDE_CODE_USE_GEMINI_VERTEX = '1'
try {
expect(getAdditionalModelOptionsCacheScope()).toBeNull()
} finally {
delete process.env.CLAUDE_CODE_USE_GEMINI_VERTEX
if (priorUseOpenAI !== undefined) {
process.env.CLAUDE_CODE_USE_OPENAI = priorUseOpenAI
for (const [flag, value] of priorFlags) {
if (value === undefined) {
delete process.env[flag]
} else {
process.env[flag] = value
}
}
}
})
+6
View File
@@ -855,6 +855,12 @@ describe('applyModelFlagFromArgs', () => {
expect(process.env.GEMINI_MODEL).toBe('gemini-2.0-flash')
})
test('sets GEMINI_VERTEX_MODEL when CLAUDE_CODE_USE_GEMINI_VERTEX is active', () => {
process.env.CLAUDE_CODE_USE_GEMINI_VERTEX = '1'
applyModelFlagFromArgs(['--model', 'gemini-2.5-flash'])
expect(process.env.GEMINI_VERTEX_MODEL).toBe('gemini-2.5-flash')
})
test('sets MISTRAL_MODEL when CLAUDE_CODE_USE_MISTRAL is active', () => {
process.env.CLAUDE_CODE_USE_MISTRAL = '1'
applyModelFlagFromArgs(['--model', 'devstral-latest'])
+8
View File
@@ -1654,6 +1654,14 @@ export async function buildLaunchEnv(options: {
if (googleApplicationCredentials) {
env.GOOGLE_APPLICATION_CREDENTIALS = googleApplicationCredentials
}
// PROFILE_ENV_KEYS clears GEMINI_ACCESS_TOKEN on relaunch; an access-token
// Vertex profile must carry its bearer forward or it relaunches unauthed.
if (env.GEMINI_VERTEX_AUTH_MODE === 'access-token') {
const geminiAccessToken = sanitizeApiKey(
processEnv.GEMINI_ACCESS_TOKEN || persistedEnv.GEMINI_ACCESS_TOKEN,
)
if (geminiAccessToken) env.GEMINI_ACCESS_TOKEN = geminiAccessToken
}
return buildCompatibilityProcessEnv({
processEnv,
+28
View File
@@ -369,6 +369,34 @@ describe('applyProviderProfileToProcessEnv', () => {
expect(process.env.GEMINI_VERTEX_PROJECT).toBe('my-gcp-project')
})
test('gemini vertex access-token profile preserves GEMINI_ACCESS_TOKEN', async () => {
const { applyProviderProfileToProcessEnv } =
await importFreshProviderProfileModules()
// buildCompatibilityProcessEnv clears managed keys; the access token must
// survive a profile switch or the first request relaunches unauthed.
process.env.GEMINI_VERTEX_AUTH_MODE = 'access-token'
process.env.GEMINI_ACCESS_TOKEN = 'ya29.test-bearer'
applyProviderProfileToProcessEnv(
buildGeminiVertexProfile({ baseUrl: 'my-gcp-project' }),
)
expect(process.env.GEMINI_ACCESS_TOKEN).toBe('ya29.test-bearer')
})
test('gemini vertex adc profile preserves GOOGLE_APPLICATION_CREDENTIALS', async () => {
const { applyProviderProfileToProcessEnv } =
await importFreshProviderProfileModules()
process.env.GEMINI_VERTEX_AUTH_MODE = 'adc'
process.env.GOOGLE_APPLICATION_CREDENTIALS = '/tmp/adc.json'
applyProviderProfileToProcessEnv(
buildGeminiVertexProfile({ baseUrl: 'my-gcp-project' }),
)
expect(process.env.GOOGLE_APPLICATION_CREDENTIALS).toBe('/tmp/adc.json')
})
test('bedrock profile sets CLAUDE_CODE_USE_BEDROCK and preserves anthropic model routing', async () => {
const { applyProviderProfileToProcessEnv } =
await importFreshProviderProfileModules()
+25 -5
View File
@@ -844,6 +844,11 @@ export function applyProviderProfileToProcessEnv(
...(profile.apiKey ? { GEMINI_API_KEY: profile.apiKey } : {}),
}
} else if (compatibilityMode === 'gemini-vertex') {
const vertexAuthMode =
process.env.GEMINI_VERTEX_AUTH_MODE === 'access-token' ||
process.env.GEMINI_VERTEX_AUTH_MODE === 'adc'
? process.env.GEMINI_VERTEX_AUTH_MODE
: 'adc'
profileEnv = buildGeminiVertexProfileEnv({
model: primaryModel,
project:
@@ -853,11 +858,19 @@ export function applyProviderProfileToProcessEnv(
process.env.GCLOUD_PROJECT ||
process.env.GOOGLE_PROJECT_ID,
location: process.env.GEMINI_VERTEX_LOCATION,
authMode:
process.env.GEMINI_VERTEX_AUTH_MODE === 'access-token' || process.env.GEMINI_VERTEX_AUTH_MODE === 'adc'
? process.env.GEMINI_VERTEX_AUTH_MODE
: 'adc',
authMode: vertexAuthMode,
})
// buildCompatibilityProcessEnv clears every managed key (incl.
// GEMINI_ACCESS_TOKEN / GOOGLE_APPLICATION_CREDENTIALS) before applying
// profileEnv, so carry the active credential forward or selecting a Vertex
// profile would erase it before the first request.
if (vertexAuthMode === 'access-token') {
const accessToken = trimOrUndefined(process.env.GEMINI_ACCESS_TOKEN)
if (accessToken) profileEnv.GEMINI_ACCESS_TOKEN = accessToken
} else {
const adcFile = trimOrUndefined(process.env.GOOGLE_APPLICATION_CREDENTIALS)
if (adcFile) profileEnv.GOOGLE_APPLICATION_CREDENTIALS = adcFile
}
} else if (isGithubCompatibilityMode(compatibilityMode)) {
profileEnv = buildGithubCompatibleProfileEnv({
model: primaryModel,
@@ -1359,7 +1372,14 @@ function buildStartupProfileFromActiveProfile(
case 'gemini-vertex': {
const env = buildGeminiVertexProfileEnv({
model: getPrimaryModel(activeProfile.model),
project: activeProfile.baseUrl,
// baseUrl carries the project id, but the preset can seed it with the
// endpoint URL — never let a URL through as a project.
project:
geminiVertexProjectFromProfile(activeProfile.baseUrl) ||
process.env.GEMINI_VERTEX_PROJECT ||
process.env.GOOGLE_CLOUD_PROJECT ||
process.env.GCLOUD_PROJECT ||
process.env.GOOGLE_PROJECT_ID,
processEnv: process.env,
})
return { profile: 'gemini-vertex', env: applySupportedProfileCustomHeaders(activeProfile, env) }