@askalf/dario 4.8.75 → 4.8.77

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
@@ -200,9 +200,9 @@ The tool doesn't know. The backend doesn't know. dario is the seam.
200
200
 
201
201
  ## Overage guard
202
202
 
203
- A subscriber should never see a single `representative-claim: overage` response during normal operation. One means something is wrong — wire-shape drift, a classifier change, an account misconfig — and continuing to forward requests in the same shape bleeds real money (accounts with extra-usage enabled) or returns a wall of rejections (accounts without it). The first hit is the signal; the second through hundredth are damage.
203
+ A subscriber should never see a single response billed outside their subscription pool during normal operation. One means something is wrong — wire-shape drift, a classifier change, an account misconfig — and continuing to forward requests in the same shape bleeds real money (accounts with extra-usage enabled) or returns a wall of rejections (accounts without it). The first hit is the signal; the second through hundredth are damage.
204
204
 
205
- So the moment any upstream response carries `representative-claim: overage`, dario **halts the proxy**. Every subsequent request returns `503` with an Anthropic-shaped error body the client surfaces verbatim, until you run `dario resume`, press `R` on the TUI, or the cooldown clears (default 30 min). The halt is visible across the TUI's Status, Hits, and Analytics tabs, fires a best-effort native OS notification, and emits named SSE events.
205
+ So the moment any upstream response bills to something other than your subscription pool — `representative-claim: overage`, `api`, or a new credit/SDK bucket like the one the 2026-06-15 Agent-SDK split introduces — dario **halts the proxy**. The check is an allow-list, not a match on `overage`: 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 credit-bucket claim dario has never seen still halts. Every subsequent request returns `503` with an Anthropic-shaped error body the client surfaces verbatim, until you run `dario resume`, press `R` on the TUI, or the cooldown clears (default 30 min). The halt is visible across the TUI's Status, Hits, and Analytics tabs, fires a best-effort native OS notification, and emits named SSE events. (In `--upstream-api-key` passthrough mode the guard is off — `api` billing is the point there, not a failure.)
206
206
 
207
207
  ```
208
208
  ┌─ dario ─────────────────────────────[ q quit · Tab next · ? help ]──┐
@@ -368,26 +368,27 @@ Follow [@ask_alf](https://x.com/ask_alf) for drift bulletins as they happen.
368
368
 
369
369
  MIT — see [LICENSE](LICENSE) and [DISCLAIMER.md](DISCLAIMER.md).
370
370
 
371
- ## Also by askalf
371
+ ## Own Your Stack
372
372
 
373
- | Project | What it does |
374
- |---|---|
375
- | [askalf platform](https://askalf.org) | Self-hosted AI workforce — agents that run real business + life work. Uses dario as its LLM substrate. *Early access at [askalf.org](https://askalf.org).* |
376
- | [hands](https://github.com/askalf/hands) | Cross-platform computer-use agent — your LLM on your mouse, keyboard, and screen. Routes through dario or any Anthropic-compat. |
377
- | [deepdive](https://github.com/askalf/deepdive) | Local research agent. One command, cited answer. Plan → search → headless fetch → extract → synthesize. |
378
- | [agent](https://github.com/askalf/agent) | Connect any device to an askalf fleet — runs the shell or Claude Code tasks the fleet dispatches. |
379
- | [browser-bridge](https://github.com/askalf/browser-bridge) | Stealth headless Chromium in a container, CDP on 9222. Playwright / Puppeteer / MCP-compatible. |
380
- | [claude-sync](https://github.com/askalf/claude-sync) | Sync Claude Code sessions across machines via a portable `.ccsync` file. |
381
- | [pgflex](https://github.com/askalf/pgflex) | One Postgres API, two modes — real PostgreSQL for production, PGlite (WASM) for dev. |
382
- | [redisflex](https://github.com/askalf/redisflex) | One Redis API, two modes — ioredis for production, in-process for dev. Includes a BullMQ-shaped in-memory queue. |
373
+ 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.
383
374
 
375
+ | Tool | |
376
+ |---|---|
377
+ | **[dario](https://github.com/askalf/dario)** | own your routing — your subscription, in any tool *(you are here)* |
378
+ | **[deepdive](https://github.com/askalf/deepdive)** | own your research — local agent, cited answers |
379
+ | **[hands](https://github.com/askalf/hands)** | own your computer-use — your LLM on your own mouse & keyboard |
380
+ | **[agent](https://github.com/askalf/agent)** | own your fleet — connect any device, dispatch real work |
381
+ | **[browser-bridge](https://github.com/askalf/browser-bridge)** | own your browser — stealth headless Chromium, your CDP endpoint |
382
+ | **[claude-sync](https://github.com/askalf/claude-sync)** | own your sessions — sync Claude Code across machines |
383
+ | **[askalf platform](https://askalf.org)** | own your operation — the self-hosted AI workforce, running on this stack |
384
384
 
385
385
  ---
386
386
 
387
- ## Built by Sprayberry Labs
387
+ ## Built by Thomas Sprayberry
388
388
 
389
- This is one of the open-source building blocks from **[Sprayberry Labs](https://sprayberrylabs.com)** an independent studio (Atlanta, GA) that ships bespoke software and **fixed-price code & security audits**, delivered with the AI workforce these tools are part of.
389
+ dario is part of **Own Your Stack** — the open toolkit behind **[Sprayberry Labs](https://sprayberrylabs.com)**, an independent studio (Atlanta, GA) that ships bespoke software with the autonomous AI workforce these tools are part of.
390
390
 
391
- Part of the [askalf](https://askalf.org) ecosystem — a self-hosted AI workforce platform, now in early access.
391
+ 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)**
392
392
 
393
- **Got a codebase that needs an expert read?** → **[Scan a repo — free mini-audit](https://sprayberrylabs.com)**, or see the **$1,500 fixed-price Audit** and build Sprints. · [sprayberrylabs.com](https://sprayberrylabs.com) · hello@sprayberrylabs.com
393
+ ---
394
+ Part of **[Own Your Stack](https://github.com/askalf)** — own your AI infrastructure instead of renting it. Built by Thomas Sprayberry.
@@ -55,6 +55,34 @@ export type BillingBucket = 'subscription' | 'subscription_fallback' | 'extra_us
55
55
  * billing bucket. Pure function; no state; safe to call from any context.
56
56
  */
57
57
  export declare function billingBucketFromClaim(claim: string | null | undefined): BillingBucket;
58
+ /**
59
+ * The `representative-claim` values that mean "billed against the subscription
60
+ * pool" — the place dario exists to keep traffic. `five_hour`/`seven_day` and
61
+ * their server-side `_fallback` variants are all subscription billing (see
62
+ * `billingBucketFromClaim` + discussion #1). Anything else is either a
63
+ * non-subscription billing classification or the `unknown` sentinel below.
64
+ */
65
+ export declare const SUBSCRIPTION_CLAIMS: ReadonlySet<string>;
66
+ /**
67
+ * The sentinel `claim` dario assigns when a response carried no rate-limit
68
+ * header at all (non-200s, stream aborts, early rejects — see `pool.ts`
69
+ * `parseRateLimits` / `EMPTY_SNAPSHOT`). It is NOT a billing classification,
70
+ * so the overage-guard must never halt on it.
71
+ */
72
+ export declare const NO_BILLING_CLAIM = "unknown";
73
+ /**
74
+ * True when a claim represents real *non-subscription* billing — the
75
+ * condition the overage-guard halts on (see `overage-guard.ts`, #288).
76
+ *
77
+ * Deliberately an allow-list, not `claim === 'overage'`: it halts on anything
78
+ * that is NOT a known subscription claim AND NOT the `unknown` sentinel. That
79
+ * catches `overage` and `api` as before, but ALSO any new credit/SDK bucket
80
+ * string Anthropic introduces — e.g. the 2026-06-15 Agent-SDK/headless split,
81
+ * whose credit-bucket claim dario has never observed (it keeps traffic in the
82
+ * pool) and so cannot hardcode. `unknown` is exempt: halting on it would halt
83
+ * the proxy on every transient non-200/stream-abort.
84
+ */
85
+ export declare function isNonSubscriptionBilling(claim: string | null | undefined): boolean;
58
86
  export declare class Analytics extends EventEmitter {
59
87
  private records;
60
88
  private maxRecords;
package/dist/analytics.js CHANGED
@@ -37,6 +37,43 @@ export function billingBucketFromClaim(claim) {
37
37
  return 'unknown';
38
38
  }
39
39
  }
40
+ /**
41
+ * The `representative-claim` values that mean "billed against the subscription
42
+ * pool" — the place dario exists to keep traffic. `five_hour`/`seven_day` and
43
+ * their server-side `_fallback` variants are all subscription billing (see
44
+ * `billingBucketFromClaim` + discussion #1). Anything else is either a
45
+ * non-subscription billing classification or the `unknown` sentinel below.
46
+ */
47
+ export const SUBSCRIPTION_CLAIMS = new Set([
48
+ 'five_hour',
49
+ 'seven_day',
50
+ 'five_hour_fallback',
51
+ 'seven_day_fallback',
52
+ ]);
53
+ /**
54
+ * The sentinel `claim` dario assigns when a response carried no rate-limit
55
+ * header at all (non-200s, stream aborts, early rejects — see `pool.ts`
56
+ * `parseRateLimits` / `EMPTY_SNAPSHOT`). It is NOT a billing classification,
57
+ * so the overage-guard must never halt on it.
58
+ */
59
+ export const NO_BILLING_CLAIM = 'unknown';
60
+ /**
61
+ * True when a claim represents real *non-subscription* billing — the
62
+ * condition the overage-guard halts on (see `overage-guard.ts`, #288).
63
+ *
64
+ * Deliberately an allow-list, not `claim === 'overage'`: it halts on anything
65
+ * that is NOT a known subscription claim AND NOT the `unknown` sentinel. That
66
+ * catches `overage` and `api` as before, but ALSO any new credit/SDK bucket
67
+ * string Anthropic introduces — e.g. the 2026-06-15 Agent-SDK/headless split,
68
+ * whose credit-bucket claim dario has never observed (it keeps traffic in the
69
+ * pool) and so cannot hardcode. `unknown` is exempt: halting on it would halt
70
+ * the proxy on every transient non-200/stream-abort.
71
+ */
72
+ export function isNonSubscriptionBilling(claim) {
73
+ if (!claim || claim === NO_BILLING_CLAIM)
74
+ return false;
75
+ return !SUBSCRIPTION_CLAIMS.has(claim);
76
+ }
40
77
  // Anthropic pricing (per 1M tokens, USD). Not authoritative — used for
41
78
  // rough burn-rate display in the /analytics summary.
42
79
  const PRICING = {
package/dist/cch.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ /** Verified per-release seeds, keyed on `major.minor.patch`. */
2
+ export declare const CCH_SEEDS: Record<string, bigint>;
3
+ /** Canonical xxHash64 of `data` with a 64-bit `seed`. */
4
+ export declare function xxh64(data: Uint8Array, seed: bigint): bigint;
5
+ /**
6
+ * Deterministic 5-hex cch for a serialized body under an EXPLICIT seed, or
7
+ * null if the body carries no `cch=XXXXX` token. Used by `scripts/cch-
8
+ * calibrate.mjs` to test candidate seeds against a live capture without going
9
+ * through the version table.
10
+ */
11
+ export declare function cchWithSeed(bodyText: string, seed: bigint): string | null;
12
+ /**
13
+ * Deterministic 5-hex `cch` for a serialized request body, or null when:
14
+ * - `version` (major.minor.patch) has no known seed, or
15
+ * - the body carries no `cch=XXXXX` billing token to stamp.
16
+ * A null return means the caller should keep its random placeholder.
17
+ */
18
+ export declare function cchForBody(bodyText: string, version: string): string | null;
package/dist/cch.js ADDED
@@ -0,0 +1,139 @@
1
+ // Deterministic Claude Code request-integrity hash (`cch`) — dario#528.
2
+ //
3
+ // Claude Code stamps each /v1/messages request with a 5-hex `cch` token inside
4
+ // the `x-anthropic-billing-header` system block. It is NOT random: it is an
5
+ // xxHash64 over a canonical PROJECTION of the request body, masked to 20 bits.
6
+ // Reverse-engineered by lwsh123k (dario#528) and verified here against live
7
+ // captures on Claude Code 2.1.177:
8
+ //
9
+ // material = the serialized body, transformed by:
10
+ // 1. reset the `cch=XXXXX` token to `cch=00000`
11
+ // 2. blank the "model" VALUE (keep the key)
12
+ // 3. delete "fallbacks", "fallback_credit_token", "max_tokens"
13
+ // 4. re-serialize as compact JSON (JSON.stringify)
14
+ // cch = ( xxHash64(material, SEED[version]) & 0xFFFFF ) as zero-padded 5-hex
15
+ //
16
+ // The excluded fields are exactly the ones a routing proxy rewrites (model,
17
+ // fallbacks, max_tokens), so the value is STABLE across dario's rewrites — the
18
+ // upstream re-derives the same projection from whatever we send and recomputes
19
+ // the identical hash. dario therefore only has to hash its own final body.
20
+ //
21
+ // The seed rotates per Claude Code release and is keyed on major.minor.patch
22
+ // (the build-tag suffix, e.g. ".e2d" vs ".dd9", does NOT change it — verified
23
+ // against two captures with different suffixes, same 2.1.177 seed). An unknown
24
+ // version returns null so the caller falls back to a random value (the
25
+ // pre-dario#528 behavior) rather than emitting a confident-but-wrong hash that
26
+ // a validating server could single out.
27
+ /** Verified per-release seeds, keyed on `major.minor.patch`. */
28
+ export const CCH_SEEDS = {
29
+ '2.1.177': 0x4d659218e32a3268n,
30
+ };
31
+ const MASK = 0xfffffn;
32
+ const U64 = (1n << 64n) - 1n;
33
+ // xxHash64 primes.
34
+ const P1 = 0x9e3779b185ebca87n;
35
+ const P2 = 0xc2b2ae3d27d4eb4fn;
36
+ const P3 = 0x165667b19e3779f9n;
37
+ const P4 = 0x85ebca77c2b2ae63n;
38
+ const P5 = 0x27d4eb2f165667c5n;
39
+ const rotl = (x, r) => ((x << r) | (x >> (64n - r))) & U64;
40
+ function round(acc, input) {
41
+ acc = (acc + input * P2) & U64;
42
+ acc = rotl(acc, 31n);
43
+ return (acc * P1) & U64;
44
+ }
45
+ function mergeRound(acc, val) {
46
+ const r = round(0n, val);
47
+ acc = (acc ^ r) & U64;
48
+ return (acc * P1 + P4) & U64;
49
+ }
50
+ /** Canonical xxHash64 of `data` with a 64-bit `seed`. */
51
+ export function xxh64(data, seed) {
52
+ const len = data.length;
53
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
54
+ let h;
55
+ let i = 0;
56
+ if (len >= 32) {
57
+ let v1 = (seed + P1 + P2) & U64;
58
+ let v2 = (seed + P2) & U64;
59
+ let v3 = seed & U64;
60
+ let v4 = (seed - P1) & U64;
61
+ const limit = len - 32;
62
+ while (i <= limit) {
63
+ v1 = round(v1, dv.getBigUint64(i, true));
64
+ i += 8;
65
+ v2 = round(v2, dv.getBigUint64(i, true));
66
+ i += 8;
67
+ v3 = round(v3, dv.getBigUint64(i, true));
68
+ i += 8;
69
+ v4 = round(v4, dv.getBigUint64(i, true));
70
+ i += 8;
71
+ }
72
+ h = (rotl(v1, 1n) + rotl(v2, 7n) + rotl(v3, 12n) + rotl(v4, 18n)) & U64;
73
+ h = mergeRound(h, v1);
74
+ h = mergeRound(h, v2);
75
+ h = mergeRound(h, v3);
76
+ h = mergeRound(h, v4);
77
+ }
78
+ else {
79
+ h = (seed + P5) & U64;
80
+ }
81
+ h = (h + BigInt(len)) & U64;
82
+ while (i + 8 <= len) {
83
+ const k1 = round(0n, dv.getBigUint64(i, true));
84
+ h = (h ^ k1) & U64;
85
+ h = (rotl(h, 27n) * P1 + P4) & U64;
86
+ i += 8;
87
+ }
88
+ if (i + 4 <= len) {
89
+ h = (h ^ ((BigInt(dv.getUint32(i, true)) * P1) & U64)) & U64;
90
+ h = (rotl(h, 23n) * P2 + P3) & U64;
91
+ i += 4;
92
+ }
93
+ while (i < len) {
94
+ h = (h ^ ((BigInt(data[i]) * P5) & U64)) & U64;
95
+ h = (rotl(h, 11n) * P1) & U64;
96
+ i += 1;
97
+ }
98
+ h = (h ^ (h >> 33n)) & U64;
99
+ h = (h * P2) & U64;
100
+ h = (h ^ (h >> 29n)) & U64;
101
+ h = (h * P3) & U64;
102
+ h = (h ^ (h >> 32n)) & U64;
103
+ return h;
104
+ }
105
+ const CCH_RE = /cch=[0-9a-fA-F]{5}/;
106
+ /** Build the canonical cch pre-image bytes from a serialized request body. */
107
+ function cchMaterial(bodyText) {
108
+ const zeroed = bodyText.replace(CCH_RE, 'cch=00000'); // first occurrence only
109
+ const body = JSON.parse(zeroed);
110
+ body.model = '';
111
+ delete body.fallbacks;
112
+ delete body.fallback_credit_token;
113
+ delete body.max_tokens;
114
+ return new TextEncoder().encode(JSON.stringify(body));
115
+ }
116
+ /**
117
+ * Deterministic 5-hex cch for a serialized body under an EXPLICIT seed, or
118
+ * null if the body carries no `cch=XXXXX` token. Used by `scripts/cch-
119
+ * calibrate.mjs` to test candidate seeds against a live capture without going
120
+ * through the version table.
121
+ */
122
+ export function cchWithSeed(bodyText, seed) {
123
+ if (!CCH_RE.test(bodyText))
124
+ return null;
125
+ const h = xxh64(cchMaterial(bodyText), seed) & MASK;
126
+ return h.toString(16).padStart(5, '0');
127
+ }
128
+ /**
129
+ * Deterministic 5-hex `cch` for a serialized request body, or null when:
130
+ * - `version` (major.minor.patch) has no known seed, or
131
+ * - the body carries no `cch=XXXXX` billing token to stamp.
132
+ * A null return means the caller should keep its random placeholder.
133
+ */
134
+ export function cchForBody(bodyText, version) {
135
+ const seed = CCH_SEEDS[version];
136
+ if (seed === undefined)
137
+ return null;
138
+ return cchWithSeed(bodyText, seed);
139
+ }
@@ -282,7 +282,7 @@ export declare function _resetInstalledVersionProbeForTest(): void;
282
282
  */
283
283
  export declare const SUPPORTED_CC_RANGE: {
284
284
  readonly min: "1.0.0";
285
- readonly maxTested: "2.1.177";
285
+ readonly maxTested: "2.1.178";
286
286
  };
287
287
  /**
288
288
  * Compare two dotted-numeric version strings. Returns negative if `a<b`,
@@ -786,7 +786,7 @@ export function _resetInstalledVersionProbeForTest() {
786
786
  */
787
787
  export const SUPPORTED_CC_RANGE = {
788
788
  min: '1.0.0',
789
- maxTested: '2.1.177',
789
+ maxTested: '2.1.178',
790
790
  };
791
791
  /**
792
792
  * Compare two dotted-numeric version strings. Returns negative if `a<b`,
@@ -1,18 +1,25 @@
1
1
  /**
2
- * Overage-guard — halt the proxy on the first `representative-claim: overage`
3
- * response to prevent silent API-rate bleed.
2
+ * Overage-guard — halt the proxy the first time a response bills to anything
3
+ * other than the subscription pool, to prevent silent API-rate bleed.
4
4
  *
5
- * Subscribers should never see a single overage hit during normal
5
+ * Subscribers should never see a single non-subscription hit during normal
6
6
  * operation. One means something is wrong (wire-shape drift, classifier
7
7
  * change, account misconfig, billing-flip after a CC release) and
8
8
  * continuing to forward requests bleeds against per-token billing.
9
9
  *
10
10
  * The guard subscribes to the Analytics record stream — every completed
11
11
  * request emits a record carrying its `claim` (raw representative-claim
12
- * value). When `claim === 'overage'` lands, the guard transitions to a
13
- * halted state and emits a `'halt'` event. The HTTP request path checks
14
- * `isHalted()` on every incoming request and returns 503 with an
15
- * Anthropic-shaped error body when halted.
12
+ * value). When a non-subscription claim lands `overage`, `api`, or any
13
+ * new credit/SDK-bucket string (the 2026-06-15 Agent-SDK split) the guard
14
+ * transitions to a halted state and emits a `'halt'` event. The detection is
15
+ * an allow-list (`isNonSubscriptionBilling`): it fires on anything that is
16
+ * not a known subscription claim and not the `unknown` sentinel (which means
17
+ * "no rate-limit header" — a transient non-200/abort, not a billing flip).
18
+ * The HTTP request path checks `isHalted()` on every incoming request and
19
+ * returns 503 with an Anthropic-shaped error body when halted.
20
+ *
21
+ * The name predates the broadening (it caught only `overage` through v4.8.x,
22
+ * #288); the issue lineage + `dario_overage_guard` error type are kept stable.
16
23
  *
17
24
  * Resume paths:
18
25
  * - explicit: `dario resume` CLI → POST /admin/resume → `clear('manual')`
@@ -26,7 +33,7 @@
26
33
  * See dario#288.
27
34
  */
28
35
  import { EventEmitter } from 'node:events';
29
- import type { Analytics, RequestRecord } from './analytics.js';
36
+ import { type Analytics, type RequestRecord } from './analytics.js';
30
37
  export interface HaltState {
31
38
  since: number;
32
39
  cooldownUntil: number;
@@ -58,9 +65,11 @@ export declare class OverageGuard extends EventEmitter {
58
65
  private analyticsListener;
59
66
  constructor(opts: OverageGuardOptions);
60
67
  /**
61
- * Subscribe to an Analytics instance. Every record emitted with
62
- * `claim === 'overage'` triggers halt (when behavior === 'halt') or a
63
- * warn-only event (when behavior === 'warn').
68
+ * Subscribe to an Analytics instance. Every record whose claim is a
69
+ * non-subscription billing classification (`isNonSubscriptionBilling`
70
+ * `overage`, `api`, or a new credit/SDK bucket) triggers halt (when
71
+ * behavior === 'halt') or a warn-only event (when behavior === 'warn').
72
+ * Subscription claims and the `unknown` sentinel are ignored.
64
73
  *
65
74
  * Idempotent — calling attach() a second time replaces the listener
66
75
  * rather than stacking; useful for tests.
@@ -1,18 +1,25 @@
1
1
  /**
2
- * Overage-guard — halt the proxy on the first `representative-claim: overage`
3
- * response to prevent silent API-rate bleed.
2
+ * Overage-guard — halt the proxy the first time a response bills to anything
3
+ * other than the subscription pool, to prevent silent API-rate bleed.
4
4
  *
5
- * Subscribers should never see a single overage hit during normal
5
+ * Subscribers should never see a single non-subscription hit during normal
6
6
  * operation. One means something is wrong (wire-shape drift, classifier
7
7
  * change, account misconfig, billing-flip after a CC release) and
8
8
  * continuing to forward requests bleeds against per-token billing.
9
9
  *
10
10
  * The guard subscribes to the Analytics record stream — every completed
11
11
  * request emits a record carrying its `claim` (raw representative-claim
12
- * value). When `claim === 'overage'` lands, the guard transitions to a
13
- * halted state and emits a `'halt'` event. The HTTP request path checks
14
- * `isHalted()` on every incoming request and returns 503 with an
15
- * Anthropic-shaped error body when halted.
12
+ * value). When a non-subscription claim lands `overage`, `api`, or any
13
+ * new credit/SDK-bucket string (the 2026-06-15 Agent-SDK split) the guard
14
+ * transitions to a halted state and emits a `'halt'` event. The detection is
15
+ * an allow-list (`isNonSubscriptionBilling`): it fires on anything that is
16
+ * not a known subscription claim and not the `unknown` sentinel (which means
17
+ * "no rate-limit header" — a transient non-200/abort, not a billing flip).
18
+ * The HTTP request path checks `isHalted()` on every incoming request and
19
+ * returns 503 with an Anthropic-shaped error body when halted.
20
+ *
21
+ * The name predates the broadening (it caught only `overage` through v4.8.x,
22
+ * #288); the issue lineage + `dario_overage_guard` error type are kept stable.
16
23
  *
17
24
  * Resume paths:
18
25
  * - explicit: `dario resume` CLI → POST /admin/resume → `clear('manual')`
@@ -26,6 +33,7 @@
26
33
  * See dario#288.
27
34
  */
28
35
  import { EventEmitter } from 'node:events';
36
+ import { isNonSubscriptionBilling } from './analytics.js';
29
37
  export class OverageGuard extends EventEmitter {
30
38
  opts;
31
39
  halted = null;
@@ -39,9 +47,11 @@ export class OverageGuard extends EventEmitter {
39
47
  this.opts = opts;
40
48
  }
41
49
  /**
42
- * Subscribe to an Analytics instance. Every record emitted with
43
- * `claim === 'overage'` triggers halt (when behavior === 'halt') or a
44
- * warn-only event (when behavior === 'warn').
50
+ * Subscribe to an Analytics instance. Every record whose claim is a
51
+ * non-subscription billing classification (`isNonSubscriptionBilling`
52
+ * `overage`, `api`, or a new credit/SDK bucket) triggers halt (when
53
+ * behavior === 'halt') or a warn-only event (when behavior === 'warn').
54
+ * Subscription claims and the `unknown` sentinel are ignored.
45
55
  *
46
56
  * Idempotent — calling attach() a second time replaces the listener
47
57
  * rather than stacking; useful for tests.
@@ -57,7 +67,7 @@ export class OverageGuard extends EventEmitter {
57
67
  return;
58
68
  }
59
69
  this.analyticsListener = (r) => {
60
- if (r.claim === 'overage') {
70
+ if (isNonSubscriptionBilling(r.claim)) {
61
71
  this.onOverageDetected(r);
62
72
  }
63
73
  };
@@ -113,7 +123,7 @@ export class OverageGuard extends EventEmitter {
113
123
  if (this.opts.notifyOs && this.opts.notifier) {
114
124
  try {
115
125
  const title = this.opts.behavior === 'halt' ? 'dario halted' : 'dario warning';
116
- const msg = `Request classified as 'overage' (per-token billing)${this.opts.behavior === 'halt' ? '. Proxy halted. Run `dario resume` to continue.' : ''}`;
126
+ const msg = `Request classified as '${r.claim}' (non-subscription billing)${this.opts.behavior === 'halt' ? '. Proxy halted. Run `dario resume` to continue.' : ''}`;
117
127
  this.opts.notifier(title, msg);
118
128
  }
119
129
  catch {
@@ -180,10 +190,10 @@ export function buildHaltErrorBody(state) {
180
190
  error: {
181
191
  type: 'dario_overage_guard',
182
192
  message: `dario halted to prevent API-rate bleed. A request was classified ` +
183
- `as 'overage' (per-token billing) instead of your subscription pool. ` +
184
- `To resume: run \`dario resume\` in another terminal, or wait until ` +
185
- `${isoCooldown} for the cooldown to auto-clear. ` +
186
- `Details: github.com/askalf/dario/issues/288`,
193
+ `as '${state.request.claim}' (non-subscription billing) instead of ` +
194
+ `your subscription pool. To resume: run \`dario resume\` in another ` +
195
+ `terminal, or wait until ${isoCooldown} for the cooldown to ` +
196
+ `auto-clear. Details: github.com/askalf/dario/issues/288`,
187
197
  },
