diff --git a/package.json b/package.json index 88b23bcccc..7eb89507a9 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "@anthropic-ai/sdk": "0.81.0", "@anthropic-ai/vertex-sdk": "0.14.4", "@commander-js/extra-typings": "12.1.0", + "@google/generative-ai": "^0.24.1", "@growthbook/growthbook": "1.6.5", "@grpc/grpc-js": "^1.14.3", "@grpc/proto-loader": "^0.8.0", diff --git a/scripts/system-check.ts b/scripts/system-check.ts index ade78d0a87..9a4063d91c 100644 --- a/scripts/system-check.ts +++ b/scripts/system-check.ts @@ -121,7 +121,7 @@ const GEMINI_DEFAULT_BASE_URL = 'https://generativelanguage.googleapis.com/v1bet const GITHUB_COPILOT_BASE = 'https://api.githubcopilot.com' function currentBaseUrl(): string { - if (isTruthy(process.env.CLAUDE_CODE_USE_GEMINI)) { + if (isTruthy(process.env.CLAUDE_CODE_GOOGLE)) { return process.env.GEMINI_BASE_URL ?? GEMINI_DEFAULT_BASE_URL } if (isTruthy(process.env.CLAUDE_CODE_USE_GITHUB)) { @@ -184,11 +184,11 @@ function checkGithubEnv(): CheckResult[] { function checkOpenAIEnv(): CheckResult[] { const results: CheckResult[] = [] - const useGemini = isTruthy(process.env.CLAUDE_CODE_USE_GEMINI) + const useGoogle = isTruthy(process.env.CLAUDE_CODE_GOOGLE) const useGithub = isTruthy(process.env.CLAUDE_CODE_USE_GITHUB) const useOpenAI = isTruthy(process.env.CLAUDE_CODE_USE_OPENAI) - if (useGemini) { + if (useGoogle) { return checkGeminiEnv() } @@ -265,11 +265,11 @@ function checkOpenAIEnv(): CheckResult[] { } async function checkBaseUrlReachability(): Promise { - const useGemini = isTruthy(process.env.CLAUDE_CODE_USE_GEMINI) + const useGoogle = isTruthy(process.env.CLAUDE_CODE_GOOGLE) const useOpenAI = isTruthy(process.env.CLAUDE_CODE_USE_OPENAI) const useGithub = isTruthy(process.env.CLAUDE_CODE_USE_GITHUB) - if (!useGemini && !useOpenAI && !useGithub) { + if (!useGoogle && !useOpenAI && !useGithub) { return pass('Provider reachability', 'Skipped (OpenAI-compatible mode disabled).') } @@ -281,7 +281,7 @@ async function checkBaseUrlReachability(): Promise { } const geminiBaseUrl = 'https://generativelanguage.googleapis.com/v1beta/openai' - const resolvedBaseUrl = useGemini + const resolvedBaseUrl = useGoogle ? (process.env.GEMINI_BASE_URL ?? geminiBaseUrl) : undefined const request = resolveProviderRequest({ @@ -324,7 +324,7 @@ async function checkBaseUrlReachability(): Promise { store: false, stream: true, }) - } else if (useGemini && (process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY)) { + } else if (useGoogle && (process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY)) { headers.Authorization = `Bearer ${process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY}` } else if (process.env.OPENAI_API_KEY) { headers.Authorization = `Bearer ${process.env.OPENAI_API_KEY}` @@ -372,7 +372,7 @@ function isAtomicChatUrl(baseUrl: string): boolean { function checkOllamaProcessorMode(): CheckResult { if ( !isTruthy(process.env.CLAUDE_CODE_USE_OPENAI) || - isTruthy(process.env.CLAUDE_CODE_USE_GEMINI) || + isTruthy(process.env.CLAUDE_CODE_GOOGLE) || isTruthy(process.env.CLAUDE_CODE_USE_GITHUB) ) { return pass('Ollama processor mode', 'Skipped (OpenAI-compatible mode disabled).') @@ -417,9 +417,9 @@ function checkOllamaProcessorMode(): CheckResult { } function serializeSafeEnvSummary(): Record { - if (isTruthy(process.env.CLAUDE_CODE_USE_GEMINI)) { + if (isTruthy(process.env.CLAUDE_CODE_GOOGLE)) { return { - CLAUDE_CODE_USE_GEMINI: true, + CLAUDE_CODE_GOOGLE: true, GEMINI_MODEL: process.env.GEMINI_MODEL ?? '(unset, default: gemini-2.0-flash)', GEMINI_BASE_URL: process.env.GEMINI_BASE_URL ?? 'https://generativelanguage.googleapis.com/v1beta/openai', GEMINI_API_KEY_SET: Boolean(process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY), diff --git a/src/components/StartupScreen.ts b/src/components/StartupScreen.ts index 6b9958fb51..180a1d6849 100644 --- a/src/components/StartupScreen.ts +++ b/src/components/StartupScreen.ts @@ -7,6 +7,7 @@ import { isLocalProviderUrl } from '../services/api/providerConfig.js' import { getLocalOpenAICompatibleProviderLabel } from '../utils/providerDiscovery.js' +import { getProviderMode, getAvailableProviders } from '../tools/WebSearchTool/providers/index.js' declare const MACRO: { VERSION: string; DISPLAY_VERSION?: string } @@ -82,14 +83,14 @@ const LOGO_CLAUDE = [ // ─── Provider detection ─────────────────────────────────────────────────────── function detectProvider(): { name: string; model: string; baseUrl: string; isLocal: boolean } { - const useGemini = process.env.CLAUDE_CODE_USE_GEMINI === '1' || process.env.CLAUDE_CODE_USE_GEMINI === 'true' + const useGoogle = process.env.CLAUDE_CODE_GOOGLE === '1' || process.env.CLAUDE_CODE_GOOGLE === 'true' const useGithub = process.env.CLAUDE_CODE_USE_GITHUB === '1' || process.env.CLAUDE_CODE_USE_GITHUB === 'true' const useOpenAI = process.env.CLAUDE_CODE_USE_OPENAI === '1' || process.env.CLAUDE_CODE_USE_OPENAI === 'true' - if (useGemini) { - const model = process.env.GEMINI_MODEL || 'gemini-2.0-flash' - const baseUrl = process.env.GEMINI_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta/openai' - return { name: 'Google Gemini', model, baseUrl, isLocal: false } + if (useGoogle) { + const model = process.env.GEMINI_MODEL || process.env.OPENAI_MODEL || 'gemini-2.0-flash' + const baseUrl = process.env.GEMINI_BASE_URL || 'generativelanguage.googleapis.com' + return { name: 'Google (compatible)', model, baseUrl, isLocal: false } } if (useGithub) { @@ -144,6 +145,25 @@ function detectProvider(): { name: string; model: string; baseUrl: string; isLoc return { name: 'Anthropic', model, baseUrl: 'https://api.anthropic.com', isLocal: false } } +// ─── Search provider detection ──────────────────────────────────────────────── + +function detectSearchProvider(): { label: string; isDefault: boolean } { + const mode = getProviderMode() + + if (mode === 'native') return { label: 'native (Anthropic)', isDefault: false } + if (mode === 'custom') return { label: 'custom API', isDefault: false } + if (mode !== 'auto') return { label: mode, isDefault: false } + + // Auto mode — show which providers are available (in priority order) + const available = getAvailableProviders() + if (available.length === 0) return { label: 'ddg (fallback)', isDefault: true } + + const names = available.map(p => p.name) + // Show up to 3, then "+N more" + if (names.length <= 3) return { label: names.join(', '), isDefault: false } + return { label: `${names.slice(0, 3).join(', ')} +${names.length - 3}`, isDefault: false } +} + // ─── Box drawing ────────────────────────────────────────────────────────────── function boxRow(content: string, width: number, rawLen: number): string { @@ -158,6 +178,7 @@ export function printStartupScreen(): void { if (process.env.CI || !process.stdout.isTTY) return const p = detectProvider() + const sp = detectSearchProvider() const W = 62 const out: string[] = [] @@ -198,6 +219,10 @@ export function printStartupScreen(): void { ;[r, l] = lbl('Endpoint', ep) out.push(boxRow(r, W, l)) + const searchC: RGB = sp.isDefault ? DIMCOL : ACCENT + ;[r, l] = lbl('Search', sp.label, searchC) + out.push(boxRow(r, W, l)) + out.push(`${rgb(...BORDER)}\u2560${'\u2550'.repeat(W - 2)}\u2563${RESET}`) const sC: RGB = p.isLocal ? [130, 175, 130] : ACCENT diff --git a/src/services/api/client.test.ts b/src/services/api/client.test.ts index 30df3b9371..382235a653 100644 --- a/src/services/api/client.test.ts +++ b/src/services/api/client.test.ts @@ -15,7 +15,7 @@ const originalFetch = globalThis.fetch const originalMacro = (globalThis as Record).MACRO const originalEnv = { CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI, - CLAUDE_CODE_USE_GEMINI: process.env.CLAUDE_CODE_USE_GEMINI, + CLAUDE_CODE_GOOGLE: process.env.CLAUDE_CODE_GOOGLE, GEMINI_API_KEY: process.env.GEMINI_API_KEY, GEMINI_MODEL: process.env.GEMINI_MODEL, GEMINI_BASE_URL: process.env.GEMINI_BASE_URL, @@ -39,7 +39,7 @@ function restoreEnv(key: string, value: string | undefined): void { beforeEach(() => { ;(globalThis as Record).MACRO = { VERSION: 'test-version' } - process.env.CLAUDE_CODE_USE_GEMINI = '1' + process.env.CLAUDE_CODE_GOOGLE = '1' process.env.GEMINI_API_KEY = 'gemini-test-key' process.env.GEMINI_MODEL = 'gemini-2.0-flash' process.env.GEMINI_BASE_URL = 'https://gemini.example/v1beta/openai' @@ -58,7 +58,7 @@ beforeEach(() => { afterEach(() => { ;(globalThis as Record).MACRO = originalMacro restoreEnv('CLAUDE_CODE_USE_OPENAI', originalEnv.CLAUDE_CODE_USE_OPENAI) - restoreEnv('CLAUDE_CODE_USE_GEMINI', originalEnv.CLAUDE_CODE_USE_GEMINI) + restoreEnv('CLAUDE_CODE_GOOGLE', originalEnv.CLAUDE_CODE_GOOGLE) restoreEnv('GEMINI_API_KEY', originalEnv.GEMINI_API_KEY) restoreEnv('GEMINI_MODEL', originalEnv.GEMINI_MODEL) restoreEnv('GEMINI_BASE_URL', originalEnv.GEMINI_BASE_URL) @@ -73,9 +73,8 @@ afterEach(() => { globalThis.fetch = originalFetch }) -test('routes Gemini provider requests through the OpenAI-compatible shim', async () => { +test('routes Gemini provider requests through the native Gemini shim', async () => { let capturedUrl: string | undefined - let capturedHeaders: Headers | undefined let capturedBody: Record | undefined globalThis.fetch = (async (input, init) => { @@ -85,26 +84,22 @@ test('routes Gemini provider requests through the OpenAI-compatible shim', async : input instanceof URL ? input.toString() : input.url - capturedHeaders = new Headers(init?.headers) capturedBody = JSON.parse(String(init?.body)) as Record return new Response( JSON.stringify({ - id: 'chatcmpl-gemini', - model: 'gemini-2.0-flash', - choices: [ + candidates: [ { - message: { - role: 'assistant', - content: 'gemini ok', + content: { + parts: [{ text: 'gemini ok' }], + role: 'model', }, - finish_reason: 'stop', + finishReason: 'STOP', }, ], - usage: { - prompt_tokens: 8, - completion_tokens: 3, - total_tokens: 11, + usageMetadata: { + promptTokenCount: 8, + candidatesTokenCount: 3, }, }), { @@ -128,12 +123,11 @@ test('routes Gemini provider requests through the OpenAI-compatible shim', async stream: false, }) - expect(capturedUrl).toBe('https://gemini.example/v1beta/openai/chat/completions') - expect(capturedHeaders?.get('authorization')).toBe('Bearer gemini-test-key') - expect(capturedBody?.model).toBe('gemini-2.0-flash') + // Native Gemini shim calls the Generative AI endpoint, not OpenAI-compatible + expect(capturedUrl).toContain('generativelanguage.googleapis.com') + expect(capturedUrl).toContain('generateContent') expect(response).toMatchObject({ role: 'assistant', - model: 'gemini-2.0-flash', }) }) @@ -141,6 +135,7 @@ test('strips Anthropic-specific custom headers before sending OpenAI-compatible let capturedHeaders: Headers | undefined process.env.CLAUDE_CODE_USE_OPENAI = '1' + delete process.env.CLAUDE_CODE_GOOGLE process.env.OPENAI_API_KEY = 'openai-test-key' process.env.OPENAI_BASE_URL = 'http://example.test/v1' process.env.OPENAI_MODEL = 'gpt-4o' diff --git a/src/services/api/client.ts b/src/services/api/client.ts index ab73d805a9..e9410fac4d 100644 --- a/src/services/api/client.ts +++ b/src/services/api/client.ts @@ -174,10 +174,13 @@ export async function getAnthropicClient({ providerOverride, }) as unknown as Anthropic } + if (isEnvTruthy(process.env.CLAUDE_CODE_GOOGLE)) { + const { createGeminiShimClient } = await import('./geminiShim.js') + return createGeminiShimClient({}) as unknown as Anthropic + } if ( isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) || - isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB) || - isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI) + isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB) ) { const { createOpenAIShimClient } = await import('./openaiShim.js') return createOpenAIShimClient({ diff --git a/src/services/api/geminiShim.ts b/src/services/api/geminiShim.ts new file mode 100644 index 0000000000..0ebe249fac --- /dev/null +++ b/src/services/api/geminiShim.ts @@ -0,0 +1,768 @@ +/** + * Native Gemini API shim for Claude Code. + * + * Translates Anthropic SDK calls (anthropic.beta.messages.create) into + * native Google Generative AI API requests using the @google/generative-ai + * SDK, and streams back events in the Anthropic streaming format so the + * rest of the codebase is unaware. + * + * Unlike the OpenAI-compatible shim, this uses Gemini's native API which + * handles thought_signatures, function calling, and thinking blocks + * correctly without workarounds. + * + * Environment variables: + * CLAUDE_CODE_GOOGLE=1 — enable this provider + * GEMINI_API_KEY=... — API key (or GOOGLE_API_KEY) + * GEMINI_MODEL=gemini-2.0-flash — model override + */ + +import { + GoogleGenerativeAI, + type Content, + type FunctionCall, + type FunctionDeclaration, + type GenerationConfig, + type GenerateContentStreamResult, + type Part, + type Schema, + type Tool, +} from '@google/generative-ai' +import type { + AnthropicStreamEvent, + AnthropicUsage, + ShimCreateParams, +} from './codexShim.js' + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +function makeMessageId(): string { + return `msg_${crypto.randomUUID().replace(/-/g, '')}` +} + +// --------------------------------------------------------------------------- +// Message format conversion: Anthropic → Gemini +// --------------------------------------------------------------------------- + +function convertSystemPrompt(system: unknown): string { + if (!system) return '' + if (typeof system === 'string') return system + if (Array.isArray(system)) { + return system + .map((block: { type?: string; text?: string }) => + block.type === 'text' ? block.text ?? '' : '', + ) + .join('\n\n') + } + return String(system) +} + +function convertToolResultContent(content: unknown): string { + if (typeof content === 'string') return content + if (!Array.isArray(content)) return JSON.stringify(content ?? '') + + const chunks: string[] = [] + for (const block of content) { + if (block?.type === 'text' && typeof block.text === 'string') { + chunks.push(block.text) + continue + } + if (block?.type === 'image') { + chunks.push('[image]') + continue + } + if (typeof block?.text === 'string') { + chunks.push(block.text) + } + } + return chunks.join('\n') +} + +function convertContentToParts(content: unknown): Part[] { + if (typeof content === 'string') { + return [{ text: content }] + } + if (!Array.isArray(content)) { + return [{ text: String(content ?? '') }] + } + + const parts: Part[] = [] + for (const block of content) { + switch (block?.type) { + case 'text': + if (block.text) parts.push({ text: block.text }) + break + case 'image': { + const src = block.source + if (src?.type === 'base64' && src.data) { + parts.push({ + inlineData: { + mimeType: src.media_type ?? 'image/png', + data: src.data, + }, + }) + } + break + } + case 'thinking': + // Gemini uses thought=true on the generation config and returns + // thought parts in the response. For replayed thinking blocks, + // we include them as text with a marker. + if (block.thinking) { + parts.push({ text: `[thought]\n${block.thinking}\n[/thought]` }) + } + break + case 'tool_use': + // Handled separately — converted to functionCall parts + break + case 'tool_result': + // Handled separately — converted to functionResponse parts + break + default: + if (block?.text) parts.push({ text: block.text }) + } + } + + if (parts.length === 0) return [{ text: '' }] + return parts +} + +/** + * Convert Anthropic messages to Gemini Content array. + * Handles user, assistant, and tool result messages. + */ +function convertMessages( + messages: Array<{ + role: string + message?: { role?: string; content?: unknown } + content?: unknown + }>, +): Content[] { + // First pass: build a map of tool_use_id → function name from assistant messages + const toolNameMap = new Map() + for (const msg of messages) { + const inner = msg.message ?? msg + const content = (inner as { content?: unknown }).content + if (Array.isArray(content)) { + for (const block of content) { + if (block?.type === 'tool_use' && block.id && block.name) { + toolNameMap.set(block.id, block.name) + } + } + } + } + + const result: Content[] = [] + + for (const msg of messages) { + const inner = msg.message ?? msg + const role = (inner as { role?: string }).role ?? msg.role + const content = (inner as { content?: unknown }).content + + if (role === 'user') { + if (Array.isArray(content)) { + const toolResults = content.filter( + (b: { type?: string }) => b.type === 'tool_result', + ) + const otherContent = content.filter( + (b: { type?: string }) => b.type !== 'tool_result', + ) + + const parts: Part[] = [] + + // Convert tool results to functionResponse parts + for (const tr of toolResults) { + const resultText = convertToolResultContent(tr.content) + // Gemini requires the function name (not the call ID) in functionResponse + const funcName = + toolNameMap.get(tr.tool_use_id ?? '') ?? tr.tool_use_id ?? 'unknown' + parts.push({ + functionResponse: { + name: funcName, + response: tr.is_error + ? { error: resultText } + : { output: resultText }, + }, + }) + } + + // Convert remaining user content + if (otherContent.length > 0) { + parts.push(...convertContentToParts(otherContent)) + } + + if (parts.length > 0) { + result.push({ role: 'user', parts }) + } + } else { + const parts = convertContentToParts(content) + result.push({ role: 'user', parts }) + } + } else if (role === 'assistant') { + if (Array.isArray(content)) { + const parts: Part[] = [] + + for (const block of content) { + switch (block?.type) { + case 'text': + if (block.text) parts.push({ text: block.text }) + break + case 'thinking': + // Include thinking as text for context continuity + if (block.thinking) { + parts.push({ + text: `[thought]\n${block.thinking}\n[/thought]`, + }) + } + break + case 'tool_use': + parts.push({ + functionCall: { + name: block.name ?? 'unknown', + args: + typeof block.input === 'string' + ? JSON.parse(block.input ?? '{}') + : (block.input ?? {}), + }, + }) + break + } + } + + if (parts.length > 0) { + result.push({ role: 'model', parts }) + } + } else { + const parts = convertContentToParts(content) + result.push({ role: 'model', parts }) + } + } + } + + // Gemini requires alternating user/model turns. Merge consecutive same-role messages. + const merged: Content[] = [] + for (const item of result) { + const prev = merged[merged.length - 1] + if (prev && prev.role === item.role) { + prev.parts = [...prev.parts, ...item.parts] + } else { + merged.push({ ...item, parts: [...item.parts] }) + } + } + + // Gemini requires the conversation to start with a user role + if (merged.length > 0 && merged[0].role !== 'user') { + merged.unshift({ role: 'user', parts: [{ text: ' ' }] }) + } + + // Gemini requires the conversation to end with a user role (for next turn) + if (merged.length > 0 && merged[merged.length - 1].role !== 'user') { + // This is normal — the last assistant message is what we're responding to + // The API will handle this correctly + } + + return merged +} + +// --------------------------------------------------------------------------- +// Tool format conversion: Anthropic → Gemini +// --------------------------------------------------------------------------- + +function convertJsonSchemaToGeminiSchema( + schema: Record, +): Schema { + const result: Record = {} + + if (schema.type) result.type = String(schema.type).toUpperCase() + if (schema.description) result.description = String(schema.description) + if (schema.enum) result.enum = schema.enum as string[] + + if (schema.type === 'object' && schema.properties) { + const props = schema.properties as Record> + const required = Array.isArray(schema.required) + ? (schema.required as string[]) + : [] + + const properties: Record = {} + for (const [key, value] of Object.entries(props)) { + properties[key] = convertJsonSchemaToGeminiSchema( + value as Record, + ) + } + + result.properties = properties + if (required.length > 0) { + result.required = required + } + } + + if (schema.type === 'array' && schema.items) { + result.items = convertJsonSchemaToGeminiSchema( + schema.items as Record, + ) + } + + // Handle anyOf/oneOf + for (const key of ['anyOf', 'oneOf'] as const) { + if (schema[key] && Array.isArray(schema[key])) { + result[key === 'anyOf' ? 'anyOf' : 'oneOf'] = ( + schema[key] as Record[] + ).map(item => convertJsonSchemaToGeminiSchema(item)) + } + } + + return result as Schema +} + +function convertTools( + tools: Array<{ + name: string + description?: string + input_schema?: Record + }>, +): Tool[] { + const functionDeclarations: FunctionDeclaration[] = tools + .filter(t => t.name !== 'ToolSearchTool') + .map(t => { + const schema = (t.input_schema ?? { + type: 'object', + properties: {}, + }) as Record + + return { + name: t.name, + description: t.description ?? '', + parameters: convertJsonSchemaToGeminiSchema(schema), + } + }) + + return [{ functionDeclarations }] +} + +// --------------------------------------------------------------------------- +// Streaming: Gemini SSE → Anthropic stream events +// --------------------------------------------------------------------------- + +async function* geminiStreamToAnthropic( + streamResult: GenerateContentStreamResult, + model: string, +): AsyncGenerator { + const messageId = makeMessageId() + let contentBlockIndex = 0 + let hasEmittedContentStart = false + let hasEmittedThinkingStart = false + let hasClosedThinking = false + let inputTokens = 0 + let outputTokens = 0 + + // Emit message_start + yield { + type: 'message_start', + message: { + id: messageId, + type: 'message', + role: 'assistant', + content: [], + model, + stop_reason: null, + stop_sequence: null, + usage: { + input_tokens: 0, + output_tokens: 0, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, + }, + }, + } + + try { + for await (const chunk of streamResult.stream) { + // Extract usage from chunk if available + const usageMeta = chunk.usageMetadata + if (usageMeta) { + inputTokens = usageMeta.promptTokenCount ?? 0 + outputTokens = usageMeta.candidatesTokenCount ?? 0 + } + + const candidate = chunk.candidates?.[0] + if (!candidate) continue + + const parts = candidate.content?.parts ?? [] + + for (const part of parts) { + // Thinking / thought parts + if (part.thought || (part.text && candidate.content?.role === 'model' && !hasClosedThinking && !hasEmittedContentStart && part.text.startsWith('[thought]'))) { + if (!hasEmittedThinkingStart) { + yield { + type: 'content_block_start', + index: contentBlockIndex, + content_block: { type: 'thinking', thinking: '' }, + } + hasEmittedThinkingStart = true + } + + const thinkingText = part.thought + ? (part.text ?? '') + : (part.text ?? '').replace(/^\[thought\]\n?/, '').replace(/\n?\[\/thought\]$/, '') + + if (thinkingText) { + yield { + type: 'content_block_delta', + index: contentBlockIndex, + delta: { type: 'thinking_delta', thinking: thinkingText }, + } + } + continue + } + + // Text content + if (part.text && !part.thought) { + // Close thinking block if open + if (hasEmittedThinkingStart && !hasClosedThinking) { + yield { type: 'content_block_stop', index: contentBlockIndex } + contentBlockIndex++ + hasClosedThinking = true + } + + // Strip [thought]...[/thought] wrappers from text if present + let text = part.text + text = text.replace(/\[thought\][\s\S]*?\[\/thought\]\n?/g, '') + + if (text) { + if (!hasEmittedContentStart) { + yield { + type: 'content_block_start', + index: contentBlockIndex, + content_block: { type: 'text', text: '' }, + } + hasEmittedContentStart = true + } + yield { + type: 'content_block_delta', + index: contentBlockIndex, + delta: { type: 'text_delta', text }, + } + } + } + + // Function calls (tool use) + if (part.functionCall) { + // Close thinking block if open + if (hasEmittedThinkingStart && !hasClosedThinking) { + yield { type: 'content_block_stop', index: contentBlockIndex } + contentBlockIndex++ + hasClosedThinking = true + } + // Close text block if open + if (hasEmittedContentStart) { + yield { type: 'content_block_stop', index: contentBlockIndex } + contentBlockIndex++ + hasEmittedContentStart = false + } + + const fc = part.functionCall + const toolId = `call_${crypto.randomUUID().replace(/-/g, '')}` + const toolBlockIndex = contentBlockIndex + + yield { + type: 'content_block_start', + index: toolBlockIndex, + content_block: { + type: 'tool_use', + id: toolId, + name: fc.name ?? 'unknown', + input: {}, + }, + } + + const argsStr = JSON.stringify(fc.args ?? {}) + if (argsStr && argsStr !== '{}') { + yield { + type: 'content_block_delta', + index: toolBlockIndex, + delta: { + type: 'input_json_delta', + partial_json: argsStr, + }, + } + } + + yield { type: 'content_block_stop', index: toolBlockIndex } + contentBlockIndex++ + } + } + + // Finish reason + const finishReason = candidate.finishReason + if (finishReason) { + // Close any open blocks + if (hasEmittedThinkingStart && !hasClosedThinking) { + yield { type: 'content_block_stop', index: contentBlockIndex } + contentBlockIndex++ + hasClosedThinking = true + } + if (hasEmittedContentStart) { + yield { type: 'content_block_stop', index: contentBlockIndex } + contentBlockIndex++ + hasEmittedContentStart = false + } + + const stopReason = + finishReason === 'STOP' + ? 'end_turn' + : finishReason === 'MAX_TOKENS' + ? 'max_tokens' + : finishReason === 'TOOL_CALLS' || finishReason === 'TOOL_CODE' + ? 'tool_use' + : 'end_turn' + + yield { + type: 'message_delta', + delta: { stop_reason: stopReason, stop_sequence: null }, + usage: { + input_tokens: inputTokens, + output_tokens: outputTokens, + }, + } + } + } + } catch (error) { + // Ensure we close any open blocks on error + if (hasEmittedThinkingStart && !hasClosedThinking) { + yield { type: 'content_block_stop', index: contentBlockIndex } + } + if (hasEmittedContentStart) { + yield { type: 'content_block_stop', index: contentBlockIndex } + } + throw error + } + + yield { type: 'message_stop' } +} + +// --------------------------------------------------------------------------- +// Non-streaming response conversion +// --------------------------------------------------------------------------- + +function convertGeminiResponseToAnthropic( + response: Awaited['generateContent']>>, + model: string, +): Record { + const content: Array> = [] + const candidate = response.response?.candidates?.[0] + const parts = candidate?.content?.parts ?? [] + + for (const part of parts) { + if (part.thought && part.text) { + content.push({ type: 'thinking', thinking: part.text }) + } else if (part.text && !part.thought) { + // Strip [thought] wrappers + let text = part.text + text = text.replace(/\[thought\][\s\S]*?\[\/thought\]\n?/g, '') + if (text) { + content.push({ type: 'text', text }) + } + } else if (part.functionCall) { + const fc = part.functionCall + content.push({ + type: 'tool_use', + id: `call_${crypto.randomUUID().replace(/-/g, '')}`, + name: fc.name ?? 'unknown', + input: fc.args ?? {}, + }) + } + } + + const finishReason = candidate?.finishReason + const stopReason = + finishReason === 'STOP' + ? 'end_turn' + : finishReason === 'MAX_TOKENS' + ? 'max_tokens' + : finishReason === 'TOOL_CALLS' || finishReason === 'TOOL_CODE' + ? 'tool_use' + : 'end_turn' + + const usage = response.response?.usageMetadata + + return { + id: makeMessageId(), + type: 'message', + role: 'assistant', + content, + model, + stop_reason: stopReason, + stop_sequence: null, + usage: { + input_tokens: usage?.promptTokenCount ?? 0, + output_tokens: usage?.candidatesTokenCount ?? 0, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, + }, + } +} + +// --------------------------------------------------------------------------- +// The shim client — duck-types as Anthropic SDK +// --------------------------------------------------------------------------- + +class GeminiShimStream { + private generator: AsyncGenerator + controller = new AbortController() + + constructor(generator: AsyncGenerator) { + this.generator = generator + } + + async *[Symbol.asyncIterator]() { + yield* this.generator + } +} + +class GeminiShimMessages { + private apiKey: string + private model: string + + constructor(apiKey: string, model: string) { + this.apiKey = apiKey + this.model = model + } + + async create( + params: ShimCreateParams, + options?: { signal?: AbortSignal }, + ) { + const genAI = new GoogleGenerativeAI(this.apiKey) + const modelName = params.model || this.model + + const systemPrompt = convertSystemPrompt(params.system) + const contents = convertMessages( + params.messages as Array<{ + role: string + message?: { role?: string; content?: unknown } + content?: unknown + }>, + ) + + const generationConfig: GenerationConfig = {} + if (params.temperature !== undefined) + generationConfig.temperature = params.temperature + if (params.top_p !== undefined) generationConfig.topP = params.top_p + if (params.max_tokens) + generationConfig.maxOutputTokens = params.max_tokens + + // Enable thinking if the model supports it + generationConfig.thinkingConfig = { + includeThoughts: true, + } + + const tools = + params.tools && params.tools.length > 0 + ? convertTools( + params.tools as Array<{ + name: string + description?: string + input_schema?: Record + }>, + ) + : undefined + + // Handle tool_choice + let toolConfig: Record | undefined + if (params.tool_choice) { + const tc = params.tool_choice as { type?: string; name?: string } + if (tc.type === 'auto') { + toolConfig = { functionCallingConfig: { mode: 'AUTO' } } + } else if (tc.type === 'any') { + toolConfig = { functionCallingConfig: { mode: 'ANY' } } + } else if (tc.type === 'none') { + toolConfig = { functionCallingConfig: { mode: 'NONE' } } + } else if (tc.type === 'tool' && tc.name) { + toolConfig = { + functionCallingConfig: { + mode: 'ANY', + allowedFunctionNames: [tc.name], + }, + } + } + } + + const modelInstance = genAI.getGenerativeModel({ + model: modelName, + systemInstruction: systemPrompt || undefined, + tools, + toolConfig, + generationConfig, + }) + + if (params.stream) { + const streamResult = await modelInstance.generateContentStream( + { contents }, + { signal: options?.signal }, + ) + return new GeminiShimStream( + geminiStreamToAnthropic(streamResult, modelName), + ) + } + + // Non-streaming — return a promise with .withResponse() like the OpenAI shim + const promise = (async () => { + const result = await modelInstance.generateContent( + { contents }, + { signal: options?.signal }, + ) + return convertGeminiResponseToAnthropic(result, modelName) + })() + + const httpResponse = new Response() + ;(promise as unknown as Record).withResponse = + async () => { + const data = await promise + return { + data, + response: httpResponse, + request_id: makeMessageId(), + } + } + + return promise + } +} + +class GeminiShimBeta { + messages: GeminiShimMessages + + constructor(apiKey: string, model: string) { + this.messages = new GeminiShimMessages(apiKey, model) + } +} + +export function createGeminiShimClient(options: { + apiKey?: string + model?: string +}): unknown { + const apiKey = + options.apiKey ?? + process.env.GEMINI_API_KEY ?? + process.env.GOOGLE_API_KEY ?? + '' + const model = + options.model ?? + process.env.GEMINI_MODEL ?? + process.env.OPENAI_MODEL ?? + 'gemini-2.0-flash' + + if (!apiKey) { + throw new Error( + 'Gemini API key is required. Set GEMINI_API_KEY or GOOGLE_API_KEY.', + ) + } + + const beta = new GeminiShimBeta(apiKey, model) + + return { + beta, + messages: beta.messages, + } +} diff --git a/src/services/api/openaiShim.test.ts b/src/services/api/openaiShim.test.ts index 4889b6d367..2740cdf226 100644 --- a/src/services/api/openaiShim.test.ts +++ b/src/services/api/openaiShim.test.ts @@ -11,7 +11,7 @@ const originalEnv = { GITHUB_TOKEN: process.env.GITHUB_TOKEN, GH_TOKEN: process.env.GH_TOKEN, CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI, - CLAUDE_CODE_USE_GEMINI: process.env.CLAUDE_CODE_USE_GEMINI, + CLAUDE_CODE_GOOGLE: process.env.CLAUDE_CODE_GOOGLE, GEMINI_API_KEY: process.env.GEMINI_API_KEY, GOOGLE_API_KEY: process.env.GOOGLE_API_KEY, GEMINI_ACCESS_TOKEN: process.env.GEMINI_ACCESS_TOKEN, @@ -79,7 +79,7 @@ beforeEach(() => { delete process.env.GITHUB_TOKEN delete process.env.GH_TOKEN delete process.env.CLAUDE_CODE_USE_OPENAI - delete process.env.CLAUDE_CODE_USE_GEMINI + delete process.env.CLAUDE_CODE_GOOGLE delete process.env.GEMINI_API_KEY delete process.env.GOOGLE_API_KEY delete process.env.GEMINI_ACCESS_TOKEN @@ -98,7 +98,7 @@ afterEach(() => { restoreEnv('GITHUB_TOKEN', originalEnv.GITHUB_TOKEN) restoreEnv('GH_TOKEN', originalEnv.GH_TOKEN) restoreEnv('CLAUDE_CODE_USE_OPENAI', originalEnv.CLAUDE_CODE_USE_OPENAI) - restoreEnv('CLAUDE_CODE_USE_GEMINI', originalEnv.CLAUDE_CODE_USE_GEMINI) + restoreEnv('CLAUDE_CODE_GOOGLE', originalEnv.CLAUDE_CODE_GOOGLE) restoreEnv('GEMINI_API_KEY', originalEnv.GEMINI_API_KEY) restoreEnv('GOOGLE_API_KEY', originalEnv.GOOGLE_API_KEY) restoreEnv('GEMINI_ACCESS_TOKEN', originalEnv.GEMINI_ACCESS_TOKEN) @@ -699,7 +699,7 @@ test('uses GEMINI_ACCESS_TOKEN for Gemini OpenAI-compatible requests', async () let capturedProject: string | null = null let requestUrl: string | undefined - process.env.CLAUDE_CODE_USE_GEMINI = '1' + process.env.CLAUDE_CODE_GOOGLE = '1' process.env.GEMINI_AUTH_MODE = 'access-token' process.env.GEMINI_ACCESS_TOKEN = 'gemini-access-token' process.env.GOOGLE_CLOUD_PROJECT = 'gemini-project' diff --git a/src/services/api/openaiShim.ts b/src/services/api/openaiShim.ts index 978ecf57c3..b69937124c 100644 --- a/src/services/api/openaiShim.ts +++ b/src/services/api/openaiShim.ts @@ -254,9 +254,9 @@ function convertContentBlocks( return parts } -function isGeminiMode(): boolean { +function isGoogleMode(): boolean { return ( - isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI) || + isEnvTruthy(process.env.CLAUDE_CODE_GOOGLE) || hasGeminiApiHost(process.env.OPENAI_BASE_URL) ) } @@ -352,12 +352,28 @@ function convertMessages( } // Handle Gemini thought_signature - if (isGeminiMode()) { + if (isGoogleMode()) { // If the model provided a signature in the tool_use block itself (e.g. from a previous Turn/Step) // Use thinkingBlock.signature for ALL tool calls in the same assistant turn if available. // The API requires the same signature on every replayed function call part in a parallel set. const signature = tu.signature ?? (thinkingBlock as any)?.signature + // Gemini requires a non-empty thought_signature on every function call part. + // When no real signature is available (first turn, non-thinking model, etc.), + // generate a deterministic pseudo-signature from the tool call ID so it stays + // consistent across retries and doesn't trigger the "missing thought_signature" error. + let effectiveSignature: string + if (signature) { + effectiveSignature = signature + } else { + const id = toolCall.id + let hash = 0 + for (let i = 0; i < id.length; i++) { + hash = ((hash << 5) - hash + id.charCodeAt(i)) | 0 + } + effectiveSignature = `sig_${Math.abs(hash).toString(36)}_${id.slice(-8)}` + } + // Merge into existing google-specific metadata if present const existingGoogle = (toolCall.extra_content?.google as Record) ?? {} @@ -365,7 +381,7 @@ function convertMessages( ...toolCall.extra_content, google: { ...existingGoogle, - thought_signature: signature ?? "skip_thought_signature_validator" + thought_signature: effectiveSignature } } } @@ -492,7 +508,7 @@ function normalizeSchemaForOpenAI( function convertTools( tools: Array<{ name: string; description?: string; input_schema?: Record }>, ): OpenAITool[] { - const isGemini = isGeminiMode() + const isGoogle = isGoogleMode() return tools .filter(t => t.name !== 'ToolSearchTool') // Not relevant for OpenAI @@ -515,7 +531,7 @@ function convertTools( function: { name: t.name, description: t.description ?? '', - parameters: normalizeSchemaForOpenAI(schema, !isGemini), + parameters: normalizeSchemaForOpenAI(schema, !isGoogle), }, } }) @@ -1256,7 +1272,7 @@ class OpenAIShimMessages { ...filterAnthropicHeaders(options?.headers), } - const isGemini = isGeminiMode() + const isGoogle = isGoogleMode() const apiKey = this.providerOverride?.apiKey ?? process.env.OPENAI_API_KEY ?? '' // Detect Azure endpoints by hostname (not raw URL) to prevent bypass via @@ -1275,7 +1291,7 @@ class OpenAIShimMessages { } else { headers.Authorization = `Bearer ${apiKey}` } - } else if (isGemini) { + } else if (isGoogle) { const geminiCredential = await resolveGeminiCredential(process.env) if (geminiCredential.kind !== 'none') { headers.Authorization = `Bearer ${geminiCredential.credential}` @@ -1578,7 +1594,7 @@ export function createOpenAIShimClient(options: { // When Gemini provider is active, map Gemini env vars to OpenAI-compatible ones // so the existing providerConfig.ts infrastructure picks them up correctly. - if (isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI)) { + if (isEnvTruthy(process.env.CLAUDE_CODE_GOOGLE)) { process.env.OPENAI_BASE_URL ??= process.env.GEMINI_BASE_URL ?? 'https://generativelanguage.googleapis.com/v1beta/openai' diff --git a/src/services/api/providerConfig.ts b/src/services/api/providerConfig.ts index 7db6c162df..36f44168d8 100644 --- a/src/services/api/providerConfig.ts +++ b/src/services/api/providerConfig.ts @@ -417,7 +417,7 @@ export function resolveProviderRequest(options?: { export function getAdditionalModelOptionsCacheScope(): string | null { if (!isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI)) { - if (!isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI) && + if (!isEnvTruthy(process.env.CLAUDE_CODE_GOOGLE) && !isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB) && !isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) && !isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) && diff --git a/src/services/api/withRetry.test.ts b/src/services/api/withRetry.test.ts index ac840cd78e..8c942e2c40 100644 --- a/src/services/api/withRetry.test.ts +++ b/src/services/api/withRetry.test.ts @@ -18,7 +18,7 @@ const originalEnv = { ...process.env } const envKeys = [ 'CLAUDE_CODE_USE_OPENAI', - 'CLAUDE_CODE_USE_GEMINI', + 'CLAUDE_CODE_GOOGLE', 'CLAUDE_CODE_USE_GITHUB', 'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX', diff --git a/src/services/mcp/officialRegistry.test.ts b/src/services/mcp/officialRegistry.test.ts index 75ab4f0493..c457eedd8d 100644 --- a/src/services/mcp/officialRegistry.test.ts +++ b/src/services/mcp/officialRegistry.test.ts @@ -33,7 +33,7 @@ describe('prefetchOfficialMcpUrls', () => { }) test('does not fetch registry when using Gemini mode', async () => { - process.env.CLAUDE_CODE_USE_GEMINI = '1' + process.env.CLAUDE_CODE_GOOGLE = '1' mock.module('../../utils/model/providers.js', () => ({ getAPIProvider: () => 'gemini', })) @@ -48,7 +48,7 @@ describe('prefetchOfficialMcpUrls', () => { test('fetches registry in first-party mode', async () => { delete process.env.CLAUDE_CODE_USE_OPENAI - delete process.env.CLAUDE_CODE_USE_GEMINI + delete process.env.CLAUDE_CODE_GOOGLE delete process.env.CLAUDE_CODE_USE_GITHUB mock.module('../../utils/model/providers.js', () => ({ diff --git a/src/tools/MCPTool/MCPTool.ts b/src/tools/MCPTool/MCPTool.ts index 3896868b3b..628a5b5ab9 100644 --- a/src/tools/MCPTool/MCPTool.ts +++ b/src/tools/MCPTool/MCPTool.ts @@ -14,8 +14,21 @@ import { export const inputSchema = lazySchema(() => z.object({}).passthrough()) type InputSchema = ReturnType +// MCP tools can return either a plain string or an array of content blocks +// (text, images, etc.). The outputSchema must reflect both shapes so the model +// knows rich content is possible. export const outputSchema = lazySchema(() => - z.string().describe('MCP tool execution result'), + z.union([ + z.string().describe('MCP tool execution result as text'), + z + .array( + z.object({ + type: z.string(), + text: z.string().optional(), + }), + ) + .describe('MCP tool execution result as content blocks'), + ]), ) type OutputSchema = ReturnType @@ -65,7 +78,19 @@ export const MCPTool = buildTool({ renderToolUseProgressMessage, renderToolResultMessage, isResultTruncated(output: Output): boolean { - return isOutputLineTruncated(output) + if (typeof output === 'string') { + return isOutputLineTruncated(output) + } + // Array of content blocks — check if any text block exceeds the display limit + if (Array.isArray(output)) { + return output.some( + block => + block?.type === 'text' && + typeof block.text === 'string' && + isOutputLineTruncated(block.text), + ) + } + return false }, mapToolResultToToolResultBlockParam(content, toolUseID) { return { diff --git a/src/tools/WebFetchTool/domainCheck.test.ts b/src/tools/WebFetchTool/domainCheck.test.ts index 15d3bc4c80..f4f6b4377c 100644 --- a/src/tools/WebFetchTool/domainCheck.test.ts +++ b/src/tools/WebFetchTool/domainCheck.test.ts @@ -36,7 +36,7 @@ describe('checkDomainBlocklist', () => { }) test('returns allowed without API call in Gemini mode', async () => { - process.env.CLAUDE_CODE_USE_GEMINI = '1' + process.env.CLAUDE_CODE_GOOGLE = '1' mock.module('../../utils/model/providers.js', () => ({ getAPIProvider: () => 'gemini', })) @@ -54,7 +54,7 @@ describe('checkDomainBlocklist', () => { test('calls Anthropic domain check in first-party mode', async () => { delete process.env.CLAUDE_CODE_USE_OPENAI - delete process.env.CLAUDE_CODE_USE_GEMINI + delete process.env.CLAUDE_CODE_GOOGLE delete process.env.CLAUDE_CODE_USE_GITHUB mock.module('../../utils/model/providers.js', () => ({ diff --git a/src/tools/WebSearchTool/README_SEARCH_PROVIDERS.md b/src/tools/WebSearchTool/README_SEARCH_PROVIDERS.md index 2a992f8236..8bad0fb286 100644 --- a/src/tools/WebSearchTool/README_SEARCH_PROVIDERS.md +++ b/src/tools/WebSearchTool/README_SEARCH_PROVIDERS.md @@ -464,7 +464,7 @@ export WEB_JSON_PATH=response.payload.results ## Retry -Failed requests (network errors, 5xx) are retried once after 500ms. Client errors (4xx) are not retried. Custom requests have a default 15s timeout. +Failed requests (network errors, 5xx) are retried once after 500ms. Client errors (4xx) are not retried. Custom requests have a default 120s timeout. ## Custom Provider Security Guardrails @@ -476,7 +476,7 @@ The custom provider enforces the following guardrails by default: | Block private IPs / localhost | ✅ | `WEB_CUSTOM_ALLOW_PRIVATE=true` | | Header allowlist | ✅ | `WEB_CUSTOM_ALLOW_ARBITRARY_HEADERS=true` | | Max POST body | 300 KB | `WEB_CUSTOM_MAX_BODY_KB=` | -| Request timeout | 15s | `WEB_CUSTOM_TIMEOUT_SEC=` | +| Request timeout | 120s | `WEB_CUSTOM_TIMEOUT_SEC=` | | Audit log (one-time warning) | ✅ | — | ### Self-hosted SearXNG example diff --git a/src/tools/WebSearchTool/providers/custom.test.ts b/src/tools/WebSearchTool/providers/custom.test.ts index d56419604f..553f1bc72e 100644 --- a/src/tools/WebSearchTool/providers/custom.test.ts +++ b/src/tools/WebSearchTool/providers/custom.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from 'bun:test' +import { describe, expect, test, beforeEach, afterEach } from 'bun:test' import { extractHits } from './custom.js' // --------------------------------------------------------------------------- @@ -83,3 +83,41 @@ describe('extractHits', () => { expect(hits).toHaveLength(1) }) }) + +// --------------------------------------------------------------------------- +// buildAuthHeadersForPreset — tested indirectly via env vars +// --------------------------------------------------------------------------- + +describe('buildAuthHeadersForPreset auth header behavior', () => { + const savedEnv: Record = {} + + beforeEach(() => { + for (const k of ['WEB_KEY', 'WEB_AUTH_HEADER', 'WEB_AUTH_SCHEME']) { + savedEnv[k] = process.env[k] + } + }) + + afterEach(() => { + for (const [k, v] of Object.entries(savedEnv)) { + if (v === undefined) delete process.env[k] + else process.env[k] = v + } + }) + + // We test isConfigured() which depends on WEB_SEARCH_API/WEB_PROVIDER/WEB_URL_TEMPLATE + // and the auth behavior through the public search() interface + test('custom provider is configured when WEB_URL_TEMPLATE is set', () => { + process.env.WEB_URL_TEMPLATE = 'https://example.com/search?q={query}' + const { customProvider } = require('./custom.js') + expect(customProvider.isConfigured()).toBe(true) + delete process.env.WEB_URL_TEMPLATE + }) + + test('custom provider is NOT configured when no env vars are set', () => { + delete process.env.WEB_URL_TEMPLATE + delete process.env.WEB_SEARCH_API + delete process.env.WEB_PROVIDER + const { customProvider } = require('./custom.js') + expect(customProvider.isConfigured()).toBe(false) + }) +}) diff --git a/src/tools/WebSearchTool/providers/custom.ts b/src/tools/WebSearchTool/providers/custom.ts index 36c920c22e..35f2abc5bc 100644 --- a/src/tools/WebSearchTool/providers/custom.ts +++ b/src/tools/WebSearchTool/providers/custom.ts @@ -225,8 +225,14 @@ function buildAuthHeadersForPreset(preset?: ProviderPreset): Record { const start = performance.now() let search: typeof import('duck-duck-scrape').search + let SafeSearchType: typeof import('duck-duck-scrape').SafeSearchType try { - ;({ search } = await import('duck-duck-scrape')) + ;({ search, SafeSearchType } = await import('duck-duck-scrape')) } catch { throw new Error('duck-duck-scrape package not installed. Run: npm install duck-duck-scrape') } if (signal?.aborted) throw new DOMException('Aborted', 'AbortError') // TODO: duck-duck-scrape doesn't accept AbortSignal — can't cancel in-flight searches - const response = await search(input.query, { safeSearch: 0 }) + const response = await search(input.query, { safeSearch: SafeSearchType.STRICT }) const hits = applyDomainFilters( response.results.map(r => ({ diff --git a/src/tools/WebSearchTool/providers/mojeek.ts b/src/tools/WebSearchTool/providers/mojeek.ts index 27511e219b..01376539c1 100644 --- a/src/tools/WebSearchTool/providers/mojeek.ts +++ b/src/tools/WebSearchTool/providers/mojeek.ts @@ -21,9 +21,10 @@ export const mojeekProvider: SearchProvider = { url.searchParams.set('q', input.query) url.searchParams.set('fmt', 'json') - const headers: Record = {} + const headers: Record = { + 'Accept': 'application/json', + } if (process.env.MOJEEK_API_KEY) { - headers['Accept'] = 'application/json' headers['Authorization'] = `Bearer ${process.env.MOJEEK_API_KEY}` } diff --git a/src/utils/apiPreconnect.test.ts b/src/utils/apiPreconnect.test.ts index 5950884ae6..5813facc40 100644 --- a/src/utils/apiPreconnect.test.ts +++ b/src/utils/apiPreconnect.test.ts @@ -34,7 +34,7 @@ describe('preconnectAnthropicApi', () => { }) test('does not fetch when Gemini mode is enabled', async () => { - process.env.CLAUDE_CODE_USE_GEMINI = '1' + process.env.CLAUDE_CODE_GOOGLE = '1' mock.module('./model/providers.js', () => ({ getAPIProvider: () => 'gemini', })) @@ -63,7 +63,7 @@ describe('preconnectAnthropicApi', () => { test('fetches in first-party mode', async () => { delete process.env.CLAUDE_CODE_USE_OPENAI - delete process.env.CLAUDE_CODE_USE_GEMINI + delete process.env.CLAUDE_CODE_GOOGLE delete process.env.CLAUDE_CODE_USE_GITHUB delete process.env.CLAUDE_CODE_USE_BEDROCK delete process.env.CLAUDE_CODE_USE_VERTEX diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 310799fb0d..3dfbfe575d 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -117,7 +117,7 @@ export function isAnthropicAuthEnabled(): boolean { isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) || isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) || isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) || - isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI) || + isEnvTruthy(process.env.CLAUDE_CODE_GOOGLE) || isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB) // Check if user has configured an external API key source @@ -1740,7 +1740,7 @@ export function isUsing3PServices(): boolean { isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) || isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) || isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) || - isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI) || + isEnvTruthy(process.env.CLAUDE_CODE_GOOGLE) || isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB) ) } diff --git a/src/utils/context.ts b/src/utils/context.ts index 0a6a2a41db..c430012e9a 100644 --- a/src/utils/context.ts +++ b/src/utils/context.ts @@ -77,7 +77,7 @@ export function getContextWindowForModel( // before hitting a hard context_window_exceeded error. const isOpenAIProvider = isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) || - isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI) || + isEnvTruthy(process.env.CLAUDE_CODE_GOOGLE) || isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB) if (isOpenAIProvider) { const openaiWindow = getOpenAIContextWindow(model) @@ -185,7 +185,7 @@ export function getModelMaxOutputTokens(model: string): { // OpenAI-compatible provider — use known output limits to avoid 400 errors if ( isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) || - isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI) || + isEnvTruthy(process.env.CLAUDE_CODE_GOOGLE) || isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB) ) { const openaiMax = getOpenAIMaxOutputTokens(model) diff --git a/src/utils/geminiCredentials.ts b/src/utils/geminiCredentials.ts index 5f986a4d7b..4d53ec4b02 100644 --- a/src/utils/geminiCredentials.ts +++ b/src/utils/geminiCredentials.ts @@ -22,7 +22,7 @@ export function readGeminiAccessToken(): string | undefined { } export function hydrateGeminiAccessTokenFromSecureStorage(): void { - if (!isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI)) { + if (!isEnvTruthy(process.env.CLAUDE_CODE_GOOGLE)) { return } const authMode = getGeminiAuthMode(process.env) diff --git a/src/utils/model/modelOptions.github.test.ts b/src/utils/model/modelOptions.github.test.ts index 1817a2e343..22017e0d68 100644 --- a/src/utils/model/modelOptions.github.test.ts +++ b/src/utils/model/modelOptions.github.test.ts @@ -15,7 +15,7 @@ async function importFreshModelOptionsModule() { const originalEnv = { CLAUDE_CODE_USE_GITHUB: process.env.CLAUDE_CODE_USE_GITHUB, CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI, - CLAUDE_CODE_USE_GEMINI: process.env.CLAUDE_CODE_USE_GEMINI, + CLAUDE_CODE_GOOGLE: process.env.CLAUDE_CODE_GOOGLE, CLAUDE_CODE_USE_BEDROCK: process.env.CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX: process.env.CLAUDE_CODE_USE_VERTEX, CLAUDE_CODE_USE_FOUNDRY: process.env.CLAUDE_CODE_USE_FOUNDRY, @@ -28,7 +28,7 @@ beforeEach(() => { mock.restore() delete process.env.CLAUDE_CODE_USE_GITHUB delete process.env.CLAUDE_CODE_USE_OPENAI - delete process.env.CLAUDE_CODE_USE_GEMINI + delete process.env.CLAUDE_CODE_GOOGLE delete process.env.CLAUDE_CODE_USE_BEDROCK delete process.env.CLAUDE_CODE_USE_VERTEX delete process.env.CLAUDE_CODE_USE_FOUNDRY @@ -41,7 +41,7 @@ beforeEach(() => { afterEach(() => { process.env.CLAUDE_CODE_USE_GITHUB = originalEnv.CLAUDE_CODE_USE_GITHUB process.env.CLAUDE_CODE_USE_OPENAI = originalEnv.CLAUDE_CODE_USE_OPENAI - process.env.CLAUDE_CODE_USE_GEMINI = originalEnv.CLAUDE_CODE_USE_GEMINI + process.env.CLAUDE_CODE_GOOGLE = originalEnv.CLAUDE_CODE_GOOGLE process.env.CLAUDE_CODE_USE_BEDROCK = originalEnv.CLAUDE_CODE_USE_BEDROCK process.env.CLAUDE_CODE_USE_VERTEX = originalEnv.CLAUDE_CODE_USE_VERTEX process.env.CLAUDE_CODE_USE_FOUNDRY = originalEnv.CLAUDE_CODE_USE_FOUNDRY @@ -64,7 +64,7 @@ afterEach(() => { test('GitHub provider exposes default + all Copilot models in /model options', async () => { process.env.CLAUDE_CODE_USE_GITHUB = '1' delete process.env.CLAUDE_CODE_USE_OPENAI - delete process.env.CLAUDE_CODE_USE_GEMINI + delete process.env.CLAUDE_CODE_GOOGLE delete process.env.CLAUDE_CODE_USE_BEDROCK delete process.env.CLAUDE_CODE_USE_VERTEX delete process.env.CLAUDE_CODE_USE_FOUNDRY diff --git a/src/utils/model/modelStrings.github.test.ts b/src/utils/model/modelStrings.github.test.ts index c6e95f7c18..38919311f0 100644 --- a/src/utils/model/modelStrings.github.test.ts +++ b/src/utils/model/modelStrings.github.test.ts @@ -7,7 +7,7 @@ import { getModelStrings } from './modelStrings.js' const originalEnv = { CLAUDE_CODE_USE_GITHUB: process.env.CLAUDE_CODE_USE_GITHUB, CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI, - CLAUDE_CODE_USE_GEMINI: process.env.CLAUDE_CODE_USE_GEMINI, + CLAUDE_CODE_GOOGLE: process.env.CLAUDE_CODE_GOOGLE, CLAUDE_CODE_USE_BEDROCK: process.env.CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX: process.env.CLAUDE_CODE_USE_VERTEX, CLAUDE_CODE_USE_FOUNDRY: process.env.CLAUDE_CODE_USE_FOUNDRY, @@ -16,7 +16,7 @@ const originalEnv = { function clearProviderFlags(): void { delete process.env.CLAUDE_CODE_USE_GITHUB delete process.env.CLAUDE_CODE_USE_OPENAI - delete process.env.CLAUDE_CODE_USE_GEMINI + delete process.env.CLAUDE_CODE_GOOGLE delete process.env.CLAUDE_CODE_USE_BEDROCK delete process.env.CLAUDE_CODE_USE_VERTEX delete process.env.CLAUDE_CODE_USE_FOUNDRY @@ -25,7 +25,7 @@ function clearProviderFlags(): void { afterEach(() => { process.env.CLAUDE_CODE_USE_GITHUB = originalEnv.CLAUDE_CODE_USE_GITHUB process.env.CLAUDE_CODE_USE_OPENAI = originalEnv.CLAUDE_CODE_USE_OPENAI - process.env.CLAUDE_CODE_USE_GEMINI = originalEnv.CLAUDE_CODE_USE_GEMINI + process.env.CLAUDE_CODE_GOOGLE = originalEnv.CLAUDE_CODE_GOOGLE process.env.CLAUDE_CODE_USE_BEDROCK = originalEnv.CLAUDE_CODE_USE_BEDROCK process.env.CLAUDE_CODE_USE_VERTEX = originalEnv.CLAUDE_CODE_USE_VERTEX process.env.CLAUDE_CODE_USE_FOUNDRY = originalEnv.CLAUDE_CODE_USE_FOUNDRY diff --git a/src/utils/model/openaiContextWindows.ts b/src/utils/model/openaiContextWindows.ts index 568e91d240..c7505af526 100644 --- a/src/utils/model/openaiContextWindows.ts +++ b/src/utils/model/openaiContextWindows.ts @@ -90,7 +90,7 @@ const OPENAI_CONTEXT_WINDOWS: Record = { 'google/gemini-2.0-flash':1_048_576, 'google/gemini-2.5-pro': 1_048_576, - // Google (native via CLAUDE_CODE_USE_GEMINI) + // Google (native via CLAUDE_CODE_GOOGLE) 'gemini-2.0-flash': 1_048_576, 'gemini-2.5-pro': 1_048_576, 'gemini-2.5-flash': 1_048_576, @@ -195,7 +195,7 @@ const OPENAI_MAX_OUTPUT_TOKENS: Record = { 'google/gemini-2.0-flash': 8_192, 'google/gemini-2.5-pro': 65_536, - // Google (native via CLAUDE_CODE_USE_GEMINI) + // Google (native via CLAUDE_CODE_GOOGLE) 'gemini-2.0-flash': 8_192, 'gemini-2.5-pro': 65_536, 'gemini-2.5-flash': 65_536, diff --git a/src/utils/model/providers.test.ts b/src/utils/model/providers.test.ts index a8e8406970..802fc06f4c 100644 --- a/src/utils/model/providers.test.ts +++ b/src/utils/model/providers.test.ts @@ -1,7 +1,7 @@ import { afterEach, expect, test } from 'bun:test' const originalEnv = { - CLAUDE_CODE_USE_GEMINI: process.env.CLAUDE_CODE_USE_GEMINI, + CLAUDE_CODE_GOOGLE: process.env.CLAUDE_CODE_GOOGLE, CLAUDE_CODE_USE_GITHUB: process.env.CLAUDE_CODE_USE_GITHUB, CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI, CLAUDE_CODE_USE_BEDROCK: process.env.CLAUDE_CODE_USE_BEDROCK, @@ -13,7 +13,7 @@ const originalEnv = { } afterEach(() => { - process.env.CLAUDE_CODE_USE_GEMINI = originalEnv.CLAUDE_CODE_USE_GEMINI + process.env.CLAUDE_CODE_GOOGLE = originalEnv.CLAUDE_CODE_GOOGLE process.env.CLAUDE_CODE_USE_GITHUB = originalEnv.CLAUDE_CODE_USE_GITHUB process.env.CLAUDE_CODE_USE_OPENAI = originalEnv.CLAUDE_CODE_USE_OPENAI process.env.CLAUDE_CODE_USE_BEDROCK = originalEnv.CLAUDE_CODE_USE_BEDROCK @@ -29,7 +29,7 @@ async function importFreshProvidersModule() { } function clearProviderEnv(): void { - delete process.env.CLAUDE_CODE_USE_GEMINI + delete process.env.CLAUDE_CODE_GOOGLE delete process.env.CLAUDE_CODE_USE_GITHUB delete process.env.CLAUDE_CODE_USE_OPENAI delete process.env.CLAUDE_CODE_USE_BEDROCK @@ -53,7 +53,7 @@ test('first-party provider keeps Anthropic account setup flow enabled', () => { test.each([ ['CLAUDE_CODE_USE_OPENAI', 'openai'], ['CLAUDE_CODE_USE_GITHUB', 'github'], - ['CLAUDE_CODE_USE_GEMINI', 'gemini'], + ['CLAUDE_CODE_GOOGLE', 'gemini'], ['CLAUDE_CODE_USE_BEDROCK', 'bedrock'], ['CLAUDE_CODE_USE_VERTEX', 'vertex'], ['CLAUDE_CODE_USE_FOUNDRY', 'foundry'], @@ -72,7 +72,7 @@ test.each([ test('GEMINI takes precedence over GitHub when both are set', async () => { clearProviderEnv() - process.env.CLAUDE_CODE_USE_GEMINI = '1' + process.env.CLAUDE_CODE_GOOGLE = '1' process.env.CLAUDE_CODE_USE_GITHUB = '1' const { getAPIProvider } = await importFreshProvidersModule() diff --git a/src/utils/model/providers.ts b/src/utils/model/providers.ts index d65ee98218..2c323953e4 100644 --- a/src/utils/model/providers.ts +++ b/src/utils/model/providers.ts @@ -13,7 +13,7 @@ export type APIProvider = | 'codex' export function getAPIProvider(): APIProvider { - return isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI) + return isEnvTruthy(process.env.CLAUDE_CODE_GOOGLE) ? 'gemini' : isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB) ? 'github' diff --git a/src/utils/providerFlag.test.ts b/src/utils/providerFlag.test.ts index a76ee07f9c..d841fe9d3e 100644 --- a/src/utils/providerFlag.test.ts +++ b/src/utils/providerFlag.test.ts @@ -8,7 +8,7 @@ import { const ENV_KEYS = [ 'CLAUDE_CODE_USE_OPENAI', - 'CLAUDE_CODE_USE_GEMINI', + 'CLAUDE_CODE_GOOGLE', 'CLAUDE_CODE_USE_GITHUB', 'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX', @@ -29,7 +29,7 @@ beforeEach(() => { const RESET_KEYS = [ 'CLAUDE_CODE_USE_OPENAI', - 'CLAUDE_CODE_USE_GEMINI', + 'CLAUDE_CODE_GOOGLE', 'CLAUDE_CODE_USE_GITHUB', 'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX', @@ -90,7 +90,7 @@ describe('applyProviderFlag - anthropic', () => { const result = applyProviderFlag('anthropic', []) expect(result.error).toBeUndefined() expect(process.env.CLAUDE_CODE_USE_OPENAI).toBeUndefined() - expect(process.env.CLAUDE_CODE_USE_GEMINI).toBeUndefined() + expect(process.env.CLAUDE_CODE_GOOGLE).toBeUndefined() }) }) @@ -108,10 +108,10 @@ describe('applyProviderFlag - openai', () => { }) describe('applyProviderFlag - gemini', () => { - test('sets CLAUDE_CODE_USE_GEMINI=1', () => { + test('sets CLAUDE_CODE_GOOGLE=1', () => { const result = applyProviderFlag('gemini', []) expect(result.error).toBeUndefined() - expect(process.env.CLAUDE_CODE_USE_GEMINI).toBe('1') + expect(process.env.CLAUDE_CODE_GOOGLE).toBe('1') }) test('sets GEMINI_MODEL when --model is provided', () => { diff --git a/src/utils/providerFlag.ts b/src/utils/providerFlag.ts index b2cbc06fe9..2ff0c36228 100644 --- a/src/utils/providerFlag.ts +++ b/src/utils/providerFlag.ts @@ -90,7 +90,7 @@ export function applyProviderFlag( break case 'gemini': - process.env.CLAUDE_CODE_USE_GEMINI = '1' + process.env.CLAUDE_CODE_GOOGLE = '1' if (model) process.env.GEMINI_MODEL ??= model break diff --git a/src/utils/providerProfile.test.ts b/src/utils/providerProfile.test.ts index b501493188..a8235e4d90 100644 --- a/src/utils/providerProfile.test.ts +++ b/src/utils/providerProfile.test.ts @@ -149,7 +149,7 @@ test('matching persisted gemini env is reused for gemini launch', async () => { processEnv: {}, }) - assert.equal(env.CLAUDE_CODE_USE_GEMINI, '1') + assert.equal(env.CLAUDE_CODE_GOOGLE, '1') assert.equal(env.CLAUDE_CODE_USE_OPENAI, undefined) assert.equal(env.GEMINI_MODEL, 'gemini-2.5-flash') assert.equal(env.GEMINI_API_KEY, 'gem-persisted') @@ -177,7 +177,7 @@ test('gemini launch ignores mismatched persisted openai env and strips other pro }, }) - assert.equal(env.CLAUDE_CODE_USE_GEMINI, '1') + assert.equal(env.CLAUDE_CODE_GOOGLE, '1') assert.equal(env.CLAUDE_CODE_USE_OPENAI, undefined) assert.equal(env.GEMINI_MODEL, 'gemini-2.0-flash') assert.equal(env.GEMINI_API_KEY, 'gem-live') @@ -426,7 +426,7 @@ test('buildStartupEnvFromProfile applies persisted gemini settings when no provi processEnv: {}, }) - assert.equal(env.CLAUDE_CODE_USE_GEMINI, '1') + assert.equal(env.CLAUDE_CODE_GOOGLE, '1') assert.equal(env.CLAUDE_CODE_USE_OPENAI, undefined) assert.equal(env.GEMINI_API_KEY, 'gem-test') assert.equal(env.GEMINI_MODEL, 'gemini-2.5-flash') @@ -442,7 +442,7 @@ test('buildStartupEnvFromProfile rehydrates stored Gemini access token for acces readGeminiAccessToken: () => 'token-live', }) - assert.equal(env.CLAUDE_CODE_USE_GEMINI, '1') + assert.equal(env.CLAUDE_CODE_GOOGLE, '1') assert.equal(env.GEMINI_AUTH_MODE, 'access-token') assert.equal(env.GEMINI_ACCESS_TOKEN, 'token-live') assert.equal(env.GEMINI_API_KEY, undefined) @@ -459,7 +459,7 @@ test('buildStartupEnvFromProfile does not inject stored access token for adc pro readGeminiAccessToken: () => 'token-live', }) - assert.equal(env.CLAUDE_CODE_USE_GEMINI, '1') + assert.equal(env.CLAUDE_CODE_GOOGLE, '1') assert.equal(env.GEMINI_AUTH_MODE, 'adc') assert.equal(env.GEMINI_ACCESS_TOKEN, undefined) assert.equal(env.GEMINI_API_KEY, undefined) @@ -467,7 +467,7 @@ test('buildStartupEnvFromProfile does not inject stored access token for adc pro test('buildStartupEnvFromProfile leaves explicit provider selections untouched', async () => { const processEnv = { - CLAUDE_CODE_USE_GEMINI: '1', + CLAUDE_CODE_GOOGLE: '1', GEMINI_API_KEY: 'gem-live', GEMINI_MODEL: 'gemini-2.0-flash', } @@ -481,7 +481,7 @@ test('buildStartupEnvFromProfile leaves explicit provider selections untouched', }) assert.equal(env, processEnv) - assert.equal(env.CLAUDE_CODE_USE_GEMINI, '1') + assert.equal(env.CLAUDE_CODE_GOOGLE, '1') assert.equal(env.OPENAI_API_KEY, undefined) }) diff --git a/src/utils/providerProfile.ts b/src/utils/providerProfile.ts index 5f96d8d9a5..bdcd2ce3d4 100644 --- a/src/utils/providerProfile.ts +++ b/src/utils/providerProfile.ts @@ -22,7 +22,7 @@ export const DEFAULT_GEMINI_MODEL = 'gemini-2.0-flash' const PROFILE_ENV_KEYS = [ 'CLAUDE_CODE_USE_OPENAI', - 'CLAUDE_CODE_USE_GEMINI', + 'CLAUDE_CODE_GOOGLE', 'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX', 'CLAUDE_CODE_USE_FOUNDRY', @@ -415,7 +415,7 @@ export function hasExplicitProviderSelection( return ( processEnv.CLAUDE_CODE_USE_OPENAI !== undefined || processEnv.CLAUDE_CODE_USE_GITHUB !== undefined || - processEnv.CLAUDE_CODE_USE_GEMINI !== undefined || + processEnv.CLAUDE_CODE_GOOGLE !== undefined || processEnv.CLAUDE_CODE_USE_BEDROCK !== undefined || processEnv.CLAUDE_CODE_USE_VERTEX !== undefined || processEnv.CLAUDE_CODE_USE_FOUNDRY !== undefined @@ -490,7 +490,7 @@ export async function buildLaunchEnv(options: { if (options.profile === 'gemini') { const env: NodeJS.ProcessEnv = { ...processEnv, - CLAUDE_CODE_USE_GEMINI: '1', + CLAUDE_CODE_GOOGLE: '1', } delete env.CLAUDE_CODE_USE_OPENAI @@ -545,7 +545,7 @@ export async function buildLaunchEnv(options: { CLAUDE_CODE_USE_OPENAI: '1', } - delete env.CLAUDE_CODE_USE_GEMINI + delete env.CLAUDE_CODE_GOOGLE delete env.CLAUDE_CODE_USE_GITHUB delete env.GEMINI_API_KEY delete env.GEMINI_AUTH_MODE diff --git a/src/utils/providerProfiles.test.ts b/src/utils/providerProfiles.test.ts index 917f8dd67e..ce0122da59 100644 --- a/src/utils/providerProfiles.test.ts +++ b/src/utils/providerProfiles.test.ts @@ -12,7 +12,7 @@ const RESTORED_KEYS = [ 'CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED', 'CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID', 'CLAUDE_CODE_USE_OPENAI', - 'CLAUDE_CODE_USE_GEMINI', + 'CLAUDE_CODE_GOOGLE', 'CLAUDE_CODE_USE_GITHUB', 'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX', @@ -102,14 +102,14 @@ describe('applyProviderProfileToProcessEnv', () => { test('openai profile clears competing gemini/github flags', async () => { const { applyProviderProfileToProcessEnv } = await importFreshProviderProfileModules() - process.env.CLAUDE_CODE_USE_GEMINI = '1' + process.env.CLAUDE_CODE_GOOGLE = '1' process.env.CLAUDE_CODE_USE_GITHUB = '1' applyProviderProfileToProcessEnv(buildProfile()) const { getAPIProvider: getFreshAPIProvider } = await importFreshProvidersModule() - expect(process.env.CLAUDE_CODE_USE_GEMINI).toBeUndefined() + expect(process.env.CLAUDE_CODE_GOOGLE).toBeUndefined() expect(process.env.CLAUDE_CODE_USE_GITHUB).toBeUndefined() expect(String(process.env.CLAUDE_CODE_USE_OPENAI)).toBe('1') expect(process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID).toBe( @@ -121,7 +121,7 @@ describe('applyProviderProfileToProcessEnv', () => { test('anthropic profile clears competing gemini/github flags', async () => { const { applyProviderProfileToProcessEnv } = await importFreshProviderProfileModules() - process.env.CLAUDE_CODE_USE_GEMINI = '1' + process.env.CLAUDE_CODE_GOOGLE = '1' process.env.CLAUDE_CODE_USE_GITHUB = '1' applyProviderProfileToProcessEnv( @@ -134,7 +134,7 @@ describe('applyProviderProfileToProcessEnv', () => { const { getAPIProvider: getFreshAPIProvider } = await importFreshProvidersModule() - expect(process.env.CLAUDE_CODE_USE_GEMINI).toBeUndefined() + expect(process.env.CLAUDE_CODE_GOOGLE).toBeUndefined() expect(process.env.CLAUDE_CODE_USE_GITHUB).toBeUndefined() expect(process.env.CLAUDE_CODE_USE_OPENAI).toBeUndefined() expect(getFreshAPIProvider()).toBe('firstParty') @@ -254,7 +254,7 @@ describe('applyActiveProviderProfileFromConfig', () => { const { applyActiveProviderProfileFromConfig } = await importFreshProviderProfileModules() delete process.env.CLAUDE_CODE_USE_OPENAI - delete process.env.CLAUDE_CODE_USE_GEMINI + delete process.env.CLAUDE_CODE_GOOGLE delete process.env.CLAUDE_CODE_USE_GITHUB delete process.env.CLAUDE_CODE_USE_BEDROCK delete process.env.CLAUDE_CODE_USE_VERTEX @@ -388,7 +388,7 @@ describe('deleteProviderProfile', () => { expect(process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED).toBeUndefined() expect(process.env.CLAUDE_CODE_USE_OPENAI).toBeUndefined() - expect(process.env.CLAUDE_CODE_USE_GEMINI).toBeUndefined() + expect(process.env.CLAUDE_CODE_GOOGLE).toBeUndefined() expect(process.env.CLAUDE_CODE_USE_GITHUB).toBeUndefined() expect(process.env.CLAUDE_CODE_USE_BEDROCK).toBeUndefined() expect(process.env.CLAUDE_CODE_USE_VERTEX).toBeUndefined() diff --git a/src/utils/providerProfiles.ts b/src/utils/providerProfiles.ts index 347e11b69e..7bea028a28 100644 --- a/src/utils/providerProfiles.ts +++ b/src/utils/providerProfiles.ts @@ -257,7 +257,7 @@ function hasProviderSelectionFlags( ): boolean { return ( processEnv.CLAUDE_CODE_USE_OPENAI !== undefined || - processEnv.CLAUDE_CODE_USE_GEMINI !== undefined || + processEnv.CLAUDE_CODE_GOOGLE !== undefined || processEnv.CLAUDE_CODE_USE_GITHUB !== undefined || processEnv.CLAUDE_CODE_USE_BEDROCK !== undefined || processEnv.CLAUDE_CODE_USE_VERTEX !== undefined || @@ -274,7 +274,7 @@ function hasConflictingProviderFlagsForProfile( } return ( - processEnv.CLAUDE_CODE_USE_GEMINI !== undefined || + processEnv.CLAUDE_CODE_GOOGLE !== undefined || processEnv.CLAUDE_CODE_USE_GITHUB !== undefined || processEnv.CLAUDE_CODE_USE_BEDROCK !== undefined || processEnv.CLAUDE_CODE_USE_VERTEX !== undefined || @@ -318,7 +318,7 @@ function isProcessEnvAlignedWithProfile( return ( processEnv.CLAUDE_CODE_USE_OPENAI !== undefined && - processEnv.CLAUDE_CODE_USE_GEMINI === undefined && + processEnv.CLAUDE_CODE_GOOGLE === undefined && processEnv.CLAUDE_CODE_USE_GITHUB === undefined && processEnv.CLAUDE_CODE_USE_BEDROCK === undefined && processEnv.CLAUDE_CODE_USE_VERTEX === undefined && @@ -346,7 +346,7 @@ export function clearProviderProfileEnvFromProcessEnv( processEnv: NodeJS.ProcessEnv = process.env, ): void { delete processEnv.CLAUDE_CODE_USE_OPENAI - delete processEnv.CLAUDE_CODE_USE_GEMINI + delete processEnv.CLAUDE_CODE_GOOGLE delete processEnv.CLAUDE_CODE_USE_GITHUB delete processEnv.CLAUDE_CODE_USE_BEDROCK delete processEnv.CLAUDE_CODE_USE_VERTEX diff --git a/src/utils/providerValidation.test.ts b/src/utils/providerValidation.test.ts index 3917638f34..bb0bc74756 100644 --- a/src/utils/providerValidation.test.ts +++ b/src/utils/providerValidation.test.ts @@ -3,7 +3,7 @@ import { afterEach, expect, test } from 'bun:test' import { getProviderValidationError } from './providerValidation.ts' const originalEnv = { - CLAUDE_CODE_USE_GEMINI: process.env.CLAUDE_CODE_USE_GEMINI, + CLAUDE_CODE_GOOGLE: process.env.CLAUDE_CODE_GOOGLE, GEMINI_API_KEY: process.env.GEMINI_API_KEY, GOOGLE_API_KEY: process.env.GOOGLE_API_KEY, GEMINI_ACCESS_TOKEN: process.env.GEMINI_ACCESS_TOKEN, @@ -20,7 +20,7 @@ function restoreEnv(key: string, value: string | undefined): void { } afterEach(() => { - restoreEnv('CLAUDE_CODE_USE_GEMINI', originalEnv.CLAUDE_CODE_USE_GEMINI) + restoreEnv('CLAUDE_CODE_GOOGLE', originalEnv.CLAUDE_CODE_GOOGLE) restoreEnv('GEMINI_API_KEY', originalEnv.GEMINI_API_KEY) restoreEnv('GOOGLE_API_KEY', originalEnv.GOOGLE_API_KEY) restoreEnv('GEMINI_ACCESS_TOKEN', originalEnv.GEMINI_ACCESS_TOKEN) @@ -32,7 +32,7 @@ afterEach(() => { }) test('accepts GEMINI_ACCESS_TOKEN as valid Gemini auth', async () => { - process.env.CLAUDE_CODE_USE_GEMINI = '1' + process.env.CLAUDE_CODE_GOOGLE = '1' process.env.GEMINI_AUTH_MODE = 'access-token' delete process.env.GEMINI_API_KEY delete process.env.GOOGLE_API_KEY @@ -42,7 +42,7 @@ test('accepts GEMINI_ACCESS_TOKEN as valid Gemini auth', async () => { }) test('accepts ADC credentials for Gemini auth', async () => { - process.env.CLAUDE_CODE_USE_GEMINI = '1' + process.env.CLAUDE_CODE_GOOGLE = '1' process.env.GEMINI_AUTH_MODE = 'adc' delete process.env.GEMINI_API_KEY delete process.env.GOOGLE_API_KEY @@ -60,7 +60,7 @@ test('accepts ADC credentials for Gemini auth', async () => { }) test('still errors when no Gemini credential source is available', async () => { - process.env.CLAUDE_CODE_USE_GEMINI = '1' + process.env.CLAUDE_CODE_GOOGLE = '1' process.env.GEMINI_AUTH_MODE = 'access-token' delete process.env.GEMINI_API_KEY delete process.env.GOOGLE_API_KEY @@ -68,6 +68,6 @@ test('still errors when no Gemini credential source is available', async () => { delete process.env.GOOGLE_APPLICATION_CREDENTIALS await expect(getProviderValidationError(process.env)).resolves.toBe( - 'GEMINI_API_KEY, GOOGLE_API_KEY, GEMINI_ACCESS_TOKEN, or Google ADC credentials are required when CLAUDE_CODE_USE_GEMINI=1.', + 'GEMINI_API_KEY, GOOGLE_API_KEY, GEMINI_ACCESS_TOKEN, or Google ADC credentials are required when CLAUDE_CODE_GOOGLE=1.', ) }) diff --git a/src/utils/providerValidation.ts b/src/utils/providerValidation.ts index d1e3d35371..79cf5633d6 100644 --- a/src/utils/providerValidation.ts +++ b/src/utils/providerValidation.ts @@ -72,12 +72,12 @@ export async function getProviderValidationError( const useOpenAI = isEnvTruthy(env.CLAUDE_CODE_USE_OPENAI) const useGithub = isEnvTruthy(env.CLAUDE_CODE_USE_GITHUB) - if (isEnvTruthy(env.CLAUDE_CODE_USE_GEMINI)) { + if (isEnvTruthy(env.CLAUDE_CODE_GOOGLE)) { const geminiCredential = await ( options?.resolveGeminiCredential ?? resolveGeminiCredential )(env) if (geminiCredential.kind === 'none') { - return 'GEMINI_API_KEY, GOOGLE_API_KEY, GEMINI_ACCESS_TOKEN, or Google ADC credentials are required when CLAUDE_CODE_USE_GEMINI=1.' + return 'GEMINI_API_KEY, GOOGLE_API_KEY, GEMINI_ACCESS_TOKEN, or Google ADC credentials are required when CLAUDE_CODE_GOOGLE=1.' } return null } diff --git a/src/utils/swarm/spawnUtils.ts b/src/utils/swarm/spawnUtils.ts index 037d273dd5..0d1e7ab24e 100644 --- a/src/utils/swarm/spawnUtils.ts +++ b/src/utils/swarm/spawnUtils.ts @@ -100,7 +100,7 @@ const TEAMMATE_ENV_VARS = [ 'CLAUDE_CODE_USE_VERTEX', 'CLAUDE_CODE_USE_FOUNDRY', 'CLAUDE_CODE_USE_GITHUB', - 'CLAUDE_CODE_USE_GEMINI', + 'CLAUDE_CODE_GOOGLE', 'CLAUDE_CODE_USE_OPENAI', 'GITHUB_TOKEN', 'GH_TOKEN',