Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 10 additions & 10 deletions scripts/system-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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()
}

Expand Down Expand Up @@ -265,11 +265,11 @@ function checkOpenAIEnv(): CheckResult[] {
}

async function checkBaseUrlReachability(): Promise<CheckResult> {
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).')
}

Expand All @@ -281,7 +281,7 @@ async function checkBaseUrlReachability(): Promise<CheckResult> {
}

const geminiBaseUrl = 'https://generativelanguage.googleapis.com/v1beta/openai'
const resolvedBaseUrl = useGemini
const resolvedBaseUrl = useGoogle
? (process.env.GEMINI_BASE_URL ?? geminiBaseUrl)
: undefined
const request = resolveProviderRequest({
Expand Down Expand Up @@ -324,7 +324,7 @@ async function checkBaseUrlReachability(): Promise<CheckResult> {
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}`
Expand Down Expand Up @@ -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).')
Expand Down Expand Up @@ -417,9 +417,9 @@ function checkOllamaProcessorMode(): CheckResult {
}

function serializeSafeEnvSummary(): Record<string, string | boolean> {
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),
Expand Down
35 changes: 30 additions & 5 deletions src/components/StartupScreen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand All @@ -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[] = []

Expand Down Expand Up @@ -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
Expand Down
37 changes: 16 additions & 21 deletions src/services/api/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const originalFetch = globalThis.fetch
const originalMacro = (globalThis as Record<string, unknown>).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,
Expand All @@ -39,7 +39,7 @@ function restoreEnv(key: string, value: string | undefined): void {

beforeEach(() => {
;(globalThis as Record<string, unknown>).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'
Expand All @@ -58,7 +58,7 @@ beforeEach(() => {
afterEach(() => {
;(globalThis as Record<string, unknown>).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)
Expand All @@ -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<string, unknown> | undefined

globalThis.fetch = (async (input, init) => {
Expand All @@ -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<string, unknown>

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,
},
}),
{
Expand All @@ -128,19 +123,19 @@ 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',
})
})

test('strips Anthropic-specific custom headers before sending OpenAI-compatible shim requests', async () => {
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'
Expand Down
7 changes: 5 additions & 2 deletions src/services/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading
Loading