From be20eebc21692ac9e1113a0104af8e2ad70cbeae Mon Sep 17 00:00:00 2001 From: HCL Date: Tue, 24 Mar 2026 07:05:11 +0800 Subject: [PATCH] fix(ui): resolve model provider from catalog instead of stale session default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: HCL --- ui/src/ui/app-render.helpers.ts | 25 ++++++++++++++++++++++-- ui/src/ui/chat/slash-command-executor.ts | 12 +++++++----- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/ui/src/ui/app-render.helpers.ts b/ui/src/ui/app-render.helpers.ts index 2f41a25fa8d..e69658a0e20 100644 --- a/ui/src/ui/app-render.helpers.ts +++ b/ui/src/ui/app-render.helpers.ts @@ -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( diff --git a/ui/src/ui/chat/slash-command-executor.ts b/ui/src/ui/chat/slash-command-executor.ts index b1d06d5e2b2..55e8b4b3182 100644 --- a/ui/src/ui/chat/slash-command-executor.ts +++ b/ui/src/ui/chat/slash-command-executor.ts @@ -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",