Skip to content

Commit d4aeca4

Browse files
authored
Fix dynamic provider model validation to prevent cross-contamination (#9054)
1 parent 3eee340 commit d4aeca4

File tree

3 files changed

+231
-79
lines changed

3 files changed

+231
-79
lines changed

packages/types/src/providers/index.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,121 @@ export * from "./vercel-ai-gateway.js"
3131
export * from "./zai.js"
3232
export * from "./deepinfra.js"
3333
export * from "./minimax.js"
34+
35+
import { anthropicDefaultModelId } from "./anthropic.js"
36+
import { bedrockDefaultModelId } from "./bedrock.js"
37+
import { cerebrasDefaultModelId } from "./cerebras.js"
38+
import { chutesDefaultModelId } from "./chutes.js"
39+
import { claudeCodeDefaultModelId } from "./claude-code.js"
40+
import { deepSeekDefaultModelId } from "./deepseek.js"
41+
import { doubaoDefaultModelId } from "./doubao.js"
42+
import { featherlessDefaultModelId } from "./featherless.js"
43+
import { fireworksDefaultModelId } from "./fireworks.js"
44+
import { geminiDefaultModelId } from "./gemini.js"
45+
import { glamaDefaultModelId } from "./glama.js"
46+
import { groqDefaultModelId } from "./groq.js"
47+
import { ioIntelligenceDefaultModelId } from "./io-intelligence.js"
48+
import { litellmDefaultModelId } from "./lite-llm.js"
49+
import { mistralDefaultModelId } from "./mistral.js"
50+
import { moonshotDefaultModelId } from "./moonshot.js"
51+
import { openRouterDefaultModelId } from "./openrouter.js"
52+
import { qwenCodeDefaultModelId } from "./qwen-code.js"
53+
import { requestyDefaultModelId } from "./requesty.js"
54+
import { rooDefaultModelId } from "./roo.js"
55+
import { sambaNovaDefaultModelId } from "./sambanova.js"
56+
import { unboundDefaultModelId } from "./unbound.js"
57+
import { vertexDefaultModelId } from "./vertex.js"
58+
import { vscodeLlmDefaultModelId } from "./vscode-llm.js"
59+
import { xaiDefaultModelId } from "./xai.js"
60+
import { vercelAiGatewayDefaultModelId } from "./vercel-ai-gateway.js"
61+
import { internationalZAiDefaultModelId, mainlandZAiDefaultModelId } from "./zai.js"
62+
import { deepInfraDefaultModelId } from "./deepinfra.js"
63+
import { minimaxDefaultModelId } from "./minimax.js"
64+
65+
// Import the ProviderName type from provider-settings to avoid duplication
66+
import type { ProviderName } from "../provider-settings.js"
67+
68+
/**
69+
* Get the default model ID for a given provider.
70+
* This function returns only the provider's default model ID, without considering user configuration.
71+
* Used as a fallback when provider models are still loading.
72+
*/
73+
export function getProviderDefaultModelId(
74+
provider: ProviderName,
75+
options: { isChina?: boolean } = { isChina: false },
76+
): string {
77+
switch (provider) {
78+
case "openrouter":
79+
return openRouterDefaultModelId
80+
case "requesty":
81+
return requestyDefaultModelId
82+
case "glama":
83+
return glamaDefaultModelId
84+
case "unbound":
85+
return unboundDefaultModelId
86+
case "litellm":
87+
return litellmDefaultModelId
88+
case "xai":
89+
return xaiDefaultModelId
90+
case "groq":
91+
return groqDefaultModelId
92+
case "huggingface":
93+
return "meta-llama/Llama-3.3-70B-Instruct"
94+
case "chutes":
95+
return chutesDefaultModelId
96+
case "bedrock":
97+
return bedrockDefaultModelId
98+
case "vertex":
99+
return vertexDefaultModelId
100+
case "gemini":
101+
return geminiDefaultModelId
102+
case "deepseek":
103+
return deepSeekDefaultModelId
104+
case "doubao":
105+
return doubaoDefaultModelId
106+
case "moonshot":
107+
return moonshotDefaultModelId
108+
case "minimax":
109+
return minimaxDefaultModelId
110+
case "zai":
111+
return options?.isChina ? mainlandZAiDefaultModelId : internationalZAiDefaultModelId
112+
case "openai-native":
113+
return "gpt-4o" // Based on openai-native patterns
114+
case "mistral":
115+
return mistralDefaultModelId
116+
case "openai":
117+
return "" // OpenAI provider uses custom model configuration
118+
case "ollama":
119+
return "" // Ollama uses dynamic model selection
120+
case "lmstudio":
121+
return "" // LMStudio uses dynamic model selection
122+
case "deepinfra":
123+
return deepInfraDefaultModelId
124+
case "vscode-lm":
125+
return vscodeLlmDefaultModelId
126+
case "claude-code":
127+
return claudeCodeDefaultModelId
128+
case "cerebras":
129+
return cerebrasDefaultModelId
130+
case "sambanova":
131+
return sambaNovaDefaultModelId
132+
case "fireworks":
133+
return fireworksDefaultModelId
134+
case "featherless":
135+
return featherlessDefaultModelId
136+
case "io-intelligence":
137+
return ioIntelligenceDefaultModelId
138+
case "roo":
139+
return rooDefaultModelId
140+
case "qwen-code":
141+
return qwenCodeDefaultModelId
142+
case "vercel-ai-gateway":
143+
return vercelAiGatewayDefaultModelId
144+
case "anthropic":
145+
case "gemini-cli":
146+
case "human-relay":
147+
case "fake-ai":
148+
default:
149+
return anthropicDefaultModelId
150+
}
151+
}

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

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ describe("useSelectedModel", () => {
9393
})
9494
})
9595

