One self-hosted endpoint for every AI subscription you pay for.
douane connects your Claude, ChatGPT (Codex), Antigravity, Grok, GitHub Copilot and OpenCode Go subscriptions, plus any API key you hold, then serves them to any coding tool through OpenAI- and Anthropic-compatible APIs. It watches each account's quota, picks the account that should serve the next conversation, and keeps a conversation on the same account so the provider's prompt cache stays warm. A single Rust binary with an embedded dashboard; no Node.js, Docker or external database at runtime.
- One base URL and one placeholder key for Claude Code, the Grok CLI, Boite and any OpenAI-compatible client.
- Chat Completions, Anthropic Messages and Responses, streaming included, with translation between them; native Codex WebSocket sessions.
- Live quotas per account: five-hour, daily, weekly and monthly windows, reset times, plan tier, extra credits and banked resets, refreshed in the background.
- Four routing strategies, per provider if you like, and failover to another account of the same provider when one runs out.
- Conversation affinity: native session ids and
prompt_cache_keypin a conversation to its account, so cache reads keep landing. - Per-account reserve: stop spending an account at 5 % (or any share) of its longest quota window, or of the windows you pick (5 hours, weekly, both), so you keep some for yourself.
- Request history with latency, token and cache counters; never conversation bodies or credentials.
- OAuth and device sign-in from the dashboard, JSON import of existing CLI credentials, and
douanectlfor scripting. - A dashboard in English and French that follows the browser's language; Settings keeps another choice.
| Provider | Connect with | Quotas |
|---|---|---|
| Claude (Pro, Max) | OAuth in the dashboard, or credential import | Five-hour, weekly and weekly Opus windows, extra usage, banked resets |
| Codex (ChatGPT plans) | OAuth in the dashboard, or credential import | Five-hour and weekly windows, credits, banked resets |
| Antigravity | Google OAuth once a client is configured, or import | Per-model quotas |
| Grok (SuperGrok) | Device code in the dashboard | Weekly or monthly credit windows |
| OpenCode Go | Subscription key in the manual form | Five-hour, weekly and monthly windows |
| Gemini CLI, Kimi | Credential import | Read from the provider |
| GitHub Copilot | Device code in the dashboard, or OpenCode's auth.json |
Monthly premium requests |
| Qwen | Credential import | Not reported |
| Custom endpoints (DeepSeek, OpenRouter, Groq, NVIDIA NIM...) | An API key in the dashboard or douanectl endpoint add |
Not reported |
What a provider exposes depends on the account and plan. A quota douane cannot read stays unknown; it is never estimated.
Each release attaches an archive per platform holding douane, douanectl, the license and this file: douane-<version>-windows-x64.zip, -macos-arm64.tar.gz, -macos-x64.tar.gz, -linux-x64.tar.gz and -linux-arm64.tar.gz. They need no runtime: Linux builds are static, the Windows build carries its C runtime. SHA256SUMS lists every file's checksum and manifest.json describes the raw executables for installers.
tar -xzf douane-<version>-linux-x64.tar.gz
cd douane-<version>-linux-x64 && ./douane --config douane.tomlOn Windows, unzip the archive and run douane.exe --config douane.toml from that folder. The executables are not signed: Windows SmartScreen asks for confirmation on first launch, and macOS needs xattr -d com.apple.quarantine douane douanectl after a browser download.
Boite installs Douane from Settings > Plugins: it downloads the executable for its platform from the release named in boite-plugin.json at the repository root (written by the first release), checks its SHA-256 and runs it on 127.0.0.1. Douane's accounts, database and configuration live outside the plugin folder, so uninstalling or updating the plugin keeps the accounts. Each release opens a pull request that moves boite-plugin.json to the new executables; Boite offers that version once it is merged.
Images for linux/amd64 and linux/arm64 are on GitHub Container Registry: latest is the last release, edge follows main.
docker compose up -d # with compose.yaml from this repositoryor without compose:
docker run -d --name douane --restart unless-stopped \
-p 127.0.0.1:8787:8787 -v douane:/data ghcr.io/beboite/douane:latestAccounts, the database and douane.toml live in the /data volume; put a configuration file there to change settings. The container runs as uid 65532, so a bind-mounted folder must be writable by that uid. docker exec douane douanectl status runs the CLI inside it. The port is published on loopback only: read Security and data before exposing it.
Build with Rust 1.90 or newer. SQLite is bundled and the dashboard is already compiled into the repository.
cargo build --workspace --release --lockedtarget/release now holds douane and the management CLI douanectl. Both are self-contained and can be copied to another machine with the same OS and architecture.
Start the service. Without a configuration file it listens on 127.0.0.1:8787 and keeps auth/ and douane.sqlite in the working directory. With one, relative paths resolve from the file's folder, and settings changed in the dashboard are saved back to it. douane.example.toml lists every setting.
douane --config douane.toml --check # validate, create nothing
douane --config douane.tomlOpen http://127.0.0.1:8787/admin, choose Connect an account and sign in to a provider. When douane runs on another machine, paste the callback URL or code shown by the provider back into the dialog: the provider's localhost redirect does not need to reach the server.
Then point a client at it:
curl http://127.0.0.1:8787/v1/models
curl http://127.0.0.1:8787/v1/chat/completions \
-H 'Authorization: Bearer douane' \
-H 'Content-Type: application/json' \
-d '{"model":"<id from /v1/models>","messages":[{"role":"user","content":"Hello"}],"stream":true}'Model ids carry their provider prefix, for example claude/... or codex/.... The key douane is a placeholder: authentication upstream uses the connected subscription.
The dashboard's Set up a tool page writes these settings for you, with your own address.
| Client | Settings |
|---|---|
| OpenAI-compatible tools | Base URL http://127.0.0.1:8787/v1, API key douane, a model id from /v1/models |
| Claude Code | ANTHROPIC_BASE_URL=http://127.0.0.1:8787 (no /v1), ANTHROPIC_AUTH_TOKEN=douane |
| Grok CLI | XAI_API_KEY=douane, GROK_MODELS_BASE_URL and GROK_XAI_API_BASE_URL set to http://127.0.0.1:8787/v1, GROK_MODELS_LIST_URL=http://127.0.0.1:8787/v1/models?provider=xai&ids=upstream |
| Boite | Subscription proxy set to the dashboard URL: Boite lists the models and draws each account's quota bars from /v1/quotas |
For the Grok CLI, a session saved by grok login takes precedence over the key; set GROK_AUTH_PATH to a missing file to keep it out.
If clients reach the API at a different address than the dashboard (a reverse proxy, a separate host name), set server.client_api_base_url so the generated snippets use it.
| Endpoint | Purpose |
|---|---|
GET /health |
Service health |
GET /v1/models |
Available models; ?provider=xai,claude filters, &ids=upstream lists provider-native ids in the provider's order |
GET /v1/quotas |
Quotas per provider, per account or averaged, for client status bars |
POST /v1/chat/completions |
Chat Completions, streaming included |
POST /v1/messages |
Anthropic Messages, streaming included |
POST /v1/messages/count_tokens |
Claude token counting, relayed to the provider |
POST /v1/responses |
Responses, streaming included |
POST /v1/responses/compact |
Native compaction on compatible upstreams |
GET /v1/responses (WebSocket) |
Native Codex sessions, and a streaming bridge for the other subscriptions |
/admin/api/* |
Accounts, quotas, models, request history, settings and OAuth |
/v0/management, /v8/management |
A CLIProxyAPI-compatible subset of its management API |
Translation between the three dialects covers text, tools and images; provider-specific extensions do not all have an equivalent. A failed request moves to another account only before its first byte reaches the client: started output is never replayed. A same-dialect request returns the provider's own rate-limit headers and request id, and client errors such as an oversized prompt come back as the provider wrote them.
Use an advertised antigravity/<model> ID through Chat Completions, Messages or
Responses. Send a stable conversation ID, such as x-opencode-session-id,
X-Session-Id, session_id or prompt_cache_key. OpenCode's custom provider
sends its own session ID. /v1/models includes thinking capabilities and
context/output limits; explicit thinking controls follow those capabilities.
Only outgoing Antigravity system text is normalized: Anthropic billing lines are removed, and the exact Claude Agent SDK and Codex self-identification phrases receive one zero-width space. Other instructions, user messages, tools and other providers are preserved. This follows the compatibility approach in CLIProxyAPI. Google can still refuse a request; its sanitized reason and retry delay remain visible.
Native signed model parts are kept in memory and restored after tool results.
Replay is scoped to the account, model, session, normalized system instructions
and preceding visible history. Hidden reasoning and opaque signatures never
enter the ledger or disk. Chat Completions exposes returned thought text as
reasoning_content; other dialects do not fabricate signed thinking blocks.
The replay cache holds at most 128 turns and 8 MiB for one hour, with a 256 KiB native-turn limit. Interrupted, invalid or oversized turns are not recorded. Unsigned tool history without a matching entry is refused locally, including after restart, expiry, account/model changes or compaction that alters the recorded context. Start a new conversation in that case. Acceptance by a live Google account remains separate from the local compatibility tests.
Copilot signs in with GitHub's device flow through the OpenCode GitHub app, the way OpenCode does; an existing OpenCode login imports as is (its auth.json). The model list is Copilot's own catalog: models whose policy is enabled for the account, each sent to the path Copilot lists for it (/v1/messages for Claude models, /responses, or /chat/completions). A client's own format is forwarded untouched when the model accepts it.
Copilot bills a premium request when its user starts a turn. douane marks tool results and other follow-ups as the agent's own (x-initiator: agent), as OpenCode does. The catalog can list a model the plan does not serve; the first model_not_supported refusal hides that model for 24 hours or until a restart, and the account stays usable.
Any OpenAI-, Anthropic- or Responses-compatible API becomes a route: its keys serve the models it lists under a prefix of your choice, so deepseek serves deepseek/deepseek-v4-pro. Keys sharing a route rotate and fail over like accounts of one provider; keys on different routes never serve each other's models.
| Field | Meaning |
|---|---|
route |
Model prefix: 1 to 32 lowercase letters, digits, - or _, never a provider's name |
base_url |
The API root, usually ending in /v1 |
dialect |
What the API speaks: chat, responses or messages; douane translates the client's format to it. Absent forwards the client's format unchanged |
models |
Model ids to serve; empty serves every model the API lists at /v1/models |
The dashboard's Connect an account dialog adds one, and each endpoint's pencil edits it in place. From a shell or an agent, the key comes from a file or stdin, never from the command line:
douanectl endpoint add deepseek https://api.deepseek.com/v1 --key-file deepseek.key --dialect chat
printf %s "$GROQ_API_KEY" | douanectl endpoint add groq https://api.groq.com/openai/v1 --key-file - --dialect chat
douanectl endpoint set deepseek --models deepseek-v4-pro,deepseek-flash
douanectl endpoint set deepseek --key-file new.key
douanectl endpoint list
douanectl endpoint remove groqThe same fields go through POST /admin/api/accounts/import with {"type":"openai_compatible","route":...,"base_url":...,"api_key":...}, and PATCH /admin/api/accounts/{id} changes route, base_url, dialect, models or api_key. An account without a route keeps its previous behavior under openai_compatible/.
Pick a strategy in Settings, globally or per provider. It applies to new conversations:
| Strategy | Behaviour |
|---|---|
smart (default) |
Sends 90 % of new conversations to the account whose weekly reset comes within 48 hours, until it is exhausted; otherwise weighs remaining quota against time left, with 20 % plain rotation |
reset-first |
Uses the account with the nearest weekly reset, and moves running conversations back to it as soon as it is eligible again |
fill-first |
Uses the first eligible account until it becomes unavailable |
round-robin |
Rotates through eligible accounts |
A conversation stays on its account while that account is eligible. douane recognises Codex, Claude and OpenCode session ids, session_id headers and prompt_cache_key, and keeps Claude cache controls untouched. A custom client should send one stable id per conversation. The affinity table holds 10,000 conversations for 24 hours of inactivity, in memory.
When an account is refused, it rests until the provider's own reset (its retry-after, Claude's rejected window, Codex's exhausted window), then returns. Accounts whose subscription quota is gone but which still hold usable credits stay available behind accounts with quota left.
A reserve, set per account on the Accounts page or with PATCH /admin/api/accounts/{id} and {"reserve_percent": 5}, is a hard floor on the account's longest window (monthly over weekly over daily over five-hour): once that window reaches the floor, douane stops using the account, live conversations included, until it resets. To watch other windows, pick them on the Accounts page or send "reserve_windows": ["five-hour", "weekly"] (any of five-hour, daily, weekly, monthly; [] returns to the longest window): the account then stops as soon as one of them reaches the floor. Close to the floor, quotas are reread at most once a minute, so a burst can overshoot by a little.
Banked resets for Claude and Codex appear on each account card. Using one asks for confirmation, and a retried click reuses the same request id, so a double click cannot spend two.
- Credentials stay in
auth_diras local files. They never appear in the dashboard, logs or errors, and refresh-token rotation is serialised and written to disk. The management API returns them in one place only, a configuration export. - On Linux and macOS,
auth_diris mode 0700 and each credential file 0600. On Windows, douane removes inherited permissions fromauth_dirand grants full control to the current user alone; keep the configuration under that user's own profile, for example%LOCALAPPDATA%\douane. - SQLite stores request metadata only: time, model, account, status, latency and token or cache counts.
- The default listener is loopback. To expose douane, put it behind a reverse proxy that authenticates the dashboard, and set
server.management_token_envto require a bearer token on/admin/api. That token is separate from client access. - Google OAuth clients for Gemini and Antigravity are read from
oauth-clients.jsonbesideauth_dir, or fromDOUANE_GEMINI_*andDOUANE_ANTIGRAVITY_*environment variables; douane ships none.
Settings > Backup in the dashboard, or GET /admin/api/export, downloads douane-export-<date>.json: settings, disabled models and every account with its tokens and keys. Restore reads that file into another douane. An account whose identity is already there is updated in place, the others are added; settings and disabled models are applied only when asked ({"bundle": <file>, "settings": true} on POST /admin/api/restore).
The file holds live sign-ins. Refresh tokens rotate on use, so an account must live in one douane at a time: after restoring it elsewhere, remove it from the first one. Keep the file private and delete it once restored.
douanectl export --output douane-export.json # owner-only file on Linux and macOS
douanectl --url http://127.0.0.1:8787 restore douane-export.json --settingsdouanectl health
douanectl status
douanectl accounts
douanectl quotas --refresh
douanectl models
douanectl import account.json --name "Work"
douanectl login codex
douanectl complete <oauth-session-id> --callback-file callback.txt
douanectl disable <account-id>
douanectl enable <account-id>
douanectl remove <account-id>
douanectl endpoint add <route> <base-url> --key-file <file|->
douanectl endpoint set <route|account-id> [--base-url ...] [--dialect ...] [--models ...] [--key-file ...]
douanectl export --output <file>
douanectl restore <file> [--settings]--url targets another instance and DOUANE_MANAGEMENT_TOKEN supplies the management token. Secrets are read from files, never from arguments.
cargo test --workspace --all-targets --lockedThe dashboard is Svelte 5 in dash/. Changing it needs Node.js 24 and pnpm 11:
cd dash
pnpm install --frozen-lockfile
pnpm check
pnpm buildCommit dash/build with the source change, then rebuild Rust to embed it. pnpm dev runs the dashboard alone and expects the management API at the same origin or behind a proxy for /admin/api. pnpm mock serves the built dashboard with sample data on http://127.0.0.1:5199/admin/, without douane or any account; the README images come from it.
CI tests Rust on the minimum supported version, checks and builds the dashboard, and builds the Docker image on amd64 and arm64, then runs docker/smoke.sh against it; a pull request runs only the jobs its files touch. Tests run against local fake upstreams; they do not prove that a real provider accepts a given account or model.
A release is a tag vX.Y.Z equal to the version in Cargo.toml: CI runs again, the tested image is published as X.Y.Z, X.Y and latest, and the GitHub release is created.
douane is an independent project, not affiliated with Anthropic, OpenAI, Google, xAI or any other provider. Use it with accounts you own and within each provider's terms.
AGPL-3.0-or-later. The model catalogue in crates/douane/data is derived from CLIProxyAPI under the MIT license; see NOTICE.



