Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/kilocode-docs/lib/nav/ai-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const AiProvidersNav: NavSection[] = [
title: "AI Gateways",
links: [
{ href: "/ai-providers/openrouter", children: "OpenRouter" },
{ href: "/ai-providers/aihubmix", children: "AIhubmix" },
{ href: "/ai-providers/glama", children: "Glama" },
{ href: "/ai-providers/requesty", children: "Requesty" },
{ href: "/ai-providers/unbound", children: "Unbound" },
Expand Down
28 changes: 28 additions & 0 deletions apps/kilocode-docs/pages/ai-providers/aihubmix.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions cli/src/constants/providers/labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const PROVIDER_LABELS: Record<ProviderName, string> = {
synthetic: "Synthetic",
"sap-ai-core": "SAP AI Core",
baseten: "BaseTen",
aihubmix: "AIhubmix",
}

/**
Expand Down
3 changes: 3 additions & 0 deletions cli/src/constants/providers/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export type RouterName =
| "vercel-ai-gateway"
| "ovhcloud"
| "nano-gpt"
| "aihubmix"

/**
* ModelInfo interface - mirrors the one from packages/types/src/model.ts
Expand Down Expand Up @@ -131,6 +132,7 @@ export const PROVIDER_TO_ROUTER_NAME: Record<ProviderName, RouterName | null> =
"io-intelligence": "io-intelligence",
"vercel-ai-gateway": "vercel-ai-gateway",
ovhcloud: "ovhcloud",
aihubmix: "aihubmix",
// Providers without dynamic model support
anthropic: null,
bedrock: null,
Expand Down Expand Up @@ -184,6 +186,7 @@ export const PROVIDER_MODEL_FIELD: Record<ProviderName, string | null> = {
"io-intelligence": "ioIntelligenceModelId",
"vercel-ai-gateway": "vercelAiGatewayModelId",
ovhcloud: "ovhCloudAiEndpointsModelId",
aihubmix: "aihubmixModelId",
// Providers without dynamic model support
anthropic: null,
bedrock: null,
Expand Down
1 change: 1 addition & 0 deletions cli/src/constants/providers/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,7 @@ export const PROVIDER_DEFAULT_MODELS: Record<ProviderName, string> = {
synthetic: "synthetic-model",
"sap-ai-core": "gpt-4o",
baseten: "zai-org/GLM-4.6",
aihubmix: "claude-opus-4-5",
}

/**
Expand Down
1 change: 1 addition & 0 deletions cli/src/constants/providers/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,5 @@ export const PROVIDER_REQUIRED_FIELDS: Record<ProviderName, string[]> = {
"virtual-quota-fallback": [], // Has array validation
minimax: ["minimaxBaseUrl", "minimaxApiKey", "apiModelId"],
baseten: ["basetenApiKey", "apiModelId"],
aihubmix: ["aihubmixApiKey", "aihubmixModelId"],
}
13 changes: 13 additions & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export const dynamicProviders = [
"requesty",
"unbound",
"glama", // kilocode_change
"aihubmix", // kilocode_change
"roo",
"chutes",
"nano-gpt", //kilocode_change
Expand Down Expand Up @@ -515,6 +516,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({
Expand Down Expand Up @@ -592,6 +600,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") })),
Expand Down Expand Up @@ -632,6 +641,7 @@ export const providerSettingsSchema = z.object({
...syntheticSchema.shape,
...ovhcloudSchema.shape,
...inceptionSchema.shape,
...aihubmixSchema.shape,
// kilocode_change end
...openAiCodexSchema.shape,
...openAiNativeSchema.shape,
Expand Down Expand Up @@ -701,6 +711,7 @@ export const modelIdKeys = [
"ovhCloudAiEndpointsModelId", // kilocode_change
"inceptionLabsModelId", // kilocode_change
"sapAiCoreModelId", // kilocode_change
"aihubmixModelId", // kilocode_change
] as const satisfies readonly (keyof ProviderSettings)[]

export type ModelIdKey = (typeof modelIdKeys)[number]
Expand Down Expand Up @@ -748,6 +759,7 @@ export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
ovhcloud: "ovhCloudAiEndpointsModelId",
inception: "inceptionLabsModelId",
"sap-ai-core": "sapAiCoreModelId",
aihubmix: "aihubmixModelId",
// kilocode_change end
groq: "apiModelId",
baseten: "apiModelId",
Expand Down Expand Up @@ -920,6 +932,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: [] },
// kilocode_change end
deepinfra: { id: "deepinfra", label: "DeepInfra", models: [] },
"vercel-ai-gateway": { id: "vercel-ai-gateway", label: "Vercel AI Gateway", models: [] },
Expand Down
19 changes: 19 additions & 0 deletions packages/types/src/providers/aihubmix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// kilocode_change - new file
// AIhubmix is a dynamic provider, models are fetched from API
// Only fallback types are defined here

export type AihubmixModelId = string

export const aihubmixDefaultModelId = "claude-opus-4-5"

export const aihubmixDefaultModelInfo = {
Comment thread
DDU1222 marked this conversation as resolved.
Outdated
maxTokens: 8192,
contextWindow: 128000,
supportsImages: true,
supportsPromptCache: false,
Comment thread
DDU1222 marked this conversation as resolved.
Outdated
supportsNativeTools: true,
defaultToolProtocol: "native" as const,
inputPrice: 3,
outputPrice: 15,
description: "AIhubmix unified model provider",
}
4 changes: 4 additions & 0 deletions packages/types/src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export * from "./synthetic.js"
export * from "./inception.js"
export * from "./minimax.js"
export * from "./glama.js"
export * from "./aihubmix.js"
// kilocode_change end
export * from "./groq.js"
export * from "./huggingface.js"
Expand Down Expand Up @@ -54,6 +55,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 { groqDefaultModelId } from "./groq.js"
import { ioIntelligenceDefaultModelId } from "./io-intelligence.js"
import { litellmDefaultModelId } from "./lite-llm.js"
Expand Down Expand Up @@ -94,6 +96,8 @@ export function getProviderDefaultModelId(
// kilocode_change start
case "glama":
return glamaDefaultModelId
case "aihubmix":
return aihubmixDefaultModelId
// kilocode_change end
case "unbound":
return unboundDefaultModelId
Expand Down
3 changes: 3 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
SyntheticHandler,
OVHcloudAIEndpointsHandler,
SapAiCoreHandler,
AihubmixHandler,
// kilocode_change end
ClaudeCodeHandler,
QwenCodeHandler,
Expand Down Expand Up @@ -253,6 +254,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)
// kilocode_change end
case "io-intelligence":
return new IOIntelligenceHandler(options)
Expand Down
136 changes: 136 additions & 0 deletions src/api/providers/aihubmix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// kilocode_change - new file
import { Anthropic } from "@anthropic-ai/sdk"

import type { ModelInfo } 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
this.delegateHandler = new AnthropicHandler({
...this.options,
apiKey: this.options.aihubmixApiKey,
anthropicBaseUrl: baseUrl,
apiModelId: this.options.aihubmixModelId,
})
Comment thread
DDU1222 marked this conversation as resolved.
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)
}

getModel(): { id: string; info: ModelInfo } {
return this.getDelegateHandler().getModel()
Comment thread
DDU1222 marked this conversation as resolved.
Outdated
}

override async countTokens(content: Array<Anthropic.Messages.ContentBlockParam>): Promise<number> {
return this.getDelegateHandler().countTokens(content)
}

async completePrompt(prompt: string): Promise<string> {
const handler = this.getDelegateHandler()
if ("completePrompt" in handler) {
return (handler as SingleCompletionHandler).completePrompt(prompt)
}
throw new Error("completePrompt not supported for this model")
}
}
Loading
Loading