Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,6 @@ tsconfig.tsbuildinfo
# Test Artifacts
packages/app/.artifacts/
packages/opencode/.artifacts/

# Editor / agent artifacts
.cursor/
1 change: 1 addition & 0 deletions packages/kilo-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/fastrouter", children: "FastRouter" },
{ href: "/ai-providers/glama", children: "Glama" },
{ href: "/ai-providers/requesty", children: "Requesty" },
{ href: "/ai-providers/unbound", children: "Unbound" },
Expand Down
1 change: 1 addition & 0 deletions packages/kilo-docs/lychee.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ exclude = [
'^https?://vercel\.link/',
# API base URL, returns 404 when fetched directly
'^https?://api\.apertis\.ai/v1/?$',
'^https?://go\.fastrouter\.ai/api/v1/?$',
# Redirects to authenticated Google Cloud console
'^https?://console\.cloud\.google\.com',
# Consistently times out in CI
Expand Down
118 changes: 118 additions & 0 deletions packages/kilo-docs/pages/ai-providers/fastrouter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
title: "Using FastRouter with Kilo Code | Unified AI API"
description: "Access models from Anthropic, OpenAI, Google, and more through FastRouter — an OpenRouter-compatible AI gateway — by configuring it in Kilo Code. Setup guide for VS Code and the CLI."
sidebar_label: FastRouter
---

# Using FastRouter With Kilo Code

[FastRouter](https://fastrouter.ai) is an OpenRouter-compatible AI gateway that routes requests to many model providers (Anthropic, OpenAI, Google, Mistral, and more) through a single API. Because it speaks the same wire format as OpenRouter, Kilo Code talks to it through the same Vercel AI SDK and supports streaming, tool calling, multimodal inputs, and reasoning out of the box.

**Website:** [https://fastrouter.ai](https://fastrouter.ai)

## Getting an API Key

1. **Sign up:** Go to [fastrouter.ai](https://fastrouter.ai) and create an account.
2. **Create a key:** Generate an API key from the keys page.
3. **Copy the key:** You will paste it into Kilo Code below.

Kilo Code fetches the FastRouter model catalog only after you have added an API key (via env var, `kilo auth login`, or `kilo.json`). Once a key is set, the provider plus the full ~170-model catalog appears in the picker.

## Configuration in Kilo Code

{% tabs %}
{% tab label="VSCode" %}

Open **Settings** (gear icon), go to the **Providers** tab, add FastRouter, and enter your API key.

The extension stores this in your `kilo.json` config file. You can also edit the config file directly — see the **CLI** tab for the file format.

{% /tab %}
{% tab label="CLI" %}

Set the API key as an environment variable or configure it in your `kilo.json` config file:

**Environment variable:**

```bash
export FASTROUTER_API_KEY="your-api-key"
```

**Config file** (`~/.config/kilo/kilo.json` or `./kilo.json`):

```jsonc
{
"provider": {
"fastrouter": {
"env": ["FASTROUTER_API_KEY"],
},
},
}
```

Then set your default model:

```jsonc
{
"model": "fastrouter/anthropic/claude-sonnet-4-5",
}
```

You can also store the key via `kilo auth fastrouter` so it lives in the shared auth store rather than your shell environment.

{% /tab %}
{% /tabs %}

## Supported Capabilities

Because FastRouter mirrors the OpenRouter API surface, all of the following work without any FastRouter-specific configuration:

- **Streaming responses** — token-by-token output for long generations.
- **Tool calling** — both single and parallel tool calls. Kilo Code's built-in tools and any MCP tools you connect work directly.
- **Multimodal inputs** — text, image, audio, video, and PDF where the underlying model supports them. The FastRouter `/models` catalog reports the supported modalities per model and Kilo Code reflects that in the picker.
- **Reasoning effort** — for reasoning-capable models, Kilo Code passes `reasoning: { effort: "minimal" | "low" | "medium" | "high" }` through to FastRouter the same way it does for OpenRouter.

## Provider-Routing Options

FastRouter accepts the same `provider` routing fields as OpenRouter (sort, order, only, data_collection, zdr). To pass them through, set them on the model's `options` in `kilo.json` — anything under `options` is forwarded verbatim:

```jsonc
{
"provider": {
"fastrouter": {
"models": {
"anthropic/claude-sonnet-4-5": {
"options": {
"provider": {
"sort": "price",
"order": ["Anthropic", "Google"],
"only": ["Anthropic"]
}
}
}
}
}
}
}
```

Refer to the FastRouter docs at [https://docs.fastrouter.ai](https://docs.fastrouter.ai) for the full list of accepted fields — Kilo Code does not validate them, so any future option works the moment FastRouter ships it.

## Disabling FastRouter

If you do not want FastRouter to load at all, add it to `disabled_providers` in `kilo.json`:

```jsonc
{
"$schema": "https://app.kilo.ai/config.json",
"disabled_providers": ["fastrouter"]
}
```

This skips both the model fetch and the provider injection.

## Tips and Notes

- **Model IDs** mirror the underlying provider — for example `anthropic/claude-sonnet-4-5`, `openai/gpt-5`, `google/gemini-2.0-pro`. Use `kilo` model picker (or `Ctrl+X m` in the TUI) to browse the live catalog.
- **Pricing** is reported by the `/models` endpoint and shown by Kilo Code's picker; FastRouter charges the underlying model's price.
- **Cache control** uses the OpenRouter-compatible `prompt_cache_key` automatically; you do not need to configure anything.
1 change: 1 addition & 0 deletions packages/kilo-docs/pages/ai-providers/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Run models on your own hardware for privacy and offline use:
Route requests through unified APIs with additional features:

- **[OpenRouter](/docs/ai-providers/openrouter)** - Access multiple providers through one API
- **[FastRouter](/docs/ai-providers/fastrouter)** - OpenRouter-compatible router with low-latency, multimodal, and tool-calling support
- **[Glama](/docs/ai-providers/glama)** - Enterprise AI gateway
- **[Requesty](/docs/ai-providers/requesty)** - Smart routing and fallbacks
- **[Cloudflare AI Gateway](/docs/ai-providers/cloudflare)** - Route providers through your Cloudflare account
Expand Down
7 changes: 6 additions & 1 deletion packages/kilo-docs/source-links.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Source Code Links

<!-- Auto-generated by script/extract-source-links.ts — DO NOT EDIT -->
<!-- 86 unique URLs extracted from extension and CLI source -->
<!-- 88 unique URLs extracted from extension and CLI source -->

- <https://api.apertis.ai/v1>
<!-- packages/opencode/src/provider/model-cache.ts -->
Expand Down Expand Up @@ -40,6 +40,8 @@
<!-- packages/opencode/src/provider/transform.ts -->
- <https://docs.x.ai/docs/guides/reasoning#control-how-hard-the-model-thinks>
<!-- packages/opencode/src/provider/transform.ts -->
- <https://fastrouter.ai>
<!-- packages/opencode/src/kilocode/provider/fastrouter.ts -->
- <https://git-scm.com>
<!-- packages/kilo-vscode/src/agent-manager/WorktreeManager.ts -->
- <https://github.com/anomalyco/opencode/issues/new?template=bug-report.yml>
Expand Down Expand Up @@ -78,6 +80,9 @@
<!-- packages/opencode/src/server/server.ts -->
- <https://gitlab.com>
<!-- packages/opencode/src/provider/provider.ts -->
- <https://go.fastrouter.ai/api/v1>
<!-- packages/opencode/src/kilocode/provider/fastrouter.ts -->
<!-- packages/opencode/src/provider/models.ts -->
- <https://julialang.org/downloads/>
<!-- packages/opencode/src/lsp/server.ts -->
- <https://kilo.ai>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export const PROVIDER_PRIORITY: Record<string, number> = {
"github-copilot": 1,
openai: 2,
google: 3,
openrouter: 4,
fastrouter: 5,
}

// ---------------------------------------------------------------------------
Expand Down
101 changes: 101 additions & 0 deletions packages/opencode/src/kilocode/provider/fastrouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// kilocode_change - new file
//
// FastRouter integration helpers.
//
// FastRouter (https://fastrouter.ai) is an OpenRouter-compatible AI gateway.
// We talk to it directly with the user's `FASTROUTER_API_KEY` — no Kilo
// gateway routing — and reuse the OpenRouter Vercel AI SDK since the wire
// format is identical.

export const FASTROUTER_API = "https://go.fastrouter.ai/api/v1"
export const FASTROUTER_MODELS_URL = `${FASTROUTER_API}/models`
export const FASTROUTER_ENV = "FASTROUTER_API_KEY"

const FETCH_TIMEOUT_MS = 10_000

type Pricing = { prompt?: string; completion?: string }
type Architecture = { input_modalities?: string[]; output_modalities?: string[] }
type Top = {
id: string
name?: string
description?: string
context_length?: number | null
max_completion_tokens?: number | null
pricing?: Pricing
architecture?: Architecture
supported_parameters?: string[]
created?: number
}

type Models = { data?: Top[] }

type FastRouterModel = {
id: string
name: string
family: string
release_date: string
attachment: boolean
reasoning: boolean
temperature: boolean
tool_call: boolean
cost: { input: number; output: number }
limit: { context: number; output: number }
options: Record<string, unknown>
modalities: {
input: Array<"text" | "audio" | "image" | "video" | "pdf">
output: Array<"text" | "audio" | "image" | "video" | "pdf">
}
}

export async function fetchFastRouterModels(): Promise<Record<string, FastRouterModel>> {
// The /models endpoint is public — no Authorization header needed. We use
// `globalThis.fetch` explicitly to avoid accidentally hitting any local
// namespaced fetch helpers from the calling module.
const res = await globalThis.fetch(FASTROUTER_MODELS_URL, {
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
})
if (!res.ok) return {}

const json = (await res.json()) as Models
const out: Record<string, FastRouterModel> = {}

for (const m of json.data ?? []) {
if (!m.id) continue
const inputs = m.architecture?.input_modalities ?? ["text"]
const outputs = m.architecture?.output_modalities ?? ["text"]
const params = m.supported_parameters ?? []

out[m.id] = {
id: m.id,
name: m.name || m.id,
family: m.id.split("/")[0] || "",
// Use ISO date if `created` is a unix timestamp; else empty string.
release_date: m.created ? new Date(m.created * 1000).toISOString().slice(0, 10) : "",
attachment: inputs.includes("image"),
reasoning: params.includes("reasoning") || params.includes("include_reasoning"),
temperature: params.includes("temperature"),
tool_call: params.includes("tools") || params.includes("tool_choice"),
// `||` (not `??`) — FastRouter returns 0/null/empty-string for unknown
// pricing and limits; we want our defaults to apply in those cases too.
cost: {
input: parseFloat(m.pricing?.prompt || "0"),
output: parseFloat(m.pricing?.completion || "0"),
},
limit: {
context: m.context_length || 128_000,
output: m.max_completion_tokens || 16_384,
},
options: {},
modalities: {
input: inputs.filter((x): x is "text" | "audio" | "image" | "video" | "pdf" =>
["text", "audio", "image", "video", "pdf"].includes(x),
),
output: outputs.filter((x): x is "text" | "audio" | "image" | "video" | "pdf" =>
["text", "audio", "image", "video", "pdf"].includes(x),
),
},
}
}

return out
}
20 changes: 20 additions & 0 deletions packages/opencode/src/kilocode/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ProviderID, ModelID } from "@/provider/schema"
import { Effect, Schema } from "effect"
import type { LanguageModelV3 } from "@ai-sdk/provider"
import { mapValues, omit, pickBy } from "remeda"
import { FASTROUTER_API, FASTROUTER_ENV } from "./fastrouter"

/** Default timeout (ms) for provider HTTP requests (connection phase). */
export const REQUEST_TIMEOUT_MS = 120_000 // 2 minutes
Expand Down Expand Up @@ -152,6 +153,25 @@ export function kiloCustomLoaders(dep: CustomDep): Record<string, CustomLoader>
autoload: false,
options: { headers: DEFAULT_HEADERS },
}),

fastrouter: Effect.fnUntraced(function* (input: any) {
const env = yield* dep.env()
const cfg = yield* dep.config()
const auth = yield* dep.auth(input.id)
const cfgKey = cfg?.provider?.["fastrouter"]?.options?.apiKey
const authKey = auth?.type === "api" ? auth.key : undefined
const envKey = env[FASTROUTER_ENV]
const hasKey = Boolean(cfgKey || authKey || envKey)
const hasModels = Object.keys(input.models ?? {}).length > 0

return {
autoload: hasKey && hasModels,
options: {
baseURL: FASTROUTER_API,
headers: { ...DEFAULT_HEADERS },
},
}
}),
}
}

Expand Down
5 changes: 5 additions & 0 deletions packages/opencode/src/provider/model-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { fetchKiloModels, type KiloModelsResult } from "@kilocode/kilo-gateway"
import { Config } from "../config/config"
import { Auth } from "../auth"
import * as Log from "@opencode-ai/core/util/log"
import { fetchFastRouterModels } from "../kilocode/provider/fastrouter"

export namespace ModelCache {
const log = Log.create({ service: "model-cache" })
Expand Down Expand Up @@ -189,6 +190,10 @@ export namespace ModelCache {
const models = await fetchApertisModels(options)
return { models }
}
if (providerID === "fastrouter") {
const models = await fetchFastRouterModels()
return { models }
}
// kilocode_change end

// Other providers not implemented yet
Expand Down
26 changes: 26 additions & 0 deletions packages/opencode/src/provider/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,17 +193,43 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | HttpClie
const get = Effect.fn("ModelsDev.get")(function* () {
const providers = { ...(yield* cachedGet) }
delete providers["kilo"]
// FastRouter ships its own /models endpoint; never trust models.dev for it.
delete providers["fastrouter"]

const config = yield* Effect.promise(() => Config.get())
const disabled = new Set(config.disabled_providers ?? [])
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
const kiloAllowed = (!enabled || enabled.has("kilo")) && !disabled.has("kilo")
const fastrouterAllowed = (!enabled || enabled.has("fastrouter")) && !disabled.has("fastrouter")
const apt = config.provider?.apertis?.options
const aptBase = apt?.baseURL ?? "https://api.apertis.ai/v1"
const aptFetch = {
...(apt?.baseURL ? { baseURL: apt.baseURL } : {}),
}

if (fastrouterAllowed) {
const cfgKey = config.provider?.["fastrouter"]?.options?.apiKey
const authEntry = yield* Effect.promise(() => Auth.get("fastrouter").catch(() => undefined))
const authKey = authEntry?.type === "api" ? authEntry.key : undefined
const envKey = process.env["FASTROUTER_API_KEY"]
const hasKey = Boolean(cfgKey || authKey || envKey)

if (hasKey) {
const fr = yield* Effect.promise(() => ModelCache.fetch("fastrouter").catch(() => ({})))
providers["fastrouter"] = {
id: "fastrouter",
name: "FastRouter",
env: ["FASTROUTER_API_KEY"],
api: "https://go.fastrouter.ai/api/v1",
npm: "@openrouter/ai-sdk-provider",
models: fr,
}
if (Object.keys(fr).length === 0) {
yield* Effect.sync(() => void ModelCache.refresh("fastrouter").catch(() => {}))
}
}
}

if (kiloAllowed) {
const opts = config.provider?.kilo?.options
const auth = yield* Effect.promise(() => Auth.get("kilo"))
Expand Down
Loading