From c5b1b2cafae6b90da51f6946271dde9f3f2fc528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Tue, 14 Oct 2025 14:16:36 -0300 Subject: [PATCH 1/6] fix: dontt fetch models from providers that are not configured --- .../__tests__/webviewMessageHandler.spec.ts | 115 ++++++++++-------- src/core/webview/webviewMessageHandler.ts | 89 +++++++++++--- 2 files changed, 136 insertions(+), 68 deletions(-) diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index a34120bd30..5e237b9a57 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -184,18 +184,21 @@ describe("webviewMessageHandler - requestOllamaModels", () => { describe("webviewMessageHandler - requestRouterModels", () => { beforeEach(() => { vi.clearAllMocks() + // kilocode_change start: Add all required API keys and deepInfraApiKey for conditional fetching mockClineProvider.getState = vi.fn().mockResolvedValue({ apiConfiguration: { openRouterApiKey: "openrouter-key", requestyApiKey: "requesty-key", - glamaApiKey: "glama-key", unboundApiKey: "unbound-key", - chutesApiKey: "chutes-key", // kilocode_change + chutesApiKey: "chutes-key", litellmApiKey: "litellm-key", litellmBaseUrl: "http://localhost:4000", - ovhCloudAiEndpointsApiKey: "ovhcloud-key", // kilocode_change + ovhCloudAiEndpointsApiKey: "ovhcloud-key", + deepInfraApiKey: "deepinfra-key", // Added for conditional fetching + kilocodeToken: "kilocode-token", // Added for conditional fetching }, }) + // kilocode_change end }) it("successfully fetches models from all providers", async () => { @@ -220,21 +223,36 @@ describe("webviewMessageHandler - requestRouterModels", () => { type: "requestRouterModels", }) - // Verify getModels was called for each provider - expect(mockGetModels).toHaveBeenCalledWith({ provider: "deepinfra" }) - expect(mockGetModels).toHaveBeenCalledWith({ provider: "openrouter", apiKey: "openrouter-key" }) // kilocode_change: apiKey - expect(mockGetModels).toHaveBeenCalledWith({ provider: "requesty", apiKey: "requesty-key" }) - expect(mockGetModels).toHaveBeenCalledWith({ provider: "glama" }) + // kilocode_change start: Updated expectations for conditional fetching + // Verify getModels was called only for configured providers + expect(mockGetModels).toHaveBeenCalledWith({ + provider: "openrouter", + apiKey: "openrouter-key", + baseUrl: undefined, + }) + expect(mockGetModels).toHaveBeenCalledWith({ provider: "requesty", apiKey: "requesty-key", baseUrl: undefined }) expect(mockGetModels).toHaveBeenCalledWith({ provider: "unbound", apiKey: "unbound-key" }) - expect(mockGetModels).toHaveBeenCalledWith({ provider: "chutes", apiKey: "chutes-key" }) // kilocode_change - expect(mockGetModels).toHaveBeenCalledWith({ provider: "vercel-ai-gateway" }) + expect(mockGetModels).toHaveBeenCalledWith({ provider: "chutes", apiKey: "chutes-key" }) + expect(mockGetModels).toHaveBeenCalledWith({ + provider: "deepinfra", + apiKey: "deepinfra-key", + baseUrl: undefined, + }) + expect(mockGetModels).toHaveBeenCalledWith({ provider: "ovhcloud", apiKey: "ovhcloud-key", baseUrl: undefined }) + expect(mockGetModels).toHaveBeenCalledWith({ + provider: "kilocode-openrouter", + kilocodeToken: "kilocode-token", + kilocodeOrganizationId: undefined, + }) expect(mockGetModels).toHaveBeenCalledWith({ provider: "litellm", apiKey: "litellm-key", baseUrl: "http://localhost:4000", }) - // Note: huggingface is not fetched in requestRouterModels - it has its own handler - // Note: io-intelligence is not fetched because no API key is provided in the mock state + // Glama and Vercel AI Gateway are NOT fetched unless they're the active provider + expect(mockGetModels).not.toHaveBeenCalledWith({ provider: "glama" }) + expect(mockGetModels).not.toHaveBeenCalledWith({ provider: "vercel-ai-gateway" }) + // kilocode_change end // Verify response was sent expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ @@ -243,17 +261,17 @@ describe("webviewMessageHandler - requestRouterModels", () => { deepinfra: mockModels, openrouter: mockModels, requesty: mockModels, - glama: mockModels, + glama: {}, // kilocode_change: Not fetched unbound: mockModels, - chutes: mockModels, // kilocode_change + chutes: mockModels, litellm: mockModels, "kilocode-openrouter": mockModels, - ollama: mockModels, // kilocode_change + ollama: {}, // kilocode_change: Not fetched lmstudio: {}, - "vercel-ai-gateway": mockModels, + "vercel-ai-gateway": {}, // kilocode_change: Not fetched huggingface: {}, "io-intelligence": {}, - ovhcloud: mockModels, // kilocode_change + ovhcloud: mockModels, }, }) }) @@ -298,16 +316,16 @@ describe("webviewMessageHandler - requestRouterModels", () => { }) it("skips LiteLLM when both config and message values are missing", async () => { + // kilocode_change start: Updated test for conditional fetching mockClineProvider.getState = vi.fn().mockResolvedValue({ apiConfiguration: { openRouterApiKey: "openrouter-key", requestyApiKey: "requesty-key", - glamaApiKey: "glama-key", unboundApiKey: "unbound-key", - // kilocode_change start ovhCloudAiEndpointsApiKey: "ovhcloud-key", chutesApiKey: "chutes-key", - // kilocode_change end + deepInfraApiKey: "deepinfra-key", + kilocodeToken: "kilocode-token", // Missing litellm config }, }) @@ -335,26 +353,27 @@ describe("webviewMessageHandler - requestRouterModels", () => { }), ) - // Verify response includes empty object for LiteLLM + // Verify response includes empty object for LiteLLM and unconfigured providers expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ type: "routerModels", routerModels: { deepinfra: mockModels, openrouter: mockModels, requesty: mockModels, - glama: mockModels, + glama: {}, // Not fetched - not active provider unbound: mockModels, - chutes: mockModels, // kilocode_change + chutes: mockModels, litellm: {}, "kilocode-openrouter": mockModels, - ollama: mockModels, // kilocode_change + ollama: {}, // Not fetched - not active provider lmstudio: {}, - "vercel-ai-gateway": mockModels, + "vercel-ai-gateway": {}, // Not fetched - not active provider huggingface: {}, "io-intelligence": {}, - ovhcloud: mockModels, // kilocode_change + ovhcloud: mockModels, }, }) + // kilocode_change end }) it("handles individual provider failures gracefully", async () => { @@ -367,24 +386,24 @@ describe("webviewMessageHandler - requestRouterModels", () => { }, } + // kilocode_change start: Updated mock sequence for conditional fetching // Mock some providers to succeed and others to fail mockGetModels .mockResolvedValueOnce(mockModels) // openrouter .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 + .mockRejectedValueOnce(new Error("Chutes API error")) // chutes .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("OVHcloud AI Endpoints error")) // ovhcloud .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm + // kilocode_change end await webviewMessageHandler(mockClineProvider, { type: "requestRouterModels", }) + // kilocode_change start: Updated expectations for conditional fetching // Verify successful providers are included expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ type: "routerModels", @@ -392,19 +411,20 @@ describe("webviewMessageHandler - requestRouterModels", () => { deepinfra: mockModels, openrouter: mockModels, requesty: {}, - glama: mockModels, + glama: {}, // Not fetched - not active provider unbound: {}, - chutes: {}, // kilocode_change + chutes: {}, litellm: {}, "kilocode-openrouter": mockModels, - ollama: {}, - ovhcloud: mockModels, // kilocode_change + ollama: {}, // Not fetched - not active provider + ovhcloud: {}, lmstudio: {}, - "vercel-ai-gateway": mockModels, + "vercel-ai-gateway": {}, // Not fetched - not active provider huggingface: {}, "io-intelligence": {}, }, }) + // kilocode_change end // Verify error messages were sent for failed providers expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ @@ -428,6 +448,13 @@ describe("webviewMessageHandler - requestRouterModels", () => { error: "Chutes API error", values: { provider: "chutes" }, }) + + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "OVHcloud AI Endpoints error", + values: { provider: "ovhcloud" }, + }) // kilocode_change end expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ @@ -439,19 +466,18 @@ describe("webviewMessageHandler - requestRouterModels", () => { }) it("handles Error objects and string errors correctly", async () => { + // kilocode_change start: Updated mock sequence for conditional fetching // Mock providers to fail with different error types mockGetModels .mockRejectedValueOnce(new Error("Structured error message")) // openrouter .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 + .mockRejectedValueOnce(new Error("Chutes API error")) // chutes .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("OVHcloud AI Endpoints error")) // ovhcloud .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm + // kilocode_change end await webviewMessageHandler(mockClineProvider, { type: "requestRouterModels", @@ -472,13 +498,6 @@ describe("webviewMessageHandler - requestRouterModels", () => { 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, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index ddd0b6186e..d21706fe3f 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -825,55 +825,104 @@ export const webviewMessageHandler = async ( } } - // kilocode_change start: openrouter auth, kilocode provider + // kilocode_change start: openrouter auth, kilocode provider, conditional model fetching const openRouterApiKey = apiConfiguration.openRouterApiKey || message?.values?.openRouterApiKey const openRouterBaseUrl = apiConfiguration.openRouterBaseUrl || message?.values?.openRouterBaseUrl - const modelFetchPromises: Array<{ key: RouterName; options: GetModelsOptions }> = [ - { + const modelFetchPromises: Array<{ key: RouterName; options: GetModelsOptions }> = [] + + // Only fetch models from providers that have the necessary configuration + // This prevents unnecessary network requests to unconfigured providers during CLI initialization + + // OpenRouter - only if API key is provided + if (openRouterApiKey) { + modelFetchPromises.push({ key: "openrouter", options: { provider: "openrouter", apiKey: openRouterApiKey, baseUrl: openRouterBaseUrl }, - }, - { + }) + } + + // Requesty - only if API key is provided + if (apiConfiguration.requestyApiKey) { + modelFetchPromises.push({ key: "requesty", options: { provider: "requesty", apiKey: apiConfiguration.requestyApiKey, baseUrl: apiConfiguration.requestyBaseUrl, }, - }, - { key: "glama", options: { provider: "glama" } }, - { key: "unbound", options: { provider: "unbound", apiKey: apiConfiguration.unboundApiKey } }, - { key: "chutes", options: { provider: "chutes", apiKey: apiConfiguration.chutesApiKey } }, // kilocode_change - { + }) + } + + // Glama - only if it's the active provider + if (apiConfiguration.apiProvider === "glama") { + modelFetchPromises.push({ key: "glama", options: { provider: "glama" } }) + } + + // Unbound - only if API key is provided + if (apiConfiguration.unboundApiKey) { + modelFetchPromises.push({ + key: "unbound", + options: { provider: "unbound", apiKey: apiConfiguration.unboundApiKey }, + }) + } + + // Chutes - only if API key is provided + if (apiConfiguration.chutesApiKey) { + modelFetchPromises.push({ + key: "chutes", + options: { provider: "chutes", apiKey: apiConfiguration.chutesApiKey }, + }) + } + + // Kilocode OpenRouter - only if token is provided + if (apiConfiguration.kilocodeToken) { + modelFetchPromises.push({ key: "kilocode-openrouter", options: { provider: "kilocode-openrouter", kilocodeToken: apiConfiguration.kilocodeToken, kilocodeOrganizationId: apiConfiguration.kilocodeOrganizationId, }, - }, - { key: "ollama", options: { provider: "ollama", baseUrl: apiConfiguration.ollamaBaseUrl } }, - { key: "vercel-ai-gateway", options: { provider: "vercel-ai-gateway" } }, - { + }) + } + + // Ollama - only if it's the active provider or base URL is configured + if (apiConfiguration.apiProvider === "ollama" || apiConfiguration.ollamaBaseUrl) { + modelFetchPromises.push({ + key: "ollama", + options: { provider: "ollama", baseUrl: apiConfiguration.ollamaBaseUrl }, + }) + } + + // Vercel AI Gateway - only if it's the active provider + if (apiConfiguration.apiProvider === "vercel-ai-gateway") { + modelFetchPromises.push({ key: "vercel-ai-gateway", options: { provider: "vercel-ai-gateway" } }) + } + + // DeepInfra - only if API key is provided + if (apiConfiguration.deepInfraApiKey) { + modelFetchPromises.push({ key: "deepinfra", options: { provider: "deepinfra", apiKey: apiConfiguration.deepInfraApiKey, baseUrl: apiConfiguration.deepInfraBaseUrl, }, - }, - // kilocode_change start - { + }) + } + + // OVHCloud - only if API key is provided + if (apiConfiguration.ovhCloudAiEndpointsApiKey) { + modelFetchPromises.push({ key: "ovhcloud", options: { provider: "ovhcloud", apiKey: apiConfiguration.ovhCloudAiEndpointsApiKey, baseUrl: apiConfiguration.ovhCloudAiEndpointsBaseUrl, }, - }, - // kilocode_change end - ] + }) + } // kilocode_change end // Add IO Intelligence if API key is provided. From 003eb763e1044c70835ebf956ad89d803b6faffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Tue, 14 Oct 2025 14:51:31 -0300 Subject: [PATCH 2/6] feat: add proxy support --- cli/README.md | 99 +++++++++++++++++++ cli/esbuild.config.mjs | 2 + cli/package.dist.json | 2 + cli/package.json | 2 + cli/src/host/ExtensionHost.ts | 15 +++ cli/src/index.ts | 4 + cli/src/utils/proxy-config.ts | 174 +++++++++++++++++++++++++++++++++ cli/src/utils/proxy-matcher.ts | 149 ++++++++++++++++++++++++++++ pnpm-lock.yaml | 6 ++ 9 files changed, 453 insertions(+) create mode 100644 cli/src/utils/proxy-config.ts create mode 100644 cli/src/utils/proxy-matcher.ts diff --git a/cli/README.md b/cli/README.md index 1a5c99b8f9..bac114ef69 100644 --- a/cli/README.md +++ b/cli/README.md @@ -159,6 +159,105 @@ This instructs the AI to proceed without user input. echo "Implement the new feature" | kilocode --ci --timeout 600 ``` +## Proxy Configuration + +The CLI supports HTTP/HTTPS proxy configuration through environment variables. This is useful when running behind corporate proxies or when you need to route traffic through a proxy server. + +The proxy configuration works with all HTTP clients used by the CLI and extension: + +- **Axios** - Used for many API calls +- **Undici** - Used by fetch-based providers +- **Native fetch** - Node.js built-in fetch + +### Supported Environment Variables + +- `HTTP_PROXY` / `http_proxy`: Proxy URL for HTTP requests +- `HTTPS_PROXY` / `https_proxy`: Proxy URL for HTTPS requests +- `ALL_PROXY` / `all_proxy`: Fallback proxy for all protocols +- `NO_PROXY` / `no_proxy`: Comma-separated list of domains to bypass proxy +- `NODE_TLS_REJECT_UNAUTHORIZED`: Set to `0` to disable SSL certificate validation (use with caution) + +### Proxy URL Format + +``` +http://[username:password@]proxy-host:port +``` + +### Examples + +#### Basic Proxy Configuration + +```bash +# Set proxy for HTTP and HTTPS +export HTTP_PROXY=http://localhost:8080 +export HTTPS_PROXY=http://localhost:8080 + +# Run CLI +kilocode +``` + +#### Proxy with Authentication + +```bash +# Proxy with username and password +export HTTPS_PROXY=http://username:password@proxy.company.com:8080 + +kilocode +``` + +#### Bypass Proxy for Specific Domains + +```bash +# Set proxy +export HTTPS_PROXY=http://localhost:8080 + +# Bypass proxy for localhost and internal domains +export NO_PROXY=localhost,127.0.0.1,*.internal.company.com,192.168.0.0/16 + +kilocode +``` + +#### Self-Signed Certificates + +```bash +# Disable SSL certificate validation (use with caution in development only) +export NODE_TLS_REJECT_UNAUTHORIZED=0 +export HTTPS_PROXY=http://localhost:8080 + +kilocode +``` + +#### One-Line Command + +```bash +# Run with proxy settings in a single command +HTTP_PROXY=http://localhost:8080 HTTPS_PROXY=http://localhost:8080 NODE_TLS_REJECT_UNAUTHORIZED=0 kilocode +``` + +### NO_PROXY Patterns + +The `NO_PROXY` environment variable supports various patterns: + +- **Exact domain**: `example.com` +- **Wildcard subdomains**: `*.example.com` +- **IP addresses**: `192.168.1.1` +- **CIDR ranges**: `192.168.0.0/16` +- **Port-specific**: `example.com:8080` +- **Multiple patterns**: `localhost,127.0.0.1,*.internal.com` + +### Troubleshooting + +If proxy is not working: + +1. **Check proxy logs**: The CLI will log proxy configuration on startup +2. **Verify proxy URL**: Ensure the proxy URL is correct and accessible +3. **Test proxy**: Use `curl` to test if the proxy is working: + ```bash + curl -x http://localhost:8080 https://api.kilocode.ai + ``` +4. **Check NO_PROXY**: Ensure the target domain is not in NO_PROXY list +5. **Certificate issues**: If you see SSL errors, you may need to set `NODE_TLS_REJECT_UNAUTHORIZED=0` (development only) + ## Local Development ### DevTools diff --git a/cli/esbuild.config.mjs b/cli/esbuild.config.mjs index 7324b4c38b..b375a5e467 100644 --- a/cli/esbuild.config.mjs +++ b/cli/esbuild.config.mjs @@ -103,6 +103,8 @@ const __dirname = __dirname__(__filename); "get-folder-size", "google-auth-library", "gray-matter", + "http-proxy-agent", + "https-proxy-agent", "i18next", "ignore", "ink", diff --git a/cli/package.dist.json b/cli/package.dist.json index d53c1b3982..f94759e33e 100644 --- a/cli/package.dist.json +++ b/cli/package.dist.json @@ -46,6 +46,8 @@ "get-folder-size": "^5.0.0", "google-auth-library": "^9.15.1", "gray-matter": "^4.0.3", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", "i18next": "^25.0.0", "ignore": "^7.0.3", "ink": "^6.3.1", diff --git a/cli/package.json b/cli/package.json index c50e9f338a..a850aeaf73 100644 --- a/cli/package.json +++ b/cli/package.json @@ -65,6 +65,8 @@ "get-folder-size": "^5.0.0", "google-auth-library": "^9.15.1", "gray-matter": "^4.0.3", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", "i18next": "^25.0.0", "ignore": "^7.0.3", "ink": "^6.3.1", diff --git a/cli/src/host/ExtensionHost.ts b/cli/src/host/ExtensionHost.ts index a34ac03ef4..2b8a565ac5 100644 --- a/cli/src/host/ExtensionHost.ts +++ b/cli/src/host/ExtensionHost.ts @@ -403,6 +403,21 @@ export class ExtensionHost extends EventEmitter { const originalResolveFilename = ModuleClass._resolveFilename const originalCompile = ModuleClass.prototype._compile + // Configure proxy BEFORE loading the extension + // This ensures the extension's HTTP clients get the proxy configuration + try { + // Load axios in the extension's require context to ensure it's the same instance + require("axios") + const { configureProxy } = await import("../utils/proxy-config.js") + + // Apply proxy configuration to all HTTP clients that will be used by the extension + logs.debug("Configuring proxy for extension context", "ExtensionHost") + configureProxy() + logs.debug("Proxy configured for extension", "ExtensionHost") + } catch (error) { + logs.warn("Failed to configure proxy for extension", "ExtensionHost", { error }) + } + // Set up module resolution interception for vscode ModuleClass._resolveFilename = function (request: string, parent: any, isMain: boolean, options?: any) { if (request === "vscode") { diff --git a/cli/src/index.ts b/cli/src/index.ts index 404daaad31..8c73f895e8 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -4,6 +4,10 @@ import { loadEnvFile } from "./utils/env-loader.js" loadEnvFile() +// Configure proxy settings immediately after loading environment +import { configureProxy } from "./utils/proxy-config.js" +configureProxy() + import { Command } from "commander" import { existsSync } from "fs" import { spawn } from "child_process" diff --git a/cli/src/utils/proxy-config.ts b/cli/src/utils/proxy-config.ts new file mode 100644 index 0000000000..d72b2d3e78 --- /dev/null +++ b/cli/src/utils/proxy-config.ts @@ -0,0 +1,174 @@ +/** + * Proxy configuration for CLI + * Reads proxy settings from environment variables and configures axios, fetch, and undici + */ + +import axios from "axios" +import { HttpProxyAgent } from "http-proxy-agent" +import { HttpsProxyAgent } from "https-proxy-agent" +import { logs } from "../services/logs.js" +import { parseNoProxy, shouldBypassProxy } from "./proxy-matcher.js" + +export interface ProxyConfig { + httpProxy?: string + httpsProxy?: string + noProxy: string[] + rejectUnauthorized: boolean +} + +/** + * Get proxy configuration from environment variables + */ +export function getProxyConfig(): ProxyConfig { + // Read proxy environment variables (case-insensitive) + const httpProxy = + process.env.HTTP_PROXY || process.env.http_proxy || process.env.ALL_PROXY || process.env.all_proxy || undefined + + const httpsProxy = + process.env.HTTPS_PROXY || + process.env.https_proxy || + process.env.HTTP_PROXY || + process.env.http_proxy || + process.env.ALL_PROXY || + process.env.all_proxy || + undefined + + const noProxyStr = process.env.NO_PROXY || process.env.no_proxy || "" + const noProxy = parseNoProxy(noProxyStr) + + // Handle NODE_TLS_REJECT_UNAUTHORIZED for self-signed certificates + const rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED !== "0" + + const result: ProxyConfig = { + noProxy, + rejectUnauthorized, + } + + if (httpProxy) { + result.httpProxy = httpProxy + } + + if (httpsProxy) { + result.httpsProxy = httpsProxy + } + + return result +} + +/** + * Configure axios, fetch, and undici to use proxy settings from environment variables + * This must be called before any HTTP requests are made + */ +export function configureProxy(): void { + const config = getProxyConfig() + + // Log proxy configuration (without credentials) + if (config.httpProxy || config.httpsProxy) { + logs.info("Configuring proxy settings:", "Proxy") + if (config.httpProxy) { + logs.info(` HTTP_PROXY: ${sanitizeProxyUrl(config.httpProxy)}`, "Proxy") + } + if (config.httpsProxy) { + logs.info(` HTTPS_PROXY: ${sanitizeProxyUrl(config.httpsProxy)}`, "Proxy") + } + if (config.noProxy.length > 0) { + logs.info(` NO_PROXY: ${config.noProxy.join(", ")}`, "Proxy") + } + if (!config.rejectUnauthorized) { + logs.warn(" TLS certificate validation: DISABLED (NODE_TLS_REJECT_UNAUTHORIZED=0)", "Proxy") + } + } + + // Create proxy agents + const httpProxyAgent = config.httpProxy ? new HttpProxyAgent(config.httpProxy) : undefined + const httpsProxyAgent = config.httpsProxy + ? new HttpsProxyAgent(config.httpsProxy, { + rejectUnauthorized: config.rejectUnauthorized, + }) + : undefined + + // Configure axios defaults + axios.defaults.httpAgent = httpProxyAgent + axios.defaults.httpsAgent = httpsProxyAgent + + // Add request interceptor to handle NO_PROXY for axios + axios.interceptors.request.use( + (requestConfig) => { + const url = requestConfig.url + if (!url) { + return requestConfig + } + + // Check if this URL should bypass the proxy + if (shouldBypassProxy(url, config.noProxy)) { + // Remove proxy agents for this request + requestConfig.httpAgent = undefined + requestConfig.httpsAgent = undefined + requestConfig.proxy = false + } + + return requestConfig + }, + (error) => { + return Promise.reject(error) + }, + ) + + // Configure undici for fetch requests + configureUndiciProxy(config).catch((error) => { + logs.debug("Failed to configure undici proxy", "Proxy", { error }) + }) + + logs.info("Proxy configuration complete (axios, fetch, undici)", "Proxy") +} + +/** + * Sanitize proxy URL for logging (remove credentials) + */ +function sanitizeProxyUrl(proxyUrl: string): string { + try { + const url = new URL(proxyUrl) + if (url.username || url.password) { + return `${url.protocol}//*****:*****@${url.host}` + } + return proxyUrl + } catch { + return proxyUrl + } +} + +/** + * Configure undici proxy (async to handle dynamic import) + */ +async function configureUndiciProxy(config: ProxyConfig): Promise { + try { + // Configure undici (used by some providers like fetchWithTimeout) + const undici = await import("undici") + if (undici && undici.setGlobalDispatcher) { + const { ProxyAgent } = undici + + // Use HTTPS proxy for all requests if configured, fallback to HTTP proxy + const proxyUri = config.httpsProxy || config.httpProxy + if (proxyUri) { + const proxyAgent = new ProxyAgent({ + uri: proxyUri, + requestTls: { + rejectUnauthorized: config.rejectUnauthorized, + }, + }) + undici.setGlobalDispatcher(proxyAgent) + logs.debug("Undici proxy agent configured", "Proxy") + } + } + } catch (error) { + logs.debug("Undici not available or failed to configure", "Proxy", { error }) + } +} + +/** + * Check if proxy is configured + */ +export function isProxyConfigured(): boolean { + const config = getProxyConfig() + return !!(config.httpProxy || config.httpsProxy) +} diff --git a/cli/src/utils/proxy-matcher.ts b/cli/src/utils/proxy-matcher.ts new file mode 100644 index 0000000000..767d96713c --- /dev/null +++ b/cli/src/utils/proxy-matcher.ts @@ -0,0 +1,149 @@ +/** + * Utility functions for matching URLs against NO_PROXY patterns + */ + +/** + * Parse NO_PROXY environment variable into an array of patterns + */ +export function parseNoProxy(noProxy: string | undefined): string[] { + if (!noProxy) { + return [] + } + + return noProxy + .split(",") + .map((pattern) => pattern.trim()) + .filter((pattern) => pattern.length > 0) +} + +/** + * Check if a hostname matches a NO_PROXY pattern + */ +export function shouldBypassProxy(url: string, noProxyPatterns: string[]): boolean { + if (noProxyPatterns.length === 0) { + return false + } + + try { + const urlObj = new URL(url) + const hostname = urlObj.hostname.toLowerCase() + const port = urlObj.port + + for (const pattern of noProxyPatterns) { + const normalizedPattern = pattern.toLowerCase() + + // Check for wildcard pattern (*.example.com) + if (normalizedPattern.startsWith("*.")) { + const domain = normalizedPattern.slice(2) + if (hostname === domain || hostname.endsWith("." + domain)) { + return true + } + } + // Check for exact match or subdomain match + else if (normalizedPattern.startsWith(".")) { + const domain = normalizedPattern.slice(1) + if (hostname === domain || hostname.endsWith("." + domain)) { + return true + } + } + // Check for pattern with port + else if (normalizedPattern.includes(":")) { + const [patternHost, patternPort] = normalizedPattern.split(":") + if (hostname === patternHost && port === patternPort) { + return true + } + } + // Check for exact hostname match + else if (hostname === normalizedPattern) { + return true + } + // Check if hostname ends with pattern (subdomain match) + else if (hostname.endsWith("." + normalizedPattern)) { + return true + } + // Check for IP address or CIDR range + else if (isIpMatch(hostname, normalizedPattern)) { + return true + } + } + } catch { + // Invalid URL, don't bypass proxy + return false + } + + return false +} + +/** + * Check if an IP address matches a pattern (including CIDR notation) + */ +function isIpMatch(hostname: string, pattern: string): boolean { + // Simple IP exact match + if (hostname === pattern) { + return true + } + + // Check for CIDR notation (e.g., 192.168.0.0/16) + if (pattern.includes("/")) { + return matchesCidr(hostname, pattern) + } + + return false +} + +/** + * Check if an IP address matches a CIDR range + */ +function matchesCidr(ip: string, cidr: string): boolean { + try { + const parts = cidr.split("/") + if (parts.length !== 2 || !parts[0] || !parts[1]) { + return false + } + + const range = parts[0] + const bitsStr = parts[1] + const mask = parseInt(bitsStr, 10) + + if (isNaN(mask) || mask < 0 || mask > 32) { + return false + } + + const ipNum = ipToNumber(ip) + const rangeNum = ipToNumber(range) + + if (ipNum === null || rangeNum === null) { + return false + } + + const maskNum = (0xffffffff << (32 - mask)) >>> 0 + return (ipNum & maskNum) === (rangeNum & maskNum) + } catch { + return false + } +} + +/** + * Convert IPv4 address to number + */ +function ipToNumber(ip: string): number | null { + const parts = ip.split(".") + if (parts.length !== 4) { + return null + } + + let num = 0 + for (let i = 0; i < 4; i++) { + const partStr = parts[i] + if (!partStr) { + return null + } + const part = parseInt(partStr, 10) + if (isNaN(part) || part < 0 || part > 255) { + return null + } + num = (num << 8) + part + } + + return num >>> 0 +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3df0fe92d6..eba4c1daa9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -687,6 +687,12 @@ importers: gray-matter: specifier: ^4.0.3 version: 4.0.3 + http-proxy-agent: + specifier: ^7.0.2 + version: 7.0.2 + https-proxy-agent: + specifier: ^7.0.5 + version: 7.0.6 i18next: specifier: ^25.0.0 version: 25.2.1(typescript@5.6.3) From 3031c876f3c9fb6f5b67093a1df67dfff14d7b1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Tue, 14 Oct 2025 15:36:57 -0300 Subject: [PATCH 3/6] fix: prevent duplicated messages --- cli/src/host/ExtensionHost.ts | 38 +++++++++++------------------------ cli/src/services/extension.ts | 20 ++---------------- cli/src/ui/UI.tsx | 2 +- 3 files changed, 15 insertions(+), 45 deletions(-) diff --git a/cli/src/host/ExtensionHost.ts b/cli/src/host/ExtensionHost.ts index 2b8a565ac5..0af4fbf476 100644 --- a/cli/src/host/ExtensionHost.ts +++ b/cli/src/host/ExtensionHost.ts @@ -288,9 +288,18 @@ export class ExtensionHost extends EventEmitter { await this.handleWebviewLaunch() } - // Forward ALL messages to the extension's webview handler - logs.debug(`Forwarding message to extension: ${message.type}`, "ExtensionHost") - this.emit("webviewMessage", message) + // Forward message directly to the webview provider instead of emitting event + // This prevents duplicate handling (event listener + direct call) + const webviewProvider = this.webviewProviders.get("kilo-code.SidebarProvider") + + if (webviewProvider && typeof webviewProvider.handleCLIMessage === "function") { + await webviewProvider.handleCLIMessage(message) + } else { + logs.warn( + `No webview provider found or handleCLIMessage not available for: ${message.type}`, + "ExtensionHost", + ) + } // Handle local state updates for CLI display after forwarding await this.handleLocalStateUpdates(message) @@ -664,29 +673,6 @@ export class ExtensionHost extends EventEmitter { } }, `extensionWebviewMessage-${message.type}`) }) - - // Set up webview message handler for messages TO the extension - this.on("webviewMessage", async (message: any) => { - await this.safeExecute(async () => { - logs.debug(`Forwarding webview message to extension: ${message.type}`, "ExtensionHost") - - // Find the registered webview provider - const webviewProvider = this.webviewProviders.get("kilo-code.SidebarProvider") - - if (webviewProvider && typeof webviewProvider.handleCLIMessage === "function") { - await webviewProvider.handleCLIMessage(message) - logs.debug( - `Successfully forwarded message to webview provider: ${message.type}`, - "ExtensionHost", - ) - } else { - logs.warn( - `No webview provider found or handleCLIMessage not available for: ${message.type}`, - "ExtensionHost", - ) - } - }, `webviewMessage-${message.type}`) - }) } } diff --git a/cli/src/services/extension.ts b/cli/src/services/extension.ts index 63bdb4ca17..4f5ef67b48 100644 --- a/cli/src/services/extension.ts +++ b/cli/src/services/extension.ts @@ -155,12 +155,8 @@ export class ExtensionService extends EventEmitter { } }) - // Handle TUI requests from message bridge - this.messageBridge.on("tuiRequest", async (message) => { - await this.handleTUIRequest(message) - }) - // Setup proper message routing to avoid IPC timeouts + // This is the ONLY handler for TUI messages - removed duplicate tuiRequest handler this.messageBridge.getTUIChannel().on("message", async (ipcMessage) => { if (ipcMessage.type === "request") { try { @@ -175,21 +171,9 @@ export class ExtensionService extends EventEmitter { }) } - /** - * Handle TUI request messages - */ - private async handleTUIRequest(message: any): Promise { - try { - if (message.data.type === "webviewMessage") { - await this.extensionHost.sendWebviewMessage(message.data.payload) - } - } catch (error) { - logs.error("Error handling TUI request", "ExtensionService", { error }) - } - } - /** * Handle TUI messages and return response + * This is the single point of entry for all TUI->Extension messages */ private async handleTUIMessage(data: any): Promise { try { diff --git a/cli/src/ui/UI.tsx b/cli/src/ui/UI.tsx index 4d776caa94..e1407ec00a 100644 --- a/cli/src/ui/UI.tsx +++ b/cli/src/ui/UI.tsx @@ -108,7 +108,7 @@ export const UI: React.FC = ({ options, onExit }) => { } } } - }, [options.prompt, executeCommand, sendUserMessage, onExit]) + }, [options.prompt]) // Simplified submit handler that delegates to appropriate hook const handleSubmit = useCallback( From f5c900553950ccff1ef2996e99d4a19483cb1ec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Tue, 14 Oct 2025 15:46:37 -0300 Subject: [PATCH 4/6] refactor: docs update --- cli/README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cli/README.md b/cli/README.md index bac114ef69..6f343bf951 100644 --- a/cli/README.md +++ b/cli/README.md @@ -163,12 +163,6 @@ This instructs the AI to proceed without user input. The CLI supports HTTP/HTTPS proxy configuration through environment variables. This is useful when running behind corporate proxies or when you need to route traffic through a proxy server. -The proxy configuration works with all HTTP clients used by the CLI and extension: - -- **Axios** - Used for many API calls -- **Undici** - Used by fetch-based providers -- **Native fetch** - Node.js built-in fetch - ### Supported Environment Variables - `HTTP_PROXY` / `http_proxy`: Proxy URL for HTTP requests From 1d5e3f59a066797fcc48e2195a78869c784dea32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Tue, 14 Oct 2025 16:00:49 -0300 Subject: [PATCH 5/6] refactor: fix ClineProvider tests --- .../webview/__tests__/ClineProvider.spec.ts | 140 +++++++++++------- .../__tests__/webviewMessageHandler.spec.ts | 4 +- 2 files changed, 89 insertions(+), 55 deletions(-) diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 1079b5a481..8f83302b7c 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -2694,19 +2694,24 @@ describe("ClineProvider - Router Models", () => { await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - // Mock getState to return API configuration + // kilocode_change start: Mock API configuration with keys for all providers to test conditional model fetching + // The implementation now only fetches models from providers that have the necessary configuration + // This prevents unnecessary network requests to unconfigured providers during CLI initialization vi.spyOn(provider, "getState").mockResolvedValue({ apiConfiguration: { + apiProvider: "openrouter", // Set active provider openRouterApiKey: "openrouter-key", requestyApiKey: "requesty-key", - glamaApiKey: "glama-key", unboundApiKey: "unbound-key", - chutesApiKey: "chutes-key", // kilocode_change + chutesApiKey: "chutes-key", litellmApiKey: "litellm-key", litellmBaseUrl: "http://localhost:4000", - ovhCloudAiEndpointsApiKey: "ovhcloud-key", // kilocode_change + ovhCloudAiEndpointsApiKey: "ovhcloud-key", + deepInfraApiKey: "deepinfra-key", + kilocodeToken: "kilocode-token", }, } as any) + // kilocode_change end const mockModels = { "model-1": { @@ -2728,100 +2733,121 @@ describe("ClineProvider - Router Models", () => { await messageHandler({ type: "requestRouterModels" }) - // Verify getModels was called for each provider with correct options - expect(getModels).toHaveBeenCalledWith({ provider: "openrouter", apiKey: "openrouter-key" }) // kilocode_change: apiKey - expect(getModels).toHaveBeenCalledWith({ provider: "requesty", apiKey: "requesty-key" }) - expect(getModels).toHaveBeenCalledWith({ provider: "glama" }) + // kilocode_change start: Verify getModels was called only for providers with API configuration + // The implementation now conditionally fetches models based on provider configuration + // This optimization prevents unnecessary network requests during initialization + expect(getModels).toHaveBeenCalledWith({ provider: "openrouter", apiKey: "openrouter-key", baseUrl: undefined }) + expect(getModels).toHaveBeenCalledWith({ provider: "requesty", apiKey: "requesty-key", baseUrl: undefined }) expect(getModels).toHaveBeenCalledWith({ provider: "unbound", apiKey: "unbound-key" }) - expect(getModels).toHaveBeenCalledWith({ provider: "chutes", apiKey: "chutes-key" }) // kilocode_change - expect(getModels).toHaveBeenCalledWith({ provider: "vercel-ai-gateway" }) - expect(getModels).toHaveBeenCalledWith({ provider: "ovhcloud", apiKey: "ovhcloud-key" }) // kilocode_change + expect(getModels).toHaveBeenCalledWith({ provider: "chutes", apiKey: "chutes-key" }) + expect(getModels).toHaveBeenCalledWith({ provider: "ovhcloud", apiKey: "ovhcloud-key", baseUrl: undefined }) expect(getModels).toHaveBeenCalledWith({ provider: "litellm", apiKey: "litellm-key", baseUrl: "http://localhost:4000", }) + expect(getModels).toHaveBeenCalledWith({ + provider: "deepinfra", + apiKey: "deepinfra-key", + baseUrl: undefined, + }) + expect(getModels).toHaveBeenCalledWith({ + provider: "kilocode-openrouter", + kilocodeToken: "kilocode-token", + kilocodeOrganizationId: undefined, + }) + // Note: glama, ollama, and vercel-ai-gateway are NOT called because they require specific apiProvider values + // kilocode_change end - // Verify response was sent + // kilocode_change start: Verify response includes models only for configured providers + // Providers without configuration return empty objects to prevent UI errors expect(mockPostMessage).toHaveBeenCalledWith({ type: "routerModels", routerModels: { deepinfra: mockModels, openrouter: mockModels, requesty: mockModels, - glama: mockModels, + glama: {}, // Empty because apiProvider !== "glama" unbound: mockModels, - chutes: mockModels, // kilocode_change + chutes: mockModels, litellm: mockModels, "kilocode-openrouter": mockModels, - ollama: mockModels, // kilocode_change + ollama: {}, // Empty because apiProvider !== "ollama" and no ollamaBaseUrl lmstudio: {}, - "vercel-ai-gateway": mockModels, - ovhcloud: mockModels, // kilocode_change + "vercel-ai-gateway": {}, // Empty because apiProvider !== "vercel-ai-gateway" + ovhcloud: mockModels, huggingface: {}, "io-intelligence": {}, }, }) + // kilocode_change end }) test("handles requestRouterModels with individual provider failures", async () => { await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] + // kilocode_change start: Mock API configuration for testing provider failure scenarios + // Tests that providers with configuration that fail still return empty objects vi.spyOn(provider, "getState").mockResolvedValue({ apiConfiguration: { + apiProvider: "openrouter", openRouterApiKey: "openrouter-key", requestyApiKey: "requesty-key", - glamaApiKey: "glama-key", unboundApiKey: "unbound-key", - chutesApiKey: "chutes-key", // kilocode_change + chutesApiKey: "chutes-key", litellmApiKey: "litellm-key", litellmBaseUrl: "http://localhost:4000", - ovhCloudAiEndpointsApiKey: "ovhcloud-key", // kilocode_change + ovhCloudAiEndpointsApiKey: "ovhcloud-key", + deepInfraApiKey: "deepinfra-key", + kilocodeToken: "kilocode-token", }, } as any) + // kilocode_change end const mockModels = { "model-1": { maxTokens: 4096, contextWindow: 8192, description: "Test model", supportsPromptCache: false }, } const { getModels } = await import("../../../api/providers/fetchers/modelCache") - // Mock some providers to succeed and others to fail + // kilocode_change start: Mock provider responses in order of execution + // Order matches the conditional fetching logic in webviewMessageHandler vi.mocked(getModels) .mockResolvedValueOnce(mockModels) // openrouter success .mockRejectedValueOnce(new Error("Requesty API error")) // requesty fail - .mockResolvedValueOnce(mockModels) // glama success .mockRejectedValueOnce(new Error("Unbound API error")) // unbound fail - .mockRejectedValueOnce(new Error("Chutes API error")) // kilocode_change: chutes fail + .mockRejectedValueOnce(new Error("Chutes API error")) // chutes 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) // kilocode_change: ovhcloud + .mockResolvedValueOnce(mockModels) // ovhcloud success .mockResolvedValueOnce(mockModels) // deepinfra success .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm fail + // Note: glama, ollama, vercel-ai-gateway are NOT called due to conditional fetching + // kilocode_change end await messageHandler({ type: "requestRouterModels" }) - // Verify main response includes successful providers and empty objects for failed ones + // kilocode_change start: Verify response handles provider failures correctly + // Failed providers return empty objects, successful ones return models expect(mockPostMessage).toHaveBeenCalledWith({ type: "routerModels", routerModels: { deepinfra: mockModels, openrouter: mockModels, - requesty: {}, - glama: mockModels, - unbound: {}, - chutes: {}, // kilocode_change - ollama: {}, + requesty: {}, // Failed + glama: {}, // Not called (apiProvider !== "glama") + unbound: {}, // Failed + chutes: {}, // Failed + ollama: {}, // Not called (apiProvider !== "ollama") lmstudio: {}, - litellm: {}, - "kilocode-openrouter": {}, - "vercel-ai-gateway": mockModels, - ovhcloud: mockModels, // kilocode_change + litellm: {}, // Failed + "kilocode-openrouter": {}, // Failed + "vercel-ai-gateway": {}, // Not called (apiProvider !== "vercel-ai-gateway") + ovhcloud: mockModels, huggingface: {}, "io-intelligence": {}, }, }) + // kilocode_change end // Verify error messages were sent for failed providers expect(mockPostMessage).toHaveBeenCalledWith({ @@ -2838,7 +2864,7 @@ describe("ClineProvider - Router Models", () => { values: { provider: "unbound" }, }) - // kilocode_change start + // kilocode_change start: Verify error message for Chutes provider failure expect(mockPostMessage).toHaveBeenCalledWith({ type: "singleRouterModelFetchResponse", success: false, @@ -2873,20 +2899,22 @@ describe("ClineProvider - Router Models", () => { await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - // Mock state without LiteLLM config + // kilocode_change start: Mock state without LiteLLM config to test message-based configuration + // Tests that LiteLLM can be configured via message values when not in global state vi.spyOn(provider, "getState").mockResolvedValue({ apiConfiguration: { + apiProvider: "openrouter", openRouterApiKey: "openrouter-key", requestyApiKey: "requesty-key", - glamaApiKey: "glama-key", unboundApiKey: "unbound-key", - // kilocode_change start ovhCloudAiEndpointsApiKey: "ovhcloud-key", chutesApiKey: "chutes-key", - // kilocode_change end - // No litellm config + deepInfraApiKey: "deepinfra-key", + kilocodeToken: "kilocode-token", + // No litellm config - will be provided via message values }, } as any) + // kilocode_change end const mockModels = { "model-1": { maxTokens: 4096, contextWindow: 8192, description: "Test model", supportsPromptCache: false }, @@ -2914,19 +2942,22 @@ describe("ClineProvider - Router Models", () => { await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] + // kilocode_change start: Mock state without LiteLLM config to test conditional fetching + // Tests that LiteLLM is skipped when no configuration is available (neither in state nor message) vi.spyOn(provider, "getState").mockResolvedValue({ apiConfiguration: { + apiProvider: "openrouter", openRouterApiKey: "openrouter-key", requestyApiKey: "requesty-key", - glamaApiKey: "glama-key", unboundApiKey: "unbound-key", - // kilocode_change start ovhCloudAiEndpointsApiKey: "ovhcloud-key", chutesApiKey: "chutes-key", - // kilocode_change end - // No litellm config + deepInfraApiKey: "deepinfra-key", + kilocodeToken: "kilocode-token", + // No litellm config - should be skipped }, } as any) + // kilocode_change end const mockModels = { "model-1": { maxTokens: 4096, contextWindow: 8192, description: "Test model", supportsPromptCache: false }, @@ -2943,26 +2974,29 @@ describe("ClineProvider - Router Models", () => { }), ) - // Verify response includes empty object for LiteLLM + // kilocode_change start: Verify response includes empty object for unconfigured providers + // LiteLLM returns empty object because it wasn't configured + // Glama, Ollama, and Vercel AI Gateway return empty objects because apiProvider doesn't match expect(mockPostMessage).toHaveBeenCalledWith({ type: "routerModels", routerModels: { deepinfra: mockModels, openrouter: mockModels, requesty: mockModels, - glama: mockModels, + glama: {}, // Not called (apiProvider !== "glama") unbound: mockModels, - chutes: mockModels, // kilocode_change - litellm: {}, + chutes: mockModels, + litellm: {}, // Not called (no config provided) "kilocode-openrouter": mockModels, - ollama: mockModels, // kilocode_change + ollama: {}, // Not called (apiProvider !== "ollama") lmstudio: {}, - "vercel-ai-gateway": mockModels, - ovhcloud: mockModels, // kilocode_change + "vercel-ai-gateway": {}, // Not called (apiProvider !== "vercel-ai-gateway") + ovhcloud: mockModels, huggingface: {}, "io-intelligence": {}, }, }) + // kilocode_change end }) test("handles requestLmStudioModels with proper response", async () => { diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 5e237b9a57..a0fa0a1f24 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -194,8 +194,8 @@ describe("webviewMessageHandler - requestRouterModels", () => { litellmApiKey: "litellm-key", litellmBaseUrl: "http://localhost:4000", ovhCloudAiEndpointsApiKey: "ovhcloud-key", - deepInfraApiKey: "deepinfra-key", // Added for conditional fetching - kilocodeToken: "kilocode-token", // Added for conditional fetching + deepInfraApiKey: "deepinfra-key", + kilocodeToken: "kilocode-token", }, }) // kilocode_change end From 2d25312452bd0e146e30d5c5975acd444e8818d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Tue, 14 Oct 2025 16:26:03 -0300 Subject: [PATCH 6/6] refactor: revert extension changes --- .../webview/__tests__/ClineProvider.spec.ts | 140 +++++++----------- .../__tests__/webviewMessageHandler.spec.ts | 115 ++++++-------- src/core/webview/webviewMessageHandler.ts | 89 +++-------- 3 files changed, 121 insertions(+), 223 deletions(-) diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 8f83302b7c..1079b5a481 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -2694,24 +2694,19 @@ describe("ClineProvider - Router Models", () => { await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - // kilocode_change start: Mock API configuration with keys for all providers to test conditional model fetching - // The implementation now only fetches models from providers that have the necessary configuration - // This prevents unnecessary network requests to unconfigured providers during CLI initialization + // Mock getState to return API configuration vi.spyOn(provider, "getState").mockResolvedValue({ apiConfiguration: { - apiProvider: "openrouter", // Set active provider openRouterApiKey: "openrouter-key", requestyApiKey: "requesty-key", + glamaApiKey: "glama-key", unboundApiKey: "unbound-key", - chutesApiKey: "chutes-key", + chutesApiKey: "chutes-key", // kilocode_change litellmApiKey: "litellm-key", litellmBaseUrl: "http://localhost:4000", - ovhCloudAiEndpointsApiKey: "ovhcloud-key", - deepInfraApiKey: "deepinfra-key", - kilocodeToken: "kilocode-token", + ovhCloudAiEndpointsApiKey: "ovhcloud-key", // kilocode_change }, } as any) - // kilocode_change end const mockModels = { "model-1": { @@ -2733,121 +2728,100 @@ describe("ClineProvider - Router Models", () => { await messageHandler({ type: "requestRouterModels" }) - // kilocode_change start: Verify getModels was called only for providers with API configuration - // The implementation now conditionally fetches models based on provider configuration - // This optimization prevents unnecessary network requests during initialization - expect(getModels).toHaveBeenCalledWith({ provider: "openrouter", apiKey: "openrouter-key", baseUrl: undefined }) - expect(getModels).toHaveBeenCalledWith({ provider: "requesty", apiKey: "requesty-key", baseUrl: undefined }) + // Verify getModels was called for each provider with correct options + expect(getModels).toHaveBeenCalledWith({ provider: "openrouter", apiKey: "openrouter-key" }) // kilocode_change: apiKey + expect(getModels).toHaveBeenCalledWith({ provider: "requesty", apiKey: "requesty-key" }) + expect(getModels).toHaveBeenCalledWith({ provider: "glama" }) expect(getModels).toHaveBeenCalledWith({ provider: "unbound", apiKey: "unbound-key" }) - expect(getModels).toHaveBeenCalledWith({ provider: "chutes", apiKey: "chutes-key" }) - expect(getModels).toHaveBeenCalledWith({ provider: "ovhcloud", apiKey: "ovhcloud-key", baseUrl: undefined }) + expect(getModels).toHaveBeenCalledWith({ provider: "chutes", apiKey: "chutes-key" }) // kilocode_change + expect(getModels).toHaveBeenCalledWith({ provider: "vercel-ai-gateway" }) + expect(getModels).toHaveBeenCalledWith({ provider: "ovhcloud", apiKey: "ovhcloud-key" }) // kilocode_change expect(getModels).toHaveBeenCalledWith({ provider: "litellm", apiKey: "litellm-key", baseUrl: "http://localhost:4000", }) - expect(getModels).toHaveBeenCalledWith({ - provider: "deepinfra", - apiKey: "deepinfra-key", - baseUrl: undefined, - }) - expect(getModels).toHaveBeenCalledWith({ - provider: "kilocode-openrouter", - kilocodeToken: "kilocode-token", - kilocodeOrganizationId: undefined, - }) - // Note: glama, ollama, and vercel-ai-gateway are NOT called because they require specific apiProvider values - // kilocode_change end - // kilocode_change start: Verify response includes models only for configured providers - // Providers without configuration return empty objects to prevent UI errors + // Verify response was sent expect(mockPostMessage).toHaveBeenCalledWith({ type: "routerModels", routerModels: { deepinfra: mockModels, openrouter: mockModels, requesty: mockModels, - glama: {}, // Empty because apiProvider !== "glama" + glama: mockModels, unbound: mockModels, - chutes: mockModels, + chutes: mockModels, // kilocode_change litellm: mockModels, "kilocode-openrouter": mockModels, - ollama: {}, // Empty because apiProvider !== "ollama" and no ollamaBaseUrl + ollama: mockModels, // kilocode_change lmstudio: {}, - "vercel-ai-gateway": {}, // Empty because apiProvider !== "vercel-ai-gateway" - ovhcloud: mockModels, + "vercel-ai-gateway": mockModels, + ovhcloud: mockModels, // kilocode_change huggingface: {}, "io-intelligence": {}, }, }) - // kilocode_change end }) test("handles requestRouterModels with individual provider failures", async () => { await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - // kilocode_change start: Mock API configuration for testing provider failure scenarios - // Tests that providers with configuration that fail still return empty objects vi.spyOn(provider, "getState").mockResolvedValue({ apiConfiguration: { - apiProvider: "openrouter", openRouterApiKey: "openrouter-key", requestyApiKey: "requesty-key", + glamaApiKey: "glama-key", unboundApiKey: "unbound-key", - chutesApiKey: "chutes-key", + chutesApiKey: "chutes-key", // kilocode_change litellmApiKey: "litellm-key", litellmBaseUrl: "http://localhost:4000", - ovhCloudAiEndpointsApiKey: "ovhcloud-key", - deepInfraApiKey: "deepinfra-key", - kilocodeToken: "kilocode-token", + ovhCloudAiEndpointsApiKey: "ovhcloud-key", // kilocode_change }, } as any) - // kilocode_change end const mockModels = { "model-1": { maxTokens: 4096, contextWindow: 8192, description: "Test model", supportsPromptCache: false }, } const { getModels } = await import("../../../api/providers/fetchers/modelCache") - // kilocode_change start: Mock provider responses in order of execution - // Order matches the conditional fetching logic in webviewMessageHandler + // Mock some providers to succeed and others to fail vi.mocked(getModels) .mockResolvedValueOnce(mockModels) // openrouter success .mockRejectedValueOnce(new Error("Requesty API error")) // requesty fail + .mockResolvedValueOnce(mockModels) // glama success .mockRejectedValueOnce(new Error("Unbound API error")) // unbound fail - .mockRejectedValueOnce(new Error("Chutes API error")) // chutes fail + .mockRejectedValueOnce(new Error("Chutes API error")) // kilocode_change: chutes fail .mockRejectedValueOnce(new Error("Kilocode-OpenRouter API error")) // kilocode-openrouter fail - .mockResolvedValueOnce(mockModels) // ovhcloud success + .mockRejectedValueOnce(new Error("Ollama API error")) // kilocode_change + .mockResolvedValueOnce(mockModels) // vercel-ai-gateway success + .mockResolvedValueOnce(mockModels) // kilocode_change: ovhcloud .mockResolvedValueOnce(mockModels) // deepinfra success .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm fail - // Note: glama, ollama, vercel-ai-gateway are NOT called due to conditional fetching - // kilocode_change end await messageHandler({ type: "requestRouterModels" }) - // kilocode_change start: Verify response handles provider failures correctly - // Failed providers return empty objects, successful ones return models + // Verify main response includes successful providers and empty objects for failed ones expect(mockPostMessage).toHaveBeenCalledWith({ type: "routerModels", routerModels: { deepinfra: mockModels, openrouter: mockModels, - requesty: {}, // Failed - glama: {}, // Not called (apiProvider !== "glama") - unbound: {}, // Failed - chutes: {}, // Failed - ollama: {}, // Not called (apiProvider !== "ollama") + requesty: {}, + glama: mockModels, + unbound: {}, + chutes: {}, // kilocode_change + ollama: {}, lmstudio: {}, - litellm: {}, // Failed - "kilocode-openrouter": {}, // Failed - "vercel-ai-gateway": {}, // Not called (apiProvider !== "vercel-ai-gateway") - ovhcloud: mockModels, + litellm: {}, + "kilocode-openrouter": {}, + "vercel-ai-gateway": mockModels, + ovhcloud: mockModels, // kilocode_change huggingface: {}, "io-intelligence": {}, }, }) - // kilocode_change end // Verify error messages were sent for failed providers expect(mockPostMessage).toHaveBeenCalledWith({ @@ -2864,7 +2838,7 @@ describe("ClineProvider - Router Models", () => { values: { provider: "unbound" }, }) - // kilocode_change start: Verify error message for Chutes provider failure + // kilocode_change start expect(mockPostMessage).toHaveBeenCalledWith({ type: "singleRouterModelFetchResponse", success: false, @@ -2899,22 +2873,20 @@ describe("ClineProvider - Router Models", () => { await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - // kilocode_change start: Mock state without LiteLLM config to test message-based configuration - // Tests that LiteLLM can be configured via message values when not in global state + // Mock state without LiteLLM config vi.spyOn(provider, "getState").mockResolvedValue({ apiConfiguration: { - apiProvider: "openrouter", openRouterApiKey: "openrouter-key", requestyApiKey: "requesty-key", + glamaApiKey: "glama-key", unboundApiKey: "unbound-key", + // kilocode_change start ovhCloudAiEndpointsApiKey: "ovhcloud-key", chutesApiKey: "chutes-key", - deepInfraApiKey: "deepinfra-key", - kilocodeToken: "kilocode-token", - // No litellm config - will be provided via message values + // kilocode_change end + // No litellm config }, } as any) - // kilocode_change end const mockModels = { "model-1": { maxTokens: 4096, contextWindow: 8192, description: "Test model", supportsPromptCache: false }, @@ -2942,22 +2914,19 @@ describe("ClineProvider - Router Models", () => { await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] - // kilocode_change start: Mock state without LiteLLM config to test conditional fetching - // Tests that LiteLLM is skipped when no configuration is available (neither in state nor message) vi.spyOn(provider, "getState").mockResolvedValue({ apiConfiguration: { - apiProvider: "openrouter", openRouterApiKey: "openrouter-key", requestyApiKey: "requesty-key", + glamaApiKey: "glama-key", unboundApiKey: "unbound-key", + // kilocode_change start ovhCloudAiEndpointsApiKey: "ovhcloud-key", chutesApiKey: "chutes-key", - deepInfraApiKey: "deepinfra-key", - kilocodeToken: "kilocode-token", - // No litellm config - should be skipped + // kilocode_change end + // No litellm config }, } as any) - // kilocode_change end const mockModels = { "model-1": { maxTokens: 4096, contextWindow: 8192, description: "Test model", supportsPromptCache: false }, @@ -2974,29 +2943,26 @@ describe("ClineProvider - Router Models", () => { }), ) - // kilocode_change start: Verify response includes empty object for unconfigured providers - // LiteLLM returns empty object because it wasn't configured - // Glama, Ollama, and Vercel AI Gateway return empty objects because apiProvider doesn't match + // Verify response includes empty object for LiteLLM expect(mockPostMessage).toHaveBeenCalledWith({ type: "routerModels", routerModels: { deepinfra: mockModels, openrouter: mockModels, requesty: mockModels, - glama: {}, // Not called (apiProvider !== "glama") + glama: mockModels, unbound: mockModels, - chutes: mockModels, - litellm: {}, // Not called (no config provided) + chutes: mockModels, // kilocode_change + litellm: {}, "kilocode-openrouter": mockModels, - ollama: {}, // Not called (apiProvider !== "ollama") + ollama: mockModels, // kilocode_change lmstudio: {}, - "vercel-ai-gateway": {}, // Not called (apiProvider !== "vercel-ai-gateway") - ovhcloud: mockModels, + "vercel-ai-gateway": mockModels, + ovhcloud: mockModels, // kilocode_change huggingface: {}, "io-intelligence": {}, }, }) - // kilocode_change end }) test("handles requestLmStudioModels with proper response", async () => { diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index a0fa0a1f24..a34120bd30 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -184,21 +184,18 @@ describe("webviewMessageHandler - requestOllamaModels", () => { describe("webviewMessageHandler - requestRouterModels", () => { beforeEach(() => { vi.clearAllMocks() - // kilocode_change start: Add all required API keys and deepInfraApiKey for conditional fetching mockClineProvider.getState = vi.fn().mockResolvedValue({ apiConfiguration: { openRouterApiKey: "openrouter-key", requestyApiKey: "requesty-key", + glamaApiKey: "glama-key", unboundApiKey: "unbound-key", - chutesApiKey: "chutes-key", + chutesApiKey: "chutes-key", // kilocode_change litellmApiKey: "litellm-key", litellmBaseUrl: "http://localhost:4000", - ovhCloudAiEndpointsApiKey: "ovhcloud-key", - deepInfraApiKey: "deepinfra-key", - kilocodeToken: "kilocode-token", + ovhCloudAiEndpointsApiKey: "ovhcloud-key", // kilocode_change }, }) - // kilocode_change end }) it("successfully fetches models from all providers", async () => { @@ -223,36 +220,21 @@ describe("webviewMessageHandler - requestRouterModels", () => { type: "requestRouterModels", }) - // kilocode_change start: Updated expectations for conditional fetching - // Verify getModels was called only for configured providers - expect(mockGetModels).toHaveBeenCalledWith({ - provider: "openrouter", - apiKey: "openrouter-key", - baseUrl: undefined, - }) - expect(mockGetModels).toHaveBeenCalledWith({ provider: "requesty", apiKey: "requesty-key", baseUrl: undefined }) + // Verify getModels was called for each provider + expect(mockGetModels).toHaveBeenCalledWith({ provider: "deepinfra" }) + expect(mockGetModels).toHaveBeenCalledWith({ provider: "openrouter", apiKey: "openrouter-key" }) // kilocode_change: apiKey + expect(mockGetModels).toHaveBeenCalledWith({ provider: "requesty", apiKey: "requesty-key" }) + expect(mockGetModels).toHaveBeenCalledWith({ provider: "glama" }) expect(mockGetModels).toHaveBeenCalledWith({ provider: "unbound", apiKey: "unbound-key" }) - expect(mockGetModels).toHaveBeenCalledWith({ provider: "chutes", apiKey: "chutes-key" }) - expect(mockGetModels).toHaveBeenCalledWith({ - provider: "deepinfra", - apiKey: "deepinfra-key", - baseUrl: undefined, - }) - expect(mockGetModels).toHaveBeenCalledWith({ provider: "ovhcloud", apiKey: "ovhcloud-key", baseUrl: undefined }) - expect(mockGetModels).toHaveBeenCalledWith({ - provider: "kilocode-openrouter", - kilocodeToken: "kilocode-token", - kilocodeOrganizationId: undefined, - }) + expect(mockGetModels).toHaveBeenCalledWith({ provider: "chutes", apiKey: "chutes-key" }) // kilocode_change + expect(mockGetModels).toHaveBeenCalledWith({ provider: "vercel-ai-gateway" }) expect(mockGetModels).toHaveBeenCalledWith({ provider: "litellm", apiKey: "litellm-key", baseUrl: "http://localhost:4000", }) - // Glama and Vercel AI Gateway are NOT fetched unless they're the active provider - expect(mockGetModels).not.toHaveBeenCalledWith({ provider: "glama" }) - expect(mockGetModels).not.toHaveBeenCalledWith({ provider: "vercel-ai-gateway" }) - // kilocode_change end + // Note: huggingface is not fetched in requestRouterModels - it has its own handler + // Note: io-intelligence is not fetched because no API key is provided in the mock state // Verify response was sent expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ @@ -261,17 +243,17 @@ describe("webviewMessageHandler - requestRouterModels", () => { deepinfra: mockModels, openrouter: mockModels, requesty: mockModels, - glama: {}, // kilocode_change: Not fetched + glama: mockModels, unbound: mockModels, - chutes: mockModels, + chutes: mockModels, // kilocode_change litellm: mockModels, "kilocode-openrouter": mockModels, - ollama: {}, // kilocode_change: Not fetched + ollama: mockModels, // kilocode_change lmstudio: {}, - "vercel-ai-gateway": {}, // kilocode_change: Not fetched + "vercel-ai-gateway": mockModels, huggingface: {}, "io-intelligence": {}, - ovhcloud: mockModels, + ovhcloud: mockModels, // kilocode_change }, }) }) @@ -316,16 +298,16 @@ describe("webviewMessageHandler - requestRouterModels", () => { }) it("skips LiteLLM when both config and message values are missing", async () => { - // kilocode_change start: Updated test for conditional fetching mockClineProvider.getState = vi.fn().mockResolvedValue({ apiConfiguration: { openRouterApiKey: "openrouter-key", requestyApiKey: "requesty-key", + glamaApiKey: "glama-key", unboundApiKey: "unbound-key", + // kilocode_change start ovhCloudAiEndpointsApiKey: "ovhcloud-key", chutesApiKey: "chutes-key", - deepInfraApiKey: "deepinfra-key", - kilocodeToken: "kilocode-token", + // kilocode_change end // Missing litellm config }, }) @@ -353,27 +335,26 @@ describe("webviewMessageHandler - requestRouterModels", () => { }), ) - // Verify response includes empty object for LiteLLM and unconfigured providers + // Verify response includes empty object for LiteLLM expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ type: "routerModels", routerModels: { deepinfra: mockModels, openrouter: mockModels, requesty: mockModels, - glama: {}, // Not fetched - not active provider + glama: mockModels, unbound: mockModels, - chutes: mockModels, + chutes: mockModels, // kilocode_change litellm: {}, "kilocode-openrouter": mockModels, - ollama: {}, // Not fetched - not active provider + ollama: mockModels, // kilocode_change lmstudio: {}, - "vercel-ai-gateway": {}, // Not fetched - not active provider + "vercel-ai-gateway": mockModels, huggingface: {}, "io-intelligence": {}, - ovhcloud: mockModels, + ovhcloud: mockModels, // kilocode_change }, }) - // kilocode_change end }) it("handles individual provider failures gracefully", async () => { @@ -386,24 +367,24 @@ describe("webviewMessageHandler - requestRouterModels", () => { }, } - // kilocode_change start: Updated mock sequence for conditional fetching // Mock some providers to succeed and others to fail mockGetModels .mockResolvedValueOnce(mockModels) // openrouter .mockRejectedValueOnce(new Error("Requesty API error")) // requesty + .mockResolvedValueOnce(mockModels) // glama .mockRejectedValueOnce(new Error("Unbound API error")) // unbound - .mockRejectedValueOnce(new Error("Chutes API error")) // chutes + .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 - .mockRejectedValueOnce(new Error("OVHcloud AI Endpoints error")) // ovhcloud + .mockResolvedValueOnce(mockModels) // kilocode_change ovhcloud .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm - // kilocode_change end await webviewMessageHandler(mockClineProvider, { type: "requestRouterModels", }) - // kilocode_change start: Updated expectations for conditional fetching // Verify successful providers are included expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ type: "routerModels", @@ -411,20 +392,19 @@ describe("webviewMessageHandler - requestRouterModels", () => { deepinfra: mockModels, openrouter: mockModels, requesty: {}, - glama: {}, // Not fetched - not active provider + glama: mockModels, unbound: {}, - chutes: {}, + chutes: {}, // kilocode_change litellm: {}, "kilocode-openrouter": mockModels, - ollama: {}, // Not fetched - not active provider - ovhcloud: {}, + ollama: {}, + ovhcloud: mockModels, // kilocode_change lmstudio: {}, - "vercel-ai-gateway": {}, // Not fetched - not active provider + "vercel-ai-gateway": mockModels, huggingface: {}, "io-intelligence": {}, }, }) - // kilocode_change end // Verify error messages were sent for failed providers expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ @@ -448,13 +428,6 @@ describe("webviewMessageHandler - requestRouterModels", () => { error: "Chutes API error", values: { provider: "chutes" }, }) - - expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ - type: "singleRouterModelFetchResponse", - success: false, - error: "OVHcloud AI Endpoints error", - values: { provider: "ovhcloud" }, - }) // kilocode_change end expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ @@ -466,18 +439,19 @@ describe("webviewMessageHandler - requestRouterModels", () => { }) it("handles Error objects and string errors correctly", async () => { - // kilocode_change start: Updated mock sequence for conditional fetching // Mock providers to fail with different error types mockGetModels .mockRejectedValueOnce(new Error("Structured error message")) // openrouter .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 + .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 + .mockRejectedValueOnce(new Error("OVHcloud AI Endpoints error")) // ovhcloud // kilocode_change .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm - // kilocode_change end await webviewMessageHandler(mockClineProvider, { type: "requestRouterModels", @@ -498,6 +472,13 @@ describe("webviewMessageHandler - requestRouterModels", () => { 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, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index d21706fe3f..ddd0b6186e 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -825,104 +825,55 @@ export const webviewMessageHandler = async ( } } - // kilocode_change start: openrouter auth, kilocode provider, conditional model fetching + // kilocode_change start: openrouter auth, kilocode provider const openRouterApiKey = apiConfiguration.openRouterApiKey || message?.values?.openRouterApiKey const openRouterBaseUrl = apiConfiguration.openRouterBaseUrl || message?.values?.openRouterBaseUrl - const modelFetchPromises: Array<{ key: RouterName; options: GetModelsOptions }> = [] - - // Only fetch models from providers that have the necessary configuration - // This prevents unnecessary network requests to unconfigured providers during CLI initialization - - // OpenRouter - only if API key is provided - if (openRouterApiKey) { - modelFetchPromises.push({ + const modelFetchPromises: Array<{ key: RouterName; options: GetModelsOptions }> = [ + { key: "openrouter", options: { provider: "openrouter", apiKey: openRouterApiKey, baseUrl: openRouterBaseUrl }, - }) - } - - // Requesty - only if API key is provided - if (apiConfiguration.requestyApiKey) { - modelFetchPromises.push({ + }, + { key: "requesty", options: { provider: "requesty", apiKey: apiConfiguration.requestyApiKey, baseUrl: apiConfiguration.requestyBaseUrl, }, - }) - } - - // Glama - only if it's the active provider - if (apiConfiguration.apiProvider === "glama") { - modelFetchPromises.push({ key: "glama", options: { provider: "glama" } }) - } - - // Unbound - only if API key is provided - if (apiConfiguration.unboundApiKey) { - modelFetchPromises.push({ - key: "unbound", - options: { provider: "unbound", apiKey: apiConfiguration.unboundApiKey }, - }) - } - - // Chutes - only if API key is provided - if (apiConfiguration.chutesApiKey) { - modelFetchPromises.push({ - key: "chutes", - options: { provider: "chutes", apiKey: apiConfiguration.chutesApiKey }, - }) - } - - // Kilocode OpenRouter - only if token is provided - if (apiConfiguration.kilocodeToken) { - modelFetchPromises.push({ + }, + { key: "glama", options: { provider: "glama" } }, + { key: "unbound", options: { provider: "unbound", apiKey: apiConfiguration.unboundApiKey } }, + { key: "chutes", options: { provider: "chutes", apiKey: apiConfiguration.chutesApiKey } }, // kilocode_change + { key: "kilocode-openrouter", options: { provider: "kilocode-openrouter", kilocodeToken: apiConfiguration.kilocodeToken, kilocodeOrganizationId: apiConfiguration.kilocodeOrganizationId, }, - }) - } - - // Ollama - only if it's the active provider or base URL is configured - if (apiConfiguration.apiProvider === "ollama" || apiConfiguration.ollamaBaseUrl) { - modelFetchPromises.push({ - key: "ollama", - options: { provider: "ollama", baseUrl: apiConfiguration.ollamaBaseUrl }, - }) - } - - // Vercel AI Gateway - only if it's the active provider - if (apiConfiguration.apiProvider === "vercel-ai-gateway") { - modelFetchPromises.push({ key: "vercel-ai-gateway", options: { provider: "vercel-ai-gateway" } }) - } - - // DeepInfra - only if API key is provided - if (apiConfiguration.deepInfraApiKey) { - modelFetchPromises.push({ + }, + { key: "ollama", options: { provider: "ollama", baseUrl: apiConfiguration.ollamaBaseUrl } }, + { key: "vercel-ai-gateway", options: { provider: "vercel-ai-gateway" } }, + { key: "deepinfra", options: { provider: "deepinfra", apiKey: apiConfiguration.deepInfraApiKey, baseUrl: apiConfiguration.deepInfraBaseUrl, }, - }) - } - - // OVHCloud - only if API key is provided - if (apiConfiguration.ovhCloudAiEndpointsApiKey) { - modelFetchPromises.push({ + }, + // kilocode_change start + { key: "ovhcloud", options: { provider: "ovhcloud", apiKey: apiConfiguration.ovhCloudAiEndpointsApiKey, baseUrl: apiConfiguration.ovhCloudAiEndpointsBaseUrl, }, - }) - } + }, + // kilocode_change end + ] // kilocode_change end // Add IO Intelligence if API key is provided.