From 5c90640e2abbcdff0149b98dc32feceb9802406f Mon Sep 17 00:00:00 2001 From: William Valentin Date: Tue, 10 Feb 2026 21:32:44 -0800 Subject: [PATCH] fix: clear error messages for missing API keys on provider switch Previously, switching to zhipuai/openrouter/xai via /model would throw a confusing 'OPENAI_API_KEY missing' error from the OpenAI SDK. Now createClientFromConfig validates API keys before constructing the client, throwing errors that name the correct env var (e.g. ZHIPUAI_API_KEY). Also fixes the misleading 'as anthropic' type cast in the /model handler to validate against MODEL_PROVIDERS and use the ModelProvider type. --- src/daemon/models.ts | 21 ++++++++++++++++++--- src/frontends/tui/minimal.ts | 10 ++++++++-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/daemon/models.ts b/src/daemon/models.ts index fb23af3..377b505 100644 --- a/src/daemon/models.ts +++ b/src/daemon/models.ts @@ -3,6 +3,21 @@ import { AnthropicClient, OpenAIClient, OllamaClient, LlamaCppClient, GeminiClie import type { ModelClient, RetryConfig, ModelTier } from '../models/index.js'; import { logger } from '../logger.js'; +/** + * Resolve an API key from config or environment variable. + * Throws a clear error naming the expected env var if neither source provides a key. + */ +function requireApiKey(cfg: ModelConfig, envVar: string): string { + const key = cfg.api_key ?? process.env[envVar]; + if (!key) { + throw new Error( + `API key required for ${cfg.provider}. ` + + `Set ${envVar} environment variable or provide api_key in config.` + ); + } + return key; +} + /** * Create a ModelClient from a provider config entry. * Dispatches on the `provider` field so all tiers and fallback entries @@ -41,19 +56,19 @@ export function createClientFromConfig(cfg: ModelConfig): ModelClient { case 'openrouter': return new OpenAIClient({ model: cfg.model, - apiKey: cfg.api_key ?? process.env.OPENROUTER_API_KEY, + apiKey: requireApiKey(cfg, 'OPENROUTER_API_KEY'), baseURL: cfg.endpoint ?? 'https://openrouter.ai/api/v1', }); case 'zhipuai': return new OpenAIClient({ model: cfg.model, - apiKey: cfg.api_key ?? process.env.ZHIPUAI_API_KEY, + apiKey: requireApiKey(cfg, 'ZHIPUAI_API_KEY'), baseURL: cfg.endpoint ?? 'https://api.z.ai/api/paas/v4', }); case 'xai': return new OpenAIClient({ model: cfg.model, - apiKey: cfg.api_key ?? process.env.XAI_API_KEY, + apiKey: requireApiKey(cfg, 'XAI_API_KEY'), baseURL: cfg.endpoint ?? 'https://api.x.ai/v1', }); case 'bedrock': diff --git a/src/frontends/tui/minimal.ts b/src/frontends/tui/minimal.ts index acea89c..e0214b1 100644 --- a/src/frontends/tui/minimal.ts +++ b/src/frontends/tui/minimal.ts @@ -5,7 +5,8 @@ import type { ModelRouter, ModelTier } from '../../models/router.js'; import type { NativeAgent } from '../../backends/native/agent.js'; import { parseCommand, getHelpText, resolveModelAlias, getCommandCompletions, getCommandTooltip, type Command } from './commands.js'; import { renderMarkdown } from './markdown.js'; -import type { ModelConfig } from '../../config/schema.js'; +import type { ModelConfig, ModelProvider } from '../../config/schema.js'; +import { MODEL_PROVIDERS } from '../../config/schema.js'; import { OllamaClient, LlamaCppClient } from '../../models/index.js'; import { createClientFromConfig } from '../../daemon/index.js'; import { loginGitHub } from '../../auth/index.js'; @@ -232,8 +233,13 @@ export class MinimalTui { const provider = providerModel.slice(0, slashIdx); const model = providerModel.slice(slashIdx + 1); + if (!MODEL_PROVIDERS.includes(provider as ModelProvider)) { + console.log(`${colors.gray}Unknown provider "${provider}". Known providers: ${MODEL_PROVIDERS.join(', ')}${colors.reset}\n`); + return; + } + try { - const client = createClientFromConfig({ provider: provider as 'anthropic', model }); + const client = createClientFromConfig({ provider: provider as ModelProvider, model }); router.setClient(tier, client, providerModel); console.log(`${colors.gray}Set ${tier} to:${colors.reset} ${providerModel}\n`); } catch (error) {