fix: chat works with any configured provider + surfaces errors

The chat defaulted to a Claude model, so a user who saved only a Gemini key got
a silent failure (backend derived Anthropic, found no key, errored — and the UI
showed nothing).

- Backend: resolveModel now falls back to whichever provider actually has a key
  (preferring the configured one), so a Gemini key just works regardless of the
  picked model. Clear 400 if no provider is configured at all.
- Frontend: the chat seeds its model/effort from the saved AI config, and now
  surfaces request failures as both a persistent alert banner and a toast —
  never silent.
- Settings: switching provider auto-selects that provider's default model.
- Refreshed the model catalog to current ids (Gemini 2.5 Pro/Flash + 2.0 Flash;
  dropped the retired gemini-1.5-pro); per-provider default model updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-13 19:27:37 +03:00
parent a80086ef72
commit ddf4b49d82
5 changed files with 139 additions and 32 deletions
+44 -11
View File
@@ -39,6 +39,28 @@ const PROVIDER_LABELS: Record<ApiProvider, string> = {
gemini: "Google Gemini",
};
const PROVIDER_ORDER: ApiProvider[] = ["anthropic", "openai", "gemini"];
// A safe default model id for each provider, used when the picked model doesn't
// belong to the provider we end up calling.
const DEFAULT_MODEL: Record<ApiProvider, string> = {
anthropic: "claude-sonnet-4-6",
openai: "gpt-4o",
gemini: "gemini-2.5-flash",
};
// Choose which provider to actually call: prefer the one the picked model maps
// to (if it has a key), else the user's configured provider (if keyed), else any
// provider that has a key. Returns null when no key is configured at all.
function chooseProvider(
settings: AiSettingsRow,
requested: ApiProvider | null,
): ApiProvider | null {
if (requested && getApiKey(settings, requested)) return requested;
if (getApiKey(settings, settings.provider)) return settings.provider;
return PROVIDER_ORDER.find((p) => getApiKey(settings, p)) ?? null;
}
// Resolve a concrete LanguageModel for a request. `requestedModelId` is the id
// the user picked in the chat input; when it maps to a cloud provider we use
// that provider's stored key, otherwise we fall back to local Ollama (also used
@@ -47,11 +69,8 @@ export function resolveModel(
settings: AiSettingsRow,
requestedModelId: string,
): ResolvedModel {
const provider =
settings.mode === "local" ? null : providerForModel(requestedModelId);
if (!provider) {
// Local mode via Ollama's OpenAI-compatible endpoint. No key required.
// Local mode (or the local sentinel) → Ollama's OpenAI-compatible endpoint.
if (settings.mode === "local" || requestedModelId === OLLAMA_SENTINEL) {
const ollama = createOpenAICompatible({
name: "ollama",
baseURL: `${settings.ollamaBaseUrl.replace(/\/$/, "")}/v1`,
@@ -63,20 +82,34 @@ export function resolveModel(
};
}
const apiKey = getApiKey(settings, provider);
if (!apiKey) {
// API mode. Pick a provider that actually has a key, falling back gracefully
// so a configured key (e.g. Gemini) is used even if the picked model belongs
// to a different, unconfigured provider.
const requested = providerForModel(requestedModelId);
const provider = chooseProvider(settings, requested);
if (!provider) {
throw new HttpError(
400,
`No API key configured for ${PROVIDER_LABELS[provider]}. Add one in Settings → AI.`,
"No AI provider API key is configured. Add one in Settings → AI, or switch to a local model.",
);
}
// Use the picked model only if it belongs to the chosen provider; otherwise
// use the user's default (or a per-provider default).
const modelName =
requested === provider
? requestedModelId
: providerForModel(settings.defaultModel) === provider
? settings.defaultModel
: DEFAULT_MODEL[provider];
const apiKey = getApiKey(settings, provider)!;
const model: LanguageModel =
provider === "anthropic"
? createAnthropic({ apiKey })(requestedModelId)
? createAnthropic({ apiKey })(modelName)
: provider === "gemini"
? createGoogleGenerativeAI({ apiKey })(requestedModelId)
: createOpenAI({ apiKey })(requestedModelId);
? createGoogleGenerativeAI({ apiKey })(modelName)
: createOpenAI({ apiKey })(modelName);
return { model, isExternal: true, providerLabel: PROVIDER_LABELS[provider] };
}