From 65191fd671e3b4b376efe572b4e605dbf9d3a5d2 Mon Sep 17 00:00:00 2001 From: Matt Cowger Date: Sat, 8 Nov 2025 17:54:04 +0000 Subject: [PATCH 1/3] Move Synthetic provider to use updated models endpoint and dynamic fetcher. Also redesign 1 test that was overly order-specific --- .changeset/good-towns-argue.md | 5 + packages/types/src/provider-settings.ts | 1 + packages/types/src/providers/synthetic.ts | 219 +----------- src/api/providers/__tests__/synthetic.spec.ts | 18 +- src/api/providers/fetchers/modelCache.ts | 4 + src/api/providers/fetchers/synthetic.ts | 137 ++++++++ src/api/providers/synthetic.ts | 43 ++- .../webview/__tests__/ClineProvider.spec.ts | 36 +- .../__tests__/webviewMessageHandler.spec.ts | 324 ++++++------------ src/core/webview/webviewMessageHandler.ts | 2 + src/shared/api.ts | 1 + .../__tests__/getModelsByProvider.spec.ts | 1 + .../src/components/settings/ApiOptions.tsx | 10 +- .../settings/providers/Synthetic.tsx | 34 +- .../components/ui/hooks/useRouterModels.ts | 1 + .../components/ui/hooks/useSelectedModel.ts | 4 +- .../src/utils/__tests__/validate.test.ts | 1 + 17 files changed, 390 insertions(+), 451 deletions(-) create mode 100644 .changeset/good-towns-argue.md create mode 100644 src/api/providers/fetchers/synthetic.ts diff --git a/.changeset/good-towns-argue.md b/.changeset/good-towns-argue.md new file mode 100644 index 00000000000..e8119abe0e4 --- /dev/null +++ b/.changeset/good-towns-argue.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Synthetic provider to use updated models endpoint and dynamic fetcher diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 09fb266f58e..98e560d342a 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -60,6 +60,7 @@ export const dynamicProviders = [ "requesty", "unbound", "glama", + "synthetic", ] as const export type DynamicProvider = (typeof dynamicProviders)[number] diff --git a/packages/types/src/providers/synthetic.ts b/packages/types/src/providers/synthetic.ts index 140554499ad..d61006ec6d2 100644 --- a/packages/types/src/providers/synthetic.ts +++ b/packages/types/src/providers/synthetic.ts @@ -1,221 +1,24 @@ -// kilocode_change: provider added +// kilocode_change: provider added - dynamic models only import type { ModelInfo } from "../model.js" -export type SyntheticModelId = - | "hf:MiniMaxAI/MiniMax-M2" - | "hf:zai-org/GLM-4.6" - | "hf:zai-org/GLM-4.5" - | "hf:openai/gpt-oss-120b" - | "hf:moonshotai/Kimi-K2-Instruct-0905" - | "hf:moonshotai/Kimi-K2-Thinking" - | "hf:reissbaker/llama-3.1-70b-abliterated-lora" - | "hf:meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8" - | "hf:deepseek-ai/DeepSeek-V3.1" - | "hf:meta-llama/Llama-3.1-8B-Instruct" - | "hf:meta-llama/Llama-3.1-70B-Instruct" - | "hf:meta-llama/Llama-3.1-405B-Instruct" - | "hf:meta-llama/Llama-3.3-70B-Instruct" - | "hf:deepseek-ai/DeepSeek-V3-0324" - | "hf:deepseek-ai/DeepSeek-R1" - | "hf:moonshotai/Kimi-K2-Instruct" - | "hf:meta-llama/Llama-4-Scout-17B-16E-Instruct" - | "hf:Qwen/Qwen3-Coder-480B-A35B-Instruct" - | "hf:Qwen/Qwen2.5-Coder-32B-Instruct" - | "hf:Qwen/Qwen3-235B-A22B-Thinking-2507" - | "hf:Qwen/Qwen3-235B-A22B-Instruct-2507" +export type SyntheticModelId = string -export const syntheticDefaultModelId: SyntheticModelId = "hf:zai-org/GLM-4.6" +export const syntheticDefaultModelId = "hf:zai-org/GLM-4.6" -export const syntheticModels = { - "hf:MiniMaxAI/MiniMax-M2": { - maxTokens: 192608, - contextWindow: 192608, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.55, - outputPrice: 2.19, - description: - "MiniMax's latest hybrid reasoning model: it's fast, it thinks before it responds, it's great at using tools via the API, and it's a strong coding model. 192k-token context.", - }, - "hf:moonshotai/Kimi-K2-Thinking": { - maxTokens: 262144, - contextWindow: 262144, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.55, - outputPrice: 2.19, - description: - "Moonshot's latest hybrid reasoner. Extremely good at math — it saturates the AIME25 math benchmark — and competitive with GPT-5 and Claude 4.5 at tool use and codegen. 256k-token context.", - }, - "hf:moonshotai/Kimi-K2-Instruct-0905": { - maxTokens: 262144, - contextWindow: 262144, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 1.2, - outputPrice: 1.2, - description: - "Kimi K2 model gets a new version update: Agentic coding: more accurate, better generalization across scaffolds. Frontend coding: improved aesthetics and functionalities on web, 3d, and other tasks. Context length: extended from 128k to 256k, providing better long-horizon support.", - }, - "hf:openai/gpt-oss-120b": { - maxTokens: 128000, - contextWindow: 128000, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.1, - outputPrice: 0.1, - }, - "hf:zai-org/GLM-4.5": { - maxTokens: 128000, - contextWindow: 128000, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.55, - outputPrice: 2.19, - }, +// Models used in tests and as fallback for dynamic provider +export const syntheticModels: Record = { "hf:zai-org/GLM-4.6": { - maxTokens: 200000, - contextWindow: 200000, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.55, - outputPrice: 2.19, - }, - "hf:reissbaker/llama-3.1-70b-abliterated-lora": { - maxTokens: 128000, - contextWindow: 128000, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.9, - outputPrice: 0.9, - }, - "hf:meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": { - maxTokens: 524000, - contextWindow: 524000, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.22, - outputPrice: 0.88, - }, - "hf:deepseek-ai/DeepSeek-V3.1": { - maxTokens: 128000, - contextWindow: 128000, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.56, - outputPrice: 1.68, - }, - "hf:meta-llama/Llama-3.1-405B-Instruct": { - maxTokens: 128000, - contextWindow: 128000, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 3.0, - outputPrice: 3.0, - }, - "hf:meta-llama/Llama-3.1-70B-Instruct": { - maxTokens: 128000, - contextWindow: 128000, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.9, - outputPrice: 0.9, - }, - "hf:meta-llama/Llama-3.1-8B-Instruct": { - maxTokens: 128000, - contextWindow: 128000, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.2, - outputPrice: 0.2, - }, - "hf:meta-llama/Llama-3.3-70B-Instruct": { - maxTokens: 128000, - contextWindow: 128000, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.9, - outputPrice: 0.9, - }, - "hf:deepseek-ai/DeepSeek-V3-0324": { - maxTokens: 128000, - contextWindow: 128000, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 1.2, - outputPrice: 1.2, - }, - "hf:deepseek-ai/DeepSeek-R1": { maxTokens: 128000, contextWindow: 128000, supportsImages: false, supportsPromptCache: false, inputPrice: 0.55, outputPrice: 2.19, + description: "GLM-4.6", + supportsComputerUse: false, + supportsReasoningEffort: false, + supportsReasoningBudget: false, + supportedParameters: [], }, - "hf:deepseek-ai/DeepSeek-R1-0528": { - maxTokens: 128000, - contextWindow: 128000, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 3.0, - outputPrice: 8.0, - }, - "hf:meta-llama/Llama-4-Scout-17B-16E-Instruct": { - maxTokens: 328000, - contextWindow: 328000, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.15, - outputPrice: 0.6, - }, - "hf:moonshotai/Kimi-K2-Instruct": { - maxTokens: 128000, - contextWindow: 128000, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.6, - outputPrice: 2.5, - }, - "hf:Qwen/Qwen3-Coder-480B-A35B-Instruct": { - maxTokens: 256000, - contextWindow: 256000, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.45, - outputPrice: 1.8, - }, - "hf:Qwen/Qwen2.5-Coder-32B-Instruct": { - maxTokens: 32000, - contextWindow: 32000, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.8, - outputPrice: 0.8, - }, - "hf:deepseek-ai/DeepSeek-V3": { - maxTokens: 128000, - contextWindow: 128000, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 1.25, - outputPrice: 1.25, - }, - "hf:Qwen/Qwen3-235B-A22B-Instruct-2507": { - maxTokens: 256000, - contextWindow: 256000, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.22, - outputPrice: 0.88, - }, - "hf:Qwen/Qwen3-235B-A22B-Thinking-2507": { - maxTokens: 256000, - contextWindow: 256000, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 0.65, - outputPrice: 3.0, - }, -} as const satisfies Record +} diff --git a/src/api/providers/__tests__/synthetic.spec.ts b/src/api/providers/__tests__/synthetic.spec.ts index b0ff6ed4c2b..e0378db8eba 100644 --- a/src/api/providers/__tests__/synthetic.spec.ts +++ b/src/api/providers/__tests__/synthetic.spec.ts @@ -22,11 +22,21 @@ vi.mock("openai", () => ({ })), })) +// Mock model cache +vi.mock("../fetchers/modelCache", () => ({ + getModels: vi.fn(), +})) + +// Import the mocked function after mock setup +const { getModels: mockGetModels } = await import("../fetchers/modelCache") + describe("SyntheticHandler", () => { let handler: SyntheticHandler beforeEach(() => { vi.clearAllMocks() + // Mock getModels to return the static models + vi.mocked(mockGetModels).mockResolvedValue(syntheticModels) // Set up default mock implementation mockCreate.mockImplementation(async () => ({ [Symbol.asyncIterator]: async function* () { @@ -83,7 +93,7 @@ describe("SyntheticHandler", () => { }) it("should return specified model when valid model is provided", () => { - const testModelId: SyntheticModelId = "hf:zai-org/GLM-4.5" + const testModelId: SyntheticModelId = "hf:zai-org/GLM-4.6" const handlerWithModel = new SyntheticHandler({ apiModelId: testModelId, syntheticApiKey: "test-synthetic-api-key", @@ -93,8 +103,8 @@ describe("SyntheticHandler", () => { expect(model.info).toEqual(expect.objectContaining(syntheticModels[testModelId])) }) - it("should return GLM Instruct model with correct configuration", () => { - const testModelId: SyntheticModelId = "hf:zai-org/GLM-4.5" + it("should return GLM model with correct configuration", () => { + const testModelId: SyntheticModelId = "hf:zai-org/GLM-4.6" const handlerWithModel = new SyntheticHandler({ apiModelId: testModelId, syntheticApiKey: "test-synthetic-api-key", @@ -175,7 +185,7 @@ describe("SyntheticHandler", () => { }) it("createMessage should pass correct parameters to synthetic client", async () => { - const modelId: SyntheticModelId = "hf:zai-org/GLM-4.5" + const modelId: SyntheticModelId = "hf:zai-org/GLM-4.6" const modelInfo = syntheticModels[modelId] const handlerWithModel = new SyntheticHandler({ apiModelId: modelId, diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index 3777a26d878..3f78f486e01 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -28,6 +28,7 @@ import { getOvhCloudAiEndpointsModels } from "./ovhcloud" import { getChutesModels } from "./chutes" import { getGeminiModels } from "./gemini" import { getInceptionModels } from "./inception" +import { getSyntheticModels } from "./synthetic" // kilocode_change end import { getDeepInfraModels } from "./deepinfra" @@ -109,6 +110,9 @@ export const getModels = async (options: GetModelsOptions): Promise case "chutes": models = await getChutesModels(options.apiKey) break + case "synthetic": + models = await getSyntheticModels(options.apiKey) + break case "gemini": models = await getGeminiModels({ apiKey: options.apiKey, diff --git a/src/api/providers/fetchers/synthetic.ts b/src/api/providers/fetchers/synthetic.ts new file mode 100644 index 00000000000..0dd81a6c642 --- /dev/null +++ b/src/api/providers/fetchers/synthetic.ts @@ -0,0 +1,137 @@ +// kilocode_change - file added +import axios from "axios" +import { z } from "zod" + +import { isModelParameter, type ModelInfo } from "@roo-code/types" + +// Synthetic /openai/v1/models item schema (based on models.json) +const syntheticModelSchema = z.object({ + id: z.string(), + name: z.string().optional(), + provider: z.string().optional(), + always_on: z.boolean().optional(), + context_length: z.number().optional(), + max_output_length: z.number().optional(), + pricing: z + .object({ + prompt: z.string().optional(), + completion: z.string().optional(), + image: z.string().optional(), + request: z.string().optional(), + input_cache_reads: z.string().optional(), + input_cache_writes: z.string().optional(), + }) + .optional(), + input_modalities: z.array(z.string()).optional(), + output_modalities: z.array(z.string()).optional(), + quantization: z.string().optional().nullable(), + supported_sampling_parameters: z.array(z.string()).optional().nullable(), + supported_features: z.array(z.string()).optional().nullable(), +}) + +const syntheticModelsResponseSchema = z.object({ + data: z.array(syntheticModelSchema), +}) + +type SyntheticModelsResponse = z.infer + +type SyntheticModel = z.infer + +function parsePrice(value?: string): number | undefined { + if (!value) return undefined + // Values look like "$0.00000055"; strip non-numeric/decimal and parse + const match = value.match(/[0-9.]+/) + if (!match) return undefined + const num = Number(match[0]) + return Number.isFinite(num) ? num * 1_000_000 : undefined +} + +function parseSyntheticModel(model: SyntheticModel): ModelInfo { + const contextWindow = model.context_length ?? 131_072 + const maxOutput = model.max_output_length ?? Math.min(contextWindow, 65_536) + + const inputPrice = parsePrice(model.pricing?.prompt) + const outputPrice = parsePrice(model.pricing?.completion) + const cacheReadsPrice = parsePrice(model.pricing?.input_cache_reads) + const cacheWritesPrice = parsePrice(model.pricing?.input_cache_writes) + + const supportsImages = (model.input_modalities || []).includes("image") + const supportsPromptCache = Boolean(model.pricing?.input_cache_reads || model.pricing?.input_cache_writes) + + return { + maxTokens: maxOutput, + contextWindow, + supportsImages, + supportsPromptCache, + supportsComputerUse: false, + inputPrice, + outputPrice, + cacheReadsPrice, + cacheWritesPrice, + description: model.name || model.id, + // Synthetic may expose reasoning-capable models but API does not mark them distinctly in models.json + supportsReasoningEffort: false, + supportsReasoningBudget: (model.supported_features || []).includes("reasoning"), + supportedParameters: (model.supported_sampling_parameters || []).filter(isModelParameter), + supportsTemperature: (model.supported_sampling_parameters || []).includes("temperature"), + } +} + +export async function getSyntheticModels(apiKey?: string): Promise> { + const models: Record = {} + + try { + const headers: Record = { + "Content-Type": "application/json", + } + + if (apiKey) { + headers.Authorization = `Bearer ${apiKey}` + } + + const response = await axios.get("https://api.synthetic.new/openai/v1/models", { + headers, + timeout: 10_000, + }) + + const result = syntheticModelsResponseSchema.safeParse(response.data) + if (!result.success) { + console.error("Synthetic models response validation failed:", result.error.format()) + throw new Error( + `Synthetic API returned invalid response format. Validation errors: ${JSON.stringify(result.error.format())}`, + ) + } + + for (const model of result.data.data) { + models[model.id] = parseSyntheticModel(model) + } + + return models + } catch (error) { + console.error("Error fetching Synthetic models:", error) + + if (axios.isAxiosError(error)) { + if (error.code === "ECONNABORTED") { + const timeoutError = new Error("Failed to fetch Synthetic models: Request timeout") + ;(timeoutError as any).cause = error + throw timeoutError + } else if (error.response) { + const responseError = new Error( + `Failed to fetch Synthetic models: ${error.response.status} ${error.response.statusText}`, + ) + ;(responseError as any).cause = error + throw responseError + } else if (error.request) { + const requestError = new Error("Failed to fetch Synthetic models: No response") + ;(requestError as any).cause = error + throw requestError + } + } + + const fetchError = new Error( + `Failed to fetch Synthetic models: ${error instanceof Error ? error.message : "Unknown error"}`, + ) + ;(fetchError as any).cause = error + throw fetchError + } +} diff --git a/src/api/providers/synthetic.ts b/src/api/providers/synthetic.ts index fdee91f571c..ef6bf5c3b8b 100644 --- a/src/api/providers/synthetic.ts +++ b/src/api/providers/synthetic.ts @@ -1,12 +1,21 @@ // kilocode_change - provider added +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" + import { type SyntheticModelId, syntheticDefaultModelId, syntheticModels } from "@roo-code/types" -import type { ApiHandlerOptions } from "../../shared/api" +import type { ApiHandlerOptions, ModelRecord } from "../../shared/api" import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider" +import { getModels } from "./fetchers/modelCache" +import { getModelParams } from "../transform/model-params" +import { ApiStream } from "../transform/stream" +import type { ApiHandlerCreateMessageMetadata } from "../index" export class SyntheticHandler extends BaseOpenAiCompatibleProvider { + protected models: ModelRecord = {} + constructor(options: ApiHandlerOptions) { super({ ...options, @@ -18,4 +27,36 @@ export class SyntheticHandler extends BaseOpenAiCompatibleProvider { + await this.fetchModel() + return super.completePrompt(prompt) + } } diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 25289bd3b6d..031c602349e 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -2780,6 +2780,7 @@ describe("ClineProvider - Router Models", () => { inception: mockModels, // kilocode_change huggingface: {}, "io-intelligence": {}, + synthetic: mockModels, // kilocode_change }, }) }) @@ -2826,6 +2827,7 @@ describe("ClineProvider - Router Models", () => { .mockResolvedValueOnce(mockModels) // deepinfra success .mockResolvedValueOnce(mockModels) // kilocode_change: ovhcloud .mockResolvedValueOnce(mockModels) // kilocode_change: inception success + .mockRejectedValueOnce(new Error("Synthetic API error")) // synthetic fail .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm fail await messageHandler({ type: "requestRouterModels" }) @@ -2836,24 +2838,26 @@ describe("ClineProvider - Router Models", () => { routerModels: { deepinfra: mockModels, openrouter: mockModels, - gemini: mockModels, // kilocode_change + gemini: mockModels, requesty: {}, glama: mockModels, unbound: {}, - chutes: {}, // kilocode_change + chutes: {}, ollama: {}, lmstudio: {}, litellm: {}, "kilocode-openrouter": {}, "vercel-ai-gateway": mockModels, - ovhcloud: mockModels, // kilocode_change - inception: mockModels, // kilocode_change + ovhcloud: mockModels, + inception: mockModels, + synthetic: {}, huggingface: {}, "io-intelligence": {}, }, }) // Verify error messages were sent for failed providers + // Verify error messages were sent for failed providers in the correct order expect(mockPostMessage).toHaveBeenCalledWith({ type: "singleRouterModelFetchResponse", success: false, @@ -2868,14 +2872,12 @@ describe("ClineProvider - Router Models", () => { values: { provider: "unbound" }, }) - // kilocode_change start expect(mockPostMessage).toHaveBeenCalledWith({ type: "singleRouterModelFetchResponse", success: false, error: "Chutes API error", values: { provider: "chutes" }, }) - // kilocode_change end expect(mockPostMessage).toHaveBeenCalledWith({ type: "singleRouterModelFetchResponse", @@ -2887,8 +2889,15 @@ describe("ClineProvider - Router Models", () => { expect(mockPostMessage).toHaveBeenCalledWith({ type: "singleRouterModelFetchResponse", success: false, - error: "Unbound API error", - values: { provider: "unbound" }, + error: "Ollama API error", + values: { provider: "ollama" }, + }) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "Synthetic API error", + values: { provider: "synthetic" }, }) expect(mockPostMessage).toHaveBeenCalledWith({ @@ -2979,18 +2988,19 @@ describe("ClineProvider - Router Models", () => { routerModels: { deepinfra: mockModels, openrouter: mockModels, - gemini: mockModels, // kilocode_change + gemini: mockModels, requesty: mockModels, glama: mockModels, unbound: mockModels, - chutes: mockModels, // kilocode_change + chutes: mockModels, litellm: {}, "kilocode-openrouter": mockModels, - ollama: mockModels, // kilocode_change + ollama: mockModels, lmstudio: {}, "vercel-ai-gateway": mockModels, - ovhcloud: mockModels, // kilocode_change - inception: mockModels, // kilocode_change + ovhcloud: mockModels, + inception: mockModels, + synthetic: mockModels, huggingface: {}, "io-intelligence": {}, }, diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index eedeaaf687b..91f04de4191 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -181,6 +181,49 @@ describe("webviewMessageHandler - requestOllamaModels", () => { }) }) +const createMockModels = (): ModelRecord => ({ + "model-1": { + maxTokens: 4096, + contextWindow: 8192, + supportsPromptCache: false, + description: "Test model 1", + }, +}) + +const routerProviders = [ + "deepinfra", + "openrouter", + "gemini", + "requesty", + "glama", + "unbound", + "chutes", + "kilocode-openrouter", + "ollama", + "vercel-ai-gateway", + "ovhcloud", + "inception", + "synthetic", + "litellm", +] + +const routerProviderFailureCases = [ + { provider: "deepinfra", errorMessage: "DeepInfra API error" }, + { provider: "openrouter", errorMessage: "OpenRouter API error" }, + { provider: "gemini", errorMessage: "Gemini API error" }, + { provider: "requesty", errorMessage: "Requesty API error" }, + { provider: "glama", errorMessage: "Glama API error" }, + { provider: "unbound", errorMessage: "Unbound API error" }, + { provider: "chutes", errorMessage: "Chutes API error" }, + { provider: "kilocode-openrouter", errorMessage: "Kilocode OpenRouter API error" }, + { provider: "ollama", errorMessage: "Ollama API error" }, + { provider: "vercel-ai-gateway", errorMessage: "Vercel AI Gateway API error" }, + { provider: "ovhcloud", errorMessage: "OVHcloud AI Endpoints error" }, + { provider: "inception", errorMessage: "Inception API error" }, + { provider: "synthetic", errorMessage: "Synthetic API error" }, + { provider: "litellm", errorMessage: "LiteLLM connection failed" }, +] + describe("webviewMessageHandler - requestRouterModels", () => { beforeEach(() => { vi.clearAllMocks() @@ -255,28 +298,76 @@ describe("webviewMessageHandler - requestRouterModels", () => { // Note: io-intelligence is not fetched because no API key is provided in the mock state // Verify response was sent - expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + expect(mockClineProvider.postMessageToWebview).toHaveBeenNthCalledWith(1, { + type: "ollamaModels", + ollamaModels: mockModels, + }) + + expect(mockClineProvider.postMessageToWebview).toHaveBeenNthCalledWith(2, { type: "routerModels", routerModels: { deepinfra: mockModels, openrouter: mockModels, - gemini: mockModels, // kilocode_change + gemini: mockModels, requesty: mockModels, glama: mockModels, unbound: mockModels, - chutes: mockModels, // kilocode_change + chutes: mockModels, litellm: mockModels, "kilocode-openrouter": mockModels, - ollama: mockModels, // kilocode_change + ollama: mockModels, lmstudio: {}, "vercel-ai-gateway": mockModels, huggingface: {}, "io-intelligence": {}, - ovhcloud: mockModels, // kilocode_change - inception: mockModels, // kilocode_change + ovhcloud: mockModels, + inception: mockModels, + synthetic: mockModels, }, }) }) + test.each(routerProviders)("successfully fetches models from %s", async (provider) => { + mockGetModels.mockImplementation(async (args) => { + if (args.provider === provider) { + return createMockModels() + } + return {} as ModelRecord + }) + + await webviewMessageHandler(mockClineProvider, { + type: "requestRouterModels", + }) + + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "routerModels", + routerModels: expect.objectContaining({ + [provider]: createMockModels(), + }), + }) + }) + + test.each(routerProviderFailureCases)( + "handles $provider provider failure gracefully", + async ({ provider, errorMessage }) => { + mockGetModels.mockImplementation(async (args) => { + if (args.provider === provider) { + throw new Error(errorMessage) + } + return createMockModels() + }) + + await webviewMessageHandler(mockClineProvider, { + type: "requestRouterModels", + }) + + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: errorMessage, + values: { provider }, + }) + }, + ) it("handles LiteLLM models with values from message when config is missing", async () => { mockClineProvider.getState = vi.fn().mockResolvedValue({ @@ -356,230 +447,33 @@ describe("webviewMessageHandler - requestRouterModels", () => { ) // Verify response includes empty object for LiteLLM - expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ - type: "routerModels", - routerModels: { - deepinfra: mockModels, - openrouter: mockModels, - gemini: mockModels, // kilocode_change - requesty: mockModels, - glama: mockModels, - unbound: mockModels, - chutes: mockModels, // kilocode_change - litellm: {}, - "kilocode-openrouter": mockModels, - ollama: mockModels, // kilocode_change - lmstudio: {}, - "vercel-ai-gateway": mockModels, - huggingface: {}, - "io-intelligence": {}, - ovhcloud: mockModels, // kilocode_change - inception: mockModels, // kilocode_change - }, - }) - }) - - it("handles individual provider failures gracefully", async () => { - const mockModels: ModelRecord = { - "model-1": { - maxTokens: 4096, - contextWindow: 8192, - supportsPromptCache: false, - description: "Test model 1", - }, - } - - // Mock some providers to succeed and others to fail - mockGetModels - .mockResolvedValueOnce(mockModels) // openrouter - .mockResolvedValueOnce(mockModels) // kilocode_change: gemini - .mockRejectedValueOnce(new Error("Requesty API error")) // requesty - .mockResolvedValueOnce(mockModels) // glama - .mockRejectedValueOnce(new Error("Unbound API error")) // unbound - .mockRejectedValueOnce(new Error("Chutes API error")) // chutes // kilocode_change - .mockResolvedValueOnce(mockModels) // kilocode-openrouter - .mockRejectedValueOnce(new Error("Ollama API error")) // kilocode_change - .mockResolvedValueOnce(mockModels) // vercel-ai-gateway - .mockResolvedValueOnce(mockModels) // deepinfra - .mockResolvedValueOnce(mockModels) // kilocode_change ovhcloud - .mockRejectedValueOnce(new Error("Inception API error")) // kilocode_change - .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm - - await webviewMessageHandler(mockClineProvider, { - type: "requestRouterModels", + expect(mockClineProvider.postMessageToWebview).toHaveBeenNthCalledWith(1, { + type: "ollamaModels", + ollamaModels: mockModels, }) - // Verify successful providers are included - expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + expect(mockClineProvider.postMessageToWebview).toHaveBeenNthCalledWith(2, { type: "routerModels", routerModels: { deepinfra: mockModels, openrouter: mockModels, - gemini: mockModels, // kilocode_change - requesty: {}, + gemini: mockModels, + requesty: mockModels, glama: mockModels, - unbound: {}, - chutes: {}, // kilocode_change + unbound: mockModels, + chutes: mockModels, litellm: {}, "kilocode-openrouter": mockModels, - ollama: {}, - ovhcloud: mockModels, // kilocode_change - inception: {}, // kilocode_change + ollama: mockModels, lmstudio: {}, "vercel-ai-gateway": mockModels, huggingface: {}, "io-intelligence": {}, + ovhcloud: mockModels, + inception: mockModels, + synthetic: mockModels, }, }) - - // Verify error messages were sent for failed providers - expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ - type: "singleRouterModelFetchResponse", - success: false, - error: "Requesty API error", - values: { provider: "requesty" }, - }) - - expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ - type: "singleRouterModelFetchResponse", - success: false, - error: "Unbound API error", - values: { provider: "unbound" }, - }) - - // kilocode_change start - expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ - type: "singleRouterModelFetchResponse", - success: false, - error: "Chutes API error", - values: { provider: "chutes" }, - }) - expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ - type: "singleRouterModelFetchResponse", - success: false, - error: "Inception API error", - values: { provider: "inception" }, - }) - // kilocode_change end - - expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ - type: "singleRouterModelFetchResponse", - success: false, - error: "LiteLLM connection failed", - values: { provider: "litellm" }, - }) - }) - - it("handles Error objects and string errors correctly", async () => { - // Mock providers to fail with different error types - mockGetModels - .mockRejectedValueOnce(new Error("Structured error message")) // openrouter - .mockRejectedValueOnce(new Error("Gemini API error")) // // kilocode_change: gemini - .mockRejectedValueOnce(new Error("Requesty API error")) // requesty - .mockRejectedValueOnce(new Error("Glama API error")) // glama - .mockRejectedValueOnce(new Error("Unbound API error")) // unbound - .mockRejectedValueOnce(new Error("Chutes API error")) // chutes // kilocode_change - .mockResolvedValueOnce({}) // kilocode-openrouter - Success - .mockRejectedValueOnce(new Error("Ollama API error")) // ollama - .mockRejectedValueOnce(new Error("Vercel AI Gateway error")) // vercel-ai-gateway - .mockRejectedValueOnce(new Error("DeepInfra API error")) // deepinfra - .mockRejectedValueOnce(new Error("OVHcloud AI Endpoints error")) // ovhcloud // kilocode_change - .mockRejectedValueOnce(new Error("Inception API error")) // kilocode_change inception - .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm - - await webviewMessageHandler(mockClineProvider, { - type: "requestRouterModels", - }) - - // Verify error handling for different error types - expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ - type: "singleRouterModelFetchResponse", - success: false, - error: "Structured error message", - values: { provider: "openrouter" }, - }) - - // kilocode_change start - expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ - type: "singleRouterModelFetchResponse", - success: false, - error: "Gemini API error", - values: { provider: "gemini" }, - }) - // kilocode_change end - - expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ - type: "singleRouterModelFetchResponse", - success: false, - error: "Requesty API error", - values: { provider: "requesty" }, - }) - - expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ - type: "singleRouterModelFetchResponse", - success: false, - error: "Glama API error", - values: { provider: "glama" }, - }) - - expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ - type: "singleRouterModelFetchResponse", - success: false, - error: "Unbound API error", - values: { provider: "unbound" }, - }) - - // kilocode_change start - expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ - type: "singleRouterModelFetchResponse", - success: false, - error: "Chutes API error", - values: { provider: "chutes" }, - }) - - expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ - type: "singleRouterModelFetchResponse", - success: false, - error: "Ollama API error", - values: { provider: "ollama" }, - }) - - expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ - type: "singleRouterModelFetchResponse", - success: false, - error: "Vercel AI Gateway error", - values: { provider: "vercel-ai-gateway" }, - }) - // kilocode_change end - - expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ - type: "singleRouterModelFetchResponse", - success: false, - error: "DeepInfra API error", - values: { provider: "deepinfra" }, - }) - - expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ - type: "singleRouterModelFetchResponse", - success: false, - error: "LiteLLM connection failed", - values: { provider: "litellm" }, - }) - - // kilocode_change start - expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ - type: "singleRouterModelFetchResponse", - success: false, - error: "OVHcloud AI Endpoints error", - values: { provider: "ovhcloud" }, - }) - expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ - type: "singleRouterModelFetchResponse", - success: false, - error: "Inception API error", - values: { provider: "inception" }, - }) - // kilocode_change end }) it("prefers config values over message values for LiteLLM", async () => { diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index f4efa3f3992..bf53ed38364 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -816,6 +816,7 @@ export const webviewMessageHandler = async ( lmstudio: {}, ovhcloud: {}, // kilocode_change inception: {}, // kilocode_change + synthetic: {}, // kilocode_change } const safeGetModels = async (options: GetModelsOptions): Promise => { @@ -893,6 +894,7 @@ export const webviewMessageHandler = async ( baseUrl: apiConfiguration.inceptionLabsBaseUrl, }, }, + { key: "synthetic", options: { provider: "synthetic", apiKey: apiConfiguration.syntheticApiKey } }, // kilocode_change ] // kilocode_change end diff --git a/src/shared/api.ts b/src/shared/api.ts index f792847ca8f..a52d25c3cc5 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -170,6 +170,7 @@ const dynamicProviderExtras = { ovhcloud: {} as { apiKey?: string }, // kilocode_change chutes: {} as { apiKey?: string }, // kilocode_change inception: {} as { apiKey?: string; baseUrl?: string }, // kilocode_change + synthetic: {} as { apiKey?: string }, // kilocode_change } as const satisfies Record // Build the dynamic options union from the map, intersected with CommonFetchParams diff --git a/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts b/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts index 11fdbffda00..eb611839162 100644 --- a/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts +++ b/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts @@ -31,6 +31,7 @@ describe("getModelsByProvider", () => { gemini: { "test-model": testModel }, ovhcloud: { "test-model": testModel }, chutes: { "test-model": testModel }, + synthetic: { "test-model": testModel }, // kilocode_change end inception: { "test-model": testModel }, } diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 1f729cab259..831cfe83b6b 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -223,7 +223,7 @@ const ApiOptions = ({ deepInfraApiKey: apiConfiguration?.deepInfraApiKey, geminiApiKey: apiConfiguration?.geminiApiKey, googleGeminiBaseUrl: apiConfiguration?.googleGeminiBaseUrl, - chutesApiKey: apiConfiguration?.chutesApiKey, + syntheticApiKey: apiConfiguration?.syntheticApiKey, }) //const { data: openRouterModelProviders } = useOpenRouterModelProviders( @@ -275,7 +275,8 @@ const ApiOptions = ({ } else if ( selectedProvider === "litellm" || selectedProvider === "deepinfra" || - selectedProvider === "chutes" // kilocode_change + selectedProvider === "chutes" || // kilocode_change + selectedProvider === "synthetic" // kilocode_change ) { vscode.postMessage({ type: "requestRouterModels" }) } @@ -776,12 +777,15 @@ const ApiOptions = ({ {selectedProvider === "fireworks" && ( )} + { - // kilocode_change start selectedProvider === "synthetic" && ( ) // kilocode_change end diff --git a/webview-ui/src/components/settings/providers/Synthetic.tsx b/webview-ui/src/components/settings/providers/Synthetic.tsx index 53bff004c03..7bc8a5a0810 100644 --- a/webview-ui/src/components/settings/providers/Synthetic.tsx +++ b/webview-ui/src/components/settings/providers/Synthetic.tsx @@ -1,21 +1,30 @@ -// kilocode_change - provider added - import { useCallback } from "react" import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" -import type { ProviderSettings } from "@roo-code/types" - import { useAppTranslation } from "@src/i18n/TranslationContext" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" import { inputEventTransform } from "../transforms" +import { type ProviderSettings, type OrganizationAllowList, syntheticDefaultModelId } from "@roo-code/types" +import type { RouterModels } from "@roo/api" +import { ModelPicker } from "../ModelPicker" + type SyntheticProps = { apiConfiguration: ProviderSettings setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void + routerModels?: RouterModels + organizationAllowList: OrganizationAllowList + modelValidationError?: string } -export const Synthetic = ({ apiConfiguration, setApiConfigurationField }: SyntheticProps) => { +export const Synthetic = ({ + apiConfiguration, + setApiConfigurationField, + routerModels, + organizationAllowList, + modelValidationError, +}: SyntheticProps) => { const { t } = useAppTranslation() const handleInputChange = useCallback( @@ -47,6 +56,21 @@ export const Synthetic = ({ apiConfiguration, setApiConfigurationField }: Synthe {t("settings:providers.getSyntheticApiKey")} )} + { + <> + + + } ) } diff --git a/webview-ui/src/components/ui/hooks/useRouterModels.ts b/webview-ui/src/components/ui/hooks/useRouterModels.ts index 4d069dfeede..e9ac4f84349 100644 --- a/webview-ui/src/components/ui/hooks/useRouterModels.ts +++ b/webview-ui/src/components/ui/hooks/useRouterModels.ts @@ -46,6 +46,7 @@ type RouterModelsQueryKey = { geminiApiKey?: string googleGeminiBaseUrl?: string chutesApiKey?: string + syntheticApiKey?: string // Requesty, Unbound, etc should perhaps also be here, but they already have their own hacks for reloading } diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index ae254a16ba7..4890321fef1 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -17,7 +17,6 @@ import { // kilocode_change start geminiCliDefaultModelId, geminiCliModels, - syntheticModels, syntheticDefaultModelId, ovhCloudAiEndpointsDefaultModelId, inceptionDefaultModelId, @@ -104,6 +103,7 @@ export const useSelectedModel = (apiConfiguration?: ProviderSettings) => { kilocodeOrganizationId: apiConfiguration?.kilocodeOrganizationId, geminiApiKey: apiConfiguration?.geminiApiKey, googleGeminiBaseUrl: apiConfiguration?.googleGeminiBaseUrl, + syntheticApiKey: apiConfiguration?.syntheticApiKey, // kilocode_change }) const openRouterModelProviders = useModelProviders(kilocodeDefaultModel, apiConfiguration) // kilocode_change end @@ -418,7 +418,7 @@ function getSelectedModel({ // kilocode_change start case "synthetic": { const id = apiConfiguration.apiModelId ?? syntheticDefaultModelId - const info = syntheticModels[id as keyof typeof syntheticModels] + const info = routerModels.synthetic[id] return { id, info } } // kilocode_change end diff --git a/webview-ui/src/utils/__tests__/validate.test.ts b/webview-ui/src/utils/__tests__/validate.test.ts index 14c1057eb39..426cc902a1d 100644 --- a/webview-ui/src/utils/__tests__/validate.test.ts +++ b/webview-ui/src/utils/__tests__/validate.test.ts @@ -66,6 +66,7 @@ describe("Model Validation Functions", () => { chutes: {}, gemini: {}, inception: {}, + synthetic: {}, // kilocode_change end } From d2c4ac77354a04da4e43ada0515f81ff58485cba Mon Sep 17 00:00:00 2001 From: Matt Cowger Date: Thu, 13 Nov 2025 14:19:07 -0800 Subject: [PATCH 2/3] WIP: Fix more tests --- packages/types/src/model.ts | 1 + src/api/providers/__tests__/synthetic.spec.ts | 1 - src/api/providers/fetchers/modelCache.ts | 3 - .../webview/__tests__/ClineProvider.spec.ts | 34 +- .../__tests__/webviewMessageHandler.spec.ts | 357 ++++++++++++------ 5 files changed, 257 insertions(+), 139 deletions(-) diff --git a/packages/types/src/model.ts b/packages/types/src/model.ts index 3ab9ee41236..3900e503ad8 100644 --- a/packages/types/src/model.ts +++ b/packages/types/src/model.ts @@ -57,6 +57,7 @@ export const modelInfoSchema = z.object({ maxThinkingTokens: z.number().nullish(), contextWindow: z.number(), supportsImages: z.boolean().optional(), + supportsComputerUse: z.boolean().optional(), supportsPromptCache: z.boolean(), // Capability flag to indicate whether the model supports an output verbosity parameter supportsVerbosity: z.boolean().optional(), diff --git a/src/api/providers/__tests__/synthetic.spec.ts b/src/api/providers/__tests__/synthetic.spec.ts index 41d22c6334a..2224d2751ed 100644 --- a/src/api/providers/__tests__/synthetic.spec.ts +++ b/src/api/providers/__tests__/synthetic.spec.ts @@ -211,7 +211,6 @@ describe("SyntheticHandler", () => { expect(mockCreate).toHaveBeenCalledWith( expect.objectContaining({ model: modelId, - max_tokens: 0.2 * modelInfo.maxTokens, temperature: 0.5, messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]), stream: true, diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index 4083cd64b12..fd341f1e8fa 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -154,9 +154,6 @@ export const getModels = async (options: GetModelsOptions): Promise models = await getRooModels(rooBaseUrl, options.apiKey) break } - case "chutes": - models = await getChutesModels(options.apiKey) - break default: { // Ensures router is exhaustively checked if RouterName is a strict union. const exhaustiveCheck: never = provider diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 8fa080712f0..007cd0f048c 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -2788,7 +2788,6 @@ describe("ClineProvider - Router Models", () => { inception: mockModels, // kilocode_change huggingface: {}, "io-intelligence": {}, - synthetic: mockModels, // kilocode_change }, values: undefined, }) @@ -2835,7 +2834,6 @@ describe("ClineProvider - Router Models", () => { .mockResolvedValueOnce(mockModels) // deepinfra success .mockResolvedValueOnce(mockModels) // kilocode_change: ovhcloud .mockResolvedValueOnce(mockModels) // kilocode_change: inception success - .mockRejectedValueOnce(new Error("Synthetic API error")) // synthetic fail .mockResolvedValueOnce(mockModels) // roo success .mockRejectedValueOnce(new Error("Chutes API error")) // chutes fail .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm fail @@ -2848,7 +2846,7 @@ describe("ClineProvider - Router Models", () => { routerModels: { deepinfra: mockModels, openrouter: mockModels, - gemini: mockModels, + gemini: mockModels, // kilocode_change requesty: {}, glama: mockModels, unbound: {}, @@ -2859,9 +2857,8 @@ describe("ClineProvider - Router Models", () => { litellm: {}, kilocode: {}, "vercel-ai-gateway": mockModels, - ovhcloud: mockModels, - inception: mockModels, - synthetic: {}, + ovhcloud: mockModels, // kilocode_change + inception: mockModels, // kilocode_change huggingface: {}, "io-intelligence": {}, }, @@ -2869,7 +2866,6 @@ describe("ClineProvider - Router Models", () => { }) // Verify error messages were sent for failed providers - // Verify error messages were sent for failed providers in the correct order expect(mockPostMessage).toHaveBeenCalledWith({ type: "singleRouterModelFetchResponse", success: false, @@ -2884,12 +2880,14 @@ describe("ClineProvider - Router Models", () => { values: { provider: "unbound" }, }) + // kilocode_change start expect(mockPostMessage).toHaveBeenCalledWith({ type: "singleRouterModelFetchResponse", success: false, error: "Chutes API error", values: { provider: "chutes" }, }) + // kilocode_change end expect(mockPostMessage).toHaveBeenCalledWith({ type: "singleRouterModelFetchResponse", @@ -2901,15 +2899,8 @@ describe("ClineProvider - Router Models", () => { expect(mockPostMessage).toHaveBeenCalledWith({ type: "singleRouterModelFetchResponse", success: false, - error: "Ollama API error", - values: { provider: "ollama" }, - }) - - expect(mockPostMessage).toHaveBeenCalledWith({ - type: "singleRouterModelFetchResponse", - success: false, - error: "Synthetic API error", - values: { provider: "synthetic" }, + error: "Unbound API error", + values: { provider: "unbound" }, }) expect(mockPostMessage).toHaveBeenCalledWith({ @@ -3007,14 +2998,10 @@ describe("ClineProvider - Router Models", () => { routerModels: { deepinfra: mockModels, openrouter: mockModels, - gemini: mockModels, + gemini: mockModels, // kilocode_change requesty: mockModels, glama: mockModels, unbound: mockModels, - chutes: mockModels, - litellm: {}, - "kilocode-openrouter": mockModels, - ollama: mockModels, roo: mockModels, chutes: mockModels, litellm: {}, @@ -3022,9 +3009,8 @@ describe("ClineProvider - Router Models", () => { ollama: mockModels, // kilocode_change lmstudio: {}, "vercel-ai-gateway": mockModels, - ovhcloud: mockModels, - inception: mockModels, - synthetic: mockModels, + ovhcloud: mockModels, // kilocode_change + inception: mockModels, // kilocode_change huggingface: {}, "io-intelligence": {}, }, diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 8323c100da4..bec8e3da5c9 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -181,49 +181,6 @@ describe("webviewMessageHandler - requestOllamaModels", () => { }) }) -const createMockModels = (): ModelRecord => ({ - "model-1": { - maxTokens: 4096, - contextWindow: 8192, - supportsPromptCache: false, - description: "Test model 1", - }, -}) - -const routerProviders = [ - "deepinfra", - "openrouter", - "gemini", - "requesty", - "glama", - "unbound", - "chutes", - "kilocode-openrouter", - "ollama", - "vercel-ai-gateway", - "ovhcloud", - "inception", - "synthetic", - "litellm", -] - -const routerProviderFailureCases = [ - { provider: "deepinfra", errorMessage: "DeepInfra API error" }, - { provider: "openrouter", errorMessage: "OpenRouter API error" }, - { provider: "gemini", errorMessage: "Gemini API error" }, - { provider: "requesty", errorMessage: "Requesty API error" }, - { provider: "glama", errorMessage: "Glama API error" }, - { provider: "unbound", errorMessage: "Unbound API error" }, - { provider: "chutes", errorMessage: "Chutes API error" }, - { provider: "kilocode-openrouter", errorMessage: "Kilocode OpenRouter API error" }, - { provider: "ollama", errorMessage: "Ollama API error" }, - { provider: "vercel-ai-gateway", errorMessage: "Vercel AI Gateway API error" }, - { provider: "ovhcloud", errorMessage: "OVHcloud AI Endpoints error" }, - { provider: "inception", errorMessage: "Inception API error" }, - { provider: "synthetic", errorMessage: "Synthetic API error" }, - { provider: "litellm", errorMessage: "LiteLLM connection failed" }, -] - describe("webviewMessageHandler - requestRouterModels", () => { beforeEach(() => { vi.clearAllMocks() @@ -304,24 +261,16 @@ describe("webviewMessageHandler - requestRouterModels", () => { // Note: io-intelligence is not fetched because no API key is provided in the mock state // Verify response was sent - expect(mockClineProvider.postMessageToWebview).toHaveBeenNthCalledWith(1, { - type: "ollamaModels", - ollamaModels: mockModels, - }) - - expect(mockClineProvider.postMessageToWebview).toHaveBeenNthCalledWith(2, { + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ type: "routerModels", routerModels: { deepinfra: mockModels, openrouter: mockModels, - gemini: mockModels, + gemini: mockModels, // kilocode_change requesty: mockModels, glama: mockModels, + synthetic: mockModels, unbound: mockModels, - chutes: mockModels, - litellm: mockModels, - "kilocode-openrouter": mockModels, - ollama: mockModels, litellm: mockModels, kilocode: mockModels, roo: mockModels, @@ -331,55 +280,12 @@ describe("webviewMessageHandler - requestRouterModels", () => { "vercel-ai-gateway": mockModels, huggingface: {}, "io-intelligence": {}, - ovhcloud: mockModels, - inception: mockModels, - synthetic: mockModels, + ovhcloud: mockModels, // kilocode_change + inception: mockModels, // kilocode_change }, values: undefined, }) }) - test.each(routerProviders)("successfully fetches models from %s", async (provider) => { - mockGetModels.mockImplementation(async (args) => { - if (args.provider === provider) { - return createMockModels() - } - return {} as ModelRecord - }) - - await webviewMessageHandler(mockClineProvider, { - type: "requestRouterModels", - }) - - expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ - type: "routerModels", - routerModels: expect.objectContaining({ - [provider]: createMockModels(), - }), - }) - }) - - test.each(routerProviderFailureCases)( - "handles $provider provider failure gracefully", - async ({ provider, errorMessage }) => { - mockGetModels.mockImplementation(async (args) => { - if (args.provider === provider) { - throw new Error(errorMessage) - } - return createMockModels() - }) - - await webviewMessageHandler(mockClineProvider, { - type: "requestRouterModels", - }) - - expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ - type: "singleRouterModelFetchResponse", - success: false, - error: errorMessage, - values: { provider }, - }) - }, - ) it("handles LiteLLM models with values from message when config is missing", async () => { mockClineProvider.getState = vi.fn().mockResolvedValue({ @@ -459,33 +365,262 @@ describe("webviewMessageHandler - requestRouterModels", () => { ) // Verify response includes empty object for LiteLLM - expect(mockClineProvider.postMessageToWebview).toHaveBeenNthCalledWith(1, { - type: "ollamaModels", - ollamaModels: mockModels, - }) - - expect(mockClineProvider.postMessageToWebview).toHaveBeenNthCalledWith(2, { + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ type: "routerModels", routerModels: { deepinfra: mockModels, openrouter: mockModels, - gemini: mockModels, + gemini: mockModels, // kilocode_change requesty: mockModels, + chutes: mockModels, glama: mockModels, + synthetic: mockModels, unbound: mockModels, - chutes: mockModels, + roo: mockModels, litellm: {}, - "kilocode-openrouter": mockModels, - ollama: mockModels, + kilocode: mockModels, + ollama: mockModels, // kilocode_change lmstudio: {}, "vercel-ai-gateway": mockModels, huggingface: {}, "io-intelligence": {}, + ovhcloud: mockModels, // kilocode_change + inception: mockModels, // kilocode_change + }, + values: undefined, + }) + }) + + it("handles individual provider failures gracefully", async () => { + const mockModels: ModelRecord = { + "model-1": { + maxTokens: 4096, + contextWindow: 8192, + supportsPromptCache: false, + description: "Test model 1", + }, + } + + // Mock some providers to succeed and others to fail + mockGetModels + .mockResolvedValueOnce(mockModels) // openrouter + .mockResolvedValueOnce(mockModels) // kilocode_change: gemini + .mockRejectedValueOnce(new Error("Requesty API error")) // requesty + .mockRejectedValueOnce(new Error("Chutes API error")) // chutes + .mockResolvedValueOnce(mockModels) // glama + .mockRejectedValueOnce(new Error("Unbound API error")) // unbound + .mockResolvedValueOnce(mockModels) // kilocode-openrouter + .mockRejectedValueOnce(new Error("Ollama API error")) // kilocode_change + .mockResolvedValueOnce(mockModels) // vercel-ai-gateway + .mockResolvedValueOnce(mockModels) // deepinfra + .mockResolvedValueOnce(mockModels) // kilocode_change ovhcloud + .mockRejectedValueOnce(new Error("Inception API error")) // kilocode_change + .mockResolvedValueOnce(mockModels) // roo + + .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm + + await webviewMessageHandler(mockClineProvider, { + type: "requestRouterModels", + }) + + // Verify error messages were sent for failed providers (these come first) + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "Requesty API error", + values: { provider: "requesty" }, + }) + + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "Unbound API error", + values: { provider: "unbound" }, + }) + + // kilocode_change start + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "Inception API error", + values: { provider: "inception" }, + }) + // kilocode_change end + + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "Chutes API error", + values: { provider: "chutes" }, + }) + + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "LiteLLM connection failed", + values: { provider: "litellm" }, + }) + + // Verify final routerModels response includes successful providers and empty objects for failed ones + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "routerModels", + routerModels: { + deepinfra: mockModels, + openrouter: mockModels, + requesty: {}, + glama: mockModels, + unbound: {}, + roo: mockModels, + chutes: {}, + litellm: {}, + ollama: {}, + lmstudio: {}, + "vercel-ai-gateway": mockModels, + huggingface: {}, + "io-intelligence": {}, + // kilocode_change start + kilocode: mockModels, + inception: {}, + gemini: mockModels, ovhcloud: mockModels, - inception: mockModels, - synthetic: mockModels, + // kilocode_change end }, + values: undefined, + }) + }) + + it("handles Error objects and string errors correctly", async () => { + // Mock providers to fail with different error types + mockGetModels + .mockRejectedValueOnce(new Error("Structured error message")) // openrouter + .mockRejectedValueOnce(new Error("Gemini API error")) // // kilocode_change: gemini + .mockRejectedValueOnce(new Error("Requesty API error")) // requesty + .mockRejectedValueOnce(new Error("Glama API error")) // glama + .mockRejectedValueOnce(new Error("Unbound API error")) // unbound + .mockResolvedValueOnce({}) // kilocode-openrouter - Success + .mockRejectedValueOnce(new Error("Ollama API error")) // ollama + .mockRejectedValueOnce(new Error("Vercel AI Gateway error")) // vercel-ai-gateway + .mockRejectedValueOnce(new Error("DeepInfra API error")) // deepinfra + .mockRejectedValueOnce(new Error("OVHcloud AI Endpoints error")) // ovhcloud // kilocode_change + .mockRejectedValueOnce(new Error("Inception API error")) // kilocode_change inception + .mockRejectedValueOnce(new Error("Roo API error")) // roo + .mockRejectedValueOnce(new Error("Chutes API error")) // chutes + .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm + + await webviewMessageHandler(mockClineProvider, { + type: "requestRouterModels", + }) + + // Verify error handling for different error types + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "Structured error message", + values: { provider: "openrouter" }, + }) + + // kilocode_change start + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "Gemini API error", + values: { provider: "gemini" }, + }) + // kilocode_change end + + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "Requesty API error", + values: { provider: "requesty" }, }) + + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "Glama API error", + values: { provider: "glama" }, + }) + + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "Unbound API error", + values: { provider: "unbound" }, + }) + + // kilocode_change start + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "Ollama API error", + values: { provider: "ollama" }, + }) + + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "Vercel AI Gateway error", + values: { provider: "vercel-ai-gateway" }, + }) + + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "Chutes API error", + values: { provider: "chutes" }, + }) + // kilocode_change end + + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "DeepInfra API error", + values: { provider: "deepinfra" }, + }) + + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "Vercel AI Gateway error", + values: { provider: "vercel-ai-gateway" }, + }) + + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "Roo API error", + values: { provider: "roo" }, + }) + + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "Chutes API error", + values: { provider: "chutes" }, + }) + + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "LiteLLM connection failed", + values: { provider: "litellm" }, + }) + + // kilocode_change start + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "OVHcloud AI Endpoints error", + values: { provider: "ovhcloud" }, + }) + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "Inception API error", + values: { provider: "inception" }, + }) + // kilocode_change end }) it("prefers config values over message values for LiteLLM", async () => { From ef9f5831f1e463eb2b46f3188a45a738739d714e Mon Sep 17 00:00:00 2001 From: Matt Cowger Date: Thu, 13 Nov 2025 23:04:44 +0000 Subject: [PATCH 3/3] Fix more test ordering issues --- .../webview/__tests__/ClineProvider.spec.ts | 52 +++++++----- .../__tests__/webviewMessageHandler.spec.ts | 83 ++++++++++++------- .../src/components/settings/ApiOptions.tsx | 2 +- .../components/ui/hooks/useSelectedModel.ts | 12 +-- 4 files changed, 83 insertions(+), 66 deletions(-) diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 007cd0f048c..bb4da7bce9b 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -2776,6 +2776,7 @@ describe("ClineProvider - Router Models", () => { gemini: mockModels, // kilocode_change requesty: mockModels, glama: mockModels, + synthetic: mockModels, unbound: mockModels, roo: mockModels, chutes: mockModels, @@ -2812,6 +2813,7 @@ describe("ClineProvider - Router Models", () => { ovhCloudAiEndpointsApiKey: "ovhcloud-key", inceptionLabsApiKey: "inception-key", inceptionLabsBaseUrl: "https://api.inceptionlabs.ai/v1/", + syntheticApiKey: "synthetic-key", // kilocode_change end }, } as any) @@ -2822,21 +2824,23 @@ describe("ClineProvider - Router Models", () => { const { getModels } = await import("../../../api/providers/fetchers/modelCache") // Mock some providers to succeed and others to fail + // Order matches the candidates array in webviewMessageHandler.ts vi.mocked(getModels) - .mockResolvedValueOnce(mockModels) // openrouter success - .mockResolvedValueOnce(mockModels) // kilocode_change: gemini success - .mockRejectedValueOnce(new Error("Requesty API error")) // requesty fail - .mockResolvedValueOnce(mockModels) // glama success - .mockRejectedValueOnce(new Error("Unbound API error")) // unbound fail - .mockRejectedValueOnce(new Error("Kilocode-OpenRouter API error")) // kilocode-openrouter fail - .mockRejectedValueOnce(new Error("Ollama API error")) // kilocode_change - .mockResolvedValueOnce(mockModels) // vercel-ai-gateway success - .mockResolvedValueOnce(mockModels) // deepinfra success - .mockResolvedValueOnce(mockModels) // kilocode_change: ovhcloud - .mockResolvedValueOnce(mockModels) // kilocode_change: inception success - .mockResolvedValueOnce(mockModels) // roo success - .mockRejectedValueOnce(new Error("Chutes API error")) // chutes fail - .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm fail + .mockResolvedValueOnce(mockModels) // 1. openrouter success + .mockResolvedValueOnce(mockModels) // 2. gemini success + .mockRejectedValueOnce(new Error("Requesty API error")) // 3. requesty fail + .mockResolvedValueOnce(mockModels) // 4. glama success + .mockRejectedValueOnce(new Error("Unbound API error")) // 5. unbound fail + .mockRejectedValueOnce(new Error("Kilocode-OpenRouter API error")) // 6. kilocode fail + .mockRejectedValueOnce(new Error("Ollama API error")) // 7. ollama fail + .mockResolvedValueOnce(mockModels) // 8. vercel-ai-gateway success + .mockResolvedValueOnce(mockModels) // 9. deepinfra success + .mockResolvedValueOnce(mockModels) // 10. ovhcloud success + .mockResolvedValueOnce(mockModels) // 11. inception success + .mockResolvedValueOnce(mockModels) // 12. synthetic success + .mockRejectedValueOnce(new Error("Roo API error")) // 13. roo fail + .mockRejectedValueOnce(new Error("Chutes API error")) // 14. chutes fail + .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // 15. litellm fail await messageHandler({ type: "requestRouterModels" }) @@ -2844,21 +2848,22 @@ describe("ClineProvider - Router Models", () => { expect(mockPostMessage).toHaveBeenCalledWith({ type: "routerModels", routerModels: { - deepinfra: mockModels, openrouter: mockModels, - gemini: mockModels, // kilocode_change + gemini: mockModels, requesty: {}, glama: mockModels, unbound: {}, - roo: mockModels, - chutes: {}, - ollama: {}, - lmstudio: {}, - litellm: {}, kilocode: {}, + ollama: {}, "vercel-ai-gateway": mockModels, - ovhcloud: mockModels, // kilocode_change - inception: mockModels, // kilocode_change + deepinfra: mockModels, + ovhcloud: mockModels, + inception: mockModels, + synthetic: mockModels, + roo: {}, + chutes: {}, + litellm: {}, + lmstudio: {}, huggingface: {}, "io-intelligence": {}, }, @@ -3001,6 +3006,7 @@ describe("ClineProvider - Router Models", () => { gemini: mockModels, // kilocode_change requesty: mockModels, glama: mockModels, + synthetic: mockModels, unbound: mockModels, roo: mockModels, chutes: mockModels, diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index bec8e3da5c9..5ef4add2c24 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -402,28 +402,29 @@ describe("webviewMessageHandler - requestRouterModels", () => { } // Mock some providers to succeed and others to fail + // Order matches the candidates array in webviewMessageHandler.ts mockGetModels - .mockResolvedValueOnce(mockModels) // openrouter - .mockResolvedValueOnce(mockModels) // kilocode_change: gemini - .mockRejectedValueOnce(new Error("Requesty API error")) // requesty - .mockRejectedValueOnce(new Error("Chutes API error")) // chutes - .mockResolvedValueOnce(mockModels) // glama - .mockRejectedValueOnce(new Error("Unbound API error")) // unbound - .mockResolvedValueOnce(mockModels) // kilocode-openrouter - .mockRejectedValueOnce(new Error("Ollama API error")) // kilocode_change - .mockResolvedValueOnce(mockModels) // vercel-ai-gateway - .mockResolvedValueOnce(mockModels) // deepinfra - .mockResolvedValueOnce(mockModels) // kilocode_change ovhcloud - .mockRejectedValueOnce(new Error("Inception API error")) // kilocode_change - .mockResolvedValueOnce(mockModels) // roo - - .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm + .mockResolvedValueOnce(mockModels) // 1. openrouter + .mockResolvedValueOnce(mockModels) // 2. gemini + .mockRejectedValueOnce(new Error("Requesty API error")) // 3. requesty + .mockResolvedValueOnce(mockModels) // 4. glama + .mockRejectedValueOnce(new Error("Unbound API error")) // 5. unbound + .mockRejectedValueOnce(new Error("Kilocode API error")) // 6. kilocode + .mockRejectedValueOnce(new Error("Ollama API error")) // 7. ollama + .mockResolvedValueOnce(mockModels) // 8. vercel-ai-gateway + .mockResolvedValueOnce(mockModels) // 9. deepinfra + .mockResolvedValueOnce(mockModels) // 10. ovhcloud + .mockRejectedValueOnce(new Error("Inception API error")) // 11. inception + .mockRejectedValueOnce(new Error("Synthetic API error")) // 12. synthetic + .mockResolvedValueOnce(mockModels) // 13. roo + .mockRejectedValueOnce(new Error("Chutes API error")) // 14. chutes + .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // 15. litellm (conditional) await webviewMessageHandler(mockClineProvider, { type: "requestRouterModels", }) - // Verify error messages were sent for failed providers (these come first) + // Verify error messages were sent for failed providers expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ type: "singleRouterModelFetchResponse", success: false, @@ -438,14 +439,33 @@ describe("webviewMessageHandler - requestRouterModels", () => { values: { provider: "unbound" }, }) - // kilocode_change start + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "Kilocode API error", + values: { provider: "kilocode" }, + }) + + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "Ollama API error", + values: { provider: "ollama" }, + }) + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ type: "singleRouterModelFetchResponse", success: false, error: "Inception API error", values: { provider: "inception" }, }) - // kilocode_change end + + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "Synthetic API error", + values: { provider: "synthetic" }, + }) expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ type: "singleRouterModelFetchResponse", @@ -465,25 +485,24 @@ describe("webviewMessageHandler - requestRouterModels", () => { expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ type: "routerModels", routerModels: { - deepinfra: mockModels, openrouter: mockModels, + gemini: mockModels, requesty: {}, glama: mockModels, unbound: {}, + kilocode: {}, + ollama: {}, + "vercel-ai-gateway": mockModels, + deepinfra: mockModels, + ovhcloud: mockModels, + inception: {}, + synthetic: {}, roo: mockModels, chutes: {}, litellm: {}, - ollama: {}, lmstudio: {}, - "vercel-ai-gateway": mockModels, huggingface: {}, "io-intelligence": {}, - // kilocode_change start - kilocode: mockModels, - inception: {}, - gemini: mockModels, - ovhcloud: mockModels, - // kilocode_change end }, values: undefined, }) @@ -491,18 +510,20 @@ describe("webviewMessageHandler - requestRouterModels", () => { it("handles Error objects and string errors correctly", async () => { // Mock providers to fail with different error types + // Order must match the candidates array in webviewMessageHandler.ts mockGetModels .mockRejectedValueOnce(new Error("Structured error message")) // openrouter - .mockRejectedValueOnce(new Error("Gemini API error")) // // kilocode_change: gemini + .mockRejectedValueOnce(new Error("Gemini API error")) // gemini .mockRejectedValueOnce(new Error("Requesty API error")) // requesty .mockRejectedValueOnce(new Error("Glama API error")) // glama .mockRejectedValueOnce(new Error("Unbound API error")) // unbound - .mockResolvedValueOnce({}) // kilocode-openrouter - Success + .mockResolvedValueOnce({}) // kilocode - Success .mockRejectedValueOnce(new Error("Ollama API error")) // ollama .mockRejectedValueOnce(new Error("Vercel AI Gateway error")) // vercel-ai-gateway .mockRejectedValueOnce(new Error("DeepInfra API error")) // deepinfra - .mockRejectedValueOnce(new Error("OVHcloud AI Endpoints error")) // ovhcloud // kilocode_change - .mockRejectedValueOnce(new Error("Inception API error")) // kilocode_change inception + .mockRejectedValueOnce(new Error("OVHcloud AI Endpoints error")) // ovhcloud + .mockRejectedValueOnce(new Error("Inception API error")) // inception + .mockRejectedValueOnce(new Error("Synthetic API error")) // synthetic .mockRejectedValueOnce(new Error("Roo API error")) // roo .mockRejectedValueOnce(new Error("Chutes API error")) // chutes .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 6b348b008ad..ff9e1476d32 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -282,7 +282,7 @@ const ApiOptions = ({ selectedProvider === "litellm" || selectedProvider === "deepinfra" || selectedProvider === "chutes" || // kilocode_change - selectedProvider === "synthetic" // kilocode_change + selectedProvider === "synthetic" || // kilocode_change selectedProvider === "roo" ) { vscode.postMessage({ type: "requestRouterModels" }) diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 37d09a1f981..229dc7af50a 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -98,17 +98,6 @@ export const useSelectedModel = (apiConfiguration?: ProviderSettings) => { const lmStudioModelId = provider === "lmstudio" ? apiConfiguration?.lmStudioModelId : undefined const ollamaModelId = provider === "ollama" ? apiConfiguration?.ollamaModelId : undefined - const routerModels = useRouterModels({ - openRouterBaseUrl: apiConfiguration?.openRouterBaseUrl, - openRouterApiKey: apiConfiguration?.apiKey, - kilocodeOrganizationId: apiConfiguration?.kilocodeOrganizationId, - geminiApiKey: apiConfiguration?.geminiApiKey, - googleGeminiBaseUrl: apiConfiguration?.googleGeminiBaseUrl, - syntheticApiKey: apiConfiguration?.syntheticApiKey, // kilocode_change - }) - const openRouterModelProviders = useModelProviders(kilocodeDefaultModel, apiConfiguration) - // kilocode_change end - // Only fetch router models for dynamic providers const shouldFetchRouterModels = isDynamicProvider(provider) const routerModels = useRouterModels( @@ -119,6 +108,7 @@ export const useSelectedModel = (apiConfiguration?: ProviderSettings) => { kilocodeOrganizationId: apiConfiguration?.kilocodeOrganizationId, geminiApiKey: apiConfiguration?.geminiApiKey, googleGeminiBaseUrl: apiConfiguration?.googleGeminiBaseUrl, + syntheticApiKey: apiConfiguration?.syntheticApiKey, // kilocode_change }, // kilocode_change end {