From 5175dba6dabd776344cb71f81efc2ef6f81ed11e Mon Sep 17 00:00:00 2001 From: Vasanthdev2004 Date: Wed, 1 Apr 2026 15:26:55 +0530 Subject: [PATCH 1/7] fix: stub internal-only modules in open build --- scripts/build.ts | 53 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/scripts/build.ts b/scripts/build.ts index dbbbcedb..014fc402 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -65,6 +65,39 @@ const result = await Bun.build({ { name: 'bun-bundle-shim', setup(build) { + const internalFeatureStubModules = new Map([ + [ + '../daemon/workerRegistry.js', + 'export async function runDaemonWorker() { throw new Error("Daemon worker is unavailable in the open build."); }', + ], + [ + '../daemon/main.js', + 'export async function daemonMain() { throw new Error("Daemon mode is unavailable in the open build."); }', + ], + [ + '../cli/bg.js', + ` +export async function psHandler() { throw new Error("Background sessions are unavailable in the open build."); } +export async function logsHandler() { throw new Error("Background sessions are unavailable in the open build."); } +export async function attachHandler() { throw new Error("Background sessions are unavailable in the open build."); } +export async function killHandler() { throw new Error("Background sessions are unavailable in the open build."); } +export async function handleBgFlag() { throw new Error("Background sessions are unavailable in the open build."); } +`, + ], + [ + '../cli/handlers/templateJobs.js', + 'export async function templatesMain() { throw new Error("Template jobs are unavailable in the open build."); }', + ], + [ + '../environment-runner/main.js', + 'export async function environmentRunnerMain() { throw new Error("Environment runner is unavailable in the open build."); }', + ], + [ + '../self-hosted-runner/main.js', + 'export async function selfHostedRunnerMain() { throw new Error("Self-hosted runner is unavailable in the open build."); }', + ], + ] as const) + // Resolve `import { feature } from 'bun:bundle'` to a shim build.onResolve({ filter: /^bun:bundle$/ }, () => ({ path: 'bun:bundle', @@ -78,6 +111,26 @@ const result = await Bun.build({ }), ) + build.onResolve( + { filter: /^\.\.\/(daemon\/workerRegistry|daemon\/main|cli\/bg|cli\/handlers\/templateJobs|environment-runner\/main|self-hosted-runner\/main)\.js$/ }, + args => { + if (!internalFeatureStubModules.has(args.path)) return null + return { + path: args.path, + namespace: 'internal-feature-stub', + } + }, + ) + build.onLoad( + { filter: /.*/, namespace: 'internal-feature-stub' }, + args => ({ + contents: + internalFeatureStubModules.get(args.path) ?? + 'export {}', + loader: 'js', + }), + ) + // Resolve react/compiler-runtime to the standalone package build.onResolve({ filter: /^react\/compiler-runtime$/ }, () => ({ path: 'react/compiler-runtime', From 9ee20cfd4ad936fc4fad44c77c6e7f4b1fb893d9 Mon Sep 17 00:00:00 2001 From: Kartvya69 Date: Wed, 1 Apr 2026 11:33:08 +0000 Subject: [PATCH 2/7] fix: preserve tui styles while fixing freeze --- src/ink/dom.ts | 7 ++++--- src/ink/reconciler.ts | 38 +++++++++++++++++++++++++++++--------- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/ink/dom.ts b/src/ink/dom.ts index f5b672ba..05c51c1f 100644 --- a/src/ink/dom.ts +++ b/src/ink/dom.ts @@ -265,13 +265,14 @@ export const setAttribute = ( markDirty(node) } -export const setStyle = (node: DOMNode, style: Styles): void => { +export const setStyle = (node: DOMNode, style: Styles | undefined): void => { + const nextStyle = style ?? {} // Compare style properties to avoid marking dirty unnecessarily. // React creates new style objects on every render even when unchanged. - if (stylesEqual(node.style, style)) { + if (stylesEqual(node.style, nextStyle)) { return } - node.style = style + node.style = nextStyle markDirty(node) } diff --git a/src/ink/reconciler.ts b/src/ink/reconciler.ts index c987b9fd..ba5e1395 100644 --- a/src/ink/reconciler.ts +++ b/src/ink/reconciler.ts @@ -59,6 +59,12 @@ $ npm install --save-dev react-devtools-core type AnyObject = Record +type UpdatePayload = { + props?: AnyObject + style?: AnyObject + nextStyle?: Styles | undefined +} + const diff = (before: AnyObject, after: AnyObject): AnyObject | undefined => { if (before === after) { return @@ -232,7 +238,7 @@ const reconciler = createReconciler< unknown, DOMElement, HostContext, - boolean, + UpdatePayload | null, NodeJS.Timeout, -1, null @@ -403,8 +409,19 @@ const reconciler = createReconciler< _type: ElementNames, oldProps: Props, newProps: Props, - ): boolean { - return oldProps !== newProps + ): UpdatePayload | null { + const props = diff(oldProps, newProps) + const style = diff(oldProps['style'] as Styles, newProps['style'] as Styles) + + if (!props && !style) { + return null + } + + return { + props, + style, + nextStyle: newProps['style'] as Styles | undefined, + } }, commitMount(node: DOMElement): void { getFocusManager(node).handleAutoFocus(node) @@ -432,13 +449,16 @@ const reconciler = createReconciler< }, commitUpdate( node: DOMElement, - _updatePayload: boolean, + updatePayload: UpdatePayload | null, _type: ElementNames, - oldProps: Props, - newProps: Props, + _oldProps: Props, + _newProps: Props, ): void { - const props = diff(oldProps, newProps) - const style = diff(oldProps['style'] as Styles, newProps['style'] as Styles) + if (!updatePayload) { + return + } + + const { props, style, nextStyle } = updatePayload if (props) { for (const [key, value] of Object.entries(props)) { @@ -462,7 +482,7 @@ const reconciler = createReconciler< } if (style && node.yogaNode) { - applyStyles(node.yogaNode, style, newProps['style'] as Styles) + applyStyles(node.yogaNode, style, nextStyle) } }, commitTextUpdate(node: TextNode, _oldText: string, newText: string): void { From 2ee43e010b708bdef5c9094df3d0981ffa51159a Mon Sep 17 00:00:00 2001 From: SenaxZz Date: Wed, 1 Apr 2026 13:53:36 +0200 Subject: [PATCH 3/7] Fix Windows ESM import by using file URL --- bin/openclaude | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bin/openclaude b/bin/openclaude index a5688523..2ecc153f 100755 --- a/bin/openclaude +++ b/bin/openclaude @@ -9,14 +9,13 @@ import { existsSync } from 'fs' import { join, dirname } from 'path' -import { fileURLToPath } from 'url' -import { getDistImportSpecifier } from './import-specifier.mjs' +import { fileURLToPath, pathToFileURL } from 'url' const __dirname = dirname(fileURLToPath(import.meta.url)) const distPath = join(__dirname, '..', 'dist', 'cli.mjs') if (existsSync(distPath)) { - await import(getDistImportSpecifier(__dirname)) + await import(pathToFileURL(distPath).href) } else { console.error(` openclaude: dist/cli.mjs not found. From a3d8ab0fec93e5176a695b9e61477ff169abeece Mon Sep 17 00:00:00 2001 From: gnanam1990 Date: Wed, 1 Apr 2026 14:46:04 +0530 Subject: [PATCH 4/7] feat: add native Gemini provider for Google AI models Adds Google Gemini as a first-class provider using Gemini's OpenAI-compatible endpoint, supporting gemini-2.0-flash, gemini-2.5-pro, and gemini-2.0-flash-lite across all three model tiers (opus/sonnet/haiku). - Add 'gemini' to APIProvider type with CLAUDE_CODE_USE_GEMINI env detection - Map all 11 model configs to appropriate Gemini models per tier - Route Gemini through existing OpenAI shim (generativelanguage.googleapis.com) - Support GEMINI_API_KEY and GOOGLE_API_KEY for authentication - Fix model display name to show actual Gemini model instead of Claude fallback - Add Gemini support to provider-launch, provider-bootstrap, system-check scripts - Add dev:gemini npm script for local development Bootstrap: bun run profile:init -- --provider gemini --api-key Launch: bun run dev:gemini Co-Authored-By: Claude Sonnet 4.6 --- package.json | 1 + scripts/provider-bootstrap.ts | 20 ++++++++++-- scripts/provider-launch.ts | 51 +++++++++++++++++++++++------- scripts/system-check.ts | 58 ++++++++++++++++++++++++++++++++-- src/services/api/openaiShim.ts | 16 ++++++++++ src/utils/model/configs.ts | 22 +++++++++++++ src/utils/model/model.ts | 26 +++++++++++++-- src/utils/model/providers.ts | 22 +++++++------ 8 files changed, 185 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index 0148fb06..48396c8f 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "dev:profile:fast": "bun run scripts/provider-launch.ts auto --fast --bare", "dev:codex": "bun run scripts/provider-launch.ts codex", "dev:openai": "bun run scripts/provider-launch.ts openai", + "dev:gemini": "bun run scripts/provider-launch.ts gemini", "dev:ollama": "bun run scripts/provider-launch.ts ollama", "dev:ollama:fast": "bun run scripts/provider-launch.ts ollama --fast --bare", "profile:init": "bun run scripts/provider-bootstrap.ts", diff --git a/scripts/provider-bootstrap.ts b/scripts/provider-bootstrap.ts index 34a407e2..7e5d1f66 100644 --- a/scripts/provider-bootstrap.ts +++ b/scripts/provider-bootstrap.ts @@ -6,7 +6,7 @@ import { resolveCodexApiCredentials, } from '../src/services/api/providerConfig.js' -type ProviderProfile = 'openai' | 'ollama' | 'codex' +type ProviderProfile = 'openai' | 'ollama' | 'codex' | 'gemini' type ProfileFile = { profile: ProviderProfile @@ -15,6 +15,9 @@ type ProfileFile = { OPENAI_MODEL?: string OPENAI_API_KEY?: string CODEX_API_KEY?: string + GEMINI_API_KEY?: string + GEMINI_MODEL?: string + GEMINI_BASE_URL?: string } createdAt: string } @@ -28,7 +31,7 @@ function parseArg(name: string): string | null { function parseProviderArg(): ProviderProfile | 'auto' { const p = parseArg('--provider')?.toLowerCase() - if (p === 'openai' || p === 'ollama' || p === 'codex') return p + if (p === 'openai' || p === 'ollama' || p === 'codex' || p === 'gemini') return p return 'auto' } @@ -69,7 +72,18 @@ async function main(): Promise { } const env: ProfileFile['env'] = {} - if (selected === 'ollama') { + + if (selected === 'gemini') { + env.GEMINI_MODEL = argModel || process.env.GEMINI_MODEL || 'gemini-2.0-flash' + const key = sanitizeApiKey(argApiKey || process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || null) + if (!key) { + console.error('Gemini profile requires an API key. Use --api-key or set GEMINI_API_KEY.') + console.error('Get a free key at: https://aistudio.google.com/apikey') + process.exit(1) + } + env.GEMINI_API_KEY = key + if (argBaseUrl) env.GEMINI_BASE_URL = argBaseUrl + } else if (selected === 'ollama') { env.OPENAI_BASE_URL = argBaseUrl || 'http://localhost:11434/v1' env.OPENAI_MODEL = argModel || process.env.OPENAI_MODEL || 'llama3.1:8b' const key = sanitizeApiKey(argApiKey || process.env.OPENAI_API_KEY || null) diff --git a/scripts/provider-launch.ts b/scripts/provider-launch.ts index 74f2431f..4594b63e 100644 --- a/scripts/provider-launch.ts +++ b/scripts/provider-launch.ts @@ -7,7 +7,7 @@ import { resolveCodexApiCredentials, } from '../src/services/api/providerConfig.js' -type ProviderProfile = 'openai' | 'ollama' | 'codex' +type ProviderProfile = 'openai' | 'ollama' | 'codex' | 'gemini' type ProfileFile = { profile: ProviderProfile @@ -16,6 +16,9 @@ type ProfileFile = { OPENAI_MODEL?: string OPENAI_API_KEY?: string CODEX_API_KEY?: string + GEMINI_API_KEY?: string + GEMINI_MODEL?: string + GEMINI_BASE_URL?: string } } @@ -37,7 +40,7 @@ function parseLaunchOptions(argv: string[]): LaunchOptions { continue } - if ((lower === 'auto' || lower === 'openai' || lower === 'ollama' || lower === 'codex') && requestedProfile === 'auto') { + if ((lower === 'auto' || lower === 'openai' || lower === 'ollama' || lower === 'codex' || lower === 'gemini') && requestedProfile === 'auto') { requestedProfile = lower as ProviderProfile | 'auto' continue } @@ -67,7 +70,7 @@ function loadPersistedProfile(): ProfileFile | null { if (!existsSync(path)) return null try { const parsed = JSON.parse(readFileSync(path, 'utf8')) as ProfileFile - if (parsed.profile === 'openai' || parsed.profile === 'ollama' || parsed.profile === 'codex') { + if (parsed.profile === 'openai' || parsed.profile === 'ollama' || parsed.profile === 'codex' || parsed.profile === 'gemini') { return parsed } return null @@ -106,6 +109,21 @@ function runCommand(command: string, env: NodeJS.ProcessEnv): Promise { function buildEnv(profile: ProviderProfile, persisted: ProfileFile | null): NodeJS.ProcessEnv { const persistedEnv = persisted?.env ?? {} + + if (profile === 'gemini') { + const env: NodeJS.ProcessEnv = { + ...process.env, + CLAUDE_CODE_USE_GEMINI: '1', + } + delete env.CLAUDE_CODE_USE_OPENAI + env.GEMINI_MODEL = process.env.GEMINI_MODEL || persistedEnv.GEMINI_MODEL || 'gemini-2.0-flash' + env.GEMINI_API_KEY = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || persistedEnv.GEMINI_API_KEY + if (persistedEnv.GEMINI_BASE_URL || process.env.GEMINI_BASE_URL) { + env.GEMINI_BASE_URL = process.env.GEMINI_BASE_URL || persistedEnv.GEMINI_BASE_URL + } + return env + } + const env: NodeJS.ProcessEnv = { ...process.env, CLAUDE_CODE_USE_OPENAI: '1', @@ -156,22 +174,26 @@ function quoteArg(arg: string): string { } function printSummary(profile: ProviderProfile, env: NodeJS.ProcessEnv): void { - const keySet = profile === 'codex' - ? Boolean(resolveCodexApiCredentials(env).apiKey) - : Boolean(env.OPENAI_API_KEY) console.log(`Launching profile: ${profile}`) - console.log(`OPENAI_BASE_URL=${env.OPENAI_BASE_URL}`) - console.log(`OPENAI_MODEL=${env.OPENAI_MODEL}`) - console.log( - `${profile === 'codex' ? 'CODEX_API_KEY_SET' : 'OPENAI_API_KEY_SET'}=${keySet}`, - ) + if (profile === 'gemini') { + console.log(`GEMINI_MODEL=${env.GEMINI_MODEL}`) + console.log(`GEMINI_API_KEY_SET=${Boolean(env.GEMINI_API_KEY)}`) + } else if (profile === 'codex') { + console.log(`OPENAI_BASE_URL=${env.OPENAI_BASE_URL}`) + console.log(`OPENAI_MODEL=${env.OPENAI_MODEL}`) + console.log(`CODEX_API_KEY_SET=${Boolean(resolveCodexApiCredentials(env).apiKey)}`) + } else { + console.log(`OPENAI_BASE_URL=${env.OPENAI_BASE_URL}`) + console.log(`OPENAI_MODEL=${env.OPENAI_MODEL}`) + console.log(`OPENAI_API_KEY_SET=${Boolean(env.OPENAI_API_KEY)}`) + } } async function main(): Promise { const options = parseLaunchOptions(process.argv.slice(2)) const requestedProfile = options.requestedProfile if (!requestedProfile) { - console.error('Usage: bun run scripts/provider-launch.ts [openai|ollama|codex|auto] [--fast] [-- ]') + console.error('Usage: bun run scripts/provider-launch.ts [openai|ollama|codex|gemini|auto] [--fast] [-- ]') process.exit(1) } @@ -193,6 +215,11 @@ async function main(): Promise { applyFastFlags(env) } + if (profile === 'gemini' && !env.GEMINI_API_KEY) { + console.error('GEMINI_API_KEY is required for gemini profile. Run: bun run profile:init -- --provider gemini --api-key ') + process.exit(1) + } + if (profile === 'openai' && (!env.OPENAI_API_KEY || env.OPENAI_API_KEY === 'SUA_CHAVE')) { console.error('OPENAI_API_KEY is required for openai profile and cannot be SUA_CHAVE. Run: bun run profile:init -- --provider openai --api-key ') process.exit(1) diff --git a/scripts/system-check.ts b/scripts/system-check.ts index 98022f6a..e129685a 100644 --- a/scripts/system-check.ts +++ b/scripts/system-check.ts @@ -92,14 +92,49 @@ function isLocalBaseUrl(baseUrl: string): boolean { return isProviderLocalUrl(baseUrl) } +const GEMINI_DEFAULT_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta/openai' + function currentBaseUrl(): string { + if (isTruthy(process.env.CLAUDE_CODE_USE_GEMINI)) { + return process.env.GEMINI_BASE_URL ?? GEMINI_DEFAULT_BASE_URL + } return process.env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1' } +function checkGeminiEnv(): CheckResult[] { + const results: CheckResult[] = [] + const model = process.env.GEMINI_MODEL + const key = process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY + const baseUrl = process.env.GEMINI_BASE_URL ?? GEMINI_DEFAULT_BASE_URL + + results.push(pass('Provider mode', 'Google Gemini provider enabled.')) + + if (!model) { + results.push(pass('GEMINI_MODEL', 'Not set. Default gemini-2.0-flash will be used.')) + } else { + results.push(pass('GEMINI_MODEL', model)) + } + + results.push(pass('GEMINI_BASE_URL', baseUrl)) + + if (!key) { + results.push(fail('GEMINI_API_KEY', 'Missing. Set GEMINI_API_KEY or GOOGLE_API_KEY.')) + } else { + results.push(pass('GEMINI_API_KEY', 'Configured.')) + } + + return results +} + function checkOpenAIEnv(): CheckResult[] { const results: CheckResult[] = [] + const useGemini = isTruthy(process.env.CLAUDE_CODE_USE_GEMINI) const useOpenAI = isTruthy(process.env.CLAUDE_CODE_USE_OPENAI) + if (useGemini) { + return checkGeminiEnv() + } + if (!useOpenAI) { results.push(pass('Provider mode', 'Anthropic login flow enabled (CLAUDE_CODE_USE_OPENAI is off).')) return results @@ -160,13 +195,20 @@ function checkOpenAIEnv(): CheckResult[] { } async function checkBaseUrlReachability(): Promise { - if (!isTruthy(process.env.CLAUDE_CODE_USE_OPENAI)) { + const useGemini = isTruthy(process.env.CLAUDE_CODE_USE_GEMINI) + const useOpenAI = isTruthy(process.env.CLAUDE_CODE_USE_OPENAI) + + if (!useGemini && !useOpenAI) { return pass('Provider reachability', 'Skipped (OpenAI-compatible mode disabled).') } + const geminiBaseUrl = 'https://generativelanguage.googleapis.com/v1beta/openai' + const resolvedBaseUrl = useGemini + ? (process.env.GEMINI_BASE_URL ?? geminiBaseUrl) + : undefined const request = resolveProviderRequest({ model: process.env.OPENAI_MODEL, - baseUrl: process.env.OPENAI_BASE_URL, + baseUrl: resolvedBaseUrl ?? process.env.OPENAI_BASE_URL, }) const endpoint = request.transport === 'codex_responses' ? `${request.baseUrl}/responses` @@ -203,6 +245,8 @@ async function checkBaseUrlReachability(): Promise { store: false, stream: true, }) + } else if (useGemini && (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}` } @@ -228,7 +272,7 @@ async function checkBaseUrlReachability(): Promise { } function checkOllamaProcessorMode(): CheckResult { - if (!isTruthy(process.env.CLAUDE_CODE_USE_OPENAI)) { + if (!isTruthy(process.env.CLAUDE_CODE_USE_OPENAI) || isTruthy(process.env.CLAUDE_CODE_USE_GEMINI)) { return pass('Ollama processor mode', 'Skipped (OpenAI-compatible mode disabled).') } @@ -267,6 +311,14 @@ function checkOllamaProcessorMode(): CheckResult { } function serializeSafeEnvSummary(): Record { + if (isTruthy(process.env.CLAUDE_CODE_USE_GEMINI)) { + return { + CLAUDE_CODE_USE_GEMINI: 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), + } + } const request = resolveProviderRequest({ model: process.env.OPENAI_MODEL, baseUrl: process.env.OPENAI_BASE_URL, diff --git a/src/services/api/openaiShim.ts b/src/services/api/openaiShim.ts index 07b38ba1..e49bdad3 100644 --- a/src/services/api/openaiShim.ts +++ b/src/services/api/openaiShim.ts @@ -741,6 +741,22 @@ export function createOpenAIShimClient(options: { maxRetries?: number timeout?: number }): unknown { + // When Gemini provider is active, map Gemini env vars to OpenAI-compatible ones + // so the existing providerConfig.ts infrastructure picks them up correctly. + if ( + process.env.CLAUDE_CODE_USE_GEMINI === '1' || + process.env.CLAUDE_CODE_USE_GEMINI === 'true' + ) { + process.env.OPENAI_BASE_URL ??= + process.env.GEMINI_BASE_URL ?? + 'https://generativelanguage.googleapis.com/v1beta/openai' + process.env.OPENAI_API_KEY ??= + process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY ?? '' + if (process.env.GEMINI_MODEL && !process.env.OPENAI_MODEL) { + process.env.OPENAI_MODEL = process.env.GEMINI_MODEL + } + } + const beta = new OpenAIShimBeta({ ...(options.defaultHeaders ?? {}), }) diff --git a/src/utils/model/configs.ts b/src/utils/model/configs.ts index 0ffd0b5f..3c5f1e79 100644 --- a/src/utils/model/configs.ts +++ b/src/utils/model/configs.ts @@ -14,6 +14,17 @@ export const OPENAI_MODEL_DEFAULTS = { haiku: 'gpt-4o-mini', // fast & cheap } as const +// --------------------------------------------------------------------------- +// Gemini model mappings +// Maps Claude model tiers to Google Gemini equivalents. +// Override with GEMINI_MODEL env var. +// --------------------------------------------------------------------------- +export const GEMINI_MODEL_DEFAULTS = { + opus: 'gemini-2.5-pro-preview-03-25', // most capable + sonnet: 'gemini-2.0-flash', // balanced + haiku: 'gemini-2.0-flash-lite', // fast & cheap +} as const + // @[MODEL LAUNCH]: Add a new CLAUDE_*_CONFIG constant here. Double check the correct model strings // here since the pattern may change. @@ -23,6 +34,7 @@ export const CLAUDE_3_7_SONNET_CONFIG = { vertex: 'claude-3-7-sonnet@20250219', foundry: 'claude-3-7-sonnet', openai: 'gpt-4o-mini', + gemini: 'gemini-2.0-flash', } as const satisfies ModelConfig export const CLAUDE_3_5_V2_SONNET_CONFIG = { @@ -31,6 +43,7 @@ export const CLAUDE_3_5_V2_SONNET_CONFIG = { vertex: 'claude-3-5-sonnet-v2@20241022', foundry: 'claude-3-5-sonnet', openai: 'gpt-4o-mini', + gemini: 'gemini-2.0-flash', } as const satisfies ModelConfig export const CLAUDE_3_5_HAIKU_CONFIG = { @@ -39,6 +52,7 @@ export const CLAUDE_3_5_HAIKU_CONFIG = { vertex: 'claude-3-5-haiku@20241022', foundry: 'claude-3-5-haiku', openai: 'gpt-4o-mini', + gemini: 'gemini-2.0-flash-lite', } as const satisfies ModelConfig export const CLAUDE_HAIKU_4_5_CONFIG = { @@ -47,6 +61,7 @@ export const CLAUDE_HAIKU_4_5_CONFIG = { vertex: 'claude-haiku-4-5@20251001', foundry: 'claude-haiku-4-5', openai: 'gpt-4o-mini', + gemini: 'gemini-2.0-flash-lite', } as const satisfies ModelConfig export const CLAUDE_SONNET_4_CONFIG = { @@ -55,6 +70,7 @@ export const CLAUDE_SONNET_4_CONFIG = { vertex: 'claude-sonnet-4@20250514', foundry: 'claude-sonnet-4', openai: 'gpt-4o-mini', + gemini: 'gemini-2.0-flash', } as const satisfies ModelConfig export const CLAUDE_SONNET_4_5_CONFIG = { @@ -63,6 +79,7 @@ export const CLAUDE_SONNET_4_5_CONFIG = { vertex: 'claude-sonnet-4-5@20250929', foundry: 'claude-sonnet-4-5', openai: 'gpt-4o', + gemini: 'gemini-2.0-flash', } as const satisfies ModelConfig export const CLAUDE_OPUS_4_CONFIG = { @@ -71,6 +88,7 @@ export const CLAUDE_OPUS_4_CONFIG = { vertex: 'claude-opus-4@20250514', foundry: 'claude-opus-4', openai: 'gpt-4o', + gemini: 'gemini-2.5-pro-preview-03-25', } as const satisfies ModelConfig export const CLAUDE_OPUS_4_1_CONFIG = { @@ -79,6 +97,7 @@ export const CLAUDE_OPUS_4_1_CONFIG = { vertex: 'claude-opus-4-1@20250805', foundry: 'claude-opus-4-1', openai: 'gpt-4o', + gemini: 'gemini-2.5-pro-preview-03-25', } as const satisfies ModelConfig export const CLAUDE_OPUS_4_5_CONFIG = { @@ -87,6 +106,7 @@ export const CLAUDE_OPUS_4_5_CONFIG = { vertex: 'claude-opus-4-5@20251101', foundry: 'claude-opus-4-5', openai: 'gpt-4o', + gemini: 'gemini-2.5-pro-preview-03-25', } as const satisfies ModelConfig export const CLAUDE_OPUS_4_6_CONFIG = { @@ -95,6 +115,7 @@ export const CLAUDE_OPUS_4_6_CONFIG = { vertex: 'claude-opus-4-6', foundry: 'claude-opus-4-6', openai: 'gpt-4o', + gemini: 'gemini-2.5-pro-preview-03-25', } as const satisfies ModelConfig export const CLAUDE_SONNET_4_6_CONFIG = { @@ -103,6 +124,7 @@ export const CLAUDE_SONNET_4_6_CONFIG = { vertex: 'claude-sonnet-4-6', foundry: 'claude-sonnet-4-6', openai: 'gpt-4o', + gemini: 'gemini-2.0-flash', } as const satisfies ModelConfig // @[MODEL LAUNCH]: Register the new config here. diff --git a/src/utils/model/model.ts b/src/utils/model/model.ts index 0cfcfece..6c81a8ef 100644 --- a/src/utils/model/model.ts +++ b/src/utils/model/model.ts @@ -35,6 +35,10 @@ export type ModelSetting = ModelName | ModelAlias | null export function getSmallFastModel(): ModelName { if (process.env.ANTHROPIC_SMALL_FAST_MODEL) return process.env.ANTHROPIC_SMALL_FAST_MODEL + // For Gemini provider, use a fast model + if (getAPIProvider() === 'gemini') { + return process.env.GEMINI_MODEL || 'gemini-2.0-flash-lite' + } // For OpenAI provider, use OPENAI_MODEL or a sensible default if (getAPIProvider() === 'openai') { return process.env.OPENAI_MODEL || 'gpt-4o-mini' @@ -71,7 +75,7 @@ export function getUserSpecifiedModelSetting(): ModelSetting | undefined { specifiedModel = modelOverride } else { const settings = getSettings_DEPRECATED() || {} - specifiedModel = process.env.ANTHROPIC_MODEL || process.env.OPENAI_MODEL || settings.model || undefined + specifiedModel = process.env.ANTHROPIC_MODEL || process.env.GEMINI_MODEL || process.env.OPENAI_MODEL || settings.model || undefined } // Ignore the user-specified model if it's not in the availableModels allowlist. @@ -111,6 +115,10 @@ export function getDefaultOpusModel(): ModelName { if (process.env.ANTHROPIC_DEFAULT_OPUS_MODEL) { return process.env.ANTHROPIC_DEFAULT_OPUS_MODEL } + // Gemini provider + if (getAPIProvider() === 'gemini') { + return process.env.GEMINI_MODEL || 'gemini-2.5-pro-preview-03-25' + } // OpenAI provider: use user-specified model or default if (getAPIProvider() === 'openai') { return process.env.OPENAI_MODEL || 'gpt-4o' @@ -129,6 +137,10 @@ export function getDefaultSonnetModel(): ModelName { if (process.env.ANTHROPIC_DEFAULT_SONNET_MODEL) { return process.env.ANTHROPIC_DEFAULT_SONNET_MODEL } + // Gemini provider + if (getAPIProvider() === 'gemini') { + return process.env.GEMINI_MODEL || 'gemini-2.0-flash' + } // OpenAI provider if (getAPIProvider() === 'openai') { return process.env.OPENAI_MODEL || 'gpt-4o' @@ -145,6 +157,10 @@ export function getDefaultHaikuModel(): ModelName { if (process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL) { return process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL } + // Gemini provider + if (getAPIProvider() === 'gemini') { + return process.env.GEMINI_MODEL || 'gemini-2.0-flash-lite' + } // OpenAI provider if (getAPIProvider() === 'openai') { return process.env.OPENAI_MODEL || 'gpt-4o-mini' @@ -193,6 +209,10 @@ export function getRuntimeMainLoopModel(params: { * @returns The default model setting to use */ export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias { + // Gemini provider: always use the configured Gemini model + if (getAPIProvider() === 'gemini') { + return process.env.GEMINI_MODEL || 'gemini-2.0-flash' + } // OpenAI provider: always use the configured OpenAI model if (getAPIProvider() === 'openai') { return process.env.OPENAI_MODEL || 'gpt-4o' @@ -381,8 +401,8 @@ export function renderModelSetting(setting: ModelName | ModelAlias): string { * if the model is not recognized as a public model. */ export function getPublicModelDisplayName(model: ModelName): string | null { - // For OpenAI provider, show the actual model name (e.g. 'gpt-4o') not a Claude alias - if (getAPIProvider() === 'openai') { + // For OpenAI/Gemini providers, show the actual model name not a Claude alias + if (getAPIProvider() === 'openai' || getAPIProvider() === 'gemini') { return null } switch (model) { diff --git a/src/utils/model/providers.ts b/src/utils/model/providers.ts index 48acd520..e0be1a34 100644 --- a/src/utils/model/providers.ts +++ b/src/utils/model/providers.ts @@ -1,18 +1,20 @@ import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics/index.js' import { isEnvTruthy } from '../envUtils.js' -export type APIProvider = 'firstParty' | 'bedrock' | 'vertex' | 'foundry' | 'openai' +export type APIProvider = 'firstParty' | 'bedrock' | 'vertex' | 'foundry' | 'openai' | 'gemini' export function getAPIProvider(): APIProvider { - return isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) - ? 'openai' - : isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) - ? 'bedrock' - : isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) - ? 'vertex' - : isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) - ? 'foundry' - : 'firstParty' + return isEnvTruthy(process.env.CLAUDE_CODE_USE_GEMINI) + ? 'gemini' + : isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI) + ? 'openai' + : isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) + ? 'bedrock' + : isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) + ? 'vertex' + : isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) + ? 'foundry' + : 'firstParty' } export function getAPIProviderForStatsig(): AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS { From e7c600de3b028899698611e60dfd146d93ce281e Mon Sep 17 00:00:00 2001 From: Kevin Codex Date: Wed, 1 Apr 2026 20:10:12 +0800 Subject: [PATCH 5/7] chore: release 0.1.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0148fb06..fe36f5c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gitlawb/openclaude", - "version": "0.1.2", + "version": "0.1.4", "description": "Claude Code opened to any LLM — OpenAI, Gemini, DeepSeek, Ollama, and 200+ models", "type": "module", "bin": { From 4ca94b2454c93056280c8a7bad7260e4d05bc603 Mon Sep 17 00:00:00 2001 From: gnanam1990 Date: Wed, 1 Apr 2026 17:42:04 +0530 Subject: [PATCH 6/7] feat: add context window guard for OpenAI-compatible models Without this fix, getContextWindowForModel() returns 200k for all OpenAI models (the Claude default), causing two problems: 1. Auto-compact/warnings trigger at wrong thresholds (200k instead of 128k) 2. getModelMaxOutputTokens() returns 32k causing 400 errors from APIs that cap output tokens lower (gpt-4o supports max 16384) Fix: - Add openaiContextWindows.ts with known context window sizes and max output token limits for 30+ OpenAI-compatible models (OpenAI, DeepSeek, Groq, Mistral, Ollama, LM Studio) - Hook into getContextWindowForModel() so correct input limits are used - Hook into getModelMaxOutputTokens() so correct output limits are sent, preventing 400 "max_tokens is too large" errors All existing warning, blocking, and auto-compact infrastructure works automatically once the correct limits are returned. Co-Authored-By: Claude Sonnet 4.6 --- src/utils/context.ts | 27 +++++ src/utils/model/openaiContextWindows.ts | 132 ++++++++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 src/utils/model/openaiContextWindows.ts diff --git a/src/utils/context.ts b/src/utils/context.ts index d9714de9..f13b2b0a 100644 --- a/src/utils/context.ts +++ b/src/utils/context.ts @@ -4,6 +4,7 @@ import { getGlobalConfig } from './config.js' import { isEnvTruthy } from './envUtils.js' import { getCanonicalName } from './model/model.js' import { getModelCapability } from './model/modelCapabilities.js' +import { getOpenAIContextWindow, getOpenAIMaxOutputTokens } from './model/openaiContextWindows.js' // Model context window size (200k tokens for all models right now) export const MODEL_CONTEXT_WINDOW_DEFAULT = 200_000 @@ -71,6 +72,19 @@ export function getContextWindowForModel( return 1_000_000 } + // OpenAI-compatible provider — use known context windows for the model + if ( + process.env.CLAUDE_CODE_USE_OPENAI === '1' || + process.env.CLAUDE_CODE_USE_OPENAI === 'true' || + process.env.CLAUDE_CODE_USE_GEMINI === '1' || + process.env.CLAUDE_CODE_USE_GEMINI === 'true' + ) { + const openaiWindow = getOpenAIContextWindow(model) + if (openaiWindow !== undefined) { + return openaiWindow + } + } + const cap = getModelCapability(model) if (cap?.max_input_tokens && cap.max_input_tokens >= 100_000) { if ( @@ -162,6 +176,19 @@ export function getModelMaxOutputTokens(model: string): { } } + // OpenAI-compatible provider — use known output limits to avoid 400 errors + if ( + process.env.CLAUDE_CODE_USE_OPENAI === '1' || + process.env.CLAUDE_CODE_USE_OPENAI === 'true' || + process.env.CLAUDE_CODE_USE_GEMINI === '1' || + process.env.CLAUDE_CODE_USE_GEMINI === 'true' + ) { + const openaiMax = getOpenAIMaxOutputTokens(model) + if (openaiMax !== undefined) { + return { default: openaiMax, upperLimit: openaiMax } + } + } + const m = getCanonicalName(model) if (m.includes('opus-4-6')) { diff --git a/src/utils/model/openaiContextWindows.ts b/src/utils/model/openaiContextWindows.ts new file mode 100644 index 00000000..fd6fb15a --- /dev/null +++ b/src/utils/model/openaiContextWindows.ts @@ -0,0 +1,132 @@ +/** + * openaiContextWindows.ts + * Context window sizes for OpenAI-compatible models used via the shim. + * Fixes: auto-compact and warnings using wrong 200k default for OpenAI models. + * + * When CLAUDE_CODE_USE_OPENAI=1, getContextWindowForModel() falls through to + * MODEL_CONTEXT_WINDOW_DEFAULT (200k). This causes the warning and blocking + * thresholds to be set at 200k even for models like gpt-4o (128k) or llama3 (8k), + * meaning users get no warning before hitting a hard API error. + * + * Prices in tokens as of April 2026 — update as needed. + */ + +const OPENAI_CONTEXT_WINDOWS: Record = { + // OpenAI + 'gpt-4o': 128_000, + 'gpt-4o-mini': 128_000, + 'gpt-4.1': 1_047_576, + 'gpt-4.1-mini': 1_047_576, + 'gpt-4.1-nano': 1_047_576, + 'gpt-4-turbo': 128_000, + 'gpt-4': 8_192, + 'o3-mini': 200_000, + 'o4-mini': 200_000, + 'o3': 200_000, + + // DeepSeek + 'deepseek-chat': 64_000, + 'deepseek-reasoner': 64_000, + + // Groq (fast inference) + 'llama-3.3-70b-versatile': 128_000, + 'llama-3.1-8b-instant': 128_000, + 'mixtral-8x7b-32768': 32_768, + + // Mistral + 'mistral-large-latest': 131_072, + 'mistral-small-latest': 131_072, + + // Google (via OpenRouter) + 'google/gemini-2.0-flash':1_048_576, + 'google/gemini-2.5-pro': 1_048_576, + + // Ollama local models + 'llama3.3:70b': 8_192, + 'llama3.1:8b': 8_192, + 'llama3.2:3b': 8_192, + 'qwen2.5-coder:32b': 32_768, + 'qwen2.5-coder:7b': 32_768, + 'deepseek-coder-v2:16b': 163_840, + 'deepseek-r1:14b': 65_536, + 'mistral:7b': 32_768, + 'phi4:14b': 16_384, + 'gemma2:27b': 8_192, + 'codellama:13b': 16_384, +} + +/** + * Max output (completion) tokens per model. + * This is separate from the context window (input limit). + * Fixes: 400 error "max_tokens is too large" when default 32k exceeds model limit. + */ +const OPENAI_MAX_OUTPUT_TOKENS: Record = { + // OpenAI + 'gpt-4o': 16_384, + 'gpt-4o-mini': 16_384, + 'gpt-4.1': 32_768, + 'gpt-4.1-mini': 32_768, + 'gpt-4.1-nano': 32_768, + 'gpt-4-turbo': 4_096, + 'gpt-4': 4_096, + 'o3-mini': 100_000, + 'o4-mini': 100_000, + 'o3': 100_000, + + // DeepSeek + 'deepseek-chat': 8_192, + 'deepseek-reasoner': 32_768, + + // Groq + 'llama-3.3-70b-versatile': 32_768, + 'llama-3.1-8b-instant': 8_192, + 'mixtral-8x7b-32768': 32_768, + + // Mistral + 'mistral-large-latest': 32_768, + 'mistral-small-latest': 32_768, + + // Google (via OpenRouter) + 'google/gemini-2.0-flash': 8_192, + 'google/gemini-2.5-pro': 32_768, + + // Ollama local models (conservative safe defaults) + 'llama3.3:70b': 4_096, + 'llama3.1:8b': 4_096, + 'llama3.2:3b': 4_096, + 'qwen2.5-coder:32b': 8_192, + 'qwen2.5-coder:7b': 8_192, + 'deepseek-coder-v2:16b': 8_192, + 'deepseek-r1:14b': 8_192, + 'mistral:7b': 4_096, + 'phi4:14b': 4_096, + 'gemma2:27b': 4_096, + 'codellama:13b': 4_096, +} + +function lookupByModel(table: Record, model: string): T | undefined { + if (table[model] !== undefined) return table[model] + for (const key of Object.keys(table)) { + if (model.startsWith(key)) return table[key] + } + return undefined +} + +/** + * Look up the context window for an OpenAI-compatible model. + * Returns undefined if the model is not in the table. + * + * Falls back to prefix matching so dated variants like + * "gpt-4o-2024-11-20" resolve to the base "gpt-4o" entry. + */ +export function getOpenAIContextWindow(model: string): number | undefined { + return lookupByModel(OPENAI_CONTEXT_WINDOWS, model) +} + +/** + * Look up the max output tokens for an OpenAI-compatible model. + * Returns undefined if the model is not in the table. + */ +export function getOpenAIMaxOutputTokens(model: string): number | undefined { + return lookupByModel(OPENAI_MAX_OUTPUT_TOKENS, model) +} From cb86f73c06f467eb2299d50726feb3560511076b Mon Sep 17 00:00:00 2001 From: gnanam1990 Date: Wed, 1 Apr 2026 18:14:41 +0530 Subject: [PATCH 7/7] fix: prevent duplicate responses in OpenAI streaming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When certain OpenAI-compatible APIs (LM Studio, some proxies) send multiple stream chunks with finish_reason set, the finish block ran multiple times — emitting content_block_stop and message_delta for each one. Each content_block_stop caused claude.ts to create and yield a new assistant message, making every response appear twice in the UI. Fix: add hasProcessedFinishReason flag (same pattern as the existing hasEmittedFinalUsage flag) so the finish block only executes once per response regardless of how many chunks contain finish_reason. Co-Authored-By: Claude Sonnet 4.6 --- src/services/api/openaiShim.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/services/api/openaiShim.ts b/src/services/api/openaiShim.ts index e49bdad3..f7a5f6b7 100644 --- a/src/services/api/openaiShim.ts +++ b/src/services/api/openaiShim.ts @@ -292,6 +292,7 @@ async function* openaiStreamToAnthropic( let hasEmittedContentStart = false let lastStopReason: 'tool_use' | 'max_tokens' | 'end_turn' | null = null let hasEmittedFinalUsage = false + let hasProcessedFinishReason = false // Emit message_start yield { @@ -422,8 +423,11 @@ async function* openaiStreamToAnthropic( } } - // Finish - if (choice.finish_reason) { + // Finish — guard ensures we only process finish_reason once even if + // multiple chunks arrive with finish_reason set (some providers do this) + if (choice.finish_reason && !hasProcessedFinishReason) { + hasProcessedFinishReason = true + // Close any open content blocks if (hasEmittedContentStart) { yield {