@askalf/dario 6.14.3 → 6.15.0
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/proxy.d.ts +7 -0
- package/dist/proxy.js +100 -0
- package/docs/multi-account-pool.md +1 -1
- package/package.json +1 -1
package/dist/proxy.d.ts
CHANGED
|
@@ -126,6 +126,13 @@ export declare function parseProviderPrefix(model: string): {
|
|
|
126
126
|
* Exported for tests.
|
|
127
127
|
*/
|
|
128
128
|
export declare const FABLE_FALLBACK_CREDIT_BETA = "fallback-credit-2026-06-01";
|
|
129
|
+
/**
|
|
130
|
+
* Marks the request the family fallback sends again through dario's own front
|
|
131
|
+
* door after a mid-flight 429 (see familyFallbackOn429 in startProxy). A
|
|
132
|
+
* request carrying it is never sent again itself, so the fallback takes one
|
|
133
|
+
* hop. A client that sends it only turns that re-send off for its request.
|
|
134
|
+
*/
|
|
135
|
+
export declare const FAMILY_FALLBACK_LEG_HEADER = "x-dario-family-fallback-leg";
|
|
129
136
|
export declare const CONTEXT_1M_BETA = "context-1m-2025-08-07";
|
|
130
137
|
export declare const MID_CONVERSATION_SYSTEM_BETA = "mid-conversation-system-2026-04-07";
|
|
131
138
|
export declare const EFFORT_BETA = "effort-2025-11-24";
|
package/dist/proxy.js
CHANGED
|
@@ -497,6 +497,13 @@ function filterBillableBetas(betas) {
|
|
|
497
497
|
* Exported for tests.
|
|
498
498
|
*/
|
|
499
499
|
export const FABLE_FALLBACK_CREDIT_BETA = 'fallback-credit-2026-06-01';
|
|
500
|
+
/**
|
|
501
|
+
* Marks the request the family fallback sends again through dario's own front
|
|
502
|
+
* door after a mid-flight 429 (see familyFallbackOn429 in startProxy). A
|
|
503
|
+
* request carrying it is never sent again itself, so the fallback takes one
|
|
504
|
+
* hop. A client that sends it only turns that re-send off for its request.
|
|
505
|
+
*/
|
|
506
|
+
export const FAMILY_FALLBACK_LEG_HEADER = 'x-dario-family-fallback-leg';
|
|
500
507
|
export const CONTEXT_1M_BETA = 'context-1m-2025-08-07';
|
|
501
508
|
export const MID_CONVERSATION_SYSTEM_BETA = 'mid-conversation-system-2026-04-07';
|
|
502
509
|
export const EFFORT_BETA = 'effort-2025-11-24';
|
|
@@ -3646,6 +3653,95 @@ export async function startProxy(opts = {}) {
|
|
|
3646
3653
|
return null;
|
|
3647
3654
|
}
|
|
3648
3655
|
};
|
|
3656
|
+
/**
|
|
3657
|
+
* Family fallback, mid-flight (family-fallback.ts). The 429 that just
|
|
3658
|
+
* came back can be the one that parks the family: the seat's included
|
|
3659
|
+
* Fable credit ran out on this request, or dario restarted and has not
|
|
3660
|
+
* yet seen the credit spent (the pool's readings live in memory). The
|
|
3661
|
+
* fallback runs before selection, so this request missed it, and would
|
|
3662
|
+
* go on to the codex fallback or back to the client as a 429.
|
|
3663
|
+
*
|
|
3664
|
+
* Instead it is sent again, as the client wrote it, through dario's own
|
|
3665
|
+
* front door. That request meets the family parked, so the fallback
|
|
3666
|
+
* re-points it and the whole Claude path builds it for the target model,
|
|
3667
|
+
* with that model's template, betas and effort. Rewriting the model on
|
|
3668
|
+
* the body already built for Fable would send Opus a Fable request.
|
|
3669
|
+
*
|
|
3670
|
+
* One hop: the re-sent request carries FAMILY_FALLBACK_LEG_HEADER and is
|
|
3671
|
+
* never sent again itself. Skipped wherever the fallback before selection
|
|
3672
|
+
* is skipped, for a continuation leg, and for a request already re-pointed.
|
|
3673
|
+
* The queue slot is released first, as a continuation does, so a
|
|
3674
|
+
* one-slot proxy does not wait on itself. True when the client was answered.
|
|
3675
|
+
*/
|
|
3676
|
+
const familyFallbackOn429 = async (rejected) => {
|
|
3677
|
+
if (familyFallback.size === 0 || upstreamApiKey || pinnedAccount || modelOverride || fastModelOverride)
|
|
3678
|
+
return false;
|
|
3679
|
+
if (isResponses || isCountTokens || requestDepth > 0 || req.headers[FAMILY_FALLBACK_LEG_HEADER] !== undefined)
|
|
3680
|
+
return false;
|
|
3681
|
+
if (res.headersSent || res.getHeader('x-dario-family-fallback') !== undefined)
|
|
3682
|
+
return false;
|
|
3683
|
+
const fromModel = parseClientBody()?.model;
|
|
3684
|
+
if (typeof fromModel !== 'string')
|
|
3685
|
+
return false;
|
|
3686
|
+
const now = Date.now();
|
|
3687
|
+
const toModel = familyFallbackModel(familyFallback, fromModel, {
|
|
3688
|
+
parked: (f) => pool.familyRateLimited(f, now),
|
|
3689
|
+
servable: (f) => pool.canServe(f, now),
|
|
3690
|
+
});
|
|
3691
|
+
if (!toModel)
|
|
3692
|
+
return false;
|
|
3693
|
+
void rejected.body?.cancel().catch(() => { });
|
|
3694
|
+
console.log(`[dario] #${requestCount} the 429 parked every seat for ${fromModel} -> sent again as ${toModel} (family fallback)`);
|
|
3695
|
+
releaseQueueSlot();
|
|
3696
|
+
const abort = new AbortController();
|
|
3697
|
+
const onClose = () => { if (!res.writableEnded)
|
|
3698
|
+
abort.abort(); };
|
|
3699
|
+
res.on('close', onClose);
|
|
3700
|
+
const timer = setTimeout(() => abort.abort(), upstreamTimeoutMs);
|
|
3701
|
+
try {
|
|
3702
|
+
const leg = await fetch(`${loopbackBase}${isOpenAI ? '/v1/chat/completions' : '/v1/messages'}`, {
|
|
3703
|
+
method: 'POST',
|
|
3704
|
+
headers: { 'content-type': 'application/json', [FAMILY_FALLBACK_LEG_HEADER]: '1', ...loopbackHeaders() },
|
|
3705
|
+
body: new Uint8Array(clientBodyBytes),
|
|
3706
|
+
signal: abort.signal,
|
|
3707
|
+
});
|
|
3708
|
+
const responseHeaders = { 'Access-Control-Allow-Origin': corsOrigin, ...SECURITY_HEADERS };
|
|
3709
|
+
for (const [key, value] of leg.headers.entries()) {
|
|
3710
|
+
if (key === 'content-type' || key === 'retry-after' || key === 'request-id' || key.startsWith('anthropic-ratelimit') || key.startsWith('x-dario-')) {
|
|
3711
|
+
responseHeaders[key] = value;
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
responseHeaders['x-dario-family-fallback'] = fromModel;
|
|
3715
|
+
res.writeHead(leg.status, responseHeaders);
|
|
3716
|
+
if (leg.body) {
|
|
3717
|
+
const reader = leg.body.getReader();
|
|
3718
|
+
while (true) {
|
|
3719
|
+
const { done, value } = await reader.read();
|
|
3720
|
+
if (done)
|
|
3721
|
+
break;
|
|
3722
|
+
if (!res.write(value))
|
|
3723
|
+
await new Promise((resolve) => { res.once('drain', resolve); res.once('close', resolve); });
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
if (!res.writableEnded)
|
|
3727
|
+
res.end();
|
|
3728
|
+
}
|
|
3729
|
+
catch (err) {
|
|
3730
|
+
console.error(`[dario] #${requestCount} family fallback: the re-sent request failed: ${sanitizeError(err)}`);
|
|
3731
|
+
if (!res.headersSent) {
|
|
3732
|
+
res.writeHead(502, JSON_HEADERS);
|
|
3733
|
+
res.end(JSON.stringify({ type: 'error', error: { type: 'api_error', message: 'family fallback: the re-sent request failed' } }));
|
|
3734
|
+
}
|
|
3735
|
+
else if (!res.writableEnded) {
|
|
3736
|
+
res.end();
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
finally {
|
|
3740
|
+
clearTimeout(timer);
|
|
3741
|
+
res.off('close', onClose);
|
|
3742
|
+
}
|
|
3743
|
+
return true;
|
|
3744
|
+
};
|
|
3649
3745
|
/**
|
|
3650
3746
|
* Where a Claude stream that died mid-way resumes: the codex half of the
|
|
3651
3747
|
* `--pool-fallback` chain, exactly the entry a mid-flight 429 would use.
|
|
@@ -5424,6 +5520,8 @@ export async function startProxy(opts = {}) {
|
|
|
5424
5520
|
// The Claude half is out of quota for this request AND for the next
|
|
5425
5521
|
// one — cool it here, honouring the upstream `retry-after` when it
|
|
5426
5522
|
// sent one, so the following request does not re-walk the chain.
|
|
5523
|
+
if (await familyFallbackOn429(upstream))
|
|
5524
|
+
return;
|
|
5427
5525
|
attemptedProviders.add('claude');
|
|
5428
5526
|
providerCooldowns.note('claude', parseRetryAfterMs(upstream.headers.get('retry-after')));
|
|
5429
5527
|
if (!pinnedAccount && await attemptPoolFallbackOn429(req, res, body, isOpenAI, attemptedProviders, { queueMs, arrivedAt })) {
|
|
@@ -5552,6 +5650,8 @@ export async function startProxy(opts = {}) {
|
|
|
5552
5650
|
// into body. See attemptPoolFallbackOn429's doc comment for why this
|
|
5553
5651
|
// is one shared function rather than an inline copy.
|
|
5554
5652
|
// Same bookkeeping as the other mid-flight site — see there.
|
|
5653
|
+
if (await familyFallbackOn429(upstream))
|
|
5654
|
+
return;
|
|
5555
5655
|
attemptedProviders.add('claude');
|
|
5556
5656
|
providerCooldowns.note('claude', parseRetryAfterMs(upstream.headers.get('retry-after')));
|
|
5557
5657
|
if (!pinnedAccount && await attemptPoolFallbackOn429(req, res, body, isOpenAI, attemptedProviders, { queueMs, arrivedAt })) {
|
|
@@ -96,7 +96,7 @@ dario proxy --family-fallback=fable=claude-opus-5-5
|
|
|
96
96
|
- **Not for.** Upstream API-key mode (no pool), a server-wide `--model`/`--fast-model` (which replaces the name anyway) and a seat-pinned request (`x-dario-account`), which asked for that seat as it is.
|
|
97
97
|
- **Shape.** The source is a family (`opus`, `sonnet`, `haiku`, `fable`); the target a `claude-*` model of another family. One step, never chained. Bad entries are dropped with a warning at startup and in `dario doctor`. A `[1m]` tag on the request carries over to a target without one.
|
|
98
98
|
|
|
99
|
-
The first Fable request after the credit runs out still goes upstream and comes back `429`: that answer is what parks Fable on the seat. Every Fable request after it is switched, including after the seat has served Opus since.
|
|
99
|
+
The first Fable request after the credit runs out still goes upstream and comes back `429`: that answer is what parks Fable on the seat. dario does not hand that `429` to the client: it sends the same request again, as the client wrote it, through its own front door, where the fallback now applies and the request is built for the target model (its own template, betas and effort). So is the first Fable request after a restart, which is when dario has not seen the credit spent yet. That re-sent request is never re-sent itself, and it does not wait on its own queue slot. Every Fable request after it is switched before it is sent, including after the seat has served Opus since.
|
|
100
100
|
|
|
101
101
|
## In-flight 429 failover
|
|
102
102
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.15.0",
|
|
4
4
|
"description": "Use your Claude and ChatGPT subscriptions in Cursor, Cline, Aider, Claude Code and the Agent SDK — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint: either plan answers either wire shape, with automatic failover when one hits its limit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|