Merge origin/main into provider-profile-recommendations

This commit is contained in:
Vasanthdev2004
2026-04-01 18:38:59 +05:30
16 changed files with 581 additions and 49 deletions

View File

@@ -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)
}

View File

@@ -59,6 +59,12 @@ $ npm install --save-dev react-devtools-core
type AnyObject = Record<string, unknown>
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 {

View File

@@ -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 {
@@ -741,6 +745,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 ?? {}),
})

View File

@@ -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')) {

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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<string, number> = {
// 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<string, number> = {
// 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<T>(table: Record<string, T>, 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)
}

View File

@@ -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 {

View File

@@ -6,6 +6,7 @@ import test from 'node:test'
import {
buildCodexProfileEnv,
buildGeminiProfileEnv,
buildLaunchEnv,
buildOllamaProfileEnv,
buildOpenAIProfileEnv,
@@ -127,6 +128,60 @@ test('openai launch ignores codex persisted transport hints', async () => {
assert.equal(env.OPENAI_API_KEY, 'sk-live')
})
test('matching persisted gemini env is reused for gemini launch', async () => {
const env = await buildLaunchEnv({
profile: 'gemini',
persisted: profile('gemini', {
GEMINI_MODEL: 'gemini-2.5-flash',
GEMINI_API_KEY: 'gem-persisted',
GEMINI_BASE_URL: 'https://example.test/v1beta/openai',
}),
goal: 'balanced',
processEnv: {},
})
assert.equal(env.CLAUDE_CODE_USE_GEMINI, '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')
assert.equal(env.GEMINI_BASE_URL, 'https://example.test/v1beta/openai')
})
test('gemini launch ignores mismatched persisted openai env and strips other provider secrets', async () => {
const env = await buildLaunchEnv({
profile: 'gemini',
persisted: profile('openai', {
OPENAI_BASE_URL: 'https://api.openai.com/v1',
OPENAI_MODEL: 'gpt-4o',
OPENAI_API_KEY: 'sk-persisted',
}),
goal: 'balanced',
processEnv: {
GEMINI_API_KEY: 'gem-live',
GOOGLE_API_KEY: 'google-live',
OPENAI_API_KEY: 'sk-live',
OPENAI_BASE_URL: 'https://api.openai.com/v1',
OPENAI_MODEL: 'gpt-4o-mini',
CODEX_API_KEY: 'codex-live',
CHATGPT_ACCOUNT_ID: 'acct_live',
CLAUDE_CODE_USE_OPENAI: '1',
},
})
assert.equal(env.CLAUDE_CODE_USE_GEMINI, '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')
assert.equal(
env.GEMINI_BASE_URL,
'https://generativelanguage.googleapis.com/v1beta/openai',
)
assert.equal(env.GOOGLE_API_KEY, undefined)
assert.equal(env.OPENAI_API_KEY, undefined)
assert.equal(env.CODEX_API_KEY, undefined)
assert.equal(env.CHATGPT_ACCOUNT_ID, undefined)
})
test('matching persisted codex env is reused for codex launch', async () => {
const env = await buildLaunchEnv({
profile: 'codex',
@@ -283,6 +338,27 @@ test('codex profiles require a chatgpt account id', () => {
assert.equal(env, null)
})
test('gemini profiles accept google api key fallback', () => {
const env = buildGeminiProfileEnv({
processEnv: {
GOOGLE_API_KEY: 'gem-live',
},
})
assert.deepEqual(env, {
GEMINI_MODEL: 'gemini-2.0-flash',
GEMINI_API_KEY: 'gem-live',
})
})
test('gemini profiles require a key', () => {
const env = buildGeminiProfileEnv({
processEnv: {},
})
assert.equal(env, null)
})
test('openai profiles ignore codex shell transport hints', () => {
const env = buildOpenAIProfileEnv({
goal: 'balanced',

View File

@@ -10,7 +10,10 @@ import {
type RecommendationGoal,
} from './providerRecommendation.ts'
export type ProviderProfile = 'openai' | 'ollama' | 'codex'
const DEFAULT_GEMINI_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta/openai'
const DEFAULT_GEMINI_MODEL = 'gemini-2.0-flash'
export type ProviderProfile = 'openai' | 'ollama' | 'codex' | 'gemini'
export type ProfileEnv = {
OPENAI_BASE_URL?: string
@@ -19,6 +22,9 @@ export type ProfileEnv = {
CODEX_API_KEY?: string
CHATGPT_ACCOUNT_ID?: string
CODEX_ACCOUNT_ID?: string
GEMINI_API_KEY?: string
GEMINI_MODEL?: string
GEMINI_BASE_URL?: string
}
export type ProfileFile = {
@@ -47,6 +53,36 @@ export function buildOllamaProfileEnv(
}
}
export function buildGeminiProfileEnv(options: {
model?: string | null
baseUrl?: string | null
apiKey?: string | null
processEnv?: NodeJS.ProcessEnv
}): ProfileEnv | null {
const processEnv = options.processEnv ?? process.env
const key = sanitizeApiKey(
options.apiKey ??
processEnv.GEMINI_API_KEY ??
processEnv.GOOGLE_API_KEY,
)
if (!key) {
return null
}
const env: ProfileEnv = {
GEMINI_MODEL:
options.model || processEnv.GEMINI_MODEL || DEFAULT_GEMINI_MODEL,
GEMINI_API_KEY: key,
}
const baseUrl = options.baseUrl || processEnv.GEMINI_BASE_URL
if (baseUrl) {
env.GEMINI_BASE_URL = baseUrl
}
return env
}
export function buildOpenAIProfileEnv(options: {
goal: RecommendationGoal
model?: string | null
@@ -142,11 +178,57 @@ export async function buildLaunchEnv(options: {
? options.persisted.env ?? {}
: {}
const shellGeminiKey = sanitizeApiKey(
processEnv.GEMINI_API_KEY ?? processEnv.GOOGLE_API_KEY,
)
const persistedGeminiKey = sanitizeApiKey(persistedEnv.GEMINI_API_KEY)
if (options.profile === 'gemini') {
const env: NodeJS.ProcessEnv = {
...processEnv,
CLAUDE_CODE_USE_GEMINI: '1',
}
delete env.CLAUDE_CODE_USE_OPENAI
env.GEMINI_MODEL =
processEnv.GEMINI_MODEL ||
persistedEnv.GEMINI_MODEL ||
DEFAULT_GEMINI_MODEL
env.GEMINI_BASE_URL =
processEnv.GEMINI_BASE_URL ||
persistedEnv.GEMINI_BASE_URL ||
DEFAULT_GEMINI_BASE_URL
const geminiKey = shellGeminiKey || persistedGeminiKey
if (geminiKey) {
env.GEMINI_API_KEY = geminiKey
} else {
delete env.GEMINI_API_KEY
}
delete env.GOOGLE_API_KEY
delete env.OPENAI_BASE_URL
delete env.OPENAI_MODEL
delete env.OPENAI_API_KEY
delete env.CODEX_API_KEY
delete env.CHATGPT_ACCOUNT_ID
delete env.CODEX_ACCOUNT_ID
return env
}
const env: NodeJS.ProcessEnv = {
...processEnv,
CLAUDE_CODE_USE_OPENAI: '1',
}
delete env.CLAUDE_CODE_USE_GEMINI
delete env.GEMINI_API_KEY
delete env.GEMINI_MODEL
delete env.GEMINI_BASE_URL
delete env.GOOGLE_API_KEY
if (options.profile === 'ollama') {
const getOllamaBaseUrl =
options.getOllamaChatBaseUrl ?? (() => 'http://localhost:11434/v1')