Skip to content

Commit 0fd06e3

Browse files
committed
fix(ui): keep provider-qualified model picker refs intact
1 parent 91b9be1 commit 0fd06e3

3 files changed

Lines changed: 148 additions & 16 deletions

File tree

ui/src/ui/views/agents-utils.ts

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -550,23 +550,45 @@ function resolveConfiguredModels(
550550
): ConfiguredModelOption[] {
551551
const cfg = configForm as ConfigSnapshot | null;
552552
const models = cfg?.agents?.defaults?.models;
553-
if (!models || typeof models !== "object") {
554-
return [];
553+
const labels = new Map<string, string>();
554+
555+
if (models && typeof models === "object") {
556+
for (const [modelId, modelRaw] of Object.entries(models)) {
557+
const trimmed = modelId.trim();
558+
if (!trimmed) {
559+
continue;
560+
}
561+
const alias =
562+
modelRaw && typeof modelRaw === "object" && "alias" in modelRaw
563+
? typeof (modelRaw as { alias?: unknown }).alias === "string"
564+
? (modelRaw as { alias?: string }).alias?.trim()
565+
: undefined
566+
: undefined;
567+
const key = trimmed.toLowerCase();
568+
labels.set(key, alias && alias !== trimmed ? `${alias} (${trimmed})` : trimmed);
569+
}
555570
}
571+
556572
const options: ConfiguredModelOption[] = [];
557-
for (const [modelId, modelRaw] of Object.entries(models)) {
558-
const trimmed = modelId.trim();
573+
const seen = new Set<string>();
574+
const addOption = (value: string) => {
575+
const trimmed = value.trim();
559576
if (!trimmed) {
560-
continue;
577+
return;
578+
}
579+
const key = trimmed.toLowerCase();
580+
if (seen.has(key)) {
581+
return;
561582
}
562-
const alias =
563-
modelRaw && typeof modelRaw === "object" && "alias" in modelRaw
564-
? typeof (modelRaw as { alias?: unknown }).alias === "string"
565-
? (modelRaw as { alias?: string }).alias?.trim()
566-
: undefined
567-
: undefined;
568-
const label = alias && alias !== trimmed ? `${alias} (${trimmed})` : trimmed;
569-
options.push({ value: trimmed, label });
583+
seen.add(key);
584+
options.push({ value: trimmed, label: labels.get(key) ?? trimmed });
585+
};
586+
587+
// Reuse the broader configured-model collector so the overview picker keeps
588+
// provider-qualified primary/fallback refs visible even when they are not in
589+
// the alias map.
590+
for (const modelId of resolveConfiguredCronModelSuggestions(configForm)) {
591+
addOption(modelId);
570592
}
571593
return options;
572594
}

ui/src/ui/views/agents.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,4 +171,59 @@ describe("renderAgents", () => {
171171

172172
expect(skillsTab?.textContent?.trim()).toContain("1");
173173
});
174+
175+
it("lists configured provider-qualified model ids in the overview picker", async () => {
176+
const container = document.createElement("div");
177+
render(
178+
renderAgents(
179+
createProps({
180+
config: {
181+
form: {
182+
agents: {
183+
defaults: {
184+
model: {
185+
primary: "minimax-cn/MiniMax-M2.7",
186+
fallbacks: ["google/gemini-3-flash-preview"],
187+
},
188+
models: {
189+
"custom-dashscope-aliyuncs-com/qwen3.5-flash": { alias: "bailian" },
190+
},
191+
},
192+
list: [{ id: "beta" }],
193+
},
194+
},
195+
loading: false,
196+
saving: false,
197+
dirty: false,
198+
},
199+
}),
200+
),
201+
container,
202+
);
203+
await Promise.resolve();
204+
205+
const modelSelect = container.querySelector<HTMLSelectElement>(".agent-model-select select");
206+
expect(modelSelect).not.toBeNull();
207+
208+
const options = Array.from(modelSelect?.querySelectorAll("option") ?? []).map((option) => ({
209+
value: option.value,
210+
label: option.textContent?.trim(),
211+
}));
212+
213+
expect(options).toEqual(
214+
expect.arrayContaining([
215+
{ value: "", label: "Inherit default (minimax-cn/MiniMax-M2.7)" },
216+
{
217+
value: "custom-dashscope-aliyuncs-com/qwen3.5-flash",
218+
label: "bailian (custom-dashscope-aliyuncs-com/qwen3.5-flash)",
219+
},
220+
{ value: "google/gemini-3-flash-preview", label: "google/gemini-3-flash-preview" },
221+
{ value: "minimax-cn/MiniMax-M2.7", label: "minimax-cn/MiniMax-M2.7" },
222+
]),
223+
);
224+
expect(options).not.toContainEqual({
225+
value: "minimax-cn/MiniMax-M2.7",
226+
label: "Current (minimax-cn/MiniMax-M2.7)",
227+
});
228+
});
174229
});

