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:
@@ -0,0 +1,79 @@
|
||||
# Provider/Model Runtime Switching
|
||||
|
||||
**Date:** 2026-02-06
|
||||
**Status:** In Progress
|
||||
|
||||
## Goal
|
||||
|
||||
Enable easy runtime switching of model providers per tier via the `/model` command, using `provider/model` syntax.
|
||||
|
||||
## Commands
|
||||
|
||||
```
|
||||
/model — Show all tiers with provider/model labels
|
||||
/model fast — Switch active tier (existing behavior)
|
||||
/model default github-copilot/claude-sonnet-4-5 — Change default tier's provider+model
|
||||
/model complex anthropic/claude-opus-4 — Change complex tier's provider+model
|
||||
/model fast github-copilot/gpt-4o-mini — Change fast tier's provider+model
|
||||
```
|
||||
|
||||
## Design Decisions
|
||||
|
||||
1. **No presets** — Direct `provider/model` targeting per tier. YAGNI.
|
||||
2. **Full override** — When you set a tier, it fully replaces the previous client.
|
||||
3. **Local tier excluded** — `/model local` continues to use `/backend` for switching. Local models are a different concern.
|
||||
4. **Auth** — Config-based `api_key` for most providers. `/login` OAuth flow for GitHub Copilot (already implemented).
|
||||
5. **Merged into /model** — No separate `/provider` command. Everything lives under `/model`.
|
||||
|
||||
## Implementation
|
||||
|
||||
### 1. ModelRouter (`src/models/router.ts`)
|
||||
|
||||
- Add `setClient(tier: ModelTier, client: ModelClient, label: string)` — replaces a tier's client at runtime
|
||||
- Add `getLabel(tier: ModelTier): string` — returns `provider/model` string for display
|
||||
- Track labels in a `Map<ModelTier, string>` populated at construction and updated by `setClient()`
|
||||
|
||||
### 2. Client Factory (`src/daemon/index.ts`)
|
||||
|
||||
Extract `createClientFromProvider(provider: string, model: string, opts?: { apiKey?: string; endpoint?: string }): ModelClient` factory function from the existing inline client creation logic. Used by both daemon startup and runtime `/model` switching.
|
||||
|
||||
### 3. Command Parser (`src/frontends/tui/commands.ts`)
|
||||
|
||||
- Extend `Command` type: `{ type: 'model'; name?: string; providerModel?: string }`
|
||||
- Parse `/model <tier> <provider/model>` — split on space to get tier + provider/model
|
||||
- Parse provider/model string: split on first `/` to get provider and model name
|
||||
- Update autocompletion to suggest available providers after tier name
|
||||
- Update tooltips
|
||||
|
||||
### 4. Daemon Wiring (`src/daemon/index.ts`)
|
||||
|
||||
Handle the new command variant:
|
||||
1. Receive `{ type: 'model', name: 'default', providerModel: 'github-copilot/claude-sonnet-4-5' }`
|
||||
2. Parse provider and model from `providerModel`
|
||||
3. Call `createClientFromProvider(provider, model)` to instantiate client
|
||||
4. Call `router.setClient('default', client, 'github-copilot/claude-sonnet-4-5')`
|
||||
5. Respond with confirmation message
|
||||
|
||||
### 5. Provider Name Mapping
|
||||
|
||||
Map short provider names to client constructors:
|
||||
|
||||
| Provider Name | Client Class |
|
||||
|---------------|-------------|
|
||||
| `anthropic` | AnthropicClient |
|
||||
| `openai` | OpenAIClient |
|
||||
| `github` / `github-copilot` | GitHubModelsClient |
|
||||
| `gemini` | GeminiClient |
|
||||
| `bedrock` | BedrockClient |
|
||||
| `ollama` | OllamaClient |
|
||||
| `llamacpp` | LlamaCppClient |
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/models/router.ts` | Add `setClient()`, `getLabel()`, label tracking |
|
||||
| `src/models/router.test.ts` | Tests for new methods |
|
||||
| `src/frontends/tui/commands.ts` | Extended parser, completions, tooltips |
|
||||
| `src/frontends/tui/commands.test.ts` | Tests for new parsing |
|
||||
| `src/daemon/index.ts` | Extract factory, wire new command handler |
|
||||
+135
-2
@@ -336,6 +336,136 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"p4-media-pipeline": {
|
||||
"status": "completed",
|
||||
"date": "2026-02-06",
|
||||
"summary": "Multimodal media pipeline: receive images from channel adapters and pass through to vision-capable models (Anthropic, OpenAI, Gemini, Bedrock)",
|
||||
"phases": {
|
||||
"type_widening": {
|
||||
"priority": "P4",
|
||||
"status": "completed",
|
||||
"description": "Widen Message.content from string to string | MessageContentPart[], add Attachment type to channel layer, add ImageSource/MessageContentPart types",
|
||||
"files_created": [
|
||||
"src/models/media.ts",
|
||||
"src/models/media.test.ts"
|
||||
],
|
||||
"files_modified": [
|
||||
"src/models/types.ts",
|
||||
"src/models/index.ts",
|
||||
"src/channels/types.ts",
|
||||
"src/channels/index.ts"
|
||||
],
|
||||
"test_status": "25/25 passing"
|
||||
},
|
||||
"model_client_multimodal": {
|
||||
"priority": "P4",
|
||||
"status": "completed",
|
||||
"description": "Update all model clients to convert MessageContentPart[] to provider-specific image formats (Anthropic base64, OpenAI data URI, Gemini inlineData, Bedrock image bytes)",
|
||||
"files_modified": [
|
||||
"src/models/anthropic.ts",
|
||||
"src/models/openai.ts",
|
||||
"src/models/gemini.ts",
|
||||
"src/models/bedrock.ts",
|
||||
"src/models/local/llamacpp.ts",
|
||||
"src/models/local/ollama.ts"
|
||||
]
|
||||
},
|
||||
"agent_attachment_passthrough": {
|
||||
"priority": "P4",
|
||||
"status": "completed",
|
||||
"description": "Wire attachments through NativeAgent.process() and AgentOrchestrator.process() to daemon message handler",
|
||||
"files_modified": [
|
||||
"src/backends/native/agent.ts",
|
||||
"src/backends/native/orchestrator.ts",
|
||||
"src/daemon/index.ts"
|
||||
]
|
||||
},
|
||||
"downstream_type_fixes": {
|
||||
"priority": "P4",
|
||||
"status": "completed",
|
||||
"description": "Fix all consumers of Message.content to use getMessageText() helper: token estimation, compaction, TUI rendering",
|
||||
"files_modified": [
|
||||
"src/context/tokens.ts",
|
||||
"src/context/compaction.ts",
|
||||
"src/frontends/tui/components/MessageList.tsx"
|
||||
]
|
||||
},
|
||||
"channel_adapter_extraction": {
|
||||
"priority": "P4",
|
||||
"status": "completed",
|
||||
"description": "Extract images from platform messages in all channel adapters",
|
||||
"sub_phases": {
|
||||
"telegram": {
|
||||
"status": "completed",
|
||||
"description": "Handle message:photo (largest size, download via getFile API, base64) and image message:document events with caption text",
|
||||
"files_modified": ["src/channels/telegram/adapter.ts"]
|
||||
},
|
||||
"discord": {
|
||||
"status": "completed",
|
||||
"description": "Extract image attachments from message.attachments Collection, pass Discord CDN URLs directly",
|
||||
"files_modified": ["src/channels/discord/adapter.ts"]
|
||||
},
|
||||
"slack": {
|
||||
"status": "completed",
|
||||
"description": "Download image files via url_private_download with bot token auth, base64 encode",
|
||||
"files_modified": ["src/channels/slack/adapter.ts"]
|
||||
},
|
||||
"whatsapp": {
|
||||
"status": "completed",
|
||||
"description": "Use downloadMedia() from whatsapp-web.js (returns base64 natively)",
|
||||
"files_modified": ["src/channels/whatsapp/adapter.ts"]
|
||||
},
|
||||
"webchat": {
|
||||
"status": "deferred",
|
||||
"description": "Requires gateway protocol update for WebSocket attachment messages"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"p5-github-copilot-provider": {
|
||||
"status": "completed",
|
||||
"date": "2026-02-06",
|
||||
"summary": "GitHub Copilot as a model provider with OAuth device flow and auto-login on first use",
|
||||
"phases": {
|
||||
"copilot_client": {
|
||||
"priority": "P5",
|
||||
"status": "completed",
|
||||
"description": "GitHubModelsClient using OpenAI SDK against api.githubcopilot.com with Copilot-specific headers, multimodal support, streaming, tool calls",
|
||||
"files_created": [
|
||||
"src/models/github.ts",
|
||||
"src/auth/github.ts",
|
||||
"src/auth/index.ts"
|
||||
],
|
||||
"files_modified": [
|
||||
"src/config/schema.ts",
|
||||
"src/models/index.ts",
|
||||
"src/models/costs.ts",
|
||||
"src/daemon/index.ts",
|
||||
"src/cli/tui.ts"
|
||||
]
|
||||
},
|
||||
"oauth_device_flow": {
|
||||
"priority": "P5",
|
||||
"status": "completed",
|
||||
"description": "Interactive OAuth device flow via /login github command, token stored at ~/.config/flynn/auth.json with chmod 0600",
|
||||
"files_modified": [
|
||||
"src/frontends/tui/minimal.ts",
|
||||
"src/frontends/tui/commands.ts"
|
||||
]
|
||||
},
|
||||
"auto_login": {
|
||||
"priority": "P5",
|
||||
"status": "completed",
|
||||
"description": "Lazy token resolution with onLoginRequired callback — triggers OAuth device flow automatically on first API call when no token is available",
|
||||
"files_modified": [
|
||||
"src/models/github.ts",
|
||||
"src/daemon/index.ts",
|
||||
"src/cli/tui.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"earlier_plans": {
|
||||
"status": "completed",
|
||||
"summary": "Original design and implementation phases from 2026-02-02 to 2026-02-05",
|
||||
@@ -361,11 +491,14 @@
|
||||
},
|
||||
|
||||
"overall_progress": {
|
||||
"total_test_count": 655,
|
||||
"total_test_count": 742,
|
||||
"all_tests_passing": true,
|
||||
"p0_completion": "3/3 (100%)",
|
||||
"p1_completion": "4/4 (100%)",
|
||||
"p2_completion": "7/7 (100%)",
|
||||
"next_up": "p3_remaining (group chat support, gateway auth, gemini provider, browser control, additional model providers)"
|
||||
"p3_completion": "completed (group chat, gateway auth, Gemini, OpenRouter, Bedrock, browser control)",
|
||||
"p4_completion": "1/1 (100%) — multimodal media pipeline",
|
||||
"p5_completion": "1/1 (100%) — GitHub Copilot provider with auto-login",
|
||||
"next_up": "p6 (image.analyze tool, audio transcription, outbound attachments, gateway protocol attachments)"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user