diff --git a/apps/kilocode-docs/docs/providers/aistupidlevel.md b/apps/kilocode-docs/docs/providers/aistupidlevel.md new file mode 100644 index 0000000000..39f35872cb --- /dev/null +++ b/apps/kilocode-docs/docs/providers/aistupidlevel.md @@ -0,0 +1,89 @@ +--- +sidebar_label: AIStupidLevel +--- + +# Using AIStupidLevel With Kilo Code + +AIStupidLevel is an intelligent AI router that continuously benchmarks 25+ AI models across multiple providers and automatically routes your requests to the best-performing model based on real-time performance data. + +**Website:** [https://aistupidlevel.info](https://aistupidlevel.info) + +## What is AIStupidLevel? + +AIStupidLevel is a smart AI router that automatically selects the best-performing model for your requests based on continuous benchmarking. Learn more at [aistupidlevel.info](https://aistupidlevel.info). + +## Getting an API Key + +1. **Sign Up:** Go to [https://aistupidlevel.info](https://aistupidlevel.info) and create an account +2. **Navigate to Router:** Click on the "Router" section in the dashboard +3. **Add Provider Keys:** Add your API keys for the providers you want to use (OpenAI, Anthropic, Google, xAI, etc.) +4. **Generate Router Key:** Create a router API key that Kilo Code will use +5. **Copy the Key:** Copy your AIStupidLevel router API key + +## Available Routing Strategies + +AIStupidLevel offers different "auto" models that optimize for specific use cases: + +| Model | Description | Best For | +|-------|-------------|----------| +| `auto` | Best overall performance across all metrics | General-purpose tasks | +| `auto-coding` | Optimized for code generation and quality | Software development, debugging | +| `auto-reasoning` | Best for complex reasoning and problem-solving | Deep analysis, mathematical problems | +| `auto-creative` | Optimized for creative writing quality | Content creation, storytelling | +| `auto-cheapest` | Most cost-effective option | High-volume, budget-conscious tasks | +| `auto-fastest` | Fastest response time | Real-time applications, quick queries | + +## Configuration in Kilo Code + +**Important:** Before using AIStupidLevel, you must add your own provider API keys (OpenAI, Anthropic, etc.) to your [AIStupidLevel dashboard](https://aistupidlevel.info/router). The router uses your keys to access the underlying models. + +1. **Open Kilo Code Settings:** Click the gear icon () in the Kilo Code panel. +2. **Select Provider:** Choose "AIStupidLevel" from the "API Provider" dropdown. +3. **Enter API Key:** Paste your AIStupidLevel router API key into the "AIStupidLevel API Key" field. +4. **Select Model:** Choose your desired routing strategy from the "Model" dropdown (e.g., `auto-coding`, `auto-reasoning`, etc.). + +## How It Works + +When you make a request through Kilo Code: + +1. **AIStupidLevel analyzes** current model performance from continuous benchmarks +2. **Selects the optimal model** based on your chosen routing strategy +3. **Routes your request** using your configured provider API keys +4. **Returns the response** with metadata about which model was selected + +The router automatically: +- Avoids models experiencing performance degradation +- Routes to cheaper models when performance is comparable +- Provides transparent routing decisions in response headers + +## Key Features + +For detailed information about AIStupidLevel's features, benchmarking methodology, and performance tracking, visit [aistupidlevel.info](https://aistupidlevel.info). + +## Response Headers + +AIStupidLevel includes custom headers in responses to show routing decisions: + +``` +X-AISM-Provider: anthropic +X-AISM-Model: claude-sonnet-4-20250514 +X-AISM-Reasoning: Selected claude-sonnet-4-20250514 from anthropic for best coding capabilities (score: 42.3). Ranked #1 of 12 available models. Last updated 2h ago. +``` + +## Pricing + +AIStupidLevel charges only for the underlying model usage (at cost) plus a small routing fee. You can monitor costs in real-time through the dashboard at [https://aistupidlevel.info/router](https://aistupidlevel.info/router). + +## Tips and Notes + +- **Model Selection:** The router automatically selects the best model based on real-time benchmarks - you don't need to manually switch models +- **Performance Monitoring:** Check the [AIStupidLevel dashboard](https://aistupidlevel.info) to see live performance rankings and routing decisions +- **Cost Tracking:** The dashboard shows your cost savings compared to always using premium models + +## Learn More + +- **Website:** [https://aistupidlevel.info](https://aistupidlevel.info) +- **Router Dashboard:** [https://aistupidlevel.info/router](https://aistupidlevel.info/router) +- **Live Benchmarks:** [https://aistupidlevel.info](https://aistupidlevel.info) +- **Community:** [r/AIStupidLevel](https://www.reddit.com/r/AIStupidlevel) +- **Twitter/X:** [@AIStupidlevel](https://x.com/AIStupidlevel) diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 5ef923a0f6..8ec634124d 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -59,6 +59,7 @@ export const dynamicProviders = [ "requesty", "unbound", "glama", + "aistupidlevel", ] as const export type DynamicProvider = (typeof dynamicProviders)[number] @@ -490,6 +491,11 @@ const vercelAiGatewaySchema = baseProviderSettingsSchema.extend({ vercelAiGatewayModelId: z.string().optional(), }) +const aiStupidLevelSchema = baseProviderSettingsSchema.extend({ + aiStupidLevelApiKey: z.string().optional(), + aiStupidLevelModelId: z.string().optional(), +}) + const defaultSchema = z.object({ apiProvider: z.undefined(), }) @@ -537,6 +543,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv qwenCodeSchema.merge(z.object({ apiProvider: z.literal("qwen-code") })), rooSchema.merge(z.object({ apiProvider: z.literal("roo") })), vercelAiGatewaySchema.merge(z.object({ apiProvider: z.literal("vercel-ai-gateway") })), + aiStupidLevelSchema.merge(z.object({ apiProvider: z.literal("aistupidlevel") })), defaultSchema, ]) @@ -583,6 +590,7 @@ export const providerSettingsSchema = z.object({ ...qwenCodeSchema.shape, ...rooSchema.shape, ...vercelAiGatewaySchema.shape, + ...aiStupidLevelSchema.shape, ...codebaseIndexProviderSchema.shape, ...ovhcloudSchema.shape, // kilocode_change }) @@ -620,6 +628,7 @@ export const modelIdKeys = [ "deepInfraModelId", "kilocodeModel", "ovhCloudAiEndpointsModelId", // kilocode_change + "aiStupidLevelModelId", ] as const satisfies readonly (keyof ProviderSettings)[] export type ModelIdKey = (typeof modelIdKeys)[number] @@ -676,6 +685,7 @@ export const modelIdKeysByProvider: Record = { kilocode: "kilocodeModel", "virtual-quota-fallback": "apiModelId", ovhcloud: "ovhCloudAiEndpointsModelId", // kilocode_change + aistupidlevel: "aiStupidLevelModelId", } /** @@ -816,6 +826,7 @@ export const MODELS_BY_PROVIDER: Record< // kilocode_change end deepinfra: { id: "deepinfra", label: "DeepInfra", models: [] }, "vercel-ai-gateway": { id: "vercel-ai-gateway", label: "Vercel AI Gateway", models: [] }, + aistupidlevel: { id: "aistupidlevel", label: "AIStupidLevel", models: [] }, // Local providers; models discovered from localhost endpoints. lmstudio: { id: "lmstudio", label: "LM Studio", models: [] }, diff --git a/packages/types/src/providers/aistupidlevel.ts b/packages/types/src/providers/aistupidlevel.ts new file mode 100644 index 0000000000..24d7be20fc --- /dev/null +++ b/packages/types/src/providers/aistupidlevel.ts @@ -0,0 +1,27 @@ +import type { ModelInfo } from "../model.js" + +export const aiStupidLevelDefaultModelId = "auto-coding" + +export const aiStupidLevelDefaultModelInfo: ModelInfo = { + maxTokens: 8192, + contextWindow: 200000, + supportsImages: true, + supportsComputerUse: false, + supportsPromptCache: false, + // Pricing varies by underlying model selected, these are approximate averages + inputPrice: 0.5, // ~$0.50 per million input tokens (average across routed models) + outputPrice: 1.5, // ~$1.50 per million output tokens (average across routed models) + description: "Optimized for code generation and quality", +} + +export const AISTUPIDLEVEL_DEFAULT_TEMPERATURE = 0.7 + +// Default fallback models when API fetch fails +export const aiStupidLevelFallbackModels = [ + "auto", + "auto-coding", + "auto-reasoning", + "auto-creative", + "auto-cheapest", + "auto-fastest", +] diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index 087cfb4e6e..eb4f1e3966 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -35,3 +35,4 @@ export * from "./xai.js" export * from "./vercel-ai-gateway.js" export * from "./zai.js" export * from "./deepinfra.js" +export * from "./aistupidlevel.js" // kilocode_change diff --git a/src/api/index.ts b/src/api/index.ts index 029bb50e1c..4bb11fccc0 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -47,6 +47,7 @@ import { VercelAiGatewayHandler, DeepInfraHandler, OVHcloudAIEndpointsHandler, // kilocode_change + AIStupidLevelHandler, // kilocode_change } from "./providers" // kilocode_change start import { KilocodeOpenrouterHandler } from "./providers/kilocode-openrouter" @@ -206,6 +207,10 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { case "ovhcloud": return new OVHcloudAIEndpointsHandler(options) // kilocode_change end + // kilocode_change start + case "aistupidlevel": + return new AIStupidLevelHandler(options) + // kilocode_change end default: apiProvider satisfies "gemini-cli" | undefined return new AnthropicHandler(options) diff --git a/src/api/providers/__tests__/aistupidlevel.spec.ts b/src/api/providers/__tests__/aistupidlevel.spec.ts new file mode 100644 index 0000000000..0c4ca552a4 --- /dev/null +++ b/src/api/providers/__tests__/aistupidlevel.spec.ts @@ -0,0 +1,303 @@ +// kilocode_change: file added +// npx vitest run api/providers/__tests__/aistupidlevel.spec.ts + +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" + +import { type AIStupidLevelModelId, aiStupidLevelDefaultModelId, aiStupidLevelModels } from "@roo-code/types" + +import { AIStupidLevelHandler } from "../aistupidlevel" + +// Create mock functions +const mockCreate = vi.fn() + +// Mock OpenAI module +vi.mock("openai", () => ({ + default: vi.fn(() => ({ + chat: { + completions: { + create: mockCreate, + }, + }, + })), +})) + +describe("AIStupidLevelHandler", () => { + let handler: AIStupidLevelHandler + + beforeEach(() => { + vi.clearAllMocks() + // Set up default mock implementation + 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 AIStupidLevelHandler({ aiStupidLevelApiKey: "test-key" }) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it("should use the correct AIStupidLevel base URL", () => { + new AIStupidLevelHandler({ aiStupidLevelApiKey: "test-aistupidlevel-api-key" }) + expect(OpenAI).toHaveBeenCalledWith( + expect.objectContaining({ baseURL: "https://api.aistupidlevel.info/v1" }), + ) + }) + + it("should use the provided API key", () => { + const aiStupidLevelApiKey = "test-aistupidlevel-api-key" + new AIStupidLevelHandler({ aiStupidLevelApiKey }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: aiStupidLevelApiKey })) + }) + + it("should throw error when API key is not provided", () => { + expect(() => new AIStupidLevelHandler({})).toThrow("API key is required") + }) + + it("should return default model when no model is specified", () => { + const model = handler.getModel() + expect(model.id).toBe(aiStupidLevelDefaultModelId) + expect(model.info).toEqual(expect.objectContaining(aiStupidLevelModels[aiStupidLevelDefaultModelId])) + }) + + it("should return specified model when valid model is provided", () => { + const testModelId: AIStupidLevelModelId = "auto-reasoning" + const handlerWithModel = new AIStupidLevelHandler({ + aiStupidLevelModelId: testModelId, + aiStupidLevelApiKey: "test-aistupidlevel-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual(expect.objectContaining(aiStupidLevelModels[testModelId])) + }) + + it("should return auto-reasoning model with correct configuration", () => { + const testModelId: AIStupidLevelModelId = "auto-reasoning" + const handlerWithModel = new AIStupidLevelHandler({ + aiStupidLevelModelId: testModelId, + aiStupidLevelApiKey: "test-aistupidlevel-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual( + expect.objectContaining({ + maxTokens: 8000, + contextWindow: 200000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }), + ) + }) + + it("completePrompt method should return text from AIStupidLevel API", async () => { + const expectedResponse = "This is a test response from AIStupidLevel" + 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 = "AIStupidLevel API error" + mockCreate.mockRejectedValueOnce(new Error(errorMessage)) + await expect(handler.completePrompt("test prompt")).rejects.toThrow( + `AIStupidLevel completion error: ${errorMessage}`, + ) + }) + + it("createMessage should yield text content from stream", async () => { + const testContent = "This is test content from AIStupidLevel 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("createMessage should pass correct parameters to AIStupidLevel client", async () => { + const modelId: AIStupidLevelModelId = "auto-reasoning" + const modelInfo = aiStupidLevelModels[modelId] + const handlerWithModel = new AIStupidLevelHandler({ + aiStupidLevelModelId: modelId, + aiStupidLevelApiKey: "test-aistupidlevel-api-key", + }) + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + } + }) + + const systemPrompt = "Test system prompt for AIStupidLevel" + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "Test message for AIStupidLevel" }, + ] + + const messageGenerator = handlerWithModel.createMessage(systemPrompt, messages) + await messageGenerator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: modelId, + max_tokens: modelInfo.maxTokens, + temperature: 0.5, + messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]), + stream: true, + stream_options: { include_usage: true }, + }), + undefined, + ) + }) + + it("should handle empty response in 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", async () => { + mockCreate.mockResolvedValueOnce({ choices: [] }) + const result = await handler.completePrompt("test prompt") + expect(result).toBe("") + }) + + it("createMessage should handle stream with multiple chunks", async () => { + mockCreate.mockImplementationOnce(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [ + { + delta: { content: "Hello" }, + index: 0, + }, + ], + usage: null, + } + yield { + choices: [ + { + delta: { content: " world" }, + index: 0, + }, + ], + usage: null, + } + yield { + choices: [ + { + delta: {}, + index: 0, + }, + ], + usage: { + prompt_tokens: 5, + completion_tokens: 10, + total_tokens: 15, + }, + } + }, + })) + + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }] + + const stream = handler.createMessage(systemPrompt, messages) + const chunks = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(chunks).toEqual([ + { type: "text", text: "Hello" }, + { type: "text", text: " world" }, + { type: "usage", inputTokens: 5, outputTokens: 10 }, + ]) + }) + + it("should handle all routing strategy models", () => { + const strategies: AIStupidLevelModelId[] = [ + "auto", + "auto-coding", + "auto-reasoning", + "auto-creative", + "auto-cheapest", + "auto-fastest", + ] + + strategies.forEach((strategy) => { + const handlerWithStrategy = new AIStupidLevelHandler({ + aiStupidLevelModelId: strategy, + aiStupidLevelApiKey: "test-key", + }) + const model = handlerWithStrategy.getModel() + expect(model.id).toBe(strategy) + expect(model.info).toEqual(expect.objectContaining(aiStupidLevelModels[strategy])) + }) + }) +}) diff --git a/src/api/providers/aistupidlevel.ts b/src/api/providers/aistupidlevel.ts new file mode 100644 index 0000000000..ed8bbac4ed --- /dev/null +++ b/src/api/providers/aistupidlevel.ts @@ -0,0 +1,99 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" + +import { aiStupidLevelDefaultModelId, aiStupidLevelDefaultModelInfo } from "@roo-code/types" + +import { ApiHandlerOptions } from "../../shared/api" + +import { ApiStream } from "../transform/stream" +import { convertToOpenAiMessages } from "../transform/openai-format" + +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import { RouterProvider } from "./router-provider" + +const AISTUPIDLEVEL_DEFAULT_TEMPERATURE = 0.7 + +export class AIStupidLevelHandler extends RouterProvider implements SingleCompletionHandler { + constructor(options: ApiHandlerOptions) { + super({ + options, + name: "aistupidlevel", + baseURL: "https://api.aistupidlevel.info/v1", + apiKey: options.aiStupidLevelApiKey, + modelId: options.aiStupidLevelModelId, + defaultModelId: aiStupidLevelDefaultModelId, + defaultModelInfo: aiStupidLevelDefaultModelInfo, + }) + } + + 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, + temperature: this.supportsTemperature(modelId) + ? (this.options.modelTemperature ?? AISTUPIDLEVEL_DEFAULT_TEMPERATURE) + : undefined, + max_completion_tokens: info.maxTokens, + stream: true, + } + + const completion = await this.client.chat.completions.create(body) + + for await (const chunk of completion) { + const delta = chunk.choices[0]?.delta + if (delta?.content) { + yield { + type: "text", + text: delta.content, + } + } + + if (chunk.usage) { + yield { + type: "usage", + inputTokens: chunk.usage.prompt_tokens || 0, + outputTokens: chunk.usage.completion_tokens || 0, + // AIStupidLevel handles cost tracking in their dashboard + // Don't override totalCost - let it be calculated from pricing + } + } + } + } + + async completePrompt(prompt: string): Promise { + const { id: modelId, info } = await this.fetchModel() + + try { + const requestOptions: OpenAI.Chat.ChatCompletionCreateParams = { + model: modelId, + messages: [{ role: "user", content: prompt }], + stream: false, + } + + if (this.supportsTemperature(modelId)) { + requestOptions.temperature = this.options.modelTemperature ?? AISTUPIDLEVEL_DEFAULT_TEMPERATURE + } + + requestOptions.max_completion_tokens = info.maxTokens + + const response = await this.client.chat.completions.create(requestOptions) + return response.choices[0]?.message.content || "" + } catch (error) { + if (error instanceof Error) { + throw new Error(`AIStupidLevel completion error: ${error.message}`) + } + throw error + } + } +} diff --git a/src/api/providers/fetchers/aistupidlevel.ts b/src/api/providers/fetchers/aistupidlevel.ts new file mode 100644 index 0000000000..cff403f660 --- /dev/null +++ b/src/api/providers/fetchers/aistupidlevel.ts @@ -0,0 +1,109 @@ +import axios from "axios" +import { z } from "zod" + +import { aiStupidLevelFallbackModels, type ModelInfo } from "@roo-code/types" + +import { parseApiPrice } from "../../../shared/cost" + +/** + * AIStupidLevelModel + */ + +const aiStupidLevelModelSchema = z.object({ + id: z.string(), + name: z.string().optional(), + description: z.string().optional(), + context_window: z.number().optional(), + max_tokens: z.number().optional(), + pricing: z + .object({ + input: z.string().optional(), + output: z.string().optional(), + }) + .optional(), +}) + +export type AIStupidLevelModel = z.infer + +/** + * AIStupidLevelModelsResponse + */ + +const aiStupidLevelModelsResponseSchema = z.object({ + data: z.array(aiStupidLevelModelSchema), +}) + +type AIStupidLevelModelsResponse = z.infer + +/** + * getAIStupidLevelModels + */ + +export async function getAIStupidLevelModels(apiKey: string): Promise> { + const models: Record = {} + const baseURL = "https://api.aistupidlevel.info/v1" + + // Create default models from fallback list + const defaultModels = aiStupidLevelFallbackModels.map((id) => ({ + id, + context_window: 200000, + max_tokens: 8192, + })) + + try { + const response = await axios.get(`${baseURL}/models`, { + headers: { + Authorization: `Bearer ${apiKey}`, + }, + timeout: 10000, + }) + + const result = aiStupidLevelModelsResponseSchema.safeParse(response.data) + const data = result.success ? result.data.data : response.data.data + + if (!result.success) { + console.error("AIStupidLevel models response is invalid", result.error.format()) + } + + if (data && data.length > 0) { + for (const model of data) { + models[model.id] = parseAIStupidLevelModel(model) + } + } else { + // Use default models if API doesn't return any + for (const model of defaultModels) { + models[model.id] = parseAIStupidLevelModel(model) + } + } + } catch (error) { + console.error( + `Error fetching AIStupidLevel models, using defaults: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + + // Use default models on error + for (const model of defaultModels) { + models[model.id] = parseAIStupidLevelModel(model) + } + } + + return models +} + +/** + * parseAIStupidLevelModel + */ + +export const parseAIStupidLevelModel = (model: AIStupidLevelModel): ModelInfo => { + const modelInfo: ModelInfo = { + maxTokens: model.max_tokens || 8192, + contextWindow: model.context_window || 200000, + supportsImages: true, // AIStupidLevel routes to models that support images + supportsComputerUse: false, + supportsPromptCache: false, + inputPrice: model.pricing?.input ? parseApiPrice(model.pricing.input) : 0.5, + outputPrice: model.pricing?.output ? parseApiPrice(model.pricing.output) : 1.5, + description: model.description || model.name, + } + + return modelInfo +} diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index 5ec5ea14f3..fe9ef006fc 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -31,6 +31,7 @@ import { getGeminiModels } from "./gemini" import { getDeepInfraModels } from "./deepinfra" import { getHuggingFaceModels } from "./huggingface" +import { getAIStupidLevelModels } from "./aistupidlevel" // kilocode_change const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 }) @@ -137,6 +138,11 @@ export const getModels = async (options: GetModelsOptions): Promise models = await getOvhCloudAiEndpointsModels() break // kilocode_change end + // kilocode_change start + case "aistupidlevel": + models = await getAIStupidLevelModels(options.apiKey) + break + // kilocode_change end default: { // Ensures router is exhaustively checked if RouterName is a strict union. const exhaustiveCheck: never = provider diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index 1e87b24e4e..d668530e0d 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -40,3 +40,4 @@ export { RooHandler } from "./roo" export { FeatherlessHandler } from "./featherless" export { VercelAiGatewayHandler } from "./vercel-ai-gateway" export { DeepInfraHandler } from "./deepinfra" +export { AIStupidLevelHandler } from "./aistupidlevel" // kilocode_change diff --git a/src/shared/api.ts b/src/shared/api.ts index 4b8b17d392..ba2250b714 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -169,6 +169,7 @@ const dynamicProviderExtras = { lmstudio: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type ovhcloud: {} as { apiKey?: string }, // kilocode_change chutes: {} as { apiKey?: string }, // kilocode_change + aistupidlevel: {} as { apiKey?: string }, } as const satisfies Record // Build the dynamic options union from the map, intersected with CommonFetchParams diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index a31d3c9663..580f810dc6 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -40,6 +40,7 @@ import { vercelAiGatewayDefaultModelId, deepInfraDefaultModelId, ovhCloudAiEndpointsDefaultModelId, // kilocode_change + aiStupidLevelDefaultModelId, nativeFunctionCallingProviders, // kilocode_change: Added import for native function calling providers } from "@roo-code/types" @@ -107,6 +108,7 @@ import { VercelAiGateway, DeepInfra, OvhCloudAiEndpoints, // kilocode_change + AIStupidLevel, } from "./providers" import { MODELS_BY_PROVIDER, PROVIDERS } from "./constants" @@ -410,6 +412,7 @@ const ApiOptions = ({ kilocode: { field: "kilocodeModel", default: kilocodeDefaultModel }, "gemini-cli": { field: "apiModelId", default: geminiCliDefaultModelId }, // kilocode_change end + aistupidlevel: { field: "aiStupidLevelModelId", default: aiStupidLevelDefaultModelId }, } const config = PROVIDER_MODEL_CONFIG[value] @@ -745,6 +748,16 @@ const ApiOptions = ({ /> )} + {selectedProvider === "aistupidlevel" && ( + + )} + {selectedProvider === "human-relay" && ( <>
diff --git a/webview-ui/src/components/settings/constants.ts b/webview-ui/src/components/settings/constants.ts index 961a3cd474..8459bb0932 100644 --- a/webview-ui/src/components/settings/constants.ts +++ b/webview-ui/src/components/settings/constants.ts @@ -97,6 +97,7 @@ export const PROVIDERS = [ // { value: "roo", label: "Roo Code Cloud" }, // kilocode_change end { value: "vercel-ai-gateway", label: "Vercel AI Gateway" }, + { value: "aistupidlevel", label: "AIStupidLevel" }, // kilocode_change ].sort((a, b) => a.label.localeCompare(b.label)) PROVIDERS.unshift({ value: "kilocode", label: "Kilo Code" }) // kilocode_change diff --git a/webview-ui/src/components/settings/providers/AIStupidLevel.tsx b/webview-ui/src/components/settings/providers/AIStupidLevel.tsx new file mode 100644 index 0000000000..440d2cd6c5 --- /dev/null +++ b/webview-ui/src/components/settings/providers/AIStupidLevel.tsx @@ -0,0 +1,73 @@ +import { useCallback } from "react" +import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" + +import { type ProviderSettings, type OrganizationAllowList, aiStupidLevelDefaultModelId } from "@roo-code/types" + +import type { RouterModels } from "@roo/api" + +import { useAppTranslation } from "@src/i18n/TranslationContext" +import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" + +import { inputEventTransform } from "../transforms" +import { ModelPicker } from "../ModelPicker" + +type AIStupidLevelProps = { + apiConfiguration: ProviderSettings + setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void + routerModels?: RouterModels + organizationAllowList: OrganizationAllowList + modelValidationError?: string +} + +export const AIStupidLevel = ({ + apiConfiguration, + setApiConfigurationField, + routerModels, + organizationAllowList, + modelValidationError, +}: AIStupidLevelProps) => { + const { t } = useAppTranslation() + + const handleInputChange = useCallback( + ( + field: K, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, + ) => + (event: E | Event) => { + setApiConfigurationField(field, transform(event as E)) + }, + [setApiConfigurationField], + ) + + return ( + <> + + + +
+ {t("settings:providers.apiKeyStorageNotice")} +
+ {!apiConfiguration?.aiStupidLevelApiKey && ( + + {t("settings:providers.getAIStupidLevelApiKey")} + + )} + + + ) +} diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index 0f0483d924..2d6d3d5c8b 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -29,6 +29,7 @@ export { XAI } from "./XAI" export { GeminiCli } from "./GeminiCli" export { VirtualQuotaFallbackProvider } from "./VirtualQuotaFallbackProvider" // kilocode_change end +export { AIStupidLevel } from "./AIStupidLevel" // kilocode_change export { ZAi } from "./ZAi" export { LiteLLM } from "./LiteLLM" export { Fireworks } from "./Fireworks" diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 069dc121ae..1efadbf9be 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -257,6 +257,8 @@ "getOpenRouterApiKey": "Get OpenRouter API Key", "vercelAiGatewayApiKey": "Vercel AI Gateway API Key", "getVercelAiGatewayApiKey": "Get Vercel AI Gateway API Key", + "aiStupidLevelApiKey": "AIStupidLevel API Key", + "getAIStupidLevelApiKey": "Get AIStupidLevel API Key", "apiKeyStorageNotice": "API keys are stored securely in VSCode's Secret Storage", "glamaApiKey": "Glama API Key", "getGlamaApiKey": "Get Glama API Key",