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
This commit is contained in:
William Valentin
2026-02-12 00:13:59 -08:00
parent 0b44adbaea
commit 05037a917e
2 changed files with 71 additions and 12 deletions
+11 -9
View File
@@ -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 <name> 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:
+60 -3
View File
@@ -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<void> {
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<void> {
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<void>((resolve) => {
spawn('pkill', [processName]).on('close', resolve);
});
} catch (error) {
// Ignore errors stopping backends
}
}
private async startBackend(provider: string, config: ModelConfig): Promise<void> {
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<void> {