Add IO Intelligence Provider (#6875)

Co-authored-by: daniel-lxs <ricciodaniel98@gmail.com>
This commit is contained in:
Ertan Dagistanli 2025-08-11 23:54:35 +03:00 committed by GitHub
parent bce579f4c1
commit 1018b885ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 850 additions and 4 deletions

View file

@ -194,6 +194,7 @@ export const SECRET_STATE_KEYS = [
"huggingFaceApiKey",
"sambaNovaApiKey",
"fireworksApiKey",
"ioIntelligenceApiKey",
] as const satisfies readonly (keyof ProviderSettings)[]
export type SecretState = Pick<ProviderSettings, (typeof SECRET_STATE_KEYS)[number]>

View file

@ -43,6 +43,7 @@ export const providerNames = [
"sambanova",
"zai",
"fireworks",
"io-intelligence",
] as const
export const providerNamesSchema = z.enum(providerNames)
@ -276,6 +277,11 @@ const fireworksSchema = apiModelIdProviderModelSchema.extend({
fireworksApiKey: z.string().optional(),
})
const ioIntelligenceSchema = apiModelIdProviderModelSchema.extend({
ioIntelligenceModelId: z.string().optional(),
ioIntelligenceApiKey: z.string().optional(),
})
const defaultSchema = z.object({
apiProvider: z.undefined(),
})
@ -311,6 +317,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
sambaNovaSchema.merge(z.object({ apiProvider: z.literal("sambanova") })),
zaiSchema.merge(z.object({ apiProvider: z.literal("zai") })),
fireworksSchema.merge(z.object({ apiProvider: z.literal("fireworks") })),
ioIntelligenceSchema.merge(z.object({ apiProvider: z.literal("io-intelligence") })),
defaultSchema,
])
@ -346,6 +353,7 @@ export const providerSettingsSchema = z.object({
...sambaNovaSchema.shape,
...zaiSchema.shape,
...fireworksSchema.shape,
...ioIntelligenceSchema.shape,
...codebaseIndexProviderSchema.shape,
})
@ -371,6 +379,7 @@ export const MODEL_ID_KEYS: Partial<keyof ProviderSettings>[] = [
"requestyModelId",
"litellmModelId",
"huggingFaceModelId",
"ioIntelligenceModelId",
]
export const getModelId = (settings: ProviderSettings): string | undefined => {

View file

@ -8,6 +8,7 @@ export * from "./gemini.js"
export * from "./glama.js"
export * from "./groq.js"
export * from "./huggingface.js"
export * from "./io-intelligence.js"
export * from "./lite-llm.js"
export * from "./lm-studio.js"
export * from "./mistral.js"

View file

@ -0,0 +1,44 @@
import type { ModelInfo } from "../model.js"
export type IOIntelligenceModelId =
| "deepseek-ai/DeepSeek-R1-0528"
| "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8"
| "Intel/Qwen3-Coder-480B-A35B-Instruct-int4-mixed-ar"
| "openai/gpt-oss-120b"
export const ioIntelligenceDefaultModelId: IOIntelligenceModelId = "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8"
export const ioIntelligenceDefaultBaseUrl = "https://api.intelligence.io.solutions/api/v1"
export const IO_INTELLIGENCE_CACHE_DURATION = 1000 * 60 * 60 // 1 hour
export const ioIntelligenceModels = {
"deepseek-ai/DeepSeek-R1-0528": {
maxTokens: 8192,
contextWindow: 128000,
supportsImages: false,
supportsPromptCache: false,
description: "DeepSeek R1 reasoning model",
},
"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": {
maxTokens: 8192,
contextWindow: 430000,
supportsImages: true,
supportsPromptCache: false,
description: "Llama 4 Maverick 17B model",
},
"Intel/Qwen3-Coder-480B-A35B-Instruct-int4-mixed-ar": {
maxTokens: 8192,
contextWindow: 106000,
supportsImages: false,
supportsPromptCache: false,
description: "Qwen3 Coder 480B specialized for coding",
},
"openai/gpt-oss-120b": {
maxTokens: 8192,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
description: "OpenAI GPT-OSS 120B model",
},
} as const satisfies Record<string, ModelInfo>

View file

@ -32,6 +32,7 @@ import {
LiteLLMHandler,
ClaudeCodeHandler,
SambaNovaHandler,
IOIntelligenceHandler,
DoubaoHandler,
ZAiHandler,
FireworksHandler,
@ -137,6 +138,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
return new ZAiHandler(options)
case "fireworks":
return new FireworksHandler(options)
case "io-intelligence":
return new IOIntelligenceHandler(options)
default:
apiProvider satisfies "gemini-cli" | undefined
return new AnthropicHandler(options)

View file

@ -0,0 +1,303 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import { IOIntelligenceHandler } from "../io-intelligence"
import type { ApiHandlerOptions } from "../../../shared/api"
import { Anthropic } from "@anthropic-ai/sdk"
const mockCreate = vi.fn()
// Mock OpenAI
vi.mock("openai", () => ({
default: class MockOpenAI {
baseURL: string
apiKey: string
chat = {
completions: {
create: vi.fn(),
},
}
constructor(options: any) {
this.baseURL = options.baseURL
this.apiKey = options.apiKey
this.chat.completions.create = mockCreate
}
},
}))
// Mock the fetcher functions
vi.mock("../fetchers/io-intelligence", () => ({
getIOIntelligenceModels: vi.fn(),
getCachedIOIntelligenceModels: vi.fn(() => ({
"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": {
maxTokens: 8192,
contextWindow: 430000,
description: "Llama 4 Maverick 17B model",
supportsImages: true,
supportsPromptCache: false,
},
"deepseek-ai/DeepSeek-R1-0528": {
maxTokens: 8192,
contextWindow: 128000,
supportsImages: false,
supportsPromptCache: false,
description: "DeepSeek R1 reasoning model",
},
"Intel/Qwen3-Coder-480B-A35B-Instruct-int4-mixed-ar": {
maxTokens: 4096,
contextWindow: 106000,
supportsImages: false,
supportsPromptCache: false,
description: "Qwen3 Coder 480B specialized for coding",
},
"openai/gpt-oss-120b": {
maxTokens: 8192,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
description: "OpenAI GPT-OSS 120B model",
},
})),
}))
// Mock constants
vi.mock("../constants", () => ({
DEFAULT_HEADERS: { "User-Agent": "roo-cline" },
}))
// Mock transform functions
vi.mock("../../transform/openai-format", () => ({
convertToOpenAiMessages: vi.fn((messages) => messages),
}))
describe("IOIntelligenceHandler", () => {
let handler: IOIntelligenceHandler
let mockOptions: ApiHandlerOptions
beforeEach(() => {
vi.clearAllMocks()
mockOptions = {
ioIntelligenceApiKey: "test-api-key",
apiModelId: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8",
modelTemperature: 0.7,
includeMaxTokens: false,
modelMaxTokens: undefined,
} as ApiHandlerOptions
mockCreate.mockImplementation(async () => ({
[Symbol.asyncIterator]: async function* () {
yield {
choices: [
{
delta: { content: "Test response" },
index: 0,
},
],
usage: null,
}
yield {
choices: [
{
delta: {},
index: 0,
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
},
}
},
}))
handler = new IOIntelligenceHandler(mockOptions)
})
afterEach(() => {
vi.restoreAllMocks()
})
it("should create OpenAI client with correct configuration", () => {
const ioIntelligenceApiKey = "test-io-intelligence-api-key"
const handler = new IOIntelligenceHandler({ ioIntelligenceApiKey })
// Verify that the handler was created successfully
expect(handler).toBeInstanceOf(IOIntelligenceHandler)
expect(handler["client"]).toBeDefined()
// Verify the client has the expected properties
expect(handler["client"].baseURL).toBe("https://api.intelligence.io.solutions/api/v1")
expect(handler["client"].apiKey).toBe(ioIntelligenceApiKey)
})
it("should initialize with correct configuration", () => {
expect(handler).toBeInstanceOf(IOIntelligenceHandler)
expect(handler["client"]).toBeDefined()
expect(handler["options"]).toEqual({
...mockOptions,
apiKey: mockOptions.ioIntelligenceApiKey,
})
})
it("should throw error when API key is missing", () => {
const optionsWithoutKey = { ...mockOptions }
delete optionsWithoutKey.ioIntelligenceApiKey
expect(() => new IOIntelligenceHandler(optionsWithoutKey)).toThrow("IO Intelligence API key is required")
})
it("should handle streaming response correctly", async () => {
const mockStream = [
{
choices: [{ delta: { content: "Hello" } }],
usage: null,
},
{
choices: [{ delta: { content: " world" } }],
usage: null,
},
{
choices: [{ delta: {} }],
usage: { prompt_tokens: 10, completion_tokens: 5 },
},
]
mockCreate.mockResolvedValue({
[Symbol.asyncIterator]: async function* () {
for (const chunk of mockStream) {
yield chunk
}
},
})
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
const stream = handler.createMessage("System prompt", messages)
const results = []
for await (const chunk of stream) {
results.push(chunk)
}
expect(results).toHaveLength(3)
expect(results[0]).toEqual({ type: "text", text: "Hello" })
expect(results[1]).toEqual({ type: "text", text: " world" })
expect(results[2]).toEqual({
type: "usage",
inputTokens: 10,
outputTokens: 5,
})
})
it("completePrompt method should return text from IO Intelligence API", async () => {
const expectedResponse = "This is a test response from IO Intelligence"
mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] })
const result = await handler.completePrompt("test prompt")
expect(result).toBe(expectedResponse)
})
it("should handle errors in completePrompt", async () => {
const errorMessage = "IO Intelligence API error"
mockCreate.mockRejectedValueOnce(new Error(errorMessage))
await expect(handler.completePrompt("test prompt")).rejects.toThrow(
`IO Intelligence completion error: ${errorMessage}`,
)
})
it("createMessage should yield text content from stream", async () => {
const testContent = "This is test content from IO Intelligence stream"
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
next: vi
.fn()
.mockResolvedValueOnce({
done: false,
value: { choices: [{ delta: { content: testContent } }] },
})
.mockResolvedValueOnce({ done: true }),
}),
}
})
const stream = handler.createMessage("system prompt", [])
const firstChunk = await stream.next()
expect(firstChunk.done).toBe(false)
expect(firstChunk.value).toEqual({ type: "text", text: testContent })
})
it("createMessage should yield usage data from stream", async () => {
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
next: vi
.fn()
.mockResolvedValueOnce({
done: false,
value: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } },
})
.mockResolvedValueOnce({ done: true }),
}),
}
})
const stream = handler.createMessage("system prompt", [])
const firstChunk = await stream.next()
expect(firstChunk.done).toBe(false)
expect(firstChunk.value).toEqual({ type: "usage", inputTokens: 10, outputTokens: 20 })
})
it("should return model info from cache when available", () => {
const model = handler.getModel()
expect(model.id).toBe("meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8")
expect(model.info).toEqual({
maxTokens: 8192,
contextWindow: 430000,
description: "Llama 4 Maverick 17B model",
supportsImages: true,
supportsPromptCache: false,
})
})
it("should return fallback model info when not in cache", () => {
const handlerWithUnknownModel = new IOIntelligenceHandler({
...mockOptions,
apiModelId: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8",
})
const model = handlerWithUnknownModel.getModel()
expect(model.id).toBe("meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8")
expect(model.info).toEqual({
maxTokens: 8192,
contextWindow: 430000,
description: "Llama 4 Maverick 17B model",
supportsImages: true,
supportsPromptCache: false,
})
})
it("should use default model when no model is specified", () => {
const handlerWithoutModel = new IOIntelligenceHandler({
...mockOptions,
apiModelId: undefined,
})
const model = handlerWithoutModel.getModel()
expect(model.id).toBe("meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8")
})
it("should handle empty response from completePrompt", async () => {
mockCreate.mockResolvedValueOnce({
choices: [{ message: { content: null } }],
})
const result = await handler.completePrompt("Test prompt")
expect(result).toBe("")
})
it("should handle missing choices in completePrompt response", async () => {
mockCreate.mockResolvedValueOnce({
choices: [],
})
const result = await handler.completePrompt("Test prompt")
expect(result).toBe("")
})
})

