fix: provider-aware model routing with fallback visibility

- Extract createClientFromConfig() to dispatch on provider field instead
  of hardcoding all tiers as AnthropicClient
- Add fallback/fallbackReason metadata to ChatResponse and ChatStreamEvent
  so callers know when a fallback model was used
- Enhance doctor check to report full model stack and warn on missing
  API keys for cloud providers
- Log fallback warnings in NativeAgent and display them in TUI
- Support tier names and local_providers entries in fallback_chain
- Add 8 tests for createClientFromConfig covering all provider types
This commit is contained in:
William Valentin
2026-02-06 09:58:56 -08:00
parent c9b1c607d5
commit e4b7f96d33
9 changed files with 334 additions and 56 deletions
+21 -3
View File
@@ -116,12 +116,30 @@ const checkModelConnectivity: Check = async (ctx) => {
if (!ctx.config) {
return { status: 'skip', label: 'Model connectivity', detail: '(config invalid)' };
}
// Skip actual API call in doctor — just verify config looks complete
const model = ctx.config.models.default;
const models = ctx.config.models;
const model = models.default;
if (!model.model) {
return { status: 'fail', label: 'Model connectivity', detail: 'no default model configured' };
}
return { status: 'pass', label: 'Model connectivity', detail: `(${model.provider}: ${model.model})` };
// Check if API key is present for providers that need one
const needsKey = ['anthropic', 'openai', 'gemini'];
if (needsKey.includes(model.provider) && !model.api_key && !model.auth_token) {
const envVar = model.provider === 'anthropic' ? 'ANTHROPIC_API_KEY' : model.provider === 'openai' ? 'OPENAI_API_KEY' : undefined;
const hasEnv = envVar && process.env[envVar];
if (!hasEnv) {
return { status: 'warn', label: 'Model connectivity', detail: `${model.provider}/${model.model} — no API key or auth token found` };
}
}
// Build a summary of the model stack
const parts = [`default: ${model.provider}/${model.model}`];
if (models.fast) parts.push(`fast: ${models.fast.provider}/${models.fast.model}`);
if (models.complex) parts.push(`complex: ${models.complex.provider}/${models.complex.model}`);
if (models.local) parts.push(`local: ${models.local.provider}/${models.local.model}`);
parts.push(`fallback: [${models.fallback_chain.join(', ')}]`);
return { status: 'pass', label: 'Model connectivity', detail: parts.join(', ') };
};
const checkTelegram: Check = async (ctx) => {