@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 +18 -17
- package/dist/analytics.d.ts +28 -0
- package/dist/analytics.js +37 -0
- package/dist/cch.d.ts +18 -0
- package/dist/cch.js +139 -0
- package/dist/live-fingerprint.d.ts +1 -1
- package/dist/live-fingerprint.js +1 -1
- package/dist/overage-guard.d.ts +20 -11
- package/dist/overage-guard.js +26 -16
- package/dist/proxy.d.ts +7 -5
- package/dist/proxy.js +36 -5
- package/dist/tui/tabs/hits.js +10 -7
- package/dist/tui/tabs/status.js +1 -1
- package/package.json +2 -1
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
|
|
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
|
|
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
|
-
##
|
|
371
|
+
## Own Your Stack
|
|
372
372
|
|
|
373
|
-
|
|
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
|
|
387
|
+
## Built by Thomas Sprayberry
|
|
388
388
|
|
|
389
|
-
|
|
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
|
-
|
|
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
|
-
|
|
393
|
+
---
|
|
394
|
+
Part of **[Own Your Stack](https://github.com/askalf)** — own your AI infrastructure instead of renting it. Built by Thomas Sprayberry.
|
package/dist/analytics.d.ts
CHANGED
|
@@ -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.
|
|
285
|
+
readonly maxTested: "2.1.178";
|
|
286
286
|
};
|
|
287
287
|
/**
|
|
288
288
|
* Compare two dotted-numeric version strings. Returns negative if `a<b`,
|
package/dist/live-fingerprint.js
CHANGED
|
@@ -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.
|
|
789
|
+
maxTested: '2.1.178',
|
|
790
790
|
};
|
|
791
791
|
/**
|
|
792
792
|
* Compare two dotted-numeric version strings. Returns negative if `a<b`,
|
package/dist/overage-guard.d.ts
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Overage-guard — halt the proxy
|
|
3
|
-
*
|
|
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
|
|
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
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
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
|
|
62
|
-
*
|
|
63
|
-
*
|
|
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.
|
package/dist/overage-guard.js
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Overage-guard — halt the proxy
|
|
3
|
-
*
|
|
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
|
|
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
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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
|
|
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 '
|
|
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 '
|
|
184
|
-
`To resume: run \`dario resume\` in another
|
|
185
|
-
|
|
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
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/dist/tui/tabs/hits.js
CHANGED
|
@@ -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')}
|
|
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
|
-
|
|
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
|
-
:
|
|
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
|
-
//
|
|
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 (
|
|
180
|
+
else if (isNonSub)
|
|
178
181
|
styled = fg('red', truncate(row, w - 2));
|
|
179
182
|
else
|
|
180
183
|
styled = truncate(row, w - 2);
|
package/dist/tui/tabs/status.js
CHANGED
|
@@ -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(
|
|
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.
|
|
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": [
|