diff --git a/README.md b/README.md index 36eed3eb..bee390ba 100644 --- a/README.md +++ b/README.md @@ -116,13 +116,15 @@ Advanced and source-build guides: | Provider | Setup Path | Notes | | --- | --- | --- | | OpenAI-compatible | `/provider` or env vars | Works with OpenAI, OpenRouter, DeepSeek, Groq, Mistral, LM Studio, and compatible local `/v1` servers | -| Gemini | `/provider` or env vars | Google Gemini support through the runtime provider layer | +| Gemini | `/provider` or env vars | Google Gemini support through the runtime provider layer (API key, access token, or local ADC) | | GitHub Models | `/onboard-github` | Interactive onboarding with saved credentials | | Codex | `/provider` | Uses existing Codex credentials when available | | Ollama | `/provider` or env vars | Local inference with no API key | | Atomic Chat | advanced setup | Local Apple Silicon backend | | Bedrock / Vertex / Foundry | env vars | Additional provider integrations for supported environments | +For Gemini, `/provider` can now save either the API-key path, a securely stored access-token path, or a local ADC profile. + --- ## What Works diff --git a/src/commands/provider/provider.test.tsx b/src/commands/provider/provider.test.tsx index 7f5560dc..c04c2d18 100644 --- a/src/commands/provider/provider.test.tsx +++ b/src/commands/provider/provider.test.tsx @@ -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: { diff --git a/src/commands/provider/provider.tsx b/src/commands/provider/provider.tsx index b43b6c3d..6bf8a06c 100644 --- a/src/commands/provider/provider.tsx +++ b/src/commands/provider/provider.tsx @@ -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 ( + onDone()}> + + Choose how this Gemini profile should authenticate. +