urai-router
Officialby etside
README.md
# Urai Router
**One `keys.txt` → every LLM provider, one local endpoint, automatic key rotation.**
Urai Router is a small, dependency-free Python gateway for people who keep several
LLM API keys. It reads a plain text file, auto-detects each provider, discovers the
models each key unlocks (including **free OpenCode Zen** models), rotates keys on
`401`, `403`, `429` and `5xx`, and exposes everything as:
- an **OpenAI-compatible HTTP API** at `http://127.0.0.1:8787/v1`
- an **MCP stdio server** for OpenCode, Kiro CLI and any MCP client
- an **Agent Skill** (`SKILL.md`) that teaches an agent how to use it
- **generated OpenCode and Kiro configuration** so both tools understand the
provider and model list without manual editing
No database. No Node modules. No Docker. Python 3.9+ standard library only.
```bash
urai-router init # create ~/.config/urai-router/keys.txt
$EDITOR ~/.config/urai-router/keys.txt
urai-router setup all --start # OpenCode + Kiro + skill, start proxy
urai-router doctor
```
---
## Why this exists (audit first)
The project started with an audit of existing open-source routers. The summary is
in [`docs/AUDIT.md`](docs/AUDIT.md). In short:
| Project | Stars | License | What it is | Why not just use it |
| --- | ---: | --- | --- | --- |
| [BerriAI/litellm](https://github.com/BerriAI/litellm) | 62k | MIT-ish | Full AI gateway / SDK | Heavy, needs Postgres/Redis for teams; not a local MCP+skill |
| [songquanpeng/one-api](https://github.com/songquanpeng/one-api) | 37k | MIT | Key redistribution server | Web UI + DB, aimed at resale, not CLI agents |
| [QuantumNous/new-api](https://github.com/QuantumNous/new-api) | 51k | AGPL-3.0 | Fork of one-api | AGPL, DB, server-first |
| [musistudio/claude-code-router](https://github.com/musistudio/claude-code-router) | 38k | MIT | Claude Code routing | Node/TS, Claude-shaped, not MCP-native |
| [jasonxu114514/opencode2api](https://github.com/jasonxu114514/opencode2api) | 1.4k | GPL-3.0 | OpenCode Zen proxy | Go, GPL, Zen-only, no keys.txt/MCP config |
| [routatic/proxy](https://github.com/routatic/proxy) | 983 | AGPL-3.0 | Multi-upstream proxy | AGPL, Go, no MCP/skill surface |
None of them combine all four requirements: **plain `keys.txt`**, **free OpenCode
Zen discovery**, **MCP-native agent tools**, and **automatic OpenCode/Kiro config**.
Urai Router is deliberately the small tool that does those four things.
---
## Install
```bash
git clone https://github.com/etside/Urai-router.git
cd Urai-router
python3 -m pip install --user -e . # or: pipx install .
urai-router --version
```
For a zero-install run: `python3 -m urai_router --help` from the repo.
---
## keys.txt
Default search order:
1. `--keys PATH`
2. `$URAI_KEYS_FILE`
3. `./keys.txt`
4. `~/.config/urai-router/keys.txt`
5. `~/.config/urai/keys.txt`
6. `~/.urai/keys.txt`
7. `~/urai-keys.txt`
8. `~/keys.txt`
```text
# provider:key (provider prefix is optional when the key is recognizable)
opencode:sk-...
openrouter:sk-or-v1-...
groq:gsk_...
deepseek:sk-...
anthropic:sk-ant-...
google:AIza...
xai:xai-...
# labels and weights control rotation
opencode:sk-... # label=main weight=3
opencode:sk-... # label=backup weight=1
```
- `#` starts a comment; `label=`, `weight=`, `@label`, `*weight` are parsed.
- `${ENV_VAR}` is expanded.
- Duplicate keys are ignored.
- An unlabelled key is auto-detected by prefix, then by a 1-token probe.
- `keys.txt` is hot-reloaded when its mtime changes.
- The file is never sent anywhere and never written into logs; `keys list` shows
masked values only.
Keep it private:
```bash
chmod 600 ~/.config/urai-router/keys.txt
```
---
## Providers understood automatically
Registered out of the box:
`opencode` (Zen), `openrouter`, `openai`, `deepseek`, `groq`, `anthropic`,
`google` (Gemini OpenAI endpoint), `xai`, `nvidia`, `moonshot`, `mistral`,
`together`, `fireworks`, `cerebras`, `huggingface`, `zai`, `minimax`,
`deepinfra`, `perplexity`, `github` models, `nebius`, `sambanova`, `cohere`,
and local `ollama` / `lmstudio` / `llamacpp`.
Add any OpenAI-compatible provider in `~/.config/urai-router/config.json`:
```json
{
"providers": {
"my-gateway": {
"name": "My Gateway",
"base_url": "https://gateway.example.com/v1",
"api_style": "openai",
"default_models": ["my-model"]
}
}
}
```
Then use `my-gateway:key...` in `keys.txt`.
### OpenCode Zen free models
`GET https://opencode.ai/zen/v1/models` is public and returns pricing metadata.
Urai Router reads it, marks `cost.text.input.price == 0` as free, and reads the
per-model `api` block so OpenAI-style models use `/chat/completions` while
Anthropic-style models use `/messages` (translated both ways, including tools
and SSE streaming).
You still need one Zen API key to call the models; the catalogue itself is public.
Free rows are time-limited by OpenCode, so run `urai-router sync` periodically.
---
## Routing
Model reference syntax:
| Reference | Meaning |
| --- | --- |
| `auto` / `best` | free models first, then any usable model |
| `free` / `urai/free` | free models only |
| `opencode/deepseek-v4.1-flash-free` | explicit provider + model |
| `openrouter/vendor/model:free` | OpenRouter model ids keep their slash |
| `deepseek-chat` | resolved across the catalogue when unambiguous |
| alias | any key in `config.json` → `aliases` |
Failover policy:
| Upstream | Action |
| --- | --- |
| `401` / `403` | mark key invalid, rotate immediately |
| `429` | honour `Retry-After`, exponential cooldown, rotate |
| `408` / `425` / `5xx` / `529` | short exponential cooldown, rotate |
| `400` / `422` | request or model problem — do not blame the key |
| `404` | try the model's alternative endpoint, then report |
Cooldowns and counters persist in
`~/.local/state/urai-router/keys-state.json` (no key material inside).
---
## Use as an OpenAI-compatible API
```bash
urai-router serve # foreground: 127.0.0.1:8787
urai-router proxy start # background, writes pid/port + proxy.log
urai-router proxy status
urai-router proxy stop
```
```bash
curl -s http://127.0.0.1:8787/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"auto","messages":[{"role":"user","content":"say pong"}]}'
```
```python
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:8787/v1", api_key="urai-local")
print(client.chat.completions.create(
model="auto",
messages=[{"role": "user", "content": "hello"}],
).choices[0].message.content)
```
Endpoints:
- `GET /health`
- `GET /v1/models` (`?free=1`, `?provider=opencode`, `?refresh=1`)
- `POST /v1/chat/completions` (streaming supported)
- `GET /v1/urai/stats` · `/v1/urai/keys` (masked) · `/v1/urai/providers`
- `GET /v1/urai/explain?model=auto`
- `POST /v1/urai/reload` · `/v1/urai/test`
Bind to `127.0.0.1` by default. If you expose the port, set a token:
```bash
urai-router serve --token "$(openssl rand -hex 24)"
```
---
## OpenCode integration
```bash
urai-router setup opencode --set-default
```
This merges the following into `~/.config/opencode/opencode.jsonc` (or `.json`),
preserving existing settings and writing a timestamped backup:
- a custom provider `urai-router` using `@ai-sdk/openai-compatible`
- `options.baseURL = http://127.0.0.1:8787/v1`
- a `models` map generated from every usable/free model (context + output limits,
★ marks free rows)
- an `mcp.urai-router` local server entry
Then restart OpenCode. Models appear as
`urai-router/opencode/deepseek-v4.1-flash-free`, `urai-router/auto`, etc.
`--set-default` points `model` and `small_model` at the first free Zen model.
Refresh after adding keys or when Zen changes its free list:
```bash
urai-router sync
```
Project-local config instead of global:
```bash
urai-router setup opencode --project
```
Dry run and diff-friendly export:
```bash
urai-router setup opencode --dry-run
urai-router export opencode
```
---
## Kiro CLI integration
```bash
urai-router setup kiro
```
Writes:
- `~/.kiro/settings/mcp.json` → `mcpServers.urai-router` (stdio, `urai-router mcp`)
- `~/.kiro/agents/urai-router.json` → an agent that includes the MCP server and
the `urai_*` tools, with `@urai-router` in `tools`
Verify:
```bash
kiro-cli mcp list
kiro-cli chat --agent urai-router
```
Kiro keeps its own model backend; the router is used through the MCP tools
(`urai_chat`, `urai_models`, `urai_health`, ...). That is intentional: Kiro does
not expose a custom OpenAI provider setting.
---
## MCP server
`urai-router mcp` speaks JSON-RPC over stdio (newline-delimited, MCP 2025-06-18,
also compatible with 2025-03-26 / 2024-11-05).
Tools:
| Tool | Purpose |
| --- | --- |
| `urai_chat` | chat completion through the router, key rotation included |
| `urai_models` | merged catalogue, `free_only` supported |
| `urai_keys` | masked health / cooldowns |
| `urai_health` | keys + catalogue + proxy status |
| `urai_stats` | alias for health |
| `urai_explain` | show routing for a model |
| `urai_refresh` | reload keys and catalogues |
| `urai_ensure_proxy` | start the local HTTP proxy and return its URL |
| `urai_check` | probe keys |
| `urai_setup` | generate OpenCode/Kiro/skill config (`dry_run` default true) |
Resources: `urai://config`, `urai://keys`, `urai://providers`, `urai://models`,
`urai://stats`.
MCP client snippet (OpenCode style):
```jsonc
{
"mcp": {
"urai-router": {
"type": "local",
"command": ["urai-router", "mcp"],
"enabled": true
}
}
}
```
MCP client snippet (Kiro style):
```json
{
"mcpServers": {
"urai-router": {
"command": "urai-router",
"args": ["mcp"],
"disabled": false,
"timeout": 30000
}
}
}
```
---
## Skill
`urai-router setup skills` copies the Agent Skill to:
- `~/.config/opencode/skills/urai-router/SKILL.md`
- `~/.agents/skills/urai-router/SKILL.md`
- `~/.claude/skills/urai-router/SKILL.md`
Use `--project` to also write `.opencode/`, `.agents/` and `.claude/` variants.
The skill teaches an agent when to call `urai_*`, how to route free models, and
how to report failover without leaking keys.
---
## CLI reference
```text
urai-router init [--path PATH] [--force]
urai-router serve [--host H] [--port P] [--token T] [--no-detect] [--no-preload]
urai-router mcp
urai-router keys list|check|detect
urai-router models [--free] [--provider P] [--refresh] [--json]
urai-router setup [opencode|kiro|skills|all] [--project] [--dry-run] [--set-default] [--start]
urai-router sync [--set-default]
urai-router doctor [--json]
urai-router test [--model M] [--prompt TEXT] [--json]
urai-router explain [--model M]
urai-router export [opencode|kiro|env|mcp|providers]
urai-router proxy start|status|stop
urai-router version
```
Global flags: `--keys`, `--config`, `--state-dir`, `-v`.
---
## Configuration
`~/.config/urai-router/config.json` (created on demand):
```json
{
"host": "127.0.0.1",
"port": 8787,
"token": "",
"max_attempts": 8,
"default_model": "auto",
"prefer_free": true,
"catalog_ttl": 900,
"zen_ttl": 300,
"request_timeout": 120,
"cooldowns": {
"rate": 60,
"invalid": 1800,
"server": 20,
"network": 15,
"max": 3600
},
"aliases": {
"fast": "opencode/deepseek-v4.1-flash-free"
},
"providers": {},
"proxy": { "cors": true, "log_requests": true }
}
```
State and cache:
- `~/.local/state/urai-router/keys-state.json` — health, cooldowns (no secrets)
- `~/.local/state/urai-router/proxy.json` — pid, port, url
- `~/.local/state/urai-router/proxy.log`
- `~/.cache/urai-router/catalog.json` — model metadata cache
---
## Architecture
```text
keys.txt ──▶ KeyStore ──▶ Router ──▶ Provider adapter ──▶ upstream
│ │ │
│ ├─ model resolution ├─ OpenAI chat/completions
│ ├─ retry / cooldown └─ Anthropic messages (+SSE)
│ └─ free-first auto
▼
proxy (:8787/v1) MCP stdio OpenCode config Kiro config SKILL.md
```
- `urai_router/providers.py` — registry, catalogue parsing, request building
- `urai_router/keys.py` — parsing, rotation, cooldowns, auto-detection, state
- `urai_router/router.py` — model resolution, failover, streaming
- `urai_router/proxy.py` — HTTP server + background process management
- `urai_router/mcp_server.py` — MCP tools/resources/prompts
- `urai_router/config_writer.py` — OpenCode/Kiro/skill writers
- `urai_router/adapters.py` — OpenAI ↔ Anthropic translation
---
## Security
- Keys are read from a local file and used only in `Authorization` / `x-api-key`
headers to the provider you configured.
- Logs and API responses only contain masked keys or labels.
- `keys-state.json` and `catalog.json` never contain key material.
- `.gitignore` excludes `keys.txt`, `.env`, and state.
- The proxy binds to loopback by default; add `--token` if you change that.
- `doctor` warns when the keys file is group/world-readable.
Report vulnerabilities privately; see `SECURITY.md`.
---
## GitHub Actions
- **CI** (`.github/workflows/ci.yml`): compile, unit tests against a local fake
upstream, package build, zipapp smoke test. No network keys required.
- **Release** (`.github/workflows/release.yml`): on a `v*` tag, builds sdist,
wheel and `urai-router.pyz`, then creates a GitHub release with `GH_TOKEN`
(the token supplied by GitHub Actions). Manual `workflow_dispatch` supported.
Create the repo and push (what was used here):
```bash
gh repo create etside/Urai-router --public --source=. --remote=origin --push
```
---
## Troubleshooting
| Symptom | Fix |
| --- | --- |
| `no usable route` | add keys, then `urai-router keys list` and `doctor` |
| models empty | `urai-router models --refresh`; check network |
| OpenCode shows no models | `urai-router sync`, restart OpenCode |
| all keys cooling | add keys or wait; check `urai-router keys list` |
| 401 from every key | `urai-router keys check`, fix/remove invalid keys |
| Zen free list changed | `urai-router sync` |
| Kiro doesn't see MCP | `kiro-cli mcp list`; re-run `setup kiro` |
| proxy not reachable | `urai-router proxy status`; see `proxy.log` |
---
## License
MIT. See `LICENSE`.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues