@askalf/dario 5.4.15 → 5.4.17

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.
@@ -0,0 +1,290 @@
1
+ # Drift monitor
2
+
3
+ Dario's bundled CC template (`src/cc-template-data.json`) is the wire-shape
4
+ fallback the proxy uses when it can't fingerprint a live CC install. For that
5
+ fallback to be honest, the bundle has to keep up with what real CC is actually
6
+ sending on the wire. CC drifts in two distinct ways, and there are two
7
+ distinct watchers — one of which needs a self-hosted runner.
8
+
9
+ ## Two classes of drift
10
+
11
+ **Class A — npm-release drift.** Anthropic ships a new `@anthropic-ai/claude-code`
12
+ to npm. The binary changes; the wire shape usually changes with it (new tools,
13
+ new system-prompt slots, beta-header swaps). This is the visible kind.
14
+
15
+ > Watched by `.github/workflows/cc-drift-watch.yml`. Runs on a GitHub-hosted
16
+ > runner, no auth required — polls npm, diffs the dist file, opens an issue
17
+ > when the bundled `_version` is behind latest.
18
+
19
+ **Class B — same-binary remote-config drift.** Anthropic does *not* ship a new
20
+ npm version, but the wire shape changes anyway. We documented an instance of
21
+ this in [CHANGELOG `4.2.1`](../CHANGELOG.md#421---2026-05-17): same CC `2.1.143`
22
+ binary, same machine, captures 24h apart produced materially different
23
+ `/v1/messages` bodies (different anthropic-beta header, +355 char system
24
+ prompt). The npm watcher can't see this; the binary is unchanged.
25
+
26
+ > Watched by `.github/workflows/cc-drift-template-watch.yml`. Runs on a
27
+ > **self-hosted** runner because it needs a live, authenticated CC install
28
+ > to capture against. Runs `node scripts/capture-and-bake.mjs --check` every
29
+ > 30 min and opens (or comments on) a `cc-drift-template`-labeled issue when
30
+ > the captured template diverges from the committed bundle.
31
+ >
32
+ > Watched-by-the-watcher: `.github/workflows/cc-drift-watcher-liveness.yml`
33
+ > runs every 2 hours on a github-hosted runner and opens a
34
+ > `cc-watcher-liveness`-labeled alert if the class-B watcher has not had a
35
+ > successful run within 8 hours (≥ 16 missed cycles). Catches "runner went
36
+ > offline silently" — the failure mode where class-B drift goes uncaught
37
+ > because the watcher itself is down. The liveness watcher lives on
38
+ > github-hosted infrastructure deliberately so it survives the exact failure
39
+ > modes it's designed to detect.
40
+
41
+ **Class C — billing-classifier drift** *(v4.6.0)*. Different signal again:
42
+ Anthropic changes the classifier *rules* — adds a new signal, tightens an
43
+ existing one, flips a threshold — and dario's canonical-rebuild output no
44
+ longer scores as `subscription` even though CC's wire shape is unchanged.
45
+ The template-drift watcher cannot see this because nothing in CC's outbound
46
+ has moved; only an end-to-end "send a real request, inspect the billing
47
+ bucket" probe catches it.
48
+
49
+ > Watched by `.github/workflows/cc-billing-classifier-canary.yml`. Runs daily
50
+ > at 06:30 UTC on the same self-hosted runner. Sends one tiny haiku request
51
+ > through `dario proxy` (canonical-rebuild mode, NOT `--passthrough`),
52
+ > captures the `representative-claim` response header, opens a
53
+ > `cc-billing-canary`-labeled alert when it flips to `overage` / `api`
54
+ > / `unknown`. Auto-closes when it next returns a subscription bucket.
55
+ > Cost: ~1 small subscription request per day.
56
+
57
+ ## What --check considers drift
58
+
59
+ The `--check` mode in `scripts/capture-and-bake.mjs` deliberately ignores
60
+ fields that always differ between runs (`_captured` timestamp, user-agent
61
+ string, `_version` / `_supportedMaxTested` labels). It flags **shape** drift in:
62
+
63
+ - **tools** added or removed (by name set)
64
+ - **anthropic_beta** header values added or removed
65
+ - **system_prompt** content (any character delta)
66
+ - **body_field_order** (top-level JSON key order)
67
+ - **header_order**
68
+ - **agent_identity** content
69
+
70
+ Exit codes:
71
+
72
+ | Code | Meaning |
73
+ |---|---|
74
+ | 0 | Full match — wire shape AND `_version` label both current |
75
+ | 1 | Infrastructure failure (CC not on PATH, capture timeout, scrub leak, or installed CC **older** than the bundle's capture — stale runner) |
76
+ | 2 | **Shape** drift vs current bundled template (needs a real re-bake) |
77
+ | 3 | **Label-only** drift — wire shape matches but `_version` lags the live CC version |
78
+
79
+ The workflow swallows exit 2 and 3 (continues to the next step) so the
80
+ remediation steps can run; exit 1 fails the job.
81
+
82
+ The stale-runner case matters because an older binary cannot observe forward
83
+ drift — it re-captures the *previous* wire shape, which `--check` would report
84
+ as exit-2 drift and the watcher would auto-rebake as a template **downgrade**.
85
+ That exact sequence reached the ship gate on 2026-07-02 (PR #632: runner CC at
86
+ 2.1.197 against the 2.1.198-baked bundle reported the afk-mode beta
87
+ "removed"). The guard compares the captured CC version against the bundle's
88
+ `_version` and exits 1 with an update-the-runner message when the binary is
89
+ older. A *deliberate* downgrade bake (an upstream CC release gets pulled and
90
+ the bundle must go backward) bypasses it with `--allow-older-cc`.
91
+
92
+ ### Exit 2 vs exit 3 — why the split, and why only one auto-merges
93
+
94
+ Because `--check` ignores `_version`, a CC release whose wire shape is
95
+ *unchanged* (the common case for a patch bump) produces a bundle whose shape
96
+ matches live CC but whose `_version` label is stale. The shape-only detector
97
+ sees no drift (exit 0 territory), yet `sdk-drift-watch.yml` — which compares
98
+ the `_version` label against `@anthropic-ai/claude-code@latest` on npm — flags
99
+ it, with **nothing to re-bake**. That mismatch used to require a hand PR every
100
+ time (issues #418, #426/#427, #445/#451).
101
+
102
+ Exit 3 captures exactly that case (`computeDrift` empty **and**
103
+ `bundled._version !== live._version`) and writes the live version to
104
+ `label-target.txt`. The **Label-sync** workflow step then runs
105
+ `scripts/label-sync.mjs`, which bumps only the three version-label fields
106
+ (`_version`, `_supportedMaxTested`, and the `claude-cli/<v>` token in the
107
+ user-agent header) — never the wire shape — patch-bumps `package.json`,
108
+ promotes the CHANGELOG, opens a `bot/template-label-*` PR, and turns on
109
+ **auto-merge**.
110
+
111
+ Auto-merge is safe for exit 3 but **not** for exit 2: an empty `computeDrift`
112
+ is a proof that the tools / system_prompt / beta headers / field orders are
113
+ byte-identical at the live version, so only the version string moves — the
114
+ same deterministic-bump risk class `cc-drift-watch.yml` already auto-merges for
115
+ `SUPPORTED_CC_RANGE.maxTested`. Auto-merge still gates on the required checks
116
+ (build ×3, compat, test, docker-cap-drop-smoke); a red check leaves the PR open
117
+ with the bot branch preserved. A shape rebake (exit 2) changes the wire-shape
118
+ contract, so a human reviews compat-test + the diff before merging.
119
+
120
+ ## Setting up the self-hosted runner
121
+
122
+ Any dedicated Linux host works. Hetzner / DO / EC2 / etc. The runner needs
123
+ Node 22, a logged-in `claude` CLI, and disk for a clone of the repo (~200 MB
124
+ including `node_modules`).
125
+
126
+ ### Prerequisites
127
+
128
+ ```bash
129
+ # 1. Node 22 + npm
130
+ curl -fsSL https://deb.nodesource.com/setup_22.x | sudo bash -
131
+ sudo apt-get install -y nodejs
132
+
133
+ # 2. GitHub CLI (`gh`) — the workflow's issue-open step shells out to it.
134
+ # Without this, --check correctly detects drift but the "Open / update
135
+ # drift issue" step fails with `gh: command not found`.
136
+ sudo apt-get install -y gh # or follow https://github.com/cli/cli#installation
137
+
138
+ # 3. CC + dario CLI (dario provides the headless OAuth flow)
139
+ sudo npm i -g @anthropic-ai/claude-code @askalf/dario
140
+
141
+ # 4. OAuth — manual flow for headless boxes. Run from the host's shell,
142
+ # follow the printed URL in any browser, paste the post-login callback
143
+ # URL back into the SSH session.
144
+ #
145
+ # SHARE the credential with any other CC clients that auto-refresh
146
+ # (e.g. a platform dario container running 24/7). Just run the standard
147
+ # `dario login --manual` against /root/.claude/.credentials.json and let
148
+ # that long-running refresh authority keep the token fresh. The workflows
149
+ # read whatever's current at fire time and never attempt a refresh from
150
+ # the runner side.
151
+ #
152
+ # History: v4.4.1 isolated the runner's credential at /root/.claude-runner
153
+ # to avoid OAuth refresh-token rotation races between the runner and other
154
+ # CC clients on the host. The isolation worked for races but introduced a
155
+ # different failure: the isolated token had no refresh authority between
156
+ # workflow fires (each <10 min, often hours apart), and Anthropic invalidates
157
+ # refresh tokens that idle too long. Result: `invalid_grant` on every
158
+ # workflow fire, recoverable only via interactive `dario login --manual`.
159
+ #
160
+ # Sharing with a 24/7 refresh authority (typical setup: the docker-stack
161
+ # dario container) fixes that. The race the isolation was protecting
162
+ # against is rare in practice — platform dario refreshes proactively
163
+ # when the access token has ~1h life remaining, so workflow runs hit a
164
+ # fresh token and don't need to refresh themselves.
165
+ dario login --manual
166
+
167
+ # 5. Smoke test:
168
+ echo "Reply with PONG" | claude --print # should print PONG
169
+ ```
170
+
171
+ ### One-time repo setup
172
+
173
+ The workflow's issue-open step calls `gh issue create --label cc-drift-template`
174
+ which fails if the label doesn't exist in the repo. Create it once before the
175
+ runner's first execution:
176
+
177
+ ```bash
178
+ gh label create cc-drift-template \
179
+ --description "Bundled CC template has drifted from live capture" \
180
+ --color FBCA04
181
+ ```
182
+
183
+ ### Register the runner
184
+
185
+ In a browser: `https://github.com/<owner>/<repo>/settings/actions/runners/new`.
186
+ Pick Linux x64. GitHub prints a `mkdir`/`curl`/`tar`/`./config.sh` snippet.
187
+ Paste it into the host. At the labels prompt, type **`dario-drift`** — the
188
+ workflow gates on `runs-on: [self-hosted, dario-drift]`, so the label is
189
+ load-bearing.
190
+
191
+ ### Install as a systemd service
192
+
193
+ ```bash
194
+ cd ~/actions-runner
195
+ sudo ./svc.sh install $(whoami) # run as the same user that owns ~/.claude
196
+ sudo ./svc.sh start
197
+ sudo ./svc.sh status # → "active (running)"
198
+ ```
199
+
200
+ If the runner runs as `root` and `~/.claude/.credentials.json` lives under
201
+ `/root/`, `RUNNER_ALLOW_RUNASROOT=1 ./config.sh ...` lets `./config.sh` run as
202
+ root; GitHub's runner otherwise refuses root by default.
203
+
204
+ ### Trigger once to verify
205
+
206
+ GitHub UI → Actions → **CC template drift watch (self-hosted)** → "Run
207
+ workflow." It should pick up the labeled runner within seconds and finish
208
+ within ~60s. Exit 0 (no drift) or exit 2 (issue auto-opened with the drift
209
+ report) both mean the pipeline is healthy. Exit 1 means the capture broke;
210
+ check the workflow logs.
211
+
212
+ After the first successful run, the `*/30 * * * *` cron takes over.
213
+
214
+ ## When --check fires an issue
215
+
216
+ The workflow opens (or comments on) an issue labeled `cc-drift-template`
217
+ containing the `[bake]` output — the list of differing slots, sizes, tool
218
+ names. From there:
219
+
220
+ ```bash
221
+ # On a maintainer machine with CC + a logged-in OAuth credential:
222
+ npm run build
223
+ node scripts/capture-and-bake.mjs # rewrites src/cc-template-data.json
224
+ git diff src/cc-template-data.json # review
225
+ # Open a PR with the re-bake; the auto-release pipeline publishes a patch
226
+ # version. The next clean --check cycle auto-closes the drift issue.
227
+ ```
228
+
229
+ ## Optional: PAT for downstream workflow triggers
230
+
231
+ Since v4.4.0, the watcher auto-opens a `bot/template-rebake-*` PR on detection. Since v4.3.0, `compat-test-self-hosted.yml` is supposed to run on PRs touching `src/cc-template-data.json`. **Without the setup below, it doesn't.** GitHub Actions has a deliberate restriction: workflows authenticated by the default `GITHUB_TOKEN` cannot trigger downstream workflow runs ([docs](https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow)). The auto-rebake PR is therefore invisible to compat-test, and the validation gate the v4.4.0 design promised is effectively bypassed.
232
+
233
+ To close the gap, create a fine-grained personal access token (PAT) scoped to this repo and expose it to the watcher as `DARIO_DRIFT_BOT_PAT`:
234
+
235
+ 1. **Generate** at `https://github.com/settings/personal-access-tokens/new`:
236
+ - Resource owner: your user (or org)
237
+ - Repository access: select `dario` only
238
+ - Permissions: **Contents: read & write**, **Pull requests: read & write**, **Issues: read & write**
239
+ - Expiration: whatever your security policy mandates (90 days / 1 year)
240
+
241
+ 2. **Store** at `Settings → Secrets and variables → Actions → New repository secret`:
242
+ - Name: `DARIO_DRIFT_BOT_PAT`
243
+ - Value: the PAT from step 1
244
+
245
+ 3. **Verify** on the next watcher cycle that detects drift. The auto-rebake PR's "Checks" tab should now include the `compat` job (which it didn't pre-v4.6.5).
246
+
247
+ The watcher workflow uses `GH_TOKEN: ${{ secrets.DARIO_DRIFT_BOT_PAT || secrets.GITHUB_TOKEN }}` for `gh` CLI ops, so the PAT is **optional** — the watcher keeps working without it, you just don't get compat-test gating on auto-rebake PRs (same behavior as v4.4.0 through v4.6.4). The fallback exists so a maintainer can defer the PAT setup without breaking the loop.
248
+
249
+ ## Runner credential rate-limit headroom
250
+
251
+ Workflows that exercise live `dario proxy` paths (compat-test, billing canary, future end-to-end probes) all consume against the runner credential's subscription pool. The cadence assumptions are:
252
+
253
+ | Workflow | Declared cadence | Observed cadence | Requests per fire |
254
+ |---|---|---|---|
255
+ | `cc-drift-template-watch.yml` (`--check`) | every 30 min (`*/30 * * * *`) | typically every 2-4h | 1 capture (no /v1/messages traffic — MITM-only) |
256
+ | `cc-billing-classifier-canary.yml` | daily 06:30 UTC | daily | 1 small haiku request |
257
+ | `compat-test-self-hosted.yml` | per qualifying PR | per qualifying PR | ~11 small requests |
258
+
259
+ **Cron scheduler reality.** GitHub Actions' free-tier cron scheduler is best-effort, not guaranteed. The class-B watcher declares `*/30 * * * *` but in practice GitHub honors it every 2-4 hours on this repo. The liveness alarm (added v4.4.2) has its threshold set to 8h (raised from 3h in v4.7.1) to absorb this skew — anything past that is signal, not scheduler noise. If you need a tighter SLA (sub-hour), self-host the runner *and* the cron driver (e.g. a cron entry on the same Hetzner box invoking `gh workflow run` directly).
260
+
261
+ At steady state, this is comfortably inside Pro/Max headroom. The failure mode to watch for is **batched firing** — manually re-triggering the same workflow several times in a single hour, or PRs landing in rapid succession that each fire compat-test. We tripped this during the v4.6.x rollout: a half-dozen manual re-runs in a 2-hour window 429'd the runner credential. Pro/Max accounts have per-hour rate caps as well as per-5h / per-7d pools, and the per-hour cap is what surfaces first.
262
+
263
+ If the runner credential is rate-limited and a workflow run reports 429s across the board, the right diagnosis order is: (a) check `claude --print` directly — if it 429s, the credential pool is dry, just wait an hour; (b) check the credential is still on a subscription account (`dario doctor`); (c) check workflow cadence assumptions haven't changed.
264
+
265
+ The runner shares its OAuth credential with any other long-running CC client on the box (typically the platform dario container, which auto-refreshes 24/7). Sharing is intentional: a workflow that fires sparsely cannot keep its own refresh token alive — Anthropic invalidates idle refresh tokens, and `invalid_grant` then breaks every subsequent run. Letting a 24/7 refresh authority own the token rotation eliminates that failure mode at the cost of competing for the same Pro/Max headroom. With current cadence (drift-template-watch every 30 min + compat per PR + canary daily), runner-side burn on the shared account is a manageable fraction of the headroom available on a Max plan; reducing cadence further is a knob if a particular workload needs more of it.
266
+
267
+ ## Why a self-hosted runner
268
+
269
+ GitHub-hosted runners can't capture CC. They have no Pro/Max subscription
270
+ session, no MITM cert trust for CC's loopback proxy, no way to authenticate
271
+ against `claude.ai/oauth`. Anything that needs real CC running against real
272
+ Anthropic has to live on a host you control with an account you've logged
273
+ in to.
274
+
275
+ The runner is read-only against the repo (`contents: read`) and only writes
276
+ to issues (`issues: write`). It cannot push, tag, or release.
277
+
278
+ ## Platform-superset preservation
279
+
280
+ CC ships different tools on different platforms — currently just `PowerShell`
281
+ on Windows, but the surface grows over time. The bundled template is meant to
282
+ be a **union** across platforms, and `filterToolsForPlatform()` strips it down
283
+ at request time. So a bake on Linux must not silently drop the Windows tool
284
+ set, or Windows dario users would lose those tools on the next release.
285
+
286
+ `scripts/capture-and-bake.mjs` preserves tools from the previous bundle whose
287
+ names are listed in `PLATFORM_ONLY_TOOLS` for a platform other than the
288
+ baking host's. The merged set is re-sorted alphabetically to match CC's wire
289
+ order. The runner can therefore bake from Linux without regressing Windows
290
+ users.
package/docs/faq.md ADDED
@@ -0,0 +1,145 @@
1
+ # FAQ
2
+
3
+ **Does this violate Anthropic's terms of service?**
4
+ Mechanically: dario's Claude backend uses your existing Claude Code credentials with the same OAuth tokens CC uses. It authenticates you as you, with your subscription, through Anthropic's official API endpoints. Whether any particular use complies with Anthropic's current terms of service is between you and Anthropic — consult their terms and your own subscription agreement. This project is an independent, unofficial, third-party tool and does not provide legal advice. See [DISCLAIMER.md](../DISCLAIMER.md).
5
+
6
+ **What subscription plans work on the Claude backend?**
7
+ Any plan whose account currently has Claude Code access — Max has it unconditionally; Pro has it as of this writing but that's an upstream decision that has moved once already (see next entry). If `claude /login` on your account works and `claude -p "hi"` returns a response on subscription billing, dario's Claude backend will work too. If Anthropic removes Claude Code from your plan tier, dario's Claude backend stops working on that account — there is nothing dario can do at the client side to change that. Swap to a plan with Claude Code access, or use an OpenAI-compat backend instead.
8
+
9
+ **Is it true Anthropic removed Claude Code from Pro plans?**
10
+ On 2026-04-21 Anthropic temporarily removed Claude Code from new Pro signups, per [wheresyoured.at](https://www.wheresyoured.at/news-anthropic-removes-pro-cc/). Existing Pro users reportedly kept access; Anthropic's Head of Growth characterized it as "a small test of 2% of new prosumer signups," and the change was reversed at an unknown time. If you are a Pro user and dario's Claude backend stops billing against your subscription without warning, this is the class of thing to check — run `claude -p "hi"` directly and see whether Anthropic itself routes you to subscription billing. If they don't, dario can't either. The practical mitigation on dario's side is [multi-account pool mode](./multi-account-pool.md) — having a backup account on a plan Anthropic hasn't moved the goalposts on, so a single plan-tier change doesn't take all your traffic down at once.
11
+
12
+ **Does it work with Team / Enterprise?**
13
+ Yes — tested and confirmed working as long as your plan includes Claude Code access.
14
+
15
+ **Anthropic announced that `claude -p` and Agent SDK usage would move to a separate credit pool on 2026-06-15. Did that break dario's Claude backend?**
16
+ No — and the split itself was **paused before it took effect**. Anthropic announced it 2026-05-13, scheduled it for 2026-06-15, then paused it before that date; the Help Center now states Agent-SDK and `claude -p` usage continue drawing from your existing subscription pool unchanged, with a promise of advance notice before any revised version. So today `claude -p` and the Agent SDK still bill subscription whether or not you use dario.
17
+
18
+ dario is built to hold either way. The Claude backend sends every request as **interactive Claude Code** wire-shape — full template replay of headers, body key order, TLS ClientHello, session-id lifecycle, inter-request timing. The upstream billing classifier sees an interactive CC session regardless of which local tool (claude -p subprocess, Agent SDK app, Cline, Aider, your own scripts) originated the call. That's the entire point of the wire-fidelity work in [`wire-fidelity.md`](./wire-fidelity.md), and it predates the announcement.
19
+
20
+ What that means in practice:
21
+
22
+ - Workloads that route through dario bill against your **subscription pool** (Pro $20, Max 5x $100, Max 20x $200) — the same today as before the announcement, and the same if a revised split ships later.
23
+ - If the split does return, workloads that bypass dario and call `claude -p` directly would count against the **separate credit pool** (a fixed monthly grant rather than the rolling subscription bucket — and once exhausted, metered API pricing). Right now, paused, they bill subscription like everything else.
24
+ - Workloads that use the Agent SDK with API keys are unaffected either way (already metered API).
25
+
26
+ Two questions to verify at any time — and the exact check to run the day a revived split lands:
27
+
28
+ 1. **Is dario's traffic still landing in the subscription bucket?** Run `claude -p "hi"` directly (no dario), check `representative-claim` and related rate-limit headers — that tells you the bucket Anthropic put the direct call in. Then run the same prompt through dario and check the same headers. Both should show a subscription bucket (`five_hour` / `seven_day`). If dario's path ever shows an agent-credit or `overage` bucket, file an issue — that's the drift the live template extractor, the [drift detector](./../scripts/capture-full-body.mjs), and the daily billing-classifier canary exist to catch (the canary runs this exact check automatically and opens an issue on a bad bucket).
29
+ 2. **Did Anthropic tighten OAuth-token classification?** If access-token bearer alone ever signals "non-interactive," dario would have to add session affinity or a re-auth dance. The same rate-limit-header diagnostic surfaces it. None of this is observed today.
30
+
31
+ No config change is needed on the user side, split or no split — same install, same `localhost:3456`, same `ANTHROPIC_BASE_URL=http://localhost:3456` env var.
32
+
33
+ **Do I need Claude Code installed?**
34
+ Recommended for the Claude backend, not strictly required. With CC installed, `dario login` picks up your credentials automatically, and the live template extractor reads your CC binary on every startup so the template stays current. Without CC, dario runs its own OAuth flow and falls back to the bundled template snapshot (scrubbed of host context at bake time as of v3.21). Drift detection warns you if your installed CC doesn't match the captured template, so upgrade windows don't silently ship stale templates.
35
+
36
+ **Do I need Bun?**
37
+ Optional, strongly recommended for Claude-backend requests. Dario auto-relaunches under Bun when available so the TLS ClientHello matches CC's runtime. Without Bun, dario runs on Node.js and works fine — the TLS ClientHello is the only observable difference. As of v3.23, `dario doctor` surfaces the mismatch explicitly and `--strict-tls` refuses to start proxy mode until it's resolved.
38
+
39
+ **Can I use dario without a Claude subscription?**
40
+ Yes. Skip `dario login`, just run `dario backend add openai --key=...` (or any OpenAI-compat URL) and `dario proxy`. Claude-backend requests will return an authentication error; OpenAI-compat requests will work normally. Dario becomes a local OpenAI-compat router with no Claude involvement.
41
+
42
+ **Can I route non-OpenAI providers through dario?**
43
+ Yes — anything that speaks the OpenAI Chat Completions API. Groq, OpenRouter, LiteLLM, vLLM, Ollama's openai-compat mode, your own vLLM server, any hosted inference endpoint that exposes `/v1/chat/completions`. Just `dario backend add <name> --key=... --base-url=...`.
44
+
45
+ **My subscription usage through dario is higher than running Claude Code directly. Why?**
46
+ Two things drive this, and neither is proxy overhead — on the genuine-Claude-Code path dario forwards your request verbatim (system prompt + tools), adding only a ~20-token billing tag. First, the dominant cost on any cold turn is your **own** system prompt: agent harnesses (OpenClaw, context-mode, custom frameworks) can inject 100K+ tokens of instructions, and dario relays them byte-for-byte, so they cost the same sent direct. Second is the **prompt-cache TTL**. Anthropic caches your system+tools prefix so repeat turns read it warm instead of rebuilding — but the default lifetime is **5 minutes**. Interactive Claude Code on a subscription requests a **1-hour** TTL (`cache_control:{"type":"ephemeral","ttl":"1h"}` plus the `extended-cache-ttl-2025-04-11` beta); many SDK/agent harnesses only send the bare 5-minute stamp. dario mirrors exactly what your client sends — so if your harness sends 5m and you leave gaps longer than 5 minutes between messages, the prefix expires and is re-created every time. (Quick check: two messages **30 seconds** apart should be cheap; two messages **6 minutes** apart on a 5-minute stamp both pay full creation — that gap is the cost, not dario.)
47
+
48
+ Fixes, cheapest first:
49
+ - Keep turns within 5 minutes where you can — the prefix stays cached.
50
+ - Trim the injected system prompt; it's the biggest line item on every cold turn.
51
+ - Have your harness emit the 1-hour stamp + the `extended-cache-ttl-2025-04-11` beta — dario mirrors it and the prefix survives idle gaps.
52
+ - If the harness can't, set **`DARIO_CACHE_TTL_1H=1`**: dario forces the 1-hour TTL (and adds the enabling beta) on every request. Tradeoff — 1-hour cache *writes* bill ~2× the 5-minute rate, so it only wins when your idle gaps routinely exceed 5 minutes; on rapid back-to-back turns it costs more. `DARIO_CACHE_TTL_5M=1` forces the opposite (always 5m). Background: [#678](https://github.com/askalf/dario/issues/678).
53
+
54
+ **Delivering the env var:** it has to reach dario's own process. Running directly, prefix it — `DARIO_CACHE_TTL_1H=1 dario proxy …`. In **Docker / Compose**, put it in the container's `environment:` (or `env_file:`) — a value only in a host `.env` that the compose file doesn't pass through never reaches the process, and the flag is silently a no-op. Confirm it landed with `docker exec <container> printenv DARIO_CACHE_TTL_1H` (expect `1`).
55
+
56
+ **Something's wrong. Where do I start?**
57
+ `dario doctor`. One command, one aggregated report — dario version, Node, platform, runtime/TLS classification, CC binary compat, template source + age + drift, OAuth status, pool state, backends, sub-agent install state, home dir. Exit code 1 if any check fails. Paste the output when you file an issue. (If you're inside Claude Code, `dario subagent install` once and then ask CC to "use the dario sub-agent to run doctor" — same output, no context switch.)
58
+
59
+ **OpenClaw returns 401 after I set `DARIO_API_KEY` (or upgrade past v3.30.6).**
60
+ If you run `dario proxy --host=0.0.0.0` (non-loopback), dario requires `DARIO_API_KEY` to be set so it's not an open subscription relay. OpenClaw 2026.2.17+ prefers `~/.openclaw/agents/main/agent/auth-profiles.json` over `openclaw.json`'s `apiKey` field or the `ANTHROPIC_API_KEY` env var — so if you have a stale Anthropic token in `auth-profiles.json` from an earlier setup, OpenClaw sends *that* token instead of `dario`, and dario rejects the request with `Authorization present but value mismatch` (visible under `dario proxy -v`, added in v3.31.2).
61
+
62
+ Three fixes, in order of simplicity:
63
+
64
+ 1. **Use loopback.** `dario proxy --host=127.0.0.1` — auth only enforced on non-loopback binds, no `DARIO_API_KEY` required, no OpenClaw changes. Best if you don't actually need LAN reach to dario.
65
+ 2. **Delete the Anthropic auth profile.** Remove the `"anthropic:default"` entry from `~/.openclaw/agents/main/agent/auth-profiles.json`. OpenClaw then falls back through the config chain and picks up `ANTHROPIC_API_KEY=dario` from the env. Confirmed working by [@tetsuco in #97](https://github.com/askalf/dario/issues/97).
66
+ 3. **Overwrite the auth profile.** `openclaw models auth paste-token --provider anthropic` and paste `dario`. Replaces whatever key was in there — keep a backup if you use it elsewhere.
67
+
68
+ Diagnose with `dario proxy -v` — the reject log (v3.31.2+) reports header-name only (never the value, since it may be a real credential you mistyped) and tells you which of the three configs is actually being hit.
69
+
70
+ **Claude Code's WebFetch fails on every domain with "Unable to verify if domain … is safe to fetch."**
71
+ Not a dario issue — dario is not in this code path, and no dario configuration can affect it. Before fetching a URL, Claude Code runs a domain-safety preflight: a direct call to `https://api.anthropic.com/api/web/domain_info?domain=…` (older builds used `claude.ai`, which the error text still names). That URL is absolute, so it ignores `ANTHROPIC_BASE_URL` entirely — the preflight never reaches dario, and only the real fetch happens after it passes. The error means the preflight *request itself* failed to complete, not that the domain is blocklisted — common in proxy setups, headless/CI boxes, and networks where that direct call can't succeed.
72
+
73
+ Fix: skip the preflight. Add to `~/.claude/settings.json` (or the project's `.claude/settings.json`) and restart Claude Code:
74
+
75
+ ```json
76
+ { "skipWebFetchPreflight": true }
77
+ ```
78
+
79
+ The setting is Claude Code's own, meant for "enterprise environments with restrictive security policies"; with it set, WebFetch skips the domain check and fetches directly. Granting `Bash(curl:*)` so CC falls back to curl works too. Background: [#822](https://github.com/askalf/dario/issues/822).
80
+
81
+ **My RDP / RemotePC session randomly drops while claude is working. Logs say `error 121` / `0x80070079` / "ERROR_SEM_TIMEOUT". Network is otherwise fine — other devices don't drop, gateway pings are clean.**
82
+ Cause: heavy claude tool work bursts CPU on a small machine, the kernel network IO threads can't get scheduled, the RDP socket write times out, your session drops. The drops are real but the network path is not — they're caused by CPU starvation above the NIC layer, which is why every adapter (Ethernet, Wi-Fi, USB Wi-Fi) drops the same way. Confirmed pattern when running claude on a 4-core / 4-thread CPU you're RDP'd into.
83
+
84
+ Two fixes, in order of progressively-stronger:
85
+
86
+ 1. **Lower claude's scheduling priority so the kernel can preempt it for network IO.** On Windows, launch it below-normal — `start /belownormal /b claude` (cmd), or set it after spawn with `(Get-Process claude).PriorityClass = 'BelowNormal'` (or Process Lasso for permanence). Same throughput when nothing else needs CPU. Escalate to `Idle` priority if drops continue — claude then only runs when nothing else is ready (~5-10% slower agent loops in practice). *(Before v5.0, `dario shim --priority=below-normal -- claude` did this for you; shim was removed in v5.0 — set the priority via the OS instead.)*
87
+ 2. **Reserve a CPU core for the OS.** On Windows, `(Get-Process claude).ProcessorAffinity = 0x07` reserves logical CPU 3 (mask covers cores 0-2). Set after spawn or via Process Lasso for permanence. On a 4-core/4-thread machine, this guarantees the kernel always has a free core for network IO no matter what claude does.
88
+
89
+ If drops continue past all three: the underlying cause is hardware capacity. The same workload on a modern 8C/16T machine will not exhibit this. Move the heavy claude session off the RDP host, or upgrade the host.
90
+
91
+ **What happens when Anthropic rotates the OAuth config?**
92
+ Dario auto-detects OAuth config from the installed Claude Code binary. When CC ships a new version with rotated values, dario picks them up on the next run. Cache at `~/.dario/cc-oauth-cache-v6.json`, keyed by the CC binary fingerprint. The cache path version bumps each time the canonical OAuth config shape changes so stale caches regenerate automatically on upgrade — v3 → v4 in v3.19.4 (scope-list flip CC v2.1.104 → v2.1.107), v4 → v5 in v3.31.3 (authorize URL `claude.com/cai/` → `claude.ai/` host normalization), v5 → v6 in v3.31.4 (6-scope restore after CC v2.1.116).
93
+
94
+ If Anthropic rotates the values before the detector is updated, you can temporarily override any field with env vars (`DARIO_OAUTH_CLIENT_ID`, `DARIO_OAUTH_AUTHORIZE_URL`, `DARIO_OAUTH_TOKEN_URL`, `DARIO_OAUTH_SCOPES`) or by writing `~/.dario/oauth-config.override.json`:
95
+
96
+ ```json
97
+ {
98
+ "clientId": "...",
99
+ "authorizeUrl": "https://claude.com/cai/oauth/authorize",
100
+ "tokenUrl": "https://platform.claude.com/v1/oauth/token",
101
+ "scopes": "user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload"
102
+ }
103
+ ```
104
+
105
+ Env vars win over the file. Set `DARIO_OAUTH_DISABLE_OVERRIDE=1` to force pure auto-detection.
106
+
107
+ **What happens when Anthropic changes the CC request template?**
108
+ Dario extracts the live request template from your installed Claude Code binary on startup — the system prompt, tool schemas, user-agent, beta flags, header insertion order, static header values, and top-level request-body key order — and uses those to replay requests instead of a version pinned into dario itself. When CC ships a new version with a tweaked template, the next `dario proxy` run picks it up automatically. Drift detection forces a refresh when the installed CC version changes under dario, and the nightly `cc-drift-watch` workflow catches upstream rotations (client_id, URLs, tool set, version) the day they ship on npm.
109
+
110
+ **Why does `dario accounts list` show an account called `login` I never added?**
111
+ That's your `dario login` credentials, materialized into the pool automatically. As of v5.0 the account pool is dario's one credential model, so a plain `dario login` is a **pool of one** stored as `~/.dario/accounts/login.json` under the reserved `login` alias — the back-fill runs on `dario login` itself and again on `dario proxy` startup. Your original `~/.dario/credentials.json` is untouched (the copy is one-way), so `dario accounts remove login` is safe if you don't want it pooled — the next `dario login` / `dario proxy` just re-materializes it. See [The account pool](./multi-account-pool.md) for the full picture.
112
+
113
+ **First time setup on a fresh Claude account.**
114
+ If dario is the first thing you run against a brand-new Claude account, prime the account with a few real Claude Code commands first:
115
+ ```bash
116
+ claude --print "hello"
117
+ claude --print "hello"
118
+ ```
119
+ This establishes a session baseline. Without priming, brand-new accounts occasionally see billing classification issues on first use.
120
+
121
+ **I'm hitting rate limits on the Claude backend. What do I do?**
122
+ Claude subscriptions have rolling 5-hour and 7-day usage windows. Check utilization with Claude Code's `/usage` command or the [statusline](https://code.claude.com/docs/en/statusline). For multi-agent workloads, add more accounts and let pool mode distribute the load: `dario accounts add <alias>`. Session stickiness keeps long conversations pinned to one account so the prompt cache isn't destroyed by rotation.
123
+
124
+ **I'm seeing `representative-claim: seven_day` in my rate-limit headers instead of `five_hour`. Am I being downgraded to API billing?**
125
+
126
+ **No.** You're still on subscription billing. Both `five_hour` and `seven_day` are the same subscription billing mode — two different accounting buckets inside it.
127
+
128
+ | Claim | What it means |
129
+ |---|---|
130
+ | `five_hour` | You're well inside your 5-hour window; billing against the short-term bucket. |
131
+ | `seven_day` | You've exhausted (or come close to exhausting) the 5-hour window for this rolling cycle, so Anthropic is charging this request against the 7-day bucket. **Still subscription billing. Still your plan.** Not API pricing, not overage. |
132
+ | `overage` | Both subscription windows are effectively exhausted. *This* is where per-token Extra Usage charges kick in — if you've enabled Extra Usage on the account. If not, you get 429'd instead. |
133
+
134
+ Seeing `seven_day` is a healthy state. Your Max plan is doing exactly what it's supposed to do: letting you keep working past short bursts of heavy use by absorbing them into the larger 7-day bucket. When your 5-hour window rolls forward enough, the claim on new requests will go back to `five_hour` on its own. If the 7-day bucket is painful, add more Claude subscriptions to the pool — each account has its own independent 5h/7d windows, and pool mode routes each request to the account with the most headroom.
135
+
136
+ Standalone writeup: [Discussion #1 — full rate-limit-header breakdown](https://github.com/askalf/dario/discussions/1).
137
+
138
+ **My multi-agent workload is getting reclassified to overage even though dario mirrors the CC wire shape per request. Why?**
139
+ Reclassification at high agent volume is not a per-request problem. The upstream billing logic takes cumulative per-OAuth-session aggregates into account — token throughput, conversation depth, streaming duration, inter-arrival timing, thinking-block volume. Dario's Claude backend can make each individual request match Claude Code and still hit this wall on a long-running agent session. Thorough diagnostic work was contributed by [@belangertrading](https://github.com/belangertrading) in [#23](https://github.com/askalf/dario/issues/23). The practical answer at the dario layer is **pool mode** — distribute load across multiple subscriptions so no single account accumulates signal along any single dimension. See [Multi-account pool mode](./multi-account-pool.md). The v3.22 – v3.28 wire-fidelity track (pacing, stream-drain, session-id lifecycle) also narrows the cumulative signal on a single account — see [Wire-fidelity axes](./wire-fidelity.md).
140
+
141
+ **My proxy is on Node, not Bun. What's the actual risk?**
142
+ Node uses OpenSSL, Bun uses BoringSSL — the TLS ClientHello differs enough to yield a distinct JA3/JA4 hash. The upstream service can see the hash. Whether any routing decisions depend on it today is not published; making the axis visible is the v3.23 contribution. If certainty matters to you, install Bun (dario auto-relaunches under it) or run `dario proxy --strict-tls` to fail loud. If it doesn't, the warning is ignorable — dario still works, the TLS ClientHello is just the one observable axis left.
143
+
144
+ **Why "dario"?**
145
+ It's a name, not an acronym. Don't overthink it.