@askalf/dario 6.0.31 → 6.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  <div align="center">
2
2
 
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset=".github/readme/hero-dark.svg">
5
+ <img alt="dario routes every AI tool you use to the subscriptions you already pay for. Coding tools on the left send requests to a local dario endpoint at localhost:3456, which forwards each one to a Claude plan (a pool of seats routed by headroom), a ChatGPT plan, or an API-key backend. When the Claude pool returns 429, the request is served by the ChatGPT plan and the response carries an x-dario-pool-fallback header." src=".github/readme/hero-light.svg" width="100%">
6
+ </picture>
7
+
3
8
  # `dario`
4
9
 
5
10
  ### Your Claude and ChatGPT subscriptions each work in exactly one place.<br/>dario makes them work **everywhere** — at subscription pricing, not per-token API bills.
@@ -7,42 +12,37 @@
7
12
  <p>
8
13
  <a href="https://www.npmjs.com/package/@askalf/dario"><img src="https://img.shields.io/npm/v/@askalf/dario?color=6f42c1&label=npm&logo=npm" alt="npm version"></a>
9
14
  <a href="https://github.com/askalf/dario/releases"><img src="https://img.shields.io/github/v/release/askalf/dario?color=6f42c1&label=release&logo=github" alt="Latest release"></a>
10
- <a href="https://github.com/askalf/dario/actions/workflows/ci.yml"><img src="https://github.com/askalf/dario/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
11
- <a href="https://github.com/askalf/dario/actions/workflows/codeql.yml"><img src="https://github.com/askalf/dario/actions/workflows/codeql.yml/badge.svg" alt="CodeQL"></a>
15
+ <a href="https://github.com/askalf/dario/actions/workflows/ci.yml"><img src="https://github.com/askalf/dario/actions/workflows/ci.yml/badge.svg?branch=master" alt="CI"></a>
16
+ <a href="https://github.com/askalf/dario/actions/workflows/codeql.yml"><img src="https://github.com/askalf/dario/actions/workflows/codeql.yml/badge.svg?branch=master" alt="CodeQL"></a>
12
17
  <a href="https://scorecard.dev/viewer/?uri=github.com/askalf/dario"><img src="https://api.scorecard.dev/projects/github.com/askalf/dario/badge" alt="OpenSSF Scorecard"></a>
13
18
  <a href="https://www.bestpractices.dev/projects/13638"><img src="https://www.bestpractices.dev/projects/13638/badge" alt="OpenSSF Best Practices"></a>
14
19
  <a href="https://github.com/askalf/dario/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/@askalf/dario?color=6f42c1" alt="License"></a>
15
20
  <a href="https://www.npmjs.com/package/@askalf/dario"><img src="https://img.shields.io/npm/dm/@askalf/dario?color=6f42c1" alt="Downloads"></a>
16
- <a href="https://x.com/ask_alf"><img src="https://img.shields.io/badge/follow-@ask__alf-1da1f2?style=flat-square" alt="Follow on X"></a>
21
+ <a href="https://www.npmjs.com/package/@askalf/dario"><img src="https://img.shields.io/node/v/@askalf/dario?color=6f42c1" alt="Node version"></a>
22
+ </p>
23
+
24
+ <p>
25
+ <a href="https://github.com/askalf/dario/blob/master/src/cc-template-data.json"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Faskalf%2Fdario%2Fmaster%2Fsrc%2Fcc-template-data.json&query=%24._version&prefix=v&label=tracks%20Claude%20Code&color=8b5cf6" alt="Claude Code version the bundled template tracks (read live from master)"></a>
26
+ <a href="https://github.com/askalf/dario/actions/workflows/cc-billing-classifier-canary.yml"><img src="https://github.com/askalf/dario/actions/workflows/cc-billing-classifier-canary.yml/badge.svg" alt="Daily live billing canary"></a>
27
+ <a href="https://github.com/askalf/dario/actions/workflows/cc-drift-watch.yml"><img src="https://github.com/askalf/dario/actions/workflows/cc-drift-watch.yml/badge.svg" alt="Hourly Claude Code drift watch"></a>
28
+ <a href="https://github.com/askalf/dario/actions/workflows/cc-drift-template-watch.yml"><img src="https://github.com/askalf/dario/actions/workflows/cc-drift-template-watch.yml/badge.svg" alt="Live template drift watch"></a>
17
29
  </p>
18
30
 
19
31
  <p><strong>One local endpoint. Every AI tool you own. The subscriptions you already pay for.</strong></p>
20
32
 
21
33
  <sub><code>npm i -g @askalf/dario</code> · <strong>0</strong> runtime deps · <a href="https://www.npmjs.com/package/@askalf/dario">SLSA-attested</a> every release · nothing phones home · ~31k lines you can read in a weekend · independent, unofficial, third-party (<a href="DISCLAIMER.md">DISCLAIMER.md</a>)</sub>
22
34
 
23
- <sub>Part of <a href="#own-your-stack"><strong>Own Your Stack</strong></a> — 11 open tools for owning your AI infra: <a href="https://github.com/askalf/redstamp">redstamp</a> · <a href="https://github.com/askalf/truecopy">truecopy</a> · <a href="https://github.com/askalf/fieldpass">fieldpass</a> · <a href="https://github.com/askalf/plumbline">plumbline</a> · <a href="#own-your-stack">full family ↓</a></sub>
35
+ <sub><a href="#start-in-60-seconds">Start</a> · <a href="#point-your-tools-at-it">Your tools</a> · <a href="#what-it-does-with-a-request">Routing</a> · <a href="#two-plans-one-endpoint">Two plans</a> · <a href="#many-seats-one-endpoint">Pool</a> · <a href="#it-tracks-a-moving-target">Drift</a> · <a href="#trust--transparency">Trust</a> · <a href="#will-my-account-get-suspended">Risk</a> · <a href="#commands">Commands</a> · <a href="#faq">FAQ</a> · <a href="docs/returning.md">Coming back after a while?</a></sub>
24
36
 
25
37
  </div>
26
38
 
27
39
  ---
28
40
 
