Merge origin/main into provider-profile-recommendations
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 ?? {}),
|
||||
})
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
132
src/utils/model/openaiContextWindows.ts
Normal file
132
src/utils/model/openaiContextWindows.ts
Normal 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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user