96-
it("should use only specific provider info when base model info is missing", () => {
96+
it("should fall back to default when configured model doesn't exist in available models", () => {
9797
const specificProviderInfo: ModelInfo = {
9898
maxTokens: 8192,
9999
contextWindow: 16384,
@@ -106,7 +106,18 @@ describe("useSelectedModel", () => {
106106

107107
mockUseRouterModels.mockReturnValue({
108108
data: {
109-
openrouter: {},
109+
openrouter: {
110+
"anthropic/claude-sonnet-4.5": {
111+
maxTokens: 8192,
112+
contextWindow: 200_000,
113+
supportsImages: true,
114+
supportsPromptCache: true,
115+
inputPrice: 3.0,
116+
outputPrice: 15.0,
117+
cacheWritesPrice: 3.75,
118+
cacheReadsPrice: 0.3,
119+
},
120+
},
110121
requesty: {},
111122
glama: {},
112123
unbound: {},
@@ -127,15 +138,29 @@ describe("useSelectedModel", () => {
127138

128139
const apiConfiguration: ProviderSettings = {
129140
apiProvider: "openrouter",
130-
openRouterModelId: "test-model",
141+
openRouterModelId: "test-model", // This model doesn't exist in available models
131142
openRouterSpecificProvider: "test-provider",
132143
}
133144

134145
const wrapper = createWrapper()
135146
const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper })
136147

137-
expect(result.current.id).toBe("test-model")
138-
expect(result.current.info).toEqual(specificProviderInfo)
148+
// Should fall back to provider default since "test-model" doesn't exist
149+
expect(result.current.id).toBe("anthropic/claude-sonnet-4.5")
150+
// Should still use specific provider info for the default model if specified
151+
expect(result.current.info).toEqual({
152+
...{
153+
maxTokens: 8192,
154+
contextWindow: 200_000,
155+
supportsImages: true,
156+
supportsPromptCache: true,
157+
inputPrice: 3.0,
158+
outputPrice: 15.0,
159+
cacheWritesPrice: 3.75,
160+
cacheReadsPrice: 0.3,
161+
},
162+
...specificProviderInfo,
163+
})
139164
})
140165

141166
it("should demonstrate the merging behavior validates the comment about missing fields", () => {
@@ -244,12 +269,12 @@ describe("useSelectedModel", () => {
244269
expect(result.current.info).toEqual(baseModelInfo)
245270
})
246271

247-
it("should fall back to default when both base and specific provider info are missing", () => {
272+
it("should fall back to default when configured model and provider don't exist", () => {
248273
mockUseRouterModels.mockReturnValue({
249274
data: {
250275
openrouter: {
251-
"anthropic/claude-sonnet-4": {
252-
// Default model
276+
"anthropic/claude-sonnet-4.5": {
277+
// Default model - using correct default model name
253278
maxTokens: 8192,
254279
contextWindow: 200_000,
255280
supportsImages: true,
@@ -285,8 +310,19 @@ describe("useSelectedModel", () => {
285310
const wrapper = createWrapper()
286311
const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper })
287312

288-
expect(result.current.id).toBe("non-existent-model")
289-
expect(result.current.info).toBeUndefined()
313+
// Should fall back to provider default since "non-existent-model" doesn't exist
314+
expect(result.current.id).toBe("anthropic/claude-sonnet-4.5")
315+
// Should use base model info since provider doesn't exist
316+
expect(result.current.info).toEqual({
317+
maxTokens: 8192,
318+
contextWindow: 200_000,
319+
supportsImages: true,
320+
supportsPromptCache: true,
321+
inputPrice: 3.0,
322+
outputPrice: 15.0,
323+
cacheWritesPrice: 3.75,
324+
cacheReadsPrice: 0.3,
325+
})
290326
})
291327
})
292328

0 commit comments

Comments
 (0)