Skip to content

feat(cli): add FastRouter as a built-in provider#10207

Open
jatingomnet wants to merge 14 commits into
Kilo-Org:mainfrom
jatingomnet:fastrouter_re_work
Open

feat(cli): add FastRouter as a built-in provider#10207
jatingomnet wants to merge 14 commits into
Kilo-Org:mainfrom
jatingomnet:fastrouter_re_work

Conversation

@jatingomnet
Copy link
Copy Markdown

Adds FastRouter (https://fastrouter.ai) as a built-in provider. FastRouter is
an OpenRouter-compatible gateway that fronts roughly 170 models from
Anthropic, OpenAI, Google, Mistral and others behind one API. Since the wire
format matches OpenRouter, this reuses @openrouter/ai-sdk-provider and
doesn't pull in a new SDK package.

This is a redo of #7969 along the lines @johnnyeric suggested. I rewrote
the integration to mirror the existing apertis provider exactly rather
than the more invasive models.dev path I took the first time. One commit,
no new dependencies, ~80 lines of real logic.

Maintainers

We'll watch issues filed against the FastRouter integration and loop in the
FastRouter team (support@fastrouter.ai) if anything needs them on the
backend side.

Companion Kilo Gateway PR

Kilo-Org/cloud#3226 registers
FastRouter as a known upstream in the gateway. It's a small symbolic add
('fastrouter' in the ProviderId union and a PROVIDERS.FASTROUTER entry)
so anything that later sets gateway: 'fastrouter' resolves cleanly instead
of crashing in OpenRouterInferenceProviderIdSchema.parse.

This kilocode PR doesn't depend on the gateway PR. FastRouter here is a
direct user-API-key integration with no gateway routing. The gateway PR is
the prerequisite for the eventual kilo-exclusive-models story.

What changed

The integration is the apertis pattern, point for point.

packages/opencode/src/kilocode/provider/fastrouter.ts is the new helper:
it hits the public /models endpoint, walks the response, and maps each
model into Kilo's Model shape. I used globalThis.fetch (to dodge the
namespace-shadow trap from last time), a 10s timeout, and || instead of
?? for the numeric/string defaults so that 0, null, and "" all fall
through to sensible values.

packages/kilo-docs/pages/ai-providers/fastrouter.md is the new docs page,
nav and gateways-index entries follow the same shape as the OpenRouter
docs.

The shared files (model-cache.ts, models.ts, transform.ts) get a few
kilocode_change-guarded additions: a fastrouter branch in fetchModels,
the inject+refresh logic in ModelsDev.get, and two more provider IDs added
to the OpenRouter-compatible conditions for prompt_cache_key and
smallOptions. The kilocode-only files (provider.ts,
dialog-provider.tsx) get the loader registration and a priority entry so
FastRouter shows up under Popular in the TUI.

Total: 10 files, +254/-2. The FastRouter icon already lives in the
upstream sprite so no asset work was needed.

The thing I cut from PR1: no models.dev injection, no background refresh
thread. Both were the root cause of the stale-data + ProviderModelNotFoundError
issues the bot flagged. Apertis works fine without them and so does this.

How I tested

CI checks (typecheck, knip, check-kilocode-change) pass locally on a clean
branch. bun test test/provider/ passes 288/289; the one failure
(plugin config enabled and disabled providers are honored) reproduces on
plain main and isn't related.

End-to-end:

  • bun run dev models fastrouter returns the full catalog (works without
    an API key too, since /models is public).
  • FastRouter shows up under Popular in the TUI provider picker.
  • One-shot prompt against fastrouter/anthropic/claude-haiku-4.5 streams
    correctly and the token/cost numbers populate in the footer.
  • Asked the agent to read a file via the read tool. The full tool-use
    round trip works and both legs are visible in the FastRouter dashboard.
  • kilo auth login → fastrouter, then restart with FASTROUTER_API_KEY
    unset. Chat still works, so the auth-store read path is exercised.
  • Switched back to kilo-gateway on a free model to confirm I haven't
    broken anything on the shared OpenRouter-shaped code paths.

