feat: add runtime provider/model switching via /model <tier> <provider/model>

- ModelRouter: add setClient(), labels map, getLabel(), getAllLabels()
- TUI commands: parse /model <tier> <provider/model> syntax with autocompletion
- TUI minimal: handle provider switching via createClientFromConfig factory
- Daemon: wire initial labels into router config
- Fix /model alias mappings (opus=complex, sonnet=default, haiku=fast)
- Add design doc and update state.json with feature status
This commit is contained in:
William Valentin
2026-02-06 23:42:14 -08:00
parent e92ce69067
commit d4530a7034
8 changed files with 527 additions and 37 deletions
+38 -7
View File
@@ -7,6 +7,7 @@ import { parseCommand, getHelpText, resolveModelAlias, getCommandCompletions, ge
import { renderMarkdown } from './markdown.js';
import type { ModelConfig } from '../../config/schema.js';
import { OllamaClient, LlamaCppClient } from '../../models/index.js';
import { createClientFromConfig } from '../../daemon/index.js';
import { loginGitHub } from '../../auth/index.js';
export { parseCommand, type Command };
@@ -180,7 +181,7 @@ export class MinimalTui {
break;
case 'model':
this.handleModelCommand(command.name);
this.handleModelCommand(command.name, command.providerModel);
break;
case 'backend':
@@ -201,21 +202,51 @@ export class MinimalTui {
}
}
private handleModelCommand(name?: string): void {
private handleModelCommand(name?: string, providerModel?: string): void {
const router = this.config.modelRouter;
if (!router) {
console.log(`${colors.gray}Model switching not available.${colors.reset}\n`);
return;
}
if (!name) {
const current = router.getTier();
const available = router.getAvailableTiers();
console.log(`${colors.gray}Current model:${colors.reset} ${current}`);
console.log(`${colors.gray}Available:${colors.reset} ${available.join(', ')}\n`);
// /model <tier> <provider/model> — change a tier's provider and model
if (name && providerModel) {
const tier = resolveModelAlias(name);
const slashIdx = providerModel.indexOf('/');
if (slashIdx === -1) {
console.log(`${colors.gray}Invalid format. Use provider/model (e.g. anthropic/claude-sonnet-4)${colors.reset}\n`);
return;
}
const provider = providerModel.slice(0, slashIdx);
const model = providerModel.slice(slashIdx + 1);
try {
const client = createClientFromConfig({ provider: provider as 'anthropic', model });
router.setClient(tier, client, providerModel);
console.log(`${colors.gray}Set ${tier} to:${colors.reset} ${providerModel}\n`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.log(`${colors.gray}Failed to create client:${colors.reset} ${message}\n`);
}
return;
}
// /model — show all tiers with labels
if (!name) {
const current = router.getTier();
const available = router.getAvailableTiers();
const labels = router.getAllLabels();
console.log(`${colors.gray}Active tier:${colors.reset} ${current}`);
for (const tier of available) {
const label = labels[tier] ?? 'unknown';
const marker = tier === current ? ' ←' : '';
console.log(` ${tier}: ${label}${marker}`);
}
console.log();
return;
}
// /model <tier> — switch active tier
const tier = resolveModelAlias(name);
if (router.setTier(tier)) {
// Also update the agent tier so chatWithRouter uses the correct client