From e12eb3a0bee59806852557623f713d63a4c64c01 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sat, 7 Feb 2026 13:58:34 -0800 Subject: [PATCH] fix: TUI now uses shared model router with auto-fallback support The TUI was building its own ModelRouter with a duplicated client factory that lacked auto same-model fallback, local_providers resolution, retry config, and per-tier fallback logic. When Anthropic failed, it skipped GitHub Models and fell straight to the local Ollama model. Replace the duplicated ~50-line createClient + router setup in tui.ts with a single call to the daemon's createModelRouter(), which already handles all of these correctly. This removes ~50 lines of duplicated code and ensures TUI and daemon have identical fallback behavior. --- src/cli/tui.ts | 64 +++------------------------------------------ src/daemon/index.ts | 2 +- 2 files changed, 5 insertions(+), 61 deletions(-) diff --git a/src/cli/tui.ts b/src/cli/tui.ts index de513af..d764072 100644 --- a/src/cli/tui.ts +++ b/src/cli/tui.ts @@ -43,77 +43,21 @@ export function registerTuiCommand(program: Command): void { // Dynamic imports to keep CLI startup fast const { SessionStore, SessionManager } = await import('../session/index.js'); - const { AnthropicClient, OpenAIClient, OllamaClient, LlamaCppClient, GitHubModelsClient, GeminiClient, BedrockClient, ModelRouter } = await import('../models/index.js'); const { MinimalTui, startFullscreenTui } = await import('../frontends/tui/index.js'); const { NativeAgent } = await import('../backends/index.js'); const { ToolRegistry, ToolExecutor, allBuiltinTools, createWebSearchTools, createProcessTools, ProcessManager } = await import('../tools/index.js'); const { HookEngine } = await import('../hooks/index.js'); + const { createModelRouter } = await import('../daemon/index.js'); const dataDir = resolve(homedir(), '.local/share/flynn'); mkdirSync(dataDir, { recursive: true }); const sessionStore = new SessionStore(resolve(dataDir, 'sessions.db')); const sessionManager = new SessionManager(sessionStore); - const models = config.models; - // Provider-agnostic client factory for TUI - function createClient(cfg: typeof models.default) { - switch (cfg.provider) { - case 'anthropic': - return new AnthropicClient({ model: cfg.model, apiKey: cfg.api_key, authToken: cfg.auth_token }); - case 'openai': - return new OpenAIClient({ model: cfg.model, apiKey: cfg.api_key }); - case 'gemini': - return new GeminiClient({ model: cfg.model, apiKey: cfg.api_key }); - case 'ollama': - return new OllamaClient({ model: cfg.model, host: cfg.endpoint, numGpu: cfg.num_gpu }); - case 'llamacpp': - return new LlamaCppClient({ endpoint: cfg.endpoint ?? 'http://localhost:8080', model: cfg.model, authToken: cfg.auth_token }); - case 'openrouter': - return new OpenAIClient({ model: cfg.model, apiKey: cfg.api_key ?? process.env.OPENROUTER_API_KEY, baseURL: cfg.endpoint ?? 'https://openrouter.ai/api/v1' }); - case 'bedrock': - return new BedrockClient({ model: cfg.model, region: cfg.endpoint, accessKeyId: cfg.api_key, secretAccessKey: cfg.auth_token }); - case 'github': - return new GitHubModelsClient({ - model: cfg.model, - apiKey: cfg.api_key, - endpoint: cfg.endpoint, - onLoginRequired: async () => { - const { loginGitHub } = await import('../auth/index.js'); - console.log('\nGitHub authentication required. Starting login flow...'); - return loginGitHub((userCode, verificationUri) => { - console.log(`\nVisit: ${verificationUri}`); - console.log(`Enter code: ${userCode}\n`); - console.log('Waiting for authorization...'); - }); - }, - }); - default: - throw new Error(`Unknown provider: ${cfg.provider}`); - } - } - - const defaultClient = createClient(models.default); - const fastClient = models.fast ? createClient(models.fast) : undefined; - const complexClient = models.complex ? createClient(models.complex) : undefined; - const localClient = models.local ? createClient(models.local) : undefined; - - const fallbackChain = []; - for (const providerName of models.fallback_chain) { - if (providerName === 'openai') { - fallbackChain.push(new OpenAIClient({ model: 'gpt-4o' })); - } else if (providerName === 'local' && localClient) { - fallbackChain.push(localClient); - } - } - - const modelRouter = new ModelRouter({ - default: defaultClient, - fast: fastClient, - complex: complexClient, - local: localClient, - fallbackChain, - }); + // Reuse the daemon's model router factory — includes auto-fallback, + // local_providers, retry config, and per-tier fallback logic. + const modelRouter = createModelRouter(config); const systemPrompt = loadSystemPrompt(); diff --git a/src/daemon/index.ts b/src/daemon/index.ts index 6fed3a1..276147c 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -175,7 +175,7 @@ export function createAutoFallbackClient(tierConfig: { provider: string; model: }); } -function createModelRouter(config: Config): ModelRouter { +export function createModelRouter(config: Config): ModelRouter { const models = config.models; const defaultClient = createClientFromConfig(models.default);