mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-11-13 07:53:41 -04:00
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:
co-authored by
Claude Fable 5
parent
660e6d3845
commit
ba2bb11901
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) }
|
||||
|
||||
Reference in New Issue
Block a user