diff --git a/.changeset/khaki-seals-hunt.md b/.changeset/khaki-seals-hunt.md new file mode 100644 index 00000000000..3a6882087ea --- /dev/null +++ b/.changeset/khaki-seals-hunt.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Feature: add new provider AIHubmix diff --git a/apps/kilocode-docs/pages/ai-providers/aihubmix.md b/apps/kilocode-docs/pages/ai-providers/aihubmix.md new file mode 100644 index 00000000000..2b50ef8ee59 --- /dev/null +++ b/apps/kilocode-docs/pages/ai-providers/aihubmix.md @@ -0,0 +1,28 @@ +--- +sidebar_label: AIhubmix +--- + +# Using AIhubmix With Kilo Code + +AIhubmix is an AI gateway that provides unified access to multiple AI models from various providers through a single API. It offers competitive pricing and supports features like prompt caching. + +**Website:** [https://aihubmix.com/](https://aihubmix.com/) + +## Getting an API Key + +1. **Sign Up/Sign In:** Go to the [AIhubmix website](https://aihubmix.com/) and create an account or sign in. +2. **Get API Key:** Go to the [API Keys page](https://console.aihubmix.com/token) to generate an API key. +3. **Copy the Key:** Copy the API key. + +## Configuration in Kilo Code + +1. **Open Kilo Code Settings:** Click the gear icon ({% codicon name="gear" /%}) in the Kilo Code panel. +2. **Select Provider:** Choose "AIhubmix" from the "API Provider" dropdown. +3. **Enter API Key:** Paste your AIhubmix API key into the "AIhubmix API Key" field. +4. **Select Model:** Choose your desired model from the "Model" dropdown. + +## Tips and Notes + +- **Model Selection:** AIhubmix offers a wide range of models. Models are sorted by their coding capability score. +- **Pricing:** AIhubmix charges based on the underlying model's pricing. See the [AIhubmix Models page](https://aihubmix.com/models) for details. +- **Prompt Caching:** Some models support prompt caching. See the AIhubmix documentation for supported models. diff --git a/docs/plans/2026-01-20-apertis-provider-design.md b/docs/plans/2026-01-20-apertis-provider-design.md index b8f6ed87aea..0538bda8d4c 100644 --- a/docs/plans/2026-01-20-apertis-provider-design.md +++ b/docs/plans/2026-01-20-apertis-provider-design.md @@ -10,13 +10,13 @@ Apertis is a unified AI API platform providing access to 450+ models from multip ## API Endpoints -| Endpoint | Format | Authentication | Use Case | -|----------|--------|----------------|----------| -| `/v1/chat/completions` | OpenAI Chat | `Authorization: Bearer` | General models (GPT, Gemini, etc.) | -| `/v1/messages` | Anthropic | `x-api-key` | Claude models | -| `/v1/responses` | OpenAI Responses | `Authorization: Bearer` | Reasoning models (o1, o3) | -| `/api/models` | - | None required | Public model list | -| `/v1/models` | OpenAI | `Authorization: Bearer` | Detailed model info | +| Endpoint | Format | Authentication | Use Case | +| ---------------------- | ---------------- | ----------------------- | ---------------------------------- | +| `/v1/chat/completions` | OpenAI Chat | `Authorization: Bearer` | General models (GPT, Gemini, etc.) | +| `/v1/messages` | Anthropic | `x-api-key` | Claude models | +| `/v1/responses` | OpenAI Responses | `Authorization: Bearer` | Reasoning models (o1, o3) | +| `/api/models` | - | None required | Public model list | +| `/v1/models` | OpenAI | `Authorization: Bearer` | Detailed model info | **Base URL:** `https://api.apertis.ai` (configurable for enterprise/self-hosted) @@ -40,6 +40,7 @@ The ApertisHandler implements intelligent routing based on model ID: ``` **Routing Rules:** + - `claude-*` → `/v1/messages` (Anthropic format) - `o1-*`, `o3-*` or reasoning enabled → `/v1/responses` (Responses API) - Others → `/v1/chat/completions` (OpenAI Chat) @@ -78,21 +79,21 @@ webview-ui/src/i18n/locales/*/settings.json # i18n translations ```typescript const apertisSchema = baseProviderSettingsSchema.extend({ - // Authentication - apertisApiKey: z.string().optional(), + // Authentication + apertisApiKey: z.string().optional(), - // Model selection - apertisModelId: z.string().optional(), + // Model selection + apertisModelId: z.string().optional(), - // Base URL (default: https://api.apertis.ai) - apertisBaseUrl: z.string().optional(), + // Base URL (default: https://api.apertis.ai) + apertisBaseUrl: z.string().optional(), - // Responses API specific - apertisInstructions: z.string().optional(), + // Responses API specific + apertisInstructions: z.string().optional(), - // Reasoning settings - apertisReasoningEffort: z.enum(["low", "medium", "high"]).optional(), - apertisReasoningSummary: z.enum(["auto", "concise", "detailed"]).optional(), + // Reasoning settings + apertisReasoningEffort: z.enum(["low", "medium", "high"]).optional(), + apertisReasoningSummary: z.enum(["auto", "concise", "detailed"]).optional(), }) ``` @@ -111,44 +112,44 @@ export const apertisDefaultModelId = "claude-sonnet-4-20250514" // src/api/providers/apertis.ts export class ApertisHandler extends BaseProvider implements SingleCompletionHandler { - private options: ApiHandlerOptions - private client: OpenAI - private anthropicClient: Anthropic - - constructor(options: ApiHandlerOptions) { - const baseURL = options.apertisBaseUrl || APERTIS_DEFAULT_BASE_URL - - this.client = new OpenAI({ - baseURL: `${baseURL}/v1`, - apiKey: options.apertisApiKey, - }) - - this.anthropicClient = new Anthropic({ - baseURL: `${baseURL}/v1`, - apiKey: options.apertisApiKey, - }) - } - - private getApiFormat(modelId: string): "messages" | "responses" | "chat" { - if (modelId.startsWith("claude-")) return "messages" - if (modelId.startsWith("o1-") || modelId.startsWith("o3-")) return "responses" - return "chat" - } - - async *createMessage(systemPrompt, messages, metadata) { - const format = this.getApiFormat(this.getModel().id) - - switch (format) { - case "messages": - yield* this.createAnthropicMessage(systemPrompt, messages, metadata) - break - case "responses": - yield* this.createResponsesMessage(systemPrompt, messages, metadata) - break - default: - yield* this.createChatMessage(systemPrompt, messages, metadata) - } - } + private options: ApiHandlerOptions + private client: OpenAI + private anthropicClient: Anthropic + + constructor(options: ApiHandlerOptions) { + const baseURL = options.apertisBaseUrl || APERTIS_DEFAULT_BASE_URL + + this.client = new OpenAI({ + baseURL: `${baseURL}/v1`, + apiKey: options.apertisApiKey, + }) + + this.anthropicClient = new Anthropic({ + baseURL: `${baseURL}/v1`, + apiKey: options.apertisApiKey, + }) + } + + private getApiFormat(modelId: string): "messages" | "responses" | "chat" { + if (modelId.startsWith("claude-")) return "messages" + if (modelId.startsWith("o1-") || modelId.startsWith("o3-")) return "responses" + return "chat" + } + + async *createMessage(systemPrompt, messages, metadata) { + const format = this.getApiFormat(this.getModel().id) + + switch (format) { + case "messages": + yield* this.createAnthropicMessage(systemPrompt, messages, metadata) + break + case "responses": + yield* this.createResponsesMessage(systemPrompt, messages, metadata) + break + default: + yield* this.createChatMessage(systemPrompt, messages, metadata) + } + } } ``` @@ -157,33 +158,31 @@ export class ApertisHandler extends BaseProvider implements SingleCompletionHand ```typescript // src/api/providers/fetchers/apertis.ts -export async function getApertisModels(options?: { - apiKey?: string - baseUrl?: string -}): Promise { - const baseUrl = options?.baseUrl || APERTIS_DEFAULT_BASE_URL +export async function getApertisModels(options?: { apiKey?: string; baseUrl?: string }): Promise { + const baseUrl = options?.baseUrl || APERTIS_DEFAULT_BASE_URL - // Use public endpoint (no auth required) - const response = await fetch(`${baseUrl}/api/models`) - const data = await response.json() + // Use public endpoint (no auth required) + const response = await fetch(`${baseUrl}/api/models`) + const data = await response.json() - const models: ModelRecord = {} + const models: ModelRecord = {} - for (const modelId of data.data) { - models[modelId] = { - contextWindow: getContextWindow(modelId), - supportsPromptCache: modelId.startsWith("claude-"), - supportsImages: supportsVision(modelId), - } - } + for (const modelId of data.data) { + models[modelId] = { + contextWindow: getContextWindow(modelId), + supportsPromptCache: modelId.startsWith("claude-"), + supportsImages: supportsVision(modelId), + } + } - return models + return models } ``` ## UI Settings Component Key features: + - API Key input with link to `https://apertis.ai/token` - Model picker with dynamic model list - Reasoning settings (shown only for o1/o3 models) @@ -191,14 +190,14 @@ Key features: ## Special Features Support -| Feature | API | Implementation | -|---------|-----|----------------| -| Extended Thinking | Messages API | `thinking.budget_tokens` parameter | -| Reasoning Effort | Responses API | `reasoning.effort` parameter | -| Reasoning Summary | Responses API | `reasoning.summary` parameter | -| Instructions | Responses API | `instructions` parameter | -| Web Search | Responses API | `tools` with web_search type | -| Streaming | All APIs | `stream: true` parameter | +| Feature | API | Implementation | +| ----------------- | ------------- | ---------------------------------- | +| Extended Thinking | Messages API | `thinking.budget_tokens` parameter | +| Reasoning Effort | Responses API | `reasoning.effort` parameter | +| Reasoning Summary | Responses API | `reasoning.summary` parameter | +| Instructions | Responses API | `instructions` parameter | +| Web Search | Responses API | `tools` with web_search type | +| Streaming | All APIs | `stream: true` parameter | ## Error Handling diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 3009a78790c..49a3da4f250 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -65,6 +65,7 @@ export const dynamicProviders = [ "requesty", "unbound", "glama", // kilocode_change + "aihubmix", // kilocode_change "roo", "chutes", "nano-gpt", //kilocode_change @@ -547,6 +548,13 @@ const fireworksSchema = apiModelIdProviderModelSchema.extend({ const syntheticSchema = apiModelIdProviderModelSchema.extend({ syntheticApiKey: z.string().optional(), }) + +const aihubmixSchema = baseProviderSettingsSchema.extend({ + aihubmixApiKey: z.string().optional(), + aihubmixBaseUrl: z.string().optional(), + aihubmixModelId: z.string().optional(), + aihubmixModelInfo: modelInfoSchema.optional(), +}) // kilocode_change end const featherlessSchema = apiModelIdProviderModelSchema.extend({ @@ -630,6 +638,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv virtualQuotaFallbackSchema.merge(z.object({ apiProvider: z.literal("virtual-quota-fallback") })), syntheticSchema.merge(z.object({ apiProvider: z.literal("synthetic") })), inceptionSchema.merge(z.object({ apiProvider: z.literal("inception") })), + aihubmixSchema.merge(z.object({ apiProvider: z.literal("aihubmix") })), // kilocode_change end groqSchema.merge(z.object({ apiProvider: z.literal("groq") })), basetenSchema.merge(z.object({ apiProvider: z.literal("baseten") })), @@ -673,6 +682,7 @@ export const providerSettingsSchema = z.object({ ...syntheticSchema.shape, ...ovhcloudSchema.shape, ...inceptionSchema.shape, + ...aihubmixSchema.shape, // kilocode_change end ...openAiCodexSchema.shape, ...openAiNativeSchema.shape, @@ -745,6 +755,7 @@ export const modelIdKeys = [ "inceptionLabsModelId", // kilocode_change "sapAiCoreModelId", // kilocode_change "apertisModelId", // kilocode_change + "aihubmixModelId", // kilocode_change ] as const satisfies readonly (keyof ProviderSettings)[] export type ModelIdKey = (typeof modelIdKeys)[number] @@ -792,6 +803,7 @@ export const modelIdKeysByProvider: Record = { ovhcloud: "ovhCloudAiEndpointsModelId", inception: "inceptionLabsModelId", "sap-ai-core": "sapAiCoreModelId", + aihubmix: "aihubmixModelId", apertis: "apertisModelId", zenmux: "zenmuxModelId", // kilocode_change // kilocode_change end @@ -968,6 +980,7 @@ export const MODELS_BY_PROVIDER: Record< inception: { id: "inception", label: "Inception", models: [] }, kilocode: { id: "kilocode", label: "Kilocode", models: [] }, "virtual-quota-fallback": { id: "virtual-quota-fallback", label: "Virtual Quota Fallback", models: [] }, + aihubmix: { id: "aihubmix", label: "AIhubmix", models: [] }, apertis: { id: "apertis", label: "Apertis", models: [] }, zenmux: { id: "zenmux", label: "ZenMux", models: [] }, // kilocode_change // kilocode_change end diff --git a/packages/types/src/providers/aihubmix.ts b/packages/types/src/providers/aihubmix.ts new file mode 100644 index 00000000000..0f29433378e --- /dev/null +++ b/packages/types/src/providers/aihubmix.ts @@ -0,0 +1,21 @@ +// kilocode_change - new file +// AIhubmix is a dynamic provider, models are fetched from API +// Only fallback types are defined here + +import type { ModelInfo } from "../model.js" + +export type AihubmixModelId = string + +export const aihubmixDefaultModelId = "claude-opus-4-5" + +export const aihubmixDefaultModelInfo: ModelInfo = { + maxTokens: 32000, + contextWindow: 200000, + supportsImages: true, + supportsPromptCache: true, + supportsNativeTools: true, + defaultToolProtocol: "native" as const, + inputPrice: 5, + outputPrice: 25, + description: "AIhubmix unified model provider", +} diff --git a/packages/types/src/providers/fireworks.ts b/packages/types/src/providers/fireworks.ts index ed2e80f7bd6..e2db90911f2 100644 --- a/packages/types/src/providers/fireworks.ts +++ b/packages/types/src/providers/fireworks.ts @@ -207,7 +207,7 @@ export const fireworksModels = { supportsImages: false, supportsPromptCache: false, supportsNativeTools: true, - defaultToolProtocol: "native", + defaultToolProtocol: "native", inputPrice: 0.22, outputPrice: 0.88, displayName: "GLM-4.5 Air", @@ -243,7 +243,7 @@ export const fireworksModels = { supportsImages: false, supportsPromptCache: true, supportsNativeTools: true, - defaultToolProtocol: "native", + defaultToolProtocol: "native", inputPrice: 0.05, outputPrice: 0.2, cacheReadsPrice: 0.04, diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index ae23fec2d69..48e3c3020ed 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -16,6 +16,7 @@ export * from "./synthetic.js" export * from "./inception.js" export * from "./minimax.js" export * from "./glama.js" +export * from "./aihubmix.js" export * from "./apertis.js" export * from "./zenmux.js" // kilocode_change end @@ -58,6 +59,7 @@ import { featherlessDefaultModelId } from "./featherless.js" import { fireworksDefaultModelId } from "./fireworks.js" import { geminiDefaultModelId } from "./gemini.js" import { glamaDefaultModelId } from "./glama.js" // kilocode_change +import { aihubmixDefaultModelId } from "./aihubmix.js" // kilocode_change import { apertisDefaultModelId } from "./apertis.js" // kilocode_change import { zenmuxDefaultModelId } from "./zenmux.js" // kilocode_change import { groqDefaultModelId } from "./groq.js" @@ -102,6 +104,8 @@ export function getProviderDefaultModelId( // kilocode_change start case "glama": return glamaDefaultModelId + case "aihubmix": + return aihubmixDefaultModelId case "apertis": return apertisDefaultModelId // kilocode_change end diff --git a/src/api/index.ts b/src/api/index.ts index f053a845e35..bc890b2eee7 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -38,6 +38,7 @@ import { SyntheticHandler, OVHcloudAIEndpointsHandler, SapAiCoreHandler, + AihubmixHandler, ApertisHandler, // kilocode_change end ClaudeCodeHandler, @@ -265,6 +266,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { return new OVHcloudAIEndpointsHandler(options) case "sap-ai-core": return new SapAiCoreHandler(options) + case "aihubmix": + return new AihubmixHandler(options) case "apertis": return new ApertisHandler(options) // kilocode_change end diff --git a/src/api/providers/__tests__/moonshot.spec.ts b/src/api/providers/__tests__/moonshot.spec.ts index 575578f9e70..5c60a842990 100644 --- a/src/api/providers/__tests__/moonshot.spec.ts +++ b/src/api/providers/__tests__/moonshot.spec.ts @@ -335,7 +335,9 @@ describe("MoonshotHandler", () => { }), }) - for await (const _chunk of strictHandler.createMessage(systemPrompt, messages, { taskId: "task-cache-2" })) { + for await (const _chunk of strictHandler.createMessage(systemPrompt, messages, { + taskId: "task-cache-2", + })) { // Drain stream } diff --git a/src/api/providers/aihubmix.ts b/src/api/providers/aihubmix.ts new file mode 100644 index 00000000000..89605819284 --- /dev/null +++ b/src/api/providers/aihubmix.ts @@ -0,0 +1,142 @@ +// kilocode_change - new file +import { Anthropic } from "@anthropic-ai/sdk" + +import type { ModelInfo } from "@roo-code/types" +import { aihubmixDefaultModelInfo } from "@roo-code/types" + +import type { ApiHandlerOptions } from "../../shared/api" +import { ApiStream } from "../transform/stream" +import { BaseProvider } from "./base-provider" +import type { ApiHandler, SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" + +// Reuse existing handlers via delegation pattern +import { AnthropicHandler } from "./anthropic" +import { OpenAiHandler } from "./openai" +import { GeminiHandler } from "./gemini" +import { OpenAiCompatibleResponsesHandler } from "./openai-responses" + +const AIHUBMIX_DEFAULT_BASE_URL = "https://aihubmix.com" +const AIHUBMIX_DEFAULT_MODEL = "claude-opus-4-5" + +type ModelRoute = "anthropic" | "openai" | "openai-responses" | "gemini" +export class AihubmixHandler extends BaseProvider implements SingleCompletionHandler { + private options: ApiHandlerOptions + private delegateHandler: ApiHandler | null = null + private lastModelId: string | null = null + + constructor(options: ApiHandlerOptions) { + super() + this.options = options + } + + /** + * Route to the appropriate handler based on model ID prefix + */ + private routeModel(modelId: string): ModelRoute { + const id = modelId.toLowerCase() + if (id.startsWith("claude")) { + return "anthropic" + } + if (id.startsWith("gemini") && !id.endsWith("-nothink") && !id.endsWith("-search")) { + return "gemini" + } + // gpt-5-pro and gpt-5-codex require OpenAI Responses API + if (id === "gpt-5-pro" || id === "gpt-5-codex") { + return "openai-responses" + } + return "openai" + } + + /** + * Create delegate handler - reuses existing handler implementations + * Maps aihubmix configuration to the corresponding handler's configuration + */ + private getDelegateHandler(): ApiHandler { + const modelId = this.options.aihubmixModelId || AIHUBMIX_DEFAULT_MODEL + + // Cache: reuse the same handler for the same model + if (this.delegateHandler && this.lastModelId === modelId) { + return this.delegateHandler + } + + const baseUrl = this.options.aihubmixBaseUrl || AIHUBMIX_DEFAULT_BASE_URL + const route = this.routeModel(modelId) + + switch (route) { + case "anthropic": + // Reuse AnthropicHandler with mapped configuration + // Explicitly set anthropicUseAuthToken: false to prevent Anthropic-specific + // auth settings from leaking through and causing silent auth failures. + this.delegateHandler = new AnthropicHandler({ + ...this.options, + apiKey: this.options.aihubmixApiKey, + anthropicBaseUrl: baseUrl, + apiModelId: this.options.aihubmixModelId, + anthropicUseAuthToken: false, + }) + break + + case "gemini": + // Reuse GeminiHandler with mapped configuration + this.delegateHandler = new GeminiHandler({ + ...this.options, + geminiApiKey: this.options.aihubmixApiKey, + googleGeminiBaseUrl: `${baseUrl}/gemini`, + apiModelId: this.options.aihubmixModelId, + }) + break + + case "openai-responses": + // Reuse OpenAiCompatibleResponsesHandler for gpt-5-pro/gpt-5-codex models + this.delegateHandler = new OpenAiCompatibleResponsesHandler({ + ...this.options, + openAiApiKey: this.options.aihubmixApiKey, + openAiBaseUrl: `${baseUrl}/v1`, + openAiModelId: this.options.aihubmixModelId, + }) + break + + case "openai": + default: + // Reuse OpenAiHandler with mapped configuration + this.delegateHandler = new OpenAiHandler({ + ...this.options, + openAiApiKey: this.options.aihubmixApiKey, + openAiBaseUrl: `${baseUrl}/v1`, + openAiModelId: this.options.aihubmixModelId, + }) + break + } + + this.lastModelId = modelId + return this.delegateHandler + } + + // ==================== Delegate to the corresponding handler ==================== + + async *createMessage( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + metadata?: ApiHandlerCreateMessageMetadata, + ): ApiStream { + yield* this.getDelegateHandler().createMessage(systemPrompt, messages, metadata) + } + + override getModel(): { id: string; info: ModelInfo } { + const id = this.options.aihubmixModelId || AIHUBMIX_DEFAULT_MODEL + const info = this.options.aihubmixModelInfo ?? aihubmixDefaultModelInfo + return { id, info } + } + + override async countTokens(content: Array): Promise { + return this.getDelegateHandler().countTokens(content) + } + + async completePrompt(prompt: string): Promise { + const handler = this.getDelegateHandler() + if ("completePrompt" in handler) { + return (handler as SingleCompletionHandler).completePrompt(prompt) + } + throw new Error("completePrompt not supported for this model") + } +} diff --git a/src/api/providers/chutes.ts b/src/api/providers/chutes.ts index 7550e2c16cf..d62d36e6694 100644 --- a/src/api/providers/chutes.ts +++ b/src/api/providers/chutes.ts @@ -102,10 +102,13 @@ export class ChutesHandler extends RouterProvider implements SingleCompletionHan const model = await this.fetchModel() if (model.id.includes("DeepSeek-R1")) { - const stream = await this.client.chat.completions.create({ - ...this.getCompletionParams(systemPrompt, messages, metadata), - messages: convertToR1Format([{ role: "user", content: systemPrompt }, ...messages]), - }, this.getRequestOptions()) + const stream = await this.client.chat.completions.create( + { + ...this.getCompletionParams(systemPrompt, messages, metadata), + messages: convertToR1Format([{ role: "user", content: systemPrompt }, ...messages]), + }, + this.getRequestOptions(), + ) const matcher = new XmlMatcher( "think", diff --git a/src/api/providers/fetchers/aihubmix.ts b/src/api/providers/fetchers/aihubmix.ts new file mode 100644 index 00000000000..410a0265957 --- /dev/null +++ b/src/api/providers/fetchers/aihubmix.ts @@ -0,0 +1,101 @@ +// kilocode_change - new file +import axios from "axios" + +import type { ModelInfo } from "@roo-code/types" + +/** + * Parse features field (may be comma-separated string or array) + */ +function parseFeatures(features: string | string[] | undefined): string[] { + if (!features) return [] + if (Array.isArray(features)) return features + return features.split(",").map((f) => f.trim()) +} + +/** + * Parse input_modalities field (may be comma-separated string or array) + */ +function parseModalities(modalities: string | string[] | undefined): string[] { + if (!modalities) return [] + if (Array.isArray(modalities)) return modalities + return modalities.split(",").map((m) => m.trim()) +} + +export interface GetAihubmixModelsOptions { + baseUrl?: string + apiKey?: string +} + +/** + * Fetch available models from AIhubmix API + * API: https://aihubmix.com/api/v1/models?type=llm&sort_by=coding + */ +export async function getAihubmixModels(options?: GetAihubmixModelsOptions): Promise> { + const models: Record = {} + const baseUrl = options?.baseUrl || "https://aihubmix.com" + + try { + const response = await axios.get(`${baseUrl}/api/v1/models?type=llm&sort_by=coding`) + + if (!response.data?.success || !Array.isArray(response.data?.data)) { + console.error("Invalid response from AIhubmix API:", response.data) + return models + } + + const rawModels = response.data.data + + let preferredIndex = 0 + for (const rawModel of rawModels) { + if (!rawModel.model_id || typeof rawModel.model_id !== "string") { + continue + } + + const features = parseFeatures(rawModel.features) + const inputModalities = parseModalities(rawModel.input_modalities) + const pricing = rawModel.pricing || {} + + // Check if model supports images + const supportsImages = inputModalities.includes("image") + + // Check if model supports thinking/reasoning + const supportsThinking = features.includes("thinking") + + // Check if model supports native tools + const supportsNativeTools = features.includes("tools") || features.includes("function_calling") + + // Check if model supports prompt cache: cache_read price differs from input price + const supportsPromptCache = + pricing.cache_read !== undefined && pricing.input !== undefined && pricing.cache_read !== pricing.input + + const modelInfo: ModelInfo = { + maxTokens: rawModel.max_output ?? 8192, + contextWindow: rawModel.context_length ?? 128000, + supportsImages, + supportsPromptCache, + supportsNativeTools, + defaultToolProtocol: "native", + inputPrice: pricing.input, + outputPrice: pricing.output, + cacheWritesPrice: pricing.cache_write, + cacheReadsPrice: pricing.cache_read, + description: rawModel.desc || "", + preferredIndex, // Preserve API return order (sort_by=coding) + // If thinking is supported, set reasoning-related properties + ...(supportsThinking && rawModel.thinking_config + ? { + supportsReasoningBudget: true, + } + : {}), + } + + models[rawModel.model_id] = modelInfo + preferredIndex++ + } + + console.log(`Fetched ${Object.keys(models).length} AIhubmix models`) + } catch (error) { + console.error(`Error fetching AIhubmix models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`) + } + + return models +} diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index eaf86708fa5..e4e2a07687e 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -33,6 +33,7 @@ import { getGeminiModels } from "./gemini" import { getInceptionModels } from "./inception" import { getSyntheticModels } from "./synthetic" import { getSapAiCoreModels } from "./sap-ai-core" +import { getAihubmixModels } from "./aihubmix" import { getApertisModels } from "./apertis" // kilocode_change end @@ -186,6 +187,12 @@ async function fetchModelsFromProvider(options: GetModelsOptions): Promise { { provider: "ovhcloud", options: { provider: "ovhcloud" } }, // kilocode_change: Add ovhcloud to background refresh { provider: "litellm", options: { provider: "litellm" } }, // kilocode_change: Add litellm to background refresh { provider: "apertis", options: { provider: "apertis" } }, // kilocode_change: Add apertis to background refresh + { provider: "aihubmix", options: { provider: "aihubmix" } }, // kilocode_change: Add aihubmix to background refresh ] // Refresh each provider in background (fire and forget) diff --git a/src/api/providers/fetchers/zenmux.ts b/src/api/providers/fetchers/zenmux.ts index 9652a7a1377..73829bafb92 100644 --- a/src/api/providers/fetchers/zenmux.ts +++ b/src/api/providers/fetchers/zenmux.ts @@ -50,7 +50,8 @@ export async function getZenmuxModels( for (const model of data) { const { id, owned_by, display_name, context_length, input_modalities } = model - const contextWindow = context_length && context_length > 0 ? context_length : zenmuxDefaultModelInfo.contextWindow + const contextWindow = + context_length && context_length > 0 ? context_length : zenmuxDefaultModelInfo.contextWindow const modelInfo: ModelInfo = { // Keep max tokens conservative and let centralized max-token logic decide runtime reservation. diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index 58589a66920..23b1951ba73 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -37,6 +37,7 @@ export { VirtualQuotaFallbackHandler } from "./virtual-quota-fallback" export { SyntheticHandler } from "./synthetic" export { InceptionLabsHandler } from "./inception" export { SapAiCoreHandler } from "./sap-ai-core" +export { AihubmixHandler } from "./aihubmix" export { ApertisHandler } from "./apertis" // kilocode_change end export { VsCodeLmHandler } from "./vscode-lm" diff --git a/src/api/providers/kilocode-openrouter.ts b/src/api/providers/kilocode-openrouter.ts index 7df541da2a2..7f3214841fa 100644 --- a/src/api/providers/kilocode-openrouter.ts +++ b/src/api/providers/kilocode-openrouter.ts @@ -210,7 +210,8 @@ export class KilocodeOpenrouterHandler extends OpenRouterHandler { Accept: "application/json", "x-api-key": this.options.kilocodeToken ?? "", Authorization: `Bearer ${this.options.kilocodeToken}`, - ...this.customRequestOptions({ taskId: taskId ?? "autocomplete", mode: "code", feature: "autocomplete" })?.headers, + ...this.customRequestOptions({ taskId: taskId ?? "autocomplete", mode: "code", feature: "autocomplete" }) + ?.headers, } // temperature: 0.2 is mentioned as a sane example in mistral's docs and is what continue uses. diff --git a/src/api/transform/ai-sdk.ts b/src/api/transform/ai-sdk.ts index 3ea490effe4..b71f5cd2dad 100644 --- a/src/api/transform/ai-sdk.ts +++ b/src/api/transform/ai-sdk.ts @@ -5,13 +5,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" -import { - tool as createTool, - jsonSchema, - type AssistantModelMessage, - type ModelMessage, - type TextStreamPart, -} from "ai" +import { tool as createTool, jsonSchema, type AssistantModelMessage, type ModelMessage, type TextStreamPart } from "ai" import type { ApiStreamChunk } from "./stream" /** diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 6a18ef39871..03ff82b9229 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -2843,6 +2843,7 @@ describe("ClineProvider - Router Models", () => { litellm: mockModels, kilocode: mockModels, "nano-gpt": mockModels, // kilocode_change + aihubmix: mockModels, // kilocode_change ollama: mockModels, // kilocode_change lmstudio: {}, "vercel-ai-gateway": mockModels, @@ -2899,6 +2900,7 @@ describe("ClineProvider - Router Models", () => { .mockResolvedValueOnce(mockModels) // vercel-ai-gateway success .mockResolvedValueOnce(mockModels) // deepinfra success .mockResolvedValueOnce(mockModels) // nano-gpt success // kilocode_change + .mockResolvedValueOnce(mockModels) // kilocode_change: aihubmix .mockResolvedValueOnce(mockModels) // kilocode_change: ovhcloud .mockResolvedValueOnce(mockModels) // kilocode_change: inception success .mockResolvedValueOnce(mockModels) // kilocode_change: synthetic success @@ -2927,6 +2929,7 @@ describe("ClineProvider - Router Models", () => { litellm: {}, kilocode: {}, "nano-gpt": mockModels, // kilocode_change + aihubmix: mockModels, // kilocode_change "vercel-ai-gateway": mockModels, ovhcloud: mockModels, // kilocode_change inception: mockModels, // kilocode_change @@ -3083,6 +3086,7 @@ describe("ClineProvider - Router Models", () => { litellm: {}, kilocode: mockModels, "nano-gpt": mockModels, // kilocode_change + aihubmix: mockModels, // kilocode_change ollama: mockModels, // kilocode_change lmstudio: {}, "vercel-ai-gateway": mockModels, diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 57c0d33e57a..466a6293602 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -364,6 +364,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { litellm: mockModels, kilocode: mockModels, "nano-gpt": mockModels, // kilocode_change + aihubmix: mockModels, // kilocode_change roo: mockModels, chutes: mockModels, zenmux: mockModels, @@ -475,6 +476,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { litellm: {}, kilocode: mockModels, "nano-gpt": mockModels, // kilocode_change + aihubmix: mockModels, // kilocode_change ollama: mockModels, // kilocode_change lmstudio: {}, "vercel-ai-gateway": mockModels, @@ -510,6 +512,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { .mockResolvedValueOnce(mockModels) // vercel-ai-gateway .mockResolvedValueOnce(mockModels) // deepinfra .mockResolvedValueOnce(mockModels) // nano-gpt // kilocode_change + .mockResolvedValueOnce(mockModels) // kilocode_change aihubmix .mockResolvedValueOnce(mockModels) // kilocode_change ovhcloud .mockRejectedValueOnce(new Error("Inception API error")) // kilocode_change .mockRejectedValueOnce(new Error("Synthetic API error")) // kilocode_change @@ -589,6 +592,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { // kilocode_change start kilocode: mockModels, "nano-gpt": mockModels, + aihubmix: mockModels, // kilocode_change inception: {}, synthetic: {}, gemini: mockModels, @@ -613,6 +617,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { .mockRejectedValueOnce(new Error("Vercel AI Gateway error")) // vercel-ai-gateway .mockRejectedValueOnce(new Error("DeepInfra API error")) // deepinfra .mockRejectedValueOnce(new Error("Nano-GPT API error")) // nano-gpt // kilocode_change + .mockRejectedValueOnce(new Error("Aihubmix API error")) // aihubmix // kilocode_change .mockRejectedValueOnce(new Error("OVHcloud AI Endpoints error")) // ovhcloud // kilocode_change .mockRejectedValueOnce(new Error("Inception API error")) // kilocode_change inception .mockRejectedValueOnce(new Error("Synthetic API error")) // kilocode_change synthetic @@ -733,6 +738,12 @@ describe("webviewMessageHandler - requestRouterModels", () => { }) // kilocode_change start + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "Aihubmix API error", + values: { provider: "aihubmix" }, + }) expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ type: "singleRouterModelFetchResponse", success: false, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 18bffd2d27f..3d2ba899500 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -925,6 +925,7 @@ export const webviewMessageHandler = async ( "sap-ai-core": {}, // kilocode_change chutes: {}, "nano-gpt": {}, // kilocode_change + aihubmix: {}, // kilocode_change zenmux: {}, } const safeGetModels = async (options: GetModelsOptions): Promise => { @@ -995,6 +996,14 @@ export const webviewMessageHandler = async ( nanoGptModelList: apiConfiguration.nanoGptModelList, }, }, + { + key: "aihubmix", + options: { + provider: "aihubmix", + apiKey: apiConfiguration.aihubmixApiKey, + baseUrl: apiConfiguration.aihubmixBaseUrl, + }, + }, // kilocode_change end { key: "ovhcloud", diff --git a/src/services/autocomplete/AutocompleteServiceManager.ts b/src/services/autocomplete/AutocompleteServiceManager.ts index 56ae855eea6..304c5fe4a76 100644 --- a/src/services/autocomplete/AutocompleteServiceManager.ts +++ b/src/services/autocomplete/AutocompleteServiceManager.ts @@ -36,7 +36,9 @@ export class AutocompleteServiceManager { constructor(context: vscode.ExtensionContext, cline: ClineProvider) { if (AutocompleteServiceManager._instance) { - throw new Error("AutocompleteServiceManager is a singleton. Use AutocompleteServiceManager.getInstance() instead.") + throw new Error( + "AutocompleteServiceManager is a singleton. Use AutocompleteServiceManager.getInstance() instead.", + ) } this.context = context diff --git a/src/services/autocomplete/classic-auto-complete/HoleFiller.ts b/src/services/autocomplete/classic-auto-complete/HoleFiller.ts index 28de4a0281a..b362ad6767c 100644 --- a/src/services/autocomplete/classic-auto-complete/HoleFiller.ts +++ b/src/services/autocomplete/classic-auto-complete/HoleFiller.ts @@ -16,7 +16,11 @@ export type { HoleFillerAutocompletePrompt, FillInAtCursorSuggestion, ChatComple * Parse the response - only handles responses with tags * Returns a FillInAtCursorSuggestion with the extracted text, or an empty string if nothing found */ -export function parseAutocompleteResponse(fullResponse: string, prefix: string, suffix: string): FillInAtCursorSuggestion { +export function parseAutocompleteResponse( + fullResponse: string, + prefix: string, + suffix: string, +): FillInAtCursorSuggestion { let fimText: string = "" // Match content strictly between and tags diff --git a/src/shared/api.ts b/src/shared/api.ts index 97162006001..8c33ff305c8 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -208,6 +208,7 @@ const dynamicProviderExtras = { sapAiCoreResourceGroup?: string sapAiCoreUseOrchestration?: boolean }, + aihubmix: {} as { apiKey?: string; baseUrl?: string }, apertis: {} as { apiKey?: string; baseUrl?: string }, // kilocode_change end } as const satisfies Record diff --git a/src/test-llm-autocompletion/mock-context-provider.ts b/src/test-llm-autocompletion/mock-context-provider.ts index 1dba4def55a..b57ef2e575e 100644 --- a/src/test-llm-autocompletion/mock-context-provider.ts +++ b/src/test-llm-autocompletion/mock-context-provider.ts @@ -130,7 +130,9 @@ export function createProviderForTesting( costTrackingCallback: CostTrackingCallback = () => {}, getSettings: () => AutocompleteServiceSettings | null = () => null, ): AutocompleteInlineCompletionProvider { - const instance = Object.create(AutocompleteInlineCompletionProvider.prototype) as AutocompleteInlineCompletionProvider + const instance = Object.create( + AutocompleteInlineCompletionProvider.prototype, + ) as AutocompleteInlineCompletionProvider // Initialize private fields using Object.assign to bypass TypeScript private access Object.assign(instance, { suggestionsHistory: [], diff --git a/webview-ui/src/components/chat/hooks/useChatAutocompleteText.ts b/webview-ui/src/components/chat/hooks/useChatAutocompleteText.ts index 52cb18fb655..eb8a90c3ed0 100644 --- a/webview-ui/src/components/chat/hooks/useChatAutocompleteText.ts +++ b/webview-ui/src/components/chat/hooks/useChatAutocompleteText.ts @@ -53,7 +53,10 @@ export function useChatAutocompleteText({ // 2. The cursor is at the end of the text // 3. We have saved autocomplete text that matches the current prefix const shouldShowAutocompleteText = - isFocusedRef.current && isCursorAtEnd && savedAutocompleteTextRef.current && currentText === savedPrefixRef.current + isFocusedRef.current && + isCursorAtEnd && + savedAutocompleteTextRef.current && + currentText === savedPrefixRef.current if (shouldShowAutocompleteText) { setAutocompleteText(savedAutocompleteTextRef.current) diff --git a/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts b/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts index 5e019e3dbe2..4e288619eed 100644 --- a/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts +++ b/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts @@ -43,6 +43,7 @@ describe("getModelsByProvider", () => { synthetic: { "test-model": testModel }, inception: { "test-model": testModel }, roo: { "test-model": testModel }, + aihubmix: { "test-model": testModel }, // kilocode_change zenmux: { "test-model": testModel }, } diff --git a/webview-ui/src/components/kilocode/hooks/useProviderModels.ts b/webview-ui/src/components/kilocode/hooks/useProviderModels.ts index 2bb0323eba8..80f24f46a69 100644 --- a/webview-ui/src/components/kilocode/hooks/useProviderModels.ts +++ b/webview-ui/src/components/kilocode/hooks/useProviderModels.ts @@ -54,6 +54,7 @@ import { cerebrasDefaultModelId, nanoGptDefaultModelId, //kilocode_change apertisDefaultModelId, // kilocode_change + aihubmixDefaultModelId, // kilocode_change ovhCloudAiEndpointsDefaultModelId, inceptionDefaultModelId, minimaxModels, @@ -316,6 +317,12 @@ export const getModelsByProvider = ({ defaultModel: nanoGptDefaultModelId, } } + case "aihubmix": { + return { + models: routerModels.aihubmix, + defaultModel: aihubmixDefaultModelId, + } + } //kilocode_change end case "minimax": { return { @@ -371,7 +378,7 @@ export const getOptionsForProvider = (provider: ProviderName, apiConfiguration?: isChina: apiConfiguration?.zaiApiLine === "china_coding" || apiConfiguration?.zaiApiLine === "china_api", } - // kilocode_change end + // kilocode_change end default: return {} } diff --git a/webview-ui/src/components/kilocode/settings/AutocompleteServiceSettings.tsx b/webview-ui/src/components/kilocode/settings/AutocompleteServiceSettings.tsx index d834cdcbff9..3630fab19bd 100644 --- a/webview-ui/src/components/kilocode/settings/AutocompleteServiceSettings.tsx +++ b/webview-ui/src/components/kilocode/settings/AutocompleteServiceSettings.tsx @@ -127,7 +127,9 @@ export const AutocompleteServiceSettingsView = ({ label={t("kilocode:autocomplete.settings.enableAutoTrigger.label")} className="flex flex-col gap-1"> - {t("kilocode:autocomplete.settings.enableAutoTrigger.label")} + + {t("kilocode:autocomplete.settings.enableAutoTrigger.label")} +
{t("kilocode:autocomplete.settings.enableAutoTrigger.description")} @@ -263,11 +265,15 @@ export const AutocompleteServiceSettingsView = ({ {provider && model ? ( <>
- {t("kilocode:autocomplete.settings.provider")}:{" "} + + {t("kilocode:autocomplete.settings.provider")}: + {" "} {provider}
- {t("kilocode:autocomplete.settings.model")}:{" "} + + {t("kilocode:autocomplete.settings.model")}: + {" "} {model}
diff --git a/webview-ui/src/components/kilocode/settings/__tests__/AutocompleteServiceSettings.spec.tsx b/webview-ui/src/components/kilocode/settings/__tests__/AutocompleteServiceSettings.spec.tsx index 1b90f1f7ced..1e0b1ec2a3d 100644 --- a/webview-ui/src/components/kilocode/settings/__tests__/AutocompleteServiceSettings.spec.tsx +++ b/webview-ui/src/components/kilocode/settings/__tests__/AutocompleteServiceSettings.spec.tsx @@ -224,7 +224,9 @@ describe("AutocompleteServiceSettingsView", () => { const onAutocompleteServiceSettingsChange = vi.fn() renderComponent({ onAutocompleteServiceSettingsChange }) - const checkboxLabel = screen.getByText(/kilocode:autocomplete.settings.enableAutoTrigger.label/).closest("label") + const checkboxLabel = screen + .getByText(/kilocode:autocomplete.settings.enableAutoTrigger.label/) + .closest("label") const checkbox = checkboxLabel?.querySelector('input[type="checkbox"]') as HTMLInputElement fireEvent.click(checkbox) @@ -250,7 +252,9 @@ describe("AutocompleteServiceSettingsView", () => { const onAutocompleteServiceSettingsChange = vi.fn() renderComponent({ onAutocompleteServiceSettingsChange }) - const checkboxLabel = screen.getByText(/kilocode:autocomplete.settings.enableChatAutocomplete.label/).closest("label") + const checkboxLabel = screen + .getByText(/kilocode:autocomplete.settings.enableChatAutocomplete.label/) + .closest("label") const checkbox = checkboxLabel?.querySelector('input[type="checkbox"]') as HTMLInputElement fireEvent.click(checkbox) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index f4ff6c18a59..7005d797dd2 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -118,6 +118,7 @@ import { OvhCloudAiEndpoints, Inception, SapAiCore, + Aihubmix, // kilocode_change end ZAi, Fireworks, @@ -666,6 +667,21 @@ const ApiOptions = ({ /* kilocode_change end */ } + { + /* kilocode_change start */ + selectedProvider === "aihubmix" && ( + + ) + /* kilocode_change end */ + } + {selectedProvider === "unbound" && ( void + routerModels?: RouterModels + simplifySettings?: boolean + organizationAllowList: OrganizationAllowList + modelValidationError?: string +} + +export const Aihubmix = ({ + apiConfiguration, + setApiConfigurationField, + routerModels, + simplifySettings, + organizationAllowList, + modelValidationError, +}: AihubmixProps) => { + const { t } = useAppTranslation() + + const [baseUrlSelected, setBaseUrlSelected] = useState(!!apiConfiguration?.aihubmixBaseUrl) + + 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?.aihubmixApiKey && ( + + {t("settings:providers.getAihubmixApiKey")} + + )} + + {!simplifySettings && ( +
+ { + setBaseUrlSelected(checked) + + if (!checked) { + setApiConfigurationField("aihubmixBaseUrl", "") + } + }}> + {t("settings:providers.useCustomBaseUrl")} + + {baseUrlSelected && ( + + )} +
+ )} + + + + ) +} diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index f1f0239e6cd..2d73713fde8 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -35,6 +35,7 @@ export { VirtualQuotaFallbackProvider } from "./VirtualQuotaFallbackProvider" export { Inception } from "./Inception" export { Synthetic } from "./Synthetic" export { default as SapAiCore } from "./SapAiCore" +export { Aihubmix } from "./Aihubmix" // kilocode_change end export { ZAi } from "./ZAi" export { LiteLLM } from "./LiteLLM" diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 63b435ae9ee..fde71448a21 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -578,6 +578,11 @@ function getSelectedModel({ } return { id, info } } + case "aihubmix": { + const id = getValidatedModelId(apiConfiguration.aihubmixModelId, routerModels.aihubmix, defaultModelId) + const info = routerModels.aihubmix?.[id] + return { id, info } + } case "zenmux": { const id = getValidatedModelId(apiConfiguration.zenmuxModelId, routerModels.zenmux, defaultModelId) const info = routerModels.zenmux?.[id] diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index fa79612efb0..1590a7053ee 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -661,7 +661,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode } }) }, - setAutocompleteServiceSettings: (value) => setState((prevState) => ({ ...prevState, ghostServiceSettings: value })), + setAutocompleteServiceSettings: (value) => + setState((prevState) => ({ ...prevState, ghostServiceSettings: value })), setCommitMessageApiConfigId: (value) => setState((prevState) => ({ ...prevState, commitMessageApiConfigId: value })), setShowAutoApproveMenu: (value) => setState((prevState) => ({ ...prevState, showAutoApproveMenu: value })), diff --git a/webview-ui/src/i18n/locales/ar/settings.json b/webview-ui/src/i18n/locales/ar/settings.json index f4c283452e9..dab1fefb824 100644 --- a/webview-ui/src/i18n/locales/ar/settings.json +++ b/webview-ui/src/i18n/locales/ar/settings.json @@ -353,6 +353,8 @@ "apiKeyStorageNotice": "المفاتيح تُحفظ بأمان في مخزن أسرار VSCode", "glamaApiKey": "مفتاح Glama", "getGlamaApiKey": "احصل على مفتاح Glama", + "aihubmixApiKey": "مفتاح AIhubmix API", + "getAihubmixApiKey": "احصل على مفتاح AIhubmix API", "openAiCodexRateLimits": { "title": "حدود الاستخدام لـ Codex{{planLabel}}", "loading": "جاري تحميل حدود الاستخدام...", diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index d8c71a5932b..9e79b85d902 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -316,6 +316,8 @@ "getVercelAiGatewayApiKey": "Obtenir clau API de Vercel AI Gateway", "glamaApiKey": "Clau API de Glama", "getGlamaApiKey": "Obtenir clau API de Glama", + "aihubmixApiKey": "Clau API de AIhubmix", + "getAihubmixApiKey": "Obtenir clau API de AIhubmix", "apiKeyStorageNotice": "Les claus API s'emmagatzemen de forma segura a l'Emmagatzematge Secret de VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/cs/settings.json b/webview-ui/src/i18n/locales/cs/settings.json index 2e3950f6c88..44251f2161a 100644 --- a/webview-ui/src/i18n/locales/cs/settings.json +++ b/webview-ui/src/i18n/locales/cs/settings.json @@ -339,6 +339,8 @@ "apiKeyStorageNotice": "Klíče API jsou bezpečně uloženy v tajném úložišti VSCode", "glamaApiKey": "Klíč API Glama", "getGlamaApiKey": "Získat klíč API Glama", + "aihubmixApiKey": "Klíč API AIhubmix", + "getAihubmixApiKey": "Získat klíč API AIhubmix", "openAiCodexRateLimits": { "title": "Limity využití pro Codex{{planLabel}}", "loading": "Načítání limitů využití...", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index d1f7bd2a6fc..3b38a5f97e3 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -328,6 +328,8 @@ "apiKeyStorageNotice": "API-Schlüssel werden sicher im VSCode Secret Storage gespeichert", "glamaApiKey": "Glama API-Schlüssel", "getGlamaApiKey": "Glama API-Schlüssel erhalten", + "aihubmixApiKey": "AIhubmix API-Schlüssel", + "getAihubmixApiKey": "AIhubmix API-Schlüssel erhalten", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", "loading": "Loading usage limits...", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 435e7ae4b8e..79e4356acba 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -335,6 +335,8 @@ "apiKeyStorageNotice": "API keys are stored securely in VSCode's Secret Storage", "glamaApiKey": "Glama API Key", "getGlamaApiKey": "Get Glama API Key", + "aihubmixApiKey": "AIhubmix API Key", + "getAihubmixApiKey": "Get AIhubmix API Key", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", "loading": "Loading usage limits...", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 329c6f18fe7..077f44f6c96 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -326,6 +326,8 @@ "apiKeyStorageNotice": "Las claves API se almacenan de forma segura en el Almacenamiento Secreto de VSCode", "glamaApiKey": "Clave API de Glama", "getGlamaApiKey": "Obtener clave API de Glama", + "aihubmixApiKey": "Clave API de AIhubmix", + "getAihubmixApiKey": "Obtener clave API de AIhubmix", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", "loading": "Loading usage limits...", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 61cac44349c..efad3998197 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -326,6 +326,8 @@ "apiKeyStorageNotice": "Les clés API sont stockées en toute sécurité dans le stockage sécurisé de VSCode", "glamaApiKey": "Clé API Glama", "getGlamaApiKey": "Obtenir la clé API Glama", + "aihubmixApiKey": "Clé API AIhubmix", + "getAihubmixApiKey": "Obtenir la clé API AIhubmix", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", "loading": "Loading usage limits...", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 52e8dc9e086..0596ef8e8e3 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -325,6 +325,8 @@ "apiKeyStorageNotice": "API कुंजियाँ VSCode के सुरक्षित स्टोरेज में सुरक्षित रूप से संग्रहीत हैं", "glamaApiKey": "Glama API कुंजी", "getGlamaApiKey": "Glama API कुंजी प्राप्त करें", + "aihubmixApiKey": "AIhubmix API कुंजी", + "getAihubmixApiKey": "AIhubmix API कुंजी प्राप्त करें", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", "loading": "Loading usage limits...", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 26eba087466..d337cb60a21 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -316,6 +316,8 @@ "getVercelAiGatewayApiKey": "Dapatkan Vercel AI Gateway API Key", "glamaApiKey": "Glama API Key", "getGlamaApiKey": "Dapatkan Glama API Key", + "aihubmixApiKey": "AIhubmix API Key", + "getAihubmixApiKey": "Dapatkan AIhubmix API Key", "apiKeyStorageNotice": "API key disimpan dengan aman di Secret Storage VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 0bf0869132e..623533692d1 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -330,6 +330,8 @@ "getVercelAiGatewayApiKey": "Ottieni chiave API Vercel AI Gateway", "glamaApiKey": "Chiave API Glama", "getGlamaApiKey": "Ottieni chiave API Glama", + "aihubmixApiKey": "Chiave API AIhubmix", + "getAihubmixApiKey": "Ottieni chiave API AIhubmix", "apiKeyStorageNotice": "Le chiavi API sono memorizzate in modo sicuro nell'Archivio Segreto di VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index c4aa5de4ba0..bf394b314e3 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -321,6 +321,8 @@ "getVercelAiGatewayApiKey": "Vercel AI Gateway APIキーを取得", "glamaApiKey": "Glama APIキー", "getGlamaApiKey": "Glama APIキーを取得", + "aihubmixApiKey": "AIhubmix APIキー", + "getAihubmixApiKey": "AIhubmix APIキーを取得", "apiKeyStorageNotice": "APIキーはVSCodeのシークレットストレージに安全に保存されます", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index c896d0398de..078476e7c55 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -316,6 +316,8 @@ "getVercelAiGatewayApiKey": "Vercel AI Gateway API 키 받기", "glamaApiKey": "Glama API 키", "getGlamaApiKey": "Glama API 키 받기", + "aihubmixApiKey": "AIhubmix API 키", + "getAihubmixApiKey": "AIhubmix API 키 받기", "apiKeyStorageNotice": "API 키는 VSCode의 보안 저장소에 안전하게 저장됩니다", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index a4957230d06..9fdb02e78fe 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -316,6 +316,8 @@ "getVercelAiGatewayApiKey": "Vercel AI Gateway API-sleutel ophalen", "glamaApiKey": "Glama API-sleutel", "getGlamaApiKey": "Glama API-sleutel ophalen", + "aihubmixApiKey": "AIhubmix API-sleutel", + "getAihubmixApiKey": "AIhubmix API-sleutel ophalen", "apiKeyStorageNotice": "API-sleutels worden veilig opgeslagen in de geheime opslag van VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 52c1bdb3438..5e3308dda3c 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -316,6 +316,8 @@ "getVercelAiGatewayApiKey": "Uzyskaj klucz API Vercel AI Gateway", "glamaApiKey": "Klucz API Glama", "getGlamaApiKey": "Uzyskaj klucz API Glama", + "aihubmixApiKey": "Klucz API AIhubmix", + "getAihubmixApiKey": "Uzyskaj klucz API AIhubmix", "apiKeyStorageNotice": "Klucze API są bezpiecznie przechowywane w Tajnym Magazynie VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 35957aee465..2fc2d8fdc84 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -316,6 +316,8 @@ "getVercelAiGatewayApiKey": "Obter chave API do Vercel AI Gateway", "glamaApiKey": "Chave API do Glama", "getGlamaApiKey": "Obter chave API do Glama", + "aihubmixApiKey": "Chave API do AIhubmix", + "getAihubmixApiKey": "Obter chave API do AIhubmix", "apiKeyStorageNotice": "As chaves de API são armazenadas com segurança no Armazenamento Secreto do VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 7adf4c5e0ab..0b374569ded 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -316,6 +316,8 @@ "getVercelAiGatewayApiKey": "Получить ключ API Vercel AI Gateway", "glamaApiKey": "API-ключ Glama", "getGlamaApiKey": "Получить API-ключ Glama", + "aihubmixApiKey": "API-ключ AIhubmix", + "getAihubmixApiKey": "Получить API-ключ AIhubmix", "apiKeyStorageNotice": "API-ключи хранятся безопасно в Secret Storage VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/sk/settings.json b/webview-ui/src/i18n/locales/sk/settings.json index 286b629719a..d1469ae7586 100644 --- a/webview-ui/src/i18n/locales/sk/settings.json +++ b/webview-ui/src/i18n/locales/sk/settings.json @@ -347,6 +347,8 @@ "apiKeyStorageNotice": "Kľúče API sú bezpečne uložené v tajnom úložisku VSCode", "glamaApiKey": "Kľúč API Glama", "getGlamaApiKey": "Získať kľúč API Glama", + "aihubmixApiKey": "Kľúč API AIhubmix", + "getAihubmixApiKey": "Získať kľúč API AIhubmix", "openAiCodexRateLimits": { "title": "Limity využitia pre Codex{{planLabel}}", "loading": "Načítanie limitov využitia...", diff --git a/webview-ui/src/i18n/locales/th/settings.json b/webview-ui/src/i18n/locales/th/settings.json index e84e8630ddd..73a2c0d5e57 100644 --- a/webview-ui/src/i18n/locales/th/settings.json +++ b/webview-ui/src/i18n/locales/th/settings.json @@ -349,6 +349,8 @@ "apiKeyStorageNotice": "คีย์ API จะถูกเก็บไว้อย่างปลอดภัยในที่เก็บข้อมูลลับของ VSCode", "glamaApiKey": "คีย์ API ของ Glama", "getGlamaApiKey": "รับคีย์ API ของ Glama", + "aihubmixApiKey": "คีย์ API ของ AIhubmix", + "getAihubmixApiKey": "รับคีย์ API ของ AIhubmix", "openAiCodexRateLimits": { "title": "ขีดจำกัดการใช้งานสำหรับ Codex{{planLabel}}", "loading": "กำลังโหลดขีดจำกัดการใช้งาน...", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 79c8641d998..79f1271b053 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -316,6 +316,8 @@ "getVercelAiGatewayApiKey": "Vercel AI Gateway API Anahtarı Al", "glamaApiKey": "Glama API Anahtarı", "getGlamaApiKey": "Glama API Anahtarı Al", + "aihubmixApiKey": "AIhubmix API Anahtarı", + "getAihubmixApiKey": "AIhubmix API Anahtarı Al", "apiKeyStorageNotice": "API anahtarları VSCode'un Gizli Depolamasında güvenli bir şekilde saklanır", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/uk/settings.json b/webview-ui/src/i18n/locales/uk/settings.json index f8e50810ae5..66a2150b85b 100644 --- a/webview-ui/src/i18n/locales/uk/settings.json +++ b/webview-ui/src/i18n/locales/uk/settings.json @@ -354,6 +354,8 @@ "apiKeyStorageNotice": "Ключі API надійно зберігаються в Secret Storage VSCode", "glamaApiKey": "Ключ API Glama", "getGlamaApiKey": "Отримати ключ API Glama", + "aihubmixApiKey": "Ключ API AIhubmix", + "getAihubmixApiKey": "Отримати ключ API AIhubmix", "openAiCodexRateLimits": { "title": "Ліміти використання для Codex{{planLabel}}", "loading": "Завантаження лімітів використання...", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index eeb4d033261..2500e617320 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -324,6 +324,8 @@ "getVercelAiGatewayApiKey": "Lấy khóa API Vercel AI Gateway", "glamaApiKey": "Khóa API Glama", "getGlamaApiKey": "Lấy khóa API Glama", + "aihubmixApiKey": "Khóa API AIhubmix", + "getAihubmixApiKey": "Lấy khóa API AIhubmix", "apiKeyStorageNotice": "Khóa API được lưu trữ an toàn trong Bộ lưu trữ bí mật của VSCode", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 852e754f953..2a32cd52764 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -316,6 +316,8 @@ "getVercelAiGatewayApiKey": "获取 Vercel AI Gateway API 密钥", "glamaApiKey": "Glama API 密钥", "getGlamaApiKey": "获取 Glama API 密钥", + "aihubmixApiKey": "AIhubmix API 密钥", + "getAihubmixApiKey": "获取 AIhubmix API 密钥", "apiKeyStorageNotice": "API 密钥安全存储在 VSCode 的密钥存储中", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 7a4634c3761..1c85bac4438 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -326,6 +326,8 @@ "getVercelAiGatewayApiKey": "取得 Vercel AI Gateway API 金鑰", "glamaApiKey": "Glama API 金鑰", "getGlamaApiKey": "取得 Glama API 金鑰", + "aihubmixApiKey": "AIhubmix API 金鑰", + "getAihubmixApiKey": "取得 AIhubmix API 金鑰", "apiKeyStorageNotice": "API 金鑰會安全地儲存在 VS Code 的 Secret Storage 中", "openAiCodexRateLimits": { "title": "Usage Limits for Codex{{planLabel}}", diff --git a/webview-ui/src/utils/__tests__/validate.spec.ts b/webview-ui/src/utils/__tests__/validate.spec.ts index b83dc61ad79..935770ddc01 100644 --- a/webview-ui/src/utils/__tests__/validate.spec.ts +++ b/webview-ui/src/utils/__tests__/validate.spec.ts @@ -85,6 +85,7 @@ describe("Model Validation Functions", () => { inception: {}, synthetic: {}, "sap-ai-core": {}, + aihubmix: {}, // kilocode_change end roo: {}, chutes: {}, diff --git a/webview-ui/src/utils/costFormatting.ts b/webview-ui/src/utils/costFormatting.ts index dae05e47454..429bbe29477 100644 --- a/webview-ui/src/utils/costFormatting.ts +++ b/webview-ui/src/utils/costFormatting.ts @@ -39,5 +39,5 @@ export function getCostBreakdownIfNeeded( * @returns Formatted string like "0.0234" or "1.23" */ export function formatCost(cost: number): string { - return (cost === 0 || cost > 0.05) ? cost.toFixed(2) : cost.toFixed(4); + return cost === 0 || cost > 0.05 ? cost.toFixed(2) : cost.toFixed(4) }