@askalf/dario 6.15.0 → 6.17.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/README.md +19 -21
- package/dist/analytics-ui.html +835 -0
- package/dist/analytics.d.ts +26 -1
- package/dist/analytics.js +51 -3
- package/dist/body-identity.d.ts +25 -0
- package/dist/body-identity.js +68 -0
- package/dist/cc-template.js +3 -14
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +44 -7
- package/dist/codex-backend.js +12 -4
- package/dist/config-file.d.ts +1 -1
- package/dist/config-file.js +2 -2
- package/dist/credits-rung.d.ts +72 -0
- package/dist/credits-rung.js +133 -0
- package/dist/doctor-core.d.ts +2 -0
- package/dist/doctor-core.js +47 -10
- package/dist/donuts.d.ts +18 -17
- package/dist/donuts.js +46 -54
- package/dist/ledger.d.ts +20 -0
- package/dist/ledger.js +40 -1
- package/dist/metrics.js +2 -1
- package/dist/midstream.js +6 -2
- package/dist/openai-backend.js +8 -3
- package/dist/pool.d.ts +18 -1
- package/dist/pool.js +31 -10
- package/dist/proxy.d.ts +14 -1
- package/dist/proxy.js +351 -105
- package/dist/relaunch-signals.d.ts +35 -0
- package/dist/relaunch-signals.js +41 -0
- package/dist/stream-drain.d.ts +4 -3
- package/dist/stream-drain.js +4 -3
- package/dist/tui/tabs/config.js +2 -2
- package/dist/upstream-deadline.d.ts +25 -0
- package/dist/upstream-deadline.js +76 -0
- package/docs/analytics.md +10 -8
- package/docs/api-equivalent-spend.md +9 -2
- package/docs/commands.md +1 -1
- package/docs/configuration.md +1 -0
- package/docs/faq.md +5 -0
- package/docs/multi-account-pool.md +3 -1
- package/docs/pool-keys-and-analytics.md +2 -0
- package/docs/two-plans.md +2 -0
- package/docs/who-its-for.md +2 -2
- package/package.json +8 -4
package/dist/analytics.d.ts
CHANGED
|
@@ -92,11 +92,17 @@ export declare function continuationStats(records: readonly RequestRecord[]): Co
|
|
|
92
92
|
* - `api` — pure API key billing, no subscription involved
|
|
93
93
|
* - `unknown` — header absent or unparseable (non-200 responses, stream aborts)
|
|
94
94
|
*
|
|
95
|
+
* A sixth, `credits`, is stamped by the proxy rather than read from a header:
|
|
96
|
+
* a request the credits rung of the failover chain served on the Console key
|
|
97
|
+
* in DARIO_CREDITS_API_KEY (credits-rung.ts). Anthropic sends no claim header
|
|
98
|
+
* on an API key request, and "the operator's own credits rung" is a different
|
|
99
|
+
* fact from "an unexpected API bill", so it gets its own bucket.
|
|
100
|
+
*
|
|
95
101
|
* Exposed in `/analytics` summaries and in verbose per-request logs so
|
|
96
102
|
* users can see at a glance which bucket their traffic is actually hitting.
|
|
97
103
|
* See #34 for background.
|
|
98
104
|
*/
|
|
99
|
-
export type BillingBucket = 'subscription' | 'subscription_fallback' | 'extra_usage' | 'api' | 'unknown';
|
|
105
|
+
export type BillingBucket = 'subscription' | 'subscription_fallback' | 'extra_usage' | 'api' | 'credits' | 'unknown';
|
|
100
106
|
/**
|
|
101
107
|
* Map the raw `representative-claim` header value to a human-friendly
|
|
102
108
|
* billing bucket. Pure function; no state; safe to call from any context.
|
|
@@ -112,6 +118,14 @@ export declare function billingBucketFromClaim(claim: string | null | undefined)
|
|
|
112
118
|
export declare const SUBSCRIPTION_CLAIMS: ReadonlySet<string>;
|
|
113
119
|
/** The claim the proxy stamps on codex-engine requests (see above). */
|
|
114
120
|
export declare const CODEX_CLAIM = "chatgpt_subscription";
|
|
121
|
+
/**
|
|
122
|
+
* The claim the proxy stamps on a request the credits rung served
|
|
123
|
+
* (credits-rung.ts). Not a subscription claim: the request was billed at API
|
|
124
|
+
* rates against the monthly credit balance, so it lands in its own bucket and
|
|
125
|
+
* the ledger files it as metered. The overage guard leaves it alone, because
|
|
126
|
+
* the rung is a chain entry the operator wrote, not a billing surprise.
|
|
127
|
+
*/
|
|
128
|
+
export declare const CREDITS_CLAIM = "anthropic_credits";
|
|
115
129
|
/**
|
|
116
130
|
* One-line per-request usage summary for verbose (-v / -vv) logs.
|
|
117
131
|
*
|
|
@@ -282,6 +296,12 @@ export declare function costOfTokens(model: string, atMs: number, t: {
|
|
|
282
296
|
cacheReadTokens: number;
|
|
283
297
|
cacheCreateTokens: number;
|
|
284
298
|
}): number;
|
|
299
|
+
/**
|
|
300
|
+
* Request counts per five-minute bin over the window, oldest bin first; the
|
|
301
|
+
* newest bin is the one `now` falls in. Records outside the window are left
|
|
302
|
+
* out, so the bins sum to the window's request count.
|
|
303
|
+
*/
|
|
304
|
+
export declare function binByFiveMinutes(records: RequestRecord[], now: number, windowMinutes: number): number[];
|
|
285
305
|
export declare class Analytics extends EventEmitter {
|
|
286
306
|
private records;
|
|
287
307
|
private maxRecords;
|
|
@@ -408,6 +428,8 @@ export interface AnalyticsSummary {
|
|
|
408
428
|
window: WindowStats & {
|
|
409
429
|
minutes: number;
|
|
410
430
|
requests: number;
|
|
431
|
+
/** Requests per five-minute bin across the window, oldest first (twelve for the default hour). */
|
|
432
|
+
perFiveMinutes: number[];
|
|
411
433
|
};
|
|
412
434
|
allTime: WindowStats & {
|
|
413
435
|
requests: number;
|
|
@@ -423,7 +445,10 @@ export interface AnalyticsSummary {
|
|
|
423
445
|
};
|
|
424
446
|
predictions: {
|
|
425
447
|
estimatedExhaustionMinutes: number | null;
|
|
448
|
+
/** Input and output tokens per minute across the window; cache reads and writes are not in it. */
|
|
426
449
|
tokenBurnRate: number;
|
|
450
|
+
/** Every kind per minute: input, output, cache reads and cache writes. */
|
|
451
|
+
totalTokenBurnRate: number;
|
|
427
452
|
costBurnRate: number;
|
|
428
453
|
};
|
|
429
454
|
}
|
package/dist/analytics.js
CHANGED
|
@@ -69,6 +69,8 @@ export function billingBucketFromClaim(claim) {
|
|
|
69
69
|
return 'extra_usage';
|
|
70
70
|
case 'api':
|
|
71
71
|
return 'api';
|
|
72
|
+
case CREDITS_CLAIM:
|
|
73
|
+
return 'credits';
|
|
72
74
|
default:
|
|
73
75
|
return 'unknown';
|
|
74
76
|
}
|
|
@@ -96,6 +98,14 @@ export const SUBSCRIPTION_CLAIMS = new Set([
|
|
|
96
98
|
]);
|
|
97
99
|
/** The claim the proxy stamps on codex-engine requests (see above). */
|
|
98
100
|
export const CODEX_CLAIM = 'chatgpt_subscription';
|
|
101
|
+
/**
|
|
102
|
+
* The claim the proxy stamps on a request the credits rung served
|
|
103
|
+
* (credits-rung.ts). Not a subscription claim: the request was billed at API
|
|
104
|
+
* rates against the monthly credit balance, so it lands in its own bucket and
|
|
105
|
+
* the ledger files it as metered. The overage guard leaves it alone, because
|
|
106
|
+
* the rung is a chain entry the operator wrote, not a billing surprise.
|
|
107
|
+
*/
|
|
108
|
+
export const CREDITS_CLAIM = 'anthropic_credits';
|
|
99
109
|
/**
|
|
100
110
|
* One-line per-request usage summary for verbose (-v / -vv) logs.
|
|
101
111
|
*
|
|
@@ -189,6 +199,12 @@ export function consumerFromBody(body) {
|
|
|
189
199
|
export function isNonSubscriptionBilling(claim) {
|
|
190
200
|
if (!claim || claim === NO_BILLING_CLAIM)
|
|
191
201
|
return false;
|
|
202
|
+
// The credits rung is metered billing the operator configured by name in
|
|
203
|
+
// the failover chain and armed with a key; the guard exists for billing
|
|
204
|
+
// nobody asked for. The claim is still not a subscription claim, so the
|
|
205
|
+
// bucket math and the ledger keep treating it as paid.
|
|
206
|
+
if (claim === CREDITS_CLAIM)
|
|
207
|
+
return false;
|
|
192
208
|
return !SUBSCRIPTION_CLAIMS.has(claim);
|
|
193
209
|
}
|
|
194
210
|
/**
|
|
@@ -363,6 +379,22 @@ function estimateCost(record) {
|
|
|
363
379
|
// estimates each side correctly rather than repricing history at today's rate.
|
|
364
380
|
return costOfTokens(record.model, record.timestamp, record);
|
|
365
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* Request counts per five-minute bin over the window, oldest bin first; the
|
|
384
|
+
* newest bin is the one `now` falls in. Records outside the window are left
|
|
385
|
+
* out, so the bins sum to the window's request count.
|
|
386
|
+
*/
|
|
387
|
+
export function binByFiveMinutes(records, now, windowMinutes) {
|
|
388
|
+
const n = Math.max(1, Math.ceil(windowMinutes / 5));
|
|
389
|
+
const bins = new Array(n).fill(0);
|
|
390
|
+
for (const r of records) {
|
|
391
|
+
const age = now - r.timestamp;
|
|
392
|
+
if (age < 0 || age >= n * 300_000)
|
|
393
|
+
continue;
|
|
394
|
+
bins[n - 1 - Math.floor(age / 300_000)]++;
|
|
395
|
+
}
|
|
396
|
+
return bins;
|
|
397
|
+
}
|
|
366
398
|
export class Analytics extends EventEmitter {
|
|
367
399
|
records = [];
|
|
368
400
|
maxRecords;
|
|
@@ -426,13 +458,18 @@ export class Analytics extends EventEmitter {
|
|
|
426
458
|
};
|
|
427
459
|
}
|
|
428
460
|
summary(windowMinutes = 60) {
|
|
429
|
-
const
|
|
461
|
+
const now = Date.now();
|
|
462
|
+
const cutoff = now - windowMinutes * 60_000;
|
|
430
463
|
const recent = this.records.filter(r => r.timestamp >= cutoff);
|
|
431
464
|
const allTime = this.records;
|
|
432
465
|
return {
|
|
433
466
|
window: {
|
|
434
467
|
minutes: windowMinutes,
|
|
435
468
|
requests: recent.length,
|
|
469
|
+
// The dashboard's sparkline and its "vs prior 30 min" figure need the
|
|
470
|
+
// whole window; /analytics/stream replays only the last 50 records,
|
|
471
|
+
// which would show an hour of steady traffic as a late surge.
|
|
472
|
+
perFiveMinutes: binByFiveMinutes(recent, now, windowMinutes),
|
|
436
473
|
...this.computeStats(recent),
|
|
437
474
|
},
|
|
438
475
|
allTime: {
|
|
@@ -459,6 +496,7 @@ export class Analytics extends EventEmitter {
|
|
|
459
496
|
subscription_fallback: 0,
|
|
460
497
|
extra_usage: 0,
|
|
461
498
|
api: 0,
|
|
499
|
+
credits: 0,
|
|
462
500
|
unknown: 0,
|
|
463
501
|
},
|
|
464
502
|
subscriptionPercent: 0,
|
|
@@ -478,6 +516,7 @@ export class Analytics extends EventEmitter {
|
|
|
478
516
|
subscription_fallback: 0,
|
|
479
517
|
extra_usage: 0,
|
|
480
518
|
api: 0,
|
|
519
|
+
credits: 0,
|
|
481
520
|
unknown: 0,
|
|
482
521
|
};
|
|
483
522
|
for (const r of records) {
|
|
@@ -601,24 +640,31 @@ export class Analytics extends EventEmitter {
|
|
|
601
640
|
}
|
|
602
641
|
predict(records) {
|
|
603
642
|
if (records.length < 3) {
|
|
604
|
-
return { estimatedExhaustionMinutes: null, tokenBurnRate: 0, costBurnRate: 0 };
|
|
643
|
+
return { estimatedExhaustionMinutes: null, tokenBurnRate: 0, totalTokenBurnRate: 0, costBurnRate: 0 };
|
|
605
644
|
}
|
|
606
645
|
const sorted = [...records].sort((a, b) => a.timestamp - b.timestamp);
|
|
607
646
|
const first = sorted[0];
|
|
608
647
|
const last = sorted[sorted.length - 1];
|
|
609
648
|
const durationMin = (last.timestamp - first.timestamp) / 60_000;
|
|
610
649
|
if (durationMin < 1) {
|
|
611
|
-
return { estimatedExhaustionMinutes: null, tokenBurnRate: 0, costBurnRate: 0 };
|
|
650
|
+
return { estimatedExhaustionMinutes: null, tokenBurnRate: 0, totalTokenBurnRate: 0, costBurnRate: 0 };
|
|
612
651
|
}
|
|
613
652
|
const totalTokens = sorted.reduce((s, r) => s + r.inputTokens + r.outputTokens, 0);
|
|
653
|
+
// Every kind the provider counts, for a reader asking "how many tokens a
|
|
654
|
+
// minute": on a cached prompt the cache reads dwarf input and output.
|
|
655
|
+
// thinkingTokens is an estimate carved out of outputTokens, not a kind of
|
|
656
|
+
// its own, so adding it would count that output twice.
|
|
657
|
+
const allTokens = sorted.reduce((s, r) => s + r.inputTokens + r.outputTokens + r.cacheReadTokens + r.cacheCreateTokens, 0);
|
|
614
658
|
const totalCost = sorted.reduce((s, r) => s + estimateCost(r), 0);
|
|
615
659
|
const tokenBurnRate = totalTokens / durationMin;
|
|
660
|
+
const totalTokenBurnRate = allTokens / durationMin;
|
|
616
661
|
const costBurnRate = (totalCost / durationMin) * 60;
|
|
617
662
|
const currentUtil = last.util5h;
|
|
618
663
|
if (currentUtil >= 0.95) {
|
|
619
664
|
return {
|
|
620
665
|
estimatedExhaustionMinutes: 0,
|
|
621
666
|
tokenBurnRate: Math.round(tokenBurnRate),
|
|
667
|
+
totalTokenBurnRate: Math.round(totalTokenBurnRate),
|
|
622
668
|
costBurnRate: Math.round(costBurnRate * 100) / 100,
|
|
623
669
|
};
|
|
624
670
|
}
|
|
@@ -627,6 +673,7 @@ export class Analytics extends EventEmitter {
|
|
|
627
673
|
return {
|
|
628
674
|
estimatedExhaustionMinutes: null,
|
|
629
675
|
tokenBurnRate: Math.round(tokenBurnRate),
|
|
676
|
+
totalTokenBurnRate: Math.round(totalTokenBurnRate),
|
|
630
677
|
costBurnRate: Math.round(costBurnRate * 100) / 100,
|
|
631
678
|
};
|
|
632
679
|
}
|
|
@@ -634,6 +681,7 @@ export class Analytics extends EventEmitter {
|
|
|
634
681
|
return {
|
|
635
682
|
estimatedExhaustionMinutes: Math.round(minutesToExhaustion),
|
|
636
683
|
tokenBurnRate: Math.round(tokenBurnRate),
|
|
684
|
+
totalTokenBurnRate: Math.round(totalTokenBurnRate),
|
|
637
685
|
costBurnRate: Math.round(costBurnRate * 100) / 100,
|
|
638
686
|
};
|
|
639
687
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** The three values `metadata.user_id` is built from. */
|
|
2
|
+
export interface BodyIdentity {
|
|
3
|
+
deviceId: string;
|
|
4
|
+
accountUuid: string;
|
|
5
|
+
sessionId: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The `metadata.user_id` string Claude Code sends: a JSON object with
|
|
9
|
+
* device_id, account_uuid and session_id in that order. Shared by the
|
|
10
|
+
* template build and the failover rebind so both produce the same bytes.
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildMetadataUserId(identity: BodyIdentity): string;
|
|
13
|
+
/**
|
|
14
|
+
* `body` with `metadata.user_id` rebuilt for `identity`, for a request that
|
|
15
|
+
* fails over to another account. `cchVersion` is the Claude Code version to
|
|
16
|
+
* stamp cch for after the rewrite, or null where the first serialize did not
|
|
17
|
+
* stamp (DARIO_CCH=random); stampCch is itself a no-op without a seed.
|
|
18
|
+
*
|
|
19
|
+
* Returns `body` itself, the same Buffer, when there is nothing to rebind: no
|
|
20
|
+
* body, a body that is not JSON, no string `metadata.user_id`, or a user_id
|
|
21
|
+
* that already names `identity`. The body was serialized by JSON.stringify,
|
|
22
|
+
* so parse and stringify again give the same bytes apart from user_id, and
|
|
23
|
+
* user_id is replaced in place so key order is kept.
|
|
24
|
+
*/
|
|
25
|
+
export declare function rebindBodyIdentity(body: Buffer | undefined, identity: BodyIdentity, cchVersion: string | null): Buffer | undefined;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The account identity a built Claude Code body carries, and how it moves to
|
|
3
|
+
* another seat when a request fails over mid-flight.
|
|
4
|
+
*
|
|
5
|
+
* The template build stamps `metadata.user_id` from the selected account's
|
|
6
|
+
* device_id and account_uuid plus the session id the
|
|
7
|
+
* `x-claude-code-session-id` header carries. A 429 or 401/403 failover sends
|
|
8
|
+
* the same body to a different account, so the body must name that account
|
|
9
|
+
* and the session id its header now carries; otherwise seat B is sent seat
|
|
10
|
+
* A's device_id and account_uuid and a session_id that disagrees with its own
|
|
11
|
+
* header. The cch stamp (src/cch.ts) hashes the body, user_id included, so a
|
|
12
|
+
* rewritten body is stamped again.
|
|
13
|
+
*
|
|
14
|
+
* Pure, and imports nothing that reads the filesystem, so it is unit-testable
|
|
15
|
+
* without a proxy or a ~/.dario.
|
|
16
|
+
*/
|
|
17
|
+
import { stampCch } from './cch.js';
|
|
18
|
+
/**
|
|
19
|
+
* The `metadata.user_id` string Claude Code sends: a JSON object with
|
|
20
|
+
* device_id, account_uuid and session_id in that order. Shared by the
|
|
21
|
+
* template build and the failover rebind so both produce the same bytes.
|
|
22
|
+
*/
|
|
23
|
+
export function buildMetadataUserId(identity) {
|
|
24
|
+
return JSON.stringify({
|
|
25
|
+
device_id: identity.deviceId,
|
|
26
|
+
account_uuid: identity.accountUuid,
|
|
27
|
+
session_id: identity.sessionId,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* `body` with `metadata.user_id` rebuilt for `identity`, for a request that
|
|
32
|
+
* fails over to another account. `cchVersion` is the Claude Code version to
|
|
33
|
+
* stamp cch for after the rewrite, or null where the first serialize did not
|
|
34
|
+
* stamp (DARIO_CCH=random); stampCch is itself a no-op without a seed.
|
|
35
|
+
*
|
|
36
|
+
* Returns `body` itself, the same Buffer, when there is nothing to rebind: no
|
|
37
|
+
* body, a body that is not JSON, no string `metadata.user_id`, or a user_id
|
|
38
|
+
* that already names `identity`. The body was serialized by JSON.stringify,
|
|
39
|
+
* so parse and stringify again give the same bytes apart from user_id, and
|
|
40
|
+
* user_id is replaced in place so key order is kept.
|
|
41
|
+
*/
|
|
42
|
+
export function rebindBodyIdentity(body, identity, cchVersion) {
|
|
43
|
+
if (!body)
|
|
44
|
+
return body;
|
|
45
|
+
let parsed;
|
|
46
|
+
try {
|
|
47
|
+
parsed = JSON.parse(body.toString('utf8'));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return body;
|
|
51
|
+
}
|
|
52
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
53
|
+
return body;
|
|
54
|
+
const metadata = parsed.metadata;
|
|
55
|
+
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata))
|
|
56
|
+
return body;
|
|
57
|
+
const meta = metadata;
|
|
58
|
+
if (typeof meta.user_id !== 'string')
|
|
59
|
+
return body;
|
|
60
|
+
const userId = buildMetadataUserId(identity);
|
|
61
|
+
if (meta.user_id === userId)
|
|
62
|
+
return body;
|
|
63
|
+
meta.user_id = userId;
|
|
64
|
+
let text = JSON.stringify(parsed);
|
|
65
|
+
if (cchVersion !== null)
|
|
66
|
+
text = stampCch(text, cchVersion);
|
|
67
|
+
return Buffer.from(text);
|
|
68
|
+
}
|
package/dist/cc-template.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { loadTemplate, promptVariantsOf, VARIANT_FAMILIES, PLATFORM_ONLY_TOOLS, INTERACTIVE_ONLY_TOOLS, CONFIG_SCOPED_TOOLS, DEFERRED_TOOLS, } from './live-fingerprint.js';
|
|
11
11
|
import { VALID_EFFORT_VALUES, parseEffortSuffix } from './effort.js';
|
|
12
|
+
import { buildMetadataUserId } from './body-identity.js';
|
|
12
13
|
// Re-exported so existing importers (scripts/capture-and-bake.mjs, the template
|
|
13
14
|
// invariant tests) keep their import site. The definitions live in
|
|
14
15
|
// live-fingerprint.ts because loadTemplate must apply the same superset rule to
|
|
@@ -1819,13 +1820,7 @@ export function buildCCRequest(clientBody, billingTag, cacheControl, identity, o
|
|
|
1819
1820
|
}
|
|
1820
1821
|
}
|
|
1821
1822
|
const body = { ...clientBody, system };
|
|
1822
|
-
body.metadata = {
|
|
1823
|
-
user_id: JSON.stringify({
|
|
1824
|
-
device_id: identity.deviceId,
|
|
1825
|
-
account_uuid: identity.accountUuid,
|
|
1826
|
-
session_id: identity.sessionId,
|
|
1827
|
-
}),
|
|
1828
|
-
};
|
|
1823
|
+
body.metadata = { user_id: buildMetadataUserId(identity) };
|
|
1829
1824
|
return { body, toolMap: new Map(), unmappedTools: [], genuineCC: true };
|
|
1830
1825
|
}
|
|
1831
1826
|
// ── Detect text-tool-protocol clients up-front ──
|
|
@@ -2275,13 +2270,7 @@ export function buildCCRequest(clientBody, billingTag, cacheControl, identity, o
|
|
|
2275
2270
|
ccRequest.tool_choice = passed;
|
|
2276
2271
|
}
|
|
2277
2272
|
// Metadata
|
|
2278
|
-
ccRequest.metadata = {
|
|
2279
|
-
user_id: JSON.stringify({
|
|
2280
|
-
device_id: identity.deviceId,
|
|
2281
|
-
account_uuid: identity.accountUuid,
|
|
2282
|
-
session_id: identity.sessionId,
|
|
2283
|
-
}),
|
|
2284
|
-
};
|
|
2273
|
+
ccRequest.metadata = { user_id: buildMetadataUserId(identity) };
|
|
2285
2274
|
ccRequest.max_tokens = resolveMaxTokens(opts.maxTokens, clientBody);
|
|
2286
2275
|
// Model-specific fields — order: thinking, context_management, output_config
|
|
2287
2276
|
//
|
package/dist/cli.d.ts
CHANGED
|
@@ -114,6 +114,13 @@ export declare function parseBooleanEnv(value: string | undefined): boolean | un
|
|
|
114
114
|
* Set(['thinking','env']) — value "thinking,env" → preserve listed
|
|
115
115
|
*/
|
|
116
116
|
export declare function resolvePreserveOrchestrationTags(args: string[], env: string | undefined): Set<string> | undefined;
|
|
117
|
+
/**
|
|
118
|
+
* --upstream-timeout / DARIO_UPSTREAM_TIMEOUT_MS. The flag wins over the env
|
|
119
|
+
* var even when it is 0, and 0 then resolves to undefined so the proxy's
|
|
120
|
+
* default applies: an explicit `--upstream-timeout=0` keeps the default
|
|
121
|
+
* rather than falling through to the env value. Exported for tests.
|
|
122
|
+
*/
|
|
123
|
+
export declare function resolveUpstreamTimeoutMs(flag: number | undefined, env: string | undefined): number | undefined;
|
|
117
124
|
/**
|
|
118
125
|
* The seat shape a running proxy returns from `/accounts`.
|
|
119
126
|
*
|
package/dist/cli.js
CHANGED
|
@@ -516,6 +516,12 @@ async function proxy() {
|
|
|
516
516
|
// used to sever every streaming response after five seconds. dario#1370.
|
|
517
517
|
const shutdownGraceMs = parsePositiveIntFlag('--shutdown-grace=')
|
|
518
518
|
?? parsePositiveIntEnv(process.env['DARIO_SHUTDOWN_GRACE_MS']);
|
|
519
|
+
// --upstream-timeout=MS: how long dario waits for upstream (default 300000).
|
|
520
|
+
// Until the response streams it caps the whole wait; once a 2xx streams it
|
|
521
|
+
// is the longest gap allowed between two chunks, so a long answer that is
|
|
522
|
+
// still sending is never cut. 0 keeps the default: an always-firing timer
|
|
523
|
+
// would fail every request.
|
|
524
|
+
const upstreamTimeoutMs = resolveUpstreamTimeoutMs(parsePositiveIntFlag('--upstream-timeout='), process.env['DARIO_UPSTREAM_TIMEOUT_MS']);
|
|
519
525
|
// --pool-strategy=headroom|fill-first — where UNBOUND (new) conversations
|
|
520
526
|
// land. `headroom` (default) spreads them to the seat with the most slack;
|
|
521
527
|
// `fill-first` concentrates them on the alphabetically-first eligible seat
|
|
@@ -526,9 +532,9 @@ async function proxy() {
|
|
|
526
532
|
// expires soonest before touching the rest.
|
|
527
533
|
const poolStrategyFromFlag = args.find((a) => a.startsWith('--pool-strategy='))?.split('=')[1];
|
|
528
534
|
if (poolStrategyFromFlag !== undefined
|
|
529
|
-
&& poolStrategyFromFlag !== 'headroom' && poolStrategyFromFlag !== '
|
|
530
|
-
&& poolStrategyFromFlag !== 'expiring-first') {
|
|
531
|
-
console.error(`[dario] Invalid --pool-strategy "${poolStrategyFromFlag}". Must be headroom, fill-first or expiring-first.`);
|
|
535
|
+
&& poolStrategyFromFlag !== 'headroom' && poolStrategyFromFlag !== 'headroom-by-plan'
|
|
536
|
+
&& poolStrategyFromFlag !== 'fill-first' && poolStrategyFromFlag !== 'expiring-first') {
|
|
537
|
+
console.error(`[dario] Invalid --pool-strategy "${poolStrategyFromFlag}". Must be headroom, headroom-by-plan, fill-first or expiring-first.`);
|
|
532
538
|
process.exit(1);
|
|
533
539
|
}
|
|
534
540
|
const poolStrategy = poolStrategyFromFlag
|
|
@@ -786,7 +792,7 @@ async function proxy() {
|
|
|
786
792
|
console.error(`[dario] Override (not recommended): pass --unsafe-no-auth if you have out-of-band network controls and accept the risk.`);
|
|
787
793
|
process.exit(1);
|
|
788
794
|
}
|
|
789
|
-
await startProxy({ port, host, verbose, verboseBodies, model, fastModel, noClaudeAuth, analyticsToken, passthrough, preserveTools, hybridTools, mergeTools, noAutoDetect, strictTls, pacingMinMs, pacingJitterMs, thinkTimeBaseMs, thinkTimePerTokenMs, thinkTimeJitterMs, thinkTimeMaxMs, sessionStartMinMs, sessionStartJitterMs, stealth, drainOnClose, sessionIdleRotateMs, sessionRotateJitterMs, sessionMaxAgeMs, sessionPerClient, preserveOrchestrationTags, noLiveCapture, strictTemplate, maxConcurrent, maxQueued, queueTimeoutMs, maxConcurrentPerConsumer, shutdownGraceMs, poolStrategy, poolHeadroomFloor, poolSharedState, poolSharedStateIntervalMs, effort, maxTokens, poolFallbackModel, familyFallback, modelAliases, logFile, passthroughBetas, skipFields, systemPrompt, overageGuardEnabled, overageGuardBehavior, overageGuardCooldownMs, overageGuardNotifyOs, honorClientThinking, preserveOutputFormat, midstreamContinue, ledger, keys, keysPath, keySockets: keySockets.length > 0 ? keySockets : undefined });
|
|
795
|
+
await startProxy({ port, host, verbose, verboseBodies, model, fastModel, noClaudeAuth, analyticsToken, passthrough, preserveTools, hybridTools, mergeTools, noAutoDetect, strictTls, pacingMinMs, pacingJitterMs, thinkTimeBaseMs, thinkTimePerTokenMs, thinkTimeJitterMs, thinkTimeMaxMs, sessionStartMinMs, sessionStartJitterMs, stealth, drainOnClose, sessionIdleRotateMs, sessionRotateJitterMs, sessionMaxAgeMs, sessionPerClient, preserveOrchestrationTags, noLiveCapture, strictTemplate, maxConcurrent, maxQueued, queueTimeoutMs, maxConcurrentPerConsumer, shutdownGraceMs, upstreamTimeoutMs, poolStrategy, poolHeadroomFloor, poolSharedState, poolSharedStateIntervalMs, effort, maxTokens, poolFallbackModel, familyFallback, modelAliases, logFile, passthroughBetas, skipFields, systemPrompt, overageGuardEnabled, overageGuardBehavior, overageGuardCooldownMs, overageGuardNotifyOs, honorClientThinking, preserveOutputFormat, midstreamContinue, ledger, keys, keysPath, keySockets: keySockets.length > 0 ? keySockets : undefined });
|
|
790
796
|
}
|
|
791
797
|
/**
|
|
792
798
|
* `dario keys` — named keys for a shared dario (v6.8, dario#1318). One
|
|
@@ -1208,6 +1214,16 @@ function parsePreserveTagsValue(value) {
|
|
|
1208
1214
|
return new Set(['*']);
|
|
1209
1215
|
return new Set(trimmed.split(',').map(s => s.trim()).filter(Boolean));
|
|
1210
1216
|
}
|
|
1217
|
+
/**
|
|
1218
|
+
* --upstream-timeout / DARIO_UPSTREAM_TIMEOUT_MS. The flag wins over the env
|
|
1219
|
+
* var even when it is 0, and 0 then resolves to undefined so the proxy's
|
|
1220
|
+
* default applies: an explicit `--upstream-timeout=0` keeps the default
|
|
1221
|
+
* rather than falling through to the env value. Exported for tests.
|
|
1222
|
+
*/
|
|
1223
|
+
export function resolveUpstreamTimeoutMs(flag, env) {
|
|
1224
|
+
const ms = flag ?? parsePositiveIntEnv(env);
|
|
1225
|
+
return ms === 0 ? undefined : ms;
|
|
1226
|
+
}
|
|
1211
1227
|
function parsePositiveIntFlag(prefix) {
|
|
1212
1228
|
const found = args.find(a => a.startsWith(prefix));
|
|
1213
1229
|
if (!found)
|
|
@@ -2212,6 +2228,12 @@ async function help() {
|
|
|
2212
2228
|
container's stop grace above it, or Docker
|
|
2213
2229
|
SIGKILLs the drain. Env:
|
|
2214
2230
|
DARIO_SHUTDOWN_GRACE_MS. (dario#1370)
|
|
2231
|
+
--upstream-timeout=MS How long dario waits on upstream (default:
|
|
2232
|
+
300000). Until the response streams it caps
|
|
2233
|
+
the whole wait; while a 2xx streams it is
|
|
2234
|
+
the longest gap between two chunks, so a
|
|
2235
|
+
live stream is never cut. 0 keeps the
|
|
2236
|
+
default. Env: DARIO_UPSTREAM_TIMEOUT_MS.
|
|
2215
2237
|
--max-concurrent=N Max in-flight requests across the WHOLE proxy,
|
|
2216
2238
|
not per seat (default: 10; a pool defaults to
|
|
2217
2239
|
10 per seat). Past it, requests wait in dario.
|
|
@@ -2239,11 +2261,13 @@ async function help() {
|
|
|
2239
2261
|
dario returns 504 "queue-timeout"
|
|
2240
2262
|
(default: 60000).
|
|
2241
2263
|
Env: DARIO_QUEUE_TIMEOUT_MS. (dario#80)
|
|
2242
|
-
--pool-strategy=<headroom|fill-first|expiring-first>
|
|
2264
|
+
--pool-strategy=<headroom|headroom-by-plan|fill-first|expiring-first>
|
|
2243
2265
|
Where new conversations land in a multi-
|
|
2244
2266
|
account pool. headroom (default) spreads
|
|
2245
2267
|
them to the seat with the most slack;
|
|
2246
|
-
|
|
2268
|
+
headroom-by-plan (opt-in) counts each
|
|
2269
|
+
seat's slack by plan size (Max Nx = N,
|
|
2270
|
+
Pro = 1); fill-first concentrates them on the
|
|
2247
2271
|
alphabetically-first eligible seat until
|
|
2248
2272
|
it drains to the floor, then spills;
|
|
2249
2273
|
expiring-first does the same in order of
|
|
@@ -2270,6 +2294,15 @@ async function help() {
|
|
|
2270
2294
|
Needs one of those configured. Empty value
|
|
2271
2295
|
disables. Env: DARIO_POOL_FALLBACK. Config:
|
|
2272
2296
|
poolFallback.model.
|
|
2297
|
+
An entry written credits:<model> is a rung on
|
|
2298
|
+
the API credits a Max plan includes: with
|
|
2299
|
+
DARIO_CREDITS_API_KEY set (env only, never a
|
|
2300
|
+
flag) the request goes to the Claude API on
|
|
2301
|
+
that key as <model>, both wire shapes, after
|
|
2302
|
+
every seat and subscription rung; /analytics
|
|
2303
|
+
and the dashboard file it under credits, the
|
|
2304
|
+
ledger counts it as metered. Without the key
|
|
2305
|
+
the entry is inert and dario doctor says so.
|
|
2273
2306
|
--family-fallback=<family=model,...>
|
|
2274
2307
|
When every pool seat is parked for a model
|
|
2275
2308
|
family, serve its requests as <model> on the
|
|
@@ -3174,12 +3207,16 @@ if (isDirectEntry) {
|
|
|
3174
3207
|
if (!('Bun' in globalThis) && !process.env.DARIO_NO_BUN) {
|
|
3175
3208
|
try {
|
|
3176
3209
|
const { execFileSync, spawn } = await import('node:child_process');
|
|
3210
|
+
const { forwardSignals, relaunchExitCode } = await import('./relaunch-signals.js');
|
|
3177
3211
|
execFileSync('bun', ['--version'], { stdio: 'ignore', timeout: 3000 });
|
|
3178
3212
|
const child = spawn('bun', ['run', ...process.argv.slice(1)], {
|
|
3179
3213
|
stdio: 'inherit',
|
|
3180
3214
|
env: { ...process.env, DARIO_NO_BUN: '1' },
|
|
3181
3215
|
});
|
|
3182
|
-
child
|
|
3216
|
+
// The child owns the graceful shutdown; as PID 1 in a container this
|
|
3217
|
+
// process would otherwise drop SIGTERM and docker stop would SIGKILL both.
|
|
3218
|
+
forwardSignals(child);
|
|
3219
|
+
child.on('exit', (code, signal) => process.exit(relaunchExitCode(code, signal)));
|
|
3183
3220
|
// Prevent this process from continuing
|
|
3184
3221
|
await new Promise(() => { });
|
|
3185
3222
|
}
|
package/dist/codex-backend.js
CHANGED
|
@@ -29,6 +29,7 @@ import { resolveClaudeTarget } from './claude-model.js';
|
|
|
29
29
|
import { parseEffortSuffix } from './effort.js';
|
|
30
30
|
import { BAKED_BASE_MODELS } from './model-catalog.js';
|
|
31
31
|
import { parseRetryAfterMs } from './provider-cooldown.js';
|
|
32
|
+
import { upstreamDeadline } from './upstream-deadline.js';
|
|
32
33
|
import { noteCodexUsageHeaders, parseCodexUsageEndpoint, recordCodexUsage, codexUsageFor } from './codex-usage.js';
|
|
33
34
|
export const CODEX_BACKEND_BASE_URL = process.env.DARIO_CODEX_BASE_URL || 'https://chatgpt.com/backend-api/codex';
|
|
34
35
|
/** The usage endpoint the Codex CLI's /status reads, beside the Responses base. */
|
|
@@ -914,7 +915,8 @@ timing) {
|
|
|
914
915
|
abort.abort();
|
|
915
916
|
} };
|
|
916
917
|
res.on('close', onClientClose);
|
|
917
|
-
|
|
918
|
+
// Caps the wait until headers, then the gap between chunks of the 2xx body.
|
|
919
|
+
const timeout = upstreamDeadline(upstreamTimeoutMs, () => abort.abort());
|
|
918
920
|
let usage = null;
|
|
919
921
|
try {
|
|
920
922
|
if (verbose)
|
|
@@ -962,6 +964,7 @@ timing) {
|
|
|
962
964
|
return true;
|
|
963
965
|
}
|
|
964
966
|
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': corsOrigin, ...securityHeaders, ...splitHeaders() });
|
|
967
|
+
timeout.streaming();
|
|
965
968
|
const reader = upstream.body.getReader();
|
|
966
969
|
const decoder = new TextDecoder();
|
|
967
970
|
let tail = '';
|
|
@@ -969,6 +972,7 @@ timing) {
|
|
|
969
972
|
try {
|
|
970
973
|
while (true) {
|
|
971
974
|
const { done, value } = await reader.read();
|
|
975
|
+
timeout.touch();
|
|
972
976
|
if (done)
|
|
973
977
|
break;
|
|
974
978
|
if (clientGone)
|
|
@@ -1033,7 +1037,7 @@ timing) {
|
|
|
1033
1037
|
return true;
|
|
1034
1038
|
}
|
|
1035
1039
|
finally {
|
|
1036
|
-
|
|
1040
|
+
timeout.clear();
|
|
1037
1041
|
res.removeListener('close', onClientClose);
|
|
1038
1042
|
}
|
|
1039
1043
|
}
|
|
@@ -1196,7 +1200,9 @@ timing) {
|
|
|
1196
1200
|
// Usage seen so far, so a stream the client abandoned still reports what the
|
|
1197
1201
|
// subscription already spent. Populated once the translators exist.
|
|
1198
1202
|
let usageSoFar = () => null;
|
|
1199
|
-
|
|
1203
|
+
// Caps the wait until headers, then the gap between chunks of the 2xx body:
|
|
1204
|
+
// the backend always streams, so a long answer is never cut mid-way.
|
|
1205
|
+
const timeout = upstreamDeadline(upstreamTimeoutMs, () => abort.abort());
|
|
1200
1206
|
try {
|
|
1201
1207
|
if (verbose)
|
|
1202
1208
|
console.log(`[dario] → codex backend: ${target} (model: ${model})`);
|
|
@@ -1324,11 +1330,13 @@ timing) {
|
|
|
1324
1330
|
}
|
|
1325
1331
|
let buffered = '';
|
|
1326
1332
|
if (upstream.body) {
|
|
1333
|
+
timeout.streaming();
|
|
1327
1334
|
const reader = upstream.body.getReader();
|
|
1328
1335
|
const decoder = new TextDecoder();
|
|
1329
1336
|
try {
|
|
1330
1337
|
while (true) {
|
|
1331
1338
|
const { done, value } = await reader.read();
|
|
1339
|
+
timeout.touch();
|
|
1332
1340
|
if (done)
|
|
1333
1341
|
break;
|
|
1334
1342
|
// The abort above normally makes this read reject; a stream that
|
|
@@ -1484,7 +1492,7 @@ timing) {
|
|
|
1484
1492
|
return true;
|
|
1485
1493
|
}
|
|
1486
1494
|
finally {
|
|
1487
|
-
|
|
1495
|
+
timeout.clear();
|
|
1488
1496
|
res.removeListener('close', onClientClose);
|
|
1489
1497
|
}
|
|
1490
1498
|
}
|
package/dist/config-file.d.ts
CHANGED
|
@@ -89,7 +89,7 @@ export interface DarioConfig {
|
|
|
89
89
|
* `expiring-first` is fill-first ordered by each seat's 7-day reset,
|
|
90
90
|
* soonest first.
|
|
91
91
|
*/
|
|
92
|
-
strategy?: 'headroom' | 'fill-first' | 'expiring-first';
|
|
92
|
+
strategy?: 'headroom' | 'headroom-by-plan' | 'fill-first' | 'expiring-first';
|
|
93
93
|
/** Headroom at/below which a seat counts as drained — ratio 0.02..0.5 (dario#1333). */
|
|
94
94
|
headroomFloor?: number;
|
|
95
95
|
};
|
package/dist/config-file.js
CHANGED
|
@@ -309,8 +309,8 @@ function sanitize(parsed) {
|
|
|
309
309
|
}
|
|
310
310
|
if (isPlainObject(parsed.pool)) {
|
|
311
311
|
out.pool = {};
|
|
312
|
-
if (parsed.pool.strategy === 'headroom' || parsed.pool.strategy === '
|
|
313
|
-
|| parsed.pool.strategy === 'expiring-first') {
|
|
312
|
+
if (parsed.pool.strategy === 'headroom' || parsed.pool.strategy === 'headroom-by-plan'
|
|
313
|
+
|| parsed.pool.strategy === 'fill-first' || parsed.pool.strategy === 'expiring-first') {
|
|
314
314
|
out.pool.strategy = parsed.pool.strategy;
|
|
315
315
|
}
|
|
316
316
|
if (typeof parsed.pool.headroomFloor === 'number' && Number.isFinite(parsed.pool.headroomFloor)) {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The credits rung of the failover chain.
|
|
3
|
+
*
|
|
4
|
+
* Since October 2026 a Max plan includes monthly Claude API credits ($100 on
|
|
5
|
+
* Max 5x, $200 on Max 20x). They are spent through a Console API key of the
|
|
6
|
+
* organization the plan is linked to, not through the seat's own five-hour
|
|
7
|
+
* and weekly windows, so until now the only ways to spend them through dario
|
|
8
|
+
* were ANTHROPIC_UPSTREAM_API_KEY (the whole Claude backend on the key, pool
|
|
9
|
+
* bypassed) or an OpenAI-compat backend pointed at Anthropic's compatibility
|
|
10
|
+
* layer, which Anthropic describes as a layer for testing models.
|
|
11
|
+
*
|
|
12
|
+
* A chain entry written `credits:<model>` names the key as a rung. When every
|
|
13
|
+
* seat is drained or cooling and no subscription rung can serve, the request
|
|
14
|
+
* goes to api.anthropic.com on the Messages path as a plain API request: the
|
|
15
|
+
* client's own body with the model swapped, `x-api-key`, no Claude Code
|
|
16
|
+
* template and no OAuth beta. Both wire shapes work, because the Claude path
|
|
17
|
+
* already translates OpenAI-shape requests and responses. The response says
|
|
18
|
+
* `x-dario-pool-fallback: credits:<model>`; the rolling window, `/metrics`
|
|
19
|
+
* and the dashboard file the request under its own billing bucket, `credits`,
|
|
20
|
+
* next to extra usage and API key; the persistent ledger counts it in its
|
|
21
|
+
* metered total, as spent; and the overage guard does not halt on it: a rung
|
|
22
|
+
* the operator configured is not a surprise bill.
|
|
23
|
+
*
|
|
24
|
+
* The key comes from DARIO_CREDITS_API_KEY only, never a flag, so it never
|
|
25
|
+
* lands in argv. The rung is opt-in twice: the entry must be in the chain AND
|
|
26
|
+
* the key must be set. Without the key the entry is inert, the proxy says so
|
|
27
|
+
* at startup and `dario doctor` says so in its Failover line.
|
|
28
|
+
*
|
|
29
|
+
* Where it sits: a subscription rung (a Codex account that lists an entry)
|
|
30
|
+
* always goes first, because the plan is the thing dario exists to spend.
|
|
31
|
+
* Among the metered rungs, this one and the openai-compat backend, the
|
|
32
|
+
* chain's own left-to-right order decides.
|
|
33
|
+
*/
|
|
34
|
+
import { type ClaudeTarget, type ModelResolver } from './claude-model.js';
|
|
35
|
+
export declare const CREDITS_PREFIX = "credits:";
|
|
36
|
+
/** The analytics `account` of a request the credits rung served. */
|
|
37
|
+
export declare const ACCOUNT_KEY_CREDITS = "__credits__";
|
|
38
|
+
/** The value of `x-dario-pool-fallback` on a response the rung served. */
|
|
39
|
+
export declare function creditsFallbackMarker(model: string): string;
|
|
40
|
+
export declare function isCreditsEntry(entry: string): boolean;
|
|
41
|
+
/** The model half of a `credits:<model>` entry as written; null for any other entry. */
|
|
42
|
+
export declare function creditsEntryModel(entry: string): string | null;
|
|
43
|
+
/** Every credits entry's model as written, in chain order. */
|
|
44
|
+
export declare function creditsEntries(chain: readonly string[]): string[];
|
|
45
|
+
/** The chain without its credits entries: what the subscription and openai-compat rungs read. */
|
|
46
|
+
export declare function withoutCreditsEntries(chain: readonly string[]): string[];
|
|
47
|
+
/**
|
|
48
|
+
* True when the credits rung is asked before the openai-compat backend: the
|
|
49
|
+
* first credits entry precedes every plain entry, or there is no plain entry.
|
|
50
|
+
* False when the chain has no credits entry at all.
|
|
51
|
+
*/
|
|
52
|
+
export declare function creditsBeforePlain(chain: readonly string[]): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* The first credits entry the Claude catalog can serve, resolved exactly like
|
|
55
|
+
* a Claude chain entry: aliases (`credits:sonnet`), a provider prefix
|
|
56
|
+
* (`credits:anthropic:claude-opus-5`) and an effort suffix spelled the way a
|
|
57
|
+
* Claude entry spells it (`credits:claude:opus:high`, `credits:claude-opus-5-high`)
|
|
58
|
+
* all work, and a name the catalog does not list is skipped rather than sent, so
|
|
59
|
+
* a typo cannot trade a recoverable 429 for a 404 billed to the key.
|
|
60
|
+
*/
|
|
61
|
+
export declare function pickCreditsTarget(chain: readonly string[], bases?: readonly string[], resolve?: ModelResolver): ClaudeTarget | null;
|
|
62
|
+
/**
|
|
63
|
+
* Outbound headers for a credits request: the headers a plain API client
|
|
64
|
+
* sends and nothing of Claude Code's identity. The client's own betas go
|
|
65
|
+
* through minus the OAuth flag, which an API key request must not carry.
|
|
66
|
+
*/
|
|
67
|
+
export declare function creditsHeaders(key: string, clientBeta: string | undefined, anthropicVersion?: string): Record<string, string>;
|
|
68
|
+
/**
|
|
69
|
+
* One phrase for the startup line and `dario doctor`, or null when the chain
|
|
70
|
+
* names no credits entry.
|
|
71
|
+
*/
|
|
72
|
+
export declare function describeCreditsRung(chain: readonly string[], hasKey: boolean): string | null;
|