188
198
  };
189
199
  }
package/dist/proxy.d.ts CHANGED
@@ -321,11 +321,13 @@ interface ProxyOptions {
321
321
  */
322
322
  upstreamApiKey?: string;
323
323
  /**
324
- * Overage-guard — halt the proxy on the first response carrying
325
- * `representative-claim: overage`. Subscribers should never see a
326
- * single overage hit during normal operation; one means something
327
- * is wrong (wire-shape drift, classifier change, account misconfig)
328
- * and continuing to forward bleeds against per-token billing.
324
+ * Overage-guard — halt the proxy on the first response that bills to
325
+ * anything other than the subscription pool (`overage`, `api`, or a new
326
+ * credit/SDK bucket the 2026-06-15 split). Subscribers should never see
327
+ * a single non-subscription hit during normal operation; one means
328
+ * something is wrong (wire-shape drift, classifier change, account
329
+ * misconfig) and continuing to forward bleeds against per-token billing.
330
+ * Auto-disabled in upstream-API-key passthrough mode (see proxy setup).
329
331
  *
330
332
  * Default: enabled, halt behavior, 30-min cooldown, OS-notify on.
331
333
  * See dario#288.
package/dist/proxy.js CHANGED
@@ -9,6 +9,7 @@ import { arch, platform } from 'node:process';
9
9
  import { getAccessToken, getStatus } from './oauth.js';
10
10
  import { buildHealthResponse } from './health-response.js';
11
11
  import { buildCCRequest, applyCcPromptCaching, parseEffortSuffix, reverseMapResponse, createStreamingReverseMapper, orderHeadersForOutbound, CC_TEMPLATE } from './cc-template.js';
12
+ import { cchForBody } from './cch.js';
12
13
  import { describeTemplate, detectDrift, checkCCCompat } from './live-fingerprint.js';
13
14
  import { AccountPool, computeStickyKey, parseRateLimits, modelFamily, isInAuthCooldown, authCooldownMs } from './pool.js';
14
15
  import { Analytics, billingBucketFromClaim } from './analytics.js';
@@ -44,7 +45,16 @@ function computeBuildTag(userMessage, version) {
44
45
  const chars = [4, 7, 20].map(i => userMessage[i] || '0').join('');
45
46
  return createHash('sha256').update(`${BILLING_SEED}${chars}${version}`).digest('hex').slice(0, 3);
46
47
  }
47
- // Per-request cch: random 5-char hex value each request (Claude Code does the same).
48
+ // Per-request cch PLACEHOLDER. Claude Code's cch is a deterministic xxHash64
49
+ // over a projection of the request body (see src/cch.ts, dario#528). We can
50
+ // only compute the real value once the final body is assembled, so we seed the
51
+ // billing tag with a random 5-hex token here and overwrite it in place with the
52
+ // deterministic value at serialize time (the `cchForBody` call near the
53
+ // JSON.stringify of the outbound body). For Claude Code versions whose seed we
54
+ // haven't reverse-engineered, `cchForBody` returns null and this random value
55
+ // is what ships — same behavior as before dario#528, and a better tell than a
56
+ // confident-but-wrong deterministic hash. Operators can force the random path
57
+ // on every request with DARIO_CCH=random.
48
58
  function computeCch() {
49
59
  return randomBytes(3).toString('hex').slice(0, 5);
50
60
  }
@@ -807,8 +817,15 @@ export async function startProxy(opts = {}) {
807
817
  // defaults (enabled=true, behavior='halt', cooldown=30min, notifyOs=true)
808
818
  // so an opts-less proxy still gets protection. The notifier is wired
809
819
  // separately below once notify.ts is loaded.
820
+ //
821
+ // Disabled in upstream-API-key passthrough mode: there the operator
822
+ // explicitly opted into the per-token API pool, so responses legitimately
823
+ // bill as `api`. The guard now halts on ANY non-subscription claim (incl.
824
+ // `api`), so leaving it on would false-halt the whole passthrough path
825
+ // (e.g. the self-hosted compat workflow). The subscription-protection
826
+ // premise doesn't apply when there's no subscription to protect.
810
827
  const overageGuard = new OverageGuard({
811
- enabled: opts.overageGuardEnabled ?? true,
828
+ enabled: (opts.overageGuardEnabled ?? true) && !upstreamApiKey,
812
829
  behavior: opts.overageGuardBehavior ?? 'halt',
813
830
  cooldownMs: opts.overageGuardCooldownMs ?? 30 * 60 * 1000,
814
831
  notifyOs: opts.overageGuardNotifyOs ?? true,
@@ -820,10 +837,10 @@ export async function startProxy(opts = {}) {
820
837
  // a TUI attached. -v / --verbose is not required — this is loud by
821
838
  // design.
822
839
  overageGuard.on('halt', (state) => {
823
- console.error(`[dario] OVERAGE-GUARD HALTED: ${state.request.model} on account=${state.request.account} returned representative-claim=overage at ${new Date(state.request.timestamp).toISOString()}. Returning 503 to new requests until \`dario resume\` or cooldown expires (${new Date(state.cooldownUntil).toISOString()}). See dario#288.`);
840
+ console.error(`[dario] OVERAGE-GUARD HALTED: ${state.request.model} on account=${state.request.account} returned representative-claim=${state.request.claim} at ${new Date(state.request.timestamp).toISOString()}. Returning 503 to new requests until \`dario resume\` or cooldown expires (${new Date(state.cooldownUntil).toISOString()}). See dario#288.`);
824
841
  });
825
842
  overageGuard.on('warn', (state) => {
826
- console.error(`[dario] OVERAGE-GUARD WARN: ${state.request.model} on account=${state.request.account} returned representative-claim=overage at ${new Date(state.request.timestamp).toISOString()}. Behavior=warn — proxy continuing to forward; investigate before bill bleeds. See dario#288.`);
843
+ console.error(`[dario] OVERAGE-GUARD WARN: ${state.request.model} on account=${state.request.account} returned representative-claim=${state.request.claim} at ${new Date(state.request.timestamp).toISOString()}. Behavior=warn — proxy continuing to forward; investigate before bill bleeds. See dario#288.`);
827
844
  });
828
845
  overageGuard.on('resume', (info) => {
829
846
  console.error(`[dario] overage-guard resumed (${info.reason}). Normal request handling restored.`);
@@ -1749,7 +1766,21 @@ export async function startProxy(opts = {}) {
1749
1766
  oc.effort = bestSupportedEffort(supportedEfforts);
1750
1767
  }
1751
1768
  }
1752
- finalBody = Buffer.from(JSON.stringify(r));
1769
+ // dario#528: overwrite the random cch placeholder with the real
1770
+ // deterministic value for Claude Code versions whose seed we've
1771
+ // reverse-engineered. cchForBody hashes a projection of THIS final
1772
+ // body, so it must run after every mutation above. It returns null
1773
+ // for unknown versions (keep the random placeholder) or bodies with
1774
+ // no billing token. Only the template-replay path is stamped —
1775
+ // passthrough / count_tokens forward the client's body (and its own
1776
+ // cch) verbatim. Reversible kill-switch: DARIO_CCH=random.
1777
+ let outboundText = JSON.stringify(r);
1778
+ if (!passthrough && !isCountTokens && process.env.DARIO_CCH !== 'random') {
1779
+ const realCch = cchForBody(outboundText, cliVersion);
1780
+ if (realCch)
1781
+ outboundText = outboundText.replace(/cch=[0-9a-fA-F]{5}/, `cch=${realCch}`);
1782
+ }
1783
+ finalBody = Buffer.from(outboundText);
1753
1784
  }
1754
1785
  catch { /* not JSON, send as-is */ }
1755
1786
  }
@@ -25,7 +25,7 @@
25
25
  */
26
26
  import { fg, dim, brand, inverse, BOX, pad, truncate } from '../render.js';
27
27
  import { renderKvRow } from '../layout.js';
28
- import { billingBucketFromClaim } from '../../analytics.js';
28
+ import { billingBucketFromClaim, isNonSubscriptionBilling } from '../../analytics.js';
29
29
  const MAX_BUFFER = 5000;
30
30
  export const HitsTab = {
31
31
  id: 'hits',
@@ -143,7 +143,7 @@ export const HitsTab = {
143
143
  if (state.halt) {
144
144
  const since = formatTimestamp(state.halt.since);
145
145
  const cooldown = formatRemaining(state.halt.cooldownUntil - Date.now());
146
- const line1 = ` ${fg('red', '⚠ HALTED')} overage detected at ${since} on ${state.halt.request.model} (account=${state.halt.request.account})`;
146
+ const line1 = ` ${fg('red', '⚠ HALTED')} ${state.halt.request.claim} detected at ${since} on ${state.halt.request.model} (account=${state.halt.request.account})`;
147
147
  const line2 = ` ${dim('→ New /v1/messages requests return 503 until')} ${fg('cyan', 'R')} ${dim('here, or')} ${fg('cyan', 'dario resume')}${dim(' from any shell. Auto-resume in')} ${cooldown}${dim('.')}`;
148
148
  lines.push(line1);
149
149
  lines.push(line2);
@@ -158,9 +158,12 @@ export const HitsTab = {
158
158
  pad('st', colStatus)));
159
159
  for (let i = startIdx; i < endIdx; i++) {
160
160
  const r = newestFirst[i];
161
- const isOverage = r.claim === 'overage';
161
+ // Flag any non-subscription billing red — the same condition the
162
+ // overage-guard halts on (overage, api, or a credit/SDK bucket), not
163
+ // just literal `overage`. See isNonSubscriptionBilling (#288).
164
+ const isNonSub = isNonSubscriptionBilling(r.claim);
162
165
  const marker = i === state.selectedIdx ? fg('cyan', '▎')
163
- : isOverage ? fg('red', '!')
166
+ : isNonSub ? fg('red', '!')
164
167
  : ' ';
165
168
  const row = marker + ' ' +
166
169
  pad(formatTime(r.timestamp), colTime) +
@@ -169,12 +172,12 @@ export const HitsTab = {
169
172
  pad(formatTokens(r.outputTokens), colOut) +
170
173
  pad(formatLatency(r.latencyMs), colLat) +
171
174
  pad(formatStatus(r.status), colStatus);
172
- // Overage rows render in red even when unselected; selection still
173
- // wins via the inverse() wrapper so the user can drill into one.
175
+ // Non-subscription rows render in red even when unselected; selection
176
+ // still wins via the inverse() wrapper so the user can drill into one.
174
177
  let styled;
175
178
  if (i === state.selectedIdx)
176
179
  styled = inverse(truncate(row, w - 2));
177
- else if (isOverage)
180
+ else if (isNonSub)
178
181
  styled = fg('red', truncate(row, w - 2));
179
182
  else
180
183
  styled = truncate(row, w - 2);
@@ -115,7 +115,7 @@ export const StatusTab = {
115
115
  const remainingMs = Math.max(0, s.cooldownUntil - Date.now());
116
116
  const remaining = formatDuration(remainingMs);
117
117
  // Red banner header — this is the loud surface when halted
118
- lines.push(' ' + fg('red', '⚠ HALTED') + ' ' + dim(`overage detected ${formatAgo(s.since)} ago`));
118
+ lines.push(' ' + fg('red', '⚠ HALTED') + ' ' + dim(`${s.request.claim} detected ${formatAgo(s.since)} ago`));
119
119
  lines.push(' ' + renderKvRow('Request', `${s.request.model} ${dim('account=' + s.request.account)}`, w - 4));
120
120
  lines.push(' ' + renderKvRow('Cause', `representative-claim = ${fg('red', s.request.claim)}`, w - 4));
121
121
  lines.push(' ' + renderKvRow('Auto-resume in', remaining === '0s' ? fg('yellow', 'now (cooldown elapsed)') : remaining, w - 4));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "4.8.75",
3
+ "version": "4.8.77",
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": {
@@ -32,6 +32,7 @@
32
32
  "compat": "node test/compat.mjs",
33
33
  "lint:pkg": "node scripts/check-package-json.mjs",
34
34
  "drift:sdk": "node scripts/check-sdk-drift.mjs",
35
+ "cch:calibrate": "node scripts/cch-calibrate.mjs",
35
36
  "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')\""
36
37
  },
37
38
  "keywords": [