@askalf/dario 3.19.1 → 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 +4 -0
- 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
|
```
|
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": {
|