@askalf/dario 6.0.32 → 6.0.34
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 +39 -59
- package/dist/accounts.d.ts +18 -0
- package/dist/accounts.js +14 -0
- package/dist/admin-api.d.ts +27 -3
- package/dist/admin-api.js +20 -1
- package/dist/analytics.d.ts +41 -1
- package/dist/analytics.js +65 -2
- package/dist/cli.js +94 -1
- package/dist/doctor-core.d.ts +15 -0
- package/dist/doctor-core.js +34 -0
- package/dist/pool-sync.d.ts +114 -0
- package/dist/pool-sync.js +200 -0
- package/dist/pool.d.ts +97 -1
- package/dist/pool.js +156 -7
- package/dist/proxy.d.ts +14 -0
- package/dist/proxy.js +136 -13
- package/dist/request-queue.d.ts +34 -4
- package/dist/request-queue.js +57 -11
- package/dist/tui/tabs/accounts.d.ts +2 -0
- package/dist/tui/tabs/accounts.js +15 -2
- package/dist/tui/tabs/hits.js +2 -0
- package/docs/admin-api.md +18 -6
- package/docs/configuration.md +10 -0
- package/docs/integrations/openclaw-walkthrough.md +2 -2
- package/docs/multi-account-pool.md +39 -0
- package/docs/multi-instance.md +27 -6
- package/package.json +18 -12
package/README.md
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
<
|
|
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>
|
|
3
|
+
<img src=".github/readme/hero.jpg" alt="dario routes every AI tool you use to the subscriptions you already pay for. Claude Code, Cursor, Cline, Aider, Codex CLI and the Agent SDK send requests to dario at localhost:3456, which forwards each one to your Claude plan or your ChatGPT plan and fails over between them on a 429." width="100%">
|
|
7
4
|
|
|
8
5
|
# `dario`
|
|
9
6
|
|
|
@@ -66,7 +63,7 @@ export ANTHROPIC_API_KEY=dario
|
|
|
66
63
|
|
|
67
64
|
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
65
|
|
|
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
|
|
66
|
+
**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.
|
|
70
67
|
|
|
71
68
|
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.
|
|
72
69
|
|
|
@@ -162,7 +159,7 @@ export ANTHROPIC_API_KEY=dario
|
|
|
162
159
|
openclaw "task description"
|
|
163
160
|
```
|
|
164
161
|
|
|
165
|
-
OpenClaw's `exec` / `process` / `web_search` / `web_fetch` / `browser`
|
|
162
|
+
OpenClaw's `exec` / `process` / `web_search` / `web_fetch` / `browser` tools are translated to Claude Code's set without a flag; a tool outside the map (`message`, for one) rides a fallback slot instead. 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
163
|
</details>
|
|
167
164
|
|
|
168
165
|
<details>
|
|
@@ -173,7 +170,7 @@ export OPENAI_BASE_URL=http://localhost:3456/v1
|
|
|
173
170
|
export OPENAI_API_KEY=dario
|
|
174
171
|
```
|
|
175
172
|
|
|
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
|
|
173
|
+
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` or anything else your API-key backend lists and it goes there byte-for-byte. Names that don't look like OpenAI's (`llama-3.3-70b`, `qwen-coder`) need the provider prefix below; dario refuses them rather than guess:
|
|
177
174
|
|
|
178
175
|
```bash
|
|
179
176
|
dario backend add openai --key=sk-proj-...
|
|
@@ -183,6 +180,8 @@ dario backend add local --key=anything --base-url=http://127.0.0.1:11434/
|
|
|
183
180
|
```
|
|
184
181
|
|
|
185
182
|
Force a backend with a prefix: `openai:gpt-4o`, `claude:opus`, `groq:llama-3.3-70b`, `local:qwen-coder`.
|
|
183
|
+
|
|
184
|
+
One holdover for old configs: six legacy OpenAI names (`gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano`, `gpt-5.3`, `gpt-4`, `gpt-3.5-turbo`) sent to `/v1/chat/completions` are translated to Claude models when no other provider claims them.
|
|
186
185
|
</details>
|
|
187
186
|
|
|
188
187
|
<details>
|
|
@@ -232,32 +231,19 @@ Something not listed? If it reads `ANTHROPIC_BASE_URL` or `OPENAI_BASE_URL`, or
|
|
|
232
231
|
|
|
233
232
|
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.
|
|
234
233
|
|
|
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
|
-
```
|
|
247
|
-
|
|
248
234
|
| Client speaks | Model | Routes to | What happens |
|
|
249
235
|
|---|---|---|---|
|
|
250
236
|
| Anthropic Messages | `claude-*` / `opus` / `sonnet` / `haiku` | Claude pool | OAuth swap + Claude Code template, then `api.anthropic.com` |
|
|
251
237
|
| Anthropic Messages | a slug your ChatGPT account lists | Codex engine | Messages→Responses translation, subscription auth |
|
|
252
|
-
| Anthropic Messages | `gpt
|
|
253
|
-
| OpenAI Chat | `gpt-*` / `o1-*` / `o3-*` | OpenAI-compat backend | Auth swap, body forwarded byte-for-byte |
|
|
238
|
+
| Anthropic Messages | `gpt-4o`, `llama-*`, any name no plan lists | Refused | `400` + `x-dario-upstream-rejection: model_unroutable`; reach an API-key backend from this shape with a provider prefix |
|
|
239
|
+
| OpenAI Chat | `gpt-*` / `o1-*` / `o3-*` / `o4-*` | OpenAI-compat backend | Auth swap, body forwarded byte-for-byte |
|
|
254
240
|
| OpenAI Chat | a slug your ChatGPT account lists | Codex engine | chat/completions→Responses translation, subscription auth |
|
|
255
241
|
| OpenAI Chat | `claude-*` | Claude pool | OpenAI→Anthropic translation, then the Claude path |
|
|
256
242
|
| Either | `<provider>:<model>` | Forced by prefix | Explicit override |
|
|
257
243
|
|
|
258
244
|
The tool doesn't know. The backend doesn't know. dario is the seam.
|
|
259
245
|
|
|
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.
|
|
246
|
+
**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. The guard steps aside for `claude-*` names (Anthropic's own 404 stays authoritative there), for requests under a `--model` / `--fast-model` override, for upstream-API-key mode, and for the legacy OpenAI names the built-in map translates.
|
|
261
247
|
|
|
262
248
|
## Two plans, one endpoint
|
|
263
249
|
|
|
@@ -285,7 +271,7 @@ curl localhost:3456/v1/messages -H 'content-type: application/json' \
|
|
|
285
271
|
|
|
286
272
|
**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.
|
|
287
273
|
|
|
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.
|
|
274
|
+
Streaming, tool calls and tool-result round trips work on both shapes, and chat-shape `image_url` parts are carried as Responses `input_image` parts with `detail` preserved: 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.
|
|
289
275
|
|
|
290
276
|
**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.
|
|
291
277
|
|
|
@@ -297,25 +283,11 @@ Two consumer plans, no API keys, and neither one able to take you down on its ow
|
|
|
297
283
|
dario proxy --pool-fallback=gpt-5.6-sol,claude-sonnet-5
|
|
298
284
|
```
|
|
299
285
|
|
|
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
|
-
|
|
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
|
-
```
|
|
286
|
+
That is a **chain**, read left to right; each provider takes the first entry it can actually serve. Prefix every entry with a tier and the same flag becomes a **tier map**: `--pool-fallback=haiku:gpt-5.4-mini,sonnet:gpt-5.6-terra,opus:gpt-5.6-sol` picks one rung per request from the tier of the model asked for (`default:` catches the rest, otherwise the first rung), so heartbeat work on Haiku never overflows onto a flagship. That shipped in 6.0.17, the same day a Haiku-tier fleet spent 79% of a weekly allowance doing exactly that. 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.
|
|
287
|
+
|
|
288
|
+
<img src=".github/readme/failover.jpg" alt="A tool sends a request to dario. The Claude plan answers 429, so dario re-serves the same request from the ChatGPT plan, which answers 200, and the response returns to the tool carrying the x-dario-pool-fallback header." width="100%">
|
|
317
289
|
|
|
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.
|
|
290
|
+
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. A 429 also cools that provider for a bounded interval, its `retry-after` if it sent one and 60 s otherwise, never longer than 15 min, and an entry that already declined is not asked again within the same request. When every entry is cooling, the request ends on one honest `429` with a `retry-after` instead of a retry storm. 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.
|
|
319
291
|
|
|
320
292
|
`dario doctor` tells you which of these you are actually in:
|
|
321
293
|
|
|
@@ -362,9 +334,11 @@ Three things it does that a round-robin doesn't:
|
|
|
362
334
|
|
|
363
335
|
- **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
336
|
- **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.
|
|
337
|
+
- **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. A seat parked on a 429 rejoins the pool on its own when its window resets.
|
|
366
338
|
|
|
367
|
-
|
|
339
|
+
<img src=".github/readme/pool.jpg" alt="Three pooled seats, work, personal and side, each with a headroom bar. dario routes the request to the seat with the most headroom." width="100%">
|
|
340
|
+
|
|
341
|
+
`--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>` (admin API required: `DARIO_ADMIN=1` and a `DARIO_ADMIN_TOKEN`). 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).
|
|
368
342
|
|
|
369
343
|
### Watch it happen
|
|
370
344
|
|
|
@@ -381,12 +355,14 @@ Type `dario` with no arguments for a full-screen control panel: live request str
|
|
|
381
355
|
|
|
382
356
|
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).
|
|
383
357
|
|
|
358
|
+
<img src=".github/readme/drift.jpg" alt="The installed claude binary feeds its request shape into dario. A timeline of Claude Code releases ends in a node flagged as drift." width="100%">
|
|
359
|
+
|
|
384
360
|
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:
|
|
385
361
|
|
|
386
362
|
| Watcher | Catches | Live |
|
|
387
363
|
|---|---|---|
|
|
388
364
|
| [`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. |  |
|
|
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. |  | 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. |  |
|
|
390
366
|
| [`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. |  |
|
|
391
367
|
| [`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. |  |
|
|
392
368
|
| [`sdk-drift-watch`](./.github/workflows/sdk-drift-watch.yml) | Agent SDK / Stainless pins drifting from what the template assumes. |  |
|
|
@@ -413,7 +389,9 @@ The full ledger lives in the [CHANGELOG](CHANGELOG.md), 500+ releases since Apri
|
|
|
413
389
|
|
|
414
390
|
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.
|
|
415
391
|
|
|
416
|
-
|
|
392
|
+
<img src=".github/readme/overage.jpg" alt="Requests from five tools are stopped short of dario, which is ringed in red and labeled halted, after the Claude plan returned a response billed as overage." width="100%">
|
|
393
|
+
|
|
394
|
+
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`, their `_fallback` and `_overage_included` variants, and the `chatgpt_subscription` claim dario stamps on Codex-served responses) 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).
|
|
417
395
|
|
|
418
396
|
### The billing split, a contingency dario is built for
|
|
419
397
|
|
|
@@ -423,17 +401,19 @@ The split isn't live, but it was announced once on short notice and could return
|
|
|
423
401
|
|
|
424
402
|
## Trust & transparency
|
|
425
403
|
|
|
404
|
+
<img src=".github/readme/trust.jpg" alt="Everything inside the box labeled your machine: the tools and dario. Only two lines leave it, to the Claude plan and the ChatGPT plan, through a padlock. Pills read 0 deps, no telemetry, MIT." width="100%">
|
|
405
|
+
|
|
426
406
|
| Signal | Status |
|
|
427
407
|
|---|---|
|
|
428
408
|
| Source | **~31k** lines of TypeScript across **67** files, auditable in a weekend. One credential path since v5: the pool. |
|
|
429
409
|
| Dependencies | **0 runtime.** Verify: `npm ls --production` |
|
|
430
410
|
| 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
411
|
| 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 | **
|
|
412
|
+
| Tests | **178 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 |
|
|
433
413
|
| Credentials | Your own subscription tokens, never logged, redacted from errors, `0600` on disk in `0700` dirs |
|
|
434
414
|
| 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` |
|
|
435
415
|
| 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
|
|
416
|
+
| 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 TUI screenshots are rendered from the real TUI and the diagrams are briefed art, not screenshots ([how](./scripts/readme/README.md)) |
|
|
437
417
|
|
|
438
418
|
```bash
|
|
439
419
|
npm audit signatures
|
|
@@ -443,10 +423,6 @@ cd $(npm root -g)/@askalf/dario && npm ls --production
|
|
|
443
423
|
|
|
444
424
|
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).
|
|
445
425
|
|
|
446
|
-
## Honest about what this is
|
|
447
|
-
|
|
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.
|
|
449
|
-
|
|
450
426
|
## Will my account get suspended?
|
|
451
427
|
|
|
452
428
|
The most common question about dario, and it deserves a straight answer: **I can't promise you won't be actioned, and I'd be skeptical of anyone who does.** Only Anthropic decides how it enforces its terms. What I can do is lay out exactly how dario works, so you can weigh the risk yourself instead of taking anyone's word for it.
|
|
@@ -459,7 +435,7 @@ The most common question about dario, and it deserves a straight answer: **I can
|
|
|
459
435
|
- **Sends requests in the shape the official client sends them**, rebuilt from your own installed binary, not spoofed from a hardcoded fake.
|
|
460
436
|
- **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.
|
|
461
437
|
|
|
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.
|
|
438
|
+
**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. dario is a transparency tool, in that it documents request behavior Anthropic doesn't publish for subscribers, and it is also, plainly, routing subscription traffic that Anthropic's own tools bill differently. Both are true; decide with both in view.
|
|
463
439
|
|
|
464
440
|
**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.
|
|
465
441
|
|
|
@@ -489,14 +465,14 @@ Longer version, with specifics: [#68](https://github.com/askalf/dario/discussion
|
|
|
489
465
|
| `dario` | The TUI: status, config editor, analytics, hits, accounts, backends |
|
|
490
466
|
| `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
467
|
| `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 |
|
|
468
|
+
| `dario doctor [--usage] [--probe] [--obedience] [--auth-check] [--bun-bootstrap] [--json]` | One aggregated health report: runtime/TLS, template and drift, OAuth, pool, refresh-grant age, failover readiness, backends |
|
|
493
469
|
| `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 |
|
|
470
|
+
| `dario accounts list` / `add` / `remove` / `check <alias>` | Pool management; `check` sends one pinned request per model through the running proxy (admin API on) |
|
|
495
471
|
| `dario backend list` / `add` / `remove` | OpenAI-compatible API-key backends |
|
|
496
472
|
| `dario codex list` / `add` / `remove` | ChatGPT accounts (the long form of `dario add altman`) |
|
|
497
473
|
| `dario usage` · `dario config` · `dario status` | Burn rate for the last hour · effective config, redacted · token health |
|
|
498
474
|
| `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 |
|
|
475
|
+
| `dario mcp` · `dario subagent install` / `remove` / `status` | Reach dario from inside any MCP client, or from inside a Claude Code session, read-only |
|
|
500
476
|
|
|
501
477
|
| Endpoint | Description |
|
|
502
478
|
|---|---|
|
|
@@ -504,8 +480,11 @@ Longer version, with specifics: [#68](https://github.com/askalf/dario/discussion
|
|
|
504
480
|
| `GET /v1/models` | Live model list: the Claude catalog plus whatever your ChatGPT plan lists |
|
|
505
481
|
| `GET /health` · `GET /livez` | Serviceability (503 when not) · liveness. `/health?probe=1` sends one real request |
|
|
506
482
|
| `GET /status` · `GET /accounts` · `GET /analytics` | OAuth detail · per-seat utilization and grant age · per-account / per-model stats and burn rate |
|
|
483
|
+
| `POST /v1/messages/count_tokens` · `POST /v1/complete` | Token counting and the legacy Text Completions shape |
|
|
484
|
+
| `GET /analytics/stream` · `GET /codex` | Live analytics over SSE · ChatGPT-seat status, read without spending or exposing a token |
|
|
485
|
+
| `/admin/*` | Provisioning, `GET /admin/accounts`, `POST /admin/resume`; only with `DARIO_ADMIN=1` ([admin API](./docs/admin-api.md)) |
|
|
507
486
|
|
|
508
|
-
|
|
487
|
+
Flags: [commands.md](./docs/commands.md), plus `dario --help` for the ones it doesn't list yet (`--effort`, `--max-tokens`, `--model-alias`, `--fast-model`, session rotation, concurrency caps, the pacing knobs behind `--stealth`) · env vars grouped by task, for Docker / k8s / systemd: [configuration.md](./docs/configuration.md) · SDK examples: [usage.md](./docs/usage.md).
|
|
509
488
|
|
|
510
489
|
<details>
|
|
511
490
|
<summary><strong>More knobs</strong> — stealth timing, system-prompt modes, client-shape overrides, VPN egress, MCP</summary>
|
|
@@ -514,6 +493,7 @@ Every flag and env var: [commands.md](./docs/commands.md) · env vars grouped by
|
|
|
514
493
|
- **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
494
|
- **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
495
|
- **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)
|
|
496
|
+
- **Model aliases and caps.** `--model-alias=<name=target>` (repeatable) advertises a name of your choosing on `/v1/models` and routes it; `--effort=<low|medium|high|xhigh|ultracode|max|client>` and `--max-tokens=<N|client>` set, or pass through, per-request effort and output caps.
|
|
517
497
|
- **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
498
|
- **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
499
|
- **PII redaction in front of dario.** Pair it with [cordon](https://github.com/askalf/cordon): [integrations/cordon.md](./docs/integrations/cordon.md)
|
|
@@ -561,7 +541,7 @@ Almost always the prompt-cache TTL, not proxy overhead: dario mirrors whatever c
|
|
|
561
541
|
<details>
|
|
562
542
|
<summary><strong>Will the billing split break my setup?</strong></summary>
|
|
563
543
|
|
|
564
|
-
|
|
544
|
+
Not today: it was announced, then paused before it took effect, and your traffic still bills subscription. What dario already does about it, and the canary that would catch a revival: [the billing split](#the-billing-split-a-contingency-dario-is-built-for).
|
|
565
545
|
</details>
|
|
566
546
|
|
|
567
547
|
<details>
|
|
@@ -589,7 +569,7 @@ PRs welcome. Small TypeScript codebase, zero runtime deps. Architecture, file-by
|
|
|
589
569
|
git clone https://github.com/askalf/dario && cd dario
|
|
590
570
|
npm install
|
|
591
571
|
npm run dev # tsx, no build step
|
|
592
|
-
npm test #
|
|
572
|
+
npm test # 178 files in parallel via test/all.test.mjs
|
|
593
573
|
npm run e2e # live proxy + OAuth (needs a working Claude backend)
|
|
594
574
|
```
|
|
595
575
|
|
package/dist/accounts.d.ts
CHANGED
|
@@ -8,12 +8,30 @@ export interface AccountCredentials {
|
|
|
8
8
|
accountUuid: string;
|
|
9
9
|
/** Epoch ms of the OAuth grant; see OAuthTokens.grantedAt / refresh-grant.ts. */
|
|
10
10
|
grantedAt?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Anthropic organization behind the token (`anthropic-organization-id` on
|
|
13
|
+
* responses), observed by the proxy the first time the seat serves and
|
|
14
|
+
* written here with the seat's next token refresh. Absent until then, and
|
|
15
|
+
* on a fresh grant.
|
|
16
|
+
*/
|
|
17
|
+
organizationId?: string;
|
|
11
18
|
}
|
|
12
19
|
export declare function listAccountAliases(): Promise<string[]>;
|
|
13
20
|
export declare function loadAccount(alias: string): Promise<AccountCredentials | null>;
|
|
14
21
|
export declare function loadAllAccounts(): Promise<AccountCredentials[]>;
|
|
15
22
|
export declare function saveAccount(creds: AccountCredentials): Promise<void>;
|
|
16
23
|
export declare function removeAccount(alias: string): Promise<boolean>;
|
|
24
|
+
/**
|
|
25
|
+
* Carry the organization the proxy observed on a seat into a record that is
|
|
26
|
+
* about to be written anyway — its token refresh (dario#1244). The observation
|
|
27
|
+
* reaches disk without a write of its own: a read-modify-write of a seat file
|
|
28
|
+
* from the request path could land after a refresh's write and put the burned
|
|
29
|
+
* refresh token back, stranding the credential family. A record that already
|
|
30
|
+
* states an organization keeps it.
|
|
31
|
+
*/
|
|
32
|
+
export declare function withObservedOrganization<T extends {
|
|
33
|
+
organizationId?: string;
|
|
34
|
+
}>(record: T, observed: string | undefined): T;
|
|
17
35
|
/** Detect deviceId + accountUuid from an installed Claude Code. */
|
|
18
36
|
export declare function detectClaudeIdentity(): Promise<{
|
|
19
37
|
deviceId: string;
|
package/dist/accounts.js
CHANGED
|
@@ -102,6 +102,19 @@ export async function removeAccount(alias) {
|
|
|
102
102
|
return false;
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Carry the organization the proxy observed on a seat into a record that is
|
|
107
|
+
* about to be written anyway — its token refresh (dario#1244). The observation
|
|
108
|
+
* reaches disk without a write of its own: a read-modify-write of a seat file
|
|
109
|
+
* from the request path could land after a refresh's write and put the burned
|
|
110
|
+
* refresh token back, stranding the credential family. A record that already
|
|
111
|
+
* states an organization keeps it.
|
|
112
|
+
*/
|
|
113
|
+
export function withObservedOrganization(record, observed) {
|
|
114
|
+
if (record.organizationId || !observed)
|
|
115
|
+
return record;
|
|
116
|
+
return { ...record, organizationId: observed };
|
|
117
|
+
}
|
|
105
118
|
/** Detect deviceId + accountUuid from an installed Claude Code. */
|
|
106
119
|
export async function detectClaudeIdentity() {
|
|
107
120
|
const paths = [
|
|
@@ -724,6 +737,7 @@ export async function resyncLoginFromCredentialsIfStale() {
|
|
|
724
737
|
deviceId: loginAcc.deviceId,
|
|
725
738
|
accountUuid: loginAcc.accountUuid,
|
|
726
739
|
grantedAt: tok.grantedAt ?? loginAcc.grantedAt,
|
|
740
|
+
organizationId: loginAcc.organizationId,
|
|
727
741
|
});
|
|
728
742
|
return 'resynced';
|
|
729
743
|
}
|
package/dist/admin-api.d.ts
CHANGED
|
@@ -40,9 +40,10 @@
|
|
|
40
40
|
* writes behind one HTTP request.
|
|
41
41
|
*
|
|
42
42
|
* `GET /admin/accounts` reports each account's persisted metadata — alias,
|
|
43
|
-
* scopes, token expiry — plus its live pool status (5h/7d utilization
|
|
44
|
-
*
|
|
45
|
-
*
|
|
43
|
+
* scopes, token expiry — plus its live pool status (5h/7d utilization and
|
|
44
|
+
* how old that reading is, when its window resets, representative-claim,
|
|
45
|
+
* routing status, request and rejection counts, consecutive auth failures)
|
|
46
|
+
* when the proxy supplies a `poolStatus` snapshot, which it does
|
|
46
47
|
* whenever pool mode is active. It's the headless, admin-token-gated
|
|
47
48
|
* equivalent of the `GET /accounts` pool view.
|
|
48
49
|
*
|
|
@@ -74,6 +75,8 @@ export interface AdminAccountRecord {
|
|
|
74
75
|
expiresAt: number;
|
|
75
76
|
/** Epoch ms of the OAuth grant (refresh-grant.ts); undefined when unknown. */
|
|
76
77
|
grantedAt?: number;
|
|
78
|
+
/** Organization observed on this seat's responses (dario#1244); undefined until seen. */
|
|
79
|
+
organizationId?: string;
|
|
77
80
|
}
|
|
78
81
|
/** Live per-account pool status keyed by alias — see `AdminDeps.poolStatus`. */
|
|
79
82
|
export interface AdminAccountLive {
|
|
@@ -90,9 +93,30 @@ export interface AdminAccountLive {
|
|
|
90
93
|
lastObservedAt: number | null;
|
|
91
94
|
/** Age of that reading in ms, or `null` when never observed. */
|
|
92
95
|
utilAgeMs: number | null;
|
|
96
|
+
/**
|
|
97
|
+
* When the window that reading was measured against rolls over — epoch ms
|
|
98
|
+
* and ms-from-now — or `null` when no response has stated one. For a
|
|
99
|
+
* `rejected` seat this is when the rejection lifts (dario#1244).
|
|
100
|
+
*/
|
|
101
|
+
resetAt: number | null;
|
|
102
|
+
resetInMs: number | null;
|
|
93
103
|
claim: string;
|
|
94
104
|
status: string;
|
|
95
105
|
requestCount: number;
|
|
106
|
+
/**
|
|
107
|
+
* Upstream 429s this account answered. `requestCount` counts requests it
|
|
108
|
+
* served and a 429 served nothing, so a seat parked on its first attempt
|
|
109
|
+
* read `request_count: 0` next to `status: rejected` (dario#1244).
|
|
110
|
+
*/
|
|
111
|
+
rejectedCount: number;
|
|
112
|
+
/** Epoch ms of the most recent 429 on this account, or `null` if never. */
|
|
113
|
+
lastRejectedAt: number | null;
|
|
114
|
+
/** Organization observed on this seat's responses, or `null` if none yet (dario#1244). */
|
|
115
|
+
organizationId: string | null;
|
|
116
|
+
/** Other aliases whose last reading names the same live window — one subscription under several aliases. */
|
|
117
|
+
sharesWindowWith: string[];
|
|
118
|
+
/** Peer instance whose reading this seat currently carries (shared pool state), or `null` for this instance's own. */
|
|
119
|
+
readingFrom: string | null;
|
|
96
120
|
/**
|
|
97
121
|
* Consecutive auth failures on this account (dario#234's cool-down
|
|
98
122
|
* counter). `status: 'auth-cooldown'` alone doesn't distinguish a single
|
package/dist/admin-api.js
CHANGED
|
@@ -99,7 +99,11 @@ async function defaultListAccounts() {
|
|
|
99
99
|
const aliases = await listAccountAliases();
|
|
100
100
|
const loaded = await Promise.all(aliases.map(async (alias) => {
|
|
101
101
|
const a = await loadAccount(alias);
|
|
102
|
-
return a ? {
|
|
102
|
+
return a ? {
|
|
103
|
+
alias: a.alias, scopes: a.scopes, expiresAt: a.expiresAt,
|
|
104
|
+
...(a.grantedAt !== undefined ? { grantedAt: a.grantedAt } : {}),
|
|
105
|
+
...(a.organizationId !== undefined ? { organizationId: a.organizationId } : {}),
|
|
106
|
+
} : null;
|
|
103
107
|
}));
|
|
104
108
|
return loaded.filter((a) => a !== null);
|
|
105
109
|
}
|
|
@@ -355,13 +359,28 @@ export async function handleAdminRequest(req, res, urlPath, deps) {
|
|
|
355
359
|
grant_age_days: grant.ageDays,
|
|
356
360
|
grant_level: grant.level,
|
|
357
361
|
refresh_wall_at: grant.wallAt,
|
|
362
|
+
// From the record (written with the seat's last token refresh), so
|
|
363
|
+
// it is known without a live pool entry.
|
|
364
|
+
organization_id: r.organizationId ?? null,
|
|
358
365
|
// Inline the running pool's live status when this account is in it.
|
|
359
366
|
...(l ? {
|
|
360
367
|
util5h: l.util5h,
|
|
361
368
|
util7d: l.util7d,
|
|
369
|
+
// The reading's age and its window's reset, so `rejected` says
|
|
370
|
+
// since when and until when — the same fields GET /accounts has
|
|
371
|
+
// carried since #1032 and #1232; this surface dropped them.
|
|
372
|
+
last_observed_at: l.lastObservedAt ?? null,
|
|
373
|
+
util_age_ms: l.utilAgeMs ?? null,
|
|
374
|
+
reset_at: l.resetAt ?? null,
|
|
375
|
+
reset_in_ms: l.resetInMs ?? null,
|
|
376
|
+
...(l.organizationId ? { organization_id: l.organizationId } : {}),
|
|
377
|
+
shares_window_with: l.sharesWindowWith ?? [],
|
|
378
|
+
reading_from: l.readingFrom ?? null,
|
|
362
379
|
claim: l.claim,
|
|
363
380
|
status: l.status,
|
|
364
381
|
request_count: l.requestCount,
|
|
382
|
+
rejected_count: l.rejectedCount ?? 0,
|
|
383
|
+
last_rejected_at: l.lastRejectedAt ?? null,
|
|
365
384
|
consecutive_auth_failures: l.consecutiveAuthFailures,
|
|
366
385
|
} : {}),
|
|
367
386
|
};
|
package/dist/analytics.d.ts
CHANGED
|
@@ -19,6 +19,13 @@
|
|
|
19
19
|
import { EventEmitter } from 'node:events';
|
|
20
20
|
export interface RequestRecord {
|
|
21
21
|
timestamp: number;
|
|
22
|
+
/**
|
|
23
|
+
* Who the request was for (dario#1244 follow-up — a gateway shared by a
|
|
24
|
+
* team had no way to say which person's traffic went where): the
|
|
25
|
+
* `x-dario-consumer` header verbatim, else a hash of the body's user id.
|
|
26
|
+
* Absent when neither was present.
|
|
27
|
+
*/
|
|
28
|
+
consumer?: string;
|
|
22
29
|
account: string;
|
|
23
30
|
model: string;
|
|
24
31
|
inputTokens: number;
|
|
@@ -95,7 +102,7 @@ export declare function formatUsageLogLine(requestCount: number, u: {
|
|
|
95
102
|
outputTokens?: number;
|
|
96
103
|
cacheReadTokens?: number;
|
|
97
104
|
cacheCreateTokens?: number;
|
|
98
|
-
}): string;
|
|
105
|
+
}, consumer?: string): string;
|
|
99
106
|
/**
|
|
100
107
|
* The sentinel `claim` dario assigns when a response carried no rate-limit
|
|
101
108
|
* header at all (non-200s, stream aborts, early rejects — see `pool.ts`
|
|
@@ -103,6 +110,22 @@ export declare function formatUsageLogLine(requestCount: number, u: {
|
|
|
103
110
|
* so the overage-guard must never halt on it.
|
|
104
111
|
*/
|
|
105
112
|
export declare const NO_BILLING_CLAIM = "unknown";
|
|
113
|
+
/** Request header naming the consumer a request is for. */
|
|
114
|
+
export declare const CONSUMER_HEADER = "x-dario-consumer";
|
|
115
|
+
/**
|
|
116
|
+
* The consumer named by the `x-dario-consumer` header: one printable-ASCII
|
|
117
|
+
* token, no spaces, at most 64 characters — anything else is treated as
|
|
118
|
+
* absent rather than becoming an analytics key.
|
|
119
|
+
*/
|
|
120
|
+
export declare function consumerFromHeader(value: string | string[] | undefined): string | undefined;
|
|
121
|
+
/**
|
|
122
|
+
* A consumer derived from the request body when no header named one: the
|
|
123
|
+
* Anthropic `metadata.user_id` (Claude Code sends
|
|
124
|
+
* `user_<hash>_account_<uuid>_session_<uuid>`; the session part is dropped
|
|
125
|
+
* so one person is one key across sessions) or the OpenAI `user` field.
|
|
126
|
+
* Hashed, so no account id or raw user id becomes an analytics key.
|
|
127
|
+
*/
|
|
128
|
+
export declare function consumerFromBody(body: Record<string, unknown> | null | undefined): string | undefined;
|
|
106
129
|
/**
|
|
107
130
|
* True when a claim represents real *non-subscription* billing — the
|
|
108
131
|
* condition the overage-guard halts on (see `overage-guard.ts`, #288).
|
|
@@ -188,6 +211,8 @@ export declare class Analytics extends EventEmitter {
|
|
|
188
211
|
summary(windowMinutes?: number): AnalyticsSummary;
|
|
189
212
|
private computeStats;
|
|
190
213
|
private perAccountStats;
|
|
214
|
+
/** Per-consumer usage — only records that carry a consumer take part. */
|
|
215
|
+
private perConsumerStats;
|
|
191
216
|
private perModelStats;
|
|
192
217
|
/**
|
|
193
218
|
* The most recent rate-limit snapshot in the window — current 5h / 7d
|
|
@@ -216,6 +241,19 @@ interface PerAccountStat {
|
|
|
216
241
|
currentUtil7d: number;
|
|
217
242
|
lastClaim: string;
|
|
218
243
|
}
|
|
244
|
+
interface PerConsumerStat {
|
|
245
|
+
requests: number;
|
|
246
|
+
inputTokens: number;
|
|
247
|
+
outputTokens: number;
|
|
248
|
+
cacheReadTokens: number;
|
|
249
|
+
cacheCreateTokens: number;
|
|
250
|
+
/** Share of this consumer's prompt tokens served from cache (see cachedPromptPercent). */
|
|
251
|
+
cachedPromptPercent: number;
|
|
252
|
+
estimatedCost: number;
|
|
253
|
+
/** Seats this consumer's requests were served by, sorted. */
|
|
254
|
+
accounts: string[];
|
|
255
|
+
lastModel: string;
|
|
256
|
+
}
|
|
219
257
|
interface PerModelStat {
|
|
220
258
|
requests: number;
|
|
221
259
|
avgInputTokens: number;
|
|
@@ -262,6 +300,8 @@ export interface AnalyticsSummary {
|
|
|
262
300
|
requests: number;
|
|
263
301
|
};
|
|
264
302
|
perAccount: Record<string, PerAccountStat>;
|
|
303
|
+
/** Keyed by consumer (`x-dario-consumer`, or the hashed user id); empty when no request named one. */
|
|
304
|
+
perConsumer: Record<string, PerConsumerStat>;
|
|
265
305
|
perModel: Record<string, PerModelStat>;
|
|
266
306
|
/** Current 5h / 7d rate-limit utilization (0–1) as of the last request. */
|
|
267
307
|
utilization: {
|