From 61d935844914146672a0905d758ca6df9d0aac2d Mon Sep 17 00:00:00 2001 From: William Valentin Date: Thu, 5 Feb 2026 13:32:35 -0800 Subject: [PATCH] docs: add backend switch implementation plan --- ...026-02-05-backend-switch-implementation.md | 418 ++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 docs/plans/2026-02-05-backend-switch-implementation.md diff --git a/docs/plans/2026-02-05-backend-switch-implementation.md b/docs/plans/2026-02-05-backend-switch-implementation.md new file mode 100644 index 0000000..07cc3e1 --- /dev/null +++ b/docs/plans/2026-02-05-backend-switch-implementation.md @@ -0,0 +1,418 @@ +# Backend Switch Command Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add `/backend` slash command to switch between local LLM providers (ollama, llamacpp) at runtime. + +**Architecture:** Extend config schema with `local_providers` object, add `setLocalClient()` to ModelRouter, add command parsing and handling in TUI. + +**Tech Stack:** TypeScript, Zod (config validation), Vitest (testing) + +--- + +## Task 1: Extend Config Schema + +**Files:** +- Modify: `src/config/schema.ts` + +### Step 1: Add local_providers to schema + +Update `src/config/schema.ts` - add `local_providers` field to modelsSchema: + +```typescript +const modelsSchema = z.object({ + local: modelConfigSchema.optional(), + fast: modelConfigSchema.optional(), + default: modelConfigSchema, + complex: modelConfigSchema.optional(), + fallback_chain: z.array(z.string()).default(['anthropic']), + local_providers: z.record(z.string(), modelConfigSchema).optional(), +}); +``` + +### Step 2: Verify build + +Run: `npx tsc --noEmit` +Expected: No errors + +### Step 3: Commit + +```bash +git add src/config/schema.ts +git commit -m "feat: add local_providers to config schema" +``` + +--- + +## Task 2: Add ModelRouter Methods + +**Files:** +- Modify: `src/models/router.ts` +- Modify: `src/models/router.test.ts` + +### Step 1: Write the failing test + +Add to `src/models/router.test.ts`: + +```typescript +describe('ModelRouter local client switching', () => { + it('allows setting a new local client', () => { + const mockDefault = { chat: vi.fn() } as unknown as ModelClient; + const mockLocal1 = { chat: vi.fn() } as unknown as ModelClient; + const mockLocal2 = { chat: vi.fn() } as unknown as ModelClient; + + const router = new ModelRouter({ + default: mockDefault, + local: mockLocal1, + fallbackChain: [], + }); + + expect(router.getLocalProviderName()).toBe(undefined); + + router.setLocalClient(mockLocal2, 'llamacpp'); + + expect(router.getLocalProviderName()).toBe('llamacpp'); + expect(router.getClient('local')).toBe(mockLocal2); + }); +}); +``` + +### Step 2: Run test to verify it fails + +Run: `npm test -- src/models/router.test.ts` +Expected: FAIL with "setLocalClient is not a function" or "getLocalProviderName is not a function" + +### Step 3: Write minimal implementation + +Add to `src/models/router.ts`: + +1. Add private field after line 17: +```typescript +private localProviderName?: string; +``` + +2. Add methods after `getClient()` (after line 113): +```typescript +setLocalClient(client: ModelClient, providerName: string): void { + this.clients.set('local', client); + this.localProviderName = providerName; +} + +getLocalProviderName(): string | undefined { + return this.localProviderName; +} +``` + +### Step 4: Run test to verify it passes + +Run: `npm test -- src/models/router.test.ts` +Expected: PASS + +### Step 5: Commit + +```bash +git add src/models/router.ts src/models/router.test.ts +git commit -m "feat: add setLocalClient and getLocalProviderName to ModelRouter" +``` + +--- + +## Task 3: Add Backend Command Parsing + +**Files:** +- Modify: `src/frontends/tui/commands.ts` +- Modify: `src/frontends/tui/commands.test.ts` + +### Step 1: Write the failing tests + +Add to `src/frontends/tui/commands.test.ts`: + +```typescript +it('parses /backend command without argument', () => { + expect(parseCommand('/backend')).toEqual({ type: 'backend' }); +}); + +it('parses /backend command with argument', () => { + expect(parseCommand('/backend llamacpp')).toEqual({ type: 'backend', provider: 'llamacpp' }); + expect(parseCommand('/backend ollama')).toEqual({ type: 'backend', provider: 'ollama' }); +}); +``` + +### Step 2: Run test to verify it fails + +Run: `npm test -- src/frontends/tui/commands.test.ts` +Expected: FAIL - returns message type instead of backend type + +### Step 3: Implement command parsing + +Update `src/frontends/tui/commands.ts`: + +1. Add to Command type (after line 8): +```typescript +| { type: 'backend'; provider?: string } +``` + +2. Add parsing after the model block (after line 47): +```typescript +// Backend (with optional argument) +if (trimmed === '/backend') { + return { type: 'backend' }; +} +if (trimmed.startsWith('/backend ')) { + const provider = trimmed.slice('/backend '.length).trim(); + return { type: 'backend', provider }; +} +``` + +3. Add to getHelpText() (after line 63, before /reset): +```typescript + /backend [provider] Show or switch local backend (ollama, llamacpp) +``` + +4. Add to SLASH_COMMANDS array (after '/model'): +```typescript +'/backend', +``` + +5. Add to COMMAND_TOOLTIPS (after '/model'): +```typescript +'/backend': 'Show or switch local backend (ollama, llamacpp)', +``` + +### Step 4: Run test to verify it passes + +Run: `npm test -- src/frontends/tui/commands.test.ts` +Expected: PASS + +### Step 5: Commit + +```bash +git add src/frontends/tui/commands.ts src/frontends/tui/commands.test.ts +git commit -m "feat: add /backend command parsing" +``` + +--- + +## Task 4: Add Backend Command Handler to MinimalTui + +**Files:** +- Modify: `src/frontends/tui/minimal.ts` +- Modify: `src/frontends/tui/minimal.test.ts` + +### Step 1: Write the failing test + +Add to `src/frontends/tui/minimal.test.ts` (import ModelConfig type first): + +```typescript +import type { ModelConfig } from '../../config/schema.js'; + +describe('MinimalTui backend command', () => { + it('switches local backend when provider is configured', async () => { + const mockSession = { + id: 'test', + getHistory: () => [], + addMessage: vi.fn(), + clear: vi.fn(), + }; + + const mockRouter = { + getTier: () => 'default' as const, + getAvailableTiers: () => ['default', 'local'], + setTier: vi.fn(() => true), + getLocalProviderName: () => 'ollama', + setLocalClient: vi.fn(), + chat: vi.fn(), + getClient: vi.fn(), + }; + + const localProviders: Record = { + llamacpp: { + provider: 'llamacpp', + model: '', + endpoint: 'http://localhost:8080', + }, + }; + + const tui = new MinimalTui({ + session: mockSession as any, + modelClient: mockRouter as any, + modelRouter: mockRouter as any, + systemPrompt: 'test', + localProviders, + }); + + // Access private method for testing + await (tui as any).handleBackendCommand('llamacpp'); + + expect(mockRouter.setLocalClient).toHaveBeenCalled(); + }); +}); +``` + +### Step 2: Run test to verify it fails + +Run: `npm test -- src/frontends/tui/minimal.test.ts` +Expected: FAIL - handleBackendCommand doesn't exist or localProviders not accepted + +### Step 3: Implement handler + +Update `src/frontends/tui/minimal.ts`: + +1. Add import at top: +```typescript +import type { ModelConfig } from '../../config/schema.js'; +import { OllamaClient, LlamaCppClient } from '../../models/index.js'; +``` + +2. Add to MinimalTuiConfig interface (after line 23): +```typescript +localProviders?: Record; +currentLocalProvider?: string; +``` + +3. Add case in handleCommand switch (after line 100, before 'message'): +```typescript +case 'backend': + this.handleBackendCommand(command.provider); + break; +``` + +4. Add handler method (after handleModelCommand, around line 130): +```typescript +private handleBackendCommand(provider?: string): void { + const router = this.config.modelRouter; + if (!router) { + console.log('Backend switching not available.\n'); + return; + } + + if (!provider) { + const current = router.getLocalProviderName() ?? this.config.currentLocalProvider ?? 'unknown'; + const available = this.getAvailableBackends(); + console.log(`Current local backend: ${current}`); + console.log(`Available: ${available.join(', ')}\n`); + return; + } + + const providerConfig = this.config.localProviders?.[provider]; + if (!providerConfig) { + const available = this.getAvailableBackends(); + console.log(`Backend '${provider}' not configured.`); + console.log(`Available: ${available.join(', ')}\n`); + return; + } + + const client = this.createLocalClient(providerConfig); + if (!client) { + console.log(`Failed to create client for '${provider}'.\n`); + return; + } + + router.setLocalClient(client, provider); + console.log(`Switched to backend: ${provider}\n`); +} + +private getAvailableBackends(): string[] { + const backends: string[] = []; + if (this.config.currentLocalProvider) { + backends.push(this.config.currentLocalProvider); + } + if (this.config.localProviders) { + backends.push(...Object.keys(this.config.localProviders)); + } + return [...new Set(backends)]; +} + +private createLocalClient(config: ModelConfig): ModelClient | null { + if (config.provider === 'ollama') { + return new OllamaClient({ + model: config.model, + host: config.endpoint, + }); + } + if (config.provider === 'llamacpp') { + return new LlamaCppClient({ + endpoint: config.endpoint ?? 'http://localhost:8080', + authToken: config.auth_token, + }); + } + return null; +} +``` + +### Step 4: Run test to verify it passes + +Run: `npm test -- src/frontends/tui/minimal.test.ts` +Expected: PASS + +### Step 5: Commit + +```bash +git add src/frontends/tui/minimal.ts src/frontends/tui/minimal.test.ts +git commit -m "feat: add /backend command handler to MinimalTui" +``` + +--- + +## Task 5: Wire Up Config in TUI Entry Point + +**Files:** +- Modify: `src/tui.ts` + +### Step 1: Read current file and update + +Read `src/tui.ts` to understand current structure, then update to pass `localProviders` and `currentLocalProvider` to MinimalTui. + +The changes needed: +1. Extract `local_providers` from config +2. Extract current local provider name from `config.models.local.provider` +3. Pass both to MinimalTui config + +### Step 2: Run full test suite + +Run: `npm test` +Expected: All tests pass + +### Step 3: Commit + +```bash +git add src/tui.ts +git commit -m "feat: wire up localProviders config to TUI" +``` + +--- + +## Task 6: Verify and Final Test + +**Files:** None (verification only) + +### Step 1: Run full test suite + +Run: `npm test` +Expected: All tests pass + +### Step 2: Type check + +Run: `npx tsc --noEmit` +Expected: No errors + +### Step 3: Build + +Run: `npm run build` +Expected: Build succeeds + +--- + +## Summary + +| Task | Description | Tests Added | +|------|-------------|-------------| +| 1 | Config schema extension | 0 | +| 2 | ModelRouter methods | 1 | +| 3 | Command parsing | 2 | +| 4 | MinimalTui handler | 1 | +| 5 | Wire up config | 0 | +| 6 | Verification | 0 | + +**Total new tests:** 4 +**Files modified:** 7