# 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