Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FreeRouter-Go

Self-hosted, OpenAI-compatible LLM router. Sends each request to the cheapest model capable of handling it, using your own API keys — no middleman markup.

Unlike keyword-only routers, routing here is data-driven: models live in a table (the vademécum) with tier_max / cost / weight / mcp_native, and the cheapest sufficient model wins. A lightweight heuristic classifier is only a fallback for requests that don't declare their own tier.

Inspiration & lineage

This project is a Go reimplementation inspired by openfreerouter/freerouter (TypeScript, MIT) — the 14-dimension classifier and the OpenAI-compatible drop-in idea come from there. FreeRouter itself is forked from ClawRouter.

It is not a 1:1 port. Two concrete bugs in the original were fixed here, and the routing model was changed to match how my own Pillbox agent system already routes:

Original (freerouter, TS) FreeRouter-Go
Keyword match via naive text.includes(kw) (substring) Word-boundary \b…\b match — no more art matching start
Savings baseline hardcoded to claude-opus pricing Baseline = most-expensive enabled model in your own table
Tiers + model choice from static JSON Vademécum in SQLite, CRUD + health-scan at runtime
Routing driven by classifier only Caller may declare tier / requires_mcp; classifier is the fallback

Two patterns are borrowed directly from my existing Go/agent work:

  • scopeCandidatesFor ordering (tier_max ASC, cost ASC, weight DESC) from Pillbox — the cheapest sufficient model wins.
  • requires_toolingrequires_mcp (Pillbox commit 5e2448c): plain tool use must not pin a request to an MCP-native model; only genuine agentic orchestration filters on mcp_native.
  • Message.Content accepts string or array of blocks — a real bug from cc_bridge (2026-06-17): OpenClaw's WhatsApp channel sends content as an array, Telegram as a string.

Stack

Go 1.25 · Gin · Gorm · SQLite (pure-Go glebarez/sqlite, builds with CGO_ENABLED=0 into a single static binary, same as cc_bridge).

Build & run

CGO_ENABLED=0 go build -o freerouter .
cp freerouter.config.example.json freerouter.config.json   # optional; runs on defaults otherwise
./freerouter                                                # FRGO_CONFIG_PATH overrides config location

API

OpenAI-compatible surface + a small admin API for the vademécum.

Method Path Purpose
GET /health liveness
GET /v1/models list routable models (OpenAI shape)
POST /v1/chat/completions classify → pick model → proxy (streaming passes through)
GET /admin/models list vademécum
POST /admin/models add a model
PUT /admin/models/:id update a model
DELETE /admin/models/:id remove a model
POST /admin/models/:id/scan probe endpoint, record health
GET /admin/tokens list issued dev tokens (prefix, enabled, last used)
POST /admin/tokens issue a token {"name":"dev"} → plaintext shown ONCE
POST /admin/tokens/:id/revoke disable a token
POST /admin/tokens/:id/enable re-enable a token
GET /admin/usage usage aggregated by user+model (?user=&model=&from=&to=)
GET /admin/usage/recent raw usage records (?limit=)
GET /admin/secrets list stored provider secrets (masked: name, preview, updated_at)
POST /admin/secrets set/rotate a secret {"name":"DEEPSEEK_KEY","value":"sk-…"}
DELETE /admin/secrets/:name remove a stored secret
GET /admin/keys list the API-key refs models use, and whether each resolves

Every /v1/chat/completions response carries the routing decision in headers: X-FreeRouter-Model, X-FreeRouter-Tier, X-FreeRouter-Savings.

Auth & usage

FreeRouter is a gateway: hand each dev a single frgo_… token instead of every provider's key. Tokens are stored hashed (sha256); the plaintext is shown once at issue time.

  • /v1/* requires Authorization: Bearer frgo_… (per-dev token).
  • /admin/* requires the static admin token (FRGO_ADMIN_TOKEN env or admin_token in config). If unset, /admin is open and a warning is logged.
  • Every billed request records {user, model, tier, prompt/completion/total tokens, cost}. Token counts are the upstream's real numbers; streamed requests get stream_options.include_usage injected so they bill too.

Query "who used how many tokens of which model": GET /admin/usage returns buckets grouped by user + model.

Provider keys

Each model references its upstream key by name (api_key_ref). A ref resolves from the DB secret store first (POST /admin/secrets), then falls back to an environment variable of the same name — so you can add or rotate provider keys through the API (or dashboard) without editing the VM's .env or restarting. GET /admin/keys shows which refs the models need and whether each one currently resolves. Secret values are stored server-side and never returned (listings are masked to a short preview).

Caller hints (data-driven path)

The request body may declare routing intent, which overrides the classifier:

{ "model": "auto", "tier": 2, "requires_mcp": false,
  "messages": [{ "role": "user", "content": "..." }] }

Or use a prompt-prefix mode override: /simple, /medium, /complex, /reason, /max (also bracket form, e.g. [simple]).

License

MIT, matching the upstream FreeRouter / ClawRouter lineage.

About

Data-driven, OpenAI-compatible LLM router in Go (vademécum routing). Inspired by openfreerouter/freerouter.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages