diff --git a/.gitignore b/.gitignore index 91bf7230c02..9c058a3149d 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,6 @@ tsconfig.tsbuildinfo # Test Artifacts packages/app/.artifacts/ packages/opencode/.artifacts/ + +# Editor / agent artifacts +.cursor/ diff --git a/packages/kilo-docs/lib/nav/ai-providers.ts b/packages/kilo-docs/lib/nav/ai-providers.ts index 408598f0f52..69ae6526732 100644 --- a/packages/kilo-docs/lib/nav/ai-providers.ts +++ b/packages/kilo-docs/lib/nav/ai-providers.ts @@ -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" }, diff --git a/packages/kilo-docs/lychee.toml b/packages/kilo-docs/lychee.toml index 5d15d940df1..d791a4c60df 100644 --- a/packages/kilo-docs/lychee.toml +++ b/packages/kilo-docs/lychee.toml @@ -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 diff --git a/packages/kilo-docs/pages/ai-providers/fastrouter.md b/packages/kilo-docs/pages/ai-providers/fastrouter.md new file mode 100644 index 00000000000..8569d736e65 --- /dev/null +++ b/packages/kilo-docs/pages/ai-providers/fastrouter.md @@ -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. diff --git a/packages/kilo-docs/pages/ai-providers/index.md b/packages/kilo-docs/pages/ai-providers/index.md index a7c8c05fee3..7af8a6e419c 100644 --- a/packages/kilo-docs/pages/ai-providers/index.md +++ b/packages/kilo-docs/pages/ai-providers/index.md @@ -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 diff --git a/packages/kilo-docs/source-links.md b/packages/kilo-docs/source-links.md index 7bfc0a61504..e7c4f76f6b7 100644 --- a/packages/kilo-docs/source-links.md +++ b/packages/kilo-docs/source-links.md @@ -1,7 +1,7 @@ # Source Code Links - + - @@ -40,6 +40,8 @@ - +- + - - @@ -78,6 +80,9 @@ - +- + + - - diff --git a/packages/opencode/src/kilocode/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/kilocode/cli/cmd/tui/component/dialog-provider.tsx index 1b38f8b0d43..1ddf0717e4a 100644 --- a/packages/opencode/src/kilocode/cli/cmd/tui/component/dialog-provider.tsx +++ b/packages/opencode/src/kilocode/cli/cmd/tui/component/dialog-provider.tsx @@ -51,6 +51,8 @@ export const PROVIDER_PRIORITY: Record = { "github-copilot": 1, openai: 2, google: 3, + openrouter: 4, + fastrouter: 5, } // --------------------------------------------------------------------------- diff --git a/packages/opencode/src/kilocode/provider/fastrouter.ts b/packages/opencode/src/kilocode/provider/fastrouter.ts new file mode 100644 index 00000000000..e73ae26fb40 --- /dev/null +++ b/packages/opencode/src/kilocode/provider/fastrouter.ts @@ -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 + modalities: { + input: Array<"text" | "audio" | "image" | "video" | "pdf"> + output: Array<"text" | "audio" | "image" | "video" | "pdf"> + } +} + +export async function fetchFastRouterModels(): Promise> { + // 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 = {} + + 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 +} diff --git a/packages/opencode/src/kilocode/provider/provider.ts b/packages/opencode/src/kilocode/provider/provider.ts index a7720734aef..da2337763e2 100644 --- a/packages/opencode/src/kilocode/provider/provider.ts +++ b/packages/opencode/src/kilocode/provider/provider.ts @@ -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 @@ -152,6 +153,25 @@ export function kiloCustomLoaders(dep: CustomDep): Record 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 }, + }, + } + }), } } diff --git a/packages/opencode/src/provider/model-cache.ts b/packages/opencode/src/provider/model-cache.ts index 37cb47d53ef..bca682b4f83 100644 --- a/packages/opencode/src/provider/model-cache.ts +++ b/packages/opencode/src/provider/model-cache.ts @@ -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" }) @@ -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 diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index 5aca1a394b8..40b9543e3da 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -193,17 +193,43 @@ export const layer: Layer.Layer 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")) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 35ea19b5f69..8fc506c026a 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -1025,7 +1025,7 @@ export function options(input: { result["promptCacheKey"] = input.sessionID } - if (input.model.providerID === "openrouter") { + if (input.model.providerID === "openrouter" || input.model.providerID === "fastrouter" /* kilocode_change */) { result["prompt_cache_key"] = input.sessionID } if (input.model.api.npm === "@ai-sdk/gateway") { @@ -1061,7 +1061,8 @@ export function smallOptions(model: Provider.Model) { if ( model.providerID === "openrouter" || model.providerID === "llmgateway" || - model.api.npm === "@kilocode/kilo-gateway" // kilocode_change + model.api.npm === "@kilocode/kilo-gateway" || // kilocode_change + model.providerID === "fastrouter" // kilocode_change ) { if (model.api.id.includes("google")) { return { reasoning: { enabled: false } }