* Build the sub-agent model choices when the user picks "lighter local". * * Logic: offer models SMALLER than what they picked for the parent. If the parent is * already small (7B), we still offer the same so they can change their mind. * The hardware tier limits us: even on a Mac Studio, no poi
( hw: HardwareProfile, parentModel: string, detected: DetectedModel[] = [], )
| 509 | * than the parent — that wouldn't be "lighter". |
| 510 | */ |
| 511 | function buildLightLocalChoices( |
| 512 | hw: HardwareProfile, |
| 513 | parentModel: string, |
| 514 | detected: DetectedModel[] = [], |
| 515 | ): Array<{ value: string; label: string; hint?: string }> { |
| 516 | const parentSize = approxModelSizeB(parentModel); |
| 517 | const choices: Array<{ value: string; label: string; hint?: string }> = []; |
| 518 | const seen = new Set<string>(); |
| 519 | |
| 520 | // First: any detected models that aren't the parent. |
| 521 | // Order them by toolCallsLikely then size (closer to parent*0.7 is ideal). |
| 522 | const detectedCandidates = detected |
| 523 | .filter(m => m.id !== parentModel) |
| 524 | .sort((a, b) => { |
| 525 | const aTC = a.toolCallsLikely ? 1 : 0; |
| 526 | const bTC = b.toolCallsLikely ? 1 : 0; |
| 527 | if (aTC !== bTC) return bTC - aTC; |
| 528 | return (b.paramsB ?? 0) - (a.paramsB ?? 0); |
| 529 | }); |
| 530 | for (const m of detectedCandidates) { |
| 531 | if (seen.has(m.id)) continue; |
| 532 | seen.add(m.id); |
| 533 | const f = formatModel(m); |
| 534 | choices.push({ value: m.id, label: f.label, hint: `installed · ${f.hint}` }); |
| 535 | } |
| 536 | |
| 537 | // Then: hardcoded lighter candidates the user might want to pull. |
| 538 | const fallbackCandidates = [ |
| 539 | { id: 'qwen3-coder', sizeB: 30, label: 'qwen3-coder', hint: '✓ tool-calls native, balanced' }, |
| 540 | { id: 'qwen2.5-coder:7b', sizeB: 7, label: 'qwen2.5-coder:7b', hint: 'best for tool use, fast' }, |
| 541 | { id: 'qwen2.5-coder:3b', sizeB: 3, label: 'qwen2.5-coder:3b', hint: 'fastest, lower quality' }, |
| 542 | { id: 'qwen2.5-coder:14b', sizeB: 14, label: 'qwen2.5-coder:14b', hint: 'balanced' }, |
| 543 | { id: 'deepseek-coder-v2:lite', sizeB: 16, label: 'deepseek-coder-v2:lite', hint: 'alternative perspective' }, |
| 544 | { id: 'llama3.2:3b', sizeB: 3, label: 'llama3.2:3b', hint: 'lean generalist' }, |
| 545 | ]; |
| 546 | const filtered = fallbackCandidates.filter(c => { |
| 547 | if (seen.has(c.id)) return false; |
| 548 | if (c.id === parentModel) return false; |
| 549 | if (parentSize === 0) return true; |
| 550 | return c.sizeB <= parentSize; |
| 551 | }); |
| 552 | filtered.sort((a, b) => a.sizeB - b.sizeB); |
| 553 | // Hardware filter |
| 554 | const usable = filtered.filter(c => { |
| 555 | const gb = c.sizeB * 0.6; |
| 556 | const avail = hw.gpu.vramGb ?? hw.ramGb; |
| 557 | return gb < avail * 0.6; |
| 558 | }); |
| 559 | const chosenFallback = usable.length > 0 ? usable : filtered; |
| 560 | for (const c of chosenFallback) { |
| 561 | if (seen.has(c.id)) continue; |
| 562 | seen.add(c.id); |
| 563 | choices.push({ value: c.id, label: c.label, hint: `requires \`ollama pull\` · ${c.hint}` }); |
| 564 | } |
| 565 | return choices; |
| 566 | } |
| 567 | |
| 568 | function approxModelSizeB(modelId: string): number { |
no test coverage detected