fix(ui): resolve model provider from catalog instead of stale session default

When the server returns a bare model name (e.g. "deepseek-chat") with
a session-level modelProvider (e.g. "zai"), the UI blindly prepends
the provider — producing "zai/deepseek-chat" instead of the correct
"deepseek/deepseek-chat". This causes "model not allowed" errors
when switching between models from different providers.

Root cause: resolveModelOverrideValue() and resolveDefaultModelValue()
in app-render.helpers.ts, plus the /model slash command handler in
slash-command-executor.ts, all call resolveServerChatModelValue()
which trusts the session's default provider. The session provider
reflects the PREVIOUS model, not the newly selected one.

Fix: for bare model names, create a raw ChatModelOverride and resolve
through normalizeChatModelOverrideValue() which looks up the correct
provider from the model catalog. Falls back to server-provided provider
only if the catalog lookup fails. All 3 call sites are fixed.

Closes #53031

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: HCL <chenglunhu@gmail.com>
This commit is contained in:
HCL 2026-03-24 07:05:11 +08:00 committed by Peter Steinberger
parent 5ab3782215
commit be20eebc21
2 changed files with 30 additions and 7 deletions

View File

@ -536,9 +536,19 @@ function resolveModelOverrideValue(state: AppViewState): string {
return "";
}
// No local override recorded yet — fall back to server data.
// Include provider prefix so the value matches option keys (provider/model).
// Use the bare model name and resolve provider from the catalog rather than
// trusting the session's modelProvider, which may be the session default and
// not the model's actual provider (e.g. "zai" for a "deepseek-chat" model).
const activeRow = resolveActiveSessionRow(state);
if (activeRow && typeof activeRow.model === "string" && activeRow.model.trim()) {
const rawOverride = createChatModelOverride(activeRow.model.trim());
if (rawOverride) {
const normalized = normalizeChatModelOverrideValue(rawOverride, state.chatModelCatalog ?? []);
if (normalized) {
return normalized;
}
}
// Fallback: use server-provided provider if catalog lookup fails.
return resolveServerChatModelValue(activeRow.model, activeRow.modelProvider);
}
return "";
@ -546,7 +556,18 @@ function resolveModelOverrideValue(state: AppViewState): string {
function resolveDefaultModelValue(state: AppViewState): string {
const defaults = state.sessionsResult?.defaults;
return resolveServerChatModelValue(defaults?.model, defaults?.modelProvider);
const model = defaults?.model;
if (typeof model !== "string" || !model.trim()) {
return "";
}
const rawOverride = createChatModelOverride(model.trim());
if (rawOverride) {
const normalized = normalizeChatModelOverrideValue(rawOverride, state.chatModelCatalog ?? []);
if (normalized) {
return normalized;
}
}
return resolveServerChatModelValue(model, defaults?.modelProvider);
}
function buildChatModelOptions(

View File

@ -16,7 +16,7 @@ import {
isSubagentSessionKey,
parseAgentSessionKey,
} from "../../../../src/routing/session-key.js";
import { createChatModelOverride, resolveServerChatModelValue } from "../chat-model-ref.ts";
import { createChatModelOverride, normalizeChatModelOverrideValue, resolveServerChatModelValue } from "../chat-model-ref.ts";
import type { GatewayBrowserClient } from "../gateway.ts";
import type {
AgentsListResult,
@ -155,10 +155,12 @@ async function executeModel(
key: sessionKey,
model: args.trim(),
});
const resolvedValue = resolveServerChatModelValue(
patched.resolved?.model ?? args.trim(),
patched.resolved?.modelProvider,
);
const patchedModel = patched.resolved?.model ?? args.trim();
const rawOverride = createChatModelOverride(patchedModel.trim());
const resolvedValue = rawOverride
? (normalizeChatModelOverrideValue(rawOverride, state.chatModelCatalog ?? []) ||
resolveServerChatModelValue(patchedModel, patched.resolved?.modelProvider))
: resolveServerChatModelValue(patchedModel, patched.resolved?.modelProvider);
return {
content: `Model set to \`${args.trim()}\`.`,
action: "refresh",