ui/src/ui/views/chat.test.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,18 @@ function createSessions(): SessionsListResult {
2525
function createChatHeaderState(
2626
overrides: {
2727
model?: string | null;
28+
modelProvider?: string | null;
29+
defaultModel?: string | null;
30+
defaultModelProvider?: string | null;
2831
models?: ModelCatalogEntry[];
2932
omitSessionFromList?: boolean;
3033
} = {},
3134
): { state: AppViewState; request: ReturnType<typeof vi.fn> } {
35+
const defaultModel = overrides.defaultModel ?? "gpt-5";
36+
const defaultModelProvider = defaultModel ? (overrides.defaultModelProvider ?? "openai") : null;
3237
let currentModel = overrides.model ?? null;
33-
let currentModelProvider = currentModel ? "openai" : null;
38+
let currentModelProvider =
39+
overrides.modelProvider ?? (currentModel ? defaultModelProvider : null);
3440
const omitSessionFromList = overrides.omitSessionFromList ?? false;
3541
const catalog = overrides.models ?? [
3642
{ id: "gpt-5", name: "GPT-5", provider: "openai" },
@@ -68,7 +74,7 @@ function createChatHeaderState(
6874
ts: 0,
6975
path: "",
7076
count: omitSessionFromList ? 0 : 1,
71-
defaults: { modelProvider: "openai", model: "gpt-5", contextTokens: null },
77+
defaults: { modelProvider: defaultModelProvider, model: defaultModel, contextTokens: null },
7278
sessions: omitSessionFromList
7379
? []
7480
: [
@@ -95,7 +101,7 @@ function createChatHeaderState(
95101
ts: 0,
96102
path: "",
97103
count: omitSessionFromList ? 0 : 1,
98-
defaults: { modelProvider: "openai", model: "gpt-5", contextTokens: null },
104+
defaults: { modelProvider: defaultModelProvider, model: defaultModel, contextTokens: null },
99105
sessions: omitSessionFromList
100106
? []
101107
: [
@@ -767,6 +773,55 @@ describe("chat view", () => {
767773
vi.unstubAllGlobals();
768774
});
769775

776+
it("keeps provider-qualified model refs when switching across providers from the chat header picker", async () => {
777+
vi.stubGlobal(
778+
"fetch",
779+
vi.fn().mockResolvedValue({
780+
ok: false,
781+
} satisfies Partial<Response>),
782+
);
783+
const { state, request } = createChatHeaderState({
784+
defaultModel: "MiniMax-M2.7",
785+
defaultModelProvider: "minimax-cn",
786+
models: [
787+
{ id: "MiniMax-M2.7", name: "MiniMax M2.7", provider: "minimax-cn" },
788+
{
789+
id: "qwen3.5-flash",
790+
name: "Qwen 3.5 Flash",
791+
provider: "custom-dashscope-aliyuncs-com",
792+
},
793+
{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview", provider: "google" },
794+
],
795+
});
796+
const container = document.createElement("div");
797+
render(renderChatSessionSelect(state), container);
798+
799+
const modelSelect = container.querySelector<HTMLSelectElement>(
800+
'select[data-chat-model-select="true"]',
801+
);
802+
expect(modelSelect).not.toBeNull();
803+
804+
const optionValues = Array.from(modelSelect?.querySelectorAll("option") ?? []).map(
805+
(option) => option.value,
806+
);
807+
expect(optionValues).toContain("custom-dashscope-aliyuncs-com/qwen3.5-flash");
808+
expect(optionValues).toContain("google/gemini-3-flash-preview");
809+
expect(optionValues).not.toContain("qwen3.5-flash");
810+
expect(optionValues).not.toContain("gemini-3-flash-preview");
811+
812+
modelSelect!.value = "custom-dashscope-aliyuncs-com/qwen3.5-flash";
813+
modelSelect!.dispatchEvent(new Event("change", { bubbles: true }));
814+
await flushTasks();
815+
816+
expect(request).toHaveBeenCalledWith("sessions.patch", {
817+
key: "main",
818+
model: "custom-dashscope-aliyuncs-com/qwen3.5-flash",
819+
});
820+
expect(state.sessionsResult?.sessions[0]?.model).toBe("qwen3.5-flash");
821+
expect(state.sessionsResult?.sessions[0]?.modelProvider).toBe("custom-dashscope-aliyuncs-com");
822+
vi.unstubAllGlobals();
823+
});
824+
770825
it("clears the session model override back to the default model", async () => {
771826
vi.stubGlobal(
772827
"fetch",

0 commit comments

Comments
 (0)