@askalf/dario 3.19.0 → 3.19.2
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 +7 -1
- package/dist/cc-template.js +7 -2
- package/dist/live-fingerprint.js +8 -0
- package/dist/proxy.js +87 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
<a href="https://www.npmjs.com/package/@askalf/dario"><img src="https://img.shields.io/npm/dm/@askalf/dario" alt="Downloads"></a>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
|
+
<p align="center">
|
|
15
|
+
<sub><strong>v4 is not a version bump.</strong> The router was the prerequisite. What comes next uses it as a substrate. — <a href="https://github.com/askalf/dario/discussions/categories/announcements">watch this space</a></sub>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
14
18
|
```bash
|
|
15
19
|
npm install -g @askalf/dario && dario proxy
|
|
16
20
|
```
|
|
@@ -88,7 +92,7 @@ Something broken? `dario doctor` prints a single aggregated health report — da
|
|
|
88
92
|
|
|
89
93
|
**You want the proxy layer off the wire entirely.** **Shim mode** (v3.12, hardened in v3.13) is an in-process `globalThis.fetch` patch injected via `NODE_OPTIONS=--require`. No HTTP hop, no port to bind, no `BASE_URL` to set. `dario shim -- claude --print "hi"` and CC thinks it's talking directly to `api.anthropic.com`. See [Shim mode](#shim-mode).
|
|
90
94
|
|
|
91
|
-
**You want to share capacity with a trusted group without surveilling each other.** The **sealed-sender overflow protocol** (v3.13) uses RSA blind signatures (Chaum 1983, implemented from scratch over Node's `crypto`) so members of a trust group can lend unused Claude capacity to each other with cryptographic unlinkability. A lender verifies "this is a valid group member" without learning *which* member. See [Sealed-sender overflow](#sealed-sender-overflow-protocol).
|
|
95
|
+
**You want to share capacity with a trusted group without surveilling each other.** The **sealed-sender overflow protocol** (v3.13) uses RSA blind signatures (Chaum 1983, implemented from scratch over Node's `crypto`) so members of a trust group can lend unused Claude capacity to each other with cryptographic unlinkability. A lender verifies "this is a valid group member" without learning *which* member. Dario ships the primitive; [mux](https://github.com/askalf/mux) is the dedicated product around it (group admin, key distribution, member workflow, borrower CLI). See [Sealed-sender overflow](#sealed-sender-overflow-protocol).
|
|
92
96
|
|
|
93
97
|
**You want to actually audit the thing.** ~7,600 lines of TypeScript across ~15 files. Zero runtime dependencies (`npm ls --production` confirms). Credentials at `~/.dario/` with `0600` permissions. `127.0.0.1`-only by default. Every release [SLSA-attested](https://www.npmjs.com/package/@askalf/dario) via GitHub Actions. Nothing phones home. Small enough to read in a weekend.
|
|
94
98
|
|
|
@@ -232,6 +236,8 @@ Trust-group members can lend each other Claude capacity with **cryptographic unl
|
|
|
232
236
|
|
|
233
237
|
Full feature-parity with `/v1/messages` (streaming, inside-request 429 failover, reverse tool mapping) for borrowed requests is intentionally a follow-up — the current release ships the cryptographic primitive and a working minimal endpoint; full integration layers on top.
|
|
234
238
|
|
|
239
|
+
**Dedicated product.** The sealed-sender protocol has a dedicated product around it: [mux](https://github.com/askalf/mux). mux carries the group admin tooling (key generation, member roster, batch signing), the member workflow (prepare / finalize / status), the borrower CLI, and the lender daemon as a coherent surface. It uses dario as its backend — a mux lender runs a dario pool and fronts it with `/v1/pool/borrow`. Dario keeps the primitive here for anyone who wants to embed it without running the full mux flow; for peer-to-peer capacity sharing as a product, use mux.
|
|
240
|
+
|
|
235
241
|
---
|
|
236
242
|
|
|
237
243
|
## Shim mode
|
package/dist/cc-template.js
CHANGED
|
@@ -232,7 +232,10 @@ const TOOL_MAP = {
|
|
|
232
232
|
execute_command: {
|
|
233
233
|
ccTool: 'Bash',
|
|
234
234
|
translateArgs: (a) => ({ command: a.command || a.cmd || '', ...(a.description ? { description: a.description } : {}) }),
|
|
235
|
-
|
|
235
|
+
// requires_approval is required by Cline's execute_command schema. Default
|
|
236
|
+
// to false — CC already gates Bash upstream through its own permission
|
|
237
|
+
// model, and the borrower controls their own auto-approval settings.
|
|
238
|
+
translateBack: (a) => ({ command: a.command ?? '', requires_approval: false, ...(a.description ? { description: a.description } : { description: a.command ?? '' }) }),
|
|
236
239
|
},
|
|
237
240
|
// Cursor
|
|
238
241
|
run_terminal_cmd: {
|
|
@@ -336,7 +339,9 @@ const TOOL_MAP = {
|
|
|
336
339
|
replace_in_file: {
|
|
337
340
|
ccTool: 'Edit',
|
|
338
341
|
translateArgs: (a) => ({ file_path: a.path || a.filePath || a.file_path || '', old_string: a.old_string || a.old || '', new_string: a.new_string || a.new || '' }),
|
|
339
|
-
|
|
342
|
+
// Cline's schema requires `diff`, not old_string/new_string — formatted as
|
|
343
|
+
// one SEARCH/REPLACE block (see replace_in_file.ts in cline/cline).
|
|
344
|
+
translateBack: (a) => ({ path: a.file_path ?? '', diff: `------- SEARCH\n${a.old_string ?? ''}\n=======\n${a.new_string ?? ''}\n+++++++ REPLACE` }),
|
|
340
345
|
},
|
|
341
346
|
// Roo Code
|
|
342
347
|
apply_diff: {
|
package/dist/live-fingerprint.js
CHANGED
|
@@ -513,6 +513,14 @@ export function extractTemplate(captured) {
|
|
|
513
513
|
const STATIC_HEADER_EXCLUDE = new Set([
|
|
514
514
|
// Auth — never replay across identities
|
|
515
515
|
'authorization',
|
|
516
|
+
// x-api-key is a CAPTURE ARTIFACT (dario#42). During capture we spawn CC
|
|
517
|
+
// with ANTHROPIC_API_KEY=sk-dario-fingerprint-capture pointing at a loopback
|
|
518
|
+
// MITM, so CC emits `x-api-key: sk-dario-fingerprint-capture`. Replaying
|
|
519
|
+
// that placeholder upstream alongside the real OAuth Bearer used to be a
|
|
520
|
+
// no-op because Anthropic ignored x-api-key when Authorization was present;
|
|
521
|
+
// as of 2026-04-17 some account tiers now 401 with "invalid x-api-key" when
|
|
522
|
+
// both are sent. Never capture it.
|
|
523
|
+
'x-api-key',
|
|
516
524
|
// Body-framing — computed per request
|
|
517
525
|
'content-type', 'content-length', 'transfer-encoding',
|
|
518
526
|
// Host / connection — managed by the HTTP stack
|
package/dist/proxy.js
CHANGED
|
@@ -493,8 +493,17 @@ export async function startProxy(opts = {}) {
|
|
|
493
493
|
// Excludes auth + body-framing + session-scoped keys by construction (see
|
|
494
494
|
// extractStaticHeaderValues in live-fingerprint.ts). No-op when the loaded
|
|
495
495
|
// template predates v2 or the bundled snapshot is in use.
|
|
496
|
+
//
|
|
497
|
+
// `x-api-key` is filtered defensively here too — pre-v3.19.2 captures still
|
|
498
|
+
// carry `x-api-key: sk-dario-fingerprint-capture` from the MITM spawn env.
|
|
499
|
+
// Replaying that placeholder alongside a real OAuth Bearer triggers a
|
|
500
|
+
// "invalid x-api-key" 401 on some account tiers as of 2026-04-17 (dario#42).
|
|
501
|
+
// The capture filter was updated in v3.19.2 to stop storing it, but the
|
|
502
|
+
// per-request skip below lets existing caches self-heal without a refresh.
|
|
496
503
|
if (!passthrough && CC_TEMPLATE.header_values) {
|
|
497
504
|
for (const [k, v] of Object.entries(CC_TEMPLATE.header_values)) {
|
|
505
|
+
if (k.toLowerCase() === 'x-api-key')
|
|
506
|
+
continue;
|
|
498
507
|
staticHeaders[k] = v;
|
|
499
508
|
}
|
|
500
509
|
}
|
|
@@ -507,6 +516,14 @@ export async function startProxy(opts = {}) {
|
|
|
507
516
|
// is the single-account slot. Reported by @boeingchoco in dario#36 — the
|
|
508
517
|
// retry loop was firing on every POST with hybrid-tools + OC.
|
|
509
518
|
const context1mUnavailable = new Set();
|
|
519
|
+
// Per-account cache of anthropic-beta flags the upstream has rejected as
|
|
520
|
+
// "Unexpected value(s)". The live-captured template lifts whatever CC emits
|
|
521
|
+
// verbatim — including flags gated to higher-tier accounts (e.g.
|
|
522
|
+
// `afk-mode-2026-01-31` is rejected on Max 5x as of 2026-04-17). On the
|
|
523
|
+
// first rejection we parse the flag out of the error message, strip it,
|
|
524
|
+
// retry once, and cache it so subsequent requests on the same account don't
|
|
525
|
+
// re-pay the 400 round-trip. Keyed by account alias (pool) or `__default__`.
|
|
526
|
+
const unavailableBetas = new Map();
|
|
510
527
|
const ACCOUNT_KEY_SINGLE = '__default__';
|
|
511
528
|
// Beta flag set — sourced from the live template when the capture recorded
|
|
512
529
|
// one (schema v2+), else falls back to the v2.1.104 bundled default. Same
|
|
@@ -514,7 +531,18 @@ export async function startProxy(opts = {}) {
|
|
|
514
531
|
// never diverge on the wire). Computed once per proxy because it's a
|
|
515
532
|
// function of the loaded template, not of the request.
|
|
516
533
|
const BETA_FALLBACK = 'claude-code-20250219,oauth-2025-04-20,context-1m-2025-08-07,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advisor-tool-2026-03-01,effort-2025-11-24';
|
|
517
|
-
|
|
534
|
+
let betaBase = CC_TEMPLATE.anthropic_beta || BETA_FALLBACK;
|
|
535
|
+
// `oauth-2025-04-20` is CC's OAuth-enablement beta flag. It is NOT present in
|
|
536
|
+
// the live-captured beta set because dario's fingerprint capture spawns CC
|
|
537
|
+
// with a placeholder `ANTHROPIC_API_KEY`, and CC only appends the oauth beta
|
|
538
|
+
// when it's actually using an OAuth bearer token. The proxy always uses
|
|
539
|
+
// OAuth upstream, so the flag is required — force it in if the captured
|
|
540
|
+
// template didn't carry it. As of 2026-04-17 some account tiers (Max 20x,
|
|
541
|
+
// Pro) return `authentication_error: invalid x-api-key` without this flag
|
|
542
|
+
// even when a valid Bearer is sent (dario#42).
|
|
543
|
+
if (!passthrough && !betaBase.split(',').includes('oauth-2025-04-20')) {
|
|
544
|
+
betaBase = betaBase ? `${betaBase},oauth-2025-04-20` : 'oauth-2025-04-20';
|
|
545
|
+
}
|
|
518
546
|
const betaWithoutContext1m = betaBase.split(',').filter((t) => t !== 'context-1m-2025-08-07').join(',');
|
|
519
547
|
// Rate governor — minimum 500ms between requests. Fast enough for agents,
|
|
520
548
|
// slow enough to not look like a scripted flood of identical traffic.
|
|
@@ -985,6 +1013,14 @@ export async function startProxy(opts = {}) {
|
|
|
985
1013
|
if (filtered)
|
|
986
1014
|
beta += ',' + filtered;
|
|
987
1015
|
}
|
|
1016
|
+
// Strip any beta flags the upstream has previously rejected on this
|
|
1017
|
+
// account so we don't re-pay the 400 round-trip (dario#42 afk-mode
|
|
1018
|
+
// fallout: captured templates carry tier-gated flags whose availability
|
|
1019
|
+
// we only learn at request time).
|
|
1020
|
+
const rejectedSet = unavailableBetas.get(acctKey);
|
|
1021
|
+
if (rejectedSet && rejectedSet.size > 0) {
|
|
1022
|
+
beta = beta.split(',').filter((t) => t.length > 0 && !rejectedSet.has(t)).join(',');
|
|
1023
|
+
}
|
|
988
1024
|
}
|
|
989
1025
|
// Rate governor — prevent inhuman request cadence
|
|
990
1026
|
const now = Date.now();
|
|
@@ -1086,7 +1122,56 @@ export async function startProxy(opts = {}) {
|
|
|
1086
1122
|
const isLongContextError = peekedBody.includes('long context')
|
|
1087
1123
|
|| peekedBody.includes('Extra usage is required')
|
|
1088
1124
|
|| peekedBody.includes('long_context');
|
|
1089
|
-
|
|
1125
|
+
// Detect "Unexpected value(s) `flag-name` for the `anthropic-beta` header"
|
|
1126
|
+
// — the upstream's way of saying this account tier doesn't have the
|
|
1127
|
+
// flag. Parse out the offending tokens (there can be more than one),
|
|
1128
|
+
// cache them, strip, and retry.
|
|
1129
|
+
const betaRejectedFlags = [];
|
|
1130
|
+
if (upstream.status === 400 && peekedBody.includes('anthropic-beta')) {
|
|
1131
|
+
const re = /Unexpected value\(s\)\s+((?:`[^`]+`(?:\s*,\s*)?)+)\s+for the `anthropic-beta` header/;
|
|
1132
|
+
const m = peekedBody.match(re);
|
|
1133
|
+
if (m) {
|
|
1134
|
+
for (const tok of m[1].matchAll(/`([^`]+)`/g))
|
|
1135
|
+
betaRejectedFlags.push(tok[1]);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
if (betaRejectedFlags.length > 0) {
|
|
1139
|
+
const acctKey = poolAccount?.alias ?? ACCOUNT_KEY_SINGLE;
|
|
1140
|
+
let set = unavailableBetas.get(acctKey);
|
|
1141
|
+
if (!set) {
|
|
1142
|
+
set = new Set();
|
|
1143
|
+
unavailableBetas.set(acctKey, set);
|
|
1144
|
+
}
|
|
1145
|
+
const newFlags = [];
|
|
1146
|
+
for (const f of betaRejectedFlags) {
|
|
1147
|
+
if (!set.has(f)) {
|
|
1148
|
+
set.add(f);
|
|
1149
|
+
newFlags.push(f);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
if (verbose && newFlags.length > 0)
|
|
1153
|
+
console.log(`[dario] #${requestCount} anthropic-beta rejected (${newFlags.join(',')}) — retrying without (cached for session)`);
|
|
1154
|
+
const reducedBeta = beta.split(',').filter((t) => t.length > 0 && !set.has(t)).join(',');
|
|
1155
|
+
const retryHeaders = { ...headers, 'anthropic-beta': reducedBeta };
|
|
1156
|
+
const retry = await fetch(targetBase, {
|
|
1157
|
+
method: req.method ?? 'POST',
|
|
1158
|
+
headers: passthrough ? retryHeaders : orderHeadersForOutbound(retryHeaders),
|
|
1159
|
+
body: finalBody ? new Uint8Array(finalBody) : undefined,
|
|
1160
|
+
signal: upstreamAbort.signal,
|
|
1161
|
+
});
|
|
1162
|
+
upstream = retry;
|
|
1163
|
+
peekedBody = null;
|
|
1164
|
+
if (pool && poolAccount) {
|
|
1165
|
+
const retrySnapshot = parseRateLimits(upstream.headers);
|
|
1166
|
+
if (upstream.status === 429) {
|
|
1167
|
+
pool.markRejected(poolAccount.alias, retrySnapshot);
|
|
1168
|
+
}
|
|
1169
|
+
else {
|
|
1170
|
+
pool.updateRateLimits(poolAccount.alias, retrySnapshot);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
else if (isLongContextError) {
|
|
1090
1175
|
// Cache the rejection so future requests on this account skip
|
|
1091
1176
|
// context-1m up front instead of re-paying the 400/429 round-trip.
|
|
1092
1177
|
const acctKey = poolAccount?.alias ?? ACCOUNT_KEY_SINGLE;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "3.19.
|
|
3
|
+
"version": "3.19.2",
|
|
4
4
|
"description": "A local LLM router. One endpoint, every provider — Claude subscriptions, OpenAI, OpenRouter, Groq, local LiteLLM, any OpenAI-compat endpoint — your tools don't need to change.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|