From b4aa27183de8d95c44e3af841146d99088d06842 Mon Sep 17 00:00:00 2001 From: gnanam1990 Date: Thu, 2 Apr 2026 21:51:26 +0530 Subject: [PATCH] fix: route CLAUDE_CODE_USE_GEMINI through OpenAI-compatible shim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Gemini provider uses Google's OpenAI-compatible endpoint (generativelanguage.googleapis.com/v1beta/openai) but the client routing condition in client.ts only checked CLAUDE_CODE_USE_OPENAI and CLAUDE_CODE_USE_GITHUB — CLAUDE_CODE_USE_GEMINI was missing. This caused every Gemini request to fall through to the Anthropic client path. Since ANTHROPIC_API_KEY is not set when using Gemini, the Anthropic SDK threw: "Could not resolve authentication method. Expected either apiKey or authToken to be set." Fix: add CLAUDE_CODE_USE_GEMINI to the OpenAI shim routing condition so Gemini requests correctly reach createOpenAIShimClient(), which maps GEMINI_API_KEY → OPENAI_API_KEY and sets OPENAI_BASE_URL to the Google endpoint. Closes #176 Co-Authored-By: Claude Sonnet 4.6 --- src/services/api/client.test.ts | 121 ++++++++++++++++++++++++++++++++ src/services/api/client.ts | 3 +- 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 src/services/api/client.test.ts diff --git a/src/services/api/client.test.ts b/src/services/api/client.test.ts new file mode 100644 index 00000000..6d92be7b --- /dev/null +++ b/src/services/api/client.test.ts @@ -0,0 +1,121 @@ +import { afterEach, beforeEach, expect, test } from 'bun:test' +import { getAnthropicClient } from './client.js' + +type FetchType = typeof globalThis.fetch + +type ShimClient = { + beta: { + messages: { + create: (params: Record) => Promise + } + } +} + +const originalFetch = globalThis.fetch +const originalMacro = (globalThis as Record).MACRO +const originalEnv = { + CLAUDE_CODE_USE_GEMINI: process.env.CLAUDE_CODE_USE_GEMINI, + GEMINI_API_KEY: process.env.GEMINI_API_KEY, + GEMINI_MODEL: process.env.GEMINI_MODEL, + GEMINI_BASE_URL: process.env.GEMINI_BASE_URL, + GOOGLE_API_KEY: process.env.GOOGLE_API_KEY, + OPENAI_API_KEY: process.env.OPENAI_API_KEY, + OPENAI_BASE_URL: process.env.OPENAI_BASE_URL, + OPENAI_MODEL: process.env.OPENAI_MODEL, + ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY, + ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN, +} + +beforeEach(() => { + ;(globalThis as Record).MACRO = { VERSION: 'test-version' } + process.env.CLAUDE_CODE_USE_GEMINI = '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' + + delete process.env.GOOGLE_API_KEY + delete process.env.OPENAI_API_KEY + delete process.env.OPENAI_BASE_URL + delete process.env.OPENAI_MODEL + delete process.env.ANTHROPIC_API_KEY + delete process.env.ANTHROPIC_AUTH_TOKEN +}) + +afterEach(() => { + ;(globalThis as Record).MACRO = originalMacro + process.env.CLAUDE_CODE_USE_GEMINI = originalEnv.CLAUDE_CODE_USE_GEMINI + process.env.GEMINI_API_KEY = originalEnv.GEMINI_API_KEY + process.env.GEMINI_MODEL = originalEnv.GEMINI_MODEL + process.env.GEMINI_BASE_URL = originalEnv.GEMINI_BASE_URL + process.env.GOOGLE_API_KEY = originalEnv.GOOGLE_API_KEY + process.env.OPENAI_API_KEY = originalEnv.OPENAI_API_KEY + process.env.OPENAI_BASE_URL = originalEnv.OPENAI_BASE_URL + process.env.OPENAI_MODEL = originalEnv.OPENAI_MODEL + process.env.ANTHROPIC_API_KEY = originalEnv.ANTHROPIC_API_KEY + process.env.ANTHROPIC_AUTH_TOKEN = originalEnv.ANTHROPIC_AUTH_TOKEN + globalThis.fetch = originalFetch +}) + +test('routes Gemini provider requests through the OpenAI-compatible shim', async () => { + let capturedUrl: string | undefined + let capturedHeaders: Headers | undefined + let capturedBody: Record | undefined + + globalThis.fetch = (async (input, init) => { + capturedUrl = + typeof input === 'string' + ? input + : 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: [ + { + message: { + role: 'assistant', + content: 'gemini ok', + }, + finish_reason: 'stop', + }, + ], + usage: { + prompt_tokens: 8, + completion_tokens: 3, + total_tokens: 11, + }, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as FetchType + + const client = (await getAnthropicClient({ + maxRetries: 0, + model: 'gemini-2.0-flash', + })) as unknown as ShimClient + + const response = await client.beta.messages.create({ + model: 'gemini-2.0-flash', + system: 'test system', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + 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') + expect(response).toMatchObject({ + role: 'assistant', + model: 'gemini-2.0-flash', + }) +}) diff --git a/src/services/api/client.ts b/src/services/api/client.ts index ee50e35c..a32e0779 100644 --- a/src/services/api/client.ts +++ b/src/services/api/client.ts @@ -156,7 +156,8 @@ export async function getAnthropicClient({ } if ( isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) || - isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB) + isEnvTruthy(process.env.CLAUDE_CODE_USE_GITHUB) || + isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI) ) { const { createOpenAIShimClient } = await import('./openaiShim.js') return createOpenAIShimClient({