Download src/server/api/models.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 17.6 kB
-
https://huggingface.co/chenbhao/codev/resolve/main/src/server/api/models.ts
- Command line
-
hf download hf://chenbhao/codev/src/server/api/models.ts
-
curl -L -o models.ts https://huggingface.co/chenbhao/codev/resolve/main/src/server/api/models.ts
17.6 kB
| /** | |
| * Models REST API | |
| * | |
| * GET /api/models — 获取可用模型列表 | |
| * GET /api/models/current — 获取当前选中的模型 | |
| * PUT /api/models/current — 切换模型 | |
| * GET /api/effort — 获取 Effort 等级 | |
| * PUT /api/effort — 设置 Effort 等级 | |
| */ | |
| import { SettingsService } from '../services/settingsService.js' | |
| import { ProviderService } from '../services/providerService.js' | |
| import { attributionHeaderEnvForModel } from '../services/attributionHeaderPolicy.js' | |
| import { ApiError, errorResponse } from '../middleware/errorHandler.js' | |
| import { hasOpenAIAuthLogin } from '../../utils/auth.js' | |
| import { OPENAI_CODEX_MODEL_CATALOG } from '../../services/openaiAuth/models.js' | |
| import { | |
| OPENAI_OFFICIAL_PROVIDER_ID, | |
| OPENAI_OFFICIAL_PROVIDER_NAME, | |
| isOpenAIOfficialProviderId, | |
| } from '../services/openaiOfficialProvider.js' | |
| // ─── Fallback models (used when no provider is configured) ──────────────────── | |
| const DEFAULT_MODELS = [ | |
| { | |
| id: 'claude-opus-4-7', | |
| name: 'Opus 4.7', | |
| description: 'Most capable for ambitious work', | |
| context: '1m', | |
| }, | |
| { | |
| id: 'claude-sonnet-4-6', | |
| name: 'Sonnet 4.6', | |
| description: 'Most efficient for everyday tasks', | |
| context: '200k', | |
| }, | |
| { | |
| id: 'claude-haiku-4-5', | |
| name: 'Haiku 4.5', | |
| description: 'Fastest for quick answers', | |
| context: '200k', | |
| }, | |
| ] as const | |
| const EFFORT_LEVELS = ['low', 'medium', 'high', 'max'] as const | |
| const DEFAULT_MODEL = 'claude-opus-4-7' | |
| const DEFAULT_EFFORT = 'medium' | |
| const settingsService = new SettingsService() | |
| const providerService = new ProviderService() | |
| type ApiModelInfo = { | |
| id: string | |
| name: string | |
| description: string | |
| context: string | |
| } | |
| function addUniqueModel( | |
| models: ApiModelInfo[], | |
| model: ApiModelInfo | null, | |
| ): void { | |
| if (!model || !model.id.trim()) { | |
| return | |
| } | |
| if (models.some(existing => existing.id === model.id)) { | |
| return | |
| } | |
| models.push(model) | |
| } | |
| function buildProviderModelList(models: { | |
| main: string | |
| haiku: string | |
| sonnet: string | |
| opus: string | |
| }): ApiModelInfo[] { | |
| const modelList: ApiModelInfo[] = [] | |
| addUniqueModel(modelList, { | |
| id: models.main, | |
| name: models.main, | |
| description: 'Main model', | |
| context: '', | |
| }) | |
| addUniqueModel(modelList, models.haiku | |
| ? { | |
| id: models.haiku, | |
| name: models.haiku, | |
| description: 'Haiku model', | |
| context: '', | |
| } | |
| : null) | |
| addUniqueModel(modelList, models.sonnet | |
| ? { | |
| id: models.sonnet, | |
| name: models.sonnet, | |
| description: 'Sonnet model', | |
| context: '', | |
| } | |
| : null) | |
| addUniqueModel(modelList, models.opus | |
| ? { | |
| id: models.opus, | |
| name: models.opus, | |
| description: 'Opus model', | |
| context: '', | |
| } | |
| : null) | |
| return modelList | |
| } | |
| function buildOpenAIModelList(): ApiModelInfo[] { | |
| return OPENAI_CODEX_MODEL_CATALOG.map(model => ({ | |
| id: model.value, | |
| name: model.label, | |
| description: model.description, | |
| context: '', | |
| })) | |
| } | |
| function getEnvConfiguredAnthropicModels(): ApiModelInfo[] { | |
| return buildProviderModelList({ | |
| main: process.env.ANTHROPIC_MODEL?.trim() || '', | |
| haiku: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL?.trim() || '', | |
| sonnet: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL?.trim() || '', | |
| opus: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL?.trim() || '', | |
| }) | |
| } | |
| function getOpenAIAuthModels(): ApiModelInfo[] { | |
| if (!hasOpenAIAuthLogin()) { | |
| return [] | |
| } | |
| return buildOpenAIModelList() | |
| } | |
| function getStandaloneModelList(): ApiModelInfo[] { | |
| const models = [...getEnvConfiguredAnthropicModels()] | |
| if (models.length === 0) { | |
| models.push(...DEFAULT_MODELS) | |
| } | |
| for (const model of getOpenAIAuthModels()) { | |
| addUniqueModel(models, model) | |
| } | |
| return models | |
| } | |
| function normalizeEffortLevel(value: unknown): (typeof EFFORT_LEVELS)[number] { | |
| return typeof value === 'string' && EFFORT_LEVELS.includes(value as (typeof EFFORT_LEVELS)[number]) | |
| ? value as (typeof EFFORT_LEVELS)[number] | |
| : DEFAULT_EFFORT | |
| } | |
| // ─── CLI provider model cache (fetched from external APIs) ───── | |
| let cliProviderModelCache: ApiModelInfo[] | null = null | |
| let cliProviderModelCacheTime = 0 | |
| const CLI_PROVIDER_CACHE_TTL = 5 * 60 * 1000 // 5 minutes | |
| export function invalidateCliProviderModelCache(): void { | |
| cliProviderModelCache = null | |
| cliProviderModelCacheTime = 0 | |
| } | |
| async function fetchCliProviderModels(): Promise<ApiModelInfo[]> { | |
| if (cliProviderModelCache && Date.now() - cliProviderModelCacheTime < CLI_PROVIDER_CACHE_TTL) { | |
| return cliProviderModelCache | |
| } | |
| try { | |
| // Read ~/.claude.json directly — avoids importing CLI-side config modules | |
| const { homedir } = await import('node:os') | |
| const { readFileSync } = await import('node:fs') | |
| const { join } = await import('node:path') | |
| let config: Record<string, unknown> = {} | |
| try { | |
| const raw = readFileSync(join(homedir(), '.claude.json'), 'utf8') | |
| config = JSON.parse(raw) | |
| } catch { | |
| return [] | |
| } | |
| const authProvider = config.authProvider as string | undefined | |
| // ── OpenCode ────────────────────────────────────────────── | |
| if (authProvider === 'opencode' && config.openCodeApiKey) { | |
| try { | |
| const res = await fetch('https://models.dev/api.json') | |
| if (res.ok) { | |
| const data = await res.json() as any | |
| const opencodeModels = data?.opencode?.models || {} | |
| const models: ApiModelInfo[] = [] | |
| for (const [modelId, modelCfg] of Object.entries(opencodeModels) as [string, any][]) { | |
| if (modelCfg.status === 'deprecated') continue | |
| const isFree = modelCfg.cost?.input === 0 && modelCfg.cost?.output === 0 | |
| models.push({ | |
| id: modelId, | |
| name: modelCfg.name || modelId, | |
| description: isFree ? 'Free model' : 'Paid model', | |
| context: '', | |
| }) | |
| } | |
| if (models.length > 0) { | |
| cliProviderModelCache = models | |
| cliProviderModelCacheTime = Date.now() | |
| return models | |
| } | |
| } | |
| } catch { | |
| // fall through | |
| } | |
| } | |
| // ── OpenRouter ──────────────────────────────────────────── | |
| if (authProvider === 'openrouter' && config.openRouterApiKey) { | |
| try { | |
| const res = await fetch('https://openrouter.ai/api/v1/models', { | |
| headers: { Authorization: `Bearer ${config.openRouterApiKey}` }, | |
| }) | |
| if (res.ok) { | |
| const data = await res.json() as any | |
| const models: ApiModelInfo[] = (data.data || []).map((m: any) => ({ | |
| id: m.id, | |
| name: m.name || m.id, | |
| description: m.description || '', | |
| context: String(m.context_length || ''), | |
| })) | |
| if (models.length > 0) { | |
| cliProviderModelCache = models | |
| cliProviderModelCacheTime = Date.now() | |
| return models | |
| } | |
| } | |
| } catch { | |
| // fall through | |
| } | |
| } | |
| // ── NVIDIA NIM ──────────────────────────────────────────── | |
| if (authProvider === 'nvidia' && config.nvidiaApiKey) { | |
| try { | |
| const res = await fetch('https://integrate.api.nvidia.com/v1/models', { | |
| headers: { Authorization: `Bearer ${config.nvidiaApiKey}` }, | |
| }) | |
| if (res.ok) { | |
| const data = await res.json() as any | |
| const models: ApiModelInfo[] = (data.data || []).map((m: any) => ({ | |
| id: m.id, | |
| name: m.id, | |
| description: m.owned_by || '', | |
| context: '', | |
| })) | |
| if (models.length > 0) { | |
| cliProviderModelCache = models | |
| cliProviderModelCacheTime = Date.now() | |
| return models | |
| } | |
| } | |
| } catch { | |
| // fall through | |
| } | |
| } | |
| } catch { | |
| // fall through | |
| } | |
| return [] | |
| } | |
| // ─── Router ─────────────────────────────────────────────────────────────────── | |
| export async function handleModelsApi( | |
| req: Request, | |
| url: URL, | |
| segments: string[], | |
| ): Promise<Response> { | |
| try { | |
| const resource = segments[1] // 'models' | 'effort' | |
| const sub = segments[2] // 'current' | undefined | |
| // ── /api/effort ─────────────────────────────────────────────────── | |
| if (resource === 'effort') { | |
| return await handleEffort(req) | |
| } | |
| // ── /api/models/* ───────────────────────────────────────────────── | |
| switch (sub) { | |
| case undefined: | |
| // GET /api/models — 优先从激活的 Provider 读取模型列表 | |
| if (req.method !== 'GET') throw methodNotAllowed(req.method) | |
| return await handleModelsList() | |
| case 'current': | |
| return await handleCurrentModel(req) | |
| default: | |
| throw ApiError.notFound(`Unknown models endpoint: ${sub}`) | |
| } | |
| } catch (error) { | |
| return errorResponse(error) | |
| } | |
| } | |
| // ─── Handlers ───────────────────────────────────────────────────────────────── | |
| export async function readCliAuthProvider(): Promise<{ | |
| authProvider?: 'anthropic' | 'openai' | 'openrouter' | 'local' | 'opencode' | 'nvidia' | |
| } | null> { | |
| try { | |
| const { homedir } = await import('node:os') | |
| const { readFileSync } = await import('node:fs') | |
| const { join } = await import('node:path') | |
| const raw = readFileSync(join(homedir(), '.claude.json'), 'utf8') | |
| return JSON.parse(raw) | |
| } catch { | |
| return null | |
| } | |
| } | |
| const CLI_PROVIDER_NAMES: Record<string, string> = { | |
| opencode: 'OpenCode Zen', | |
| nvidia: 'NVIDIA', | |
| openrouter: 'OpenRouter', | |
| local: 'Local', | |
| anthropic: 'Anthropic', | |
| openai: 'OpenAI', | |
| } | |
| async function handleModelsList(): Promise<Response> { | |
| const cliConfig = await readCliAuthProvider() | |
| const cliAuthProvider = cliConfig?.authProvider | |
| const { providers, activeId } = await providerService.listProviders() | |
| if (activeId) { | |
| const activeProvider = providers.find((p) => p.id === activeId) | |
| if (activeProvider) { | |
| const presetType = activeProvider.presetId?.replace(/^tui-/, '') | |
| if (presetType && cliAuthProvider === presetType) { | |
| const cliModels = await fetchCliProviderModels() | |
| if (cliModels.length > 0) { | |
| return Response.json({ | |
| models: cliModels, | |
| provider: { id: activeProvider.id, name: activeProvider.name }, | |
| }) | |
| } | |
| } | |
| if (isOpenAIOfficialProviderId(activeId)) { | |
| return Response.json({ | |
| models: buildOpenAIModelList(), | |
| provider: { id: OPENAI_OFFICIAL_PROVIDER_ID, name: OPENAI_OFFICIAL_PROVIDER_NAME }, | |
| }) | |
| } | |
| return Response.json({ | |
| models: buildProviderModelList(activeProvider.models), | |
| provider: { id: activeProvider.id, name: activeProvider.name }, | |
| }) | |
| } | |
| } | |
| if (cliAuthProvider && cliAuthProvider !== 'anthropic' && cliAuthProvider !== 'openai') { | |
| const cliModels = await fetchCliProviderModels() | |
| return Response.json({ | |
| models: cliModels, | |
| provider: { | |
| id: `cli-${cliAuthProvider}`, | |
| name: CLI_PROVIDER_NAMES[cliAuthProvider] || cliAuthProvider, | |
| }, | |
| }) | |
| } | |
| return Response.json({ models: getStandaloneModelList(), provider: null }) | |
| } | |
| async function handleCurrentModel(req: Request): Promise<Response> { | |
| if (req.method === 'GET') { | |
| // Build the full model list: prefer active provider's models, fall back to defaults | |
| const { providers, activeId } = await providerService.listProviders() | |
| const isOpenAIProviderActive = isOpenAIOfficialProviderId(activeId) | |
| const activeProvider = activeId ? providers.find((p) => p.id === activeId) : null | |
| const settings = activeProvider || isOpenAIProviderActive | |
| ? await providerService.getManagedSettings() | |
| : await settingsService.getUserSettings() | |
| const explicitModel = (settings.model as string) || '' | |
| const contextTier = (settings.modelContext as string) || undefined | |
| const env = (settings.env as Record<string, string>) || {} | |
| const envModel = process.env.ANTHROPIC_MODEL?.trim() || '' | |
| let currentModelId: string | |
| let currentModelName: string | |
| if (isOpenAIProviderActive) { | |
| currentModelId = explicitModel || env.ANTHROPIC_MODEL || 'gpt-5.3-codex' | |
| currentModelName = currentModelId | |
| } else if (activeProvider) { | |
| // Provider is active — only use the provider-managed cc-haha settings. | |
| // This avoids leaking global ~/.claude/settings.json model choices into | |
| // the active provider flow. | |
| const providerEnvModel = env.ANTHROPIC_MODEL | |
| if (providerEnvModel && !explicitModel) { | |
| currentModelId = providerEnvModel | |
| currentModelName = providerEnvModel | |
| } else { | |
| currentModelId = explicitModel || providerEnvModel || activeProvider.models.main | |
| currentModelName = currentModelId | |
| } | |
| } else { | |
| // No provider — use settings model with context tier | |
| currentModelId = explicitModel || envModel || DEFAULT_MODEL | |
| currentModelName = currentModelId | |
| } | |
| const lookupId = contextTier ? `${currentModelId}:${contextTier}` : currentModelId | |
| // Build available models for name lookup | |
| const cliModelsFallback = !isOpenAIProviderActive && !activeProvider | |
| ? await fetchCliProviderModels() | |
| : [] | |
| const availableModels = isOpenAIProviderActive | |
| ? buildOpenAIModelList() | |
| : activeProvider | |
| ? buildProviderModelList(activeProvider.models) | |
| : cliModelsFallback.length > 0 | |
| ? cliModelsFallback | |
| : getStandaloneModelList() | |
| const modelEntry = availableModels.find((m) => m.id === lookupId) | |
| || availableModels.find((m) => m.id === currentModelId) | |
| || { | |
| id: currentModelId, | |
| name: currentModelName, | |
| description: 'Custom model', | |
| context: contextTier || 'unknown', | |
| } | |
| return Response.json({ model: { ...modelEntry, context: contextTier || modelEntry.context } }) | |
| } | |
| if (req.method === 'PUT') { | |
| const body = await parseJsonBody(req) | |
| const modelId = body.modelId | |
| if (typeof modelId !== 'string' || !modelId) { | |
| throw ApiError.badRequest('Missing or invalid "modelId" in request body') | |
| } | |
| // Parse composite IDs like 'claude-opus-4-7-20250610:1m' | |
| // Persist the base model ID for CLI compatibility and context tier separately | |
| const colonIdx = modelId.indexOf(':') | |
| const baseId = colonIdx !== -1 ? modelId.slice(0, colonIdx) : modelId | |
| const contextTier = colonIdx !== -1 ? modelId.slice(colonIdx + 1) : undefined | |
| const updates: Record<string, unknown> = { model: baseId } | |
| if (contextTier) { | |
| updates.modelContext = contextTier | |
| } else { | |
| // Clear context tier when switching to a non-composite model | |
| updates.modelContext = undefined | |
| } | |
| const { activeId } = await providerService.listProviders() | |
| if (activeId) { | |
| const currentManagedSettings = await providerService.getManagedSettings() | |
| const currentEnv = | |
| (currentManagedSettings.env as Record<string, string> | undefined) ?? {} | |
| await providerService.updateManagedSettings({ | |
| ...updates, | |
| env: { | |
| ...currentEnv, | |
| ...attributionHeaderEnvForModel(baseId), | |
| }, | |
| }) | |
| } else { | |
| await settingsService.updateUserSettings(updates) | |
| } | |
| return Response.json({ ok: true, model: modelId }) | |
| } | |
| throw methodNotAllowed(req.method) | |
| } | |
| async function handleEffort(req: Request): Promise<Response> { | |
| if (req.method === 'GET') { | |
| const settings = await settingsService.getUserSettings() | |
| const level = normalizeEffortLevel(settings.effort) | |
| return Response.json({ level, available: EFFORT_LEVELS }) | |
| } | |
| if (req.method === 'PUT') { | |
| const body = await parseJsonBody(req) | |
| const level = body.level | |
| if (typeof level !== 'string') { | |
| throw ApiError.badRequest('Missing or invalid "level" in request body') | |
| } | |
| if (!EFFORT_LEVELS.includes(level as (typeof EFFORT_LEVELS)[number])) { | |
| throw ApiError.badRequest( | |
| `Invalid effort level: "${level}". Valid levels: ${EFFORT_LEVELS.join(', ')}`, | |
| ) | |
| } | |
| await settingsService.updateUserSettings({ effort: level }) | |
| return Response.json({ ok: true, level }) | |
| } | |
| throw methodNotAllowed(req.method) | |
| } | |
| // ─── Helpers ────────────────────────────────────────────────────────────────── | |
| async function parseJsonBody(req: Request): Promise<Record<string, unknown>> { | |
| try { | |
| return (await req.json()) as Record<string, unknown> | |
| } catch { | |
| throw ApiError.badRequest('Invalid JSON body') | |
| } | |
| } | |
| function methodNotAllowed(method: string): ApiError { | |
| return new ApiError(405, `Method ${method} not allowed`, 'METHOD_NOT_ALLOWED') | |
| } | |