Skip to content

Commit 1290009

Browse files
committed
fix(webview): show sane defaults for custom OpenAI-compatible models
Custom model IDs for Fireworks, Baseten and SambaNova resolved to undefined model info in the webview, so the task header fell back to a context window of 1. Mirror the backend BaseOpenAiCompatibleProvider getModel(): honor the configured custom ID with openAiModelInfoSaneDefaults, use catalog metadata for known models, and fall back to the provider default only when no model is configured.
1 parent 09e7326 commit 1290009

2 files changed

Lines changed: 136 additions & 15 deletions

File tree

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

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ import {
2727
minimaxModels,
2828
friendliDefaultModelId,
2929
friendliModels,
30+
fireworksDefaultModelId,
31+
fireworksModels,
32+
basetenDefaultModelId,
33+
basetenModels,
34+
sambaNovaDefaultModelId,
35+
sambaNovaModels,
3036
deepSeekDefaultModelId,
3137
deepSeekModels,
3238
openRouterDefaultModelId,
@@ -1490,6 +1496,109 @@ describe("useSelectedModel", () => {
14901496
})
14911497
})
14921498

1499+
describe("OpenAI-compatible static catalog providers (fireworks, baseten, sambanova)", () => {
1500+
beforeEach(() => {
1501+
mockUseRouterModels.mockReturnValue(createRouterModelsResult({ openrouter: {}, requesty: {}, litellm: {} }))
1502+
mockUseOpenRouterModelProviders.mockReturnValue(createOpenRouterModelProvidersResult({}))
1503+
})
1504+
1505+
it("returns the default Fireworks model when no model is configured", () => {
1506+
const { result } = renderHook(() => useSelectedModel({ apiProvider: providerIdentifiers.fireworks }), {
1507+
wrapper: createWrapper(),
1508+
})
1509+
1510+
expect(result.current.id).toBe(fireworksDefaultModelId)
1511+
expect(result.current.info).toEqual(fireworksModels[fireworksDefaultModelId])
1512+
})
1513+
1514+
it("returns known Fireworks model metadata", () => {
1515+
const id = "accounts/fireworks/models/kimi-k2-instruct"
1516+
const { result } = renderHook(
1517+
() => useSelectedModel({ apiProvider: providerIdentifiers.fireworks, apiModelId: id }),
1518+
{ wrapper: createWrapper() },
1519+
)
1520+
1521+
expect(result.current.id).toBe(id)
1522+
expect(result.current.info).toEqual(fireworksModels[id])
1523+
})
1524+
1525+
const staticCatalogCases = [
1526+
{
1527+
apiProvider: providerIdentifiers.fireworks,
1528+
defaultId: fireworksDefaultModelId,
1529+
models: fireworksModels as Record<string, ModelInfo>,
1530+
knownId: "accounts/fireworks/models/kimi-k2-instruct",
1531+
},
1532+
{
1533+
apiProvider: providerIdentifiers.baseten,
1534+
defaultId: basetenDefaultModelId,
1535+
models: basetenModels as Record<string, ModelInfo>,
1536+
knownId: Object.keys(basetenModels).find((id) => id !== basetenDefaultModelId)!,
1537+
},
1538+
{
1539+
apiProvider: providerIdentifiers.sambanova,
1540+
defaultId: sambaNovaDefaultModelId,
1541+
models: sambaNovaModels as Record<string, ModelInfo>,
1542+
knownId: Object.keys(sambaNovaModels).find((id) => id !== sambaNovaDefaultModelId)!,
1543+
},
1544+
]
1545+
1546+
it.each(staticCatalogCases)(
1547+
"returns the default $apiProvider model when no model is configured",
1548+
({ apiProvider, defaultId, models }) => {
1549+
const { result } = renderHook(() => useSelectedModel({ apiProvider }), { wrapper: createWrapper() })
1550+
1551+
expect(result.current.provider).toBe(apiProvider)
1552+
expect(result.current.id).toBe(defaultId)
1553+
expect(result.current.info).toEqual(models[defaultId])
1554+
expect(result.current.info).toBeDefined()
1555+
},
1556+
)
1557+
1558+
it.each(staticCatalogCases)(
1559+
"falls back to the default $apiProvider model when apiModelId is an empty string",
1560+
({ apiProvider, defaultId, models }) => {
1561+
const { result } = renderHook(() => useSelectedModel({ apiProvider, apiModelId: "" }), {
1562+
wrapper: createWrapper(),
1563+
})
1564+
1565+
expect(result.current.id).toBe(defaultId)
1566+
expect(result.current.info).toEqual(models[defaultId])
1567+
expect(result.current.info).toBeDefined()
1568+
},
1569+
)
1570+
1571+
it.each(staticCatalogCases)(
1572+
"returns known $apiProvider catalog metadata",
1573+
({ apiProvider, models, knownId }) => {
1574+
expect(knownId).toBeDefined()
1575+
const { result } = renderHook(() => useSelectedModel({ apiProvider, apiModelId: knownId }), {
1576+
wrapper: createWrapper(),
1577+
})
1578+
1579+
expect(result.current.provider).toBe(apiProvider)
1580+
expect(result.current.id).toBe(knownId)
1581+
expect(result.current.info).toEqual(models[knownId])
1582+
},
1583+
)
1584+
1585+
it.each([providerIdentifiers.fireworks, providerIdentifiers.baseten, providerIdentifiers.sambanova])(
1586+
"honors a custom %s model ID with sane default metadata (not undefined info)",
1587+
(apiProvider) => {
1588+
// Regression: custom models had undefined info, so the task header fell back to a
1589+
// context window of 1. The backend uses openAiModelInfoSaneDefaults, so the UI must too.
1590+
const { result } = renderHook(
1591+
() => useSelectedModel({ apiProvider, apiModelId: "some/custom-model-not-in-list" }),
1592+
{ wrapper: createWrapper() },
1593+
)
1594+
1595+
expect(result.current.id).toBe("some/custom-model-not-in-list")
1596+
expect(result.current.info).toEqual(openAiModelInfoSaneDefaults)
1597+
expect(result.current.info?.contextWindow).toBeGreaterThan(1)
1598+
},
1599+
)
1600+
})
1601+
14931602
describe("Z AI provider", () => {
14941603
it("uses the International Coding catalog when no API line is configured", () => {
14951604
const apiConfiguration: ProviderSettings = {

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

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,27 @@ function getValidatedModelId(
6060
return configuredId && availableModels?.[configuredId] ? configuredId : defaultModelId
6161
}
6262

63+
/**
64+
* Resolves a static-catalog model for providers backed by `BaseOpenAiCompatibleProvider`
65+
* (Fireworks, Baseten, SambaNova). Mirrors the backend `getModel()`: a user-supplied custom
66+
* model ID that isn't in the static list is honored with `openAiModelInfoSaneDefaults`, so the
67+
* UI shows sensible limits (e.g. context window) instead of treating the selection as unknown.
68+
*/
69+
function getStaticCatalogModel(
70+
configuredId: string | undefined,
71+
models: Record<string, ModelInfo>,
72+
defaultModelId: string,
73+
): { id: string; info: ModelInfo | undefined } {
74+
// An empty configured ID means "unset": fall back to the provider default.
75+
if (!configuredId) {
76+
return { id: defaultModelId, info: models[defaultModelId] }
77+
}
78+
if (!Object.prototype.hasOwnProperty.call(models, configuredId)) {
79+
return { id: configuredId, info: { ...openAiModelInfoSaneDefaults } }
80+
}
81+
return { id: configuredId, info: models[configuredId] }
82+
}
83+
6384
/**
6485
* Resolves the model currently selected for the active API provider.
6586
*
@@ -233,11 +254,8 @@ function getSelectedModel({
233254
const info = xaiModels[id as keyof typeof xaiModels]
234255
return info ? { id, info } : { id, info: undefined }
235256
}
236-
case providerIdentifiers.baseten: {
237-
const id = apiConfiguration.apiModelId ?? defaultModelId
238-
const info = basetenModels[id as keyof typeof basetenModels]
239-
return { id, info }
240-
}
257+
case providerIdentifiers.baseten:
258+
return getStaticCatalogModel(apiConfiguration.apiModelId, basetenModels, defaultModelId)
241259
case providerIdentifiers.bedrock: {
242260
const id = apiConfiguration.apiModelId ?? defaultModelId
243261

@@ -430,16 +448,10 @@ function getSelectedModel({
430448
}
431449
return { id, info }
432450
}
433-
case providerIdentifiers.sambanova: {
434-
const id = apiConfiguration.apiModelId ?? defaultModelId
435-
const info = sambaNovaModels[id as keyof typeof sambaNovaModels]
436-
return { id, info }
437-
}
438-
case providerIdentifiers.fireworks: {
439-
const id = apiConfiguration.apiModelId ?? defaultModelId
440-
const info = fireworksModels[id as keyof typeof fireworksModels]
441-
return { id, info }
442-
}
451+
case providerIdentifiers.sambanova:
452+
return getStaticCatalogModel(apiConfiguration.apiModelId, sambaNovaModels, defaultModelId)
453+
case providerIdentifiers.fireworks:
454+
return getStaticCatalogModel(apiConfiguration.apiModelId, fireworksModels, defaultModelId)
443455
case providerIdentifiers.friendli: {
444456
const id = apiConfiguration.apiModelId ?? defaultModelId
445457
const info = friendliModels[id as keyof typeof friendliModels]

0 commit comments

Comments
 (0)