View file

@ -24,6 +24,7 @@ vi.mock("../openrouter")
vi.mock("../requesty")
vi.mock("../glama")
vi.mock("../unbound")
vi.mock("../io-intelligence")
// Then imports
import type { Mock } from "vitest"
@ -33,15 +34,18 @@ import { getOpenRouterModels } from "../openrouter"
import { getRequestyModels } from "../requesty"
import { getGlamaModels } from "../glama"
import { getUnboundModels } from "../unbound"
import { getIOIntelligenceModels } from "../io-intelligence"
const mockGetLiteLLMModels = getLiteLLMModels as Mock<typeof getLiteLLMModels>
const mockGetOpenRouterModels = getOpenRouterModels as Mock<typeof getOpenRouterModels>
const mockGetRequestyModels = getRequestyModels as Mock<typeof getRequestyModels>
const mockGetGlamaModels = getGlamaModels as Mock<typeof getGlamaModels>
const mockGetUnboundModels = getUnboundModels as Mock<typeof getUnboundModels>
const mockGetIOIntelligenceModels = getIOIntelligenceModels as Mock<typeof getIOIntelligenceModels>
const DUMMY_REQUESTY_KEY = "requesty-key-for-testing"
const DUMMY_UNBOUND_KEY = "unbound-key-for-testing"
const DUMMY_IOINTELLIGENCE_KEY = "io-intelligence-key-for-testing"
describe("getModels with new GetModelsOptions", () => {
beforeEach(() => {
@ -137,6 +141,23 @@ describe("getModels with new GetModelsOptions", () => {
expect(result).toEqual(mockModels)
})
it("calls IOIntelligenceModels for IO-Intelligence provider", async () => {
const mockModels = {
"io-intelligence/model": {
maxTokens: 4096,
contextWindow: 8192,
supportsPromptCache: false,
description: "IO Intelligence Model",
},
}
mockGetIOIntelligenceModels.mockResolvedValue(mockModels)
const result = await getModels({ provider: "io-intelligence", apiKey: DUMMY_IOINTELLIGENCE_KEY })
expect(mockGetIOIntelligenceModels).toHaveBeenCalled()
expect(result).toEqual(mockModels)
})
it("handles errors and re-throws them", async () => {
const expectedError = new Error("LiteLLM connection failed")
mockGetLiteLLMModels.mockRejectedValue(expectedError)

View file

@ -0,0 +1,189 @@
import axios from "axios"
import { z } from "zod"
import type { ModelInfo } from "@roo-code/types"
import { IO_INTELLIGENCE_CACHE_DURATION } from "@roo-code/types"
import type { ModelRecord } from "../../../shared/api"
/**
* IO Intelligence Model Schema
*/
const ioIntelligenceModelSchema = z.object({
id: z.string(),
object: z.literal("model"),
created: z.number(),
owned_by: z.string(),
root: z.string().nullable().optional(),
parent: z.string().nullable().optional(),
max_model_len: z.number().nullable().optional(),
permission: z.array(
z.object({
id: z.string(),
object: z.literal("model_permission"),
created: z.number(),
allow_create_engine: z.boolean(),
allow_sampling: z.boolean(),
allow_logprobs: z.boolean(),
allow_search_indices: z.boolean(),
allow_view: z.boolean(),
allow_fine_tuning: z.boolean(),
organization: z.string(),
group: z.string().nullable(),
is_blocking: z.boolean(),
}),
),
})
export type IOIntelligenceModel = z.infer<typeof ioIntelligenceModelSchema>
/**
* IO Intelligence API Response Schema
*/
const ioIntelligenceApiResponseSchema = z.object({
object: z.literal("list"),
data: z.array(ioIntelligenceModelSchema),
})
type IOIntelligenceApiResponse = z.infer<typeof ioIntelligenceApiResponseSchema>
/**
* Cache entry for storing fetched models
*/
interface CacheEntry {
data: ModelRecord
timestamp: number
}
let cache: CacheEntry | null = null
/**
* Model context length mapping based on the documentation
* <mcreference link="https://docs.io.net/reference/get-started-with-io-intelligence-api" index="1">1</mcreference>
*/
const MODEL_CONTEXT_LENGTHS: Record<string, number> = {
"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": 430000,
"deepseek-ai/DeepSeek-R1-0528": 128000,
"Intel/Qwen3-Coder-480B-A35B-Instruct-int4-mixed-ar": 106000,
"openai/gpt-oss-120b": 131072,
}
/**
* Vision models that support images
*/
const VISION_MODELS = new Set([
"Qwen/Qwen2.5-VL-32B-Instruct",
"meta-llama/Llama-3.2-90B-Vision-Instruct",
"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8",
])
/**
* Parse an IO Intelligence model into ModelInfo format
*/
function parseIOIntelligenceModel(model: IOIntelligenceModel): ModelInfo {
const contextLength = MODEL_CONTEXT_LENGTHS[model.id] || 8192
// Cap maxTokens at 32k for very large context windows, or 20% of context length, whichever is smaller
const maxTokens = Math.min(contextLength, Math.ceil(contextLength * 0.2), 32768)
const supportsImages = VISION_MODELS.has(model.id)
return {
maxTokens,
contextWindow: contextLength,
supportsImages,
supportsPromptCache: false,
supportsComputerUse: false,
description: `${model.id} via IO Intelligence`,
}
}
/**
* Fetches available models from IO Intelligence
* <mcreference link="https://docs.io.net/reference/get-started-with-io-intelligence-api" index="1">1</mcreference>
*/
export async function getIOIntelligenceModels(apiKey?: string): Promise<ModelRecord> {
const now = Date.now()
// Check cache
if (cache && now - cache.timestamp < IO_INTELLIGENCE_CACHE_DURATION) {
return cache.data
}
const models: ModelRecord = {}
try {
const headers: Record<string, string> = {
"Content-Type": "application/json",
}
// Add authorization header if API key is provided
if (apiKey) {
headers.Authorization = `Bearer ${apiKey}`
} else {
console.error("IO Intelligence API key is required")
throw new Error("IO Intelligence API key is required")
}
const response = await axios.get<IOIntelligenceApiResponse>(
"https://api.intelligence.io.solutions/api/v1/models",
{
headers,
timeout: 10000, // 10 second timeout
},
)
const result = ioIntelligenceApiResponseSchema.safeParse(response.data)
if (!result.success) {
console.error("IO Intelligence models response validation failed:", result.error.format())
throw new Error("Invalid response format from IO Intelligence API")
}
for (const model of result.data.data) {
models[model.id] = parseIOIntelligenceModel(model)
}
// Update cache
cache = {
data: models,
timestamp: now,
}
return models
} catch (error) {
console.error("Error fetching IO Intelligence models:", error)
// Return cached data if available
if (cache) {
return cache.data
}
// Re-throw with more context
if (axios.isAxiosError(error)) {
if (error.response) {
throw new Error(
`Failed to fetch IO Intelligence models: ${error.response.status} ${error.response.statusText}`,
)
} else if (error.request) {
throw new Error(
"Failed to fetch IO Intelligence models: No response from server. Check your internet connection.",
)
}
}
throw new Error(
`Failed to fetch IO Intelligence models: ${error instanceof Error ? error.message : "Unknown error"}`,
)
}
}
/**
* Get cached models without making an API request
*/
export function getCachedIOIntelligenceModels(): ModelRecord | null {
return cache?.data || null
}
/**
* Clear the cache
*/
export function clearIOIntelligenceCache(): void {
cache = null
}

View file

@ -17,7 +17,7 @@ import { getLiteLLMModels } from "./litellm"
import { GetModelsOptions } from "../../../shared/api"
import { getOllamaModels } from "./ollama"
import { getLMStudioModels } from "./lmstudio"
import { getIOIntelligenceModels } from "./io-intelligence"
const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 })
async function writeModels(router: RouterName, data: ModelRecord) {
@ -78,6 +78,9 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
case "lmstudio":
models = await getLMStudioModels(options.baseUrl)
break
case "io-intelligence":
models = await getIOIntelligenceModels(options.apiKey)
break
default: {
// Ensures router is exhaustively checked if RouterName is a strict union
const exhaustiveCheck: never = provider

View file

@ -13,6 +13,7 @@ export { GlamaHandler } from "./glama"
export { GroqHandler } from "./groq"
export { HuggingFaceHandler } from "./huggingface"
export { HumanRelayHandler } from "./human-relay"
export { IOIntelligenceHandler } from "./io-intelligence"
export { LiteLLMHandler } from "./lite-llm"
export { LmStudioHandler } from "./lm-studio"
export { MistralHandler } from "./mistral"

View file

@ -0,0 +1,42 @@
import { ioIntelligenceDefaultModelId, ioIntelligenceModels, type IOIntelligenceModelId } from "@roo-code/types"
import type { ApiHandlerOptions } from "../../shared/api"
import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider"
export class IOIntelligenceHandler extends BaseOpenAiCompatibleProvider<IOIntelligenceModelId> {
constructor(options: ApiHandlerOptions) {
if (!options.ioIntelligenceApiKey) {
throw new Error("IO Intelligence API key is required")
}
super({
...options,
providerName: "IO Intelligence",
baseURL: "https://api.intelligence.io.solutions/api/v1",
defaultProviderModelId: ioIntelligenceDefaultModelId,
providerModels: ioIntelligenceModels,
defaultTemperature: 0.7,
apiKey: options.ioIntelligenceApiKey,
})
}
override getModel() {
const modelId = this.options.ioIntelligenceModelId || (ioIntelligenceDefaultModelId as IOIntelligenceModelId)
const modelInfo =
this.providerModels[modelId as IOIntelligenceModelId] ?? this.providerModels[ioIntelligenceDefaultModelId]
if (modelInfo) {
return { id: modelId as IOIntelligenceModelId, info: modelInfo }
}
// Return the requested model ID even if not found, with fallback info
return {
id: modelId as IOIntelligenceModelId,
info: {
maxTokens: 8192,
contextWindow: 128000,
supportsImages: false,
supportsPromptCache: false,
},
}
}
}

View file

@ -548,6 +548,15 @@ export const webviewMessageHandler = async (
{ key: "unbound", options: { provider: "unbound", apiKey: apiConfiguration.unboundApiKey } },
]
// Add IO Intelligence if API key is provided
const ioIntelligenceApiKey = apiConfiguration.ioIntelligenceApiKey
if (ioIntelligenceApiKey) {
modelFetchPromises.push({
key: "io-intelligence",
options: { provider: "io-intelligence", apiKey: ioIntelligenceApiKey },
})
}
// Don't fetch Ollama and LM Studio models by default anymore
// They have their own specific handlers: requestOllamaModels and requestLmStudioModels

View file

@ -87,6 +87,8 @@ export class ProfileValidator {
return profile.ollamaModelId
case "requesty":
return profile.requestyModelId
case "io-intelligence":
return profile.ioIntelligenceModelId
case "human-relay":
case "fake-ai":
default:

View file

@ -229,6 +229,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: {
"io-intelligence": { allowAll: false, models: ["test-model"] },
},
}
const profile: ProviderSettings = {
apiProvider: "io-intelligence" as any,
ioIntelligenceModelId: "test-model",
}
expect(ProfileValidator.isProfileAllowed(profile, allowList)).toBe(true)
})
it("should extract vsCodeLmModelSelector.id for vscode-lm provider", () => {
const allowList: OrganizationAllowList = {
allowAll: false,

View file

@ -18,7 +18,16 @@ export type ApiHandlerOptions = Omit<ProviderSettings, "apiProvider"> & {
// RouterName
const routerNames = ["openrouter", "requesty", "glama", "unbound", "litellm", "ollama", "lmstudio"] as const
const routerNames = [
"openrouter",
"requesty",
"glama",
"unbound",
"litellm",
"ollama",
"lmstudio",
"io-intelligence",
] as const
export type RouterName = (typeof routerNames)[number]
@ -121,3 +130,4 @@ export type GetModelsOptions =
| { provider: "litellm"; apiKey: string; baseUrl: string }
| { provider: "ollama"; baseUrl?: string }
| { provider: "lmstudio"; baseUrl?: string }
| { provider: "io-intelligence"; apiKey: string }

View file

@ -31,6 +31,7 @@ import {
internationalZAiDefaultModelId,
mainlandZAiDefaultModelId,
fireworksDefaultModelId,
ioIntelligenceDefaultModelId,
} from "@roo-code/types"
import { vscode } from "@src/utils/vscode"
@ -68,6 +69,7 @@ import {
Glama,
Groq,
HuggingFace,
IOIntelligence,
LMStudio,
LiteLLM,
Mistral,
@ -320,6 +322,7 @@ const ApiOptions = ({
: internationalZAiDefaultModelId,
},
fireworks: { field: "apiModelId", default: fireworksDefaultModelId },
"io-intelligence": { field: "ioIntelligenceModelId", default: ioIntelligenceDefaultModelId },
openai: { field: "openAiModelId" },
ollama: { field: "ollamaModelId" },
lmstudio: { field: "lmStudioModelId" },
@ -548,6 +551,15 @@ const ApiOptions = ({
<ZAi apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "io-intelligence" && (
<IOIntelligence
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
/>
)}
{selectedProvider === "human-relay" && (
<>
<div className="text-sm text-vscode-descriptionForeground">

View file

@ -28,7 +28,13 @@ import { ApiErrorMessage } from "./ApiErrorMessage"
type ModelIdKey = keyof Pick<
ProviderSettings,
"glamaModelId" | "openRouterModelId" | "unboundModelId" | "requestyModelId" | "openAiModelId" | "litellmModelId"
| "glamaModelId"
| "openRouterModelId"
| "unboundModelId"
| "requestyModelId"
| "openAiModelId"
| "litellmModelId"
| "ioIntelligenceModelId"
>
interface ModelPickerProps {

View file

@ -69,4 +69,5 @@ export const PROVIDERS = [
{ value: "sambanova", label: "SambaNova" },
{ value: "zai", label: "Z AI" },
{ value: "fireworks", label: "Fireworks AI" },
{ value: "io-intelligence", label: "IO Intelligence" },
].sort((a, b) => a.label.localeCompare(b.label))

View file

@ -0,0 +1,72 @@
import { useCallback } from "react"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import type { ProviderSettings, OrganizationAllowList } from "@roo-code/types"
import { ioIntelligenceDefaultModelId, ioIntelligenceModels } from "@roo-code/types"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { ModelPicker } from "../ModelPicker"
import { inputEventTransform } from "../transforms"
type IOIntelligenceProps = {
apiConfiguration: ProviderSettings
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
organizationAllowList: OrganizationAllowList
modelValidationError?: string
}
export const IOIntelligence = ({
apiConfiguration,
setApiConfigurationField,
organizationAllowList,
modelValidationError,
}: IOIntelligenceProps) => {
const { t } = useAppTranslation()
const { routerModels } = useExtensionState()
const handleInputChange = useCallback(
<K extends keyof ProviderSettings, E>(
field: K,
transform: (event: E) => ProviderSettings[K] = inputEventTransform,
) =>
(event: E | Event) => {
setApiConfigurationField(field, transform(event as E))
},
[setApiConfigurationField],
)
return (
<>
<VSCodeTextField
value={apiConfiguration?.ioIntelligenceApiKey || ""}
type="password"
onInput={handleInputChange("ioIntelligenceApiKey")}
placeholder={t("settings:providers.ioIntelligenceApiKeyPlaceholder")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.ioIntelligenceApiKey")}</label>
</VSCodeTextField>
<div className="text-sm text-vscode-descriptionForeground -mt-2">
{t("settings:providers.apiKeyStorageNotice")}
</div>
{!apiConfiguration?.ioIntelligenceApiKey && (
<VSCodeButtonLink href="https://ai.io.net/ai/api-keys" appearance="secondary">
{t("settings:providers.getIoIntelligenceApiKey")}
</VSCodeButtonLink>
)}
<ModelPicker
apiConfiguration={apiConfiguration}
defaultModelId={ioIntelligenceDefaultModelId}
models={routerModels?.["io-intelligence"] ?? ioIntelligenceModels}
modelIdKey="ioIntelligenceModelId"
serviceName="IO Intelligence"
serviceUrl="https://api.intelligence.io.solutions/api/v1/models"
setApiConfigurationField={setApiConfigurationField}
organizationAllowList={organizationAllowList}
errorMessage={modelValidationError}
/>
</>
)
}

View file

@ -9,6 +9,7 @@ export { Gemini } from "./Gemini"
export { Glama } from "./Glama"
export { Groq } from "./Groq"
export { HuggingFace } from "./HuggingFace"
export { IOIntelligence } from "./IOIntelligence"
export { LMStudio } from "./LMStudio"
export { Mistral } from "./Mistral"
export { Moonshot } from "./Moonshot"

View file

@ -58,6 +58,7 @@ describe("useSelectedModel", () => {
glama: {},
unbound: {},
litellm: {},
"io-intelligence": {},
},
isLoading: false,
isError: false,
@ -110,6 +111,7 @@ describe("useSelectedModel", () => {
glama: {},
unbound: {},
litellm: {},
"io-intelligence": {},
},
isLoading: false,
isError: false,
@ -164,6 +166,7 @@ describe("useSelectedModel", () => {
glama: {},
unbound: {},
litellm: {},
"io-intelligence": {},
},
isLoading: false,
isError: false,
@ -219,6 +222,7 @@ describe("useSelectedModel", () => {
glama: {},
unbound: {},
litellm: {},
"io-intelligence": {},
},
isLoading: false,
isError: false,
@ -263,6 +267,7 @@ describe("useSelectedModel", () => {
glama: {},
unbound: {},
litellm: {},
"io-intelligence": {},
},
isLoading: false,
isError: false,
@ -310,7 +315,7 @@ describe("useSelectedModel", () => {
it("should return loading state when open router model providers are loading", () => {
mockUseRouterModels.mockReturnValue({
data: { openrouter: {}, requesty: {}, glama: {}, unbound: {}, litellm: {} },
data: { openrouter: {}, requesty: {}, glama: {}, unbound: {}, litellm: {}, "io-intelligence": {} },
isLoading: false,
isError: false,
} as any)
@ -379,6 +384,7 @@ describe("useSelectedModel", () => {
glama: {},
unbound: {},
litellm: {},
"io-intelligence": {},
},
isLoading: false,
isError: false,
@ -417,6 +423,7 @@ describe("useSelectedModel", () => {
glama: {},
unbound: {},
litellm: {},
"io-intelligence": {},
},
isLoading: false,
isError: false,

View file

@ -46,6 +46,8 @@ import {
mainlandZAiModels,
fireworksModels,
fireworksDefaultModelId,
ioIntelligenceDefaultModelId,
ioIntelligenceModels,
} from "@roo-code/types"
import type { ModelRecord, RouterModels } from "@roo/api"
@ -277,6 +279,12 @@ function getSelectedModel({
const info = fireworksModels[id as keyof typeof fireworksModels]
return { id, info }
}
case "io-intelligence": {
const id = apiConfiguration.ioIntelligenceModelId ?? ioIntelligenceDefaultModelId
const info =
routerModels["io-intelligence"]?.[id] ?? ioIntelligenceModels[id as keyof typeof ioIntelligenceModels]
return { id, info }
}
// case "anthropic":
// case "human-relay":
// case "fake-ai":

View file

@ -263,6 +263,9 @@
"getChutesApiKey": "Obtenir clau API de Chutes",
"fireworksApiKey": "Clau API de Fireworks",
"getFireworksApiKey": "Obtenir clau API de Fireworks",
"ioIntelligenceApiKey": "Clau API d'IO Intelligence",
"ioIntelligenceApiKeyPlaceholder": "Introdueix la teva clau d'API de IO Intelligence",
"getIoIntelligenceApiKey": "Obtenir clau API d'IO Intelligence",
"deepSeekApiKey": "Clau API de DeepSeek",
"getDeepSeekApiKey": "Obtenir clau API de DeepSeek",
"doubaoApiKey": "Clau API de Doubao",

View file

@ -265,6 +265,9 @@
"getChutesApiKey": "Chutes API-Schlüssel erhalten",
"fireworksApiKey": "Fireworks API-Schlüssel",
"getFireworksApiKey": "Fireworks API-Schlüssel erhalten",
"ioIntelligenceApiKey": "IO Intelligence API-Schlüssel",
"ioIntelligenceApiKeyPlaceholder": "Gib deinen IO Intelligence API-Schlüssel ein",
"getIoIntelligenceApiKey": "IO Intelligence API-Schlüssel erhalten",
"deepSeekApiKey": "DeepSeek API-Schlüssel",
"getDeepSeekApiKey": "DeepSeek API-Schlüssel erhalten",
"moonshotApiKey": "Moonshot API-Schlüssel",

View file

@ -262,6 +262,9 @@
"getChutesApiKey": "Get Chutes API Key",
"fireworksApiKey": "Fireworks API Key",
"getFireworksApiKey": "Get Fireworks API Key",
"ioIntelligenceApiKey": "IO Intelligence API Key",
"ioIntelligenceApiKeyPlaceholder": "Enter your IO Intelligence API key",
"getIoIntelligenceApiKey": "Get IO Intelligence API Key",
"deepSeekApiKey": "DeepSeek API Key",
"getDeepSeekApiKey": "Get DeepSeek API Key",
"doubaoApiKey": "Doubao API Key",

View file

@ -263,6 +263,9 @@
"getChutesApiKey": "Obtener clave API de Chutes",
"fireworksApiKey": "Clave API de Fireworks",
"getFireworksApiKey": "Obtener clave API de Fireworks",
"ioIntelligenceApiKey": "Clave API de IO Intelligence",
"ioIntelligenceApiKeyPlaceholder": "Introduce tu clave de API de IO Intelligence",
"getIoIntelligenceApiKey": "Obtener clave API de IO Intelligence",
"deepSeekApiKey": "Clave API de DeepSeek",
"getDeepSeekApiKey": "Obtener clave API de DeepSeek",
"doubaoApiKey": "Clave API de Doubao",

View file

@ -263,6 +263,9 @@
"getChutesApiKey": "Obtenir la clé API Chutes",
"fireworksApiKey": "Clé API Fireworks",
"getFireworksApiKey": "Obtenir la clé API Fireworks",
"ioIntelligenceApiKey": "Clé API IO Intelligence",
"ioIntelligenceApiKeyPlaceholder": "Saisissez votre clé d'API IO Intelligence",
"getIoIntelligenceApiKey": "Obtenir la clé API IO Intelligence",
"deepSeekApiKey": "Clé API DeepSeek",
"getDeepSeekApiKey": "Obtenir la clé API DeepSeek",
"doubaoApiKey": "Clé API Doubao",

View file

@ -263,6 +263,9 @@
"getChutesApiKey": "Chutes API कुंजी प्राप्त करें",
"fireworksApiKey": "Fireworks API कुंजी",
"getFireworksApiKey": "Fireworks API कुंजी प्राप्त करें",
"ioIntelligenceApiKey": "IO Intelligence API कुंजी",
"ioIntelligenceApiKeyPlaceholder": "अपना आईओ इंटेलिजेंस एपीआई कुंजी दर्ज करें",
"getIoIntelligenceApiKey": "IO Intelligence API कुंजी प्राप्त करें",
"deepSeekApiKey": "DeepSeek API कुंजी",
"getDeepSeekApiKey": "DeepSeek API कुंजी प्राप्त करें",
"doubaoApiKey": "डौबाओ API कुंजी",

View file

@ -267,6 +267,9 @@
"getChutesApiKey": "Dapatkan Chutes API Key",
"fireworksApiKey": "Fireworks API Key",
"getFireworksApiKey": "Dapatkan Fireworks API Key",
"ioIntelligenceApiKey": "IO Intelligence API Key",
"ioIntelligenceApiKeyPlaceholder": "Masukkan kunci API IO Intelligence Anda",
"getIoIntelligenceApiKey": "Dapatkan IO Intelligence API Key",
"deepSeekApiKey": "DeepSeek API Key",
"getDeepSeekApiKey": "Dapatkan DeepSeek API Key",
"doubaoApiKey": "Kunci API Doubao",

View file

@ -263,6 +263,9 @@
"getChutesApiKey": "Ottieni chiave API Chutes",
"fireworksApiKey": "Chiave API Fireworks",
"getFireworksApiKey": "Ottieni chiave API Fireworks",
"ioIntelligenceApiKey": "Chiave API IO Intelligence",
"ioIntelligenceApiKeyPlaceholder": "Inserisci la tua chiave API IO Intelligence",
"getIoIntelligenceApiKey": "Ottieni chiave API IO Intelligence",
"deepSeekApiKey": "Chiave API DeepSeek",
"getDeepSeekApiKey": "Ottieni chiave API DeepSeek",
"doubaoApiKey": "Chiave API Doubao",

View file

@ -263,6 +263,9 @@
"getChutesApiKey": "Chutes APIキーを取得",
"fireworksApiKey": "Fireworks APIキー",
"getFireworksApiKey": "Fireworks APIキーを取得",
"ioIntelligenceApiKey": "IO Intelligence APIキー",
"ioIntelligenceApiKeyPlaceholder": "IO Intelligence APIキーを入力してください",
"getIoIntelligenceApiKey": "IO Intelligence APIキーを取得",
"deepSeekApiKey": "DeepSeek APIキー",
"getDeepSeekApiKey": "DeepSeek APIキーを取得",
"doubaoApiKey": "Doubao APIキー",

View file

@ -263,6 +263,9 @@
"getChutesApiKey": "Chutes API 키 받기",
"fireworksApiKey": "Fireworks API 키",
"getFireworksApiKey": "Fireworks API 키 받기",
"ioIntelligenceApiKey": "IO Intelligence API 키",
"ioIntelligenceApiKeyPlaceholder": "IO Intelligence API 키를 입력하세요",
"getIoIntelligenceApiKey": "IO Intelligence API 키 받기",
"deepSeekApiKey": "DeepSeek API 키",
"getDeepSeekApiKey": "DeepSeek API 키 받기",
"doubaoApiKey": "Doubao API 키",

View file

@ -263,6 +263,9 @@
"getChutesApiKey": "Chutes API-sleutel ophalen",
"fireworksApiKey": "Fireworks API-sleutel",
"getFireworksApiKey": "Fireworks API-sleutel ophalen",
"ioIntelligenceApiKey": "IO Intelligence API-sleutel",
"ioIntelligenceApiKeyPlaceholder": "Voer je IO Intelligence API-sleutel in",
"getIoIntelligenceApiKey": "IO Intelligence API-sleutel ophalen",
"deepSeekApiKey": "DeepSeek API-sleutel",
"getDeepSeekApiKey": "DeepSeek API-sleutel ophalen",
"doubaoApiKey": "Doubao API-sleutel",

View file

@ -263,6 +263,9 @@
"getChutesApiKey": "Uzyskaj klucz API Chutes",
"fireworksApiKey": "Klucz API Fireworks",
"getFireworksApiKey": "Uzyskaj klucz API Fireworks",
"ioIntelligenceApiKey": "Klucz API IO Intelligence",
"ioIntelligenceApiKeyPlaceholder": "Wprowadź swój klucz API IO Intelligence",
"getIoIntelligenceApiKey": "Uzyskaj klucz API IO Intelligence",
"deepSeekApiKey": "Klucz API DeepSeek",
"getDeepSeekApiKey": "Uzyskaj klucz API DeepSeek",
"doubaoApiKey": "Klucz API Doubao",

View file

@ -263,6 +263,9 @@
"getChutesApiKey": "Obter chave de API Chutes",
"fireworksApiKey": "Chave de API Fireworks",
"getFireworksApiKey": "Obter chave de API Fireworks",
"ioIntelligenceApiKey": "Chave de API IO Intelligence",
"ioIntelligenceApiKeyPlaceholder": "Insira sua chave de API da IO Intelligence",
"getIoIntelligenceApiKey": "Obter chave de API IO Intelligence",
"deepSeekApiKey": "Chave de API DeepSeek",
"getDeepSeekApiKey": "Obter chave de API DeepSeek",
"doubaoApiKey": "Chave de API Doubao",

View file

@ -263,6 +263,9 @@
"getChutesApiKey": "Получить Chutes API-ключ",
"fireworksApiKey": "Fireworks API-ключ",
"getFireworksApiKey": "Получить Fireworks API-ключ",
"ioIntelligenceApiKey": "IO Intelligence API-ключ",
"ioIntelligenceApiKeyPlaceholder": "Введите свой ключ API IO Intelligence",
"getIoIntelligenceApiKey": "Получить IO Intelligence API-ключ",
"deepSeekApiKey": "DeepSeek API-ключ",
"getDeepSeekApiKey": "Получить DeepSeek API-ключ",
"doubaoApiKey": "Doubao API-ключ",

View file

@ -263,6 +263,9 @@
"getChutesApiKey": "Chutes API Anahtarı Al",
"fireworksApiKey": "Fireworks API Anahtarı",
"getFireworksApiKey": "Fireworks API Anahtarı Al",
"ioIntelligenceApiKey": "IO Intelligence API Anahtarı",
"ioIntelligenceApiKeyPlaceholder": "IO Intelligence API anahtarınızı girin",
"getIoIntelligenceApiKey": "IO Intelligence API Anahtarı Al",
"deepSeekApiKey": "DeepSeek API Anahtarı",
"getDeepSeekApiKey": "DeepSeek API Anahtarı Al",
"doubaoApiKey": "Doubao API Anahtarı",

View file

@ -263,6 +263,9 @@
"getChutesApiKey": "Lấy khóa API Chutes",
"fireworksApiKey": "Khóa API Fireworks",
"getFireworksApiKey": "Lấy khóa API Fireworks",
"ioIntelligenceApiKey": "Khóa API IO Intelligence",
"ioIntelligenceApiKeyPlaceholder": "Nhập khóa API IO Intelligence của bạn",
"getIoIntelligenceApiKey": "Lấy khóa API IO Intelligence",
"deepSeekApiKey": "Khóa API DeepSeek",
"getDeepSeekApiKey": "Lấy khóa API DeepSeek",
"doubaoApiKey": "Khóa API Doubao",

View file

@ -263,6 +263,9 @@
"getChutesApiKey": "获取 Chutes API 密钥",
"fireworksApiKey": "Fireworks API 密钥",
"getFireworksApiKey": "获取 Fireworks API 密钥",
"ioIntelligenceApiKey": "IO Intelligence API 密钥",
"ioIntelligenceApiKeyPlaceholder": "输入您的 IO Intelligence API 密钥",
"getIoIntelligenceApiKey": "获取 IO Intelligence API 密钥",
"deepSeekApiKey": "DeepSeek API 密钥",
"getDeepSeekApiKey": "获取 DeepSeek API 密钥",
"doubaoApiKey": "豆包 API 密钥",

View file

@ -263,6 +263,9 @@
"getChutesApiKey": "取得 Chutes API 金鑰",
"fireworksApiKey": "Fireworks API 金鑰",
"getFireworksApiKey": "取得 Fireworks API 金鑰",
"ioIntelligenceApiKey": "IO Intelligence API 金鑰",
"ioIntelligenceApiKeyPlaceholder": "輸入您的 IO Intelligence API 金鑰",
"getIoIntelligenceApiKey": "取得 IO Intelligence API 金鑰",
"deepSeekApiKey": "DeepSeek API 金鑰",
"getDeepSeekApiKey": "取得 DeepSeek API 金鑰",
"doubaoApiKey": "豆包 API 金鑰",

View file

@ -38,6 +38,7 @@ describe("Model Validation Functions", () => {
litellm: {},
ollama: {},
lmstudio: {},
"io-intelligence": {},
}
const allowAllOrganization: OrganizationAllowList = {
@ -185,5 +186,25 @@ describe("Model Validation Functions", () => {
)
expect(result).toBeUndefined() // Should exclude model-specific org errors
})
it("returns undefined for valid IO Intelligence model", () => {
const config: ProviderSettings = {
apiProvider: "io-intelligence",
glamaModelId: "valid-model",
}
const result = getModelValidationError(config, mockRouterModels, allowAllOrganization)
expect(result).toBeUndefined()
})
it("returns error for invalid IO Intelligence model", () => {
const config: ProviderSettings = {
apiProvider: "io-intelligence",
glamaModelId: "invalid-model",
}
const result = getModelValidationError(config, mockRouterModels, allowAllOrganization)
expect(result).toBeUndefined()
})
})
})

View file

@ -120,6 +120,11 @@ function validateModelsAndKeysProvided(apiConfiguration: ProviderSettings): stri
return i18next.t("settings:validation.apiKey")
}
break
case "io-intelligence":
if (!apiConfiguration.ioIntelligenceApiKey) {
return i18next.t("settings:validation.apiKey")
}
break
}
return undefined
@ -186,6 +191,8 @@ function getModelIdForProvider(apiConfiguration: ProviderSettings, provider: str
return apiConfiguration.vsCodeLmModelSelector?.id
case "huggingface":
return apiConfiguration.huggingFaceModelId
case "io-intelligence":
return apiConfiguration.ioIntelligenceModelId
default:
return apiConfiguration.apiModelId
}
@ -256,6 +263,9 @@ export function validateModelId(apiConfiguration: ProviderSettings, routerModels
case "litellm":
modelId = apiConfiguration.litellmModelId
break
case "io-intelligence":
modelId = apiConfiguration.ioIntelligenceModelId
break
}
if (!modelId) {