I didn't add a unit test for the catalog fetcher. Apertis doesn't have one
and the previous review didn't ask, but it's a 20-line addition if you'd
like one in a follow-up.

On the plugin path

You raised plugins as the recommended alternative in #7969. I thought about
it. The reasons I went back in-tree:

  • Mastra already ships FastRouter as a built-in provider, so there's
    precedent in the broader ecosystem for treating a popular gateway this
    way.
  • No existing OpenCode FastRouter plugin exists today, so users would still
    hit a discovery gap.

If you'd still prefer this as a plugin, I have a scaffold ready and am
happy to close this PR.

Discord

The team is following up and will join soon

One callout for reviewers

The unconditional delete providers["fastrouter"] in models.ts is
deliberate. It guarantees that if models.dev ever ships a fastrouter
entry, the live FastRouter API response always wins. Behavior on a /models
outage: the provider is still injected with an empty model map and
autoload: false, so it stays visible in the picker but selecting it fails
until the next refresh fires
(automatically triggered when the catalog is
empty).

Screenshots for local run with the changes

Screenshot 2026-05-13 at 4 37 04 PM Screenshot 2026-05-13 at 4 37 33 PM Screenshot 2026-05-13 at 4 39 03 PM

FastRouter (https://fastrouter.ai) is an OpenRouter-compatible AI gateway.
The integration mirrors the existing `apertis` provider — a fully formed
provider entry is injected into `ModelsDev.get()` from a public catalog
(`https://go.fastrouter.ai/api/v1/models`), with the model fetcher living
in `packages/opencode/src/kilocode/provider/fastrouter.ts` and the runtime
loader registered through `kiloCustomLoaders`. No new SDK dependency:
FastRouter reuses `@openrouter/ai-sdk-provider` since the wire format is
identical.

Highlights:
- No `models.dev` involvement — `delete providers["fastrouter"]` runs
  unconditionally before injection so any stray upstream entry is always
  overwritten by the live FastRouter API response.
- The `/models` endpoint is public, so the FastRouter provider and its
  catalog appear in the picker even before the user adds an API key.
- API key resolution: `FASTROUTER_API_KEY` env var, `kilo auth fastrouter`,
  or `provider.fastrouter.options.apiKey` in `kilo.json` (matches OpenRouter).
- TUI surfacing: `fastrouter` added to the Kilo `PROVIDER_PRIORITY` map so
  it shows under "Popular" alongside `openrouter`.
- OpenRouter-compatible code paths in `transform.ts` extended to cover
  `model.providerID === "fastrouter"` (reasoning effort, prompt cache key).
- Docs: new `ai-providers/fastrouter.md` page plus nav and gateways-index
  entries.

Diff is intentionally minimal: 50 added / 2 changed lines across 5 shared
files (each guarded by `kilocode_change` markers) plus 2 new files in
kilocode-only directories that need no markers.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jatingomnet
Copy link
Copy Markdown
Author

@johnnyeric @chrarnoldus @marius-kilocode made a new PR for the overall fastrouter integration as a provider as per the comments mentioned earlier

}

if (fastrouterAllowed) {
const fr = yield* Effect.promise(() => ModelCache.fetch("fastrouter").catch(() => ({})))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: FastRouter model fetch fires for all users, not just those who configure FastRouter

Unlike kilo (which requires a key before fetching) or apertis (which gates fetching on having an API key in fetchApertisModels), FastRouter's /models endpoint is public and is hit unconditionally on every ModelsDev.get() call for any user who hasn't explicitly added fastrouter to disabled_providers. This makes FastRouter an opt-out provider rather than opt-in — every Kilo user silently pings go.fastrouter.ai every 5 minutes.

The 5-minute TTL cache prevents hammering, but users who have never configured FastRouter and don't know about it will still generate background traffic. Consider gating the fetch on whether the user has FastRouter configured (key present) or at least documenting this opt-out behavior clearly in the code.


type Models = { data?: Top[] }

export async function fetchFastRouterModels(): Promise<Record<string, any>> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Consider tightening the return type from Record<string, any> to Record<string, unknown> to avoid silently accepting arbitrary values. The apertis fetcher uses the same loose type, but any bypasses TypeScript's type checking on every property access downstream.

@kilo-code-bot
Copy link
Copy Markdown
Contributor

kilo-code-bot Bot commented May 13, 2026

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/src/provider/models.ts 211 FastRouter model fetch is opt-out: fires for all users even without API key configured

SUGGESTION

File Line Issue
packages/opencode/src/kilocode/provider/fastrouter.ts 32 Return type Record<string, any> bypasses TypeScript checks; Record<string, unknown> is safer
Other Observations (not in diff)

Integration correctness: The implementation correctly reuses @openrouter/ai-sdk-provider for the wire format. Key injection flows through the standard provider.key path at provider.ts:1505, so setting apiKey in the custom loader options is not needed. The sdkKey function maps @openrouter/ai-sdk-provider"openrouter", so provider-option remapping for cached messages works correctly for fastrouter too.

usage: { include: true } coverage: FastRouter uses npm: "@openrouter/ai-sdk-provider", so it correctly hits the transform.ts:916 branch and gets token-usage tracking without any additional change.

smallOptions and prompt_cache_key: Both branches were added with kilocode_change markers — correct and consistent.

Memory: No memory leak risk. ModelCache uses module-level Map objects with TTL-based eviction — same pattern as kilo and apertis.

Docs page: The fastrouter.md page is well-structured and covers env var, config file, auth store, capability matrix, routing options, and opt-out. No images are included in the doc page itself — fine since the feature doesn't require screenshots.

Files Reviewed (9 files)
  • .gitignore — no issues
  • packages/kilo-docs/lib/nav/ai-providers.ts — no issues
  • packages/kilo-docs/pages/ai-providers/fastrouter.md — no issues
  • packages/kilo-docs/pages/ai-providers/index.md — no issues
  • packages/opencode/src/kilocode/cli/cmd/tui/component/dialog-provider.tsx — no issues
  • packages/opencode/src/kilocode/provider/fastrouter.ts — 1 suggestion
  • packages/opencode/src/kilocode/provider/provider.ts — no issues
  • packages/opencode/src/provider/model-cache.ts — no issues
  • packages/opencode/src/provider/models.ts — 1 warning
  • packages/opencode/src/provider/transform.ts — no issues

Fix these issues in Kilo Cloud


Reviewed by claude-4.6-sonnet-20260217 · 2,353,556 tokens

jatingomnet and others added 2 commits May 13, 2026 17:35
Addresses the kilo-code-bot warning on Kilo-Org#10207. The previous catalog
injection ran for every install regardless of whether a FastRouter API
key was configured, which meant every Kilo CLI start spawned an HTTP
request to go.fastrouter.ai with no user intent behind it.

Now the inject + ModelCache.fetch + background refresh only fire when
a key resolves from one of the three documented sources: kilo.json
provider.fastrouter.options.apiKey, the auth store (kilo auth login),
or the FASTROUTER_API_KEY env var. Matches how kilo and apertis gate
their fetches inline in models.ts. The runtime path for users with a
key is unchanged.

Also addresses the bot's typing suggestion: fetchFastRouterModels now
returns Record<string, FastRouterModel> with a locally-defined shape
type so future field-name drift is caught at compile time. A local
type keeps us free of the models.ts -> model-cache.ts -> fastrouter.ts
cycle a shared ModelsDev.Model import would create.

Docs page updated to drop the "public catalog visible without a key"
claim that no longer matches the runtime behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jatingomnet
Copy link
Copy Markdown
Author

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/src/provider/models.ts 211 FastRouter model fetch is opt-out: fires for all users even without API key configured

SUGGESTION

File Line Issue
packages/opencode/src/kilocode/provider/fastrouter.ts 32 Return type Record<string, any> bypasses TypeScript checks; Record<string, unknown> is safer
Other Observations (not in diff)
Integration correctness: The implementation correctly reuses @openrouter/ai-sdk-provider for the wire format. Key injection flows through the standard provider.key path at provider.ts:1505, so setting apiKey in the custom loader options is not needed. The sdkKey function maps @openrouter/ai-sdk-provider"openrouter", so provider-option remapping for cached messages works correctly for fastrouter too.

usage: { include: true } coverage: FastRouter uses npm: "@openrouter/ai-sdk-provider", so it correctly hits the transform.ts:916 branch and gets token-usage tracking without any additional change.

smallOptions and prompt_cache_key: Both branches were added with kilocode_change markers — correct and consistent.

Memory: No memory leak risk. ModelCache uses module-level Map objects with TTL-based eviction — same pattern as kilo and apertis.

Docs page: The fastrouter.md page is well-structured and covers env var, config file, auth store, capability matrix, routing options, and opt-out. No images are included in the doc page itself — fine since the feature doesn't require screenshots.

Files Reviewed (9 files)

  • .gitignore — no issues
  • packages/kilo-docs/lib/nav/ai-providers.ts — no issues
  • packages/kilo-docs/pages/ai-providers/fastrouter.md — no issues
  • packages/kilo-docs/pages/ai-providers/index.md — no issues
  • packages/opencode/src/kilocode/cli/cmd/tui/component/dialog-provider.tsx — no issues
  • packages/opencode/src/kilocode/provider/fastrouter.ts — 1 suggestion
  • packages/opencode/src/kilocode/provider/provider.ts — no issues
  • packages/opencode/src/provider/model-cache.ts — no issues
  • packages/opencode/src/provider/models.ts — 1 warning
  • packages/opencode/src/provider/transform.ts — no issues

Fix these issues in Kilo Cloud

Reviewed by claude-4.6-sonnet-20260217 · 2,353,556 tokens

these changes have been made, please check again

@johnnyeric johnnyeric requested a review from markijbema May 14, 2026 10:20
jatingomnet and others added 4 commits May 14, 2026 17:55
The previous link pointed at https://go.fastrouter.ai/ which is the API
host (only /api/v1/... routes resolve). Lychee link-check on CI flagged
the root path as 404. Sign-up actually lives on the marketing site at
https://fastrouter.ai/, so point the docs there.

Co-authored-by: Cursor <cursoragent@cursor.com>
Adds the two FastRouter URLs (https://fastrouter.ai and
https://go.fastrouter.ai/api/v1) referenced from
packages/opencode/src/kilocode/provider/fastrouter.ts. Fixes the
extract-source-links --check CI step.

Generated by `bun run script/extract-source-links.ts`.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jatingomnet
Copy link
Copy Markdown
Author

@markijbema @johnnyeric can we run the workflows again, fixed the issues with the two failing workflows

jatingomnet and others added 4 commits May 14, 2026 23:23
`https://go.fastrouter.ai/api/v1` is the FastRouter inference base URL
referenced by the `FASTROUTER_API` constant in source code. lychee
extracts it via source-links.md and a plain GET against the bare base
returns 404 (only `/v1/models`, `/v1/chat/completions`, etc. are real
paths). Mirror the existing apertis exclusion right above it.

Verified locally with lychee v0.23.0 (the exact version used in CI):
0 errors across packages/kilo-docs/source-links.md and
packages/kilo-docs/pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jatingomnet
Copy link
Copy Markdown
Author

@markijbema @johnnyeric the lychee fix should be done now and i ran the other tests in my system, things were working

@arimesser
Copy link
Copy Markdown

@jatingomnet we submitted the contact form on your site to discuss this in more detail. Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants