feat: add Gemini ADC and access token auth (#312)
* feat: add Gemini ADC and access token auth * feat: add Gemini token and ADC provider setup * feat: add Gemini token and ADC provider setup * fix: honor Gemini auth mode on restart
This commit is contained in:
@@ -197,6 +197,23 @@ test('buildProfileSaveMessage maps provider fields without echoing secrets', ()
|
||||
expect(message).not.toContain('sk-secret-12345678')
|
||||
})
|
||||
|
||||
test('buildProfileSaveMessage describes Gemini access token / ADC mode clearly', () => {
|
||||
const message = buildProfileSaveMessage(
|
||||
'gemini',
|
||||
{
|
||||
GEMINI_AUTH_MODE: 'access-token',
|
||||
GEMINI_MODEL: 'gemini-2.5-flash',
|
||||
GEMINI_BASE_URL: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||
},
|
||||
'D:/codings/Opensource/openclaude/.openclaude-profile.json',
|
||||
)
|
||||
|
||||
expect(message).toContain('Saved Google Gemini profile.')
|
||||
expect(message).toContain('Model: gemini-2.5-flash')
|
||||
expect(message).toContain('Credentials: access token (stored securely)')
|
||||
expect(message).not.toContain('AIza')
|
||||
})
|
||||
|
||||
test('buildCurrentProviderSummary redacts poisoned model and endpoint values', () => {
|
||||
const summary = buildCurrentProviderSummary({
|
||||
processEnv: {
|
||||
|
||||
@@ -36,6 +36,14 @@ import {
|
||||
type ProfileFile,
|
||||
type ProviderProfile,
|
||||
} from '../../utils/providerProfile.js'
|
||||
import {
|
||||
getGeminiProjectIdHint,
|
||||
mayHaveGeminiAdcCredentials,
|
||||
} from '../../utils/geminiAuth.js'
|
||||
import {
|
||||
readGeminiAccessToken,
|
||||
saveGeminiAccessToken,
|
||||
} from '../../utils/geminiCredentials.js'
|
||||
import {
|
||||
getGoalDefaultOpenAIModel,
|
||||
normalizeRecommendationGoal,
|
||||
@@ -60,8 +68,14 @@ type Step =
|
||||
baseUrl: string | null
|
||||
defaultModel: string
|
||||
}
|
||||
| { name: 'gemini-auth-method' }
|
||||
| { name: 'gemini-key' }
|
||||
| { name: 'gemini-model'; apiKey: string }
|
||||
| { name: 'gemini-access-token' }
|
||||
| {
|
||||
name: 'gemini-model'
|
||||
apiKey?: string
|
||||
authMode: 'api-key' | 'access-token' | 'adc'
|
||||
}
|
||||
| { name: 'codex-check' }
|
||||
|
||||
type CurrentProviderSummary = {
|
||||
@@ -216,9 +230,13 @@ function buildSavedProfileSummary(
|
||||
env,
|
||||
),
|
||||
credentialLabel:
|
||||
maskSecretForDisplay(env.GEMINI_API_KEY) !== undefined
|
||||
? 'configured'
|
||||
: undefined,
|
||||
env.GEMINI_AUTH_MODE === 'access-token'
|
||||
? 'access token (stored securely)'
|
||||
: env.GEMINI_AUTH_MODE === 'adc'
|
||||
? 'local ADC'
|
||||
: maskSecretForDisplay(env.GEMINI_API_KEY) !== undefined
|
||||
? 'configured'
|
||||
: undefined,
|
||||
}
|
||||
case 'codex':
|
||||
return {
|
||||
@@ -427,7 +445,7 @@ function ProviderChooser({
|
||||
{
|
||||
label: 'Gemini',
|
||||
value: 'gemini',
|
||||
description: 'Use a Google Gemini API key',
|
||||
description: 'Use Google Gemini with API key, access token, or local ADC',
|
||||
},
|
||||
{
|
||||
label: 'Codex',
|
||||
@@ -926,7 +944,7 @@ export function ProviderWizard({
|
||||
defaultModel: defaults.openAIModel,
|
||||
})
|
||||
} else if (value === 'gemini') {
|
||||
setStep({ name: 'gemini-key' })
|
||||
setStep({ name: 'gemini-auth-method' })
|
||||
} else if (value === 'clear') {
|
||||
const filePath = deleteProfileFile()
|
||||
onDone(`Removed saved provider profile at ${filePath}. Restart OpenClaude to go back to normal startup.`, {
|
||||
@@ -1066,12 +1084,76 @@ export function ProviderWizard({
|
||||
/>
|
||||
)
|
||||
|
||||
case 'gemini-auth-method': {
|
||||
const hasShellGeminiKey = Boolean(
|
||||
process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY,
|
||||
)
|
||||
const hasShellGeminiAccessToken = Boolean(process.env.GEMINI_ACCESS_TOKEN)
|
||||
const hasStoredGeminiAccessToken = Boolean(readGeminiAccessToken())
|
||||
const hasAdc = mayHaveGeminiAdcCredentials(process.env)
|
||||
const projectHint = getGeminiProjectIdHint(process.env)
|
||||
|
||||
const options: OptionWithDescription[] = [
|
||||
{
|
||||
label: 'API key',
|
||||
value: 'api-key',
|
||||
description: hasShellGeminiKey
|
||||
? 'Use the current Gemini API key from this shell, or enter a new one'
|
||||
: 'Use a Google Gemini API key',
|
||||
},
|
||||
{
|
||||
label: 'Access token',
|
||||
value: 'access-token',
|
||||
description: hasShellGeminiAccessToken || hasStoredGeminiAccessToken
|
||||
? `Use ${
|
||||
hasShellGeminiAccessToken
|
||||
? 'the current GEMINI_ACCESS_TOKEN'
|
||||
: 'the securely stored Gemini access token'
|
||||
}`
|
||||
: 'Enter a Gemini access token and store it securely',
|
||||
},
|
||||
{
|
||||
label: 'Local ADC',
|
||||
value: 'adc',
|
||||
description: hasAdc
|
||||
? `Use local Google ADC credentials${projectHint ? ` (project: ${projectHint})` : ''}`
|
||||
: 'Use local Google ADC credentials after running gcloud auth application-default login',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Dialog title="Gemini setup" onCancel={() => onDone()}>
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text>Choose how this Gemini profile should authenticate.</Text>
|
||||
<Select
|
||||
options={options}
|
||||
inlineDescriptions
|
||||
visibleOptionCount={options.length}
|
||||
onChange={value => {
|
||||
if (value === 'api-key') {
|
||||
setStep({ name: 'gemini-key' })
|
||||
} else if (value === 'access-token') {
|
||||
setStep({ name: 'gemini-access-token' })
|
||||
} else {
|
||||
setStep({
|
||||
name: 'gemini-model',
|
||||
authMode: 'adc',
|
||||
})
|
||||
}
|
||||
}}
|
||||
onCancel={() => setStep({ name: 'choose' })}
|
||||
/>
|
||||
</Box>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
case 'gemini-key':
|
||||
return (
|
||||
<TextEntryDialog
|
||||
resetStateKey={step.name}
|
||||
title="Gemini setup"
|
||||
subtitle="Step 1 of 2"
|
||||
subtitle="Step 1 of 3"
|
||||
description={
|
||||
process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY
|
||||
? 'Enter a Gemini API key, or leave this blank to reuse the current GEMINI_API_KEY/GOOGLE_API_KEY from this session.'
|
||||
@@ -1089,25 +1171,95 @@ export function ProviderWizard({
|
||||
process.env.GEMINI_API_KEY ||
|
||||
process.env.GOOGLE_API_KEY ||
|
||||
''
|
||||
setStep({ name: 'gemini-model', apiKey })
|
||||
setStep({ name: 'gemini-model', apiKey, authMode: 'api-key' })
|
||||
}}
|
||||
onCancel={() => setStep({ name: 'choose' })}
|
||||
onCancel={() => setStep({ name: 'gemini-auth-method' })}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'gemini-access-token': {
|
||||
const currentToken =
|
||||
process.env.GEMINI_ACCESS_TOKEN || readGeminiAccessToken() || ''
|
||||
return (
|
||||
<TextEntryDialog
|
||||
resetStateKey={step.name}
|
||||
title="Gemini setup"
|
||||
subtitle="Step 2 of 3"
|
||||
description={
|
||||
currentToken
|
||||
? 'Enter a Gemini access token, or leave this blank to reuse the current token from this session or secure storage.'
|
||||
: 'Enter a Gemini access token. It will be stored securely for this profile.'
|
||||
}
|
||||
initialValue=""
|
||||
placeholder="ya29...."
|
||||
mask="*"
|
||||
allowEmpty={Boolean(currentToken)}
|
||||
validate={value => {
|
||||
const token = value.trim() || currentToken
|
||||
return token ? null : 'Enter a Gemini access token or go back and choose Local ADC.'
|
||||
}}
|
||||
onSubmit={value => {
|
||||
const token = value.trim() || currentToken
|
||||
const saved = saveGeminiAccessToken(token)
|
||||
if (!saved.success) {
|
||||
onDone(
|
||||
`Failed to save Gemini access token: ${saved.warning ?? 'unknown error'}`,
|
||||
{
|
||||
display: 'system',
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
setStep({
|
||||
name: 'gemini-model',
|
||||
authMode: 'access-token',
|
||||
})
|
||||
}}
|
||||
onCancel={() => setStep({ name: 'gemini-auth-method' })}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
case 'gemini-model':
|
||||
return (
|
||||
<TextEntryDialog
|
||||
resetStateKey={step.name}
|
||||
title="Gemini setup"
|
||||
subtitle="Step 2 of 2"
|
||||
description={`Enter a Gemini model name. Leave blank for ${DEFAULT_GEMINI_MODEL}.`}
|
||||
subtitle={
|
||||
step.authMode === 'api-key'
|
||||
? 'Step 3 of 3'
|
||||
: step.authMode === 'access-token'
|
||||
? 'Step 3 of 3'
|
||||
: 'Step 2 of 2'
|
||||
}
|
||||
description={
|
||||
step.authMode === 'api-key'
|
||||
? `Enter a Gemini model name. Leave blank for ${DEFAULT_GEMINI_MODEL}.`
|
||||
: step.authMode === 'access-token'
|
||||
? `Enter a Gemini model name. Leave blank for ${DEFAULT_GEMINI_MODEL}. This profile will use the stored Gemini access token at runtime.`
|
||||
: `Enter a Gemini model name. Leave blank for ${DEFAULT_GEMINI_MODEL}. This profile will use local Google ADC credentials at runtime.`
|
||||
}
|
||||
initialValue={defaults.geminiModel}
|
||||
placeholder={DEFAULT_GEMINI_MODEL}
|
||||
allowEmpty
|
||||
onSubmit={value => {
|
||||
if (
|
||||
step.authMode === 'adc' &&
|
||||
!mayHaveGeminiAdcCredentials(process.env)
|
||||
) {
|
||||
onDone(
|
||||
'Local ADC credentials were not detected. Run `gcloud auth application-default login` first, then save the Gemini ADC profile again.',
|
||||
{
|
||||
display: 'system',
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const env = buildGeminiProfileEnv({
|
||||
apiKey: step.apiKey,
|
||||
authMode: step.authMode,
|
||||
model: value.trim() || DEFAULT_GEMINI_MODEL,
|
||||
processEnv: {},
|
||||
})
|
||||
@@ -1115,7 +1267,13 @@ export function ProviderWizard({
|
||||
finishProfileSave(onDone, 'gemini', env)
|
||||
}
|
||||
}}
|
||||
onCancel={() => setStep({ name: 'gemini-key' })}
|
||||
onCancel={() =>
|
||||
step.authMode === 'api-key'
|
||||
? setStep({ name: 'gemini-key' })
|
||||
: step.authMode === 'access-token'
|
||||
? setStep({ name: 'gemini-access-token' })
|
||||
: setStep({ name: 'gemini-auth-method' })
|
||||
}
|
||||
/>
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user