( hw: HardwareProfile, detected: DetectedModel[], apiModels: ApiCatalogModel[] = [], )
| 441 | } |
| 442 | |
| 443 | export function buildModelChoices( |
| 444 | hw: HardwareProfile, |
| 445 | detected: DetectedModel[], |
| 446 | apiModels: ApiCatalogModel[] = [], |
| 447 | ): Array<{ value: string; label: string; hint?: string }> { |
| 448 | const choices: Array<{ value: string; label: string; hint?: string }> = []; |
| 449 | const seen = new Set<string>(); |
| 450 | |
| 451 | // 1. Detected — these are guaranteed working |
| 452 | for (const m of detected) { |
| 453 | if (seen.has(m.id)) continue; |
| 454 | seen.add(m.id); |
| 455 | const f = formatModel(m); |
| 456 | choices.push({ value: m.id, label: f.label, hint: `installed · ${f.hint}` }); |
| 457 | } |
| 458 | |
| 459 | // 1b. Models from configured custom APIs (provider/id form so routing is unambiguous). |
| 460 | for (const m of apiModels) { |
| 461 | const value = `${m.provider}/${m.id}`; |
| 462 | if (seen.has(value)) continue; |
| 463 | seen.add(value); |
| 464 | const ctx = m.contextWindow ? ` · ${Math.round(m.contextWindow / 1000)}k ctx` : ''; |
| 465 | choices.push({ value, label: value, hint: `API · ${m.provider}${ctx}` }); |
| 466 | } |
| 467 | |
| 468 | // 2. Hardware-tier recommendations — not yet installed, will need a pull |
| 469 | for (const m of hw.recommendedModels) { |
| 470 | if (seen.has(m)) continue; |
| 471 | seen.add(m); |
| 472 | const fitNote = isModelComfortableForHardware(m, hw) ? '✓ comfortable' : 'tight — may swap'; |
| 473 | choices.push({ |
| 474 | value: m, |
| 475 | label: `${m}`, |
| 476 | hint: `requires \`ollama pull\` · ${fitNote}`, |
| 477 | }); |
| 478 | } |
| 479 | |
| 480 | // 3. Cloud options — always listed last |
| 481 | choices.push({ value: 'claude-sonnet-4-6', label: 'claude-sonnet-4-6', hint: 'cloud, needs ANTHROPIC_API_KEY' }); |
| 482 | choices.push({ value: 'gpt-4o-mini', label: 'gpt-4o-mini', hint: 'cloud, needs OPENAI_API_KEY' }); |
| 483 | choices.push({ value: 'deepseek-coder', label: 'deepseek-coder', hint: 'cloud, needs DEEPSEEK_API_KEY' }); |
| 484 | return choices; |
| 485 | } |
| 486 | |
| 487 | /** Whether a given model id is comfortable on this hardware. Heuristic. */ |
| 488 | function isModelComfortableForHardware(modelId: string, hw: HardwareProfile): boolean { |
no test coverage detected