fix: route CLAUDE_CODE_USE_GEMINI through OpenAI-compatible shim

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 <noreply@anthropic.com>
This commit is contained in:
gnanam1990
2026-04-02 21:51:26 +05:30
parent 3353101e83
commit b4aa27183d
2 changed files with 123 additions and 1 deletions

View File

@@ -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<string, unknown>) => Promise<unknown>
}
}
}
const originalFetch = globalThis.fetch
const originalMacro = (globalThis as Record<string, unknown>).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<string, unknown>).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<string, unknown>).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<string, unknown> | 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<string, unknown>
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',
})
})

View File

@@ -156,7 +156,8 @@ export async function getAnthropicClient({
} }
if ( if (
isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) || 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') const { createOpenAIShimClient } = await import('./openaiShim.js')
return createOpenAIShimClient({ return createOpenAIShimClient({