# DodoRouter β complete documentation
> Every page under https://dodorouter.com/docs, concatenated as plain Markdown in reading order. See https://dodorouter.com/llms.txt for a condensed, link-only index instead of this full text.
---
# Overview
Source: https://dodorouter.com/docs/
# DodoRouter docs
DodoRouter is an LLM proxy and router. Point any OpenAI-, Anthropic-, or Responses-API client at it once, and it forwards your requests through a fallback chain of providers you configure β retrying the next provider automatically on rate limits, outages, or context overflows β while logging every request.
π€ Reading this with an AI agent?
Fetch https://dodorouter.com/llms.txt for a condensed, link-based index, or https://dodorouter.com/llms-full.txt for every page on this site concatenated as one plain-Markdown file. Every individual page also has a raw-Markdown twin at its own URL with .md appended β e.g. https://dodorouter.com/docs/quickstart.md β so an agent can fetch just the page it needs.
## Jump in
Quickstart β
Register, create a router, send your first request. ~5 minutes.
Self-hosting β
Clone, configure, and run your own instance.
API reference β
Endpoints, auth, request/response formats, errors.
Connect a coding agent β
Claude Code, OpenCode, Forge, Codex CLI β verified configs.
## How a request flows
A DodoRouter deployment has one account, any number of **routers**, and a pool of **provider keys** (your OpenAI, Anthropic, z.ai, Moonshot, etc. credentials). Each router has its own DodoRouter API key and its own ordered list of **routing steps** β ordered pairs of (provider, model) that DodoRouter tries in sequence.
Your client (a script, an SDK, or a coding-agent CLI like Claude Code) talks to DodoRouter using a completely standard OpenAI or Anthropic request. DodoRouter ignores the `model` field you send and instead dispatches to routing step 1. If that provider call fails in a way that's safe to retry (rate limit, 5xx, timeout, context overflow, auth error), DodoRouter automatically moves to step 2, then step 3, and so on, only returning an error once every step has failed. The response is converted back to the format your client expects, so the client never has to know which provider actually served the request.
```
your client β DodoRouter (/r/{router}/v1/...)
β step 1: provider A / model X β 429 rate limited
β step 2: provider B / model Y β 200 success
β response, converted back to your client's format
```
DodoRouter is a Phoenix (Elixir) application. It ships as a hosted service at [api.dodorouter.com](https://api.dodorouter.com) and as self-hostable open source at [github.com/foxwise-ai/dodorouter](https://github.com/foxwise-ai/dodorouter).
---
# Quickstart (hosted)
Source: https://dodorouter.com/docs/quickstart/
# Quickstart (hosted)
This walks through the exact steps on the hosted instance at `api.dodorouter.com`. Everything here also works identically on a [self-hosted](/docs/self-hosting/) instance β just swap the base URL.
## 1. Create an account
Go to [api.dodorouter.com/users/register](https://api.dodorouter.com/users/register) and enter your email. DodoRouter uses passwordless, magic-link login: there's no password to set at this step, just an email address and agreement to the Terms of Service.
DodoRouter emails you a one-time login link. Open it and click **Continue to DodoRouter** β this both confirms your account and logs you in. (If you'd rather use a password day-to-day, you can set one later under **Settings**; magic-link login keeps working either way.)
## 2. Create a router
On first login you're dropped straight into **New Router**. Give it a name (e.g. `my-agent`) β a URL-safe slug is derived from it automatically. Saving generates your router's API key, shown **once** in a banner. Copy it now; DodoRouter only ever stores a salted hash, so if you lose it you'll need to regenerate a new one from the **API Keys** page (which invalidates the old one).
## 3. Add a provider API key
Go to **Providers**, pick a provider (e.g. OpenAI or Anthropic), click **Add Key**, and paste in your API key from that provider. DodoRouter verifies it in the background and shows a green check once confirmed valid.
## 4. Add a routing step
Open your router, and under **Routing Chain** click **Add Step**. Choose the provider, then the model (pick from the list, synced from live provider catalogs, or choose **Customβ¦** to type an exact model id). Add a second and third step with different providers if you want automatic fallback β that's the whole point.
## 5. Send your first request
Use the router's API key as a Bearer token. The `model` value you send doesn't matter β DodoRouter always uses whatever model is configured on the routing step β so `"default"` is a fine placeholder.
```bash
curl https://api.dodorouter.com/r/my-agent/v1/chat/completions \
-H "Authorization: Bearer sk-dodo-YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "default",
"messages": [{"role": "user", "content": "Hello!"}]
}'
```
Verified response shape (this exact request was run against a live DodoRouter instance):
```json
{
"choices": [{
"finish_reason": "stop",
"index": 0,
"message": { "content": "Hello! How can I help you today?", "role": "assistant" }
}],
"model": "gpt-5.5",
"usage": { "completion_tokens": 10, "prompt_tokens": 13, "total_tokens": 23,
"prompt_tokens_details": { "cached_tokens": 0 } }
}
```
Every request also gets logged in real time under **Logs**, with full request/response bodies, timing breakdown, token usage, and cost.
---
# Self-hosting
Source: https://dodorouter.com/docs/self-hosting/
# Self-hosting
DodoRouter is a standard Phoenix 1.8 application backed by PostgreSQL. Source: [github.com/foxwise-ai/dodorouter](https://github.com/foxwise-ai/dodorouter). License terms are covered in [License](#license) below β in short, free to self-host and modify, but you can't resell it as your own competing hosted service.
## Prerequisites
- Elixir 1.15+ and a matching Erlang/OTP
- PostgreSQL 12+
- An [Infisical](https://infisical.com) project (free tier is fine) β see the callout below, this is currently required, not optional
β Infisical is currently required
Every provider API key you add (OpenAI, Anthropic, z.ai, β¦) is stored via Infisical, a secrets manager β there is currently no local/plaintext fallback in the code. Without INFISICAL_TOKEN and INFISICAL_PROJECT_ID set, the app boots fine and the dashboard loads, but clicking Add Key on the Providers page will fail with "Failed to store API key securely" β so you won't be able to configure any provider, and no requests will succeed. Sign up at Infisical, create a project, create a Machine Identity with a Universal Auth client, and use that identity's access token as INFISICAL_TOKEN.
## 1. Clone and install
```bash
git clone https://github.com/foxwise-ai/dodorouter.git
cd dodorouter
mix deps.get
```
## 2. Configure environment variables
For local development, export these before starting the server (e.g. in `.envrc` with [direnv](https://direnv.net), or a `.env` you source):
```bash
# Database β defaults match config/dev.exs; override if yours differs
export DB_USERNAME=postgres
export DB_PASSWORD=postgres
export DB_HOSTNAME=localhost
export DB_PORT=5432
export DB_NAME=dodo_router_dev
# Infisical β required, see callout above
export INFISICAL_TOKEN=""
export INFISICAL_PROJECT_ID=""
export INFISICAL_ENVIRONMENT=dev # optional, defaults to "prod"
```
## 3. Set up the database
```bash
mix ecto.setup
```
## 4. Start the server
```bash
mix phx.server
```
Visit `http://localhost:4000`. Register an account, confirm via the magic-link email β in dev, DodoRouter uses Swoosh's local mailbox instead of sending real email, so open `http://localhost:4000/dev/mailbox` to read it. From there, follow the same steps as the [Quickstart](/docs/quickstart/).
Verified: `GET /health` returns `{"status":"ok"}` once the DB connection is healthy, and `GET /api/version` returns the running app version, e.g. `{"version":"0.1.75"}` β handy for confirming the app booted and for health-check probes.
## Production environment variables
| Variable | Required | Notes |
|---|---|---|
| `DATABASE_URL` | Yes | e.g. `ecto://user:pass@host/dodo_router_prod` |
| `SECRET_KEY_BASE` | Yes | Generate with `mix phx.gen.secret` |
| `PHX_HOST` | Yes | Your public domain |
| `PHX_SERVER` | Yes | Set to `true` to actually start the endpoint under a release |
| `PORT` | No | Defaults to 4000 |
| `INFISICAL_TOKEN` | Yes* | *Required for provider-key storage to work at all (see callout above) |
| `INFISICAL_PROJECT_ID` | Yes* | Paired with the token above |
| `INFISICAL_ENVIRONMENT` | No | Defaults to `prod` |
| `RESEND_API_KEY` | No | Transactional email (magic links). Without it in prod, mail simply won't send |
| `EMAIL_FROM` | No | Defaults to `noreply@dodorouter.com` |
| `STRIPE_SECRET_KEY` | No | Only set this if you want the $19/mo billing paywall enabled. Unset = billing disabled, everyone has full access |
| `STRIPE_WEBHOOK_SECRET` | No | Required only alongside `STRIPE_SECRET_KEY` |
| `ECTO_IPV6` | No | Set to `true` to enable IPv6 for the DB socket |
| `POOL_SIZE` | No | Ecto pool size, defaults to 10 |
For deploy specifics (hot code upgrades, systemd, Castle release management), see the project's own `AGENTS.md` in the repo β those are maintainer-facing operational details rather than user-facing setup, so we won't duplicate them here. See also [Deployment](/docs/deployment/) for a minimal production boot sequence.
## License
DodoRouter is source-available under the [O'Saasy License](https://osaasy.dev). In short: you're free to use, modify, self-host, and even sell copies of it β the one thing you can't do is turn around and offer it as a competing hosted/managed service where the software itself is the product. See the full license text in the repository for the exact terms.
---
# Core Concepts
Source: https://dodorouter.com/docs/concepts/
# Core concepts
## Routers
A router is the unit a client connects to. Each has a unique slug (used in the URL path), its own API key, and its own routing chain. Most people create one router per application or environment (e.g. `my-agent-prod`, `my-agent-staging`), since the routing chain, session grouping, and logs are all scoped per router.
## Routing steps & fallback
Each router has an ordered list of steps. A step is: provider + model, optionally a temperature/max_tokens override, a reasoning-effort level, and which of your provider keys to use. DodoRouter tries step 1 first. If it fails with an error class that's safe to retry β rate limited, server error, timeout, auth error, model unavailable, or context overflow β it moves to the next step, carrying over any partial content a streaming provider had already produced so the next provider continues rather than starting over. It gives up and returns an error only once every step has failed.
There's one router-level toggle that changes this: **"Skip fallback on context overflow"**. Off by default (a request too big for step 1 tries step 2, which might have a bigger context window). Turn it on if you'd rather fail fast on oversized requests than silently burn quota retrying them against every provider in the chain.
## Provider keys vs. router API keys
These are two different kinds of key and it's easy to mix them up:
- **Provider keys** (Providers page) β your upstream credentials: an OpenAI key, an Anthropic key, a z.ai key, etc. DodoRouter uses these to call the actual LLM provider. You attach one to each routing step.
- **Router API keys** (API Keys page) β the key *your* client uses to call DodoRouter itself, in the form `sk-dodo-β¦`. One per router. Regenerating it immediately invalidates the old one.
## Plan types: standard vs. coding subscriptions
z.ai and Moonshot (Kimi) both offer a flat-rate "coding" subscription plan alongside their regular pay-as-you-go API, each with its own base URL and, for cost tracking, $0 marginal per-token pricing. When adding a routing step for these providers, you'll see both a standard key option and a "coding" one; pick whichever matches the credential you added on the Providers page.
## Reasoning effort
Reasoning models expose "how hard to think" differently per provider β a string like `reasoning_effort` for OpenAI, a token budget for Anthropic extended thinking, a thinking budget for Gemini, or a plain on/off toggle for DeepSeek/z.ai/Moonshot-style models. Set a canonical level per routing step β `none, minimal, low, medium, high, xhigh, max` β and DodoRouter translates it into whatever the upstream provider actually expects. If your client already sends its own reasoning parameter in the request, that always wins over the routing step's default.
## Sessions
Sessions group related requests (e.g. every call in one coding-agent conversation) under a shared id, purely for the Logs UI β DodoRouter doesn't use sessions for routing decisions. Send an `X-Session-Id` header (and optionally `X-Session-Name` for a human-readable label) with each request, and they'll show up grouped under **Sessions** on that router. The header name is configurable per router (Router β Edit β "Session header", default `x-session-id`) β useful if a client library already sends its own conversation-id header under a different name and you'd rather reuse that than add a new one.
## Recordings
A recording is an explicit start/stop capture window, separate from sessions β useful for bracketing "everything that happened during this one debugging run" or a scripted test, regardless of session id. Start one from the dashboard or via `POST /recordings/start`, make your requests, stop it, then review exactly that batch under Recordings.
## Logs & replay
Every proxied request is logged with full request/response bodies (large payloads are truncated for storage, flagged as such), token usage, cost, latency breakdown (total / provider time / TTFB / upload), and the full fallback trace if more than one step was attempted. From any log you can **Replay** the same conversation against a different provider/model/reasoning-effort and get a side-by-side diff β handy for "would a cheaper model have handled this the same way?" comparisons.
---
# API Reference
Source: https://dodorouter.com/docs/api/
# API reference
## Base URL & authentication
All proxy endpoints are scoped per router: `{base_url}/r/{router_slug}/v1/β¦`. In every example in this reference, `{base_url}` is `https://api.dodorouter.com` for the hosted service (or your own domain if self-hosted), and `{router}` is your router's slug β swap both in before running any command.
Authenticate with your router's API key, either as a Bearer token or, if your client library doesn't support custom bearer tokens, as `x-api-key`:
```http
Authorization: Bearer sk-dodo-YOUR_KEY
# or:
x-api-key: sk-dodo-YOUR_KEY
```
A missing or invalid key returns `401` with `{"error":{"message":"Invalid API key",β¦}}` (verified). On the hosted service, a router whose owner has no active subscription returns `402` instead β not applicable to self-hosted instances unless you've enabled billing yourself.
## Endpoints
| Method & path | Purpose |
|---|---|
| `POST /r/{slug}/v1/chat/completions` | [OpenAI Chat Completions](/docs/api/chat-completions/) format, sync or streaming |
| `POST /r/{slug}/v1/messages` | [Anthropic Messages](/docs/api/messages/) format, sync or streaming |
| `POST /r/{slug}/v1/responses` | [OpenAI Responses API](/docs/api/responses/) format, sync or streaming |
| `GET /r/{slug}/v1/models` | Returns a single synthetic model entry named after the router slug (for clients that require a models list before use) |
| `POST /r/{slug}/recordings/start` | Start a recording, optional `{"name":"β¦"}` body |
| `GET /r/{slug}/recordings/active` | Currently active recording, or 404 |
| `POST /r/{slug}/recordings/active/stop` | Stop the active recording |
| `POST /v1/chat/completions` | Legacy endpoint, identical behavior to the router-scoped chat/completions route above, kept for backwards compatibility |
| `GET /health` | No auth. `{"status":"ok"}`, 503 if DB is unreachable or the instance is draining for a hot upgrade |
| `GET /api/version` | No auth. Running app version |
Every proxy response also carries `x-request-id`, `x-timing-total-ms`, and `x-timing-provider-ms` response headers.
## The `model` field is ignored
This trips people up, so it's worth stating plainly: whatever `model` you put in the request body is discarded. DodoRouter always substitutes the model configured on the routing step it's currently attempting. Send `"default"`, your router's slug, or anything else β it makes no functional difference. To control which model actually answers, edit the router's [routing chain](/docs/concepts/#routing-steps-fallback), not the request.
## Error format
| Status | When |
|---|---|
| `400` | No routing steps configured on the router, or every attempted step failed with a context-overflow error (request too big for every configured model) |
| `401` | Missing/invalid router API key |
| `402` | Hosted service only: router's owner has no active subscription |
| `502` | Every routing step was attempted and every one failed (non-context-overflow reasons) |
| `200` + SSE error event | Streaming request where at least one chunk was already sent to the client before every step failed β can't change the HTTP status mid-stream, so the error arrives as an SSE event in the format matching the endpoint (OpenAI/Anthropic/Responses) |
Verified: a bad API key against a running instance returns HTTP `401`.
---
# Chat Completions (OpenAI format)
Source: https://dodorouter.com/docs/api/chat-completions/
# Chat Completions (OpenAI format)
```bash
curl {base_url}/r/{router}/v1/chat/completions \
-H "Authorization: Bearer sk-dodo-YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "default",
"stream": false,
"messages": [{"role": "user", "content": "Reply with exactly the word: pong"}]
}'
```
Verified live response:
```json
{
"choices": [{"finish_reason": "stop", "index": 0,
"message": {"content": "pong", "role": "assistant"}}],
"model": "gpt-5.5",
"usage": {"completion_tokens": 5, "prompt_tokens": 13, "total_tokens": 18,
"prompt_tokens_details": {"cached_tokens": 0}}
}
```
## Streaming
With `"stream": true`, the response is a standard OpenAI-style SSE stream of `chat.completion.chunk` deltas terminated by `data: [DONE]` β verified working, including tool-call streaming.
---
# Messages (Anthropic format)
Source: https://dodorouter.com/docs/api/messages/
# Messages (Anthropic format)
Full bidirectional conversion, including tool_use / tool_result blocks, system prompts (string or block array, with `cache_control` preserved), and multi-block content.
```bash
curl {base_url}/r/{router}/v1/messages \
-H "Authorization: Bearer sk-dodo-YOUR_KEY" \
-H "Content-Type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d '{
"model": "claude-sonnet-4-5",
"max_tokens": 100,
"messages": [{"role": "user", "content": "Reply with exactly the word: pong"}]
}'
```
Verified live response:
```json
{
"id": "msg_5a8d090908f8feb21be52ef0", "type": "message", "role": "assistant",
"content": [{"type": "text", "text": "pong"}],
"model": "gpt-5.5", "stop_reason": "end_turn", "stop_sequence": null,
"usage": {"input_tokens": 13, "output_tokens": 5}
}
```
Note the `model` in the response is whatever the winning routing step actually used (`gpt-5.5` here) β not the `claude-sonnet-4-5` requested, consistent with [the model field being ignored](/docs/api/#the-model-field-is-ignored). This is exactly how you'd point [Claude Code](/docs/integrations/claude-code/) at non-Anthropic models through DodoRouter.
---
# Responses API
Source: https://dodorouter.com/docs/api/responses/
# Responses API
Used by Codex-style clients. Both sync and streaming are supported and verified:
```bash
curl {base_url}/r/{router}/v1/responses \
-H "Authorization: Bearer sk-dodo-YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "default", "input": "Reply with exactly the word: pong"}'
```
## Streaming event sequence
With `"stream": true`, DodoRouter emits the full item lifecycle a strict Responses-API client expects:
`response.created` β `response.output_item.added` β `response.content_part.added` β one or more `response.output_text.delta` β `response.output_text.done` β `response.content_part.done` β `response.output_item.done` β `response.completed`.
Verified with a real [Codex CLI](/docs/integrations/codex-cli/) streaming run β text renders live in the terminal. An earlier version of this endpoint skipped the `output_item.added` / `content_part.added` events, which caused strict clients to log `OutputTextDelta without active item` and stall; that gap is fixed.
---
# Supported Providers
Source: https://dodorouter.com/docs/providers/
# Supported providers
Each row is a distinct adapter. Where a provider offers both a pay-as-you-go API and a flat-rate coding subscription, they're listed as separate key types since they use different base URLs and pricing.
| Provider | Key type(s) | Auth | Notes |
|---|---|---|---|
| OpenAI | `openai` | Bearer | GPT-4o/4.1, o1/o3, GPT-5.x |
| OpenAI Codex (ChatGPT) | `openai-codex` | OAuth (device flow, in-app) | Uses your ChatGPT plan's included usage rather than per-token billing; cost tracked as $0 |
| Anthropic | `anthropic` | x-api-key | Claude models |
| Anthropic (Claude Pro/Max) | `anthropic_oauth` | Bearer setup-token | Paste a token from `claude setup-token` (run the Claude Code CLI's own command); flat-rate, no per-token cost |
| Google Gemini | `google` | Query param | Gemini 1.5/2.x family |
| Groq | `groq` | Bearer | Fast inference on open models |
| Mistral | `mistral` | Bearer | Mistral/Codestral family |
| Cohere | `cohere` | Bearer | Command family, Cohere's v2 chat API |
| DeepSeek | `deepseek` | Bearer | deepseek-chat, deepseek-reasoner |
| Moonshot / Kimi | `moonshot` | Bearer | Pay-as-you-go API |
| Moonshot / Kimi Code | `moonshot_coding` | Bearer | Flat-rate coding subscription plan, $0 marginal cost |
| xAI (Grok) | `xai` | Bearer | Grok family |
| z.ai (GLM) | `zai_standard` | Bearer | Pay-as-you-go API |
| z.ai Coding Plan | `zai_coding` | Bearer | Flat-rate coding subscription plan, $0 marginal cost |
Every adapter normalizes provider-specific quirks so the fallback chain "just works": each provider signals a context-window overflow differently (Anthropic returns a 400 with "prompt is too long" in the message; z.ai returns HTTP 200 with `finish_reason: "model_context_window_exceeded"`; OpenAI uses an explicit error code) and DodoRouter detects all of these and treats them the same way β as a fallback-eligible error, unless you've turned on ["Skip fallback on context overflow"](/docs/concepts/#routing-steps-fallback). Cache-token reporting (for cost and cache-hit-rate tracking) is likewise normalized across each provider's own field names.
---
# Models & Pricing
Source: https://dodorouter.com/docs/models/
# Models & pricing
DodoRouter syncs a model catalog from [models.dev](https://models.dev), an open model database, giving you context-window size, per-million-token pricing (input, output, cache read, cache write), capability flags (vision, function calling, prompt caching, etc.), and which reasoning effort levels a model actually accepts. This is what powers the model dropdown when you add a routing step β pick a known model and DodoRouter already knows its limits and price, or choose **Customβ¦** to type any model id it doesn't have listed yet (it'll still route correctly; you just won't get cost estimation for an unknown model).
Subscription-based keys (`openai-codex`, `anthropic_oauth`, `moonshot_coding`, `zai_coding`) are costed at $0/token in the dashboard β you're already paying a flat monthly fee for that usage, so DodoRouter shows requests through them as "included in plan" rather than computing a per-token dollar amount.
---
# Dashboard Guide
Source: https://dodorouter.com/docs/dashboard/
# Dashboard guide
Everything below lives in the left sidebar once you're logged in.
## Dashboard
A live, auto-refreshing (every 5s) overview for whichever router you select: request volume over the last hour, success rate, p95 latency, token usage, cache-hit rate, the router's routing chain at a glance, and a recent-requests table.
## Routers
Create/rename/delete routers, and open a router's own page β which is where you'll spend most of your setup time. On a router's page you get: a live-updating "Connect" panel with ready-to-copy code snippets (cURL / Python / Node, in whichever of the three API formats you pick), the routing-chain editor (add steps, reorder them with up/down arrows, assign a specific provider key per step, toggle context-overflow behavior), recent logs, and a setup checklist that walks you through the four things a router needs before it can serve a request: a provider key, a routing step, a key assigned to that step, and a first request.
## Providers
Add/rename/delete provider API keys, grouped by provider. Each key shows a live status badge β verified valid (green), invalid (red), out of quota (amber), or not-yet-verified (click to check) β so a routing step referencing a broken key is visibly flagged before you find out the hard way in production.
## API Keys
One card per router showing its endpoint URL and masked key prefix, with a one-click **Regenerate** (old key stops working immediately; new one is shown once).
## Logs
Every request, live-streamed in as it happens. Filter by router or favorites. Each log opens into full detail: conversation view, raw request/response JSON, per-attempt fallback trace, timing, cost, and a **Replay** action that reruns the same conversation against a different model and diffs the two results (inline diff, side-by-side, or raw JSON).
## Sessions & Recordings
Reached from a router's page. [Sessions](/docs/concepts/#sessions) group requests by your `x-session-id` header; [Recordings](/docs/concepts/#recordings) are explicit start/stop capture windows started from the dashboard or the [recordings API](/docs/api/#endpoints). Both give you a scoped log list with their own stat tiles (requests, tokens, avg latency, success rate).
---
# Connect a Coding Agent
Source: https://dodorouter.com/docs/integrations/
# Connect a coding agent
Every configuration in this section was actually run against a live DodoRouter instance β not copied from each tool's generic docs. Replace `sk-dodo-YOUR_KEY` and the router slug with your own in each example.
All four talk to DodoRouter using one of the three [API formats](/docs/api/) β Anthropic Messages, an OpenAI-compatible provider package, or the Responses API β and all four are unaffected by [the model field being ignored](/docs/api/#the-model-field-is-ignored): whatever your routing chain resolves to is what actually answers, regardless of what model name the client thinks it's talking to.
---
# Claude Code
Source: https://dodorouter.com/docs/integrations/claude-code/
# Claude Code
Claude Code talks the Anthropic Messages API, so point it at your router's `/v1/messages`-serving base and pass your DodoRouter key as the auth token:
```bash
export ANTHROPIC_BASE_URL="{base_url}/r/{router}"
export ANTHROPIC_AUTH_TOKEN="sk-dodo-YOUR_KEY"
claude
```
Verified: `claude -p "Reply with exactly the single word: pong"` with the env vars above returned `pong`. Since [DodoRouter ignores the requested model](/docs/api/#the-model-field-is-ignored), Claude Code will actually run whatever provider/model your routing chain resolves to first β including non-Anthropic models β while still speaking Anthropic's wire format to Claude Code itself. This is also how you route Claude Code's usage through a fallback chain for reliability, even if every step in that chain is Claude models via different keys.
To make this permanent for a project, add the same two variables to that project's `.claude/settings.json` under an `"env"` key instead of exporting them in your shell.
---
# OpenCode
Source: https://dodorouter.com/docs/integrations/opencode/
# OpenCode
OpenCode's provider system supports any OpenAI-compatible endpoint via the `@ai-sdk/openai-compatible` package. Add a custom provider to `opencode.jsonc` (either project-local or `~/.config/opencode/opencode.jsonc` for all projects):
```json
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"dodorouter": {
"npm": "@ai-sdk/openai-compatible",
"options": {
"baseURL": "{base_url}/r/{router}/v1",
"apiKey": "sk-dodo-YOUR_KEY"
},
"models": {
"default": { "name": "DodoRouter" }
}
}
}
}
```
Then run it against that provider/model:
```bash
opencode run -m dodorouter/default "Reply with exactly the single word: pong"
```
Verified: returned `pong`. The model key (`default` above) can be any label β it isn't sent upstream as a real model id, since DodoRouter substitutes whatever the routing chain resolves to. Add more entries under `"models"` if you want several OpenCode-visible "models" that all point at the same DodoRouter provider but happen to carry different display names.
---
# Forge
Source: https://dodorouter.com/docs/integrations/forge/
# Forge
Forge has a built-in `openai_compatible` provider slot. The easiest path is the interactive menu:
```bash
forge provider login
# choose "openai_compatible", then enter your base URL and API key when prompted
```
This writes an entry to `~/.forge/.credentials.json` shaped like this (confirmed from a real, already-configured Forge installation pointed at DodoRouter):
```json
{
"id": "openai_compatible",
"auth_details": { "api_key": "sk-dodo-YOUR_KEY" },
"url_params": { "OPENAI_URL": "{base_url}/r/{router}/v1" }
}
```
and sets in `~/.forge/.forge.toml`:
```toml
[session]
provider_id = "openai_compatible"
model_id = "default"
```
Then run `forge` as usual, or non-interactively with `forge -p "your prompt"`.
Verified with a caveat
We pointed a real Forge installation at a live DodoRouter router using exactly this config and it connected, authenticated, and completed several successful tool-calling turns (each logged by DodoRouter as HTTP 200 with the correct content). During one longer automated run, Forge's own client logged "Empty completion received" and retried repeatedly on a turn where DodoRouter's logs show it actually returned a perfectly valid response (right content, finish_reason: "stop"). That looks like an intermittent client-side parsing hiccup in Forge rather than anything DodoRouter did wrong, and it's exactly the config the project's own maintainers run day-to-day against the hosted service β but if you hit a similar stall, check the request in DodoRouter's Logs before assuming the request failed.
---
# Codex CLI
Source: https://dodorouter.com/docs/integrations/codex-cli/
# Codex CLI
OpenAI's Codex CLI supports custom model providers in `~/.codex/config.toml`:
```toml
model = "default"
model_provider = "dodorouter"
[model_providers.dodorouter]
name = "DodoRouter"
base_url = "{base_url}/r/{router}/v1"
env_key = "DODO_API_KEY"
```
`env_key` names an environment variable Codex reads the API key from, so the key itself never has to live in the config file:
```bash
export DODO_API_KEY="sk-dodo-YOUR_KEY"
codex exec "Reply with exactly the single word: pong"
```
Verified: Codex CLI's default streaming mode renders the response live in the terminal (`codex` then `pong`), and DodoRouter's own logs confirm the underlying completion (`"pong"`, `finish_reason: "stop"`, HTTP 200, tokens billed correctly). An earlier version of DodoRouter's streamed [Responses API](/docs/api/responses/) didn't emit the item-lifecycle events Codex CLI's client strictly requires before a text delta, which caused it to log `OutputTextDelta without active item` and stall β that's now fixed. All four integrations in this section are clean.
---
# Deployment
Source: https://dodorouter.com/docs/deployment/
# Deployment
DodoRouter ships as an Elixir release (`mix release`) with full ERTS included, so a deployed instance doesn't need Elixir/Erlang installed on the target machine β just PostgreSQL reachable and the environment variables from [Self-hosting](/docs/self-hosting/#production-environment-variables) set.
A minimal production boot looks like:
```bash
MIX_ENV=prod mix release
DATABASE_URL=ecto://user:pass@host/dodo_router_prod \
SECRET_KEY_BASE=$(mix phx.gen.secret) \
PHX_HOST=your-domain.com \
PHX_SERVER=true \
INFISICAL_TOKEN=β¦ INFISICAL_PROJECT_ID=β¦ \
_build/prod/rel/dodo_router/bin/dodo_router start
```
Run database migrations before starting a new version: `bin/dodo_router eval DodoRouter.Release.migrate`. Health-check your deployment against `GET /health`, which returns 503 both when the database is unreachable and while the instance is intentionally draining connections during a graceful shutdown β point your load balancer's health check at it so it stops sending traffic during a rolling deploy.
---
# Troubleshooting & FAQ
Source: https://dodorouter.com/docs/troubleshooting/
# Troubleshooting & FAQ
### "No routing configured for router 'β¦'"
The router has zero routing steps. Add at least one on the router's page under Routing Chain.
### "Failed to store API key securely" when adding a provider key
Self-hosted only: `INFISICAL_TOKEN` / `INFISICAL_PROJECT_ID` aren't set or are invalid. See the callout in [Self-hosting](/docs/self-hosting/).
### 401 Invalid API key
You're using a provider key (from the Providers page) where a router API key is expected, or vice versa β see [Provider keys vs. router API keys](/docs/concepts/#provider-keys-vs-router-api-keys). Also check you didn't regenerate the router's key since your client last read it.
### 402 Payment Required
Hosted service only: the account owning this router doesn't have an active subscription. Visit Billing to subscribe. Doesn't apply to self-hosted instances unless you've configured Stripe billing yourself.
### 400 "Input exceeds context window of this model"
Every routing step failed with a context-overflow error β either you only have one step and its model's context window is too small, or you've enabled ["Skip fallback on context overflow"](/docs/concepts/#routing-steps-fallback) and the first step alone couldn't fit the request. Add a step with a larger context window, or disable that toggle so DodoRouter tries the rest of the chain.
### 502 "All providers failed"
Every step in the chain returned a non-recoverable error. Open the request in Logs β it shows the per-step error for each attempt, which is almost always more specific than the top-level message.
### A routing step shows "invalid" or "out of credits" next to its assigned key
DodoRouter tracks key health from real dispatch outcomes. Fix or replace the key on the Providers page; the warning links straight there.
### Do I need to send a real model name?
No β see [The model field is ignored](/docs/api/#the-model-field-is-ignored). DodoRouter always uses the model configured on the routing step it's attempting.