@askalf/dario 6.0.2 → 6.0.4
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/dist/claude-model.js +35 -16
- package/dist/proxy.js +55 -17
- package/package.json +1 -1
package/dist/claude-model.js
CHANGED
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
* `--model-alias` values, pinned aliases (`opus48`) and catalog family
|
|
20
20
|
* shorthands (`opus`, `sonnet1m`) map to a canonical id. The caller
|
|
21
21
|
* supplies that resolver so classification and forwarding cannot
|
|
22
|
-
* disagree.
|
|
22
|
+
* disagree. The prefix rule applies to the alias TARGET too, since an
|
|
23
|
+
* operator alias is written the way a request model is written — see
|
|
24
|
+
* stripClaudePrefix.
|
|
23
25
|
* 2. VALIDATE the resolved id against the catalog base set. A `claude-`
|
|
24
26
|
* prefix on its own proves nothing — `claude-sonnet-6` has the prefix and
|
|
25
27
|
* does not exist — so the id (sans `[1m]`) must be a base the pool can
|
|
@@ -37,6 +39,23 @@
|
|
|
37
39
|
import { BAKED_BASE_MODELS, resolveAliasAgainst } from './model-catalog.js';
|
|
38
40
|
/** Provider prefixes that force the Claude path (mirrors proxy.ts's PROVIDER_PREFIXES). */
|
|
39
41
|
const CLAUDE_PREFIXES = new Set(['claude', 'anthropic']);
|
|
42
|
+
/**
|
|
43
|
+
* Strip a Claude-side provider prefix, or refuse the name outright.
|
|
44
|
+
*
|
|
45
|
+
* Null on any prefix that isn't `claude:`/`anthropic:` — a recognized
|
|
46
|
+
* non-Claude prefix is a definite NO, not a fall-through to the name test
|
|
47
|
+
* (`openai:claude-sonnet-5` is the operator pointing somewhere else on
|
|
48
|
+
* purpose), and an unrecognized one names a model this pool has no claim on.
|
|
49
|
+
* `model` is expected already trimmed + lowercased.
|
|
50
|
+
*/
|
|
51
|
+
function stripClaudePrefix(model) {
|
|
52
|
+
const idx = model.indexOf(':');
|
|
53
|
+
if (idx <= 0)
|
|
54
|
+
return model || null;
|
|
55
|
+
if (!CLAUDE_PREFIXES.has(model.slice(0, idx)))
|
|
56
|
+
return null;
|
|
57
|
+
return model.slice(idx + 1) || null;
|
|
58
|
+
}
|
|
40
59
|
/**
|
|
41
60
|
* The canonical id the Claude pool would serve `model` as, or null when it
|
|
42
61
|
* cannot serve it. `bases` is the catalog base set — `getCachedBases()` at a
|
|
@@ -45,22 +64,22 @@ const CLAUDE_PREFIXES = new Set(['claude', 'anthropic']);
|
|
|
45
64
|
* (operator aliases + pinned aliases) where one exists.
|
|
46
65
|
*/
|
|
47
66
|
export function resolveClaudeServable(model, bases = BAKED_BASE_MODELS, resolve) {
|
|
48
|
-
|
|
49
|
-
if (
|
|
67
|
+
const entry = stripClaudePrefix(model.trim().toLowerCase());
|
|
68
|
+
if (entry === null)
|
|
69
|
+
return null;
|
|
70
|
+
// The alias TARGET may carry a prefix of its own — operator aliases are
|
|
71
|
+
// declared the way a request model is written, and `--model-alias=backup=
|
|
72
|
+
// claude:opus` is the natural way to say it (dario#1151). The request path
|
|
73
|
+
// parses that prefix AFTER alias resolution; without the same pass here the
|
|
74
|
+
// target reached the base check as the literal `claude:opus`, matched
|
|
75
|
+
// nothing, and a perfectly valid fallback was skipped.
|
|
76
|
+
const target = stripClaudePrefix((resolve ? resolve(entry) : (resolveAliasAgainst(entry, bases) ?? entry)).trim().toLowerCase());
|
|
77
|
+
if (target === null)
|
|
50
78
|
return null;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// the name test below — `openai:claude-sonnet-5` is the operator pointing
|
|
56
|
-
// somewhere else on purpose.
|
|
57
|
-
if (!CLAUDE_PREFIXES.has(prefix))
|
|
58
|
-
return null;
|
|
59
|
-
m = m.slice(idx + 1);
|
|
60
|
-
if (!m)
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
const resolved = (resolve ? resolve(m) : (resolveAliasAgainst(m, bases) ?? m)).trim().toLowerCase();
|
|
79
|
+
// A target that arrived prefixed still holds a shorthand (`claude:opus` →
|
|
80
|
+
// `opus`), so the catalog pass runs on it. Idempotent for a target already
|
|
81
|
+
// canonical: resolveAliasAgainst only answers for family shorthands.
|
|
82
|
+
const resolved = resolveAliasAgainst(target, bases) ?? target;
|
|
64
83
|
const base = resolved.endsWith('[1m]') ? resolved.slice(0, -4) : resolved;
|
|
65
84
|
return bases.some((b) => b.toLowerCase() === base) ? resolved : null;
|
|
66
85
|
}
|
package/dist/proxy.js
CHANGED
|
@@ -1863,6 +1863,42 @@ export async function startProxy(opts = {}) {
|
|
|
1863
1863
|
const hasNextOption = openaiBackend !== null && shape === 'openai';
|
|
1864
1864
|
return await forwardToCodex(req, res, fallbackBody, creds, corsOrigin, { ...SECURITY_HEADERS, 'x-dario-pool-fallback': fallbackModel }, upstreamTimeoutMs, verbose, shape, fetch, hasNextOption);
|
|
1865
1865
|
};
|
|
1866
|
+
/**
|
|
1867
|
+
* Mid-flight 429, no peer account left to retry — the pool is genuinely
|
|
1868
|
+
* exhausted mid-request, not just at selection. Tries every fallback in
|
|
1869
|
+
* order (subscription, then api-key backend) and returns true once one has
|
|
1870
|
+
* written the response; false means nothing could serve it and the caller
|
|
1871
|
+
* writes its own honest 429.
|
|
1872
|
+
*
|
|
1873
|
+
* A SINGLE function for BOTH mid-flight 429 sites in the request handler
|
|
1874
|
+
* (the non-passthrough recovery-loop path and the general/passthrough
|
|
1875
|
+
* path), by design. Those two sites used to carry independent, hand-copied
|
|
1876
|
+
* versions of this logic, and v6.0.0 wired the fallback into only ONE of
|
|
1877
|
+
* them. The other kept returning the raw upstream 429 — and that unwired
|
|
1878
|
+
* site is the one reached by every non-passthrough request, i.e. everything
|
|
1879
|
+
* except a literal Claude Code CLI session, forge's SDK Engine included.
|
|
1880
|
+
* That is what left the whole fleet unable to fail over during the live
|
|
1881
|
+
* outage on 2026-08-30 while `dario doctor` reported failover armed.
|
|
1882
|
+
*
|
|
1883
|
+
* One function reachable from both call sites is what makes that class of
|
|
1884
|
+
* bug structurally impossible to reintroduce: a future third call site gets
|
|
1885
|
+
* this by construction, not by remembering to copy six lines correctly.
|
|
1886
|
+
*/
|
|
1887
|
+
const attemptPoolFallbackOn429 = async (req, res, body, isOpenAI) => {
|
|
1888
|
+
if (await tryCodexPoolFallback(req, res, body, poolFallbackModels, isOpenAI ? 'openai' : 'anthropic', 'pool exhausted mid-flight (429, no peer)')) {
|
|
1889
|
+
return true;
|
|
1890
|
+
}
|
|
1891
|
+
if (isOpenAI && poolFallbackModel && openaiBackend) {
|
|
1892
|
+
const fallbackBody = buildPoolFallbackBody(body, poolFallbackModel);
|
|
1893
|
+
if (fallbackBody) {
|
|
1894
|
+
console.log(`[dario] #${requestCount} pool exhausted mid-flight (429, no peer) → ${openaiBackend.name} as ${poolFallbackModel}`);
|
|
1895
|
+
requestCount++;
|
|
1896
|
+
await forwardToOpenAI(req, res, fallbackBody, openaiBackend, corsOrigin, { ...SECURITY_HEADERS, 'x-dario-pool-fallback': poolFallbackModel }, upstreamTimeoutMs, verbose);
|
|
1897
|
+
return true;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
return false;
|
|
1901
|
+
};
|
|
1866
1902
|
const server = createServer(async (req, res) => {
|
|
1867
1903
|
if (req.method === 'OPTIONS') {
|
|
1868
1904
|
res.writeHead(204, CORS_HEADERS);
|
|
@@ -3590,6 +3626,19 @@ export async function startProxy(opts = {}) {
|
|
|
3590
3626
|
continue dispatchLoop;
|
|
3591
3627
|
}
|
|
3592
3628
|
}
|
|
3629
|
+
// Pool-exhausted fallback, mid-flight: no peer left to fail over
|
|
3630
|
+
// to. THE BUG (live incident, 2026-08-30) and its independent
|
|
3631
|
+
// rediscovery: this branch — reached by every non-passthrough
|
|
3632
|
+
// request, i.e. everything except a literal Claude Code CLI session
|
|
3633
|
+
// — never called the fallback wired into the OTHER, structurally
|
|
3634
|
+
// near-identical 429 site below. #1153 fixed it here with its own
|
|
3635
|
+
// inline copy; that copy is collapsed into the single shared
|
|
3636
|
+
// attemptPoolFallbackOn429 below, so the file goes back to having
|
|
3637
|
+
// exactly ONE copy of this logic instead of two near-identical ones
|
|
3638
|
+
// — which is the exact shape of bug this incident already was.
|
|
3639
|
+
if (await attemptPoolFallbackOn429(req, res, body, isOpenAI)) {
|
|
3640
|
+
return;
|
|
3641
|
+
}
|
|
3593
3642
|
const enriched = enrich429(peekedBody, upstream.headers);
|
|
3594
3643
|
const responseHeaders = {
|
|
3595
3644
|
'Content-Type': 'application/json',
|
|
@@ -3680,25 +3729,14 @@ export async function startProxy(opts = {}) {
|
|
|
3680
3729
|
continue dispatchLoop;
|
|
3681
3730
|
}
|
|
3682
3731
|
}
|
|
3683
|
-
// Pool-exhausted fallback: no peer left to fail over to.
|
|
3684
|
-
//
|
|
3685
|
-
//
|
|
3686
|
-
//
|
|
3687
|
-
//
|
|
3688
|
-
|
|
3689
|
-
// selection-time path.
|
|
3690
|
-
if (await tryCodexPoolFallback(req, res, body, poolFallbackModels, isOpenAI ? 'openai' : 'anthropic', 'pool exhausted mid-flight (429, no peer)')) {
|
|
3732
|
+
// Pool-exhausted fallback: no peer left to fail over to. `body`
|
|
3733
|
+
// still holds the client's own bytes as received — the Anthropic
|
|
3734
|
+
// translation (OpenAI-shape clients) went into finalBody, never back
|
|
3735
|
+
// into body. See attemptPoolFallbackOn429's doc comment for why this
|
|
3736
|
+
// is one shared function rather than an inline copy.
|
|
3737
|
+
if (await attemptPoolFallbackOn429(req, res, body, isOpenAI)) {
|
|
3691
3738
|
return;
|
|
3692
3739
|
}
|
|
3693
|
-
if (isOpenAI && poolFallbackModel && openaiBackend) {
|
|
3694
|
-
const fallbackBody = buildPoolFallbackBody(body, poolFallbackModel);
|
|
3695
|
-
if (fallbackBody) {
|
|
3696
|
-
console.log(`[dario] #${requestCount} pool exhausted mid-flight (429, no peer) — /v1/chat/completions → ${openaiBackend.name} as ${poolFallbackModel}`);
|
|
3697
|
-
requestCount++;
|
|
3698
|
-
await forwardToOpenAI(req, res, fallbackBody, openaiBackend, corsOrigin, { ...SECURITY_HEADERS, 'x-dario-pool-fallback': poolFallbackModel }, upstreamTimeoutMs, verbose);
|
|
3699
|
-
return;
|
|
3700
|
-
}
|
|
3701
|
-
}
|
|
3702
3740
|
const errBody = await upstream.text().catch(() => '');
|
|
3703
3741
|
const enriched = enrich429(errBody, upstream.headers);
|
|
3704
3742
|
const responseHeaders = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.4",
|
|
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": {
|