From 2aeafc278e00c9d851e8cef1425d227e0c404df2 Mon Sep 17 00:00:00 2001 From: Smiral Rashinkar Date: Fri, 18 Sep 2026 00:17:08 +0530 Subject: [PATCH 01/23] feat: add IO Intelligence provider Restore the IO Intelligence (io.net) provider removed in #11297, updated to the current architecture: dynamic model catalog fetched from GET /models, streaming chat completions with reasoning and tool-call support, usage reporting, settings UI, and translations for all supported locales. --- .../utils/__tests__/context-window.test.ts | 1 + apps/cli/src/lib/utils/context-window.ts | 3 +- packages/types/src/global-settings.ts | 1 + packages/types/src/provider-identifiers.ts | 2 +- packages/types/src/provider-settings.ts | 7 + packages/types/src/provider-settings/index.ts | 2 + .../src/provider-settings/io-intelligence.ts | 17 ++ packages/types/src/providers/index.ts | 4 + .../types/src/providers/io-intelligence.ts | 63 +++++++ src/api/__tests__/index.spec.ts | 2 + src/api/index.ts | 3 + .../__tests__/io-intelligence.spec.ts | 177 ++++++++++++++++++ .../fetchers/__tests__/modelCache.spec.ts | 23 +++ src/api/providers/fetchers/io-intelligence.ts | 75 ++++++++ src/api/providers/fetchers/modelCache.ts | 9 + src/api/providers/index.ts | 1 + src/api/providers/io-intelligence.ts | 155 +++++++++++++++ .../webview/__tests__/ClineProvider.spec.ts | 4 + ...webviewMessageHandler.routerModels.spec.ts | 12 ++ .../__tests__/webviewMessageHandler.spec.ts | 6 + src/core/webview/webviewMessageHandler.ts | 13 ++ src/shared/ProfileValidator.ts | 2 + src/shared/__tests__/ProfileValidator.spec.ts | 16 ++ src/shared/api.ts | 1 + .../src/components/settings/ApiOptions.tsx | 12 ++ .../src/components/settings/ModelPicker.tsx | 1 + .../ApiOptions.interactions.spec.tsx | 2 + .../ApiOptions.provider-filtering.spec.tsx | 1 + .../settings/__tests__/ApiOptions.spec.tsx | 1 + .../src/components/settings/constants.ts | 1 + .../settings/providers/IOIntelligence.tsx | 94 ++++++++++ .../providers/__tests__/NanoGPT.spec.tsx | 1 + .../components/settings/providers/index.ts | 1 + .../settings/utils/providerModelConfig.ts | 5 + .../components/ui/hooks/useSelectedModel.ts | 10 + webview-ui/src/i18n/locales/ca/settings.json | 6 + webview-ui/src/i18n/locales/de/settings.json | 6 + webview-ui/src/i18n/locales/en/settings.json | 6 + webview-ui/src/i18n/locales/es/settings.json | 6 + webview-ui/src/i18n/locales/fr/settings.json | 6 + webview-ui/src/i18n/locales/hi/settings.json | 6 + webview-ui/src/i18n/locales/id/settings.json | 6 + webview-ui/src/i18n/locales/it/settings.json | 6 + webview-ui/src/i18n/locales/ja/settings.json | 6 + webview-ui/src/i18n/locales/ko/settings.json | 6 + webview-ui/src/i18n/locales/nl/settings.json | 6 + webview-ui/src/i18n/locales/pl/settings.json | 6 + .../src/i18n/locales/pt-BR/settings.json | 6 + webview-ui/src/i18n/locales/ru/settings.json | 6 + webview-ui/src/i18n/locales/tr/settings.json | 6 + webview-ui/src/i18n/locales/vi/settings.json | 6 + .../src/i18n/locales/zh-CN/settings.json | 6 + .../src/i18n/locales/zh-TW/settings.json | 6 + .../src/utils/__tests__/validate.spec.ts | 1 + webview-ui/src/utils/validate.ts | 5 + 55 files changed, 840 insertions(+), 2 deletions(-) create mode 100644 packages/types/src/provider-settings/io-intelligence.ts create mode 100644 packages/types/src/providers/io-intelligence.ts create mode 100644 src/api/providers/__tests__/io-intelligence.spec.ts create mode 100644 src/api/providers/fetchers/io-intelligence.ts create mode 100644 src/api/providers/io-intelligence.ts create mode 100644 webview-ui/src/components/settings/providers/IOIntelligence.tsx diff --git a/apps/cli/src/lib/utils/__tests__/context-window.test.ts b/apps/cli/src/lib/utils/__tests__/context-window.test.ts index 4347292166..d5822c80d1 100644 --- a/apps/cli/src/lib/utils/__tests__/context-window.test.ts +++ b/apps/cli/src/lib/utils/__tests__/context-window.test.ts @@ -14,6 +14,7 @@ describe("getContextWindow", () => { [providerIdentifiers.vercelAiGateway, "vercelAiGatewayModelId"], [providerIdentifiers.opencodeGo, "opencodeGoModelId"], [providerIdentifiers.kenari, "kenariModelId"], + [providerIdentifiers.ioIntelligence, "ioIntelligenceModelId"], [providerIdentifiers.nanogpt, "nanoGptModelId"], [providerIdentifiers.zooGateway, "zooGatewayModelId"], ] as const)("uses the provider-specific model field for %s", (provider, modelField) => { diff --git a/apps/cli/src/lib/utils/context-window.ts b/apps/cli/src/lib/utils/context-window.ts index b78ab0e5b5..e1ed167309 100644 --- a/apps/cli/src/lib/utils/context-window.ts +++ b/apps/cli/src/lib/utils/context-window.ts @@ -50,6 +50,8 @@ function getModelIdForProvider(config: ProviderSettings): string | undefined { return config.unboundModelId case providerIdentifiers.litellm: return config.litellmModelId + case providerIdentifiers.ioIntelligence: + return config.ioIntelligenceModelId case providerIdentifiers.vercelAiGateway: return config.vercelAiGatewayModelId case providerIdentifiers.opencodeGo: @@ -88,7 +90,6 @@ function getModelIdForProvider(config: ProviderSettings): string | undefined { case retiredProviderIdentifiers.featherless: case retiredProviderIdentifiers.groq: case retiredProviderIdentifiers.huggingface: - case retiredProviderIdentifiers.ioIntelligence: case retiredProviderIdentifiers.roo: case providerIdentifiers.vscodeLm: case providerIdentifiers.fakeAi: diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 692798d00d..48718298ad 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -339,6 +339,7 @@ export const SECRET_STATE_KEYS = [ "vercelAiGatewayApiKey", "opencodeGoApiKey", "kenariApiKey", + "ioIntelligenceApiKey", "nanoGptApiKey", "basetenApiKey", ] as const diff --git a/packages/types/src/provider-identifiers.ts b/packages/types/src/provider-identifiers.ts index fdf507bb6b..f1d32b22bc 100644 --- a/packages/types/src/provider-identifiers.ts +++ b/packages/types/src/provider-identifiers.ts @@ -15,6 +15,7 @@ export const providerIdentifiers = { opencodeGo: "opencode-go", kenari: "kenari", nanogpt: "nanogpt", + ioIntelligence: "io-intelligence", ollama: "ollama", lmstudio: "lmstudio", vscodeLm: "vscode-lm", @@ -52,7 +53,6 @@ export const retiredProviderIdentifiers = { featherless: "featherless", groq: "groq", huggingface: "huggingface", - ioIntelligence: "io-intelligence", roo: "roo", } as const diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 0b898f1b66..5027802326 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -73,6 +73,7 @@ export const dynamicProviders = [ providerIdentifiers.opencodeGo, providerIdentifiers.kenari, providerIdentifiers.nanogpt, + providerIdentifiers.ioIntelligence, providerIdentifiers.kimiCode, ] as const @@ -287,6 +288,7 @@ export const modelIdKeys = [ "opencodeGoModelId", "kenariModelId", "nanoGptModelId", + "ioIntelligenceModelId", "zooGatewayModelId", ] as const satisfies readonly ModelIdKey[] @@ -515,6 +517,11 @@ export const MODELS_BY_PROVIDER: Record< }, [providerIdentifiers.opencodeGo]: { id: providerIdentifiers.opencodeGo, label: "Opencode Go", models: [] }, [providerIdentifiers.kenari]: { id: providerIdentifiers.kenari, label: "Kenari", models: [] }, + [providerIdentifiers.ioIntelligence]: { + id: providerIdentifiers.ioIntelligence, + label: "IO Intelligence", + models: [], + }, [providerIdentifiers.nanogpt]: { id: providerIdentifiers.nanogpt, label: "NanoGPT", models: [] }, [providerIdentifiers.zooGateway]: { id: providerIdentifiers.zooGateway, label: "Zoo Gateway", models: [] }, diff --git a/packages/types/src/provider-settings/index.ts b/packages/types/src/provider-settings/index.ts index 3fe8790af1..609c21ed01 100644 --- a/packages/types/src/provider-settings/index.ts +++ b/packages/types/src/provider-settings/index.ts @@ -30,6 +30,7 @@ import { qwenCodeProviderDefinition } from "./qwen-code.js" import { vercelAiGatewayProviderDefinition } from "./vercel-ai-gateway.js" import { opencodeGoProviderDefinition } from "./opencode-go.js" import { kenariProviderDefinition } from "./kenari.js" +import { ioIntelligenceProviderDefinition } from "./io-intelligence.js" import { nanoGptProviderDefinition } from "./nanogpt.js" import { zooGatewayProviderDefinition } from "./zoo-gateway.js" import { basetenProviderDefinition } from "./baseten.js" @@ -82,6 +83,7 @@ export const providerDefinitionList = [ opencodeGoProviderDefinition, kenariProviderDefinition, nanoGptProviderDefinition, + ioIntelligenceProviderDefinition, zooGatewayProviderDefinition, basetenProviderDefinition, ] as const satisfies readonly ProviderDefinition[] diff --git a/packages/types/src/provider-settings/io-intelligence.ts b/packages/types/src/provider-settings/io-intelligence.ts new file mode 100644 index 0000000000..7198e8c1c1 --- /dev/null +++ b/packages/types/src/provider-settings/io-intelligence.ts @@ -0,0 +1,17 @@ +import { z } from "zod" + +import { providerIdentifiers } from "../provider-identifiers.js" +import { baseProviderSettingsShape, createModelIdAccessor, createProviderDefinition } from "./common.js" + +const IO_INTELLIGENCE_MODEL_ID_FIELD = "ioIntelligenceModelId" + +export const ioIntelligenceProviderDefinition = createProviderDefinition({ + apiProvider: providerIdentifiers.ioIntelligence, + modelIdKey: IO_INTELLIGENCE_MODEL_ID_FIELD, + getModelId: createModelIdAccessor(IO_INTELLIGENCE_MODEL_ID_FIELD), + schema: { + ...baseProviderSettingsShape, + ioIntelligenceApiKey: z.string().optional(), + [IO_INTELLIGENCE_MODEL_ID_FIELD]: z.string().optional(), + }, +}) diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index b26476eaed..09a5e2db6b 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -25,6 +25,7 @@ export * from "./xai.js" export * from "./vercel-ai-gateway.js" export * from "./opencode-go.js" export * from "./kenari.js" +export * from "./io-intelligence.js" export * from "./nanogpt.js" export * from "./kimi-code.js" export * from "./zai.js" @@ -55,6 +56,7 @@ import { xaiDefaultModelId } from "./xai.js" import { vercelAiGatewayDefaultModelId } from "./vercel-ai-gateway.js" import { opencodeGoDefaultModelId } from "./opencode-go.js" import { kenariDefaultModelId } from "./kenari.js" +import { ioIntelligenceDefaultModelId } from "./io-intelligence.js" import { nanoGptDefaultModelId } from "./nanogpt.js" import { kimiCodeDefaultModelId } from "./kimi-code.js" import { internationalZAiDefaultModelId, mainlandZAiDefaultModelId } from "./zai.js" @@ -135,6 +137,8 @@ export function getProviderDefaultModelId( return opencodeGoDefaultModelId case providerIdentifiers.kenari: return kenariDefaultModelId + case providerIdentifiers.ioIntelligence: + return ioIntelligenceDefaultModelId case providerIdentifiers.nanogpt: return nanoGptDefaultModelId case providerIdentifiers.kimiCode: diff --git a/packages/types/src/providers/io-intelligence.ts b/packages/types/src/providers/io-intelligence.ts new file mode 100644 index 0000000000..615f49ffc4 --- /dev/null +++ b/packages/types/src/providers/io-intelligence.ts @@ -0,0 +1,63 @@ +import type { ModelInfo } from "../model.js" + +export const IO_INTELLIGENCE_BASE_URL = "https://api.intelligence.io.solutions/api/v1" + +export const ioIntelligenceDefaultModelId = "meta-llama/Llama-3.3-70B-Instruct" + +export const ioIntelligenceDefaultModelInfo: ModelInfo = { + maxTokens: 8192, + contextWindow: 128_000, + supportsImages: false, + supportsPromptCache: false, + description: "IO Intelligence model. The full model catalog is resolved dynamically from the /models endpoint.", +} + +/** + * Static fallback catalog. IO Intelligence serves an OpenAI-compatible + * /models endpoint; these ids mirror the live catalog so that a sensible + * set is available before the dynamic fetch completes. + */ +export const ioIntelligenceModels: Record = { + "meta-llama/Llama-3.3-70B-Instruct": { + maxTokens: 8192, + contextWindow: 128_000, + supportsImages: false, + supportsPromptCache: false, + description: "Meta: Llama 3.3 70B Instruct", + }, + "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": { + maxTokens: 8192, + contextWindow: 430_000, + supportsImages: false, + supportsPromptCache: false, + description: "Meta-Llama: Llama 4 Maverick 17B 128E Instruct FP8", + }, + "deepseek-ai/DeepSeek-R1-0528": { + maxTokens: 8192, + contextWindow: 128_000, + supportsImages: false, + supportsPromptCache: false, + description: "DeepSeek: R1 0528", + }, + "Intel/Qwen3-Coder-480B-A35B-Instruct-int4-mixed-ar": { + maxTokens: 8192, + contextWindow: 106_000, + supportsImages: false, + supportsPromptCache: false, + description: "Intel: Qwen3 Coder 480B A35B Instruct INT4 Mixed AR", + }, + "openai/gpt-oss-120b": { + maxTokens: 8192, + contextWindow: 131_072, + supportsImages: false, + supportsPromptCache: false, + description: "OpenAI: gpt-oss-120b", + }, + "zai-org/GLM-4.6": { + maxTokens: 8192, + contextWindow: 128_000, + supportsImages: false, + supportsPromptCache: false, + description: "Z.ai: GLM 4.6", + }, +} diff --git a/src/api/__tests__/index.spec.ts b/src/api/__tests__/index.spec.ts index 2fe940f53a..ddf2d60ef6 100644 --- a/src/api/__tests__/index.spec.ts +++ b/src/api/__tests__/index.spec.ts @@ -39,6 +39,7 @@ import { FireworksHandler, FriendliHandler, GeminiHandler, + IOIntelligenceHandler, KenariHandler, KimiCodeHandler, LiteLLMHandler, @@ -100,6 +101,7 @@ const expectedHandlers = { [providerIdentifiers.vercelAiGateway]: VercelAiGatewayHandler, [providerIdentifiers.opencodeGo]: OpencodeGoHandler, [providerIdentifiers.kenari]: KenariHandler, + [providerIdentifiers.ioIntelligence]: IOIntelligenceHandler, [providerIdentifiers.nanogpt]: NanoGptHandler, [providerIdentifiers.zooGateway]: ZooGatewayHandler, [providerIdentifiers.minimax]: MiniMaxHandler, diff --git a/src/api/index.ts b/src/api/index.ts index 98c3c5dc7b..119ad1e189 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -42,6 +42,7 @@ import { VercelAiGatewayHandler, OpencodeGoHandler, KenariHandler, + IOIntelligenceHandler, NanoGptHandler, ZooGatewayHandler, MiniMaxHandler, @@ -225,6 +226,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { return new OpencodeGoHandler(options) case providerIdentifiers.kenari: return new KenariHandler(options) + case providerIdentifiers.ioIntelligence: + return new IOIntelligenceHandler(options) case providerIdentifiers.nanogpt: return new NanoGptHandler(options) case providerIdentifiers.zooGateway: diff --git a/src/api/providers/__tests__/io-intelligence.spec.ts b/src/api/providers/__tests__/io-intelligence.spec.ts new file mode 100644 index 0000000000..00c18011d9 --- /dev/null +++ b/src/api/providers/__tests__/io-intelligence.spec.ts @@ -0,0 +1,177 @@ +vi.mock("vscode", () => ({ + workspace: { getConfiguration: () => ({ get: (_key: string, defaultValue?: unknown) => defaultValue }) }, +})) + +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" + +import { ioIntelligenceDefaultModelId, providerIdentifiers } from "@roo-code/types" + +import { buildApiHandler } from "../../index" +import { asyncStreamFrom, collectStream } from "../../../test-utils/stream" +import { IOIntelligenceHandler } from "../io-intelligence" +import { getModels } from "../fetchers/modelCache" + +vi.mock("openai") +vi.mock("../fetchers/modelCache", () => ({ + getModels: vi.fn().mockResolvedValue({ + "meta-llama/Llama-3.3-70B-Instruct": { + maxTokens: 8192, + contextWindow: 128000, + supportsImages: false, + supportsPromptCache: false, + }, + }), + getModelsFromCache: vi.fn(), + refreshModels: vi.fn().mockResolvedValue({}), +})) + +const mockCreate = vi.fn() +vi.mocked(OpenAI).mockImplementation(function () { + return { chat: { completions: { create: mockCreate } } } as unknown as OpenAI +}) + +const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }] + +describe("IOIntelligenceHandler", () => { + beforeEach(() => { + vi.clearAllMocks() + vi.mocked(getModels).mockResolvedValue({ + "meta-llama/Llama-3.3-70B-Instruct": { + maxTokens: 8192, + contextWindow: 128000, + supportsImages: false, + supportsPromptCache: false, + }, + }) + mockCreate.mockResolvedValue(asyncStreamFrom([])) + }) + + it("is constructed by the backend provider registry", () => { + expect(buildApiHandler({ apiProvider: providerIdentifiers.ioIntelligence })).toBeInstanceOf( + IOIntelligenceHandler, + ) + }) + + it("resolves the default model when none is configured", async () => { + const handler = new IOIntelligenceHandler({}) + await collectStream(handler.createMessage("system", messages)) + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ model: ioIntelligenceDefaultModelId }), + expect.objectContaining({ signal: undefined }), + ) + }) + + it("uses the configured model id and streams text, reasoning, and tool calls", async () => { + mockCreate.mockResolvedValue( + asyncStreamFrom([ + { choices: [{ delta: { content: "answer" } }] }, + { choices: [{ delta: { reasoning_content: "thinking" } }] }, + { + choices: [ + { + delta: { + tool_calls: [ + { index: 0, id: "call-1", function: { name: "read_file", arguments: '{"path":' } }, + ], + }, + }, + ], + }, + ]), + ) + const chunks = await collectStream( + new IOIntelligenceHandler({ + ioIntelligenceModelId: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8", + }).createMessage("sys", messages), + ) + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ model: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8" }), + expect.objectContaining({ signal: undefined }), + ) + expect(chunks).toEqual([ + { type: "text", text: "answer" }, + { type: "reasoning", text: "thinking" }, + { type: "tool_call_partial", index: 0, id: "call-1", name: "read_file", arguments: '{"path":' }, + ]) + }) + + it("forwards native tools, usage streaming, max_tokens, and cancellation", async () => { + const signal = new AbortController().signal + const tools: OpenAI.Chat.ChatCompletionTool[] = [ + { type: "function", function: { name: "read_file", description: "Read", parameters: { type: "object" } } }, + ] + const handler = new IOIntelligenceHandler({ + ioIntelligenceModelId: "meta-llama/Llama-3.3-70B-Instruct", + modelTemperature: 0.7, + }) + await collectStream( + handler.createMessage("sys", messages, { + taskId: "task", + tools, + tool_choice: "required", + parallelToolCalls: false, + abortSignal: signal, + }), + ) + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + stream: true, + stream_options: { include_usage: true }, + max_tokens: 8192, + temperature: 0.7, + tools: [ + expect.objectContaining({ + type: "function", + function: expect.objectContaining({ name: "read_file", description: "Read" }), + }), + ], + tool_choice: "required", + parallel_tool_calls: false, + }), + { signal }, + ) + expect(mockCreate.mock.calls[0][0]).not.toHaveProperty("max_completion_tokens") + }) + + it("maps usage from the final chunk", async () => { + mockCreate.mockResolvedValue( + asyncStreamFrom([ + { + choices: [], + usage: { + prompt_tokens: 20, + completion_tokens: 10, + prompt_tokens_details: { cached_tokens: 5 }, + }, + }, + ]), + ) + expect( + await collectStream( + new IOIntelligenceHandler({ ioIntelligenceModelId: "meta-llama/Llama-3.3-70B-Instruct" }).createMessage( + "sys", + messages, + ), + ), + ).toEqual([ + { + type: "usage", + inputTokens: 20, + outputTokens: 10, + cacheReadTokens: 5, + }, + ]) + }) + + it("redacts the API key from streaming errors", async () => { + mockCreate.mockRejectedValue(new Error("upstream rejected secret-key")) + const handler = new IOIntelligenceHandler({ + ioIntelligenceApiKey: "secret-key", + ioIntelligenceModelId: "meta-llama/Llama-3.3-70B-Instruct", + }) + await expect(collectStream(handler.createMessage("sys", messages))).rejects.toMatchObject({ + message: "IO Intelligence streaming error: upstream rejected [REDACTED]", + }) + }) +}) diff --git a/src/api/providers/fetchers/__tests__/modelCache.spec.ts b/src/api/providers/fetchers/__tests__/modelCache.spec.ts index 108aa1827b..d8d0a03afc 100644 --- a/src/api/providers/fetchers/__tests__/modelCache.spec.ts +++ b/src/api/providers/fetchers/__tests__/modelCache.spec.ts @@ -44,6 +44,7 @@ vi.mock("fs", () => ({ vi.mock("../litellm") vi.mock("../openrouter") vi.mock("../requesty") +vi.mock("../io-intelligence") vi.mock("../kenari") vi.mock("../nanogpt") vi.mock("../moonshot") @@ -70,6 +71,7 @@ import { getModels, getModelsFromCache } from "../modelCache" import { getLiteLLMModels } from "../litellm" import { getOpenRouterModels } from "../openrouter" import { getRequestyModels } from "../requesty" +import { getIOIntelligenceModels } from "../io-intelligence" import { getKenariModels } from "../kenari" import { getNanoGptModels } from "../nanogpt" import { getMoonshotModels } from "../moonshot" @@ -78,12 +80,14 @@ import { getZooGatewayModels } from "../zoo-gateway" const mockGetLiteLLMModels = getLiteLLMModels as Mock const mockGetOpenRouterModels = getOpenRouterModels as Mock const mockGetRequestyModels = getRequestyModels as Mock +const mockGetIOIntelligenceModels = getIOIntelligenceModels as Mock const mockGetKenariModels = getKenariModels as Mock const mockGetNanoGptModels = getNanoGptModels as Mock const mockGetMoonshotModels = getMoonshotModels as Mock const mockGetZooGatewayModels = getZooGatewayModels as Mock const DUMMY_REQUESTY_KEY = "requesty-key-for-testing" +const DUMMY_IOINTELLIGENCE_KEY = "io-intelligence-key-for-testing" describe("getModels with new GetModelsOptions", () => { beforeEach(() => { @@ -216,6 +220,25 @@ describe("getModels with new GetModelsOptions", () => { expect(result).toEqual(mockModels) }) + it("calls getIOIntelligenceModels for the io-intelligence provider", async () => { + const mockModels = { + "meta-llama/Llama-3.3-70B-Instruct": { + maxTokens: 8192, + contextWindow: 128000, + supportsPromptCache: false, + }, + } + mockGetIOIntelligenceModels.mockResolvedValue(mockModels) + + const result = await getModels({ + provider: providerIdentifiers.ioIntelligence, + apiKey: DUMMY_IOINTELLIGENCE_KEY, + }) + + expect(mockGetIOIntelligenceModels).toHaveBeenCalledWith(DUMMY_IOINTELLIGENCE_KEY) + expect(result).toEqual(mockModels) + }) + it("handles errors and re-throws them", async () => { const expectedError = new Error("LiteLLM connection failed") mockGetLiteLLMModels.mockRejectedValue(expectedError) diff --git a/src/api/providers/fetchers/io-intelligence.ts b/src/api/providers/fetchers/io-intelligence.ts new file mode 100644 index 0000000000..8cbc67dec2 --- /dev/null +++ b/src/api/providers/fetchers/io-intelligence.ts @@ -0,0 +1,75 @@ +import axios from "axios" +import { z } from "zod" + +import { + IO_INTELLIGENCE_BASE_URL, + ioIntelligenceDefaultModelInfo, + type ModelInfo, + type ModelRecord, +} from "@roo-code/types" + +const ioIntelligenceModelSchema = z.object({ + id: z.string().min(1), + name: z.string().optional(), + object: z.string().optional(), + created: z.number().optional(), + owned_by: z.string().optional(), + root: z.string().nullable().optional(), + parent: z.string().nullable().optional(), + max_model_len: z.number().nullable().optional(), +}) + +export type IOIntelligenceModel = z.infer + +const ioIntelligenceModelsResponseSchema = z.object({ + object: z.string().optional(), + data: z.array(z.unknown()), +}) + +function getSafeErrorMessage(error: unknown, apiKey?: string): string { + const message = error instanceof Error ? error.message : String(error) + return apiKey ? message.replaceAll(apiKey, "[REDACTED]") : message +} + +export const parseIoIntelligenceModel = (model: IOIntelligenceModel): ModelInfo => ({ + maxTokens: ioIntelligenceDefaultModelInfo.maxTokens, + contextWindow: model.max_model_len ?? ioIntelligenceDefaultModelInfo.contextWindow, + supportsImages: false, + supportsPromptCache: false, + ...(model.name !== undefined ? { displayName: model.name } : {}), + description: model.name ?? model.id, +}) + +/** + * Fetches IO Intelligence's public /models catalog, optionally scoped by a + * Bearer key. Model ids are Hugging Face-style `org/name` identifiers. + */ +export async function getIOIntelligenceModels(apiKey?: string): Promise { + try { + const response = await axios.get(`${IO_INTELLIGENCE_BASE_URL}/models`, { + headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined, + timeout: 10_000, + }) + const responseResult = ioIntelligenceModelsResponseSchema.safeParse(response.data) + if (!responseResult.success) { + console.warn("IO Intelligence models response did not match the expected top-level schema") + return {} + } + + const models: ModelRecord = {} + for (const rawModel of responseResult.data.data) { + const modelResult = ioIntelligenceModelSchema.safeParse(rawModel) + if (!modelResult.success) { + console.warn("Skipping invalid IO Intelligence model entry") + continue + } + + models[modelResult.data.id] = parseIoIntelligenceModel(modelResult.data) + } + + return models + } catch (error) { + console.error(`Error fetching IO Intelligence models: ${getSafeErrorMessage(error, apiKey)}`) + return {} + } +} diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index 50dbe12f6e..0028f91b65 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -21,6 +21,7 @@ import { getOpenRouterModels } from "./openrouter" import { getVercelAiGatewayModels } from "./vercel-ai-gateway" import { getOpencodeGoModels } from "./opencode-go" import { getKenariModels } from "./kenari" +import { getIOIntelligenceModels } from "./io-intelligence" import { getNanoGptModels } from "./nanogpt" import { getRequestyModels } from "./requesty" import { getUnboundModels } from "./unbound" @@ -255,6 +256,10 @@ async function fetchModelsFromProvider(options: GetModelsOptions): Promise { provider: providerIdentifiers.nanogpt, options: { provider: providerIdentifiers.nanogpt }, }, + { + provider: providerIdentifiers.ioIntelligence, + options: { provider: providerIdentifiers.ioIntelligence }, + }, ] // Refresh each provider in background (fire and forget) diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index 8ba1eae382..3d2cab72c6 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -30,6 +30,7 @@ export { FriendliHandler } from "./friendli" export { VercelAiGatewayHandler } from "./vercel-ai-gateway" export { OpencodeGoHandler } from "./opencode-go" export { KenariHandler } from "./kenari" +export { IOIntelligenceHandler } from "./io-intelligence" export { NanoGptHandler } from "./nanogpt" export { ZooGatewayHandler } from "./zoo-gateway" export { MiniMaxHandler } from "./minimax" diff --git a/src/api/providers/io-intelligence.ts b/src/api/providers/io-intelligence.ts new file mode 100644 index 0000000000..e43f9a3e47 --- /dev/null +++ b/src/api/providers/io-intelligence.ts @@ -0,0 +1,155 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" + +import { + IO_INTELLIGENCE_BASE_URL, + ioIntelligenceDefaultModelId, + ioIntelligenceDefaultModelInfo, + providerIdentifiers, +} from "@roo-code/types" + +import type { ApiHandlerOptions } from "../../shared/api" + +import type { ApiStream } from "../transform/stream" +import { convertToOpenAiMessages } from "../transform/openai-format" +import type { ApiHandlerCreateMessageMetadata, CompletePromptOptions, SingleCompletionHandler } from "../index" +import { RouterProvider } from "./router-provider" +import { handleProviderError } from "./utils/error-handler" +import { extractReasoningFromDelta } from "./utils/extract-reasoning" + +/** + * IO Intelligence (io.net) provider. + * + * OpenAI-compatible Chat Completions endpoint: + * https://api.intelligence.io.solutions/api/v1/chat/completions + * + * Model ids are Hugging Face-style `org/name` identifiers and are resolved + * dynamically from the public /models endpoint, with a static fallback + * catalog. Supports text generation, reasoning content (DeepSeek/GLM style), + * tool calls, and non-streaming prompt completion. + */ +export class IOIntelligenceHandler extends RouterProvider implements SingleCompletionHandler { + /** Creates a new handler bound to the user's API key and selected model. */ + constructor(options: ApiHandlerOptions) { + super({ + options, + name: providerIdentifiers.ioIntelligence, + baseURL: IO_INTELLIGENCE_BASE_URL, + apiKey: options.ioIntelligenceApiKey, + modelId: options.ioIntelligenceModelId, + defaultModelId: ioIntelligenceDefaultModelId, + defaultModelInfo: ioIntelligenceDefaultModelInfo, + }) + } + + private createSafeError(operation: string, error: unknown): Error { + return handleProviderError(error, "IO Intelligence", { + messagePrefix: operation, + messageTransformer: (message) => + this.options.ioIntelligenceApiKey + ? `IO Intelligence ${operation} error: ${message.replaceAll(this.options.ioIntelligenceApiKey, "[REDACTED]")}` + : `IO Intelligence ${operation} error: ${message}`, + }) + } + + /** + * Streams a chat completion response, yielding typed chunks for text, + * reasoning, partial tool calls, and token usage. + */ + override async *createMessage( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + metadata?: ApiHandlerCreateMessageMetadata, + ): ApiStream { + const { id: modelId, info } = await this.fetchModel() + + const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [ + { role: "system", content: systemPrompt }, + ...convertToOpenAiMessages(messages), + ] + + const body: OpenAI.Chat.ChatCompletionCreateParams = { + model: modelId, + messages: openAiMessages, + max_tokens: info.maxTokens, + stream: true, + stream_options: { include_usage: true }, + tools: this.convertToolsForOpenAI(metadata?.tools), + tool_choice: metadata?.tool_choice, + parallel_tool_calls: metadata?.parallelToolCalls ?? true, + } + + if (this.supportsTemperature(modelId)) { + body.temperature = this.options.modelTemperature + } + + let completion: Awaited> + try { + completion = await this.client.chat.completions.create(body, { signal: metadata?.abortSignal }) + } catch (error) { + throw this.createSafeError("streaming", error) + } + + for await (const chunk of completion) { + const delta = chunk.choices[0]?.delta + + // Reasoning models (DeepSeek R1, GLM) stream reasoning via + // reasoning_content with an OpenRouter-style `reasoning` fallback. + const reasoningText = extractReasoningFromDelta(delta) + if (reasoningText) { + yield { type: "reasoning", text: reasoningText } + } + + if (delta?.content) { + yield { type: "text", text: delta.content } + } + + // Emit raw tool call chunks - NativeToolCallParser handles state management. + if (delta?.tool_calls) { + for (const toolCall of delta.tool_calls) { + yield { + type: "tool_call_partial", + index: toolCall.index, + id: toolCall.id, + name: toolCall.function?.name, + arguments: toolCall.function?.arguments, + } + } + } + + if (chunk.usage) { + yield { + type: "usage", + inputTokens: chunk.usage.prompt_tokens || 0, + outputTokens: chunk.usage.completion_tokens || 0, + cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || undefined, + } + } + } + } + + /** + * Performs a non-streaming chat completion and returns the full response text. + * + * @param prompt - The user prompt to send as a single user message. + * @returns The model's reply text, or an empty string if no content is returned. + * @throws Error with an IO Intelligence prefix if the request fails. + */ + async completePrompt(prompt: string, options?: CompletePromptOptions): Promise { + const { id: modelId, info } = await this.fetchModel() + + try { + const requestOptions: OpenAI.Chat.ChatCompletionCreateParams = { + model: modelId, + messages: [{ role: "user", content: prompt }], + max_tokens: info.maxTokens, + stream: false, + } + + const response = await this.client.chat.completions.create(requestOptions) + return response.choices[0]?.message.content || "" + } catch (error) { + throw this.createSafeError("completion", error) + } + } +} diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 97c4dd877e..d176e066a1 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -3801,6 +3801,7 @@ describe("ClineProvider - Router Models", () => { moonshot: {}, "opencode-go": mockModels, kenari: mockModels, + "io-intelligence": mockModels, nanogpt: mockModels, "kimi-code": {}, }, @@ -3836,6 +3837,7 @@ describe("ClineProvider - Router Models", () => { .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm fail .mockResolvedValueOnce(mockModels) // opencode-go (public endpoint) .mockResolvedValueOnce(mockModels) // kenari (public endpoint) + .mockResolvedValueOnce(mockModels) // io-intelligence (public endpoint) .mockResolvedValueOnce(mockModels) // nanogpt (public endpoint) await messageHandler({ type: "requestRouterModels" }) @@ -3857,6 +3859,7 @@ describe("ClineProvider - Router Models", () => { moonshot: {}, "opencode-go": mockModels, kenari: mockModels, + "io-intelligence": mockModels, nanogpt: mockModels, "kimi-code": {}, }, @@ -3958,6 +3961,7 @@ describe("ClineProvider - Router Models", () => { moonshot: {}, "opencode-go": mockModels, kenari: mockModels, + "io-intelligence": mockModels, nanogpt: mockModels, "kimi-code": {}, }, diff --git a/src/core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts index 5a4b3e7be3..51ffd5a614 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts @@ -94,6 +94,8 @@ describe("webviewMessageHandler - requestRouterModels provider filter", () => { return { "requesty/model": { contextWindow: 8192, supportsPromptCache: false } } case providerIdentifiers.vercelAiGateway: return { "vercel/model": { contextWindow: 8192, supportsPromptCache: false } } + case providerIdentifiers.ioIntelligence: + return { "io/model": { contextWindow: 8192, supportsPromptCache: false } } case providerIdentifiers.litellm: return { "litellm/model": { contextWindow: 8192, supportsPromptCache: false } } default: @@ -162,6 +164,8 @@ describe("webviewMessageHandler - requestRouterModels provider filter", () => { return { "requesty/model": { contextWindow: 8192, supportsPromptCache: false } } case providerIdentifiers.vercelAiGateway: return { "vercel/model": { contextWindow: 8192, supportsPromptCache: false } } + case providerIdentifiers.ioIntelligence: + return { "io/model": { contextWindow: 8192, supportsPromptCache: false } } case providerIdentifiers.litellm: return { "litellm/model": { contextWindow: 8192, supportsPromptCache: false } } default: @@ -210,6 +214,8 @@ describe("webviewMessageHandler - requestRouterModels provider filter", () => { return { "requesty/model": { contextWindow: 8192, supportsPromptCache: false } } case providerIdentifiers.vercelAiGateway: return { "vercel/model": { contextWindow: 8192, supportsPromptCache: false } } + case providerIdentifiers.ioIntelligence: + return { "io/model": { contextWindow: 8192, supportsPromptCache: false } } case providerIdentifiers.litellm: return { "litellm/model": { contextWindow: 8192, supportsPromptCache: false } } default: @@ -467,6 +473,8 @@ describe("webviewMessageHandler - requestRouterModels provider filter", () => { return { "requesty/model": { contextWindow: 8192, supportsPromptCache: false } } case providerIdentifiers.vercelAiGateway: return { "vercel/model": { contextWindow: 8192, supportsPromptCache: false } } + case providerIdentifiers.ioIntelligence: + return { "io/model": { contextWindow: 8192, supportsPromptCache: false } } case providerIdentifiers.litellm: return { "litellm/model": { contextWindow: 8192, supportsPromptCache: false } } default: @@ -554,6 +562,8 @@ describe("webviewMessageHandler - requestRouterModels provider filter", () => { return { "requesty/model": { contextWindow: 8192, supportsPromptCache: false } } case providerIdentifiers.vercelAiGateway: return { "vercel/model": { contextWindow: 8192, supportsPromptCache: false } } + case providerIdentifiers.ioIntelligence: + return { "io/model": { contextWindow: 8192, supportsPromptCache: false } } case providerIdentifiers.litellm: return { "litellm/model": { contextWindow: 8192, supportsPromptCache: false } } default: @@ -605,6 +615,8 @@ describe("webviewMessageHandler - requestRouterModels provider filter", () => { return { "requesty/model": { contextWindow: 8192, supportsPromptCache: false } } case providerIdentifiers.vercelAiGateway: return { "vercel/model": { contextWindow: 8192, supportsPromptCache: false } } + case providerIdentifiers.ioIntelligence: + return { "io/model": { contextWindow: 8192, supportsPromptCache: false } } case providerIdentifiers.litellm: return { "litellm/model": { contextWindow: 8192, supportsPromptCache: false } } default: diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 4c2a301965..f05db6e463 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -556,6 +556,8 @@ describe("webviewMessageHandler - requestRouterModels", () => { ) // Kenari's /models endpoint is public, so it is fetched like the other no-auth routers. expect(mockGetModels).toHaveBeenCalledWith(expect.objectContaining({ provider: providerIdentifiers.kenari })) + // IO Intelligence's /models catalog is public and may optionally be scoped by a key. + expect(mockGetModels).toHaveBeenCalledWith({ provider: providerIdentifiers.ioIntelligence, apiKey: undefined }) // NanoGPT's detailed catalog is public and may optionally be scoped by a key. expect(mockGetModels).toHaveBeenCalledWith({ provider: providerIdentifiers.nanogpt, apiKey: undefined }) @@ -576,6 +578,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { moonshot: {}, "opencode-go": mockModels, kenari: mockModels, + "io-intelligence": mockModels, nanogpt: mockModels, "kimi-code": {}, }, @@ -824,6 +827,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { moonshot: {}, "opencode-go": mockModels, kenari: mockModels, + "io-intelligence": mockModels, nanogpt: mockModels, "kimi-code": {}, }, @@ -851,6 +855,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm .mockResolvedValueOnce(mockModels) // opencode-go .mockResolvedValueOnce(mockModels) // kenari + .mockResolvedValueOnce(mockModels) // io-intelligence .mockResolvedValueOnce(mockModels) // nanogpt await webviewMessageHandler(mockClineProvider, { @@ -889,6 +894,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { moonshot: {}, "opencode-go": mockModels, kenari: mockModels, + "io-intelligence": mockModels, nanogpt: mockModels, "kimi-code": {}, }, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 34a35ea3ca..5a0c60474c 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1132,6 +1132,7 @@ export const webviewMessageHandler = async ( [providerIdentifiers.moonshot]: {}, [providerIdentifiers.opencodeGo]: {}, [providerIdentifiers.kenari]: {}, + [providerIdentifiers.ioIntelligence]: {}, [providerIdentifiers.nanogpt]: {}, [providerIdentifiers.kimiCode]: {}, } @@ -1302,6 +1303,18 @@ export const webviewMessageHandler = async ( options: { provider: providerIdentifiers.kenari, apiKey: kenariApiKey }, }) + // IO Intelligence's /models catalog is public; an optional key can scope + // the visible model set. Prefer an explicitly supplied unsaved key. + const ioIntelligenceApiKey = message?.values?.ioIntelligenceApiKey ?? apiConfiguration.ioIntelligenceApiKey + if (message?.values?.ioIntelligenceApiKey !== undefined) { + await flushModels({ provider: providerIdentifiers.ioIntelligence, apiKey: ioIntelligenceApiKey }, true) + } + + candidates.push({ + key: providerIdentifiers.ioIntelligence, + options: { provider: providerIdentifiers.ioIntelligence, apiKey: ioIntelligenceApiKey }, + }) + // NanoGPT's detailed catalog is public, while an optional key can expose a // different allowlist. Prefer an explicitly supplied unsaved key and use the // same key-scoped options for refresh and retrieval. diff --git a/src/shared/ProfileValidator.ts b/src/shared/ProfileValidator.ts index 923582bd06..60e5c02b7c 100644 --- a/src/shared/ProfileValidator.ts +++ b/src/shared/ProfileValidator.ts @@ -78,6 +78,8 @@ export class ProfileValidator { return profile.ollamaModelId case providerIdentifiers.requesty: return profile.requestyModelId + case providerIdentifiers.ioIntelligence: + return profile.ioIntelligenceModelId case providerIdentifiers.unbound: return profile.unboundModelId case providerIdentifiers.fakeAi: diff --git a/src/shared/__tests__/ProfileValidator.spec.ts b/src/shared/__tests__/ProfileValidator.spec.ts index 865fa5cf51..cdff408529 100644 --- a/src/shared/__tests__/ProfileValidator.spec.ts +++ b/src/shared/__tests__/ProfileValidator.spec.ts @@ -281,6 +281,22 @@ describe("ProfileValidator", () => { expect(ProfileValidator.isProfileAllowed(profile, allowList)).toBe(true) }) + // Test for io-intelligence provider which uses ioIntelligenceModelId + it(`should extract ioIntelligenceModelId for io-intelligence provider`, () => { + const allowList: OrganizationAllowList = { + allowAll: false, + providers: { + [providerIdentifiers.ioIntelligence]: { allowAll: false, models: ["test-model"] }, + }, + } + const profile: ProviderSettings = { + apiProvider: providerIdentifiers.ioIntelligence, + ioIntelligenceModelId: "test-model", + } + + expect(ProfileValidator.isProfileAllowed(profile, allowList)).toBe(true) + }) + it("should extract vsCodeLmModelSelector.id for vscode-lm provider", () => { const allowList: OrganizationAllowList = { allowAll: false, diff --git a/src/shared/api.ts b/src/shared/api.ts index d1cc21cad0..abfe370ab1 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -190,6 +190,7 @@ const dynamicProviderExtras = { [providerIdentifiers.moonshot]: {} as { apiKey?: string; baseUrl?: string }, [providerIdentifiers.opencodeGo]: {} as { apiKey?: string }, [providerIdentifiers.kenari]: {} as { apiKey?: string }, + [providerIdentifiers.ioIntelligence]: {} as { apiKey?: string }, [providerIdentifiers.nanogpt]: {} as { apiKey?: string }, [providerIdentifiers.kimiCode]: {} as { apiKey?: string }, } as const satisfies Record diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 714b507774..87f1ac31ed 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -82,6 +82,7 @@ import { VercelAiGateway, OpenCodeGo, Kenari, + IOIntelligence, NanoGPT, ZooGateway, MiniMax, @@ -681,6 +682,17 @@ const ApiOptions = ({ /> )} + {selectedProvider === providerIdentifiers.ioIntelligence && ( + + )} + {selectedProvider === providerIdentifiers.kenari && ( { VercelAiGateway: provider("provider-vercel-ai-gateway"), OpenCodeGo: provider("provider-opencode-go"), Kenari: provider("provider-kenari"), + IOIntelligence: provider("provider-io-intelligence"), NanoGPT: provider("provider-nanogpt"), ZooGateway: provider("provider-zoo-gateway"), MiniMax: provider("provider-minimax"), @@ -358,6 +359,7 @@ describe("ApiOptions interactions", () => { providerIdentifiers.vercelAiGateway, providerIdentifiers.opencodeGo, providerIdentifiers.kenari, + providerIdentifiers.ioIntelligence, providerIdentifiers.nanogpt, providerIdentifiers.zooGateway, providerIdentifiers.fireworks, diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.provider-filtering.spec.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.provider-filtering.spec.tsx index 58e23511a7..9706533982 100644 --- a/webview-ui/src/components/settings/__tests__/ApiOptions.provider-filtering.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ApiOptions.provider-filtering.spec.tsx @@ -215,6 +215,7 @@ describe("ApiOptions Provider Filtering", () => { expect(providerValues).toContain("ollama") expect(providerValues).toContain("lmstudio") expect(providerValues).toContain("litellm") + expect(providerValues).toContain("io-intelligence") expect(providerValues).toContain("requesty") }) diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx index 341fffe856..073d6c0a2a 100644 --- a/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx @@ -430,6 +430,7 @@ describe("ApiOptions", () => { expect(optionTexts).toContain("OpenAI") expect(optionTexts).toContain("Anthropic") expect(optionTexts).toContain("NanoGPT") + expect(optionTexts).toContain("IO Intelligence") // Note: The mock doesn't implement search functionality, so we're just verifying // that the select element is rendered with the expected options diff --git a/webview-ui/src/components/settings/constants.ts b/webview-ui/src/components/settings/constants.ts index 8c51aca2fe..f252c879b3 100644 --- a/webview-ui/src/components/settings/constants.ts +++ b/webview-ui/src/components/settings/constants.ts @@ -70,6 +70,7 @@ export const PROVIDERS: Array<{ value: string; label: string; proxy: boolean }> { value: providerIdentifiers.vercelAiGateway, label: "Vercel AI Gateway", proxy: false }, { value: providerIdentifiers.opencodeGo, label: "Opencode Go", proxy: false }, { value: providerIdentifiers.kenari, label: "Kenari", proxy: false }, + { value: providerIdentifiers.ioIntelligence, label: "IO Intelligence", proxy: false }, { value: providerIdentifiers.nanogpt, label: "NanoGPT", proxy: false }, { value: providerIdentifiers.zooGateway, label: "Zoo Gateway", proxy: false }, { value: providerIdentifiers.minimax, label: "MiniMax", proxy: false }, diff --git a/webview-ui/src/components/settings/providers/IOIntelligence.tsx b/webview-ui/src/components/settings/providers/IOIntelligence.tsx new file mode 100644 index 0000000000..a437e9f13c --- /dev/null +++ b/webview-ui/src/components/settings/providers/IOIntelligence.tsx @@ -0,0 +1,94 @@ +import { useCallback, useEffect } from "react" +import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" + +import { + type OrganizationAllowList, + type ProviderSettings, + type RouterModels, + ioIntelligenceDefaultModelId, + providerIdentifiers, + RouterModelsMessageType, +} from "@roo-code/types" + +import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" +import { useAppTranslation } from "@src/i18n/TranslationContext" +import { vscode } from "@src/utils/vscode" + +import { ModelPicker } from "../ModelPicker" +import { inputEventTransform } from "../transforms" + +type IOIntelligenceProps = { + apiConfiguration: ProviderSettings + setApiConfigurationField: (field: K, value: ProviderSettings[K]) => void + routerModels?: RouterModels + organizationAllowList: OrganizationAllowList + modelValidationError?: string + simplifySettings?: boolean +} + +export const IOIntelligence = ({ + apiConfiguration, + setApiConfigurationField, + routerModels, + organizationAllowList, + modelValidationError, + simplifySettings, +}: IOIntelligenceProps) => { + const { t } = useAppTranslation() + + const handleInputChange = useCallback( + ( + field: K, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, + ) => + (event: E | Event) => { + setApiConfigurationField(field, transform(event as E)) + }, + [setApiConfigurationField], + ) + + useEffect(() => { + vscode.postMessage({ + type: RouterModelsMessageType.requestRouterModels, + values: { + provider: providerIdentifiers.ioIntelligence, + ioIntelligenceApiKey: apiConfiguration.ioIntelligenceApiKey, + }, + }) + }, [apiConfiguration.ioIntelligenceApiKey]) + + return ( + <> + + + +
+ {t("settings:providers.apiKeyStorageNotice")} +
+ {!apiConfiguration.ioIntelligenceApiKey && ( + + {t("settings:providers.ioIntelligence.getApiKey")} + + )} + + + + ) +} diff --git a/webview-ui/src/components/settings/providers/__tests__/NanoGPT.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/NanoGPT.spec.tsx index bb810caa75..698fa90e29 100644 --- a/webview-ui/src/components/settings/providers/__tests__/NanoGPT.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/NanoGPT.spec.tsx @@ -103,6 +103,7 @@ describe("NanoGPT", () => { "opencode-go": {}, kenari: {}, nanogpt: { "openai/test": { contextWindow: 1, maxTokens: 1, supportsPromptCache: false } }, + "io-intelligence": {}, "kimi-code": {}, ollama: {}, lmstudio: {}, diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index 12ea4cd786..ee583e563e 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -26,6 +26,7 @@ export { Friendli } from "./Friendli" export { VercelAiGateway } from "./VercelAiGateway" export { OpenCodeGo } from "./OpenCodeGo" export { Kenari } from "./Kenari" +export { IOIntelligence } from "./IOIntelligence" export { NanoGPT } from "./NanoGPT" export { ZooGateway } from "./ZooGateway" export { MiniMax } from "./MiniMax" diff --git a/webview-ui/src/components/settings/utils/providerModelConfig.ts b/webview-ui/src/components/settings/utils/providerModelConfig.ts index 7230e4cbe4..5ae0798dd9 100644 --- a/webview-ui/src/components/settings/utils/providerModelConfig.ts +++ b/webview-ui/src/components/settings/utils/providerModelConfig.ts @@ -29,6 +29,7 @@ import { vercelAiGatewayDefaultModelId, opencodeGoDefaultModelId, kenariDefaultModelId, + ioIntelligenceDefaultModelId, nanoGptDefaultModelId, zooGatewayDefaultModelId, zaiApiLineConfigs, @@ -145,6 +146,10 @@ const PROVIDER_MODEL_CONFIG: Partial> }, [providerIdentifiers.opencodeGo]: { field: "opencodeGoModelId", default: opencodeGoDefaultModelId }, [providerIdentifiers.kenari]: { field: "kenariModelId", default: kenariDefaultModelId }, + [providerIdentifiers.ioIntelligence]: { + field: "ioIntelligenceModelId", + default: ioIntelligenceDefaultModelId, + }, [providerIdentifiers.nanogpt]: { field: "nanoGptModelId", default: nanoGptDefaultModelId }, [providerIdentifiers.zooGateway]: { field: "zooGatewayModelId", default: zooGatewayDefaultModelId }, [providerIdentifiers.openai]: { field: "openAiModelId" }, diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 8d4b70ad4a..dd87ba1e03 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -31,6 +31,7 @@ import { lMStudioDefaultModelInfo, opencodeGoDefaultModelInfo, kenariDefaultModelInfo, + ioIntelligenceDefaultModelInfo, nanoGptDefaultModelInfo, BEDROCK_1M_CONTEXT_MODEL_IDS, VERTEX_1M_CONTEXT_MODEL_IDS, @@ -451,6 +452,15 @@ function getSelectedModel({ const info = routerModels[providerIdentifiers.kenari]?.[id] ?? kenariDefaultModelInfo return { id, info } } + case providerIdentifiers.ioIntelligence: { + const id = getValidatedModelId( + apiConfiguration.ioIntelligenceModelId, + routerModels[providerIdentifiers.ioIntelligence], + defaultModelId, + ) + const info = routerModels[providerIdentifiers.ioIntelligence]?.[id] ?? ioIntelligenceDefaultModelInfo + return { id, info } + } case providerIdentifiers.nanogpt: { const id = getValidatedModelId( apiConfiguration.nanoGptModelId, diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 762a1290af..2903d2e1a1 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -402,6 +402,12 @@ "getOpencodeGoApiKey": "Obtenir clau API de Opencode Go", "kenariApiKey": "Clau API de Kenari", "getKenariApiKey": "Obtenir clau API de Kenari", + "ioIntelligence": { + "provider": "IO Intelligence", + "apiKey": "Clau API de IO Intelligence", + "getApiKey": "Obtenir clau API de IO Intelligence", + "model": "Model" + }, "nanoGpt": { "provider": "NanoGPT", "apiKey": "Clau API de NanoGPT", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index f56984bc73..7b21c8a086 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -402,6 +402,12 @@ "getOpencodeGoApiKey": "Opencode Go API-Schlüssel erhalten", "kenariApiKey": "Kenari API-Schlüssel", "getKenariApiKey": "Kenari API-Schlüssel erhalten", + "ioIntelligence": { + "provider": "IO Intelligence", + "apiKey": "IO Intelligence API-Schlüssel", + "getApiKey": "IO Intelligence API-Schlüssel holen", + "model": "Modell" + }, "nanoGpt": { "provider": "NanoGPT", "apiKey": "NanoGPT API-Schlüssel", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 77f8a4f86d..13f950c5dd 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -482,6 +482,12 @@ "getOpencodeGoApiKey": "Get Opencode Go API Key", "kenariApiKey": "Kenari API Key", "getKenariApiKey": "Get Kenari API Key", + "ioIntelligence": { + "provider": "IO Intelligence", + "apiKey": "IO Intelligence API Key", + "getApiKey": "Get IO Intelligence API Key", + "model": "Model" + }, "nanoGpt": { "provider": "NanoGPT", "apiKey": "NanoGPT API Key", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index e730483fa0..feb303811d 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -402,6 +402,12 @@ "getOpencodeGoApiKey": "Obtener clave API de Opencode Go", "kenariApiKey": "Clave API de Kenari", "getKenariApiKey": "Obtener clave API de Kenari", + "ioIntelligence": { + "provider": "IO Intelligence", + "apiKey": "Clave API de IO Intelligence", + "getApiKey": "Obtener clave API de IO Intelligence", + "model": "Modelo" + }, "nanoGpt": { "provider": "NanoGPT", "apiKey": "Clave API de NanoGPT", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 78770da21d..44a583ca2c 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -402,6 +402,12 @@ "getOpencodeGoApiKey": "Obtenir la clé API Opencode Go", "kenariApiKey": "Clé API Kenari", "getKenariApiKey": "Obtenir la clé API Kenari", + "ioIntelligence": { + "provider": "IO Intelligence", + "apiKey": "Clé API IO Intelligence", + "getApiKey": "Obtenir une clé API IO Intelligence", + "model": "Modèle" + }, "nanoGpt": { "provider": "NanoGPT", "apiKey": "Clé API NanoGPT", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 413d3515bc..6807eb5cd8 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -402,6 +402,12 @@ "getOpencodeGoApiKey": "Opencode Go API कुंजी प्राप्त करें", "kenariApiKey": "Kenari API कुंजी", "getKenariApiKey": "Kenari API कुंजी प्राप्त करें", + "ioIntelligence": { + "provider": "IO Intelligence", + "apiKey": "IO Intelligence API कुंजी", + "getApiKey": "IO Intelligence API कुंजी प्राप्त करें", + "model": "मॉडल" + }, "nanoGpt": { "provider": "NanoGPT", "apiKey": "NanoGPT API कुंजी", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 9b9928da64..f18e0fb4fe 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -402,6 +402,12 @@ "getOpencodeGoApiKey": "Dapatkan Opencode Go API Key", "kenariApiKey": "Kenari API Key", "getKenariApiKey": "Dapatkan Kenari API Key", + "ioIntelligence": { + "provider": "IO Intelligence", + "apiKey": "Kunci API IO Intelligence", + "getApiKey": "Dapatkan Kunci API IO Intelligence", + "model": "Model" + }, "nanoGpt": { "provider": "NanoGPT", "apiKey": "NanoGPT API Key", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 8a43ec3d35..b2e8c0c070 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -402,6 +402,12 @@ "getOpencodeGoApiKey": "Ottieni chiave API Opencode Go", "kenariApiKey": "Chiave API Kenari", "getKenariApiKey": "Ottieni chiave API Kenari", + "ioIntelligence": { + "provider": "IO Intelligence", + "apiKey": "Chiave API IO Intelligence", + "getApiKey": "Ottieni chiave API IO Intelligence", + "model": "Modello" + }, "nanoGpt": { "provider": "NanoGPT", "apiKey": "Chiave API NanoGPT", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index b2cfbe977e..2bb2719e08 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -402,6 +402,12 @@ "getOpencodeGoApiKey": "Opencode Go APIキーを取得", "kenariApiKey": "Kenari APIキー", "getKenariApiKey": "Kenari APIキーを取得", + "ioIntelligence": { + "provider": "IO Intelligence", + "apiKey": "IO Intelligence APIキー", + "getApiKey": "IO Intelligence APIキーを取得", + "model": "モデル" + }, "nanoGpt": { "provider": "NanoGPT", "apiKey": "NanoGPT APIキー", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 1ff9addeb4..5ed72d1abe 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -402,6 +402,12 @@ "getOpencodeGoApiKey": "Opencode Go API 키 받기", "kenariApiKey": "Kenari API 키", "getKenariApiKey": "Kenari API 키 받기", + "ioIntelligence": { + "provider": "IO Intelligence", + "apiKey": "IO Intelligence API 키", + "getApiKey": "IO Intelligence API 키 받기", + "model": "모델" + }, "nanoGpt": { "provider": "NanoGPT", "apiKey": "NanoGPT API 키", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 4361d091a1..72eae0ba33 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -402,6 +402,12 @@ "getOpencodeGoApiKey": "Opencode Go API-sleutel ophalen", "kenariApiKey": "Kenari API-sleutel", "getKenariApiKey": "Kenari API-sleutel ophalen", + "ioIntelligence": { + "provider": "IO Intelligence", + "apiKey": "IO Intelligence API-sleutel", + "getApiKey": "IO Intelligence API-sleutel ophalen", + "model": "Model" + }, "nanoGpt": { "provider": "NanoGPT", "apiKey": "NanoGPT API-sleutel", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 277bcaa470..242fe9cb58 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -402,6 +402,12 @@ "getOpencodeGoApiKey": "Uzyskaj klucz API Opencode Go", "kenariApiKey": "Klucz API Kenari", "getKenariApiKey": "Uzyskaj klucz API Kenari", + "ioIntelligence": { + "provider": "IO Intelligence", + "apiKey": "Klucz API IO Intelligence", + "getApiKey": "Uzyskaj klucz API IO Intelligence", + "model": "Model" + }, "nanoGpt": { "provider": "NanoGPT", "apiKey": "Klucz API NanoGPT", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 8ce67bcd48..cdd290a339 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -402,6 +402,12 @@ "getOpencodeGoApiKey": "Obter chave API do Opencode Go", "kenariApiKey": "Chave API do Kenari", "getKenariApiKey": "Obter chave API do Kenari", + "ioIntelligence": { + "provider": "IO Intelligence", + "apiKey": "Chave de API do IO Intelligence", + "getApiKey": "Obter chave de API do IO Intelligence", + "model": "Modelo" + }, "nanoGpt": { "provider": "NanoGPT", "apiKey": "Chave API do NanoGPT", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 0ac516c190..4b57881db1 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -402,6 +402,12 @@ "getOpencodeGoApiKey": "Получить ключ API Opencode Go", "kenariApiKey": "Ключ API Kenari", "getKenariApiKey": "Получить ключ API Kenari", + "ioIntelligence": { + "provider": "IO Intelligence", + "apiKey": "API-ключ IO Intelligence", + "getApiKey": "Получить API-ключ IO Intelligence", + "model": "Модель" + }, "nanoGpt": { "provider": "NanoGPT", "apiKey": "Ключ API NanoGPT", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 5d3a5cb89a..c8b4ff6c83 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -402,6 +402,12 @@ "getOpencodeGoApiKey": "Opencode Go API Anahtarı Al", "kenariApiKey": "Kenari API Anahtarı", "getKenariApiKey": "Kenari API Anahtarı Al", + "ioIntelligence": { + "provider": "IO Intelligence", + "apiKey": "IO Intelligence API Anahtarı", + "getApiKey": "IO Intelligence API Anahtarı Al", + "model": "Model" + }, "nanoGpt": { "provider": "NanoGPT", "apiKey": "NanoGPT API Anahtarı", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index adb1be64e3..dcaafcf105 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -402,6 +402,12 @@ "getOpencodeGoApiKey": "Lấy khóa API Opencode Go", "kenariApiKey": "Khóa API Kenari", "getKenariApiKey": "Lấy khóa API Kenari", + "ioIntelligence": { + "provider": "IO Intelligence", + "apiKey": "Khóa API IO Intelligence", + "getApiKey": "Lấy khóa API IO Intelligence", + "model": "Mô hình" + }, "nanoGpt": { "provider": "NanoGPT", "apiKey": "Khóa API NanoGPT", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 916f629efe..f9c345f231 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -402,6 +402,12 @@ "getOpencodeGoApiKey": "获取 Opencode Go API 密钥", "kenariApiKey": "Kenari API 密钥", "getKenariApiKey": "获取 Kenari API 密钥", + "ioIntelligence": { + "provider": "IO Intelligence", + "apiKey": "IO Intelligence API 密钥", + "getApiKey": "获取 IO Intelligence API 密钥", + "model": "模型" + }, "nanoGpt": { "provider": "NanoGPT", "apiKey": "NanoGPT API 密钥", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index d1f9258dcf..1ce1a19fd6 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -429,6 +429,12 @@ "getOpencodeGoApiKey": "取得 Opencode Go API 金鑰", "kenariApiKey": "Kenari API 金鑰", "getKenariApiKey": "取得 Kenari API 金鑰", + "ioIntelligence": { + "provider": "IO Intelligence", + "apiKey": "IO Intelligence API 金鑰", + "getApiKey": "取得 IO Intelligence API 金鑰", + "model": "模型" + }, "nanoGpt": { "provider": "NanoGPT", "apiKey": "NanoGPT API 金鑰", diff --git a/webview-ui/src/utils/__tests__/validate.spec.ts b/webview-ui/src/utils/__tests__/validate.spec.ts index a1936b89d7..f4c90c6ddd 100644 --- a/webview-ui/src/utils/__tests__/validate.spec.ts +++ b/webview-ui/src/utils/__tests__/validate.spec.ts @@ -52,6 +52,7 @@ describe("Model Validation Functions", () => { requesty: {}, unbound: {}, litellm: {}, + "io-intelligence": {}, ollama: {}, lmstudio: {}, "vercel-ai-gateway": {}, diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 3e32116374..ea3e6d5e7e 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -182,6 +182,11 @@ function validateModelsAndKeysProvided( return i18next.t("settings:validation.apiKey") } break + case providerIdentifiers.ioIntelligence: + if (!apiConfiguration.ioIntelligenceApiKey) { + return i18next.t("settings:validation.apiKey") + } + break case providerIdentifiers.nanogpt: if (!apiConfiguration.nanoGptApiKey) { return i18next.t("settings:validation.apiKey") From e5420956f7e6e86410fdf0956ad960e0b6cb42d6 Mon Sep 17 00:00:00 2001 From: Smiral Rashinkar Date: Fri, 18 Sep 2026 05:49:47 +0530 Subject: [PATCH 02/23] fix(api): derive IO Intelligence model metadata from the live catalog - parse context_window, max_tokens, tool/vision/cache capabilities, and per-token pricing (normalized per million) from the /models catalog - prefer context_window over the always-null max_model_len field - skip catalog entries that explicitly declare no tool support - add io-intelligence to KEY_SCOPED_PROVIDERS for per-key cache isolation - un-retire test fixtures, add provider parity tests across types, api, and webview-ui, and record a changeset --- .changeset/add-io-intelligence-provider.md | 5 + .../src/__tests__/io-intelligence.test.ts | 26 +++ .../__tests__/provider-identifiers.test.ts | 3 +- .../src/__tests__/provider-model-id.test.ts | 1 + .../types/src/providers/io-intelligence.ts | 50 ------ .../__tests__/io-intelligence.spec.ts | 161 ++++++++++++++++++ .../fetchers/__tests__/modelCache.spec.ts | 25 +++ src/api/providers/fetchers/io-intelligence.ts | 50 ++++-- src/api/providers/fetchers/modelCache.ts | 1 + src/api/providers/io-intelligence.ts | 74 ++++---- .../config/__tests__/ContextProxy.spec.ts | 19 +++ .../SettingsView.unsaved-changes.spec.tsx | 81 +++++++++ .../settings/providers/IOIntelligence.tsx | 2 +- .../__tests__/providerModelConfig.spec.ts | 8 + .../hooks/__tests__/useSelectedModel.spec.ts | 46 +++++ 15 files changed, 448 insertions(+), 104 deletions(-) create mode 100644 .changeset/add-io-intelligence-provider.md create mode 100644 packages/types/src/__tests__/io-intelligence.test.ts create mode 100644 src/api/providers/fetchers/__tests__/io-intelligence.spec.ts diff --git a/.changeset/add-io-intelligence-provider.md b/.changeset/add-io-intelligence-provider.md new file mode 100644 index 0000000000..373b5e89bd --- /dev/null +++ b/.changeset/add-io-intelligence-provider.md @@ -0,0 +1,5 @@ +--- +"zoo-code": minor +--- + +Add the IO Intelligence (io.net) provider with a dynamically fetched model catalog, secret-state API key storage, and catalog-derived metadata (context window, output limits, vision and prompt-cache capability flags, per-million pricing). diff --git a/packages/types/src/__tests__/io-intelligence.test.ts b/packages/types/src/__tests__/io-intelligence.test.ts new file mode 100644 index 0000000000..ca0e4a4390 --- /dev/null +++ b/packages/types/src/__tests__/io-intelligence.test.ts @@ -0,0 +1,26 @@ +import { + dynamicProviders, + getModelId, + getProviderDefaultModelId, + ioIntelligenceDefaultModelId, + isSecretStateKey, + providerIdentifiers, + providerSettingsSchema, +} from "../index.js" + +describe("IO Intelligence shared contract", () => { + it("registers the stable dynamic-provider identity and default model", () => { + expect(providerIdentifiers.ioIntelligence).toBe("io-intelligence") + expect(dynamicProviders).toContain(providerIdentifiers.ioIntelligence) + expect(getProviderDefaultModelId(providerIdentifiers.ioIntelligence)).toBe(ioIntelligenceDefaultModelId) + }) + + it("classifies the API key as secret and resolves the configured model", () => { + expect(isSecretStateKey("ioIntelligenceApiKey")).toBe(true) + const settings = providerSettingsSchema.parse({ + apiProvider: providerIdentifiers.ioIntelligence, + ioIntelligenceModelId: "model", + }) + expect(getModelId(settings)).toBe("model") + }) +}) diff --git a/packages/types/src/__tests__/provider-identifiers.test.ts b/packages/types/src/__tests__/provider-identifiers.test.ts index d9ba4fb799..b0375578b4 100644 --- a/packages/types/src/__tests__/provider-identifiers.test.ts +++ b/packages/types/src/__tests__/provider-identifiers.test.ts @@ -35,6 +35,7 @@ const expectedProviderIdentifiers = [ "opencode-go", "kenari", "nanogpt", + "io-intelligence", "ollama", "lmstudio", "vscode-lm", @@ -69,7 +70,6 @@ const expectedRetiredProviderIdentifiers = [ "featherless", "groq", "huggingface", - "io-intelligence", "roo", ] /* eslint-enable zoo/no-raw-provider-identifiers */ @@ -111,6 +111,7 @@ describe("provider identifiers", () => { providerIdentifiers.opencodeGo, providerIdentifiers.kenari, providerIdentifiers.nanogpt, + providerIdentifiers.ioIntelligence, providerIdentifiers.kimiCode, ]) expect(localProviders).toEqual([providerIdentifiers.ollama, providerIdentifiers.lmstudio]) diff --git a/packages/types/src/__tests__/provider-model-id.test.ts b/packages/types/src/__tests__/provider-model-id.test.ts index a623b24b27..ccd5f33a54 100644 --- a/packages/types/src/__tests__/provider-model-id.test.ts +++ b/packages/types/src/__tests__/provider-model-id.test.ts @@ -15,6 +15,7 @@ const expectedModelIdKeys = [ "opencodeGoModelId", "kenariModelId", "nanoGptModelId", + "ioIntelligenceModelId", "zooGatewayModelId", ] as const diff --git a/packages/types/src/providers/io-intelligence.ts b/packages/types/src/providers/io-intelligence.ts index 615f49ffc4..5e18806163 100644 --- a/packages/types/src/providers/io-intelligence.ts +++ b/packages/types/src/providers/io-intelligence.ts @@ -11,53 +11,3 @@ export const ioIntelligenceDefaultModelInfo: ModelInfo = { supportsPromptCache: false, description: "IO Intelligence model. The full model catalog is resolved dynamically from the /models endpoint.", } - -/** - * Static fallback catalog. IO Intelligence serves an OpenAI-compatible - * /models endpoint; these ids mirror the live catalog so that a sensible - * set is available before the dynamic fetch completes. - */ -export const ioIntelligenceModels: Record = { - "meta-llama/Llama-3.3-70B-Instruct": { - maxTokens: 8192, - contextWindow: 128_000, - supportsImages: false, - supportsPromptCache: false, - description: "Meta: Llama 3.3 70B Instruct", - }, - "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": { - maxTokens: 8192, - contextWindow: 430_000, - supportsImages: false, - supportsPromptCache: false, - description: "Meta-Llama: Llama 4 Maverick 17B 128E Instruct FP8", - }, - "deepseek-ai/DeepSeek-R1-0528": { - maxTokens: 8192, - contextWindow: 128_000, - supportsImages: false, - supportsPromptCache: false, - description: "DeepSeek: R1 0528", - }, - "Intel/Qwen3-Coder-480B-A35B-Instruct-int4-mixed-ar": { - maxTokens: 8192, - contextWindow: 106_000, - supportsImages: false, - supportsPromptCache: false, - description: "Intel: Qwen3 Coder 480B A35B Instruct INT4 Mixed AR", - }, - "openai/gpt-oss-120b": { - maxTokens: 8192, - contextWindow: 131_072, - supportsImages: false, - supportsPromptCache: false, - description: "OpenAI: gpt-oss-120b", - }, - "zai-org/GLM-4.6": { - maxTokens: 8192, - contextWindow: 128_000, - supportsImages: false, - supportsPromptCache: false, - description: "Z.ai: GLM 4.6", - }, -} diff --git a/src/api/providers/fetchers/__tests__/io-intelligence.spec.ts b/src/api/providers/fetchers/__tests__/io-intelligence.spec.ts new file mode 100644 index 0000000000..6738c17add --- /dev/null +++ b/src/api/providers/fetchers/__tests__/io-intelligence.spec.ts @@ -0,0 +1,161 @@ +import axios from "axios" + +import { IO_INTELLIGENCE_BASE_URL, ioIntelligenceDefaultModelInfo } from "@roo-code/types" + +import { getIOIntelligenceModels, parseIoIntelligenceModel } from "../io-intelligence" + +vi.mock("axios") + +describe("IO Intelligence model fetcher", () => { + beforeEach(() => vi.clearAllMocks()) + + it("requests the catalog with optional Bearer authorization", async () => { + vi.mocked(axios.get).mockResolvedValue({ data: { data: [] } }) + await getIOIntelligenceModels("key-a") + expect(axios.get).toHaveBeenCalledWith(`${IO_INTELLIGENCE_BASE_URL}/models`, { + headers: { Authorization: "Bearer key-a" }, + timeout: 10_000, + }) + }) + + it("supports unauthenticated catalog requests", async () => { + vi.mocked(axios.get).mockResolvedValue({ data: { data: [] } }) + await getIOIntelligenceModels() + expect(axios.get).toHaveBeenCalledWith(`${IO_INTELLIGENCE_BASE_URL}/models`, { + headers: undefined, + timeout: 10_000, + }) + }) + + it("maps detailed metadata and exact per-million pricing for multiple models", async () => { + vi.mocked(axios.get).mockResolvedValue({ + data: { + unknown_top_level: true, + data: [ + { + id: "vision-model", + name: "Vision Model", + created: 1_789_049_414, + owned_by: "io-intelligence", + max_model_len: null, + context_window: 262_144, + max_tokens: 131_072, + supports_tools: true, + supports_reasoning: true, + supports_prompt_cache: true, + input_modalities: ["text", "image"], + input_token_price: 0.000000306, + output_token_price: 0.000001224, + cache_read_token_price: 0.000000153, + unknown: "allowed", + }, + { id: "text-model", input_modalities: ["text"] }, + ], + }, + }) + + const models = await getIOIntelligenceModels() + expect(Object.keys(models)).toEqual(["vision-model", "text-model"]) + expect(models["vision-model"]).toEqual({ + contextWindow: 262_144, + maxTokens: 131_072, + supportsImages: true, + supportsPromptCache: true, + displayName: "Vision Model", + description: "Vision Model", + inputPrice: 0.306, + outputPrice: 1.224, + cacheReadsPrice: 0.153, + }) + expect(models["text-model"].supportsImages).toBe(false) + }) + + it("skips malformed records and models explicitly lacking tool support", async () => { + vi.mocked(axios.get).mockResolvedValue({ + data: { + data: [{ id: "eligible" }, { missing: "id" }, { id: "chat-only", supports_tools: false }], + }, + }) + const warning = vi.spyOn(console, "warn").mockImplementation(() => undefined) + expect(await getIOIntelligenceModels()).toEqual({ + eligible: { + contextWindow: ioIntelligenceDefaultModelInfo.contextWindow, + maxTokens: ioIntelligenceDefaultModelInfo.maxTokens, + supportsImages: false, + supportsPromptCache: false, + description: "eligible", + }, + }) + expect(warning).toHaveBeenCalledOnce() + warning.mockRestore() + }) + + it("keeps models with null token metadata and prefers context_window over max_model_len", () => { + expect( + parseIoIntelligenceModel({ id: "both", max_model_len: 65_536, context_window: 262_144 }).contextWindow, + ).toBe(262_144) + + const info = parseIoIntelligenceModel({ + id: "legacy-entry", + max_model_len: 65_536, + context_window: null, + max_tokens: null, + }) + expect(info.contextWindow).toBe(65_536) + expect(info.maxTokens).toBe(ioIntelligenceDefaultModelInfo.maxTokens) + }) + + it.each([{ data: null }, [], null])("returns no models for invalid top-level data %#", async (data) => { + vi.mocked(axios.get).mockResolvedValue({ data }) + const warning = vi.spyOn(console, "warn").mockImplementation(() => undefined) + expect(await getIOIntelligenceModels()).toEqual({}) + warning.mockRestore() + }) + + it.each([new Error("network unavailable"), "network unavailable"])( + "returns no models on network failure", + async (error) => { + vi.mocked(axios.get).mockRejectedValue(error) + const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined) + expect(await getIOIntelligenceModels()).toEqual({}) + consoleError.mockRestore() + }, + ) + + it("rejects negative numeric metadata without leaking the API key in errors", async () => { + vi.mocked(axios.get) + .mockResolvedValueOnce({ + data: { + data: [ + { id: "valid-free", input_token_price: 0, output_token_price: 0 }, + { id: "invalid-price", input_token_price: -1 }, + ], + }, + }) + .mockRejectedValueOnce(new Error("upstream rejected secret-key")) + const warning = vi.spyOn(console, "warn").mockImplementation(() => undefined) + const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined) + + expect(await getIOIntelligenceModels("secret-key")).toEqual({ + "valid-free": expect.objectContaining({ inputPrice: 0, outputPrice: 0 }), + }) + expect(await getIOIntelligenceModels("secret-key")).toEqual({}) + expect(consoleError).toHaveBeenLastCalledWith( + "Error fetching IO Intelligence models: upstream rejected [REDACTED]", + ) + + warning.mockRestore() + consoleError.mockRestore() + }) + + it("does not invent absent optional metadata", () => { + const info = parseIoIntelligenceModel({ id: "minimal" }) + expect(info).toEqual({ + contextWindow: ioIntelligenceDefaultModelInfo.contextWindow, + maxTokens: ioIntelligenceDefaultModelInfo.maxTokens, + supportsImages: false, + supportsPromptCache: false, + description: "minimal", + }) + }) +}) diff --git a/src/api/providers/fetchers/__tests__/modelCache.spec.ts b/src/api/providers/fetchers/__tests__/modelCache.spec.ts index d8d0a03afc..4c82d20308 100644 --- a/src/api/providers/fetchers/__tests__/modelCache.spec.ts +++ b/src/api/providers/fetchers/__tests__/modelCache.spec.ts @@ -1146,6 +1146,31 @@ describe("NanoGPT key-scoped cache isolation", () => { }) }) +describe("IO Intelligence key-scoped cache isolation", () => { + const ioIntelligenceModels = { + "meta-llama/Llama-3.3-70B-Instruct": { maxTokens: 8_192, contextWindow: 128_000, supportsPromptCache: false }, + } + + beforeEach(() => { + vi.clearAllMocks() + mockGetIOIntelligenceModels.mockResolvedValue(ioIntelligenceModels) + }) + + it("separates public, key A, and key B cache identities without exposing raw keys", async () => { + const mockCache = vi.mocked(new (vi.mocked(NodeCache))()) + mockCache.get.mockReturnValue(undefined) + + await getModels({ provider: providerIdentifiers.ioIntelligence }) + await getModels({ provider: providerIdentifiers.ioIntelligence, apiKey: "ionet-key-a" }) + await getModels({ provider: providerIdentifiers.ioIntelligence, apiKey: "ionet-key-b" }) + + const cacheKeys = mockCache.set.mock.calls.map(([key]) => key as string) + expect(new Set(cacheKeys).size).toBe(3) + expect(cacheKeys).toContain("io-intelligence") + expect(cacheKeys.every((key) => !key.includes("ionet-key-a") && !key.includes("ionet-key-b"))).toBe(true) + }) +}) + describe("compound cache key derivation across scoping dimensions", () => { // Exercises every branch of getCacheKey via the public getModels() entry point. // litellm is url-scoped AND key-scoped; openrouter is neither, so it hits the bare diff --git a/src/api/providers/fetchers/io-intelligence.ts b/src/api/providers/fetchers/io-intelligence.ts index 8cbc67dec2..b2cdcb2c0b 100644 --- a/src/api/providers/fetchers/io-intelligence.ts +++ b/src/api/providers/fetchers/io-intelligence.ts @@ -11,38 +11,49 @@ import { const ioIntelligenceModelSchema = z.object({ id: z.string().min(1), name: z.string().optional(), - object: z.string().optional(), - created: z.number().optional(), - owned_by: z.string().optional(), - root: z.string().nullable().optional(), - parent: z.string().nullable().optional(), max_model_len: z.number().nullable().optional(), + context_window: z.number().int().positive().nullish(), + max_tokens: z.number().int().positive().nullish(), + supports_tools: z.boolean().optional(), + supports_prompt_cache: z.boolean().optional(), + input_modalities: z.array(z.string()).optional(), + input_token_price: z.number().nonnegative().optional(), + output_token_price: z.number().nonnegative().optional(), + cache_read_token_price: z.number().nonnegative().optional(), }) export type IOIntelligenceModel = z.infer -const ioIntelligenceModelsResponseSchema = z.object({ - object: z.string().optional(), - data: z.array(z.unknown()), -}) - function getSafeErrorMessage(error: unknown, apiKey?: string): string { const message = error instanceof Error ? error.message : String(error) return apiKey ? message.replaceAll(apiKey, "[REDACTED]") : message } +const ioIntelligenceModelsResponseSchema = z.object({ + object: z.string().optional(), + data: z.array(z.unknown()), +}) + export const parseIoIntelligenceModel = (model: IOIntelligenceModel): ModelInfo => ({ - maxTokens: ioIntelligenceDefaultModelInfo.maxTokens, - contextWindow: model.max_model_len ?? ioIntelligenceDefaultModelInfo.contextWindow, - supportsImages: false, - supportsPromptCache: false, + maxTokens: model.max_tokens ?? ioIntelligenceDefaultModelInfo.maxTokens, + contextWindow: model.context_window ?? model.max_model_len ?? ioIntelligenceDefaultModelInfo.contextWindow, + supportsImages: model.input_modalities?.includes("image") ?? false, + supportsPromptCache: model.supports_prompt_cache ?? false, + ...(model.input_token_price !== undefined ? { inputPrice: model.input_token_price * 1_000_000 } : {}), + ...(model.output_token_price !== undefined ? { outputPrice: model.output_token_price * 1_000_000 } : {}), + ...(model.cache_read_token_price !== undefined + ? { cacheReadsPrice: model.cache_read_token_price * 1_000_000 } + : {}), ...(model.name !== undefined ? { displayName: model.name } : {}), description: model.name ?? model.id, }) /** - * Fetches IO Intelligence's public /models catalog, optionally scoped by a - * Bearer key. Model ids are Hugging Face-style `org/name` identifiers. + * Fetches the public IO Intelligence (io.net) model catalog. + * + * The catalog can be listed without an API key, while a Bearer key scopes the + * visible models for the account (io.net exposes per-tier access). Prices are + * published per token and normalized to per-million-token units. */ export async function getIOIntelligenceModels(apiKey?: string): Promise { try { @@ -64,6 +75,13 @@ export async function getIOIntelligenceModels(apiKey?: string): Promise = new Set([ providerIdentifiers.zooGateway, // Per-session-token account identity providerIdentifiers.kimiCode, // Per-session-token account identity providerIdentifiers.nanogpt, // Public catalog can still vary by API-key allowlist + providerIdentifiers.ioIntelligence, // Public catalog can still vary by API-key allowlist ]) // Providers whose model lists are scoped to the signed-in user (e.g. per-account diff --git a/src/api/providers/io-intelligence.ts b/src/api/providers/io-intelligence.ts index e43f9a3e47..ed0fbbf01b 100644 --- a/src/api/providers/io-intelligence.ts +++ b/src/api/providers/io-intelligence.ts @@ -24,9 +24,9 @@ import { extractReasoningFromDelta } from "./utils/extract-reasoning" * https://api.intelligence.io.solutions/api/v1/chat/completions * * Model ids are Hugging Face-style `org/name` identifiers and are resolved - * dynamically from the public /models endpoint, with a static fallback - * catalog. Supports text generation, reasoning content (DeepSeek/GLM style), - * tool calls, and non-streaming prompt completion. + * dynamically from the public /models endpoint. Supports text generation, + * reasoning content (DeepSeek/GLM style), tool calls, and non-streaming + * prompt completion. */ export class IOIntelligenceHandler extends RouterProvider implements SingleCompletionHandler { /** Creates a new handler bound to the user's API key and selected model. */ @@ -83,48 +83,47 @@ export class IOIntelligenceHandler extends RouterProvider implements SingleCompl body.temperature = this.options.modelTemperature } - let completion: Awaited> try { - completion = await this.client.chat.completions.create(body, { signal: metadata?.abortSignal }) - } catch (error) { - throw this.createSafeError("streaming", error) - } + const completion = await this.client.chat.completions.create(body, { signal: metadata?.abortSignal }) - for await (const chunk of completion) { - const delta = chunk.choices[0]?.delta + for await (const chunk of completion) { + const delta = chunk.choices[0]?.delta - // Reasoning models (DeepSeek R1, GLM) stream reasoning via - // reasoning_content with an OpenRouter-style `reasoning` fallback. - const reasoningText = extractReasoningFromDelta(delta) - if (reasoningText) { - yield { type: "reasoning", text: reasoningText } - } + // Reasoning models (DeepSeek R1, GLM) stream reasoning via + // reasoning_content with an OpenRouter-style `reasoning` fallback. + const reasoningText = extractReasoningFromDelta(delta) + if (reasoningText) { + yield { type: "reasoning", text: reasoningText } + } - if (delta?.content) { - yield { type: "text", text: delta.content } - } + if (delta?.content) { + yield { type: "text", text: delta.content } + } - // Emit raw tool call chunks - NativeToolCallParser handles state management. - if (delta?.tool_calls) { - for (const toolCall of delta.tool_calls) { - yield { - type: "tool_call_partial", - index: toolCall.index, - id: toolCall.id, - name: toolCall.function?.name, - arguments: toolCall.function?.arguments, + // Emit raw tool call chunks - NativeToolCallParser handles state management. + if (delta?.tool_calls) { + for (const toolCall of delta.tool_calls) { + yield { + type: "tool_call_partial", + index: toolCall.index, + id: toolCall.id, + name: toolCall.function?.name, + arguments: toolCall.function?.arguments, + } } } - } - if (chunk.usage) { - yield { - type: "usage", - inputTokens: chunk.usage.prompt_tokens || 0, - outputTokens: chunk.usage.completion_tokens || 0, - cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || undefined, + if (chunk.usage) { + yield { + type: "usage", + inputTokens: chunk.usage.prompt_tokens || 0, + outputTokens: chunk.usage.completion_tokens || 0, + cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || undefined, + } } } + } catch (error) { + throw this.createSafeError("streaming", error) } } @@ -146,7 +145,10 @@ export class IOIntelligenceHandler extends RouterProvider implements SingleCompl stream: false, } - const response = await this.client.chat.completions.create(requestOptions) + const response = await this.client.chat.completions.create(requestOptions, { + signal: options?.abortSignal, + timeout: options?.timeoutMs, + }) return response.choices[0]?.message.content || "" } catch (error) { throw this.createSafeError("completion", error) diff --git a/src/core/config/__tests__/ContextProxy.spec.ts b/src/core/config/__tests__/ContextProxy.spec.ts index 2319a6b1a5..b4d3b1b8aa 100644 --- a/src/core/config/__tests__/ContextProxy.spec.ts +++ b/src/core/config/__tests__/ContextProxy.spec.ts @@ -309,6 +309,25 @@ describe("ContextProxy", () => { }) describe("setProviderSettings", () => { + it("stores and returns the complete IO Intelligence configuration across secret and global state", async () => { + await proxy.setProviderSettings({ + apiProvider: providerIdentifiers.ioIntelligence, + ioIntelligenceApiKey: "ionet-secret", + ioIntelligenceModelId: "meta-llama/Llama-3.3-70B-Instruct", + }) + + expect(mockSecrets.store).toHaveBeenCalledWith("ioIntelligenceApiKey", "ionet-secret") + expect(mockGlobalState.update).toHaveBeenCalledWith( + "ioIntelligenceModelId", + "meta-llama/Llama-3.3-70B-Instruct", + ) + expect(proxy.getProviderSettings()).toMatchObject({ + apiProvider: providerIdentifiers.ioIntelligence, + ioIntelligenceApiKey: "ionet-secret", + ioIntelligenceModelId: "meta-llama/Llama-3.3-70B-Instruct", + }) + }) + it("stores and returns the complete NanoGPT configuration across secret and global state", async () => { await proxy.setProviderSettings({ apiProvider: providerIdentifiers.nanogpt, diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx index 72e1cc7c5d..f95d37300f 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx @@ -761,4 +761,85 @@ describe("SettingsView - Unsaved Changes Detection", () => { expect(onDone).toHaveBeenCalledOnce() expect(postMessage).not.toHaveBeenCalledWith(expect.objectContaining({ type: "upsertApiConfiguration" })) }) + + it("buffers and saves the complete IO Intelligence provider configuration from cached state", async () => { + const liveApiConfiguration = { + apiProvider: providerIdentifiers.ioIntelligence, + ioIntelligenceApiKey: "original-key", + ioIntelligenceModelId: "meta-llama/original", + } + ;(useExtensionState as ReturnType).mockReturnValue({ + ...defaultExtensionState, + apiConfiguration: liveApiConfiguration, + }) + vi.mocked(ApiOptions).mockImplementation(({ apiConfiguration, setApiConfigurationField }) => ( +
+ setApiConfigurationField("ioIntelligenceApiKey", event.target.value)} + /> + setApiConfigurationField("ioIntelligenceModelId", event.target.value)} + /> +
+ )) + + renderWithExtensionState(, { queryClient }) + + expect(await screen.findByTestId("cached-ionet-key")).toHaveValue("original-key") + expect(screen.getByTestId("cached-ionet-model")).toHaveValue("meta-llama/original") + + fireEvent.change(screen.getByTestId("cached-ionet-key"), { target: { value: "unsaved-key" } }) + fireEvent.change(screen.getByTestId("cached-ionet-model"), { target: { value: "zai-org/next" } }) + + expect(liveApiConfiguration).toEqual({ + apiProvider: providerIdentifiers.ioIntelligence, + ioIntelligenceApiKey: "original-key", + ioIntelligenceModelId: "meta-llama/original", + }) + expect(postMessage).not.toHaveBeenCalledWith(expect.objectContaining({ type: "upsertApiConfiguration" })) + + fireEvent.click(screen.getByTestId("save-button")) + + expect(postMessage).toHaveBeenCalledWith({ + type: "upsertApiConfiguration", + text: "default", + apiConfiguration: { + apiProvider: providerIdentifiers.ioIntelligence, + ioIntelligenceApiKey: "unsaved-key", + ioIntelligenceModelId: "zai-org/next", + }, + }) + }) + + it("discards IO Intelligence cached edits and restores the extension values", async () => { + const onDone = vi.fn() + ;(useExtensionState as ReturnType).mockReturnValue({ + ...defaultExtensionState, + apiConfiguration: { + apiProvider: providerIdentifiers.ioIntelligence, + ioIntelligenceApiKey: "saved-key", + ioIntelligenceModelId: "meta-llama/saved", + }, + }) + vi.mocked(ApiOptions).mockImplementation(({ apiConfiguration, setApiConfigurationField }) => ( + setApiConfigurationField("ioIntelligenceApiKey", event.target.value)} + /> + )) + + renderWithExtensionState(, { queryClient }) + fireEvent.change(await screen.findByTestId("cached-ionet-key"), { target: { value: "discard-me" } }) + fireEvent.click(screen.getByText("settings:common.done")) + fireEvent.click(await screen.findByText("settings:unsavedChangesDialog.discardButton")) + + await waitFor(() => expect(screen.getByTestId("cached-ionet-key")).toHaveValue("saved-key")) + expect(onDone).toHaveBeenCalledOnce() + expect(postMessage).not.toHaveBeenCalledWith(expect.objectContaining({ type: "upsertApiConfiguration" })) + }) }) diff --git a/webview-ui/src/components/settings/providers/IOIntelligence.tsx b/webview-ui/src/components/settings/providers/IOIntelligence.tsx index a437e9f13c..07fc24b47c 100644 --- a/webview-ui/src/components/settings/providers/IOIntelligence.tsx +++ b/webview-ui/src/components/settings/providers/IOIntelligence.tsx @@ -80,7 +80,7 @@ export const IOIntelligence = ({ apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} defaultModelId={ioIntelligenceDefaultModelId} - models={routerModels?.["io-intelligence"] ?? {}} + models={routerModels?.[providerIdentifiers.ioIntelligence] ?? {}} modelIdKey="ioIntelligenceModelId" serviceName={t("settings:providers.ioIntelligence.provider")} serviceUrl="https://api.intelligence.io.solutions/api/v1/models" diff --git a/webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts b/webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts index b695b3529e..a8203fa740 100644 --- a/webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts +++ b/webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts @@ -1,5 +1,6 @@ import { anthropicDefaultModelId, + ioIntelligenceDefaultModelId, mainlandZAiDefaultModelId, nanoGptDefaultModelId, providerIdentifiers, @@ -181,6 +182,13 @@ describe("providerModelConfig", () => { default: nanoGptDefaultModelId, }) }) + + it("returns IO Intelligence's dynamic model field and fallback", () => { + expect(getProviderModelConfig(providerIdentifiers.ioIntelligence)).toEqual({ + field: "ioIntelligenceModelId", + default: ioIntelligenceDefaultModelId, + }) + }) }) describe("getProviderDocsSlug", () => { diff --git a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts index d5ef35672b..4f01a8b2e4 100644 --- a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts +++ b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts @@ -13,6 +13,8 @@ import { anthropicModels, BEDROCK_1M_CONTEXT_MODEL_IDS, litellmDefaultModelInfo, + ioIntelligenceDefaultModelId, + ioIntelligenceDefaultModelInfo, kenariDefaultModelId, kenariDefaultModelInfo, nanoGptDefaultModelId, @@ -1127,6 +1129,50 @@ describe("useSelectedModel", () => { }) }) + describe("io-intelligence provider", () => { + it("uses dynamic metadata for the configured IO Intelligence model", () => { + const dynamicInfo: ModelInfo = { + maxTokens: 8_192, + contextWindow: 131_072, + supportsPromptCache: true, + description: "Dynamic IO Intelligence model", + } + mockUseRouterModels.mockReturnValue( + createRouterModelsResult({ "io-intelligence": { "zai-org/GLM-4.6": dynamicInfo } }), + ) + mockUseOpenRouterModelProviders.mockReturnValue(createOpenRouterModelProvidersResult({})) + + const { result } = renderHook( + () => + useSelectedModel({ + apiProvider: providerIdentifiers.ioIntelligence, + ioIntelligenceModelId: "zai-org/GLM-4.6", + }), + { wrapper: createWrapper() }, + ) + + expect(result.current.id).toBe("zai-org/GLM-4.6") + expect(result.current.info).toEqual(dynamicInfo) + }) + + it("uses IO Intelligence's shared fallback when its dynamic catalog is empty", () => { + mockUseRouterModels.mockReturnValue(createRouterModelsResult({ "io-intelligence": {} })) + mockUseOpenRouterModelProviders.mockReturnValue(createOpenRouterModelProvidersResult({})) + + const { result } = renderHook( + () => + useSelectedModel({ + apiProvider: providerIdentifiers.ioIntelligence, + ioIntelligenceModelId: "missing-model", + }), + { wrapper: createWrapper() }, + ) + + expect(result.current.id).toBe(ioIntelligenceDefaultModelId) + expect(result.current.info).toEqual(ioIntelligenceDefaultModelInfo) + }) + }) + describe("openai provider", () => { beforeEach(() => { mockUseRouterModels.mockReturnValue(createRouterModelsResult({ openrouter: {}, requesty: {}, litellm: {} })) From 514746be1ef132067a4012a43845697d3c71470b Mon Sep 17 00:00:00 2001 From: Smiral Rashinkar Date: Fri, 18 Sep 2026 07:01:06 +0530 Subject: [PATCH 03/23] fix(api): forward IO Intelligence completion options only when set The OpenAI SDK validates a present-but-undefined timeout key, so completePrompt threw before issuing a request whenever it was called without options. Request options are now built conditionally, matching the SDK's key-presence semantics, and regression tests cover both the bare and optioned calls. Adds the missing settings-component spec. --- .../__tests__/io-intelligence.spec.ts | 21 +++ src/api/providers/io-intelligence.ts | 20 ++- .../__tests__/IOIntelligence.spec.tsx | 153 ++++++++++++++++++ 3 files changed, 190 insertions(+), 4 deletions(-) create mode 100644 webview-ui/src/components/settings/providers/__tests__/IOIntelligence.spec.tsx diff --git a/src/api/providers/__tests__/io-intelligence.spec.ts b/src/api/providers/__tests__/io-intelligence.spec.ts index 00c18011d9..65356e342d 100644 --- a/src/api/providers/__tests__/io-intelligence.spec.ts +++ b/src/api/providers/__tests__/io-intelligence.spec.ts @@ -174,4 +174,25 @@ describe("IOIntelligenceHandler", () => { message: "IO Intelligence streaming error: upstream rejected [REDACTED]", }) }) + + it("completePrompt without options forwards no request options", async () => { + mockCreate.mockResolvedValue({ + choices: [{ message: { role: "assistant", content: "ok" } }], + }) + const handler = new IOIntelligenceHandler({ ioIntelligenceModelId: "meta-llama/Llama-3.3-70B-Instruct" }) + expect(await handler.completePrompt("ping")).toBe("ok") + // The OpenAI SDK rejects a present-but-undefined `timeout` key, so a bare + // call must not pass request options at all. + expect(mockCreate.mock.calls[0][1]).toBeUndefined() + }) + + it("completePrompt forwards abort and timeout options when provided", async () => { + mockCreate.mockResolvedValue({ + choices: [{ message: { role: "assistant", content: "ok" } }], + }) + const signal = new AbortController().signal + const handler = new IOIntelligenceHandler({ ioIntelligenceModelId: "meta-llama/Llama-3.3-70B-Instruct" }) + await handler.completePrompt("ping", { abortSignal: signal, timeoutMs: 5_000 }) + expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({ stream: false }), { signal, timeout: 5_000 }) + }) }) diff --git a/src/api/providers/io-intelligence.ts b/src/api/providers/io-intelligence.ts index ed0fbbf01b..c9305135dd 100644 --- a/src/api/providers/io-intelligence.ts +++ b/src/api/providers/io-intelligence.ts @@ -145,10 +145,22 @@ export class IOIntelligenceHandler extends RouterProvider implements SingleCompl stream: false, } - const response = await this.client.chat.completions.create(requestOptions, { - signal: options?.abortSignal, - timeout: options?.timeoutMs, - }) + // The OpenAI SDK validates a present-but-undefined `timeout` key, so + // request options are only forwarded when they carry a real value. + const completionOptions: OpenAI.RequestOptions = {} + if (options?.abortSignal !== undefined) { + completionOptions.signal = options.abortSignal + } + if (options?.timeoutMs !== undefined) { + completionOptions.timeout = options.timeoutMs + } + const hasCompletionOptions = + completionOptions.signal !== undefined || completionOptions.timeout !== undefined + + const response = await this.client.chat.completions.create( + requestOptions, + hasCompletionOptions ? completionOptions : undefined, + ) return response.choices[0]?.message.content || "" } catch (error) { throw this.createSafeError("completion", error) diff --git a/webview-ui/src/components/settings/providers/__tests__/IOIntelligence.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/IOIntelligence.spec.tsx new file mode 100644 index 0000000000..b79144f7cd --- /dev/null +++ b/webview-ui/src/components/settings/providers/__tests__/IOIntelligence.spec.tsx @@ -0,0 +1,153 @@ +import { fireEvent, render, screen } from "@testing-library/react" + +import { + type OrganizationAllowList, + type ProviderSettings, + type RouterModels, + ioIntelligenceDefaultModelId, + providerIdentifiers, + RouterModelsMessageType, +} from "@roo-code/types" + +import { IOIntelligence } from "../IOIntelligence" + +const { postMessageMock } = vi.hoisted(() => ({ postMessageMock: vi.fn() })) + +vi.mock("@src/utils/vscode", () => ({ vscode: { postMessage: postMessageMock } })) + +vi.mock("@src/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ t: (key: string) => key }), +})) + +vi.mock("@vscode/webview-ui-toolkit/react", () => ({ + VSCodeTextField: ({ + children, + value, + onInput, + type, + }: React.ComponentProps<"input"> & { children: React.ReactNode }) => ( +
+ {children} + +
+ ), +})) + +vi.mock("@src/components/common/VSCodeButtonLink", () => ({ + VSCodeButtonLink: ({ children, href }: React.ComponentProps<"a">) => ( + + {children} + + ), +})) + +vi.mock("../../ModelPicker", () => ({ + ModelPicker: ({ + defaultModelId, + models, + modelIdKey, + serviceName, + }: { + defaultModelId: string + models: object + modelIdKey: string + serviceName: string + }) => ( +
+ ), +})) + +describe("IOIntelligence", () => { + const organizationAllowList: OrganizationAllowList = { allowAll: true, providers: {} } + const setApiConfigurationField = vi.fn() + const routerModels: RouterModels = { + openrouter: {}, + "vercel-ai-gateway": {}, + "zoo-gateway": {}, + litellm: {}, + requesty: {}, + unbound: {}, + poe: {}, + deepseek: {}, + moonshot: {}, + "opencode-go": {}, + kenari: {}, + nanogpt: {}, + "io-intelligence": { + "meta-llama/Llama-3.3-70B-Instruct": { contextWindow: 128000, maxTokens: 8192, supportsPromptCache: false }, + }, + "kimi-code": {}, + ollama: {}, + lmstudio: {}, + } + + const renderComponent = (apiConfiguration: ProviderSettings = {}) => + render( + , + ) + + beforeEach(() => vi.clearAllMocks()) + + it("renders the secret key input, CTA, and dynamic model picker", () => { + renderComponent({ ioIntelligenceApiKey: "stored" }) + + expect(screen.getByTestId("ionet-api-key")).toHaveAttribute("type", "password") + expect(screen.getByText("settings:providers.ioIntelligence.apiKey")).toBeInTheDocument() + expect(screen.queryByTestId("ionet-get-key")).not.toBeInTheDocument() + expect(screen.getByTestId("model-picker")).toHaveAttribute( + "data-default-model-id", + ioIntelligenceDefaultModelId, + ) + expect(screen.getByTestId("model-picker")).toHaveAttribute("data-model-id-key", "ioIntelligenceModelId") + expect(screen.getByTestId("model-picker")).toHaveAttribute("data-model-count", "1") + expect(screen.getByText("settings:providers.apiKeyStorageNotice")).toBeInTheDocument() + }) + + it("shows the get-key CTA only when no key is configured", () => { + renderComponent({}) + + expect(screen.getByTestId("ionet-get-key")).toHaveAttribute("href", "https://ai.io.net/ai/api-keys") + expect(screen.getByText("settings:providers.ioIntelligence.getApiKey")).toBeInTheDocument() + }) + + it("updates the cached key with exact values", () => { + renderComponent({ ioIntelligenceApiKey: "" }) + + fireEvent.input(screen.getByTestId("ionet-api-key"), { target: { value: "new-secret" } }) + + expect(setApiConfigurationField).toHaveBeenCalledWith("ioIntelligenceApiKey", "new-secret") + }) + + it("refreshes models with the unsaved cached key whenever it changes", () => { + const { rerender } = renderComponent({ ioIntelligenceApiKey: "first-key" }) + expect(postMessageMock).toHaveBeenLastCalledWith({ + type: RouterModelsMessageType.requestRouterModels, + values: { provider: providerIdentifiers.ioIntelligence, ioIntelligenceApiKey: "first-key" }, + }) + + rerender( + , + ) + + expect(postMessageMock).toHaveBeenLastCalledWith({ + type: RouterModelsMessageType.requestRouterModels, + values: { provider: providerIdentifiers.ioIntelligence, ioIntelligenceApiKey: "unsaved-key" }, + }) + }) +}) From 71387bf439ac64eee3799e85a77d50c962c2f3ba Mon Sep 17 00:00:00 2001 From: Smiral Rashinkar Date: Fri, 18 Sep 2026 07:13:33 +0530 Subject: [PATCH 04/23] fix(api): harden IO Intelligence schema and temperature guard Aligns the max_model_len zod field with its sibling numeric fields (integer, positive, nullish) and gates the streaming temperature parameter on a configured value and the model's capability flag, matching the NanoGPT handler. --- src/api/providers/fetchers/io-intelligence.ts | 2 +- src/api/providers/io-intelligence.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/api/providers/fetchers/io-intelligence.ts b/src/api/providers/fetchers/io-intelligence.ts index b2cdcb2c0b..40522abfe0 100644 --- a/src/api/providers/fetchers/io-intelligence.ts +++ b/src/api/providers/fetchers/io-intelligence.ts @@ -11,7 +11,7 @@ import { const ioIntelligenceModelSchema = z.object({ id: z.string().min(1), name: z.string().optional(), - max_model_len: z.number().nullable().optional(), + max_model_len: z.number().int().positive().nullish(), context_window: z.number().int().positive().nullish(), max_tokens: z.number().int().positive().nullish(), supports_tools: z.boolean().optional(), diff --git a/src/api/providers/io-intelligence.ts b/src/api/providers/io-intelligence.ts index c9305135dd..f6477f83ed 100644 --- a/src/api/providers/io-intelligence.ts +++ b/src/api/providers/io-intelligence.ts @@ -79,7 +79,11 @@ export class IOIntelligenceHandler extends RouterProvider implements SingleCompl parallel_tool_calls: metadata?.parallelToolCalls ?? true, } - if (this.supportsTemperature(modelId)) { + if ( + this.options.modelTemperature !== undefined && + info.supportsTemperature !== false && + this.supportsTemperature(modelId) + ) { body.temperature = this.options.modelTemperature } From d293bebb302a79fab32ae8def5bee6dd99c9bfd3 Mon Sep 17 00:00:00 2001 From: Smiral Rashinkar Date: Sat, 19 Sep 2026 13:02:28 +0530 Subject: [PATCH 05/23] fix(api): forward IO Intelligence temperature in completePrompt Mirror the streaming path's three-condition guard (configured temperature, catalog supportsTemperature not explicitly false, model supports it) in the non-streaming completePrompt body, matching the NanoGPT handler. Add a regression test asserting the temperature reaches the request body. --- src/api/providers/__tests__/io-intelligence.spec.ts | 12 ++++++++++++ src/api/providers/io-intelligence.ts | 8 ++++++++ 2 files changed, 20 insertions(+) diff --git a/src/api/providers/__tests__/io-intelligence.spec.ts b/src/api/providers/__tests__/io-intelligence.spec.ts index 65356e342d..eff6c12b64 100644 --- a/src/api/providers/__tests__/io-intelligence.spec.ts +++ b/src/api/providers/__tests__/io-intelligence.spec.ts @@ -195,4 +195,16 @@ describe("IOIntelligenceHandler", () => { await handler.completePrompt("ping", { abortSignal: signal, timeoutMs: 5_000 }) expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({ stream: false }), { signal, timeout: 5_000 }) }) + + it("completePrompt forwards the configured temperature", async () => { + mockCreate.mockResolvedValue({ + choices: [{ message: { role: "assistant", content: "ok" } }], + }) + const handler = new IOIntelligenceHandler({ + ioIntelligenceModelId: "meta-llama/Llama-3.3-70B-Instruct", + modelTemperature: 0.7, + }) + expect(await handler.completePrompt("ping")).toBe("ok") + expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({ stream: false, temperature: 0.7 }), undefined) + }) }) diff --git a/src/api/providers/io-intelligence.ts b/src/api/providers/io-intelligence.ts index f6477f83ed..2dc04587ec 100644 --- a/src/api/providers/io-intelligence.ts +++ b/src/api/providers/io-intelligence.ts @@ -149,6 +149,14 @@ export class IOIntelligenceHandler extends RouterProvider implements SingleCompl stream: false, } + if ( + this.options.modelTemperature !== undefined && + info.supportsTemperature !== false && + this.supportsTemperature(modelId) + ) { + requestOptions.temperature = this.options.modelTemperature + } + // The OpenAI SDK validates a present-but-undefined `timeout` key, so // request options are only forwarded when they carry a real value. const completionOptions: OpenAI.RequestOptions = {} From fa2d1ac004c6bc0f86b9c209d14a315a9e8a68e4 Mon Sep 17 00:00:00 2001 From: Smiral Rashinkar Date: Sat, 19 Sep 2026 13:34:36 +0530 Subject: [PATCH 06/23] test(api): use a catalog-listed model id in the IO Intelligence fixture --- src/api/providers/__tests__/io-intelligence.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/providers/__tests__/io-intelligence.spec.ts b/src/api/providers/__tests__/io-intelligence.spec.ts index eff6c12b64..4e8770525f 100644 --- a/src/api/providers/__tests__/io-intelligence.spec.ts +++ b/src/api/providers/__tests__/io-intelligence.spec.ts @@ -82,11 +82,11 @@ describe("IOIntelligenceHandler", () => { ) const chunks = await collectStream( new IOIntelligenceHandler({ - ioIntelligenceModelId: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8", + ioIntelligenceModelId: "deepseek-ai/DeepSeek-V3.1", }).createMessage("sys", messages), ) expect(mockCreate).toHaveBeenCalledWith( - expect.objectContaining({ model: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8" }), + expect.objectContaining({ model: "deepseek-ai/DeepSeek-V3.1" }), expect.objectContaining({ signal: undefined }), ) expect(chunks).toEqual([ From 6c25367073e2b2f8d375a12e3827cc5939e06ad1 Mon Sep 17 00:00:00 2001 From: Smiral Rashinkar Date: Sat, 19 Sep 2026 13:45:05 +0530 Subject: [PATCH 07/23] fix(webview): link the IO Intelligence model hint to the product page - point the ModelPicker serviceUrl at https://io.net/intelligence instead of the raw models API endpoint, matching how the other providers link a human-readable page - use deepseek-ai/DeepSeek-V3.2 as the handler-spec fixture id; the previously used DeepSeek-V3.1 is no longer listed in the io.net catalog --- src/api/providers/__tests__/io-intelligence.spec.ts | 4 ++-- .../src/components/settings/providers/IOIntelligence.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/providers/__tests__/io-intelligence.spec.ts b/src/api/providers/__tests__/io-intelligence.spec.ts index 4e8770525f..0f3c23f852 100644 --- a/src/api/providers/__tests__/io-intelligence.spec.ts +++ b/src/api/providers/__tests__/io-intelligence.spec.ts @@ -82,11 +82,11 @@ describe("IOIntelligenceHandler", () => { ) const chunks = await collectStream( new IOIntelligenceHandler({ - ioIntelligenceModelId: "deepseek-ai/DeepSeek-V3.1", + ioIntelligenceModelId: "deepseek-ai/DeepSeek-V3.2", }).createMessage("sys", messages), ) expect(mockCreate).toHaveBeenCalledWith( - expect.objectContaining({ model: "deepseek-ai/DeepSeek-V3.1" }), + expect.objectContaining({ model: "deepseek-ai/DeepSeek-V3.2" }), expect.objectContaining({ signal: undefined }), ) expect(chunks).toEqual([ diff --git a/webview-ui/src/components/settings/providers/IOIntelligence.tsx b/webview-ui/src/components/settings/providers/IOIntelligence.tsx index 07fc24b47c..590f3861e9 100644 --- a/webview-ui/src/components/settings/providers/IOIntelligence.tsx +++ b/webview-ui/src/components/settings/providers/IOIntelligence.tsx @@ -83,7 +83,7 @@ export const IOIntelligence = ({ models={routerModels?.[providerIdentifiers.ioIntelligence] ?? {}} modelIdKey="ioIntelligenceModelId" serviceName={t("settings:providers.ioIntelligence.provider")} - serviceUrl="https://api.intelligence.io.solutions/api/v1/models" + serviceUrl="https://io.net/intelligence" label={t("settings:providers.ioIntelligence.model")} organizationAllowList={organizationAllowList} errorMessage={modelValidationError} From ad3d75d6d869ed1105ef2d863496775c7a6e6745 Mon Sep 17 00:00:00 2001 From: Smiral Rashinkar Date: Sat, 19 Sep 2026 13:54:29 +0530 Subject: [PATCH 08/23] fix: align IO Intelligence usage mapping and render order with the precedent - use ?? instead of || for cacheReadTokens so a legitimate cached_tokens of 0 is preserved, matching the NanoGPT usage mapping - move the ApiOptions render block after Kenari so io-intelligence sits in the same position as every other registration in this change --- src/api/providers/io-intelligence.ts | 2 +- webview-ui/src/components/settings/ApiOptions.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/api/providers/io-intelligence.ts b/src/api/providers/io-intelligence.ts index 2dc04587ec..2c329139fb 100644 --- a/src/api/providers/io-intelligence.ts +++ b/src/api/providers/io-intelligence.ts @@ -122,7 +122,7 @@ export class IOIntelligenceHandler extends RouterProvider implements SingleCompl type: "usage", inputTokens: chunk.usage.prompt_tokens || 0, outputTokens: chunk.usage.completion_tokens || 0, - cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || undefined, + cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens ?? undefined, } } } diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 87f1ac31ed..01f4c7d8c1 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -682,8 +682,8 @@ const ApiOptions = ({ /> )} - {selectedProvider === providerIdentifiers.ioIntelligence && ( - )} - {selectedProvider === providerIdentifiers.kenari && ( - Date: Mon, 21 Sep 2026 15:23:00 +0530 Subject: [PATCH 09/23] test(api): cover cached_tokens:0 preservation in the IO Intelligence usage mapping --- .../__tests__/io-intelligence.spec.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/api/providers/__tests__/io-intelligence.spec.ts b/src/api/providers/__tests__/io-intelligence.spec.ts index 0f3c23f852..148a42ea03 100644 --- a/src/api/providers/__tests__/io-intelligence.spec.ts +++ b/src/api/providers/__tests__/io-intelligence.spec.ts @@ -164,6 +164,36 @@ describe("IOIntelligenceHandler", () => { ]) }) + it("preserves a zero cache-read count instead of coercing it away", async () => { + mockCreate.mockResolvedValue( + asyncStreamFrom([ + { + choices: [], + usage: { + prompt_tokens: 20, + completion_tokens: 10, + prompt_tokens_details: { cached_tokens: 0 }, + }, + }, + ]), + ) + expect( + await collectStream( + new IOIntelligenceHandler({ ioIntelligenceModelId: "meta-llama/Llama-3.3-70B-Instruct" }).createMessage( + "sys", + messages, + ), + ), + ).toEqual([ + { + type: "usage", + inputTokens: 20, + outputTokens: 10, + cacheReadTokens: 0, + }, + ]) + }) + it("redacts the API key from streaming errors", async () => { mockCreate.mockRejectedValue(new Error("upstream rejected secret-key")) const handler = new IOIntelligenceHandler({ From e44e06ac88719dfdc79e80458cdaba3be3498497 Mon Sep 17 00:00:00 2001 From: Smiral Rashinkar Date: Wed, 23 Sep 2026 13:16:24 +0530 Subject: [PATCH 10/23] chore: drop the IO Intelligence changeset Routine provider PRs do not add changesets in this repo; they are only added during release preparation. --- .changeset/add-io-intelligence-provider.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/add-io-intelligence-provider.md diff --git a/.changeset/add-io-intelligence-provider.md b/.changeset/add-io-intelligence-provider.md deleted file mode 100644 index 373b5e89bd..0000000000 --- a/.changeset/add-io-intelligence-provider.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"zoo-code": minor ---- - -Add the IO Intelligence (io.net) provider with a dynamically fetched model catalog, secret-state API key storage, and catalog-derived metadata (context window, output limits, vision and prompt-cache capability flags, per-million pricing). From 23b0842a89f60fa01cf4d21b20c2609f9d7722b0 Mon Sep 17 00:00:00 2001 From: Smiral Rashinkar Date: Wed, 23 Sep 2026 13:16:35 +0530 Subject: [PATCH 11/23] fix(api): redact the IO Intelligence key before the provider error is logged handleProviderError logs the message and stack before it applies the messageTransformer, so an upstream error that echoed the API key reached the extension-host log even though the thrown error was redacted. Scrub the key from the error itself (and from non-Error values) before handing it over, and keep the transformer redaction for messages derived from other error fields. The spec now spies on console.error and also covers the non-Error and no-key paths. --- .../__tests__/io-intelligence.spec.ts | 74 ++++++++++++++++--- src/api/providers/io-intelligence.ts | 21 ++++-- 2 files changed, 81 insertions(+), 14 deletions(-) diff --git a/src/api/providers/__tests__/io-intelligence.spec.ts b/src/api/providers/__tests__/io-intelligence.spec.ts index 148a42ea03..84423fd662 100644 --- a/src/api/providers/__tests__/io-intelligence.spec.ts +++ b/src/api/providers/__tests__/io-intelligence.spec.ts @@ -33,6 +33,13 @@ vi.mocked(OpenAI).mockImplementation(function () { const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }] +/** Flattens every argument of every console.error call into one searchable string. */ +const loggedText = (spy: { mock: { calls: unknown[][] } }) => + spy.mock.calls + .flat() + .map((arg) => (typeof arg === "string" ? arg : JSON.stringify(arg))) + .join("\n") + describe("IOIntelligenceHandler", () => { beforeEach(() => { vi.clearAllMocks() @@ -194,15 +201,64 @@ describe("IOIntelligenceHandler", () => { ]) }) - it("redacts the API key from streaming errors", async () => { - mockCreate.mockRejectedValue(new Error("upstream rejected secret-key")) - const handler = new IOIntelligenceHandler({ - ioIntelligenceApiKey: "secret-key", - ioIntelligenceModelId: "meta-llama/Llama-3.3-70B-Instruct", - }) - await expect(collectStream(handler.createMessage("sys", messages))).rejects.toMatchObject({ - message: "IO Intelligence streaming error: upstream rejected [REDACTED]", - }) + it("redacts the API key from streaming errors and from the logged error", async () => { + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + try { + mockCreate.mockRejectedValue(new Error("upstream rejected secret-key")) + const handler = new IOIntelligenceHandler({ + ioIntelligenceApiKey: "secret-key", + ioIntelligenceModelId: "meta-llama/Llama-3.3-70B-Instruct", + }) + await expect(collectStream(handler.createMessage("sys", messages))).rejects.toMatchObject({ + message: "IO Intelligence streaming error: upstream rejected [REDACTED]", + }) + // The error handler logs message + stack before the transformer runs, + // so the key must already be gone from both. + expect(consoleErrorSpy).toHaveBeenCalledWith( + "[IO Intelligence] API error:", + expect.objectContaining({ + message: "upstream rejected [REDACTED]", + stack: expect.stringContaining("[REDACTED]"), + }), + ) + expect(loggedText(consoleErrorSpy)).not.toContain("secret-key") + } finally { + consoleErrorSpy.mockRestore() + } + }) + + it("redacts the API key from non-Error rejections and their log line", async () => { + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + try { + mockCreate.mockRejectedValue("upstream rejected secret-key") + const handler = new IOIntelligenceHandler({ + ioIntelligenceApiKey: "secret-key", + ioIntelligenceModelId: "meta-llama/Llama-3.3-70B-Instruct", + }) + await expect(collectStream(handler.createMessage("sys", messages))).rejects.toMatchObject({ + message: "IO Intelligence streaming error: upstream rejected [REDACTED]", + }) + expect(consoleErrorSpy).toHaveBeenCalledWith( + "[IO Intelligence] Non-Error exception:", + "upstream rejected [REDACTED]", + ) + expect(loggedText(consoleErrorSpy)).not.toContain("secret-key") + } finally { + consoleErrorSpy.mockRestore() + } + }) + + it("completePrompt wraps upstream errors without a configured key", async () => { + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + try { + mockCreate.mockRejectedValue(new Error("boom")) + const handler = new IOIntelligenceHandler({ ioIntelligenceModelId: "meta-llama/Llama-3.3-70B-Instruct" }) + await expect(handler.completePrompt("ping")).rejects.toMatchObject({ + message: "IO Intelligence completion error: boom", + }) + } finally { + consoleErrorSpy.mockRestore() + } }) it("completePrompt without options forwards no request options", async () => { diff --git a/src/api/providers/io-intelligence.ts b/src/api/providers/io-intelligence.ts index 2c329139fb..e83ad45869 100644 --- a/src/api/providers/io-intelligence.ts +++ b/src/api/providers/io-intelligence.ts @@ -43,12 +43,23 @@ export class IOIntelligenceHandler extends RouterProvider implements SingleCompl } private createSafeError(operation: string, error: unknown): Error { - return handleProviderError(error, "IO Intelligence", { + const apiKey = this.options.ioIntelligenceApiKey + const redact = (text: string) => (apiKey ? text.replaceAll(apiKey, "[REDACTED]") : text) + + // handleProviderError logs the message and stack before it applies the + // transformer, so the key has to be scrubbed from the error itself first. + if (error instanceof Error) { + error.message = redact(error.message) + if (error.stack) { + error.stack = redact(error.stack) + } + } + const safeError = error instanceof Error ? error : redact(String(error)) + + return handleProviderError(safeError, "IO Intelligence", { messagePrefix: operation, - messageTransformer: (message) => - this.options.ioIntelligenceApiKey - ? `IO Intelligence ${operation} error: ${message.replaceAll(this.options.ioIntelligenceApiKey, "[REDACTED]")}` - : `IO Intelligence ${operation} error: ${message}`, + // The transformer input can also come from `error.error.metadata.raw`. + messageTransformer: (message) => `IO Intelligence ${operation} error: ${redact(message)}`, }) } From 9949b13272c44373f1c98619c09930335028ee69 Mon Sep 17 00:00:00 2001 From: Smiral Rashinkar Date: Wed, 23 Sep 2026 13:16:36 +0530 Subject: [PATCH 12/23] test: reject a disallowed IO Intelligence model in ProfileValidator Add ioIntelligenceModelId to the positive-and-negative provider table and assert that a model absent from the allowlist is rejected. --- src/shared/__tests__/ProfileValidator.spec.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/shared/__tests__/ProfileValidator.spec.ts b/src/shared/__tests__/ProfileValidator.spec.ts index cdff408529..ddf282f13f 100644 --- a/src/shared/__tests__/ProfileValidator.spec.ts +++ b/src/shared/__tests__/ProfileValidator.spec.ts @@ -26,6 +26,7 @@ describe("ProfileValidator", () => { ["ollama", { ollamaModelId: "model" }], ["requesty", { requestyModelId: "model" }], ["unbound", { unboundModelId: "model" }], + ["ioIntelligence", { ioIntelligenceModelId: "model" }], ])("resolves %s model fields through canonical identifiers", (identifierKey, profileSettings) => { const canonicalIdentifier = providerIdentifiers[identifierKey as keyof typeof providerIdentifiers] const modelId = "model" @@ -295,6 +296,9 @@ describe("ProfileValidator", () => { } expect(ProfileValidator.isProfileAllowed(profile, allowList)).toBe(true) + expect( + ProfileValidator.isProfileAllowed({ ...profile, ioIntelligenceModelId: "other-model" }, allowList), + ).toBe(false) }) it("should extract vsCodeLmModelSelector.id for vscode-lm provider", () => { From 9802e785f0c15f3b6a18760a34da353dd94155b0 Mon Sep 17 00:00:00 2001 From: Smiral Rashinkar Date: Wed, 23 Sep 2026 13:16:36 +0530 Subject: [PATCH 13/23] fix(webview): debounce IO Intelligence catalog refreshes while the key is typed The API-key effect posted requestRouterModels on every keystroke, and because the IO Intelligence cache is keyed by the API key each partial value triggered a fresh catalog request. Use the same 250ms useDebounce as ApiOptions so only the settled key is sent, and drop a pending request on unmount. --- .../settings/providers/IOIntelligence.tsx | 28 +++++++---- .../__tests__/IOIntelligence.spec.tsx | 48 +++++++++++++++---- 2 files changed, 56 insertions(+), 20 deletions(-) diff --git a/webview-ui/src/components/settings/providers/IOIntelligence.tsx b/webview-ui/src/components/settings/providers/IOIntelligence.tsx index 590f3861e9..07a3a1d8bc 100644 --- a/webview-ui/src/components/settings/providers/IOIntelligence.tsx +++ b/webview-ui/src/components/settings/providers/IOIntelligence.tsx @@ -1,4 +1,5 @@ -import { useCallback, useEffect } from "react" +import { useCallback } from "react" +import { useDebounce } from "react-use" import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { @@ -47,15 +48,22 @@ export const IOIntelligence = ({ [setApiConfigurationField], ) - useEffect(() => { - vscode.postMessage({ - type: RouterModelsMessageType.requestRouterModels, - values: { - provider: providerIdentifiers.ioIntelligence, - ioIntelligenceApiKey: apiConfiguration.ioIntelligenceApiKey, - }, - }) - }, [apiConfiguration.ioIntelligenceApiKey]) + // Debounced model refresh, only executed 250ms after the user stops + // typing the key (same cadence as the provider refreshes in ApiOptions), + // so each keystroke does not trigger a catalog request. + useDebounce( + () => { + vscode.postMessage({ + type: RouterModelsMessageType.requestRouterModels, + values: { + provider: providerIdentifiers.ioIntelligence, + ioIntelligenceApiKey: apiConfiguration.ioIntelligenceApiKey, + }, + }) + }, + 250, + [apiConfiguration.ioIntelligenceApiKey], + ) return ( <> diff --git a/webview-ui/src/components/settings/providers/__tests__/IOIntelligence.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/IOIntelligence.spec.tsx index b79144f7cd..ae6f7bc63a 100644 --- a/webview-ui/src/components/settings/providers/__tests__/IOIntelligence.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/IOIntelligence.spec.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen } from "@testing-library/react" +import { act, fireEvent, render, screen } from "@testing-library/react" import { type OrganizationAllowList, @@ -99,6 +99,8 @@ describe("IOIntelligence", () => { beforeEach(() => vi.clearAllMocks()) + afterEach(() => vi.useRealTimers()) + it("renders the secret key input, CTA, and dynamic model picker", () => { renderComponent({ ioIntelligenceApiKey: "stored" }) @@ -129,25 +131,51 @@ describe("IOIntelligence", () => { expect(setApiConfigurationField).toHaveBeenCalledWith("ioIntelligenceApiKey", "new-secret") }) - it("refreshes models with the unsaved cached key whenever it changes", () => { + it("refreshes models with the unsaved cached key 250ms after it stops changing", () => { + vi.useFakeTimers() + const rerenderWithKey = (rerender: ReturnType["rerender"], ioIntelligenceApiKey: string) => + rerender( + , + ) + const { rerender } = renderComponent({ ioIntelligenceApiKey: "first-key" }) + + act(() => vi.advanceTimersByTime(249)) + expect(postMessageMock).not.toHaveBeenCalled() + act(() => vi.advanceTimersByTime(1)) + expect(postMessageMock).toHaveBeenCalledTimes(1) expect(postMessageMock).toHaveBeenLastCalledWith({ type: RouterModelsMessageType.requestRouterModels, values: { provider: providerIdentifiers.ioIntelligence, ioIntelligenceApiKey: "first-key" }, }) - rerender( - , - ) + // Each edit restarts the timer, so intermediate values never reach the host. + rerenderWithKey(rerender, "unsaved-") + act(() => vi.advanceTimersByTime(200)) + rerenderWithKey(rerender, "unsaved-key") + act(() => vi.advanceTimersByTime(249)) + expect(postMessageMock).toHaveBeenCalledTimes(1) + act(() => vi.advanceTimersByTime(1)) + expect(postMessageMock).toHaveBeenCalledTimes(2) expect(postMessageMock).toHaveBeenLastCalledWith({ type: RouterModelsMessageType.requestRouterModels, values: { provider: providerIdentifiers.ioIntelligence, ioIntelligenceApiKey: "unsaved-key" }, }) }) + + it("drops a pending model refresh when it unmounts", () => { + vi.useFakeTimers() + const { unmount } = renderComponent({ ioIntelligenceApiKey: "first-key" }) + + unmount() + act(() => vi.advanceTimersByTime(250)) + + expect(postMessageMock).not.toHaveBeenCalled() + }) }) From ffb2bcc83807a61e52c320034ab134233888a345 Mon Sep 17 00:00:00 2001 From: Smiral Rashinkar Date: Wed, 23 Sep 2026 13:16:37 +0530 Subject: [PATCH 14/23] fix(webview): keep an unlisted IO Intelligence model id as the picker selection A custom id stored through ModelPicker's "Use custom model" path was replaced by the default id in useSelectedModel when the fetched catalog did not list it, so the picker displayed a different model from the one the handler sends requests with (RouterProvider.getModel resolves modelId || defaultModelId). Preserve the configured id and only fall back the metadata, mirroring the litellm and kimi-code cases. Covered by a hook test and a real-ModelPicker selection test. --- .../settings/__tests__/ModelPicker.spec.tsx | 81 +++++++++++++++++++ .../hooks/__tests__/useSelectedModel.spec.ts | 31 ++++++- .../components/ui/hooks/useSelectedModel.ts | 10 +-- 3 files changed, 116 insertions(+), 6 deletions(-) diff --git a/webview-ui/src/components/settings/__tests__/ModelPicker.spec.tsx b/webview-ui/src/components/settings/__tests__/ModelPicker.spec.tsx index 9a21ff678f..de08eca01f 100644 --- a/webview-ui/src/components/settings/__tests__/ModelPicker.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ModelPicker.spec.tsx @@ -6,6 +6,7 @@ import { QueryClient } from "@tanstack/react-query" import { type Mock } from "vitest" import { + ioIntelligenceDefaultModelId, litellmDefaultModelId, type ModelInfo, type ProviderSettings, @@ -364,4 +365,84 @@ describe("ModelPicker", () => { expect(screen.getByTestId("model-picker-button")).not.toHaveTextContent(litellmDefaultModelId) }) }) + + describe("IO Intelligence custom model selection", () => { + const ioIntelligenceModels: Record = { + "meta-llama/Llama-3.3-70B-Instruct": { description: "Catalog model", ...modelInfo }, + } + + const renderIOIntelligencePicker = (apiConfiguration: ProviderSettings, setField: SetApiConfigurationField) => + renderWithExtensionState( + , + { queryClient }, + ) + + beforeEach(() => { + mockUseRouterModels.mockReturnValue(createRouterModelsResult({ "io-intelligence": ioIntelligenceModels })) + }) + + it("keeps a custom model ID in the picker instead of reverting to the default", async () => { + // The stored id is what the handler sends requests with, so the picker + // must keep displaying it even though the fetched catalog lacks it. + const customModelId = "custom-org/custom-model" + let apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.ioIntelligence } + const setField = vi.fn(function (field: K, value: ProviderSettings[K]) { + apiConfiguration = { ...apiConfiguration, [field]: value } + }) + + const { rerender } = await act(async () => { + return renderIOIntelligencePicker(apiConfiguration, setField) + }) + + expect(screen.getByTestId("model-picker-button")).toHaveTextContent(ioIntelligenceDefaultModelId) + + await act(async () => { + fireEvent.click(screen.getByTestId("model-picker-button")) + }) + await act(async () => { + vi.advanceTimersByTime(100) + }) + await act(async () => { + fireEvent.input(screen.getByTestId("model-input"), { target: { value: customModelId } }) + }) + await act(async () => { + vi.advanceTimersByTime(100) + }) + await act(async () => { + fireEvent.click(screen.getByTestId("use-custom-model")) + }) + await act(async () => { + vi.advanceTimersByTime(100) + }) + + expect(setField).toHaveBeenCalledWith("ioIntelligenceModelId", customModelId) + + await act(async () => { + rerender( + , + ) + }) + + expect(screen.getByTestId("model-picker-button")).toHaveTextContent(customModelId) + expect(screen.getByTestId("model-picker-button")).not.toHaveTextContent(ioIntelligenceDefaultModelId) + }) + }) }) diff --git a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts index 4f01a8b2e4..814a3066fb 100644 --- a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts +++ b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts @@ -1163,7 +1163,7 @@ describe("useSelectedModel", () => { () => useSelectedModel({ apiProvider: providerIdentifiers.ioIntelligence, - ioIntelligenceModelId: "missing-model", + // ioIntelligenceModelId intentionally omitted }), { wrapper: createWrapper() }, ) @@ -1171,6 +1171,35 @@ describe("useSelectedModel", () => { expect(result.current.id).toBe(ioIntelligenceDefaultModelId) expect(result.current.info).toEqual(ioIntelligenceDefaultModelInfo) }) + + it("preserves a configured model ID that is absent from the fetched catalog", () => { + mockUseRouterModels.mockReturnValue( + createRouterModelsResult({ + "io-intelligence": { + "meta-llama/Llama-3.3-70B-Instruct": { + maxTokens: 8_192, + contextWindow: 131_072, + supportsPromptCache: false, + }, + }, + }), + ) + mockUseOpenRouterModelProviders.mockReturnValue(createOpenRouterModelProvidersResult({})) + + const { result } = renderHook( + () => + useSelectedModel({ + apiProvider: providerIdentifiers.ioIntelligence, + ioIntelligenceModelId: "custom-org/custom-model", + }), + { wrapper: createWrapper() }, + ) + + // The stored id is what requests are sent with, so the picker must show it + // rather than the default; only the metadata falls back. + expect(result.current.id).toBe("custom-org/custom-model") + expect(result.current.info).toEqual(ioIntelligenceDefaultModelInfo) + }) }) describe("openai provider", () => { diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index dd87ba1e03..ad88b60b96 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -453,11 +453,11 @@ function getSelectedModel({ return { id, info } } case providerIdentifiers.ioIntelligence: { - const id = getValidatedModelId( - apiConfiguration.ioIntelligenceModelId, - routerModels[providerIdentifiers.ioIntelligence], - defaultModelId, - ) + // A configured id is the user's explicit selection (ModelPicker's + // "Use custom model" path stores ids absent from the fetched catalog) + // and the handler sends requests with it, so keep it instead of + // displaying a default model that requests do not use. + const id = apiConfiguration.ioIntelligenceModelId || defaultModelId const info = routerModels[providerIdentifiers.ioIntelligence]?.[id] ?? ioIntelligenceDefaultModelInfo return { id, info } } From 59465523095f6c6a5dfcf5d8446d72a67bdde1e0 Mon Sep 17 00:00:00 2001 From: Smiral Rashinkar Date: Thu, 24 Sep 2026 13:29:43 +0530 Subject: [PATCH 15/23] fix: redact raw error metadata and keep IO Intelligence ready after a catalog failure handleProviderError prefers error.error.metadata.raw as the logged message, so scrub the API key from that field alongside message and stack before the handler logs it. Treat IO Intelligence like LiteLLM in the readiness check so a settled catalog error still resolves the configured model id instead of falling back to the provider default. Regression tests for both. --- .../__tests__/io-intelligence.spec.ts | 27 +++++++++++++++++++ src/api/providers/io-intelligence.ts | 9 +++++-- .../hooks/__tests__/useSelectedModel.spec.ts | 23 ++++++++++++++++ .../components/ui/hooks/useSelectedModel.ts | 12 ++++----- 4 files changed, 63 insertions(+), 8 deletions(-) diff --git a/src/api/providers/__tests__/io-intelligence.spec.ts b/src/api/providers/__tests__/io-intelligence.spec.ts index 84423fd662..81f381c730 100644 --- a/src/api/providers/__tests__/io-intelligence.spec.ts +++ b/src/api/providers/__tests__/io-intelligence.spec.ts @@ -227,6 +227,33 @@ describe("IOIntelligenceHandler", () => { } }) + it("redacts the API key from the SDK's raw error metadata before it is logged", async () => { + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + try { + // The OpenAI SDK attaches the upstream body as `error.metadata.raw`, + // which the error handler prefers over `message` when logging. + mockCreate.mockRejectedValue( + Object.assign(new Error("Request failed"), { + error: { metadata: { raw: '{"detail":"invalid key secret-key"}' } }, + }), + ) + const handler = new IOIntelligenceHandler({ + ioIntelligenceApiKey: "secret-key", + ioIntelligenceModelId: "meta-llama/Llama-3.3-70B-Instruct", + }) + await expect(collectStream(handler.createMessage("sys", messages))).rejects.toMatchObject({ + message: 'IO Intelligence streaming error: {"detail":"invalid key [REDACTED]"}', + }) + expect(consoleErrorSpy).toHaveBeenCalledWith( + "[IO Intelligence] API error:", + expect.objectContaining({ message: '{"detail":"invalid key [REDACTED]"}' }), + ) + expect(loggedText(consoleErrorSpy)).not.toContain("secret-key") + } finally { + consoleErrorSpy.mockRestore() + } + }) + it("redacts the API key from non-Error rejections and their log line", async () => { const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) try { diff --git a/src/api/providers/io-intelligence.ts b/src/api/providers/io-intelligence.ts index e83ad45869..67c1215b62 100644 --- a/src/api/providers/io-intelligence.ts +++ b/src/api/providers/io-intelligence.ts @@ -46,13 +46,18 @@ export class IOIntelligenceHandler extends RouterProvider implements SingleCompl const apiKey = this.options.ioIntelligenceApiKey const redact = (text: string) => (apiKey ? text.replaceAll(apiKey, "[REDACTED]") : text) - // handleProviderError logs the message and stack before it applies the - // transformer, so the key has to be scrubbed from the error itself first. + // handleProviderError logs the message, stack and the SDK's raw error + // metadata before it applies the transformer, so the key has to be + // scrubbed from the error itself first. if (error instanceof Error) { error.message = redact(error.message) if (error.stack) { error.stack = redact(error.stack) } + const metadata = (error as { error?: { metadata?: { raw?: unknown } } }).error?.metadata + if (metadata && typeof metadata.raw === "string") { + metadata.raw = redact(metadata.raw) + } } const safeError = error instanceof Error ? error : redact(String(error)) diff --git a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts index 814a3066fb..0580296b44 100644 --- a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts +++ b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts @@ -1200,6 +1200,29 @@ describe("useSelectedModel", () => { expect(result.current.id).toBe("custom-org/custom-model") expect(result.current.info).toEqual(ioIntelligenceDefaultModelInfo) }) + + it("preserves the configured model ID when the catalog fetch errors", () => { + // A cold /models failure settles the router query with no data. The + // hook must still resolve and keep the configured ID (the handler + // sends requests with it) instead of resetting to the provider default. + mockUseRouterModels.mockReturnValue( + createRouterModelsResult(undefined, { isLoading: false, isError: true }), + ) + mockUseOpenRouterModelProviders.mockReturnValue(createOpenRouterModelProvidersResult({})) + + const { result } = renderHook( + () => + useSelectedModel({ + apiProvider: providerIdentifiers.ioIntelligence, + ioIntelligenceModelId: "zai-org/GLM-4.6", + }), + { wrapper: createWrapper() }, + ) + + expect(result.current.id).toBe("zai-org/GLM-4.6") + expect(result.current.info).toEqual(ioIntelligenceDefaultModelInfo) + expect(result.current.isError).toBe(true) + }) }) describe("openai provider", () => { diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index ad88b60b96..6a96aa2d69 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -96,14 +96,14 @@ export const useSelectedModel = (apiConfiguration?: ProviderSettings) => { const needLmStudio = typeof lmStudioModelId !== "undefined" const needOllama = typeof ollamaModelId !== "undefined" - // LiteLLM may legitimately have no entry in the router payload (partial - // listing, failed fetch, renamed deployment) even though the configured - // ID is a valid selection, so it only needs the fetch to settle. Other - // dynamic providers require a populated provider entry before the - // selection is resolved. + // LiteLLM and IO Intelligence may legitimately have no entry in the router + // payload (partial listing, failed fetch, renamed deployment) even though + // the configured ID is a valid selection, so they only need the fetch to + // settle. Other dynamic providers require a populated provider entry + // before the selection is resolved. const hasValidRouterData = needRouterModels && dynamicProvider - ? dynamicProvider === providerIdentifiers.litellm + ? dynamicProvider === providerIdentifiers.litellm || dynamicProvider === providerIdentifiers.ioIntelligence ? !routerModels.isLoading : routerModels.data && routerModels.data[dynamicProvider] !== undefined && From a7379bc8159592be2da8db6bbb973850116bec5e Mon Sep 17 00:00:00 2001 From: Smiral Rashinkar Date: Sat, 26 Sep 2026 19:03:30 +0530 Subject: [PATCH 16/23] fix: serialize object-valued raw error bodies before redacting them handleProviderError calls string methods on error.error.metadata.raw, so an object-valued upstream body reached it unredacted and threw a TypeError before the status was preserved. Serialize it first, then redact. Regression test with an object body and a 401 status. --- .../__tests__/io-intelligence.spec.ts | 27 +++++++++++++++++++ src/api/providers/io-intelligence.ts | 7 +++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/api/providers/__tests__/io-intelligence.spec.ts b/src/api/providers/__tests__/io-intelligence.spec.ts index 81f381c730..a35cfb51ff 100644 --- a/src/api/providers/__tests__/io-intelligence.spec.ts +++ b/src/api/providers/__tests__/io-intelligence.spec.ts @@ -254,6 +254,33 @@ describe("IOIntelligenceHandler", () => { } }) + it("serializes and redacts an object-valued raw error body and keeps the status", async () => { + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + try { + mockCreate.mockRejectedValue( + Object.assign(new Error("Request failed"), { + status: 401, + error: { metadata: { raw: { detail: "invalid key secret-key" } } }, + }), + ) + const handler = new IOIntelligenceHandler({ + ioIntelligenceApiKey: "secret-key", + ioIntelligenceModelId: "meta-llama/Llama-3.3-70B-Instruct", + }) + await expect(collectStream(handler.createMessage("sys", messages))).rejects.toMatchObject({ + message: 'IO Intelligence streaming error: {"detail":"invalid key [REDACTED]"}', + status: 401, + }) + expect(consoleErrorSpy).toHaveBeenCalledWith( + "[IO Intelligence] API error:", + expect.objectContaining({ message: '{"detail":"invalid key [REDACTED]"}', status: 401 }), + ) + expect(loggedText(consoleErrorSpy)).not.toContain("secret-key") + } finally { + consoleErrorSpy.mockRestore() + } + }) + it("redacts the API key from non-Error rejections and their log line", async () => { const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) try { diff --git a/src/api/providers/io-intelligence.ts b/src/api/providers/io-intelligence.ts index 67c1215b62..e65cee8c9b 100644 --- a/src/api/providers/io-intelligence.ts +++ b/src/api/providers/io-intelligence.ts @@ -54,9 +54,12 @@ export class IOIntelligenceHandler extends RouterProvider implements SingleCompl if (error.stack) { error.stack = redact(error.stack) } + // handleProviderError calls string methods on this field, so an + // object-valued upstream body is serialized before it is redacted. const metadata = (error as { error?: { metadata?: { raw?: unknown } } }).error?.metadata - if (metadata && typeof metadata.raw === "string") { - metadata.raw = redact(metadata.raw) + if (metadata && metadata.raw !== undefined && metadata.raw !== null) { + const raw = metadata.raw + metadata.raw = redact(typeof raw === "string" ? raw : (JSON.stringify(raw) ?? String(raw))) } } const safeError = error instanceof Error ? error : redact(String(error)) From f525c0c8f616c4d04bae3441bf56d7f2886db594 Mon Sep 17 00:00:00 2001 From: Smiral Rashinkar Date: Sat, 26 Sep 2026 20:47:01 +0530 Subject: [PATCH 17/23] fix(io-intelligence): stop rendering catalog names as model descriptions The IO Intelligence catalog has no free-text description field, so the fetcher was copying the remote model name into ModelInfo.description, which the settings view renders as Markdown. Leave description unset and keep the name as the plain-text displayName only. Add focused tests for the requestRouterModels path (stored key, unsaved key from message values, explicit empty key) and for the IO Intelligence provider validation. --- .../__tests__/io-intelligence.spec.ts | 3 - src/api/providers/fetchers/io-intelligence.ts | 4 +- ...webviewMessageHandler.routerModels.spec.ts | 72 +++++++++++++++++++ .../src/utils/__tests__/validate.spec.ts | 21 ++++++ 4 files changed, 96 insertions(+), 4 deletions(-) diff --git a/src/api/providers/fetchers/__tests__/io-intelligence.spec.ts b/src/api/providers/fetchers/__tests__/io-intelligence.spec.ts index 6738c17add..6fcf02448c 100644 --- a/src/api/providers/fetchers/__tests__/io-intelligence.spec.ts +++ b/src/api/providers/fetchers/__tests__/io-intelligence.spec.ts @@ -62,7 +62,6 @@ describe("IO Intelligence model fetcher", () => { supportsImages: true, supportsPromptCache: true, displayName: "Vision Model", - description: "Vision Model", inputPrice: 0.306, outputPrice: 1.224, cacheReadsPrice: 0.153, @@ -83,7 +82,6 @@ describe("IO Intelligence model fetcher", () => { maxTokens: ioIntelligenceDefaultModelInfo.maxTokens, supportsImages: false, supportsPromptCache: false, - description: "eligible", }, }) expect(warning).toHaveBeenCalledOnce() @@ -155,7 +153,6 @@ describe("IO Intelligence model fetcher", () => { maxTokens: ioIntelligenceDefaultModelInfo.maxTokens, supportsImages: false, supportsPromptCache: false, - description: "minimal", }) }) }) diff --git a/src/api/providers/fetchers/io-intelligence.ts b/src/api/providers/fetchers/io-intelligence.ts index 40522abfe0..3c0f5b8932 100644 --- a/src/api/providers/fetchers/io-intelligence.ts +++ b/src/api/providers/fetchers/io-intelligence.ts @@ -44,8 +44,10 @@ export const parseIoIntelligenceModel = (model: IOIntelligenceModel): ModelInfo ...(model.cache_read_token_price !== undefined ? { cacheReadsPrice: model.cache_read_token_price * 1_000_000 } : {}), + // The catalog has no free-text description. Its name and id are remote + // strings and ModelInfo.description is rendered as Markdown, so neither is + // copied there; the name is only surfaced as the plain-text displayName. ...(model.name !== undefined ? { displayName: model.name } : {}), - description: model.name ?? model.id, }) /** diff --git a/src/core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts index 51ffd5a614..bf30bc1598 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts @@ -453,6 +453,78 @@ describe("webviewMessageHandler - requestRouterModels provider filter", () => { expect(getModelsMock).toHaveBeenCalledWith(deepSeekOptions) }) + it("fetches IO Intelligence models with the stored key and does not flush the cache", async () => { + mockProvider.getState.mockResolvedValue({ + apiConfiguration: { ioIntelligenceApiKey: "stored-io-key" }, + }) + + await webviewMessageHandler(mockProvider, { + type: RouterModelsMessageType.requestRouterModels, + }) + + const ioFlushCalls = flushModelsMock.mock.calls.filter( + (c) => c[0]?.provider === providerIdentifiers.ioIntelligence, + ) + expect(ioFlushCalls.length).toBe(0) + + const ioCalls = getModelsMock.mock.calls.filter((c) => c[0]?.provider === providerIdentifiers.ioIntelligence) + expect(ioCalls.length).toBe(1) + expect(ioCalls[0][0]).toEqual({ provider: providerIdentifiers.ioIntelligence, apiKey: "stored-io-key" }) + + expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ + type: RouterModelsMessageType.routerModels, + routerModels: expect.objectContaining({ + [providerIdentifiers.ioIntelligence]: { + "io/model": { contextWindow: 8192, supportsPromptCache: false }, + }, + }), + }), + ) + }) + + it("flushes IO Intelligence models with an unsaved key from message values instead of the stored key", async () => { + mockProvider.getState.mockResolvedValue({ + apiConfiguration: { ioIntelligenceApiKey: "stored-io-key" }, + }) + + await webviewMessageHandler(mockProvider, { + type: RouterModelsMessageType.requestRouterModels, + values: { ioIntelligenceApiKey: "unsaved-io-key" }, + }) + + const ioFlushCalls = flushModelsMock.mock.calls.filter( + (c) => c[0]?.provider === providerIdentifiers.ioIntelligence, + ) + expect(ioFlushCalls.length).toBe(1) + expect(ioFlushCalls[0][0]).toEqual({ provider: providerIdentifiers.ioIntelligence, apiKey: "unsaved-io-key" }) + + const ioCalls = getModelsMock.mock.calls.filter((c) => c[0]?.provider === providerIdentifiers.ioIntelligence) + expect(ioCalls.length).toBe(1) + expect(ioCalls[0][0]).toEqual({ provider: providerIdentifiers.ioIntelligence, apiKey: "unsaved-io-key" }) + }) + + it("treats an explicitly empty unsaved IO Intelligence key as the public catalog, not the stored key", async () => { + mockProvider.getState.mockResolvedValue({ + apiConfiguration: { ioIntelligenceApiKey: "stored-io-key" }, + }) + + await webviewMessageHandler(mockProvider, { + type: RouterModelsMessageType.requestRouterModels, + values: { ioIntelligenceApiKey: "" }, + }) + + const ioFlushCalls = flushModelsMock.mock.calls.filter( + (c) => c[0]?.provider === providerIdentifiers.ioIntelligence, + ) + expect(ioFlushCalls.length).toBe(1) + expect(ioFlushCalls[0][0]).toEqual({ provider: providerIdentifiers.ioIntelligence, apiKey: "" }) + + const ioCalls = getModelsMock.mock.calls.filter((c) => c[0]?.provider === providerIdentifiers.ioIntelligence) + expect(ioCalls.length).toBe(1) + expect(ioCalls[0][0]).toEqual({ provider: providerIdentifiers.ioIntelligence, apiKey: "" }) + }) + it("fetches Moonshot models when stored Moonshot credentials exist", async () => { mockProvider.getState.mockResolvedValue({ apiConfiguration: { diff --git a/webview-ui/src/utils/__tests__/validate.spec.ts b/webview-ui/src/utils/__tests__/validate.spec.ts index f4c90c6ddd..05f067ce91 100644 --- a/webview-ui/src/utils/__tests__/validate.spec.ts +++ b/webview-ui/src/utils/__tests__/validate.spec.ts @@ -417,6 +417,27 @@ describe("Model Validation Functions", () => { }) }) + describe("IO Intelligence validation", () => { + it("returns an apiKey error when the IO Intelligence API key is unset", () => { + const config: ProviderSettings = { + apiProvider: providerIdentifiers.ioIntelligence, + ioIntelligenceModelId: "zai-org/GLM-4.6", + } + + expect(validateApiConfiguration(config, mockRouterModels)).toBe("settings:validation.apiKey") + }) + + it("accepts an IO Intelligence API key and selected model", () => { + const config: ProviderSettings = { + apiProvider: providerIdentifiers.ioIntelligence, + ioIntelligenceApiKey: "valid-key", + ioIntelligenceModelId: "zai-org/GLM-4.6", + } + + expect(validateApiConfiguration(config, mockRouterModels)).toBeUndefined() + }) + }) + describe("NanoGPT validation", () => { it("returns an apiKey error when the NanoGPT API key is missing", () => { const config: ProviderSettings = { From 2ed2d75487d94a3779bb39e34f381e9ec815a2de Mon Sep 17 00:00:00 2001 From: Smiral Rashinkar Date: Sat, 26 Sep 2026 21:15:07 +0530 Subject: [PATCH 18/23] fix(io-intelligence): do not forward OpenAI Compatible custom headers The IO Intelligence endpoint is fixed, so the custom headers configured for the OpenAI Compatible provider are dropped before the client is built. Add tests for the header isolation and for omitting temperature on a model that does not support it (streaming and completePrompt). --- .../__tests__/io-intelligence.spec.ts | 35 +++++++++++++++++++ src/api/providers/io-intelligence.ts | 4 ++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/api/providers/__tests__/io-intelligence.spec.ts b/src/api/providers/__tests__/io-intelligence.spec.ts index a35cfb51ff..076058134c 100644 --- a/src/api/providers/__tests__/io-intelligence.spec.ts +++ b/src/api/providers/__tests__/io-intelligence.spec.ts @@ -336,6 +336,41 @@ describe("IOIntelligenceHandler", () => { expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({ stream: false }), { signal, timeout: 5_000 }) }) + it("does not send the OpenAI Compatible custom headers to the fixed endpoint", () => { + new IOIntelligenceHandler({ + ioIntelligenceApiKey: "test-key", + openAiHeaders: { "X-Custom-Header": "leaked" }, + }) + expect(vi.mocked(OpenAI)).toHaveBeenCalledTimes(1) + const clientOptions = vi.mocked(OpenAI).mock.calls[0][0] + expect(clientOptions?.defaultHeaders).not.toHaveProperty("X-Custom-Header") + }) + + it("omits temperature for a model that does not support it", async () => { + vi.mocked(getModels).mockResolvedValue({ + "openai/o3-mini": { + maxTokens: 8192, + contextWindow: 128000, + supportsImages: false, + supportsPromptCache: false, + }, + }) + const handler = new IOIntelligenceHandler({ + ioIntelligenceModelId: "openai/o3-mini", + modelTemperature: 0.7, + }) + + await collectStream(handler.createMessage("system", messages)) + expect(mockCreate).toHaveBeenCalledTimes(1) + expect(mockCreate.mock.calls[0][0]).not.toHaveProperty("temperature") + + mockCreate.mockClear() + mockCreate.mockResolvedValue({ choices: [{ message: { role: "assistant", content: "ok" } }] }) + expect(await handler.completePrompt("ping")).toBe("ok") + expect(mockCreate).toHaveBeenCalledTimes(1) + expect(mockCreate.mock.calls[0][0]).not.toHaveProperty("temperature") + }) + it("completePrompt forwards the configured temperature", async () => { mockCreate.mockResolvedValue({ choices: [{ message: { role: "assistant", content: "ok" } }], diff --git a/src/api/providers/io-intelligence.ts b/src/api/providers/io-intelligence.ts index e65cee8c9b..7fbe287209 100644 --- a/src/api/providers/io-intelligence.ts +++ b/src/api/providers/io-intelligence.ts @@ -32,7 +32,9 @@ export class IOIntelligenceHandler extends RouterProvider implements SingleCompl /** Creates a new handler bound to the user's API key and selected model. */ constructor(options: ApiHandlerOptions) { super({ - options, + // The endpoint is fixed, so the custom headers configured for the + // OpenAI Compatible provider must not be sent to it. + options: { ...options, openAiHeaders: undefined }, name: providerIdentifiers.ioIntelligence, baseURL: IO_INTELLIGENCE_BASE_URL, apiKey: options.ioIntelligenceApiKey, From eee097a13f33e180ed4c0803d0fe3e224b7b341a Mon Sep 17 00:00:00 2001 From: Smiral Rashinkar Date: Sat, 26 Sep 2026 21:27:44 +0530 Subject: [PATCH 19/23] test(io-intelligence): assert the catalog published for each router-models key --- ...webviewMessageHandler.routerModels.spec.ts | 47 ++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/src/core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts index bf30bc1598..d5c7d96eb2 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts @@ -453,10 +453,41 @@ describe("webviewMessageHandler - requestRouterModels provider filter", () => { expect(getModelsMock).toHaveBeenCalledWith(deepSeekOptions) }) + // One catalog per IO Intelligence key, so a fetch made with the wrong key publishes the wrong catalog. + const ioIntelligenceCatalogs: Record< + string, + Record + > = { + "stored-io-key": { "io/stored-key-model": { contextWindow: 8192, supportsPromptCache: false } }, + "unsaved-io-key": { "io/unsaved-key-model": { contextWindow: 8192, supportsPromptCache: false } }, + "": { "io/public-model": { contextWindow: 8192, supportsPromptCache: false } }, + } + + const mockIoIntelligenceCatalogsByKey = () => { + const baseGetModels = getModelsMock.getMockImplementation() + getModelsMock.mockImplementation(async (options) => + options?.provider === providerIdentifiers.ioIntelligence + ? ioIntelligenceCatalogs[options.apiKey] + : baseGetModels?.(options), + ) + } + + const expectPublishedIoIntelligenceCatalog = (apiKey: string) => { + expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ + type: RouterModelsMessageType.routerModels, + routerModels: expect.objectContaining({ + [providerIdentifiers.ioIntelligence]: ioIntelligenceCatalogs[apiKey], + }), + }), + ) + } + it("fetches IO Intelligence models with the stored key and does not flush the cache", async () => { mockProvider.getState.mockResolvedValue({ apiConfiguration: { ioIntelligenceApiKey: "stored-io-key" }, }) + mockIoIntelligenceCatalogsByKey() await webviewMessageHandler(mockProvider, { type: RouterModelsMessageType.requestRouterModels, @@ -470,23 +501,14 @@ describe("webviewMessageHandler - requestRouterModels provider filter", () => { const ioCalls = getModelsMock.mock.calls.filter((c) => c[0]?.provider === providerIdentifiers.ioIntelligence) expect(ioCalls.length).toBe(1) expect(ioCalls[0][0]).toEqual({ provider: providerIdentifiers.ioIntelligence, apiKey: "stored-io-key" }) - - expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith( - expect.objectContaining({ - type: RouterModelsMessageType.routerModels, - routerModels: expect.objectContaining({ - [providerIdentifiers.ioIntelligence]: { - "io/model": { contextWindow: 8192, supportsPromptCache: false }, - }, - }), - }), - ) + expectPublishedIoIntelligenceCatalog("stored-io-key") }) it("flushes IO Intelligence models with an unsaved key from message values instead of the stored key", async () => { mockProvider.getState.mockResolvedValue({ apiConfiguration: { ioIntelligenceApiKey: "stored-io-key" }, }) + mockIoIntelligenceCatalogsByKey() await webviewMessageHandler(mockProvider, { type: RouterModelsMessageType.requestRouterModels, @@ -502,12 +524,14 @@ describe("webviewMessageHandler - requestRouterModels provider filter", () => { const ioCalls = getModelsMock.mock.calls.filter((c) => c[0]?.provider === providerIdentifiers.ioIntelligence) expect(ioCalls.length).toBe(1) expect(ioCalls[0][0]).toEqual({ provider: providerIdentifiers.ioIntelligence, apiKey: "unsaved-io-key" }) + expectPublishedIoIntelligenceCatalog("unsaved-io-key") }) it("treats an explicitly empty unsaved IO Intelligence key as the public catalog, not the stored key", async () => { mockProvider.getState.mockResolvedValue({ apiConfiguration: { ioIntelligenceApiKey: "stored-io-key" }, }) + mockIoIntelligenceCatalogsByKey() await webviewMessageHandler(mockProvider, { type: RouterModelsMessageType.requestRouterModels, @@ -523,6 +547,7 @@ describe("webviewMessageHandler - requestRouterModels provider filter", () => { const ioCalls = getModelsMock.mock.calls.filter((c) => c[0]?.provider === providerIdentifiers.ioIntelligence) expect(ioCalls.length).toBe(1) expect(ioCalls[0][0]).toEqual({ provider: providerIdentifiers.ioIntelligence, apiKey: "" }) + expectPublishedIoIntelligenceCatalog("") }) it("fetches Moonshot models when stored Moonshot credentials exist", async () => { From 073ef6f659a6235b827a4a5d6123af5675b70e78 Mon Sep 17 00:00:00 2001 From: Smiral Rashinkar Date: Sun, 27 Sep 2026 00:07:08 +0530 Subject: [PATCH 20/23] fix(io-intelligence): build the model catalog on a null-prototype object --- .../fetchers/__tests__/io-intelligence.spec.ts | 11 +++++++++++ src/api/providers/fetchers/io-intelligence.ts | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/api/providers/fetchers/__tests__/io-intelligence.spec.ts b/src/api/providers/fetchers/__tests__/io-intelligence.spec.ts index 6fcf02448c..e9c8f22a5a 100644 --- a/src/api/providers/fetchers/__tests__/io-intelligence.spec.ts +++ b/src/api/providers/fetchers/__tests__/io-intelligence.spec.ts @@ -88,6 +88,17 @@ describe("IO Intelligence model fetcher", () => { warning.mockRestore() }) + it("keeps a hostile catalog id as an own entry instead of polluting the prototype chain", async () => { + vi.mocked(axios.get).mockResolvedValue({ + data: { data: [{ id: "__proto__", max_tokens: 1 }, { id: "constructor" }] }, + }) + const models = await getIOIntelligenceModels() + expect(Object.getPrototypeOf(models)).toBeNull() + expect(Object.keys(models)).toEqual(["__proto__", "constructor"]) + expect(models["__proto__"]).toMatchObject({ maxTokens: 1 }) + expect(({} as Record).maxTokens).toBeUndefined() + }) + it("keeps models with null token metadata and prefers context_window over max_model_len", () => { expect( parseIoIntelligenceModel({ id: "both", max_model_len: 65_536, context_window: 262_144 }).contextWindow, diff --git a/src/api/providers/fetchers/io-intelligence.ts b/src/api/providers/fetchers/io-intelligence.ts index 3c0f5b8932..2fd61dac3b 100644 --- a/src/api/providers/fetchers/io-intelligence.ts +++ b/src/api/providers/fetchers/io-intelligence.ts @@ -69,7 +69,8 @@ export async function getIOIntelligenceModels(apiKey?: string): Promise Date: Fri, 2 Oct 2026 13:45:09 +0700 Subject: [PATCH 21/23] test(io-intelligence): pin the request messages and the completePrompt payload --- .../__tests__/io-intelligence.spec.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/api/providers/__tests__/io-intelligence.spec.ts b/src/api/providers/__tests__/io-intelligence.spec.ts index 076058134c..11166a1533 100644 --- a/src/api/providers/__tests__/io-intelligence.spec.ts +++ b/src/api/providers/__tests__/io-intelligence.spec.ts @@ -123,6 +123,11 @@ describe("IOIntelligenceHandler", () => { ) expect(mockCreate).toHaveBeenCalledWith( expect.objectContaining({ + model: "meta-llama/Llama-3.3-70B-Instruct", + messages: [ + { role: "system", content: "sys" }, + { role: "user", content: "Hello" }, + ], stream: true, stream_options: { include_usage: true }, max_tokens: 8192, @@ -315,6 +320,29 @@ describe("IOIntelligenceHandler", () => { } }) + it("completePrompt sends the prompt as a single user message with the model's max_tokens", async () => { + mockCreate.mockResolvedValue({ + choices: [{ message: { role: "assistant", content: "ok" } }], + }) + const handler = new IOIntelligenceHandler({ ioIntelligenceModelId: "meta-llama/Llama-3.3-70B-Instruct" }) + expect(await handler.completePrompt("ping")).toBe("ok") + expect(mockCreate).toHaveBeenCalledWith( + { + model: "meta-llama/Llama-3.3-70B-Instruct", + messages: [{ role: "user", content: "ping" }], + max_tokens: 8192, + stream: false, + }, + undefined, + ) + }) + + it("completePrompt returns an empty string when the response has no choices", async () => { + mockCreate.mockResolvedValue({ choices: [] }) + const handler = new IOIntelligenceHandler({ ioIntelligenceModelId: "meta-llama/Llama-3.3-70B-Instruct" }) + expect(await handler.completePrompt("ping")).toBe("") + }) + it("completePrompt without options forwards no request options", async () => { mockCreate.mockResolvedValue({ choices: [{ message: { role: "assistant", content: "ok" } }], From 7a4fbefd1280da5c51a558b3c5d9093bd29f2677 Mon Sep 17 00:00:00 2001 From: Smiral Rashinkar Date: Fri, 2 Oct 2026 14:03:22 +0700 Subject: [PATCH 22/23] test(io-intelligence): add a visual snapshot of the provider settings --- webview-ui/playwright/gallery/stories.tsx | 33 ++++++++++++++++++ .../__tests__/IOIntelligence.visual.tsx | 13 +++++++ .../io-intelligence-settings.png | Bin 0 -> 21898 bytes 3 files changed, 46 insertions(+) create mode 100644 webview-ui/src/components/settings/providers/__tests__/IOIntelligence.visual.tsx create mode 100644 webview-ui/src/components/settings/providers/__tests__/__screenshots__/io-intelligence-settings.png diff --git a/webview-ui/playwright/gallery/stories.tsx b/webview-ui/playwright/gallery/stories.tsx index 04ca36b2c6..d5321a3c48 100644 --- a/webview-ui/playwright/gallery/stories.tsx +++ b/webview-ui/playwright/gallery/stories.tsx @@ -73,6 +73,39 @@ export const stories: Record = { ) }, + "io-intelligence-settings": async () => { + const [{ AppProviders }, { IOIntelligence }, { providerIdentifiers }] = await Promise.all([ + import("../AppProviders"), + import("@/components/settings/providers/IOIntelligence"), + import("@roo-code/types"), + ]) + type ProviderSettings = React.ComponentProps["apiConfiguration"] + + function IOIntelligenceSettingsStory() { + const [apiConfiguration, setApiConfiguration] = useState({ + apiProvider: providerIdentifiers.ioIntelligence, + }) + return ( +
+
+ + setApiConfiguration((current) => ({ ...current, [field]: value })) + } + organizationAllowList={{ allowAll: true, providers: {} }} + /> +
+
+ ) + } + + return ( + + + + ) + }, "layout-clipped-text": () => (
diff --git a/webview-ui/src/components/settings/providers/__tests__/IOIntelligence.visual.tsx b/webview-ui/src/components/settings/providers/__tests__/IOIntelligence.visual.tsx new file mode 100644 index 0000000000..6e56d6346a --- /dev/null +++ b/webview-ui/src/components/settings/providers/__tests__/IOIntelligence.visual.tsx @@ -0,0 +1,13 @@ +import { expect, test } from "../../../../../playwright/coverage-fixture" +import { mountedStory } from "../../../../../playwright/mounted-story" + +test("renders the IO Intelligence settings for an unconfigured profile", async ({ mount, page }) => { + await page.setViewportSize({ width: 480, height: 480 }) + const component = mountedStory(await mount("io-intelligence-settings")) + const story = component.getByTestId("io-intelligence-settings-story") + + await expect(story.getByText("IO Intelligence API Key", { exact: true })).toBeVisible() + await expect(story.getByRole("button", { name: "Get IO Intelligence API Key" })).toBeVisible() + await expect(story.getByTestId("model-picker-button")).toHaveText("meta-llama/Llama-3.3-70B-Instruct") + await expect(story).toHaveScreenshot("io-intelligence-settings.png") +}) diff --git a/webview-ui/src/components/settings/providers/__tests__/__screenshots__/io-intelligence-settings.png b/webview-ui/src/components/settings/providers/__tests__/__screenshots__/io-intelligence-settings.png new file mode 100644 index 0000000000000000000000000000000000000000..b5ad92f3ef1eff32e5b85769c8178666364b064f GIT binary patch literal 21898 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 literal 0 HcmV?d00001 From e121cade26170fb92a3d81a775627d5b4da4ca68 Mon Sep 17 00:00:00 2001 From: Smiral Rashinkar Date: Mon, 5 Oct 2026 18:12:22 +0700 Subject: [PATCH 23/23] test(io-intelligence): keep only the screenshot check in the visual test --- .../settings/providers/__tests__/IOIntelligence.visual.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/webview-ui/src/components/settings/providers/__tests__/IOIntelligence.visual.tsx b/webview-ui/src/components/settings/providers/__tests__/IOIntelligence.visual.tsx index 6e56d6346a..f845abe39e 100644 --- a/webview-ui/src/components/settings/providers/__tests__/IOIntelligence.visual.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/IOIntelligence.visual.tsx @@ -6,8 +6,5 @@ test("renders the IO Intelligence settings for an unconfigured profile", async ( const component = mountedStory(await mount("io-intelligence-settings")) const story = component.getByTestId("io-intelligence-settings-story") - await expect(story.getByText("IO Intelligence API Key", { exact: true })).toBeVisible() - await expect(story.getByRole("button", { name: "Get IO Intelligence API Key" })).toBeVisible() - await expect(story.getByTestId("model-picker-button")).toHaveText("meta-llama/Llama-3.3-70B-Instruct") await expect(story).toHaveScreenshot("io-intelligence-settings.png") })