Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions .changeset/khaki-seals-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Feature: add new provider AIHubmix
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.
161 changes: 80 additions & 81 deletions docs/plans/2026-01-20-apertis-provider-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)
Expand Down Expand Up @@ -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(),
})
```

Expand All @@ -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)
}
}
}
```

Expand All @@ -157,48 +158,46 @@ export class ApertisHandler extends BaseProvider implements SingleCompletionHand
```typescript
// src/api/providers/fetchers/apertis.ts

export async function getApertisModels(options?: {
apiKey?: string
baseUrl?: string
}): Promise<ModelRecord> {
const baseUrl = options?.baseUrl || APERTIS_DEFAULT_BASE_URL
export async function getApertisModels(options?: { apiKey?: string; baseUrl?: string }): Promise<ModelRecord> {
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)
- Advanced settings (collapsible) with Base URL option

## 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

Expand Down
13 changes: 13 additions & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const dynamicProviders = [
"requesty",
"unbound",
"glama", // kilocode_change
"aihubmix", // kilocode_change
"roo",
"chutes",
"nano-gpt", //kilocode_change
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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") })),
Expand Down Expand Up @@ -673,6 +682,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 @@ -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]
Expand Down Expand Up @@ -792,6 +803,7 @@ export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
ovhcloud: "ovhCloudAiEndpointsModelId",
inception: "inceptionLabsModelId",
"sap-ai-core": "sapAiCoreModelId",
aihubmix: "aihubmixModelId",
apertis: "apertisModelId",
zenmux: "zenmuxModelId", // kilocode_change
// kilocode_change end
Expand Down Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions packages/types/src/providers/aihubmix.ts
Original file line number Diff line number Diff line change
@@ -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",
}
4 changes: 2 additions & 2 deletions packages/types/src/providers/fireworks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions packages/types/src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
SyntheticHandler,
OVHcloudAIEndpointsHandler,
SapAiCoreHandler,
AihubmixHandler,
ApertisHandler,
// kilocode_change end
ClaudeCodeHandler,
Expand Down Expand Up @@ -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
Expand Down
Loading