Skip to content

Commit ffb2bcc

Browse files
committed
fix(webview): keep an unlisted IO Intelligence model id as the picker selection
A custom id stored through ModelPicker's "Use custom model" path was replaced by the default id in useSelectedModel when the fetched catalog did not list it, so the picker displayed a different model from the one the handler sends requests with (RouterProvider.getModel resolves modelId || defaultModelId). Preserve the configured id and only fall back the metadata, mirroring the litellm and kimi-code cases. Covered by a hook test and a real-ModelPicker selection test.
1 parent 9802e78 commit ffb2bcc

3 files changed

Lines changed: 116 additions & 6 deletions

File tree

‎webview-ui/src/components/settings/__tests__/ModelPicker.spec.tsx‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { QueryClient } from "@tanstack/react-query"
66
import { type Mock } from "vitest"
77

88
import {
9+
ioIntelligenceDefaultModelId,
910
litellmDefaultModelId,
1011
type ModelInfo,
1112
type ProviderSettings,
@@ -364,4 +365,84 @@ describe("ModelPicker", () => {
364365
expect(screen.getByTestId("model-picker-button")).not.toHaveTextContent(litellmDefaultModelId)
365366
})
366367
})
368+
369+
describe("IO Intelligence custom model selection", () => {
370+
const ioIntelligenceModels: Record<string, ModelInfo> = {
371+
"meta-llama/Llama-3.3-70B-Instruct": { description: "Catalog model", ...modelInfo },
372+
}
373+
374+
const renderIOIntelligencePicker = (apiConfiguration: ProviderSettings, setField: SetApiConfigurationField) =>
375+
renderWithExtensionState(
376+
<ModelPicker
377+
apiConfiguration={apiConfiguration}
378+
defaultModelId={ioIntelligenceDefaultModelId}
379+
models={ioIntelligenceModels}
380+
modelIdKey="ioIntelligenceModelId"
381+
serviceName="IO Intelligence"
382+
serviceUrl="https://io.net/intelligence"
383+
setApiConfigurationField={setField}
384+
organizationAllowList={{ allowAll: true, providers: {} }}
385+
/>,
386+
{ queryClient },
387+
)
388+
389+
beforeEach(() => {
390+
mockUseRouterModels.mockReturnValue(createRouterModelsResult({ "io-intelligence": ioIntelligenceModels }))
391+
})
392+
393+
it("keeps a custom model ID in the picker instead of reverting to the default", async () => {
394+
// The stored id is what the handler sends requests with, so the picker
395+
// must keep displaying it even though the fetched catalog lacks it.
396+
const customModelId = "custom-org/custom-model"
397+
let apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.ioIntelligence }
398+
const setField = vi.fn(function <K extends keyof ProviderSettings>(field: K, value: ProviderSettings[K]) {
399+
apiConfiguration = { ...apiConfiguration, [field]: value }
400+
})
401+
402+
const { rerender } = await act(async () => {
403+
return renderIOIntelligencePicker(apiConfiguration, setField)
404+
})
405+
406+
expect(screen.getByTestId("model-picker-button")).toHaveTextContent(ioIntelligenceDefaultModelId)
407+
408+
await act(async () => {
409+
fireEvent.click(screen.getByTestId("model-picker-button"))
410+
})
411+
await act(async () => {
412+
vi.advanceTimersByTime(100)
413+
})
414+
await act(async () => {
415+
fireEvent.input(screen.getByTestId("model-input"), { target: { value: customModelId } })
416+
})
417+
await act(async () => {
418+
vi.advanceTimersByTime(100)
419+
})
420+
await act(async () => {
421+
fireEvent.click(screen.getByTestId("use-custom-model"))
422+
})
423+
await act(async () => {
424+
vi.advanceTimersByTime(100)
425+
})
426+
427+
expect(setField).toHaveBeenCalledWith("ioIntelligenceModelId", customModelId)
428+
429+
await act(async () => {
430+
rerender(
431+
<ModelPicker
432+
apiConfiguration={apiConfiguration}
433+
defaultModelId={ioIntelligenceDefaultModelId}
434+
models={ioIntelligenceModels}
435+
modelIdKey="ioIntelligenceModelId"
436+
serviceName="IO Intelligence"
437+
serviceUrl="https://io.net/intelligence"
438+
setApiConfigurationField={setField}
439+
organizationAllowList={{ allowAll: true, providers: {} }}
440+
/>,
441+
)
442+
})
443+
444+
expect(screen.getByTestId("model-picker-button")).toHaveTextContent(customModelId)
445+
expect(screen.getByTestId("model-picker-button")).not.toHaveTextContent(ioIntelligenceDefaultModelId)
446+
})
447+
})
367448
})

‎webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1163,14 +1163,43 @@ describe("useSelectedModel", () => {
11631163
() =>
11641164
useSelectedModel({
11651165
apiProvider: providerIdentifiers.ioIntelligence,
1166-
ioIntelligenceModelId: "missing-model",
1166+
// ioIntelligenceModelId intentionally omitted
11671167
}),
11681168
{ wrapper: createWrapper() },
11691169
)
11701170

11711171
expect(result.current.id).toBe(ioIntelligenceDefaultModelId)
11721172
expect(result.current.info).toEqual(ioIntelligenceDefaultModelInfo)
11731173
})
1174+
1175+
it("preserves a configured model ID that is absent from the fetched catalog", () => {
1176+
mockUseRouterModels.mockReturnValue(
1177+
createRouterModelsResult({
1178+
"io-intelligence": {
1179+
"meta-llama/Llama-3.3-70B-Instruct": {
1180+
maxTokens: 8_192,
1181+
contextWindow: 131_072,
1182+
supportsPromptCache: false,
1183+
},
1184+
},
1185+
}),
1186+
)
1187+
mockUseOpenRouterModelProviders.mockReturnValue(createOpenRouterModelProvidersResult({}))
1188+
1189+
const { result } = renderHook(
1190+
() =>
1191+
useSelectedModel({
1192+
apiProvider: providerIdentifiers.ioIntelligence,
1193+
ioIntelligenceModelId: "custom-org/custom-model",
1194+
}),
1195+
{ wrapper: createWrapper() },
1196+
)
1197+
1198+
// The stored id is what requests are sent with, so the picker must show it
1199+
// rather than the default; only the metadata falls back.
1200+
expect(result.current.id).toBe("custom-org/custom-model")
1201+
expect(result.current.info).toEqual(ioIntelligenceDefaultModelInfo)
1202+
})
11741203
})
11751204

11761205
describe("openai provider", () => {

‎webview-ui/src/components/ui/hooks/useSelectedModel.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -453,11 +453,11 @@ function getSelectedModel({
453453
return { id, info }
454454
}
455455
case providerIdentifiers.ioIntelligence: {
456-
const id = getValidatedModelId(
457-
apiConfiguration.ioIntelligenceModelId,
458-
routerModels[providerIdentifiers.ioIntelligence],
459-
defaultModelId,
460-
)
456+
// A configured id is the user's explicit selection (ModelPicker's
457+
// "Use custom model" path stores ids absent from the fetched catalog)
458+
// and the handler sends requests with it, so keep it instead of
459+
// displaying a default model that requests do not use.
460+
const id = apiConfiguration.ioIntelligenceModelId || defaultModelId
461461
const info = routerModels[providerIdentifiers.ioIntelligence]?.[id] ?? ioIntelligenceDefaultModelInfo
462462
return { id, info }
463463
}

0 commit comments

Comments
 (0)