29
- > ## 🎉 dario `v6.0` — any client shape, any subscription, and failover between them
30
- >
31
- > Through v5, dario was a Claude proxy that had recently learned to reach a ChatGPT subscription on one path. v6 finishes that: **either subscription can serve either wire shape, and either one can cover for the other.**
32
- >
33
- > - **🔀 Both wire shapes, both plans.** Your ChatGPT plan now answers `/v1/messages`, not just `/v1/chat/completions` — so Claude Code, the Anthropic SDKs and agent runtimes can be served by it without knowing. Your Claude plan already answered both.
34
- > - **🪂 Failover between subscriptions.** `--pool-fallback=gpt-5.6-sol,claude-sonnet-5` is a *chain*: a drained Claude pool is served by ChatGPT, and a rate-limited ChatGPT is handed back to Claude. Two consumer plans, no API keys, and neither one going down takes you with it. → [Failover](#failover-between-subscriptions)
35
- > - **🧪 Shadow compare.** `x-dario-compare: <model>` answers you normally *and* runs the same prompt past the other family, writing both to `~/.dario/compare/`. Which model is better at **your** work, measured on your own traffic. → [Shadow compare](#shadow-compare)
36
- > - **➕ `dario add altman` / `dario add amodei`.** Attach a plan by whose it is.
37
- > - **🩺 `dario doctor` reports failover readiness** — including *armed but INERT*, the state that is green on every other check and cannot actually do anything.
38
- >
39
- > **Upgrading from v5?** Nothing to do — every v6 feature is opt-in and a single-value `--pool-fallback` behaves exactly as it did. [CHANGELOG](CHANGELOG.md#600---2026-08-30)
41
+ You're already paying $20, $100 or $200 a month for Claude,[^plans] or for a ChatGPT plan. Then Cursor wants an API key. Aider wants an API key. Cline, Continue, Zed, your own scripts — every one of them bills you **again**, per token, while the plan you bought sits idle in the one app it shipped with.
40
42
 
41
- ---
43
+ **dario is one local endpoint that routes all of them through the plans you already pay for.** Point any Anthropic- or OpenAI-compatible tool at `http://localhost:3456` and you're done. No per-tool config, no second bill, and when one plan hits its limit the other one takes the request.
42
44
 
43
- You're already paying $20, $100, or $200 a month for Claude. Then Cursor wants an API key. Aider wants an API key. Cline, Continue, Zed, your scripts — every one of them bills you **again**, per token, while the subscription you already bought sits idle in Claude Code.
44
-
45
- **dario is one local endpoint that routes all of them through the Claude subscription you already pay for.** Point any Anthropic- or OpenAI-compatible tool at `http://localhost:3456` and you're done. No per-tool config, no second bill.
45
+ ## Start in 60 seconds
46
46
 
47
47
  ```bash
48
48
  # 1. Install
@@ -54,90 +54,216 @@ dario login # or `dario login --manual` for SSH / headless
54
54
  # 3. Start the local proxy
55
55
  dario proxy # separate terminal or background
56
56
 
57
- # 4. Point any Anthropic-compat tool at it
57
+ # 4. Point any Anthropic-compatible tool at it
58
58
  export ANTHROPIC_BASE_URL=http://localhost:3456
59
59
  export ANTHROPIC_API_KEY=dario
60
60
  ```
61
61
 
62
- That's the whole setup. Every tool that honors those env vars now runs on your subscription.
62
+ <picture>
63
+ <source media="(prefers-color-scheme: dark)" srcset=".github/readme/quickstart-dark.svg">
64
+ <img alt="Terminal: npm install -g @askalf/dario, dario login (Opening browser to sign in… Login successful!), dario proxy (dario — http://localhost:3456. Your Claude subscription is now an API. Usage: ANTHROPIC_BASE_URL=http://localhost:3456, ANTHROPIC_API_KEY=dario. OAuth healthy, Model passthrough, Pool: 1 account), then export the two variables and run aider --model sonnet." src=".github/readme/quickstart-light.svg" width="100%">
65
+ </picture>
66
+
67
+ That's the whole setup. Every tool that honors those env vars now runs on your subscription. OpenAI-shaped tools use `OPENAI_BASE_URL=http://localhost:3456/v1` instead, same key.
68
+
69
+ **Works with:** Claude Code, Cursor, Aider, Cline, Roo Code, Kilo Code, Continue.dev, Zed, OpenHands, OpenClaw, Hermes, Codex CLI, the [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk), the Anthropic and OpenAI SDKs, curl, your own scripts. Per-tool snippets are one section down; the honest per-tool status is in the [compatibility matrix](./docs/integrations/compat-matrix.md).
70
+
71
+ Prefer Docker? `ghcr.io/askalf/dario:latest` — multi-arch (`amd64` + `arm64`), published from the same workflow as every npm release ([guide](./docs/docker.md)). Something off? `dario doctor` prints one paste-ready health report.
63
72
 
64
- **Works with:** Claude Code, Cursor, Aider, Cline, Roo Code, Continue.dev, Zed, Windsurf, OpenHands, OpenClaw, Hermes, Codex CLI, the [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk), your own scripts.
73
+ ## Point your tools at it
65
74
 
66
- Add other providers and reuse the same proxy:
75
+ Two base URLs, one key. Anthropic-shaped clients talk to `http://localhost:3456`; OpenAI-shaped clients talk to `http://localhost:3456/v1`. The key is `dario` (any value works until you set `DARIO_API_KEY`, which then has to match).
76
+
77
+ <details>
78
+ <summary><strong>Claude Code</strong> — forwarded verbatim</summary>
79
+
80
+ ```bash
81
+ export ANTHROPIC_BASE_URL=http://localhost:3456
82
+ export ANTHROPIC_API_KEY=dario
83
+ claude
84
+ ```
85
+
86
+ A genuine Claude Code request already *is* the Claude Code shape, so dario forwards it byte-for-byte — system prompt, tools, thinking, key order untouched — swapping in only the pool's credential, its billing tag and cache breakpoints. That covers the main loop, its Task/Agent sub-agents and the permission classifier. What you gain is the pool: several seats behind the one URL, with headroom routing and 429 failover. Background: [#678](https://github.com/askalf/dario/issues/678).
87
+ </details>
88
+
89
+ <details>
90
+ <summary><strong>Cursor</strong> — needs a public HTTPS tunnel, and the <code>anthropic:</code> prefix</summary>
91
+
92
+ Cursor's BYOK is backend-mediated: the app sends your base URL up to Cursor's servers and *they* make the call, behind an SSRF guard that rejects `localhost` by design (confirmed by Cursor staff, threads linked in the long-form guide). So:
93
+
94
+ ```bash
95
+ dario proxy # terminal 1
96
+ cloudflared tunnel --url http://localhost:3456 # terminal 2 → https://<random>.trycloudflare.com
97
+ ```
98
+
99
+ In Cursor → Settings → Models: enable **Override OpenAI Base URL** with `https://<random>.trycloudflare.com/v1`, key `dario`, and add models as `anthropic:opus` / `anthropic:sonnet` / `anthropic:haiku`. The `anthropic:` prefix routes to the Claude backend without the `claude-` substring that makes Cursor switch to a tool format the OpenAI path can't parse, and it dodges Cursor's built-in-name collision. Use **Agent** mode (<kbd>Cmd</kbd>/<kbd>Ctrl</kbd>+<kbd>I</kbd>); Chat sends no tools. Treat the tunnel URL as a credential. Full walkthrough with every gotcha: [agent-compat.md#cursor](./docs/integrations/agent-compat.md#cursor).
100
+ </details>
101
+
102
+ <details>
103
+ <summary><strong>Cline · Roo Code · Kilo Code</strong> — API provider "Anthropic"</summary>
104
+
105
+ Provider **Anthropic** · API key `dario` · Anthropic Base URL `http://localhost:3456` · model `claude-sonnet-5` / `claude-opus-5` / `claude-haiku-4-5`.
106
+
107
+ These clients speak an XML tool protocol. dario detects them from their system-prompt identity markers and flips into preserve-tools mode on its own, so their schemas pass through and their parsers keep working. `--no-auto-detect` if you'd rather choose. [Details](./docs/integrations/agent-compat.md#cline--roo-code--kilo-code).
108
+ </details>
109
+
110
+ <details>
111
+ <summary><strong>Aider</strong></summary>
112
+
113
+ ```bash
114
+ export ANTHROPIC_BASE_URL=http://localhost:3456
115
+ export ANTHROPIC_API_KEY=dario
116
+ aider --model sonnet # or opus, haiku, any claude-* id
117
+ ```
118
+ </details>
119
+
120
+ <details>
121
+ <summary><strong>Continue.dev</strong></summary>
122
+
123
+ ```yaml
124
+ # ~/.continue/config.yaml
125
+ models:
126
+ - name: Claude Sonnet (dario)
127
+ provider: anthropic
128
+ model: claude-sonnet-5
129
+ apiBase: http://localhost:3456
130
+ apiKey: dario
131
+ ```
132
+ </details>
133
+
134
+ <details>
135
+ <summary><strong>Zed</strong></summary>
136
+
137
+ ```json
138
+ { "language_models": { "anthropic": { "api_url": "http://localhost:3456", "version": "2023-06-01" } } }
139
+ ```
140
+
141
+ Set `ANTHROPIC_API_KEY=dario` in the environment Zed launches from; the model picker then lists Claude models routed through your plan.
142
+ </details>
143
+
144
+ <details>
145
+ <summary><strong>OpenHands</strong></summary>
146
+
147
+ ```bash
148
+ export LLM_BASE_URL=http://localhost:3456
149
+ export LLM_API_KEY=dario
150
+ export LLM_MODEL=anthropic/claude-sonnet-5
151
+ ```
152
+
153
+ The `anthropic/` prefix tells LiteLLM (OpenHands' router) to take the Anthropic path, which dario is now fronting. End-to-end walkthrough: [openhands-walkthrough.md](./docs/integrations/openhands-walkthrough.md).
154
+ </details>
155
+
156
+ <details>
157
+ <summary><strong>OpenClaw</strong></summary>
158
+
159
+ ```bash
160
+ export ANTHROPIC_BASE_URL=http://localhost:3456
161
+ export ANTHROPIC_API_KEY=dario
162
+ openclaw "task description"
163
+ ```
164
+
165
+ OpenClaw's `exec` / `process` / `web_search` / `web_fetch` / `browser` / `message` tools are translated to Claude Code's set without a flag. Newer OpenClaw reads `auth-profiles.json` before env vars, so a stale key there wins — the [walkthrough](./docs/integrations/openclaw-walkthrough.md) covers it.
166
+ </details>
167
+
168
+ <details>
169
+ <summary><strong>Codex CLI · OpenAI SDK · any OpenAI-compatible tool</strong></summary>
170
+
171
+ ```bash
172
+ export OPENAI_BASE_URL=http://localhost:3456/v1
173
+ export OPENAI_API_KEY=dario
174
+ ```
175
+
176
+ Ask for `gpt-5.5` and it is served by your ChatGPT plan once you've run `dario add altman`. Ask for `claude-sonnet-5` on the same URL and it is served by your Claude plan, translated both ways. Ask for `gpt-4o`, `llama-3.3-70b` or anything an API-key backend lists and it goes there byte-for-byte:
67
177
 
68
178
  ```bash
69
179
  dario backend add openai --key=sk-proj-...
70
180
  dario backend add groq --key=gsk_... --base-url=https://api.groq.com/openai/v1
71
181
  dario backend add openrouter --key=sk-or-... --base-url=https://openrouter.ai/api/v1
72
182
  dario backend add local --key=anything --base-url=http://127.0.0.1:11434/v1
73
-
74
- export OPENAI_BASE_URL=http://localhost:3456/v1
75
- export OPENAI_API_KEY=dario
76
183
  ```
77
184
 
78
- Force a specific backend with a model prefix: `openai:gpt-4o`, `claude:opus`, `groq:llama-3.3-70b`, `local:qwen-coder`. Prefer Docker? `ghcr.io/askalf/dario:latest` — multi-arch (`amd64`+`arm64`), published every release ([guide](./docs/docker.md)). Something off? `dario doctor` prints one paste-ready health report.
185
+ Force a backend with a prefix: `openai:gpt-4o`, `claude:opus`, `groq:llama-3.3-70b`, `local:qwen-coder`.
186
+ </details>
79
187
 
80
- ### The interactive TUI
188
+ <details>
189
+ <summary><strong>Claude Agent SDK · Anthropic SDK</strong> (TypeScript, Python)</summary>
81
190
 
82
- Type `dario` with no args (in another terminal) for a full-screen control panel — live request stream, per-model burn-rate, rate-limit utilization, billing-bucket breakdown, and an in-place config editor that writes to `~/.dario/config.json`. Subscription accounting you watch happen instead of reading out of log files. Pure ANSI, zero new runtime deps.
191
+ ```ts
192
+ import Anthropic from "@anthropic-ai/sdk";
193
+ const client = new Anthropic({ baseURL: "http://localhost:3456", apiKey: "dario" });
194
+ ```
83
195
 
196
+ ```python
197
+ import anthropic
198
+ client = anthropic.Anthropic(base_url="http://localhost:3456", api_key="dario")
84
199
  ```
85
- ┌─ dario ─────────────────────────────[ q quit · Tab next · ? help ]──┐
86
- │ Status Config ▎Analytics▎ Hits Accounts Backends │
87
- ├─────────────────────────────────────────────────────────────────────┤
88
- │ ANALYTICS — last 60 min │
89
- │ │
90
- │ Requests: 247 (4.1/min) Tokens in: 142,830 │
91
- │ Tokens out: 38,200 Subscription %: 98% │
92
- │ │
93
- │ Per-model: │
94
- │ opus-5 ████████████░░░░░░░░ 60% (148 req) │
95
- │ sonnet-5 █████░░░░░░░░░░░░░░░ 26% ( 64 req) │
96
- │ haiku-4-5 ███░░░░░░░░░░░░░░░░░ 14% ( 35 req) │
97
- │ │
98
- │ Rate-limit: │
99
- │ 5h ████░░░░░░░░░░░░░░░░░░░░░░░░ 18% │
100
- │ 7d ██░░░░░░░░░░░░░░░░░░░░░░░░░░ 8% │
101
- └─────────────────────────────────────────────────────────────────────┘
200
+
201
+ Zero code change beyond the base URL. Streaming, tool use, prompt caching and extended thinking all pass through. More in [usage.md](./docs/usage.md).
202
+ </details>
203
+
204
+ <details>
205
+ <summary><strong>curl</strong></summary>
206
+
207
+ ```bash
208
+ curl http://localhost:3456/v1/messages -H "content-type: application/json" \
209
+ -d '{"model":"claude-sonnet-5","max_tokens":256,"messages":[{"role":"user","content":"Hello!"}]}'
210
+
211
+ curl http://localhost:3456/v1/chat/completions -H "content-type: application/json" \
212
+ -d '{"model":"gpt-5.5","messages":[{"role":"user","content":"Hello!"}]}'
102
213
  ```
214
+ </details>
103
215
 
104
- ---
216
+ <details>
217
+ <summary><strong>Docker</strong> · Kubernetes · a Pi in a closet</summary>
105
218
 
106
- ## The money
219
+ ```bash
220
+ docker volume create dario-config
221
+ docker run --rm -it -v dario-config:/home/dario/.dario ghcr.io/askalf/dario:latest login --manual
222
+ docker run -d --name dario -p 3456:3456 -v dario-config:/home/dario/.dario \
223
+ -e DARIO_API_KEY="$(openssl rand -hex 32)" ghcr.io/askalf/dario:latest
224
+ ```
107
225
 
108
- | Setup | Monthly cost — heavy user |
109
- |---|---|
110
- | Cursor + Anthropic API direct | **$80–$300** |
111
- | Multi-tool heavy use (Cursor + Aider + Cline + Continue), per-token | **$200–$600+** |
112
- | **Any of the above + dario** | **$20–$200 flat** — your existing Pro/Max plan, nothing extra |
226
+ The image binds `0.0.0.0`, so a key is mandatory; without one dario refuses to start rather than become an open relay for your subscription. No console at all? Start empty with `DARIO_ADMIN=1` and provision the first account over HTTP with the [admin API](./docs/admin-api.md). Two replicas sharing accounts need the [refresh lock](./docs/multi-instance.md). [Docker guide](./docs/docker.md).
227
+ </details>
113
228
 
114
- One subscription, every tool. Switching providers is a model-name change, not a reconfigure — add a backend once and the same `localhost:3456` speaks OpenAI, Groq, OpenRouter, or a local Ollama too.
229
+ Something not listed? If it reads `ANTHROPIC_BASE_URL` or `OPENAI_BASE_URL`, or has a "Base URL" field, it works. The [compatibility matrix](./docs/integrations/compat-matrix.md) says which tools are exercised end-to-end, which are inferred from a shared code path, and which are untested — one honest cell per tool.
115
230
 
116
- ---
231
+ ## What it does with a request
117
232
 
118
- ## What it routes
233
+ You point every tool at one URL. dario reads each request, decides which plan or backend owns it, and forwards it in that backend's native protocol.
119
234
 
120
- You point every tool at one URL. dario reads each request, decides which backend owns it, and forwards it in that backend's native protocol.
235
+ ```mermaid
236
+ flowchart LR
237
+ T["Any tool<br/>Anthropic or OpenAI shape"] --> D{"dario<br/>localhost:3456"}
238
+ D -->|"claude-* · opus · sonnet · haiku"| P["Claude pool<br/>headroom · sticky · 429 retry"]
239
+ D -->|"a slug your ChatGPT plan lists"| X["Codex engine<br/>ChatGPT plan"]
240
+ D -->|"gpt-4o · llama-* · provider prefix"| K["API-key backends"]
241
+ P -->|"OAuth swap + Claude Code template replay"| A[("api.anthropic.com")]
242
+ X -->|"Messages / chat → Responses"| O[("chatgpt.com backend")]
243
+ K -->|"byte-for-byte"| B[("OpenAI-compatible APIs")]
244
+ P -. "429 / 5xx" .-> X
245
+ X -. "429 / 5xx" .-> P
246
+ ```
121
247
 
122
248
  | Client speaks | Model | Routes to | What happens |
123
249
  |---|---|---|---|
124
- | Anthropic Messages | `claude-*` / `opus` / `sonnet` / `haiku` | Claude backend | OAuth swap + CC template → `api.anthropic.com` |
125
- | Anthropic Messages | a slug your ChatGPT account lists | Codex backend | Messages→Responses translation, subscription auth |
250
+ | Anthropic Messages | `claude-*` / `opus` / `sonnet` / `haiku` | Claude pool | OAuth swap + Claude Code template, then `api.anthropic.com` |
251
+ | Anthropic Messages | a slug your ChatGPT account lists | Codex engine | Messages→Responses translation, subscription auth |
126
252
  | Anthropic Messages | `gpt-*`, `llama-*`, … | OpenAI-compat backend | Anthropic→OpenAI translation, forwarded |
127
253
  | OpenAI Chat | `gpt-*` / `o1-*` / `o3-*` | OpenAI-compat backend | Auth swap, body forwarded byte-for-byte |
128
- | OpenAI Chat | a slug your ChatGPT account lists | Codex backend | chat/completions→Responses translation, subscription auth |
129
- | OpenAI Chat | `claude-*` | Claude backend | OpenAI→Anthropic translation, then Claude path |
254
+ | OpenAI Chat | a slug your ChatGPT account lists | Codex engine | chat/completions→Responses translation, subscription auth |
255
+ | OpenAI Chat | `claude-*` | Claude pool | OpenAI→Anthropic translation, then the Claude path |
130
256
  | Either | `<provider>:<model>` | Forced by prefix | Explicit override |
131
257
 
132
258
  The tool doesn't know. The backend doesn't know. dario is the seam.
133
259
 
134
- **The full Claude lineup, autodetected.** Fable 5, Opus 5, Sonnet 5, and Haiku 4.5 — plus `[1m]` long-context variants on every family except haiku — by full id (`claude-opus-5`) or shortcut (`fable` / `opus` / `sonnet` / `haiku`, append `1m` for the long-context form; `opus48` / `opus47` / `opus46` / `sonnet46` pin a specific generation and never float). `GET /v1/models` reads Anthropic's live catalog (TTL-cached, baked fallback when offline), so a new model resolves the day it lands with no dario release, and the model-specific request shape is applied automatically. The TUI's Status tab lists whatever the catalog currently advertises, so it tracks the same set without a release either. Families pulled upstream are filtered from both the live catalog and the fallback so `/v1/models` never advertises a model that 404s — reversible via `DARIO_SUSPENDED_MODELS` if a family is ever pulled again.
260
+ **The full Claude lineup, autodetected.** Fable 5, Opus 5, Sonnet 5 and Haiku 4.5, plus `[1m]` long-context variants on every family except Haiku, by full id (`claude-opus-5`) or shortcut (`fable` / `opus` / `sonnet` / `haiku`; append `1m` for the long-context form; `opus48` / `opus47` / `opus46` / `sonnet46` pin a generation and never float). `GET /v1/models` reads Anthropic's live catalog (TTL-cached, baked fallback offline), so a new model resolves the day it lands with no dario release, and the model-specific request shape is applied automatically. Families pulled upstream are filtered from both the live catalog and the fallback, so `/v1/models` never advertises a model that 404s. A name no provider lists at all, such as a ChatGPT slug your account doesn't have or a typo that belongs to no family, is refused locally with `400` and `x-dario-upstream-rejection: model_unroutable` instead of spending a pool request on an upstream 404.
135
261
 
136
- ---
262
+ ## Two plans, one endpoint
137
263
 
138
- ## ChatGPT subscription accounts (Codex engine)
264
+ ### Your ChatGPT plan, on both endpoints
139
265
 
140
- Your ChatGPT Plus/Pro plan, served on **both** of dario's endpoints — so any client that speaks `/v1/chat/completions` can use it (Codex CLI, the OpenAI SDKs, your own scripts), and so can any client that speaks `/v1/messages` (Claude Code, the Anthropic SDKs, agent runtimes). The harness does not need to know which subscription is behind it.
266
+ A ChatGPT Plus or Pro plan is served on **both** of dario's endpoints: any client that speaks `/v1/chat/completions` can use it (Codex CLI, the OpenAI SDKs, your scripts), and so can any client that speaks `/v1/messages` (Claude Code, the Anthropic SDKs, agent runtimes). The harness never needs to know which subscription is behind it.
141
267
 
142
268
  ```bash
143
269
  dario add altman # prints an authorize URL; paste the redirect URL back
@@ -145,11 +271,7 @@ dario codex list
145
271
  dario codex remove altman
146
272
  ```
147
273
 
148
- `dario add altman` names whose plan you are attaching; `dario add amodei` attaches a Claude account instead. `dario codex add <name>` is the same command and still works.
149
-
150
- The browser lands on a `localhost` page that doesn't load — that's expected, nothing is listening there. Copy the whole address bar and paste it at the prompt; dario reads the code out of it. A bare code (or `code#state`) works too.
151
-
152
- Once an account is stored, a request naming a model that account may use is served from the subscription — on either endpoint:
274
+ `dario add altman` names whose plan you are attaching; `dario add amodei` attaches a Claude account instead. The browser lands on a `localhost` page that doesn't load — expected, nothing is listening there. Copy the whole address bar and paste it at the prompt; dario reads the code out of it.
153
275
 
154
276
  ```bash
155
277
  curl localhost:3456/v1/models | jq -r '.data[].id'
@@ -161,19 +283,13 @@ curl localhost:3456/v1/messages -H 'content-type: application/json' \
161
283
  -d '{"model":"gpt-5.5","max_tokens":64,"messages":[{"role":"user","content":"hi"}]}'
162
284
  ```
163
285
 
164
- **Model names are discovered, not hardcoded.** The set a ChatGPT subscription may use is per-account and moves; dario asks the backend which models this account lists, caches the answer, and advertises them on `GET /v1/models` so a client's model picker finds them. Anything not on that list — `gpt-4o` and friends — is untouched and still routes to a configured API-key backend as before. `codex:<model>` / `chatgpt:<model>` forces the route explicitly.
165
-
166
- Streaming, tool calls, and tool-result round trips work on both shapes: dario translates chat/completions **or** Messages into the Responses API the subscription backend speaks, and translates the stream back into `chat.completion.chunk` or Anthropic message events to match what the client asked in. There is no `/v1/responses` inbound yet.
286
+ **Model names are discovered, not hardcoded.** The set a ChatGPT subscription may use is per-account and moves; dario asks the backend which models this account lists, caches the answer, and advertises them on `GET /v1/models`. Anything not on that list (`gpt-4o` and friends) still routes to a configured API-key backend as before. `codex:<model>` / `chatgpt:<model>` forces the route.
167
287
 
168
- **Chat/completions fidelity:** text and `image_url` user-content parts (HTTPS URLs and data URIs, including the `detail` fidelity setting) carry through to Responses input items. The Codex subscription backend does not accept every chat field, so `response_format`, `stop`, `n`, `logprobs`, `stream_options` and other unmapped chat-only fields are intentionally lossy — and so are the sampling parameters `temperature`, `top_p`, `max_tokens` and `max_completion_tokens`, which translate cleanly but are then rejected by the backend and stripped before the request goes out. With `--verbose`, dario reports each field that does not reach Codex once per process.
288
+ Streaming, tool calls and tool-result round trips work on both shapes: dario translates chat/completions **or** Messages into the Responses API the subscription backend speaks, and translates the stream back into `chat.completion.chunk` or Anthropic message events. There is no `/v1/responses` inbound yet. The Codex backend does not accept every chat field, so `response_format`, `stop`, `n`, `logprobs`, `stream_options` and the sampling parameters `temperature`, `top_p`, `max_tokens`, `max_completion_tokens` are intentionally lossy; with `--verbose`, dario reports each field that does not reach Codex once per process. Codex accounts live in `~/.dario/codex-accounts/`, separate from the Claude pool.
169
289
 
170
290
  **Prompt caching:** the backend caches prompt prefixes of 1,024 tokens and up on its own; what dario adds is the `prompt_cache_key` that routes same-prefix requests to the cache that holds them, the way the Codex CLI does with its session id. A chat/completions client that sets its own key keeps it; an Anthropic-shape request gets one per Claude Code session (a hash of `metadata.user_id`, never the raw ids); anything else is keyed on its model, instructions and tool names, so repeated system prompts from any caller land together. Cached tokens come back as `prompt_tokens_details.cached_tokens` on chat/completions and as `cache_read_input_tokens` on `/v1/messages`, and show up in `/analytics` and the `-v` usage line like a Claude request's do.
171
291
 
172
- Codex accounts live in `~/.dario/codex-accounts/`, entirely separate from the Claude pool. Nothing about `dario login`, `dario accounts`, or Claude routing changes.
173
-
174
- ---
175
-
176
- ## Failover between subscriptions
292
+ ### Failover between subscriptions
177
293
 
178
294
  Two consumer plans, no API keys, and neither one able to take you down on its own.
179
295
 
@@ -181,13 +297,25 @@ Two consumer plans, no API keys, and neither one able to take you down on its ow
181
297
  dario proxy --pool-fallback=gpt-5.6-sol,claude-sonnet-5
182
298
  ```
183
299
 
184
- That is a **chain**, read left to right, and each provider takes the first entry it can actually serve. When the Claude pool is drained or cooling, the request is served as `gpt-5.6-sol` from your ChatGPT subscription. When the subscription is rate-limited or down, the request is handed back to the Claude pool as `claude-sonnet-5`. Every substituted response carries `x-dario-pool-fallback: <model>` — a silently swapped model family is exactly the surprise this project exists to avoid.
185
-
186
- A single-entry chain is one-way and means what it always meant, so an existing config is unaffected. Failover is entirely opt-in: without `--pool-fallback`, a drained pool still returns its honest 429/503.
187
-
188
- The Claude entry has to be a model the pool can actually serve. "Not a GPT model" is not the same thing, and swapping in a typo would trade a recoverable 429 for an unrecoverable 404 — so each entry is checked positively against the live model catalog and skipped if it fails, letting the real error surface. Canonical ids (`claude-sonnet-5`), long-context variants (`claude-sonnet-5[1m]`), catalog shorthands (`opus`, `sonnet1m`) and explicit provider prefixes (`claude:opus`, `anthropic:sonnet`) all qualify.
300
+ That is a **chain**, read left to right; each provider takes the first entry it can actually serve. When the Claude pool is drained or cooling, the request is served as `gpt-5.6-sol` from your ChatGPT subscription. When the subscription is rate-limited or down, the request is handed back to the Claude pool as `claude-sonnet-5`. Every substituted response carries `x-dario-pool-fallback: <model>` — a silently swapped model family is exactly the surprise this project exists to avoid.
301
+
302
+ ```mermaid
303
+ sequenceDiagram
304
+ participant T as Tool
305
+ participant D as dario
306
+ participant C as Claude pool
307
+ participant G as ChatGPT plan
308
+ T->>D: POST /v1/messages (claude-opus-5)
309
+ D->>C: seat with the most headroom
310
+ C-->>D: 429 rate_limit_error
311
+ D->>C: same request, next-best seat
312
+ C-->>D: 429 (pool drained)
313
+ D->>G: same request as gpt-5.6-sol (Messages → Responses)
314
+ G-->>D: 200
315
+ D-->>T: 200 + x-dario-pool-fallback: gpt-5.6-sol
316
+ ```
189
317
 
190
- Only a **429 or 5xx** fails over. A 400 surfaces to you, because a bad request that fails over just reproduces itself on the other provider and buries the real cause.
318
+ A single-entry chain is one-way and means what it always meant, so an existing config is unaffected. Failover is opt-in: without `--pool-fallback`, a drained pool still returns its honest 429/503. Only a **429 or 5xx** fails over; a 400 surfaces, because a bad request that fails over just reproduces itself on the other provider and buries the real cause. The Claude entry has to be a model the pool can actually serve, checked positively against the live catalog, so a typo can't trade a recoverable 429 for an unrecoverable 404.
191
319
 
192
320
  `dario doctor` tells you which of these you are actually in:
193
321
 
@@ -199,9 +327,10 @@ Only a **429 or 5xx** fails over. A 400 surfaces to you, because a bad request t
199
327
 
200
328
  That warning is the whole reason the check exists. Armed with nothing to fall back to is green on every other check and incapable of doing anything.
201
329
 
202
- ---
330
+ > [!NOTE]
331
+ > Upgrading from v5? Nothing to do. Every v6 feature is opt-in and a single-value `--pool-fallback` behaves exactly as it did. [CHANGELOG](CHANGELOG.md#600---2026-08-30)
203
332
 
204
- ## Shadow compare
333
+ ### Shadow compare
205
334
 
206
335
  Once either subscription can serve either wire shape, the interesting question stops being *can I reach GPT* and becomes *which of these is better at my work*. Benchmarks answer that badly. Your own traffic answers it well.
207
336
 
@@ -209,20 +338,14 @@ Once either subscription can serve either wire shape, the interesting question s
209
338
  curl localhost:3456/v1/messages \
210
339
  -H 'content-type: application/json' \
211
340
  -H 'x-dario-compare: gpt-5.6-sol' \
212
- -d '{"model":"claude-opus-4-8","max_tokens":1024,"messages":[…]}'
341
+ -d '{"model":"claude-opus-5","max_tokens":1024,"messages":[…]}'
213
342
  ```
214
343
 
215
- You get the Claude answer, exactly as you would have. Beside it, dario runs the same prompt past `gpt-5.6-sol` and writes both to `~/.dario/compare/<timestamp>-<model>.json`, in your own wire shape so you are comparing like with like rather than eyeballing across two formats.
344
+ You get the Claude answer, exactly as you would have. Beside it, dario runs the same prompt past `gpt-5.6-sol` and writes both to `~/.dario/compare/<timestamp>-<model>.json`, in your own wire shape, so you are comparing like with like. The comparison cannot degrade the request it observes: it only reads bytes already on their way out, your request is never held open for it, and a comparison that fails, times out or has nowhere to go is dropped with the record still written. Both sides are stored as raw payloads, because extracting text is where a bug would quietly make two answers look more alike than they are.
216
345
 
217
- The comparison cannot degrade the request it observes: it only reads bytes already on their way out, your request is never held open for it, and a comparison that fails, times out, or has nowhere to go is dropped with the record still written. Both sides are stored as raw payloads — extracting text is where a bug would quietly make two answers look more alike than they are.
346
+ ## Many seats, one endpoint
218
347
 
219
- Compares run against a Codex account. Comparing against the Claude pool would occupy a seat for a request nobody is waiting on.
220
-
221
- ---
222
-
223
- ## Multi-account pool
224
-
225
- **In v5 every dario is a pool** — a plain `dario login` is a pool of one, no separate mode to switch on. One Claude subscription has a ceiling; hold more than one seat — a personal Max and a work Max, a couple of Pros, team seats — and the same `localhost:3456` routes every request to whichever seat has the most headroom, live, per request. A single `dario accounts add` even bootstraps a servable proxy with no `dario login` step:
348
+ **Every dario is a pool.** A plain `dario login` is a pool of one; there is no separate mode to switch on. Hold more than one seat — a personal Max and a work Max, a couple of Pros, team seats — and the same `localhost:3456` routes every request to whichever seat has the most headroom, live, per request.
226
349
 
227
350
  ```bash
228
351
  dario accounts add work
@@ -230,95 +353,87 @@ dario accounts add personal
230
353
  dario proxy
231
354
  ```
232
355
 
233
- Three things it does that a round-robin doesn't:
234
-
235
- - **Per-model headroom routing.** Anthropic meters each model family separately — a `5h` bucket, a `7d` bucket, and a per-model `7d_<family>` bucket. dario reads all of them off every response and routes each request by the bucket that governs it: an Opus call to the seat with Opus room, a Sonnet call to the seat with Sonnet room, independently. Plan tiers mix freely — dario cares about headroom, not tier.
236
- - **Session stickiness.** Claude's prompt cache is scoped to `{account × cache key}`, so rotating a long conversation across seats on headroom alone re-pays cache-create every turn — a **5–10× token-cost multiplier** on the cached portion. dario pins each conversation to one account (hashed from its first message, deterministic) for the life of the session, and rebinds only when that account is exhausted.
237
- - **In-flight 429 failover.** A seat hits its wall mid-request and dario retries the *same request* against the next-best account before your client ever sees an error. The sticky binding follows to the new seat, so the next turn doesn't re-select the cold one.
238
-
239
- ```
240
- ┌─ dario ─────────────────────────────[ q quit · Tab next · ? help ]──┐
241
- │ Status Config Analytics Hits ▎Accounts▎ Backends │
242
- ├─────────────────────────────────────────────────────────────────────┤
243
- │ ACCOUNTS — 3 pooled · routing by headroom │
244
- │ │
245
- │ work Max 20x 5h ██░░░░░ 12% 7d ████░░░ 41% ← next opus │
246
- │ personal Max 5x 5h █████░░ 78% 7d ██████░ 88% │
247
- │ side Pro 5h ░░░░░░░ 3% 7d █░░░░░░ 9% ← next sonnet│
248
- │ │
249
- │ sticky bindings: 4 active · 429 failovers (1h): 2 │
250
- └─────────────────────────────────────────────────────────────────────┘
251
- ```
356
+ <picture>
357
+ <source media="(prefers-color-scheme: dark)" srcset=".github/readme/tui-accounts-dark.svg">
358
+ <img alt="The dario TUI Accounts tab: a table of pooled seats (work, personal, side) with token expiry, 5-hour and 7-day utilization, and status." src=".github/readme/tui-accounts-light.svg" width="100%">
359
+ </picture>
252
360
 
253
- `dario accounts {add,list,remove}` from any shell, or provision entirely over HTTP with the headless [admin API](#capabilities) — zero-console Docker / k8s / Pi installs included. Routing internals and the live `/accounts` + `/analytics` endpoints: [`docs/multi-account-pool.md`](./docs/multi-account-pool.md); covered end-to-end by [`test/pool-e2e.mjs`](./test/pool-e2e.mjs).
361
+ Three things it does that a round-robin doesn't:
254
362
 
255
- ---
363
+ - **Per-model headroom routing.** Anthropic meters each model family separately: a `5h` bucket, a `7d` bucket and a per-model `7d_<family>` bucket. dario reads all of them off every response and routes each request by the bucket that governs it — an Opus call to the seat with Opus room, a Sonnet call to the seat with Sonnet room, independently. Plan tiers mix freely; dario cares about headroom, not tier.
364
+ - **Session stickiness.** Claude's prompt cache is scoped to `{account × cache key}`, so rotating a long conversation across seats on headroom alone re-pays cache-create every turn, a **5–10× token-cost multiplier** on the cached portion. dario pins each conversation to one seat (hashed from its first message, deterministic) for the life of the session and rebinds only when that seat is exhausted.
365
+ - **In-flight 429 failover.** A seat hits its wall mid-request and dario retries the *same request* against the next-best seat before your client ever sees an error. The sticky binding follows, so the next turn doesn't re-select the cold one.
256
366
 
257
- ## Overage guard
367
+ `--pool-strategy=fill-first` concentrates new conversations on one seat until it drains, for primary/backup setups. Refresh tokens expire about 28 days after the original grant regardless of rotation, so every seat's grant age is tracked and surfaced in `dario accounts list`, `dario doctor` and `GET /accounts` before it becomes a silent outage. Provision over HTTP with the headless [admin API](./docs/admin-api.md); pin one request to one seat with `dario accounts check <alias>`. Internals and the live `/accounts` + `/analytics` endpoints: [multi-account-pool.md](./docs/multi-account-pool.md); covered end-to-end by [`test/pool-e2e.mjs`](./test/pool-e2e.mjs).
258
368
 
259
- During normal operation, a subscriber should never see a single response billed outside their subscription pool. If one is, something is wrong — wire-shape drift, an account misconfig, a change upstream — and forwarding more requests in the same shape either bleeds real money (accounts with extra-usage enabled) or returns a wall of rejections. The first hit is the signal; the rest are damage.
369
+ ### Watch it happen
260
370
 
261
- So the moment any upstream response bills to something other than your subscription pool, dario **halts the proxy**. The check is an allow-list, not a match on one string: anything that isn't a known subscription claim (`five_hour` / `seven_day` and their fallbacks) and isn't the `unknown` no-header sentinel trips it — so a billing bucket dario has never seen still halts. Subsequent requests return `503` with an Anthropic-shaped error body until you run `dario resume`, press `R` in the TUI, or the cooldown clears (default 30 min). The halt shows across the TUI, fires a best-effort OS notification, and emits named SSE events. Tune it via `~/.dario/config.json` → `overageGuard` or `--overage-behavior=warn` / `--no-overage-guard` / `--overage-cooldown=<ms>`. (In upstream-API-key passthrough mode — `ANTHROPIC_UPSTREAM_API_KEY` — the guard is off; `api` billing is the point there.) Verified end-to-end by [`test/overage-guard-e2e-live.mjs`](./test/overage-guard-e2e-live.mjs). Background: [#288](https://github.com/askalf/dario/issues/288).
371
+ Type `dario` with no arguments for a full-screen control panel: live request stream, per-model burn rate, rate-limit utilization per seat, billing-bucket breakdown, and an in-place config editor that writes `~/.dario/config.json`. Pure ANSI, zero new runtime deps. <kbd>Tab</kbd> moves between tabs, <kbd>r</kbd> refreshes, <kbd>R</kbd> resumes a halted overage guard, <kbd>q</kbd> quits.
262
372
 
263
- ---
373
+ <picture>
374
+ <source media="(prefers-color-scheme: dark)" srcset=".github/readme/tui-analytics-dark.svg">
375
+ <img alt="The dario TUI Analytics tab: requests per minute, tokens in and out, thinking tokens, average latency, subscription percentage, a per-model bar chart, per-account rate-limit bars for the 5-hour and 7-day windows, and a billing breakdown." src=".github/readme/tui-analytics-light.svg" width="100%">
376
+ </picture>
264
377
 
265
- ## Staying current: dario tracks a moving target
378
+ <sub>Both screenshots are rendered from the real TUI against a fixture proxy by <a href="scripts/readme/tui.mjs"><code>scripts/readme/tui.mjs</code></a>, so a layout change shows up here instead of rotting a mock-up. The numbers are illustrative; the pixels are not.</sub>
266
379
 
267
- Claude Code's request shape changes between releases — new betas, tool renames, per-model thinking configs — usually with no subscriber-facing note. dario doesn't *guess* that shape: it captures it live from your own installed `claude` binary on every startup, diffs it against each upstream release, and replays it byte-for-byte. That's why your subscription routes the same through dario as it does through Claude Code itself — the request that leaves your machine *is* the shape your plan expects. Details: [`docs/wire-fidelity.md`](./docs/wire-fidelity.md) · [#13](https://github.com/askalf/dario/discussions/13) · [#14](https://github.com/askalf/dario/discussions/14).
380
+ ## It tracks a moving target
268
381
 
269
- Keeping that current is the whole job, and it's automated. Three watchers run unattended:
382
+ Claude Code's request shape changes between releases — new betas, tool renames, per-model thinking configs — usually with no subscriber-facing note. dario doesn't *guess* that shape: it captures it live from your own installed `claude` binary on every startup, diffs it against each upstream release, and replays it faithfully. That's why your subscription routes the same through dario as it does through Claude Code itself: the request that leaves your machine *is* the shape your plan expects. Details: [wire-fidelity.md](./docs/wire-fidelity.md) · [#13](https://github.com/askalf/dario/discussions/13) · [#14](https://github.com/askalf/dario/discussions/14).
270
383
 
271
- - **npm-release drift** — [`cc-drift-watch.yml`](./.github/workflows/cc-drift-watch.yml) catches each new Claude Code npm release; [`cc-drift-auto-release.yml`](./.github/workflows/cc-drift-auto-release.yml) auto-drafts, merges, and ships within minutes.
272
- - **Same-binary remote-config drift** — [`cc-drift-template-watch.yml`](./.github/workflows/cc-drift-template-watch.yml) runs on a self-hosted runner with a live Claude session (the only place this class is visible) and opens an auto-rebake PR with the diff inline. Anthropic ships changes through Claude Code's *remote config*, not just npm.
273
- - **Rate-map drift** — [`cc-billing-classifier-canary.yml`](./.github/workflows/cc-billing-classifier-canary.yml) sends one live request a day and asserts the response still bills to a subscription bucket.
384
+ Keeping that current is the whole job, and it's automated. These watchers run unattended; each badge is the live status of that workflow's latest run, and its label is the cadence:
274
385
 
275
- Guarded by a PR-time compat gate that runs the full suite against a live proxy before any wire-shape change merges, and a liveness alarm if a watcher goes quiet. A few recent changes the watchers caught and shipped fixes for, same-day:
386
+ | Watcher | Catches | Live |
387
+ |---|---|---|
388
+ | [`cc-drift-watch`](./.github/workflows/cc-drift-watch.yml) | A new Claude Code npm release that changes the wire shape. Auto-drafts the fix; [`cc-drift-auto-release`](./.github/workflows/cc-drift-auto-release.yml) merges and ships it within minutes. | ![hourly](https://img.shields.io/github/actions/workflow/status/askalf/dario/cc-drift-watch.yml?branch=master&label=hourly) |
389
+ | [`cc-drift-template-watch`](./.github/workflows/cc-drift-template-watch.yml) | Same-binary *remote-config* drift, which no npm diff can see. Runs against a live Claude session on a self-hosted runner and opens a rebake PR with the diff inline. | ![every 30 min](https://img.shields.io/github/actions/workflow/status/askalf/dario/cc-drift-template-watch.yml?branch=master&label=every%2030%20min) |
390
+ | [`cc-billing-classifier-canary`](./.github/workflows/cc-billing-classifier-canary.yml) | Classifier drift: one real request a day must still bill to a subscription bucket. | ![daily](https://img.shields.io/github/actions/workflow/status/askalf/dario/cc-billing-classifier-canary.yml?branch=master&label=daily) |
391
+ | [`wire-drift-self-hosted`](./.github/workflows/wire-drift-self-hosted.yml) | Per-model beta headers and billing blocks the installed `claude` actually sends, model by model. | ![daily](https://img.shields.io/github/actions/workflow/status/askalf/dario/wire-drift-self-hosted.yml?branch=master&label=daily) |
392
+ | [`sdk-drift-watch`](./.github/workflows/sdk-drift-watch.yml) | Agent SDK / Stainless pins drifting from what the template assumes. | ![daily](https://img.shields.io/github/actions/workflow/status/askalf/dario/sdk-drift-watch.yml?branch=master&label=daily) |
393
+ | [`pricing-drift-watch`](./.github/workflows/pricing-drift-watch.yml) | dario's pricing table drifting from Anthropic's published rates, so the TUI's cost figures stay honest. | ![daily](https://img.shields.io/github/actions/workflow/status/askalf/dario/pricing-drift-watch.yml?branch=master&label=daily) |
394
+ | [`codex-drift-watch`](./.github/workflows/codex-drift-watch.yml) | The ChatGPT backend's model list or wire contract moving under the translator. | ![daily](https://img.shields.io/github/actions/workflow/status/askalf/dario/codex-drift-watch.yml?branch=master&label=daily) |
395
+ | [`cc-oauth-health`](./.github/workflows/cc-oauth-health.yml) | The maintainer's own production proxy going unhealthy on any axis. | ![every 30 min](https://img.shields.io/github/actions/workflow/status/askalf/dario/cc-oauth-health.yml?branch=master&label=every%2030%20min) |
396
+ | [`dario-doctor-watch`](./.github/workflows/dario-doctor-watch.yml) | Runtime drift only a live `dario doctor --obedience` surfaces: identity, obedience, usage buckets. | ![every 6 h](https://img.shields.io/github/actions/workflow/status/askalf/dario/dario-doctor-watch.yml?branch=master&label=every%206%20h) |
397
+ | [`deployed-version-watch`](./.github/workflows/deployed-version-watch.yml) | Publishing is not deploying: is what's running what was last released? | ![hourly](https://img.shields.io/github/actions/workflow/status/askalf/dario/deployed-version-watch.yml?branch=master&label=hourly) |
398
+ | [`cc-drift-watcher-liveness`](./.github/workflows/cc-drift-watcher-liveness.yml) | The watcher itself going quiet. Lives on GitHub-hosted infrastructure on purpose, so it survives the failures it watches for. | ![every 2 h](https://img.shields.io/github/actions/workflow/status/askalf/dario/cc-drift-watcher-liveness.yml?branch=master&label=every%202%20h) |
399
+
400
+ Guarded by a PR-time compat gate that runs the full suite against a live proxy before any wire-shape change merges. A few changes the watchers caught and shipped fixes for, same day:
276
401
 
277
402
  | Change (no subscriber-facing note) | Effect | dario shipped |
278
403
  |---|---|---|
279
404
  | `context-1m` dropped from the default beta set on the OAuth path | Subscription requests default to the 200K window on Sonnet/Opus | v3.38.3–4 |
280
405
  | `thinking: {type:"adaptive"}` gated per-model server-side | Sonnet/Opus 4-5 400 every request through any proxy | [v3.38.5](https://github.com/askalf/dario/pull/273) |
281
- | Per-model `anthropic-beta` sets (opus 10, sonnet 9, haiku 6 — they track the baked base, so counts shift when CC's set does) | Proxies sending one set diverge for non-opus models | [v4.8.53](https://github.com/askalf/dario/pull/478) |
406
+ | Per-model `anthropic-beta` sets | Proxies sending one set diverge for non-Opus models | [v4.8.53](https://github.com/askalf/dario/pull/478) |
282
407
 
283
- The full ledger lives in the [CHANGELOG](CHANGELOG.md). Setup + walkthrough: [`docs/drift-monitor.md`](./docs/drift-monitor.md). Residual manual cases — OAuth rotation, runner re-registration — are in the [recovery runbook](./docs/recovery.md).
408
+ The full ledger lives in the [CHANGELOG](CHANGELOG.md), 500+ releases since April 2026. Setup and walkthrough: [drift-monitor.md](./docs/drift-monitor.md). The residual manual cases — OAuth rotation, runner re-registration — are in the [recovery runbook](./docs/recovery.md).
284
409
 
285
- ---
286
-
287
- ## The billing split — a contingency dario is built for
410
+ ## Guardrails
288
411
 
289
- On **2026-05-13** Anthropic [announced](https://support.claude.com/en/articles/15036540-use-the-claude-agent-sdk-with-your-claude-plan) that, from 2026-06-15, Agent-SDK and `claude -p` (headless) traffic would leave the subscription pool for a small separate monthly credit ($20 / $100 / $200 by plan), then metered API rates. **They paused it before that date** — those surfaces still bill subscription today, and Anthropic says it will give advance notice before any revised version. Nothing changed; no credits were issued.
412
+ ### Overage guard
290
413
 
291
- The split isn't live, but it was announced once on short notice and could return — so dario is built for it either way. Every request is rebuilt into interactive Claude Code shape before it leaves your machine (and, with `--stealth`, the response-correlated timing an interactive session has), so your traffic sits in the subscription pool whether a split is paused or live. The [daily canary](#staying-current-dario-tracks-a-moving-target) is the tripwire: it surfaces a revived split within a day instead of on a surprise invoice. Verify on your own machine right now — `dario doctor --usage` fires one request and prints the rate-limit headers; `representative-claim` should read `five_hour` or `seven_day` (both subscription buckets). Full timeline: [`docs/why-now-2026-06.md`](./docs/why-now-2026-06.md).
414
+ During normal operation, a subscriber should never see a single response billed outside their subscription pool. If one is, something is wrong — wire-shape drift, an account misconfig, a change upstream — and forwarding more requests in the same shape either bleeds real money (accounts with extra usage enabled) or returns a wall of rejections. The first hit is the signal; the rest are damage.
292
415
 
293
- ---
416
+ So the moment any upstream response bills to something other than your subscription pool, dario **halts the proxy**. The check is an allow-list, not a match on one string: anything that isn't a known subscription claim (`five_hour` / `seven_day` and their fallbacks) and isn't the `unknown` no-header sentinel trips it, so a billing bucket dario has never seen still halts. Subsequent requests return `503` with an Anthropic-shaped error body until you run `dario resume`, press <kbd>R</kbd> in the TUI, or the cooldown clears (default 30 min). The halt shows across the TUI, fires a best-effort OS notification, and emits named SSE events. Tune it via `~/.dario/config.json` → `overageGuard`, or `--overage-behavior=warn` / `--no-overage-guard` / `--overage-cooldown=<ms>`. In upstream-API-key passthrough mode (`ANTHROPIC_UPSTREAM_API_KEY`) the guard is off; `api` billing is the point there. Verified end-to-end by [`test/overage-guard-e2e-live.mjs`](./test/overage-guard-e2e-live.mjs). Background: [#288](https://github.com/askalf/dario/issues/288).
294
417
 
295
- ## Capabilities
418
+ ### The billing split, a contingency dario is built for
296
419
 
297
- - **Multi-account pool.** Several Claude seats behind one endpoint, routed by per-model headroom with sticky-session cache locality and in-flight 429 failover. → [Multi-account pool](#multi-account-pool)
298
- - **Byte-faithful passthrough for real Claude Code.** A genuine CC request already *is* the CC shape, so dario forwards it verbatim — system prompt, tools, thinking, key order untouched — keeping only its billing tag, identity, and cache breakpoints. Covers CC's whole family: the main loop, its Task/Agent sub-agents, and the permission classifier. Non-CC clients get the full template rebuild that keeps them routing. Background: [#678](https://github.com/askalf/dario/issues/678).
299
- - **Headless admin API (`DARIO_ADMIN=1`).** Provision and manage pool accounts entirely over HTTP — start with zero accounts, `POST /admin/login/start`, paste the code back, routable the moment the `200` lands (live hot-reload, no restart). Token-gated even on loopback, audit-logged, rate-limited. Built for Docker / k8s / Pi. → [`docs/admin-api.md`](./docs/admin-api.md)
300
- - **More than one instance, same accounts.** Anthropic's refresh tokens are single-use, so two replicas refreshing the same account leaves one holding a dead token. An optional refresh lock (Redis or Cloudflare backend, same contract, fails open) makes the loser adopt the winner's fresh credentials instead. Safe credential sharing — *not* full HA: rate-limit accounting and sticky routing stay per-instance. → [`docs/multi-instance.md`](./docs/multi-instance.md)
301
- - **Runs any agent.** A 64-entry schema-verified `TOOL_MAP` pre-maps Cline, Roo, Kilo, Cursor, Windsurf, Continue, Copilot, OpenHands, OpenClaw, Hermes, and [hands](https://github.com/askalf/hands) tool names to CC's native set — no flag, no validator errors. MCP tools (`mcp__server__tool`) forward verbatim. [Compatibility matrix](./docs/integrations/compat-matrix.md) · [agent-compat.md](./docs/integrations/agent-compat.md).
302
- - **Behavioral stealth (`--stealth`).** Adds *when* a request arrives to *what* it looks like — response-length-correlated think time and session-start latency. → [`docs/wire-fidelity.md`](./docs/wire-fidelity.md)
303
- - **VPN / egress routing.** Route dario's upstream traffic through a VPN without putting the whole host on one. → [`docs/vpn-routing.md`](./docs/vpn-routing.md)
304
- - **Recover output (`--system-prompt=partial`).** Strips CC's tone/verbosity constraints for 1.2–2.8× more output on open-ended work, without changing which pool you bill to. [#183](https://github.com/askalf/dario/discussions/183) · [`docs/system-prompt.md`](./docs/system-prompt.md)
305
- - **Client-shape overrides.** `--honor-client-thinking` passes a client's own `thinking` block through unchanged; `--preserve-output-format` carries a client's `output_config.format` JSON schema through so structured-output SDKs (e.g. the Vercel AI SDK's `generateObject`) get schema-constrained output. Both off by default.
306
- - **Reachable from inside CC / any MCP client.** `dario subagent install` registers a CC sub-agent for in-session diagnostics; `dario mcp` exposes dario as a read-only MCP server. → [`docs/sub-agent.md`](./docs/sub-agent.md) · [`docs/mcp-server.md`](./docs/mcp-server.md)
420
+ On **2026-05-13** Anthropic [announced](https://support.claude.com/en/articles/15036540-use-the-claude-agent-sdk-with-your-claude-plan) that, from 2026-06-15, Agent SDK and `claude -p` (headless) traffic would leave the subscription pool for a small separate monthly credit, then metered API rates. **They paused it before that date.** Those surfaces still bill subscription today, and Anthropic says it will give advance notice before any revised version. Nothing changed; no credits were issued.
307
421
 
308
- ---
422
+ The split isn't live, but it was announced once on short notice and could return, so dario is built for it either way. Every request is rebuilt into interactive Claude Code shape before it leaves your machine (and, with `--stealth`, the response-correlated timing an interactive session has), so your traffic sits in the subscription pool whether a split is paused or live. The daily canary above is the tripwire: it surfaces a revived split within a day instead of on a surprise invoice. Verify on your own machine right now: `dario doctor --usage` fires one request and prints the rate-limit headers; `representative-claim` should read `five_hour` or `seven_day`, both subscription buckets. Full timeline: [why-now-2026-06.md](./docs/why-now-2026-06.md).
309
423
 
310
424
  ## Trust & transparency
311
425
 
312
426
  | Signal | Status |
313
427
  |---|---|
314
- | Source | **~31k** lines of TypeScript across **67** files — auditable in a weekend (v5 removed shim; the pool is the one code path) |
428
+ | Source | **~31k** lines of TypeScript across **67** files, auditable in a weekend. One credential path since v5: the pool. |
315
429
  | Dependencies | **0 runtime.** Verify: `npm ls --production` |
316
- | Provenance | Every release [SLSA-attested](https://www.npmjs.com/package/@askalf/dario) via GitHub Actions + Sigstore |
317
- | Scanning | [CodeQL](https://github.com/askalf/dario/actions/workflows/codeql.yml) on every push and weekly |
318
- | Tests | **132 test files**, 125 run in parallel by `test/all.test.mjs` (e2e / compat / stealth opt out and have their own entry points) — green on every release |
430
+ | Provenance | Every release [SLSA-attested](https://www.npmjs.com/package/@askalf/dario) via GitHub Actions + Sigstore, published with OIDC trusted publishing — no long-lived npm token exists to leak |
431
+ | Scanning | [CodeQL](https://github.com/askalf/dario/actions/workflows/codeql.yml) on every push and weekly · [ClusterFuzzLite](./.github/workflows/cflite.yml) fuzzes the SSE translator and rejection parsers weekly · [OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/askalf/dario) and [Best Practices](https://www.bestpractices.dev/projects/13638) badges above are live |
432
+ | Tests | **176 test files** run in parallel by `npm test` on Node 18, 20 and 22; the live e2e / compat / stealth suites have their own entry points. Green on every release |
319
433
  | Credentials | Your own subscription tokens, never logged, redacted from errors, `0600` on disk in `0700` dirs |
320
- | Network | Binds `127.0.0.1` by default; upstream only to configured backends over HTTPS; hardcoded SSRF allow-list |
434
+ | Network | Binds `127.0.0.1` by default; upstream only to configured backends over HTTPS; hardcoded SSRF allow-list; refuses a non-loopback bind without `DARIO_API_KEY` |
321
435
  | Telemetry | **None.** No analytics, no tracking, nothing phones home |
436
+ | This README | CI fails if the line count above drifts from `src/` or a link or anchor here stops resolving ([`check-readme-line-count.mjs`](./scripts/check-readme-line-count.mjs), [`check-readme-links.mjs`](./scripts/check-readme-links.mjs)); the screenshots are generated from the real TUI ([how](./scripts/readme/README.md)) |
322
437
 
323
438
  ```bash
324
439
  npm audit signatures
@@ -326,13 +441,11 @@ npm view @askalf/dario dist.integrity
326
441
  cd $(npm root -g)/@askalf/dario && npm ls --production
327
442
  ```
328
443
 
329
- ---
444
+ Security reports go to **security@askalf.org**, not a public issue: [SECURITY.md](SECURITY.md). API stability commitments (`@stable` / `@experimental` / `@deprecated`, deprecation cycles): [STABILITY.md](STABILITY.md).
330
445
 
331
446
  ## Honest about what this is
332
447
 
333
- dario uses your own subscription credentials, authenticates you as you, and impersonates nobody. What it changes is the **client** — it rebuilds each request into the exact shape Claude Code emits (captured live from your installed binary) so your plan routes the same no matter which tool actually sent it. Be clear-eyed on both sides of that: it's a transparency tool, in that it documents request behavior Anthropic doesn't publish for subscribers — and it's also, plainly, running through your subscription traffic that Anthropic's own tools bill differently. Both are true. dario is unofficial and unaffiliated ([DISCLAIMER.md](./DISCLAIMER.md)); decide with both in view.
334
-
335
- ---
448
+ dario uses your own subscription credentials, authenticates you as you, and impersonates nobody. What it changes is the **client**: it rebuilds each request into the exact shape Claude Code emits (captured live from your installed binary) so your plan routes the same no matter which tool actually sent it. Be clear-eyed on both sides of that. It's a transparency tool, in that it documents request behavior Anthropic doesn't publish for subscribers, and it's also, plainly, running through your subscription traffic that Anthropic's own tools bill differently. Both are true. dario is unofficial and unaffiliated ([DISCLAIMER.md](./DISCLAIMER.md)); decide with both in view.
336
449
 
337
450
  ## Will my account get suspended?
338
451
 
@@ -340,109 +453,184 @@ The most common question about dario, and it deserves a straight answer: **I can
340
453
 
341
454
  **What dario does:**
342
455
 
343
- - **Runs entirely on your machine.** Your subscription token never touches my servers or anyone else's — requests go straight from your computer to Anthropic.
344
- - **Authenticates as you, with your own Claude login** — the same OAuth credential Claude Code itself uses. It impersonates nobody and shares nothing.
456
+ - **Runs entirely on your machine.** Your subscription token never touches my servers or anyone else's; requests go straight from your computer to Anthropic.
457
+ - **Authenticates as you, with your own Claude login**, the same OAuth credential Claude Code itself uses. It impersonates nobody and shares nothing.
345
458
  - **Doesn't modify your account, billing, or subscription settings.**
346
- - **Sends requests in the shape the official client sends them** — rebuilt from your own installed binary, not spoofed from a hardcoded fake.
347
- - **Reports nothing, anywhere.** No telemetry, no analytics, nothing phones home — [verifiable in the source](#trust--transparency), which is the point of keeping it auditable in an afternoon.
459
+ - **Sends requests in the shape the official client sends them**, rebuilt from your own installed binary, not spoofed from a hardcoded fake.
460
+ - **Reports nothing, anywhere.** No telemetry, no analytics, nothing phones home; [verifiable in the source](#trust--transparency), which is the point of keeping it auditable in a weekend.
348
461
 
349
- **What dario does that Claude Code doesn't:** it lets tools *other than* Claude Code use that subscription. That's the whole point of it, and it's also the part that sits outside what Anthropic's own client does. Whether that falls within your plan's terms is Anthropic's call, not mine — read [their terms](https://www.anthropic.com/legal/consumer-terms), read [DISCLAIMER.md](./DISCLAIMER.md), and decide deliberately.
462
+ **What dario does that Claude Code doesn't:** it lets tools *other than* Claude Code use that subscription. That's the whole point of it, and it's also the part that sits outside what Anthropic's own client does. Whether that falls within your plan's terms is Anthropic's call, not mine. Read [their terms](https://www.anthropic.com/legal/consumer-terms), read [DISCLAIMER.md](./DISCLAIMER.md), and decide deliberately.
350
463
 
351
- **On policy risk specifically:** Anthropic's position on third-party clients has moved before and can move again. dario is built to surface that fast rather than paper over it — see [The billing split](#the-billing-split--a-contingency-dario-is-built-for) for the contingency already in place and the daily canary watching for it.
464
+ **On policy risk specifically:** Anthropic's position on third-party clients has moved before and can move again. dario is built to surface that fast rather than paper over it; see [the billing split](#the-billing-split-a-contingency-dario-is-built-for) for the contingency already in place and the daily canary watching for it.
352
465
 
353
466
  Ongoing discussion, including other users' experiences: [#724](https://github.com/askalf/dario/discussions/724).
354
467
 
355
- ---
356
-
357
468
  ## Who it's for
358
469
 
359
- **Best fit:** developers juggling multiple LLM tools and per-tool API keys · Claude Pro/Max subscribers who want their plan usable everywhere, not just in Claude Code · teams running local/hosted OpenAI-compat servers who want one stable local endpoint · Agent SDK users who want subscription routing with zero code change (`baseURL: 'http://localhost:3456'`) · power users wanting multi-account pooling + 429 failover.
470
+ **Best fit:** developers juggling multiple LLM tools and per-tool API keys · Claude Pro/Max subscribers who want their plan usable everywhere, not just in Claude Code · ChatGPT Plus/Pro subscribers who want their plan in OpenAI-compatible harnesses · teams running local or hosted OpenAI-compat servers who want one stable local endpoint · Agent SDK users who want subscription routing with zero code change · power users wanting multi-account pooling with 429 failover.
360
471
 
361
- **Not a fit:** you need vendor-managed production SLAs (use the provider APIs) · you want a hosted multi-tenant team platform with dashboards / SSO (dario is a single-owner local proxy) · you want a chat UI (use claude.ai).
472
+ **Not a fit:** you need vendor-managed production SLAs (use the provider APIs) · you want a hosted multi-tenant team platform with dashboards and SSO (dario is a single-owner local proxy) · you want a chat UI (use claude.ai).
362
473
 
363
- ---
474
+ **How it compares.** Only one of these routes a consumer subscription; the others route API keys, and that is the whole split.
364
475
 
365
- ## Commands
476
+ | Tool | What it is | When it wins |
477
+ |---|---|---|
478
+ | **dario** | Local proxy that routes your Claude and ChatGPT plans, plus any OpenAI-compatible API | You already pay for a plan and want every tool on your machine to use it |
479
+ | **LiteLLM** | Python SDK + proxy, 100+ providers via API keys, enterprise features | You have API keys, want central spend controls, or run a hosted multi-tenant service |
480
+ | **OpenRouter** | Hosted aggregator, one API key for hundreds of models | You want model breadth and are fine with pay-per-token |
481
+ | **Kong AI Gateway** | Enterprise on-prem API gateway for LLMs | You already run Kong and need AI traffic under the same governance |
366
482
 
367
- `dario` (TUI) · `login` · `proxy` · `doctor` · `add {altman,amodei}` · `accounts {list,add,remove}` · `backend {list,add,remove}` · `codex {list,add,remove}` · `mcp` · `subagent {install,status,remove}` · `usage` · `config` · `upgrade` · `status` · `refresh` · `resume` · `logout` · `help`
483
+ Longer version, with specifics: [#68](https://github.com/askalf/dario/discussions/68).
368
484
 
369
- Per-flag reference: [`docs/commands.md`](./docs/commands.md) · env vars grouped by task, for Docker / k8s / systemd: [`docs/configuration.md`](./docs/configuration.md) · SDK examples + per-tool setup: [`docs/usage.md`](./docs/usage.md)
485
+ ## Commands
370
486
 
371
- ---
487
+ | Command | What it does |
488
+ |---|---|
489
+ | `dario` | The TUI: status, config editor, analytics, hits, accounts, backends |
490
+ | `dario login [--manual]` | Log in to your Claude plan. Picks up Claude Code's credentials or runs its own OAuth flow; `--manual` for SSH / containers |
491
+ | `dario proxy` | Start the local endpoint on `:3456` |
492
+ | `dario doctor [--usage] [--probe] [--json]` | One aggregated health report: runtime/TLS, template and drift, OAuth, pool, refresh-grant age, failover readiness, backends |
493
+ | `dario add altman` / `dario add amodei` | Attach a ChatGPT plan / a Claude account, by whose it is |
494
+ | `dario accounts list` / `add` / `remove` / `check <alias>` | Pool management; `check` sends one pinned request per model through the running proxy |
495
+ | `dario backend list` / `add` / `remove` | OpenAI-compatible API-key backends |
496
+ | `dario codex list` / `add` / `remove` | ChatGPT accounts (the long form of `dario add altman`) |
497
+ | `dario usage` · `dario config` · `dario status` | Burn rate for the last hour · effective config, redacted · token health |
498
+ | `dario resume` · `dario refresh` · `dario logout` · `dario upgrade` | Clear an overage halt · force a token refresh · delete credentials · safe self-update |
499
+ | `dario mcp` · `dario subagent install` | Reach dario from inside any MCP client, or from inside a Claude Code session, read-only |
500
+
501
+ | Endpoint | Description |
502
+ |---|---|
503
+ | `POST /v1/messages` · `POST /v1/chat/completions` | The two wire shapes, any plan behind either |
504
+ | `GET /v1/models` | Live model list: the Claude catalog plus whatever your ChatGPT plan lists |
505
+ | `GET /health` · `GET /livez` | Serviceability (503 when not) · liveness. `/health?probe=1` sends one real request |
506
+ | `GET /status` · `GET /accounts` · `GET /analytics` | OAuth detail · per-seat utilization and grant age · per-account / per-model stats and burn rate |
507
+
508
+ Every flag and env var: [commands.md](./docs/commands.md) · env vars grouped by task, for Docker / k8s / systemd: [configuration.md](./docs/configuration.md) · SDK examples: [usage.md](./docs/usage.md).
509
+
510
+ <details>
511
+ <summary><strong>More knobs</strong> — stealth timing, system-prompt modes, client-shape overrides, VPN egress, MCP</summary>
512
+
513
+ - **Behavioral stealth (`--stealth`).** Adds *when* a request arrives to *what* it looks like: response-length-correlated think time and session-start latency. [wire-fidelity.md](./docs/wire-fidelity.md)
514
+ - **Recover output (`--system-prompt=partial`).** Strips Claude Code's tone and verbosity constraints for 1.2–2.8× more output on open-ended work, without changing which pool you bill to. [#183](https://github.com/askalf/dario/discussions/183) · [system-prompt.md](./docs/system-prompt.md)
515
+ - **Client-shape overrides.** `--honor-client-thinking` passes a client's own `thinking` block through; `--preserve-output-format` carries a client's `output_config.format` JSON schema through so structured-output SDKs get schema-constrained output. Both off by default.
516
+ - **Runs any agent.** A 64-entry schema-verified `TOOL_MAP` pre-maps Cline, Roo, Kilo, Cursor, Windsurf, Continue, Copilot, OpenHands, OpenClaw and Hermes tool names to Claude Code's native set; MCP tools (`mcp__server__tool`) forward verbatim. Custom schemas: `--preserve-tools` or `--hybrid-tools`. [agent-compat.md](./docs/integrations/agent-compat.md)
517
+ - **VPN / egress routing.** Route dario's upstream traffic through a VPN without putting the whole host on one. [vpn-routing.md](./docs/vpn-routing.md)
518
+ - **More than one instance, same accounts.** Refresh tokens are single-use, so two replicas refreshing the same seat leave one holding a dead token; the optional refresh lock (Redis or Cloudflare) makes the loser adopt the winner's credentials. [multi-instance.md](./docs/multi-instance.md)
519
+ - **PII redaction in front of dario.** Pair it with [cordon](https://github.com/askalf/cordon): [integrations/cordon.md](./docs/integrations/cordon.md)
520
+ - **Reachable from inside Claude Code or any MCP client.** `dario subagent install` registers a sub-agent for in-session diagnostics; `dario mcp` exposes dario as a read-only MCP server. [sub-agent.md](./docs/sub-agent.md) · [mcp-server.md](./docs/mcp-server.md)
521
+ </details>
372
522
 
373
523
  ## FAQ
374
524
 
375
- **Does this violate Anthropic's terms?**
376
- Mechanically, dario uses your existing Claude Code OAuth tokens — it authenticates you as you, with your subscription, through Anthropic's official endpoints. Whether any particular use complies with current terms is between you and Anthropic; consult their terms and your agreement. Independent, unofficial, third-party — see [DISCLAIMER.md](DISCLAIMER.md). On the suspension question specifically: [Will my account get suspended?](#will-my-account-get-suspended)
525
+ <details>
526
+ <summary><strong>Does this violate Anthropic's terms?</strong></summary>
377
527
 
378
- **Do I need Claude Code installed?**
379
- Recommended, not required. With CC, `dario login` picks up credentials automatically and the template extractor reads your binary on every startup. Without it, dario runs its own OAuth flow and falls back to the bundled (scrubbed) template snapshot.
528
+ Mechanically, dario uses your existing Claude Code OAuth tokens: it authenticates you as you, with your subscription, through Anthropic's official endpoints. Whether any particular use complies with current terms is between you and Anthropic; consult their terms and your agreement. Independent, unofficial, third-party — see [DISCLAIMER.md](DISCLAIMER.md). On the suspension question specifically: [Will my account get suspended?](#will-my-account-get-suspended)
529
+ </details>
380
530
 
381
- **Do I need Bun?**
382
- Optional, recommended — Bun's TLS ClientHello matches CC's runtime. Without it dario works fine; `dario doctor` flags the mismatch and `--strict-tls` hard-fails until resolved.
531
+ <details>
532
+ <summary><strong>Do I need Claude Code installed?</strong></summary>
383
533
 
384
- **Can I use dario without a Claude subscription?**
385
- Yes. Skip `dario login`, run `dario backend add openai --key=…`, and you have a local OpenAI-compat router with no Claude involvement.
534
+ Recommended, not required. With it, `dario login` picks up credentials automatically and the template extractor reads your binary on every startup. Without it, dario runs its own OAuth flow and falls back to the bundled (scrubbed) template snapshot, which the drift watchers keep current.
535
+ </details>
386
536
 
387
- **`representative-claim: seven_day` in my headers — am I downgraded?**
388
- No. `five_hour` and `seven_day` are both subscription billing — different accounting buckets, same mode. `overage` is the one that flips you to per-token. [#1](https://github.com/askalf/dario/discussions/1).
537
+ <details>
538
+ <summary><strong>Do I need Bun?</strong></summary>
389
539
 
390
- **Will the billing split break my setup?**
391
- It was announced, then paused before it took effect — today nothing changed and your traffic still bills subscription. If it returns (Anthropic promised advance notice), dario already rewrites every request to interactive-CC shape, and the daily canary surfaces the change within a day. See [The billing split](#the-billing-split--a-contingency-dario-is-built-for).
540
+ Optional, recommended: Bun's TLS ClientHello matches Claude Code's runtime, and dario relaunches itself under Bun when it finds one on `PATH`. Without it dario works fine on Node; `dario doctor` flags the mismatch and `--strict-tls` hard-fails until resolved.
541
+ </details>
392
542
 
393
- Full FAQ: [`docs/faq.md`](./docs/faq.md)
543
+ <details>
544
+ <summary><strong>Can I use dario without a Claude subscription?</strong></summary>
394
545
 
395
- ---
546
+ Yes. Skip `dario login`, run `dario add altman` for a ChatGPT plan or `dario backend add openai --key=…` for an API key, and you have a local router with no Claude involvement. `--no-claude-auth` keeps the Claude token untouched entirely.
547
+ </details>
548
+
549
+ <details>
550
+ <summary><strong><code>representative-claim: seven_day</code> in my headers — am I downgraded?</strong></summary>
551
+
552
+ No. `five_hour` and `seven_day` are both subscription billing, different accounting buckets in the same mode. `overage` is the one that flips you to per-token, and the overage guard halts on it. [#1](https://github.com/askalf/dario/discussions/1)
553
+ </details>
554
+
555
+ <details>
556
+ <summary><strong>My usage through dario is higher than through Claude Code directly. Why?</strong></summary>
396
557
 
397
- ## Technical deep dives
558
+ Almost always the prompt-cache TTL, not proxy overhead: dario mirrors whatever cache stamp your client sends, and many harnesses send the 5-minute one, so gaps longer than five minutes between turns re-create the prefix. `DARIO_CACHE_TTL_1H=1` forces the 1-hour TTL. The full breakdown, with the two-message check that tells you which case you're in: [faq.md](./docs/faq.md).
559
+ </details>
398
560
 
399
- - [#183 — Modifying CC's system prompt doesn't change billing; stripping its constraints recovers 1.2–2.8× output](https://github.com/askalf/dario/discussions/183)
561
+ <details>
562
+ <summary><strong>Will the billing split break my setup?</strong></summary>
563
+
564
+ It was announced, then paused before it took effect; today nothing changed and your traffic still bills subscription. If it returns (Anthropic promised advance notice), dario already rewrites every request to interactive-Claude-Code shape, and the daily canary surfaces the change within a day. See [the billing split](#the-billing-split-a-contingency-dario-is-built-for).
565
+ </details>
566
+
567
+ <details>
568
+ <summary><strong>Why "dario"?</strong></summary>
569
+
570
+ It's a name, not an acronym. Don't overthink it.
571
+ </details>
572
+
573
+ Full FAQ, including per-tool 401s and Team/Enterprise plans: [faq.md](./docs/faq.md).
574
+
575
+ ## Deep dives
576
+
577
+ - [#183 — Modifying Claude Code's system prompt doesn't change billing; stripping its constraints recovers 1.2–2.8× output](https://github.com/askalf/dario/discussions/183)
400
578
  - [#68 — dario vs LiteLLM / OpenRouter / Kong AI Gateway (when each wins)](https://github.com/askalf/dario/discussions/68)
401
- - [#14 — Template Replay: why we replay the shape instead of matching signals](https://github.com/askalf/dario/discussions/14)
579
+ - [#14 — Template replay: why we replay the shape instead of matching signals](https://github.com/askalf/dario/discussions/14)
402
580
  - [#13 — Claude Code's request shape, documented](https://github.com/askalf/dario/discussions/13)
403
581
  - [#1 — Rate-limit header analysis](https://github.com/askalf/dario/discussions/1)
404
-
405
- ---
582
+ - [system-prompt-classifier-study.md](./docs/research/system-prompt-classifier-study.md), the measurements behind `--system-prompt=partial`
406
583
 
407
584
  ## Contributing
408
585
 
409
- PRs welcome. Small TypeScript codebase, zero runtime deps. Architecture + file-by-file map in [`CONTRIBUTING.md`](CONTRIBUTING.md).
586
+ PRs welcome. Small TypeScript codebase, zero runtime deps. Architecture, file-by-file map and the review bar in [CONTRIBUTING.md](CONTRIBUTING.md); release mechanics in [RELEASING.md](RELEASING.md).
410
587
 
411
588
  ```bash
412
589
  git clone https://github.com/askalf/dario && cd dario
413
590
  npm install
414
591
  npm run dev # tsx, no build step
415
- npm test # 125 suites in parallel via test/all.test.mjs
592
+ npm test # 176 files in parallel via test/all.test.mjs
416
593
  npm run e2e # live proxy + OAuth (needs a working Claude backend)
417
594
  ```
418
595
 
419
596
  Drift and audit runners, none of them part of `npm test`:
420
597
 
421
598
  ```bash
422
- npm run drift:wire # compare a live CC capture against the baked template
423
- npm run drift:sdk # Agent-SDK / Stainless pin drift
424
- npm run audit:tui # drives the real TUI through a fake TTY at 12 geometries
425
- npm run check:overage # overage-classifier check against live headers
426
- npm run stress # concurrency / queue behaviour under load
427
- npm run cch:calibrate # re-derive the billing-tag cch seed for a new CC build
599
+ npm run drift:wire # compare a live Claude Code capture against the baked template
600
+ npm run drift:sdk # Agent SDK / Stainless pin drift
601
+ npm run audit:tui # drives the real TUI through a fake TTY at 12 geometries
602
+ npm run check:overage # overage-classifier check against live headers
603
+ npm run stress # concurrency / queue behaviour under load
604
+ npm run cch:calibrate # re-derive the billing-tag cch seed for a new Claude Code build
605
+ npm run readme:assets # regenerate the diagrams and TUI screenshots above
428
606
  ```
429
607
 
430
- Two easy ways to help beyond code: **star the repo** (the clearest signal this is useful), and **file drift** — open an issue when a rate-limit header flips or a tool that worked yesterday breaks today, and it gets documented in public alongside the fix. Follow [@ask_alf](https://x.com/ask_alf) for drift bulletins as they land.
608
+ Two easy ways to help beyond code: **star the repo**, the clearest signal this is useful, and **file drift**: open an issue when a rate-limit header flips or a tool that worked yesterday breaks today, and it gets documented in public alongside the fix. Follow [@ask_alf](https://x.com/ask_alf) for drift bulletins as they land.
609
+
610
+ <picture>
611
+ <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=askalf/dario&type=Date&theme=dark">
612
+ <img alt="Star history of askalf/dario" src="https://api.star-history.com/svg?repos=askalf/dario&type=Date" width="100%">
613
+ </picture>
431
614
 
432
615
  ### Contributors
433
616
 
434
617
  | Who | Contributions |
435
618
  |---|---|
436
619
  | [@GodsBoy](https://github.com/GodsBoy) | Proxy auth, token redaction, error sanitization ([#2](https://github.com/askalf/dario/pull/2)) |
437
- | [@belangertrading](https://github.com/belangertrading) | Billing-classification investigation ([#4](https://github.com/askalf/dario/issues/4), [#6](https://github.com/askalf/dario/issues/6), [#7](https://github.com/askalf/dario/issues/7), [#12](https://github.com/askalf/dario/issues/12), [#23](https://github.com/askalf/dario/issues/23)) |
620
+ | [@belangertrading](https://github.com/belangertrading) | Billing-classification investigation ([#4](https://github.com/askalf/dario/issues/4), [#6](https://github.com/askalf/dario/issues/6), [#7](https://github.com/askalf/dario/issues/7), [#12](https://github.com/askalf/dario/issues/12), [#23](https://github.com/askalf/dario/issues/23)), multi-agent billing FAQ ([#27](https://github.com/askalf/dario/pull/27)) |
621
+ | [@earlvanze](https://github.com/earlvanze) | OpenClaw tool mappings ([#19](https://github.com/askalf/dario/pull/19)), OAuth manual override ([#47](https://github.com/askalf/dario/pull/47)), HTTPS warning ([#53](https://github.com/askalf/dario/pull/53)) |
438
622
  | [@iNicholasBE](https://github.com/iNicholasBE) | macOS keychain credential detection ([#30](https://github.com/askalf/dario/pull/30)) |
439
623
  | [@boeingchoco](https://github.com/boeingchoco) | Reverse tool-param translation ([#29](https://github.com/askalf/dario/issues/29)), SSE framing regression catch, hybrid-tool motivation ([#33](https://github.com/askalf/dario/issues/33), [#36](https://github.com/askalf/dario/issues/36)) |
440
624
  | [@tetsuco](https://github.com/tetsuco) | Scrubber path corruption ([#35](https://github.com/askalf/dario/issues/35)), OpenClaw reverse-mapping collisions ([#37](https://github.com/askalf/dario/issues/37)), 20x-tier report ([#42](https://github.com/askalf/dario/issues/42)) |
441
625
  | [@mikelovatt](https://github.com/mikelovatt) | Silent subscription-drain surfaced via friendly billing buckets ([#34](https://github.com/askalf/dario/issues/34)) |
442
626
  | [@ringge](https://github.com/ringge) | `--no-auto-detect` for text-tool auto-preserve ([#40](https://github.com/askalf/dario/issues/40)) |
443
- | [@earlvanze](https://github.com/earlvanze) | OpenClaw tool mappings ([#19](https://github.com/askalf/dario/pull/19)), OAuth manual override ([#47](https://github.com/askalf/dario/pull/47)), HTTPS warning ([#53](https://github.com/askalf/dario/pull/53)) |
444
-
445
- ---
627
+ | [@Saik0s](https://github.com/Saik0s) | Wildcard CORS allow-headers, Opus 4.7 catalog entry ([#222](https://github.com/askalf/dario/pull/222)) |
628
+ | [@boredland](https://github.com/boredland) | Time-to-reset in `dario doctor --usage` ([#550](https://github.com/askalf/dario/pull/550)) |
629
+ | [@pnewell](https://github.com/pnewell) | `--preserve-output-format` for structured-output SDKs ([#583](https://github.com/askalf/dario/pull/583)) |
630
+ | [@jerzydziewierz](https://github.com/jerzydziewierz) | TUI Config tab clipping and scrolling ([#861](https://github.com/askalf/dario/pull/861)) |
631
+ | [@chaogebaba](https://github.com/chaogebaba) | Auto-release must never fire from a fork ([#1029](https://github.com/askalf/dario/pull/1029)) |
632
+ | [@anupamme](https://github.com/anupamme) | Refresh-lock ownership by server-issued lock id ([#1059](https://github.com/askalf/dario/pull/1059)) |
633
+ | [@LiveNathan](https://github.com/LiveNathan) | Never send or stamp empty text blocks ([#1067](https://github.com/askalf/dario/pull/1067)) |
446
634
 
447
635
  ## Disclaimers
448
636
 
@@ -450,11 +638,11 @@ Two easy ways to help beyond code: **star the repo** (the clearest signal this i
450
638
 
451
639
  ## License
452
640
 
453
- MIT — see [LICENSE](LICENSE) and [DISCLAIMER.md](DISCLAIMER.md).
641
+ MIT — see [LICENSE](LICENSE) and [DISCLAIMER.md](DISCLAIMER.md). The embedded README font is Space Mono under the [SIL Open Font License](./scripts/readme/fonts/OFL.txt).
454
642
 
455
643
  ## Own Your Stack
456
644
 
457
- dario is the routing layer of **[Own Your Stack](https://github.com/askalf)** — open tools for owning your AI infrastructure instead of renting it by the token. One subscription. Your box. Your terms.
645
+ dario is the routing layer of **[Own Your Stack](https://github.com/askalf)**, open tools for owning your AI infrastructure instead of renting it by the token. One subscription. Your box. Your terms.
458
646
 
459
647
  - **[dario](https://github.com/askalf/dario)** — own your routing _(you are here)_
460
648
  - **[hybrid](https://github.com/askalf/hybrid)** — own your inference
@@ -463,20 +651,16 @@ dario is the routing layer of **[Own Your Stack](https://github.com/askalf)**
463
651
  - **[truecopy](https://github.com/askalf/truecopy)** — own your agent skills
464
652
  - **[agent-security-stack](https://github.com/askalf/agent-security-stack)** — own your agent security stack: redstamp + truecopy + strongroom leases, one MCP server
465
653
  - **[cordon](https://github.com/askalf/cordon)** — own your prompts · [pair it with dario](./docs/integrations/cordon.md)
466
- - **[fieldpass](https://github.com/askalf/fieldpass)** — own your agent browser
467
654
  - **[plumbline](https://github.com/askalf/plumbline)** — own your agent oversight
468
655
  - **[amnesia](https://github.com/askalf/amnesia)** — own your search
469
656
  - **[pgflex](https://github.com/askalf/pgflex)** — own your Postgres
470
657
  - **[redisflex](https://github.com/askalf/redisflex)** — own your Redis
471
658
  - **[askalf](https://askalf.org)** — own your operation: the AI operation that runs Sprayberry Labs
472
659
 
473
- ---
474
-
475
660
  ## Built by Thomas Sprayberry
476
661
 
477
- dario is part of **Own Your Stack** — the open toolkit behind **[Sprayberry Labs](https://sprayberrylabs.com)**, the software studio with one human on staff — run by [askalf](https://askalf.org), the AI operation these tools are part of.
662
+ dario is part of **Own Your Stack**, the open toolkit behind **[Sprayberry Labs](https://sprayberrylabs.com)**, the software studio with one human on staff, run by [askalf](https://askalf.org), the AI operation these tools are part of.
478
663
 
479
- Built in the open, scars included. Follow the build → **[@ask_alf](https://x.com/ask_alf)** · **[sprayberrylabs.com/own-your-stack](https://sprayberrylabs.com/own-your-stack)**
664
+ Built in the open, scars included. Follow the build: **[@ask_alf](https://x.com/ask_alf)** · **[sprayberrylabs.com/own-your-stack](https://sprayberrylabs.com/own-your-stack)**
480
665
 
481
- ---
482
- Part of **[Own Your Stack](https://github.com/askalf)** — own your AI infrastructure instead of renting it. Built by Thomas Sprayberry.
666
+ [^plans]: Pro at $20 a month, Max 5x at $100, Max 20x at $200, as listed on [claude.com/pricing](https://claude.com/pricing) on 2026-09-06. Annual billing is cheaper; check the page for what's current.
@@ -72,7 +72,17 @@ function servableTarget(target, bases) {
72
72
  return null;
73
73
  const resolved = resolveAliasAgainst(stripped, bases) ?? stripped;
74
74
  const base = resolved.endsWith('[1m]') ? resolved.slice(0, -4) : resolved;
75
- return bases.some((b) => b.toLowerCase() === base) ? resolved : null;
75
+ // The catalog keeps ONE spelling per model — the short id when upstream
76
+ // lists both `claude-opus-4-8` and `claude-opus-4-8-YYYYMMDD` (see
77
+ // normalizeUpstreamIds) — while a client may send either. Compare with the
78
+ // date stripped on both sides, and return the name as written: Anthropic
79
+ // accepts both forms, so the pool forwards whichever the caller chose.
80
+ const key = undated(base);
81
+ return bases.some((b) => undated(b.toLowerCase()) === key) ? resolved : null;
82
+ }
83
+ /** `claude-opus-4-8-20260101` → `claude-opus-4-8`; anything else unchanged. */
84
+ function undated(id) {
85
+ return id.replace(/-\d{8}$/, '');
76
86
  }
77
87
  /**
78
88
  * The id the Claude pool would serve `model` as AND the effort the entry asked
package/dist/proxy.js CHANGED
@@ -23,6 +23,8 @@ import { handleAdminRequest } from './admin-api.js';
23
23
  import { createTokenBucket } from './rate-limit.js';
24
24
  import { getOpenAIBackend, isOpenAIModel, forwardToOpenAI } from './openai-backend.js';
25
25
  import { forwardToCodex, getCodexModelSlugs, peekCodexModelSlugs, pickCodexFallback, pickClaudeTarget, CODEX_BACKEND_BASE_URL } from './codex-backend.js';
26
+ import { isClaudeServableModel } from './claude-model.js';
27
+ import { MODEL_UNROUTABLE } from './upstream-rejection.js';
26
28
  import { readCompareTarget, teeResponse, runCompare, writeCompareRecord, COMPARE_RESULT_HEADER } from './compare.js';
27
29
  import { listCodexAccountAliases, loadAllCodexAccounts, codexAccountNeedsRefresh, hasAnyCodexAccount, selectCodexAccount, getFreshCodexAccount, getCodexRefreshFailure, CodexCredentialsUnavailableError } from './codex-accounts.js';
28
30
  import { route as routeProvider } from './provider-adapter.js';
@@ -2726,6 +2728,11 @@ export async function startProxy(opts = {}) {
2726
2728
  // wire shape, and no upstream round-trip for a request nothing can serve.
2727
2729
  // Upstreams do the same (Anthropic: "The request body is not valid
2728
2730
  // JSON"; OpenAI: "We could not parse the JSON body of your request").
2731
+ //
2732
+ // The object this guard parses is kept as `parsedBody` and reused by the
2733
+ // provider-prefix block and the template build below, so the bytes are
2734
+ // JSON.parsed once per request (#642-audit; second-read finding on #1231).
2735
+ let parsedBody = null;
2729
2736
  {
2730
2737
  let invalid = null;
2731
2738
  if (body.length === 0)
@@ -2740,6 +2747,8 @@ export async function startProxy(opts = {}) {
2740
2747
  const v = JSON.parse(text);
2741
2748
  if (v === null || typeof v !== 'object' || Array.isArray(v))
2742
2749
  invalid = 'request body must be a JSON object';
2750
+ else
2751
+ parsedBody = v;
2743
2752
  }
2744
2753
  catch (err) {
2745
2754
  invalid = `request body is not valid JSON: ${err instanceof Error ? err.message : String(err)}`;
@@ -2775,15 +2784,14 @@ export async function startProxy(opts = {}) {
2775
2784
  // recognizes through its own Anthropic gateway, bypassing localhost).
2776
2785
  let forcedProvider = cliProviderOverride;
2777
2786
  let requestEffort; // dario#419 — per-request effort parsed from a model-name suffix (model:high / model-high)
2778
- // Parsed body, shared between the provider-prefix detection below and the
2779
- // template-build block further down so the same bytes are not JSON.parsed
2780
- // twice per request (#642-audit). Mutations in the prefix block re-serialize
2781
- // `body` FROM this object, so it always represents the current body.
2782
- let parsedBody = null;
2783
- if (body.length > 0) {
2787
+ // `parsedBody` was parsed by the invalid-body guard above and is shared
2788
+ // with the template-build block further down so the same bytes are not
2789
+ // JSON.parsed twice per request (#642-audit). Mutations in the prefix block
2790
+ // re-serialize `body` FROM this object, so it always represents the current
2791
+ // body.
2792
+ if (parsedBody !== null) {
2784
2793
  try {
2785
- const parsed = JSON.parse(body.toString());
2786
- parsedBody = parsed;
2794
+ const parsed = parsedBody;
2787
2795
  // User-defined aliases first — before provider-prefix parsing, so
2788
2796
  // an alias target carrying a prefix (`my-fast` → `openai:gpt-4o`)
2789
2797
  // retargets the backend through the existing machinery below.
@@ -2927,7 +2935,7 @@ export async function startProxy(opts = {}) {
2927
2935
  // reaches the subscription even when an API-key backend is configured too.
2928
2936
  if (body.length > 0) {
2929
2937
  try {
2930
- const peek = JSON.parse(body.toString());
2938
+ const peek = (parsedBody ?? {}); // parsed once by the invalid-body guard; `body` is re-serialized from it
2931
2939
  const rawModel = (peek.model || '').toString();
2932
2940
  const requestPoolFallbackModels = selectPoolFallbackModels(poolFallbackSpec, rawModel);
2933
2941
  const requestPoolFallbackModel = requestPoolFallbackModels[0] ?? null;
@@ -3117,6 +3125,42 @@ export async function startProxy(opts = {}) {
3117
3125
  await forwardToOpenAI(req, res, body, openaiBackend, corsOrigin, SECURITY_HEADERS, upstreamTimeoutMs, verbose);
3118
3126
  return;
3119
3127
  }
3128
+ // dario#1236 — the Claude adapter claims by default, so a model NO
3129
+ // provider lists used to reach api.anthropic.com verbatim and come
3130
+ // back as Anthropic's 404 `model: gpt-5.6-sol`, attributed to
3131
+ // whichever seat sent it, after spending a pool request. Ask the
3132
+ // positive question the failover chain already asks (claude-model.ts)
3133
+ // and refuse locally instead.
3134
+ //
3135
+ // Deliberately NOT refused: a `claude-*` name the catalog does not
3136
+ // know (the live catalog can lag a model by a fetch, and on a cold
3137
+ // start it is the baked list — Anthropic's own 404 stays
3138
+ // authoritative for those); a request under a server-wide
3139
+ // --model/--fast-model override, which replaces the name; upstream
3140
+ // API-key mode, which has no pool to protect and may reach models the
3141
+ // OAuth catalog never lists; and an OpenAI-shape name the legacy
3142
+ // OPENAI_MODEL_MAP translates to a Claude model.
3143
+ if (rawModel && decision.provider === 'claude' && !upstreamApiKey && !modelOverride && !fastModelOverride
3144
+ && !(isOpenAI && OPENAI_MODEL_MAP[rawModel])
3145
+ && !/^claude-/i.test(rawModel.trim())
3146
+ && !isClaudeServableModel(rawModel, getCachedBases(), (m) => resolveClaudeAlias(applyModelAlias(m, modelAliases) ?? m))) {
3147
+ const consulted = [
3148
+ codexCreds || codexUnavailable
3149
+ ? `codex account ${(codexCreds ?? codexUnavailable).alias} (${codexModels.length} listed slug${codexModels.length === 1 ? '' : 's'})`
3150
+ : 'no codex account',
3151
+ openaiBackend ? `openai backend ${openaiBackend.name}${isOpenAI ? '' : ' (OpenAI path only)'}` : 'no openai backend',
3152
+ `claude catalog (${getCachedBases().length} bases)`,
3153
+ ].join(', ');
3154
+ if (verbose)
3155
+ console.log(`[dario] #${requestCount} ${req.method} ${urlPath} (model: ${rawModel}) no provider lists ${rawModel}; refusing — consulted ${consulted}`);
3156
+ requestCount++;
3157
+ const message = `no provider lists model "${rawModel}" (consulted ${consulted}); refused locally rather than forwarded to the Claude pool, which would 404 it after spending a request`;
3158
+ res.writeHead(400, { ...JSON_HEADERS, 'x-dario-upstream-rejection': MODEL_UNROUTABLE });
3159
+ res.end(JSON.stringify(isOpenAI
3160
+ ? { error: { message, type: 'invalid_request_error', param: 'model', code: 'model_not_found' } }
3161
+ : { type: 'error', error: { type: 'invalid_request_error', message } }));
3162
+ return;
3163
+ }
3120
3164
  }
3121
3165
  catch { /* not JSON — fall through to existing path */ }
3122
3166
  }
@@ -3,6 +3,14 @@ export interface UpstreamRejection {
3
3
  class: UpstreamRejectionClass;
4
4
  marker: 'billing_required' | 'rate_limited' | 'credential_rejected' | 'upstream_rejected';
5
5
  }
6
+ /**
7
+ * `x-dario-upstream-rejection` value for a request dario refused LOCALLY
8
+ * because no provider lists its model (dario#1236). Not a classification of
9
+ * an upstream answer — there was no upstream request, which is the point — but
10
+ * it rides the same header so a fleet log or tracker reads one field for
11
+ * every "this request was not served" verdict.
12
+ */
13
+ export declare const MODEL_UNROUTABLE = "model_unroutable";
6
14
  /** Classify subscription entitlement failures separately from temporary quota exhaustion. */
7
15
  export declare function classifyUpstreamRejection(status: number, body: string): UpstreamRejection;
8
16
  /** Operator action paired with the failure class. Never suggest credential churn for billing. */
@@ -1,3 +1,11 @@
1
+ /**
2
+ * `x-dario-upstream-rejection` value for a request dario refused LOCALLY
3
+ * because no provider lists its model (dario#1236). Not a classification of
4
+ * an upstream answer — there was no upstream request, which is the point — but
5
+ * it rides the same header so a fleet log or tracker reads one field for
6
+ * every "this request was not served" verdict.
7
+ */
8
+ export const MODEL_UNROUTABLE = 'model_unroutable';
1
9
  /** Classify subscription entitlement failures separately from temporary quota exhaustion. */
2
10
  export function classifyUpstreamRejection(status, body) {
3
11
  const normalized = body.toLowerCase();
@@ -24,8 +24,8 @@ services:
24
24
  expose: ["3456"]
25
25
 
26
26
  cordon:
27
- # cordon does not publish an image yet; build it from the repo.
28
- build: https://github.com/askalf/cordon.git
27
+ # multi-arch, Sigstore-attested: gh attestation verify oci://ghcr.io/askalf/cordon:v0.2.0 --repo askalf/cordon
28
+ image: ghcr.io/askalf/cordon:v0.2.0
29
29
  environment:
30
30
  ANTHROPIC_BASE: http://dario:3456
31
31
  OPENAI_BASE: http://dario:3456
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "6.0.31",
3
+ "version": "6.0.32",
4
4
  "description": "Use your Claude Pro/Max subscription in any tool — Cursor, Cline, Aider, the Agent SDK, your scripts — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -40,7 +40,9 @@
40
40
  "check:overage": "node scripts/check-overage-live.mjs",
41
41
  "cch:calibrate": "node scripts/cch-calibrate.mjs",
42
42
  "fix:pkg": "node -e \"const fs=require('fs');fs.writeFileSync('package.json',JSON.stringify(JSON.parse(fs.readFileSync('package.json','utf-8')),null,2)+'\\n')\"",
43
- "audit:tui": "node tools/tui-audit/audit.mjs"
43
+ "audit:tui": "node tools/tui-audit/audit.mjs",
44
+ "readme:assets": "node scripts/readme/hero.mjs && node scripts/readme/terminal.mjs && node scripts/readme/tui.mjs",
45
+ "check:readme": "node scripts/check-readme-line-count.mjs && node scripts/check-readme-links.mjs"
44
46
  },
45
47
  "keywords": [
46
48
  "llm",