From 05037a917e215932c6dae5e30be4f2363c365ec1 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Thu, 12 Feb 2026 00:13:59 -0800 Subject: [PATCH] feat(backend): auto-stop/start daemon when switching backends - Add local_providers with ollama and llamacpp configurations - /backend command now stops current daemon before starting new one - Start backends as detached processes to avoid blocking TUI - Wait 500ms for daemon to initialize before switching --- config/default.yaml | 20 ++++++------ src/frontends/tui/minimal.ts | 63 ++++++++++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 12 deletions(-) diff --git a/config/default.yaml b/config/default.yaml index 1462e19..11c1dfe 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -55,17 +55,19 @@ models: # fallback_chain. Useful for secondary API accounts or self-hosted # endpoints that aren't tied to a specific tier. # - # local_providers: - # openrouter-backup: - # provider: openrouter - # model: anthropic/claude-sonnet-4 - # ollama-big: - # provider: ollama - # model: llama3.1:70b - # endpoint: http://gpu-server:11434 + # Use /backend in the TUI to switch between these providers + local_providers: + ollama: + provider: ollama + model: glm-4.7-flash + endpoint: http://localhost:11434 + llamacpp: + provider: llamacpp + model: gpt-oss-20b + endpoint: http://localhost:8080 # # Then reference them in fallback_chain: - # fallback_chain: [openrouter-backup, ollama-big, local] + # fallback_chain: [ollama, llamacpp, local] hooks: confirm: diff --git a/src/frontends/tui/minimal.ts b/src/frontends/tui/minimal.ts index a3dc8d3..4d34d44 100644 --- a/src/frontends/tui/minimal.ts +++ b/src/frontends/tui/minimal.ts @@ -194,7 +194,7 @@ export class MinimalTui { break; case 'backend': - this.handleBackendCommand(command.provider); + await this.handleBackendCommand(command.provider); break; case 'login': @@ -277,7 +277,7 @@ export class MinimalTui { } } - private handleBackendCommand(provider?: string): void { + private async handleBackendCommand(provider?: string): Promise { const router = this.config.modelRouter; if (!router) { console.log('Backend switching not available.\n'); @@ -300,6 +300,17 @@ export class MinimalTui { return; } + // Stop current daemon if running + const currentBackend = router.getLocalProviderName(); + if (currentBackend && currentBackend !== provider) { + console.log(`${colors.gray}Stopping ${currentBackend}...${colors.reset}`); + await this.stopBackend(currentBackend); + } + + // Start new daemon + console.log(`${colors.gray}Starting ${provider}...${colors.reset}`); + await this.startBackend(provider, providerConfig); + const client = this.createLocalClient(providerConfig); if (!client) { console.log(`Failed to create client for '${provider}'.\n`); @@ -307,7 +318,53 @@ export class MinimalTui { } router.setLocalClient(client, provider); - console.log(`Switched to backend: ${provider}\n`); + console.log(`${colors.gray}Switched to backend: ${provider}${colors.reset}\n`); + } + + private async stopBackend(provider: string): Promise { + try { + const { spawn } = await import('child_process'); + let processName: string; + switch (provider) { + case 'ollama': + processName = 'ollama'; + break; + case 'llamacpp': + processName = 'llama-server'; + break; + default: + return; + } + await new Promise((resolve) => { + spawn('pkill', [processName]).on('close', resolve); + }); + } catch (error) { + // Ignore errors stopping backends + } + } + + private async startBackend(provider: string, config: ModelConfig): Promise { + try { + const { spawn } = await import('child_process'); + const args: string[] = []; + + switch (provider) { + case 'ollama': + spawn('ollama', ['serve'], { detached: true, stdio: 'ignore' }).unref(); + break; + case 'llamacpp': + args.push('--model', config.model); + args.push('--port', new URL(config.endpoint ?? 'http://localhost:8080').port || '8080'); + args.push('--host', '0.0.0.0'); + spawn('llama-server', args, { detached: true, stdio: 'ignore' }).unref(); + break; + } + + // Wait briefly for the daemon to start + await new Promise(resolve => setTimeout(resolve, 500)); + } catch (error) { + console.log(`${colors.gray}Warning: Failed to start ${provider}: ${error instanceof Error ? error.message : String(error)}${colors.reset}\n`); + } } private async handleLoginCommand(provider?: string): Promise {