File size: 7,535 Bytes
bfdb3c0 e40073c bfdb3c0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 | /**
* Direct provider model fetcher.
*
* Fetches available models directly from provider APIs, mirroring the logic
* in TUI's openRouterModels.ts / nvidiaClient.ts / opencodeClient.ts.
*
* This bypasses the cc-haha sidecar's /api/models endpoint entirely,
* reusing TUI's model acquisition approach in the desktop WebView.
*/
import { getTuiConfig } from './config'
import type { ModelInfo } from '../types/settings'
// βββ Type ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
type FetchResult = {
models: ModelInfo[]
provider: { id: string; name: string } | null
}
// βββ Cache βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
let modelCache: { key: string; models: ModelInfo[] } | null = null
let cacheTime = 0
const CACHE_TTL = 5 * 60 * 1000 // 5 minutes
export function clearProviderModelCache(): void {
modelCache = null
cacheTime = 0
orCache = null
orCacheTime = 0
nvCache = null
nvCacheTime = 0
}
// βββ Main entry ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export async function fetchProviderModels(): Promise<FetchResult> {
const config = await getTuiConfig()
const authProvider = (config.authProvider as string | undefined) || null
const cacheKey = authProvider ?? '__default__'
if (modelCache && modelCache.key === cacheKey && Date.now() - cacheTime < CACHE_TTL) {
return { models: modelCache.models, provider: null }
}
let models: ModelInfo[] = []
let providerInfo: { id: string; name: string } | null = null
switch (authProvider) {
case 'openrouter': {
const apiKey = config.openRouterApiKey as string | undefined
models = await fetchOpenRouterModels(apiKey)
providerInfo = models.length > 0
? { id: 'cli-openrouter', name: 'OpenRouter' }
: null
break
}
case 'nvidia': {
const apiKey = config.nvidiaApiKey as string | undefined
const baseUrl = config.nvidiaBaseUrl as string | undefined
models = await fetchNvidiaModels(apiKey, baseUrl || 'https://integrate.api.nvidia.com/v1')
providerInfo = models.length > 0
? { id: 'cli-nvidia', name: 'NVIDIA' }
: null
break
}
case 'opencode': {
models = await fetchOpencodeModels()
providerInfo = models.length > 0
? { id: 'cli-opencode', name: 'OpenCode Zen' }
: null
break
}
case 'openai':
// OpenAI models are best fetched via the official provider API.
// For now, the static catalog from modelCatalog.ts is used.
break
case 'local': {
const modelName = config.localModelName as string | undefined
if (modelName) {
models = [{ id: modelName, name: modelName, description: 'Local model', context: '' }]
}
providerInfo = { id: 'cli-local', name: 'Local' }
break
}
default:
// firstParty / anthropic β no external fetch, use static defaults
break
}
modelCache = { key: cacheKey, models }
cacheTime = Date.now()
return { models, provider: providerInfo }
}
// βββ OpenRouter ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
let orCache: ModelInfo[] | null = null
let orCacheTime = 0
const OR_CACHE_TTL = 5 * 60 * 1000
async function fetchOpenRouterModels(apiKey?: string | null): Promise<ModelInfo[]> {
if (orCache && Date.now() - orCacheTime < OR_CACHE_TTL) {
return orCache
}
if (!apiKey) {
orCache = []
orCacheTime = Date.now()
return []
}
try {
const res = await fetch('https://openrouter.ai/api/v1/models', {
headers: { Authorization: `Bearer ${apiKey}` },
signal: AbortSignal.timeout(20_000),
})
if (!res.ok) {
orCache = []
return []
}
const data = (await res.json()) as { data?: Array<{ id: string; name: string; description?: string; context_length?: number }> }
const models: ModelInfo[] = (data.data || [])
.filter((m) => {
const id = m.id.toLowerCase()
return id.includes('/') && !id.startsWith('router') && !id.startsWith('free') && !id.startsWith('aggregat')
})
.map((m) => ({
id: m.id,
name: m.name || m.id,
description: (m.description || '').length > 100 ? m.description!.slice(0, 97) + '...' : (m.description || ''),
context: String(m.context_length || ''),
}))
orCache = models
orCacheTime = Date.now()
return models
} catch {
orCache = orCache || []
return orCache
}
}
// βββ NVIDIA NIM ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
let nvCache: ModelInfo[] | null = null
let nvCacheTime = 0
async function fetchNvidiaModels(apiKey?: string | null, baseUrl?: string): Promise<ModelInfo[]> {
if (nvCache && Date.now() - nvCacheTime < OR_CACHE_TTL) {
return nvCache
}
if (!apiKey) {
nvCache = []
nvCacheTime = Date.now()
return []
}
const normalized = (baseUrl || 'https://integrate.api.nvidia.com/v1').replace(/\/$/, '')
const modelsUrl = normalized.endsWith('/v1') ? `${normalized}/models` : `${normalized}/v1/models`
try {
const res = await fetch(modelsUrl, {
headers: { Authorization: `Bearer ${apiKey}` },
signal: AbortSignal.timeout(20_000),
})
if (!res.ok) {
nvCache = []
return []
}
const json = (await res.json()) as { data?: Array<{ id: string }> }
if (json.data && Array.isArray(json.data)) {
const models: ModelInfo[] = json.data.map((m) => ({
id: m.id,
name: m.id,
description: 'NVIDIA NIM model',
context: '',
}))
nvCache = models
nvCacheTime = Date.now()
return models
}
nvCache = []
return []
} catch {
nvCache = nvCache || []
return nvCache
}
}
// βββ OpenCode Zen ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export async function fetchOpencodeModels(): Promise<ModelInfo[]> {
try {
const res = await fetch('https://models.dev/api.json', {
signal: AbortSignal.timeout(15_000),
})
if (!res.ok) return []
const data = (await res.json()) as {
opencode?: { models?: Record<string, { name?: string; status?: string; cost?: { input?: number; output?: number } }> }
}
const opencodeModels = data?.opencode?.models || {}
const models: ModelInfo[] = []
for (const [modelId, cfg] of Object.entries(opencodeModels)) {
if (cfg.status === 'deprecated') continue
const isFree = cfg.cost?.input === 0 && cfg.cost?.output === 0
models.push({
id: modelId,
name: cfg.name || modelId,
description: isFree ? 'Free model' : 'Paid model',
context: '',
})
}
return models
} catch {
return []
}
}
|