/** * Model fetcher — retrieves available models from each provider's API. * * Each function returns a sorted list of model IDs. * Network errors return empty arrays (non-fatal). */ export interface ModelInfo { id: string; name: string; provider: string; } const TIMEOUT = 8000; // Known Claude models as ultimate fallback const KNOWN_CLAUDE_MODELS: ModelInfo[] = [ { id: 'claude-opus-4-6', name: 'Claude Opus 4.6', provider: 'claude' }, { id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', provider: 'claude' }, { id: 'claude-opus-4-5-20251101', name: 'Claude Opus 4.5', provider: 'claude' }, { id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4', provider: 'claude' }, { id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5', provider: 'claude' }, ]; /** * Try to get an API key for Claude: * 1. ANTHROPIC_API_KEY env var (explicit) * 2. Claude CLI OAuth token from ~/.claude/.credentials.json (auto-detected) */ function getClaudeApiKey(): string | null { if (process.env.ANTHROPIC_API_KEY) return process.env.ANTHROPIC_API_KEY; try { const fs = require('fs'); const path = require('path'); const home = process.env.HOME || '/home'; const credPath = path.join(home, '.claude', '.credentials.json'); const data = JSON.parse(fs.readFileSync(credPath, 'utf8')); const token = data?.claudeAiOauth?.accessToken; if (token && typeof token === 'string' && token.startsWith('sk-ant-')) { // Check if token is expired const expiresAt = data.claudeAiOauth.expiresAt; if (expiresAt && Date.now() > expiresAt) return null; return token; } } catch { /* credentials file not found or unreadable */ } return null; } export async function fetchClaudeModels(): Promise { const apiKey = getClaudeApiKey(); if (!apiKey) return KNOWN_CLAUDE_MODELS; try { const res = await fetch('https://api.anthropic.com/v1/models?limit=100', { headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', }, signal: AbortSignal.timeout(TIMEOUT), }); if (!res.ok) return KNOWN_CLAUDE_MODELS; const data = await res.json() as { data?: Array<{ id: string; display_name?: string }> }; const live = (data.data ?? []) .map(m => ({ id: m.id, name: m.display_name ?? m.id, provider: 'claude' })) .sort((a, b) => b.id.localeCompare(a.id)); // newest first return live.length > 0 ? live : KNOWN_CLAUDE_MODELS; } catch { return KNOWN_CLAUDE_MODELS; } } export async function fetchCodexModels(): Promise { const apiKey = process.env.OPENAI_API_KEY; if (!apiKey) return []; try { const res = await fetch('https://api.openai.com/v1/models', { headers: { 'Authorization': `Bearer ${apiKey}` }, signal: AbortSignal.timeout(TIMEOUT), }); if (!res.ok) return []; const data = await res.json() as { data?: Array<{ id: string }> }; return (data.data ?? []) .map(m => ({ id: m.id, name: m.id, provider: 'codex' })) .sort((a, b) => a.id.localeCompare(b.id)); } catch { return []; } } export async function fetchOllamaModels(): Promise { try { const res = await fetch('http://localhost:11434/api/tags', { signal: AbortSignal.timeout(TIMEOUT), }); if (!res.ok) return []; const data = await res.json() as { models?: Array<{ name: string; model?: string }> }; return (data.models ?? []) .map(m => ({ id: m.name, name: m.name, provider: 'ollama' })) .sort((a, b) => a.id.localeCompare(b.id)); } catch { return []; } } export async function fetchGeminiModels(): Promise { const apiKey = process.env.GEMINI_API_KEY; if (!apiKey) return []; try { const res = await fetch( `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`, { signal: AbortSignal.timeout(TIMEOUT) }, ); if (!res.ok) return []; const data = await res.json() as { models?: Array<{ name: string; displayName?: string }>; }; return (data.models ?? []) .map(m => ({ id: m.name.replace('models/', ''), name: m.displayName ?? m.name, provider: 'gemini', })) .sort((a, b) => a.id.localeCompare(b.id)); } catch { return []; } } /** * Fetch models for a specific provider. */ export async function fetchModelsForProvider( provider: string, ): Promise { switch (provider) { case 'claude': return fetchClaudeModels(); case 'codex': return fetchCodexModels(); case 'ollama': return fetchOllamaModels(); case 'gemini': return fetchGeminiModels(); default: return []; } }