@askalf/dario 6.16.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/cli.js +9 -0
- 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/proxy.d.ts +10 -0
- package/dist/proxy.js +292 -74
- package/docs/analytics.md +10 -8
- package/docs/api-equivalent-spend.md +9 -2
- package/docs/faq.md +5 -0
- package/docs/multi-account-pool.md +1 -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/proxy.js
CHANGED
|
@@ -19,10 +19,10 @@ import { describeTemplate, detectDrift, checkCCCompat, probeInstalledCCVersion }
|
|
|
19
19
|
import { AccountPool, computeStickyKey, parseRateLimits, modelFamily, isInAuthCooldown, authCooldownMs, accountIneligibility, reportedAccountStatus, activeParkedBuckets, reconcilePoolAccounts, resolvePoolStrategy, resolvePoolHeadroomFloor, parsePoolHeadroomFloor, describePoolStrategy, utilFreshness, rateLimitWindow, accountAction, accountPeers, distinctAccounts, describeRejection, maskEmail } from './pool.js';
|
|
20
20
|
import { backfillIdentity } from './accounts.js';
|
|
21
21
|
import { PoolSync, DEFAULT_POOL_SYNC_INTERVAL_MS } from './pool-sync.js';
|
|
22
|
-
import { Analytics, billingBucketFromClaim, costOfTokensFailClosed, formatUsageLogLine, SUBSCRIPTION_CLAIMS, consumerFromHeader, consumerFromBody, CONSUMER_HEADER, CODEX_CLAIM } from './analytics.js';
|
|
22
|
+
import { Analytics, billingBucketFromClaim, costOfTokensFailClosed, formatUsageLogLine, SUBSCRIPTION_CLAIMS, consumerFromHeader, consumerFromBody, CONSUMER_HEADER, CODEX_CLAIM, CREDITS_CLAIM } from './analytics.js';
|
|
23
23
|
import { Ledger, resolveLedgerPath, ledgerDisabledByEnv } from './ledger.js';
|
|
24
24
|
import { renderPrometheus } from './metrics.js';
|
|
25
|
-
import { renderSpendDonuts, renderAnalyticsView,
|
|
25
|
+
import { renderSpendDonuts, renderAnalyticsView, analyticsUiPage } from './donuts.js';
|
|
26
26
|
import { KeyStore, keyAllowsModel, resolveKeysPath, looksLikeNamedKey, budgetVerdict, budgetHeaders, requestBudgetReservation, addReservation, subtractReservation, EMPTY_RESERVATION } from './keys.js';
|
|
27
27
|
import { OverageGuard, buildHaltErrorBody } from './overage-guard.js';
|
|
28
28
|
import { notify as osNotify } from './notify.js';
|
|
@@ -56,6 +56,7 @@ import { codexUsageView, resolveCodexUsagePollMs } from './codex-usage.js';
|
|
|
56
56
|
import { seedStaleCodexUsage } from './codex-backend.js';
|
|
57
57
|
import { route as routeProvider } from './provider-adapter.js';
|
|
58
58
|
import { selectPoolFallbackModels } from './pool-fallback-tier.js';
|
|
59
|
+
import { ACCOUNT_KEY_CREDITS, creditsBeforePlain, creditsEntries, creditsFallbackMarker, creditsHeaders, describeCreditsRung, pickCreditsTarget, withoutCreditsEntries } from './credits-rung.js';
|
|
59
60
|
import { parseFamilyFallback, familyFallbackModel, describeFamilyFallback } from './family-fallback.js';
|
|
60
61
|
import { RequestQueue, QueueFullError, QueueTimeoutError, DEFAULT_MAX_CONCURRENT, DEFAULT_MAX_QUEUED, DEFAULT_QUEUE_TIMEOUT_MS, resolveMaxConcurrent } from './request-queue.js';
|
|
61
62
|
import { waitForIdle, drainThenClose, shutdownTimers, DEFAULT_SHUTDOWN_GRACE_MS } from './shutdown-drain.js';
|
|
@@ -1349,6 +1350,9 @@ export async function startProxy(opts = {}) {
|
|
|
1349
1350
|
const upstreamFetch = chaosCut && chaosState ? chaosCutFetch(opts.fetchImpl ?? fetch, chaosCut, chaosState) : (opts.fetchImpl ?? fetch);
|
|
1350
1351
|
const codexFetch = chaosCut && chaosState ? chaosCutFetch(fetch, chaosCut, chaosState) : fetch;
|
|
1351
1352
|
const upstreamApiKey = (opts.upstreamApiKey ?? process.env.ANTHROPIC_UPSTREAM_API_KEY ?? '').trim();
|
|
1353
|
+
// The credits rung's key (credits-rung.ts). Read once here; whether it is
|
|
1354
|
+
// ever used depends on the chain naming a `credits:<model>` entry.
|
|
1355
|
+
const creditsApiKey = (opts.creditsApiKey ?? process.env.DARIO_CREDITS_API_KEY ?? '').trim();
|
|
1352
1356
|
if (upstreamApiKey)
|
|
1353
1357
|
console.error('[dario] upstream auth: per-token API key (x-api-key) — OAuth/Max + account pool bypassed');
|
|
1354
1358
|
else if (ignoreCcCredentials())
|
|
@@ -1555,17 +1559,37 @@ export async function startProxy(opts = {}) {
|
|
|
1555
1559
|
}
|
|
1556
1560
|
};
|
|
1557
1561
|
if (poolFallbackModel) {
|
|
1562
|
+
// The rungs in the order they are asked: the subscription first, then the
|
|
1563
|
+
// metered rungs (the credits key and the openai-compat backend) in the
|
|
1564
|
+
// chain's own order. A `credits:` entry is not a model the other rungs
|
|
1565
|
+
// can serve, so their model is the first plain entry.
|
|
1566
|
+
const plainFallbackModel = withoutCreditsEntries(poolFallbackModels)[0] ?? null;
|
|
1567
|
+
const creditsLine = describeCreditsRung(poolFallbackModels, creditsApiKey.length > 0);
|
|
1568
|
+
const creditsArmed = creditsLine !== null && creditsApiKey.length > 0;
|
|
1558
1569
|
const targets = [];
|
|
1559
|
-
if (startupCodexAliases.length > 0)
|
|
1560
|
-
targets.push(`codex subscription (${startupCodexAliases.join(', ')})
|
|
1561
|
-
|
|
1562
|
-
|
|
1570
|
+
if (startupCodexAliases.length > 0 && plainFallbackModel)
|
|
1571
|
+
targets.push(`codex subscription (${startupCodexAliases.join(', ')}) as ${plainFallbackModel}, both wire shapes`);
|
|
1572
|
+
const metered = [];
|
|
1573
|
+
if (openaiBackend && plainFallbackModel)
|
|
1574
|
+
metered.push(`${openaiBackend.name} as ${plainFallbackModel}, OpenAI shape only`);
|
|
1575
|
+
if (creditsArmed) {
|
|
1576
|
+
if (creditsBeforePlain(poolFallbackModels))
|
|
1577
|
+
metered.unshift(creditsLine);
|
|
1578
|
+
else
|
|
1579
|
+
metered.push(creditsLine);
|
|
1580
|
+
}
|
|
1581
|
+
targets.push(...metered);
|
|
1563
1582
|
if (targets.length > 0) {
|
|
1564
|
-
console.log(` Pool fallback: exhausted-pool requests → ${targets.join(', then ')}
|
|
1583
|
+
console.log(` Pool fallback: exhausted-pool requests → ${targets.join(', then ')} (marked x-dario-pool-fallback)`);
|
|
1565
1584
|
}
|
|
1566
1585
|
else {
|
|
1567
|
-
console.warn('[dario] --pool-fallback is set but there is nothing to fall back TO
|
|
1586
|
+
console.warn('[dario] --pool-fallback is set but there is nothing to fall back TO: add a Codex account (`dario codex add …`), an OpenAI-compat backend (`dario backend add …`), or a credits:<model> entry with DARIO_CREDITS_API_KEY set. Fallback is inert.');
|
|
1568
1587
|
}
|
|
1588
|
+
if (creditsLine && !creditsArmed)
|
|
1589
|
+
console.warn(`[dario] --pool-fallback ${creditsLine}`);
|
|
1590
|
+
}
|
|
1591
|
+
else if (creditsApiKey) {
|
|
1592
|
+
console.warn('[dario] DARIO_CREDITS_API_KEY is set but no --pool-fallback entry names credits:<model>, so nothing spends it');
|
|
1569
1593
|
}
|
|
1570
1594
|
// Family fallback (strictly opt-in, family-fallback.ts). Bad entries are
|
|
1571
1595
|
// dropped with a warning; the rest still apply.
|
|
@@ -2479,7 +2503,9 @@ export async function startProxy(opts = {}) {
|
|
|
2479
2503
|
//
|
|
2480
2504
|
// With NO next option and no peer, do not defer: the real upstream error is
|
|
2481
2505
|
// more useful to the client than replacing it with a generic 503.
|
|
2482
|
-
|
|
2506
|
+
// An armed credits rung is a next option on both wire shapes.
|
|
2507
|
+
const hasNextOption = (openaiBackend !== null && shape === 'openai')
|
|
2508
|
+
|| (creditsApiKey.length > 0 && creditsEntries(fallbackModels).length > 0);
|
|
2483
2509
|
// The next seat that could serve one of these fallback models, excluding
|
|
2484
2510
|
// everything already tried. `peek` is the cached read, so the scan costs no
|
|
2485
2511
|
// upstream call; a seat whose model list is unknown is still worth a try.
|
|
@@ -2577,21 +2603,37 @@ export async function startProxy(opts = {}) {
|
|
|
2577
2603
|
* bug structurally impossible to reintroduce: a future third call site gets
|
|
2578
2604
|
* this by construction, not by remembering to copy six lines correctly.
|
|
2579
2605
|
*/
|
|
2580
|
-
const attemptPoolFallbackOn429 = async (req, res, body, isOpenAI, attempted, timing
|
|
2581
|
-
|
|
2582
|
-
|
|
2606
|
+
const attemptPoolFallbackOn429 = async (req, res, body, isOpenAI, attempted, timing,
|
|
2607
|
+
/**
|
|
2608
|
+
* Re-arm THIS request on the credits rung (credits-rung.ts): returns true
|
|
2609
|
+
* once the request is re-pointed at the key, and the caller re-enters its
|
|
2610
|
+
* dispatch loop. Absent on a site that cannot re-dispatch.
|
|
2611
|
+
*/
|
|
2612
|
+
armCredits) => {
|
|
2613
|
+
const chain = selectPoolFallbackForBody(body);
|
|
2614
|
+
if (await tryCodexPoolFallback(req, res, body, chain, isOpenAI ? 'openai' : 'anthropic', 'pool exhausted mid-flight (429, no peer)', attempted, timing)) {
|
|
2615
|
+
return 'served';
|
|
2583
2616
|
}
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2617
|
+
// The metered rungs, in the chain's own order. The credits rung is never
|
|
2618
|
+
// asked twice in one request: a 429 from the key falls through to the
|
|
2619
|
+
// openai-compat backend or to the honest 429.
|
|
2620
|
+
const plainModel = withoutCreditsEntries(chain)[0] ?? null;
|
|
2621
|
+
const creditsFirst = creditsBeforePlain(chain);
|
|
2622
|
+
const tryCredits = () => armCredits !== undefined && !attempted.has('credits') && armCredits();
|
|
2623
|
+
if (creditsFirst && tryCredits())
|
|
2624
|
+
return 'credits';
|
|
2625
|
+
if (isOpenAI && plainModel && openaiBackend) {
|
|
2626
|
+
const fallbackBody = buildPoolFallbackBody(body, plainModel);
|
|
2587
2627
|
if (fallbackBody) {
|
|
2588
|
-
console.log(`[dario] #${requestCount} pool exhausted mid-flight (429, no peer) → ${openaiBackend.name} as ${
|
|
2628
|
+
console.log(`[dario] #${requestCount} pool exhausted mid-flight (429, no peer) → ${openaiBackend.name} as ${plainModel}`);
|
|
2589
2629
|
requestCount++;
|
|
2590
|
-
await forwardToOpenAI(req, res, fallbackBody, openaiBackend, corsOrigin, { ...SECURITY_HEADERS, 'x-dario-pool-fallback':
|
|
2591
|
-
return
|
|
2630
|
+
await forwardToOpenAI(req, res, fallbackBody, openaiBackend, corsOrigin, { ...SECURITY_HEADERS, 'x-dario-pool-fallback': plainModel }, upstreamTimeoutMs, verbose);
|
|
2631
|
+
return 'served';
|
|
2592
2632
|
}
|
|
2593
2633
|
}
|
|
2594
|
-
|
|
2634
|
+
if (!creditsFirst && tryCredits())
|
|
2635
|
+
return 'credits';
|
|
2636
|
+
return 'none';
|
|
2595
2637
|
};
|
|
2596
2638
|
const server = createServer(async (req, res) => {
|
|
2597
2639
|
// The request's first stamp (src/timing.ts): every split below is
|
|
@@ -2829,11 +2871,12 @@ export async function startProxy(opts = {}) {
|
|
|
2829
2871
|
if (handled)
|
|
2830
2872
|
return;
|
|
2831
2873
|
}
|
|
2832
|
-
// The dashboard
|
|
2833
|
-
//
|
|
2874
|
+
// The dashboard page carries no data, so it needs no credential: it asks
|
|
2875
|
+
// for the token, then reads /analytics, /accounts, /codex, /health, the
|
|
2876
|
+
// ledger and the keys with it and tails /analytics/stream (donuts.ts).
|
|
2834
2877
|
if (urlPath === '/analytics/ui' && req.method === 'GET') {
|
|
2835
2878
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', ...SECURITY_HEADERS });
|
|
2836
|
-
res.end(
|
|
2879
|
+
res.end(analyticsUiPage());
|
|
2837
2880
|
return;
|
|
2838
2881
|
}
|
|
2839
2882
|
// Read-only analytics token: accepted on the read-only surfaces only, and
|
|
@@ -3013,7 +3056,11 @@ export async function startProxy(opts = {}) {
|
|
|
3013
3056
|
// `queue` rides along the summary (dario#905): request-queue.ts always
|
|
3014
3057
|
// documented snapshot() as "exposed for /analytics", but it was never
|
|
3015
3058
|
// actually wired in, so slot exhaustion was invisible from outside.
|
|
3016
|
-
|
|
3059
|
+
// `overage` is the halt state for every reader of this route (the
|
|
3060
|
+
// dashboard, dario#1734): the stream replays a halt only while one is
|
|
3061
|
+
// in force and the analytics token cannot read /admin/resume, so a
|
|
3062
|
+
// viewer that saw the halt and missed the resume resyncs from here.
|
|
3063
|
+
res.end(JSON.stringify({ ...analytics.summary(), queue: queue.snapshot(), overage: { halted: overageGuard.isHalted(), state: overageGuard.state() }, lifetime: ledger ? ledger.summary() : null, budgets: keyBudgetsSnapshot() }));
|
|
3017
3064
|
return;
|
|
3018
3065
|
}
|
|
3019
3066
|
// Prometheus text exposition of the same state (dario#1341). A view, not
|
|
@@ -3052,7 +3099,9 @@ export async function startProxy(opts = {}) {
|
|
|
3052
3099
|
return;
|
|
3053
3100
|
}
|
|
3054
3101
|
// The ledger's per-day table, for anyone charting it. `lifetime` on
|
|
3055
|
-
// /analytics is the summary; this is the data behind it.
|
|
3102
|
+
// /analytics is the summary; this is the data behind it. `priced` is the
|
|
3103
|
+
// same rows as USD at each day's rate (ledger.ts), so a reader, the
|
|
3104
|
+
// dashboard first, never needs a price list of its own.
|
|
3056
3105
|
if (urlPath === '/analytics/ledger' && req.method === 'GET') {
|
|
3057
3106
|
if (!ledger) {
|
|
3058
3107
|
res.writeHead(404, JSON_HEADERS);
|
|
@@ -3060,7 +3109,7 @@ export async function startProxy(opts = {}) {
|
|
|
3060
3109
|
return;
|
|
3061
3110
|
}
|
|
3062
3111
|
res.writeHead(200, JSON_HEADERS);
|
|
3063
|
-
res.end(JSON.stringify({ path: ledger.path, ...ledger.snapshot() }));
|
|
3112
|
+
res.end(JSON.stringify({ path: ledger.path, ...ledger.snapshot(), priced: ledger.priced() }));
|
|
3064
3113
|
return;
|
|
3065
3114
|
}
|
|
3066
3115
|
// Analytics live stream — SSE of new RequestRecord JSON, one event
|
|
@@ -3372,6 +3421,17 @@ export async function startProxy(opts = {}) {
|
|
|
3372
3421
|
// form gives the declared type without seeding a narrowing.
|
|
3373
3422
|
let poolAccount = null;
|
|
3374
3423
|
let accessToken = '';
|
|
3424
|
+
// Set when the credits rung (credits-rung.ts) is serving this request:
|
|
3425
|
+
// the Claude path then runs on the key instead of a seat. The same `as`
|
|
3426
|
+
// form as poolAccount, for the same narrowing reason.
|
|
3427
|
+
let creditsRung = null;
|
|
3428
|
+
// The account an analytics record names: the seat, the credits rung, or
|
|
3429
|
+
// the api-key slot. A function because a mid-flight 429 can move the
|
|
3430
|
+
// request from a seat to the credits rung after the first attempt.
|
|
3431
|
+
const accountKeyNow = () => creditsRung ? ACCOUNT_KEY_CREDITS : (poolAccount?.alias ?? ACCOUNT_KEY_APIKEY);
|
|
3432
|
+
// The claim a record carries: Anthropic sends none on an API key
|
|
3433
|
+
// request, so the credits rung stamps its own, as the codex engine does.
|
|
3434
|
+
const claimNow = (rl) => creditsRung ? CREDITS_CLAIM : rl.claim;
|
|
3375
3435
|
// Providers this ONE request has already asked. Paired with the
|
|
3376
3436
|
// process-wide cool-downs: the set stops a revisit inside the request,
|
|
3377
3437
|
// the cool-downs stop the next request repeating the discovery.
|
|
@@ -3575,7 +3635,11 @@ export async function startProxy(opts = {}) {
|
|
|
3575
3635
|
//
|
|
3576
3636
|
// An EMPTY pool still 503s: that's a setup error the operator needs to
|
|
3577
3637
|
// see, not traffic to quietly re-bill somewhere else.
|
|
3578
|
-
|
|
3638
|
+
// A chain whose only entries are credits rungs without the key is
|
|
3639
|
+
// inert and defers nothing: the honest 429/503 stands.
|
|
3640
|
+
const chainCanServe = withoutCreditsEntries(poolFallbackModels).length > 0
|
|
3641
|
+
|| (creditsApiKey.length > 0 && creditsEntries(poolFallbackModels).length > 0);
|
|
3642
|
+
const fallbackViable = chainCanServe && pool.size > 0;
|
|
3579
3643
|
if (!fallbackViable) {
|
|
3580
3644
|
if (parkedUntil !== null)
|
|
3581
3645
|
writePoolParked(parkedUntil);
|
|
@@ -4556,6 +4620,30 @@ export async function startProxy(opts = {}) {
|
|
|
4556
4620
|
}
|
|
4557
4621
|
if (!selectPoolAccount())
|
|
4558
4622
|
return;
|
|
4623
|
+
// The credits rung (credits-rung.ts): re-point THIS request at the key
|
|
4624
|
+
// as the first `credits:<model>` entry the catalog can serve. False when
|
|
4625
|
+
// the chain names no such entry, the key is unset, the request is pinned
|
|
4626
|
+
// to a seat, or the rung already answered this request. The response is
|
|
4627
|
+
// marked here, before any byte of the answer, and the Claude path below
|
|
4628
|
+
// then runs on the key: `x-api-key`, the client's own body, no template.
|
|
4629
|
+
const armCreditsRung = (why) => {
|
|
4630
|
+
if (!creditsApiKey || creditsRung || pinnedAccount || attemptedProviders.has('credits'))
|
|
4631
|
+
return false;
|
|
4632
|
+
const target = pickCreditsTarget(selectPoolFallbackForBody(body), getCachedBases(), (m) => resolveClaudeAlias(applyModelAlias(m, modelAliases) ?? m));
|
|
4633
|
+
if (!target)
|
|
4634
|
+
return false;
|
|
4635
|
+
attemptedProviders.add('credits');
|
|
4636
|
+
creditsRung = target;
|
|
4637
|
+
poolAccount = null;
|
|
4638
|
+
accessToken = '';
|
|
4639
|
+
requestModel = target.model;
|
|
4640
|
+
requestFamily = modelFamily(target.model);
|
|
4641
|
+
if (target.effort)
|
|
4642
|
+
requestEffort = target.effort;
|
|
4643
|
+
res.setHeader('x-dario-pool-fallback', creditsFallbackMarker(target.model));
|
|
4644
|
+
console.log(`[dario] #${requestCount} ${why} → API credits as ${target.model}${target.effort ? ` (effort: ${target.effort})` : ''} (x-api-key)`);
|
|
4645
|
+
return true;
|
|
4646
|
+
};
|
|
4559
4647
|
// Pool-exhausted fallback dispatch. In OAuth mode poolAccount can only
|
|
4560
4648
|
// be null here when the selection above deferred to this path (armed
|
|
4561
4649
|
// fallback + drained pool + OpenAI-shape request): swap the model and
|
|
@@ -4583,19 +4671,34 @@ export async function startProxy(opts = {}) {
|
|
|
4583
4671
|
// that was ~2% of dario's own CPU on every request that never took the
|
|
4584
4672
|
// branch (scripts/bench-overhead.mjs profile, v6.9.2). Resolved only
|
|
4585
4673
|
// when the branch can be taken.
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4674
|
+
//
|
|
4675
|
+
// The metered rungs come after the subscription, in the chain's own
|
|
4676
|
+
// order: the credits rung (both wire shapes, since it rides the Claude
|
|
4677
|
+
// path) and the openai-compat backend (OpenAI shape only). A `credits:`
|
|
4678
|
+
// entry is no model for that backend, so its model is the first plain
|
|
4679
|
+
// entry.
|
|
4680
|
+
if (!upstreamApiKey && !poolAccount && !pinnedAccount) {
|
|
4681
|
+
const chainForBody = selectPoolFallbackForBody(body);
|
|
4682
|
+
const plainModel = withoutCreditsEntries(chainForBody)[0] ?? null;
|
|
4683
|
+
const openaiCanServe = openaiBackend !== null && isOpenAI && plainModel !== null;
|
|
4684
|
+
const creditsFirst = creditsBeforePlain(chainForBody);
|
|
4685
|
+
if (creditsFirst && armCreditsRung('pool exhausted')) {
|
|
4686
|
+
// Served below, on the key.
|
|
4687
|
+
}
|
|
4688
|
+
else if (openaiCanServe) {
|
|
4689
|
+
const fallbackBody = buildPoolFallbackBody(body, plainModel);
|
|
4690
|
+
if (!fallbackBody) {
|
|
4691
|
+
writePoolUnavailable();
|
|
4692
|
+
return;
|
|
4693
|
+
}
|
|
4694
|
+
console.log(`[dario] #${requestCount} pool exhausted, /v1/chat/completions → ${openaiBackend.name} as ${plainModel}`);
|
|
4695
|
+
requestCount++;
|
|
4696
|
+
await forwardToOpenAI(req, res, fallbackBody, openaiBackend, corsOrigin, { ...SECURITY_HEADERS, 'x-dario-pool-fallback': plainModel }, upstreamTimeoutMs, verbose);
|
|
4593
4697
|
return;
|
|
4594
4698
|
}
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
return;
|
|
4699
|
+
else if (!creditsFirst) {
|
|
4700
|
+
armCreditsRung('pool exhausted');
|
|
4701
|
+
}
|
|
4599
4702
|
}
|
|
4600
4703
|
// Deferred at selection because a fallback was armed, but nothing could
|
|
4601
4704
|
// actually serve it — no Codex account lists the model, and either there
|
|
@@ -4603,7 +4706,7 @@ export async function startProxy(opts = {}) {
|
|
|
4603
4706
|
// with the truth the selector used to give. Without this the request
|
|
4604
4707
|
// would fall through to the Claude path with no account and an empty
|
|
4605
4708
|
// bearer token, turning a clean 503 into a confusing upstream 401.
|
|
4606
|
-
if (!upstreamApiKey && !poolAccount) {
|
|
4709
|
+
if (!upstreamApiKey && !poolAccount && !creditsRung) {
|
|
4607
4710
|
// A fallback was armed but nothing could serve, and the pool itself is
|
|
4608
4711
|
// parked: the exact reset beats a cool-down estimate (dario#1244).
|
|
4609
4712
|
const parkedNow = pool.parkedUntil(Date.now(), requestFamily);
|
|
@@ -4658,7 +4761,49 @@ export async function startProxy(opts = {}) {
|
|
|
4658
4761
|
userId: req.headers['x-user-id'],
|
|
4659
4762
|
timestamp: new Date().toISOString(),
|
|
4660
4763
|
} : undefined;
|
|
4661
|
-
|
|
4764
|
+
// The credits rung sends the client's own body as a plain API request:
|
|
4765
|
+
// sanitized, translated when OpenAI-shaped, the model swapped for the
|
|
4766
|
+
// rung's, the effort the entry named applied when the client set none.
|
|
4767
|
+
// No Claude Code template, identity or cch stamp: those make a request
|
|
4768
|
+
// look like Claude Code's for the OAuth gate, and an API key request
|
|
4769
|
+
// would be billed for every byte of them for nothing.
|
|
4770
|
+
const thinCreditsBody = (target) => {
|
|
4771
|
+
try {
|
|
4772
|
+
const parsed = JSON.parse(body.toString());
|
|
4773
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
4774
|
+
return null;
|
|
4775
|
+
const client = parsed;
|
|
4776
|
+
sanitizeMessages(client, opts.preserveOrchestrationTags);
|
|
4777
|
+
const r = (isOpenAI ? openaiToAnthropic(client, target.model) : { ...client, model: target.model });
|
|
4778
|
+
r.model = stripContext1mTag(typeof r.model === 'string' ? r.model : target.model);
|
|
4779
|
+
const effort = requestEffort ?? opts.effort;
|
|
4780
|
+
if (effort) {
|
|
4781
|
+
const oc = (r.output_config && typeof r.output_config === 'object' && !Array.isArray(r.output_config))
|
|
4782
|
+
? { ...r.output_config }
|
|
4783
|
+
: {};
|
|
4784
|
+
if (typeof oc.effort !== 'string') {
|
|
4785
|
+
oc.effort = effort;
|
|
4786
|
+
r.output_config = oc;
|
|
4787
|
+
}
|
|
4788
|
+
}
|
|
4789
|
+
return Buffer.from(JSON.stringify(r));
|
|
4790
|
+
}
|
|
4791
|
+
catch {
|
|
4792
|
+
return null;
|
|
4793
|
+
}
|
|
4794
|
+
};
|
|
4795
|
+
if (creditsRung) {
|
|
4796
|
+
const thin = thinCreditsBody(creditsRung);
|
|
4797
|
+
if (!thin) {
|
|
4798
|
+
console.log(`[dario] #${requestCount} credits rung: the request body could not be re-pointed at ${creditsRung.model}`);
|
|
4799
|
+
writePoolUnavailable();
|
|
4800
|
+
return;
|
|
4801
|
+
}
|
|
4802
|
+
finalBody = thin;
|
|
4803
|
+
// The client's own tool names go out and come back untouched.
|
|
4804
|
+
preserveToolsEffective = true;
|
|
4805
|
+
}
|
|
4806
|
+
else if (body.length > 0) {
|
|
4662
4807
|
try {
|
|
4663
4808
|
// Reuse the object parsed for provider-prefix detection above — the same
|
|
4664
4809
|
// bytes, so re-parsing is wasted work on large bodies (#642-audit). Falls
|
|
@@ -4978,7 +5123,12 @@ export async function startProxy(opts = {}) {
|
|
|
4978
5123
|
// Beta headers
|
|
4979
5124
|
const clientBeta = req.headers['anthropic-beta'];
|
|
4980
5125
|
let beta;
|
|
4981
|
-
if (
|
|
5126
|
+
if (creditsRung) {
|
|
5127
|
+
// A plain API request: the client's betas minus the OAuth flag go out
|
|
5128
|
+
// in creditsHeaders below; nothing of the template's set applies.
|
|
5129
|
+
beta = '';
|
|
5130
|
+
}
|
|
5131
|
+
else if (passthrough || isCountTokens) {
|
|
4982
5132
|
// Passthrough (and thin count_tokens): only add oauth beta,
|
|
4983
5133
|
// forward client betas as-is — no template beta set.
|
|
4984
5134
|
beta = 'oauth-2025-04-20';
|
|
@@ -4992,7 +5142,7 @@ export async function startProxy(opts = {}) {
|
|
|
4992
5142
|
// context-1m requires Extra Usage — if it 400s, we auto-retry without
|
|
4993
5143
|
// it, and cache the rejection so subsequent requests on this account
|
|
4994
5144
|
// skip context-1m entirely (dario#36).
|
|
4995
|
-
const acctKey =
|
|
5145
|
+
const acctKey = accountKeyNow();
|
|
4996
5146
|
const skipContext1m = context1mUnavailable.has(acctKey);
|
|
4997
5147
|
// Model-conditional betas (fable fallback-credit; [1m] context-1m),
|
|
4998
5148
|
// mirroring real CC — see betaForModel. betaWithoutContext1m still
|
|
@@ -5050,7 +5200,7 @@ export async function startProxy(opts = {}) {
|
|
|
5050
5200
|
// (below) does not re-pace on the peer: the retry is the rare path, and
|
|
5051
5201
|
// holding it for a floor would stack a second wait on a request that
|
|
5052
5202
|
// has already been refused once.
|
|
5053
|
-
const seatClock = pacingClocks.seat(
|
|
5203
|
+
const seatClock = pacingClocks.seat(accountKeyNow());
|
|
5054
5204
|
const nowForPacing = Date.now();
|
|
5055
5205
|
const pacingDelay = computePacingDelay(nowForPacing, seatClock.lastRequestTime, pacingCfg);
|
|
5056
5206
|
const thinkDelay = thinkTimeEnabled
|
|
@@ -5096,12 +5246,16 @@ export async function startProxy(opts = {}) {
|
|
|
5096
5246
|
const forwardedIdentity = (passthrough || genuineCCRequest)
|
|
5097
5247
|
? forwardClientCCIdentityHeaders(req.headers)
|
|
5098
5248
|
: {};
|
|
5099
|
-
const
|
|
5249
|
+
const clientAnthropicVersion = passthrough ? (req.headers['anthropic-version'] || '2023-06-01') : '2023-06-01';
|
|
5250
|
+
// On the credits rung the outbound set is a plain API client's
|
|
5251
|
+
// (credits-rung.ts): the key, the version, the client's betas minus the
|
|
5252
|
+
// OAuth flag. Nothing of Claude Code's identity or session.
|
|
5253
|
+
const headers = creditsRung ? creditsHeaders(creditsApiKey, clientBeta, clientAnthropicVersion) : {
|
|
5100
5254
|
...staticHeaders,
|
|
5101
5255
|
...forwardedIdentity,
|
|
5102
5256
|
...upstreamAuthHeaders(upstreamApiKey, accessToken),
|
|
5103
5257
|
'x-claude-code-session-id': outboundSessionId,
|
|
5104
|
-
'anthropic-version':
|
|
5258
|
+
'anthropic-version': clientAnthropicVersion,
|
|
5105
5259
|
'anthropic-beta': beta,
|
|
5106
5260
|
// Prefer the client's own when passthrough forwarded one: a genuine CC
|
|
5107
5261
|
// request already carries a real request id, and synthesising over it
|
|
@@ -5192,12 +5346,37 @@ export async function startProxy(opts = {}) {
|
|
|
5192
5346
|
pool.rebindSticky(stickyKey, next.alias);
|
|
5193
5347
|
poolSync?.bindSticky(stickyKey, next.alias);
|
|
5194
5348
|
};
|
|
5349
|
+
// Mid-flight move to the credits rung (credits-rung.ts): a 429 with no
|
|
5350
|
+
// peer seat left, and the chain names a credits entry the key can serve.
|
|
5351
|
+
// The request is rebuilt as a plain API request and the headers are
|
|
5352
|
+
// replaced in place, because the loop head re-derives the outbound set
|
|
5353
|
+
// from `headers` on every pass; the caller then re-enters the loop.
|
|
5354
|
+
const rearmOnCredits = () => {
|
|
5355
|
+
if (!armCreditsRung('pool exhausted mid-flight (429, no peer)'))
|
|
5356
|
+
return false;
|
|
5357
|
+
const thin = thinCreditsBody(creditsRung);
|
|
5358
|
+
if (!thin) {
|
|
5359
|
+
console.log(`[dario] #${requestCount} credits rung: the request body could not be re-pointed at ${creditsRung.model}`);
|
|
5360
|
+
creditsRung = null;
|
|
5361
|
+
res.removeHeader('x-dario-pool-fallback');
|
|
5362
|
+
return false;
|
|
5363
|
+
}
|
|
5364
|
+
finalBody = thin;
|
|
5365
|
+
ccToolMap = null;
|
|
5366
|
+
preserveToolsEffective = true;
|
|
5367
|
+
bodyIdentityOwned = false;
|
|
5368
|
+
for (const k of Object.keys(headers))
|
|
5369
|
+
delete headers[k];
|
|
5370
|
+
Object.assign(headers, creditsHeaders(creditsApiKey, clientBeta, clientAnthropicVersion));
|
|
5371
|
+
return true;
|
|
5372
|
+
};
|
|
5195
5373
|
dispatchLoop: while (true) {
|
|
5196
5374
|
// Reorder outbound headers to match CC's captured header sequence
|
|
5197
5375
|
// when the live template recorded one. No-op on bundled-only installs.
|
|
5198
5376
|
// Skipped in passthrough mode — passthrough means "don't shape the
|
|
5199
5377
|
// request to look like CC," and reordering is a form of shaping.
|
|
5200
|
-
|
|
5378
|
+
// Skipped on the credits rung too: a plain API request has no CC order.
|
|
5379
|
+
const outboundHeaders = (passthrough || creditsRung) ? headers : orderHeadersForOutbound(headers);
|
|
5201
5380
|
fetchStartedAt ??= Date.now();
|
|
5202
5381
|
upstream = await upstreamFetch(targetBase, {
|
|
5203
5382
|
method: req.method ?? 'POST',
|
|
@@ -5287,7 +5466,7 @@ export async function startProxy(opts = {}) {
|
|
|
5287
5466
|
}
|
|
5288
5467
|
}
|
|
5289
5468
|
if (betaRejectedFlags.length > 0 && recoveryPasses < MAX_RECOVERY_PASSES) {
|
|
5290
|
-
const acctKey =
|
|
5469
|
+
const acctKey = accountKeyNow();
|
|
5291
5470
|
let set = unavailableBetas.get(acctKey);
|
|
5292
5471
|
if (!set) {
|
|
5293
5472
|
set = new Set();
|
|
@@ -5500,7 +5679,7 @@ export async function startProxy(opts = {}) {
|
|
|
5500
5679
|
else if (isLongContextError && recoveryPasses < MAX_RECOVERY_PASSES) {
|
|
5501
5680
|
// Cache the rejection so future requests on this account skip
|
|
5502
5681
|
// context-1m up front instead of re-paying the 400/429 round-trip.
|
|
5503
|
-
const acctKey =
|
|
5682
|
+
const acctKey = accountKeyNow();
|
|
5504
5683
|
const firstRejection = !context1mUnavailable.has(acctKey);
|
|
5505
5684
|
context1mUnavailable.add(acctKey);
|
|
5506
5685
|
if (verbose && firstRejection)
|
|
@@ -5549,12 +5728,20 @@ export async function startProxy(opts = {}) {
|
|
|
5549
5728
|
// The Claude half is out of quota for this request AND for the next
|
|
5550
5729
|
// one — cool it here, honouring the upstream `retry-after` when it
|
|
5551
5730
|
// sent one, so the following request does not re-walk the chain.
|
|
5552
|
-
if (await familyFallbackOn429(upstream))
|
|
5731
|
+
if (!creditsRung && await familyFallbackOn429(upstream))
|
|
5553
5732
|
return;
|
|
5554
5733
|
attemptedProviders.add('claude');
|
|
5555
|
-
|
|
5556
|
-
if (!
|
|
5557
|
-
|
|
5734
|
+
// A 429 from the credits key is the key's limit, not the pool's.
|
|
5735
|
+
if (!creditsRung)
|
|
5736
|
+
providerCooldowns.note('claude', parseRetryAfterMs(upstream.headers.get('retry-after')));
|
|
5737
|
+
if (!pinnedAccount) {
|
|
5738
|
+
const outcome = await attemptPoolFallbackOn429(req, res, body, isOpenAI, attemptedProviders, { queueMs, arrivedAt }, rearmOnCredits);
|
|
5739
|
+
if (outcome === 'served')
|
|
5740
|
+
return;
|
|
5741
|
+
if (outcome === 'credits') {
|
|
5742
|
+
peekedBody = null;
|
|
5743
|
+
continue dispatchLoop;
|
|
5744
|
+
}
|
|
5558
5745
|
}
|
|
5559
5746
|
if (allProvidersCooled(['codex', 'claude'], providerCooldowns)) {
|
|
5560
5747
|
writeAllProvidersRateLimited(['codex', 'claude']);
|
|
@@ -5582,10 +5769,10 @@ export async function startProxy(opts = {}) {
|
|
|
5582
5769
|
analytics.record({
|
|
5583
5770
|
timestamp: Date.now(),
|
|
5584
5771
|
consumer,
|
|
5585
|
-
account:
|
|
5772
|
+
account: accountKeyNow(),
|
|
5586
5773
|
model: requestModel,
|
|
5587
5774
|
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0, thinkingTokens: 0,
|
|
5588
|
-
claim: rl
|
|
5775
|
+
claim: claimNow(rl), util5h: rl.util5h, util7d: rl.util7d, overageUtil: rl.overageUtil,
|
|
5589
5776
|
latencyMs: Date.now() - startTime, status: 429, isStream: false, isOpenAI, timing: timingNow(),
|
|
5590
5777
|
});
|
|
5591
5778
|
}
|
|
@@ -5609,6 +5796,9 @@ export async function startProxy(opts = {}) {
|
|
|
5609
5796
|
}
|
|
5610
5797
|
const rejection = classifyUpstreamRejection(upstream.status, peekedBody);
|
|
5611
5798
|
responseHeaders['x-dario-upstream-rejection'] = rejection.marker;
|
|
5799
|
+
if (creditsRung) {
|
|
5800
|
+
console.error(`[dario] #${requestCount} credits rung refused (${upstream.status}, ${rejection.marker}) as ${creditsRung.model}: the key may be out of credits or not linked to the plan's organization; the upstream error is forwarded`);
|
|
5801
|
+
}
|
|
5612
5802
|
requestCount++;
|
|
5613
5803
|
res.writeHead(upstream.status, responseHeaders);
|
|
5614
5804
|
res.end(peekedBody);
|
|
@@ -5622,6 +5812,12 @@ export async function startProxy(opts = {}) {
|
|
|
5622
5812
|
// headers, so headroom math sees a healthy idle account. Mark the
|
|
5623
5813
|
// cool-down here, try the next-best account, fall through to the
|
|
5624
5814
|
// normal forwarding only if no peer is available.
|
|
5815
|
+
// On the credits rung there is no seat to cool and no peer to try: a
|
|
5816
|
+
// 401/403 from the key is the key's own answer and is forwarded as is
|
|
5817
|
+
// (the forwarder below reuses the body the classifier read).
|
|
5818
|
+
if (creditsRung && (upstream.status === 401 || upstream.status === 403)) {
|
|
5819
|
+
console.error(`[dario] #${requestCount} credits rung refused (${upstream.status}) as ${creditsRung.model}: the key was not accepted (invalid, revoked, or without permission for the model); the upstream error is forwarded`);
|
|
5820
|
+
}
|
|
5625
5821
|
if (poolAccount && (upstream.status === 401 || upstream.status === 403)) {
|
|
5626
5822
|
pool.markAuthFailure(poolAccount.alias);
|
|
5627
5823
|
if (verbose) {
|
|
@@ -5667,12 +5863,21 @@ export async function startProxy(opts = {}) {
|
|
|
5667
5863
|
// into body. See attemptPoolFallbackOn429's doc comment for why this
|
|
5668
5864
|
// is one shared function rather than an inline copy.
|
|
5669
5865
|
// Same bookkeeping as the other mid-flight site — see there.
|
|
5670
|
-
if (await familyFallbackOn429(upstream))
|
|
5866
|
+
if (!creditsRung && await familyFallbackOn429(upstream))
|
|
5671
5867
|
return;
|
|
5672
5868
|
attemptedProviders.add('claude');
|
|
5673
|
-
|
|
5674
|
-
if (!
|
|
5675
|
-
|
|
5869
|
+
// A 429 from the credits key is the key's limit, not the pool's.
|
|
5870
|
+
if (!creditsRung)
|
|
5871
|
+
providerCooldowns.note('claude', parseRetryAfterMs(upstream.headers.get('retry-after')));
|
|
5872
|
+
if (!pinnedAccount) {
|
|
5873
|
+
const outcome = await attemptPoolFallbackOn429(req, res, body, isOpenAI, attemptedProviders, { queueMs, arrivedAt }, rearmOnCredits);
|
|
5874
|
+
if (outcome === 'served')
|
|
5875
|
+
return;
|
|
5876
|
+
if (outcome === 'credits') {
|
|
5877
|
+
// This site has not read the 429's body; release it before the retry.
|
|
5878
|
+
void upstream.body?.cancel().catch(() => { });
|
|
5879
|
+
continue dispatchLoop;
|
|
5880
|
+
}
|
|
5676
5881
|
}
|
|
5677
5882
|
if (allProvidersCooled(['codex', 'claude'], providerCooldowns)) {
|
|
5678
5883
|
writeAllProvidersRateLimited(['codex', 'claude']);
|
|
@@ -5696,10 +5901,10 @@ export async function startProxy(opts = {}) {
|
|
|
5696
5901
|
analytics.record({
|
|
5697
5902
|
timestamp: Date.now(),
|
|
5698
5903
|
consumer,
|
|
5699
|
-
account:
|
|
5904
|
+
account: accountKeyNow(),
|
|
5700
5905
|
model: requestModel,
|
|
5701
5906
|
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0, thinkingTokens: 0,
|
|
5702
|
-
claim: rl
|
|
5907
|
+
claim: claimNow(rl), util5h: rl.util5h, util7d: rl.util7d, overageUtil: rl.overageUtil,
|
|
5703
5908
|
latencyMs: Date.now() - startTime, status: 429, isStream: false, isOpenAI, timing: timingNow(),
|
|
5704
5909
|
});
|
|
5705
5910
|
}
|
|
@@ -5733,6 +5938,11 @@ export async function startProxy(opts = {}) {
|
|
|
5733
5938
|
responseHeaders[key] = value;
|
|
5734
5939
|
}
|
|
5735
5940
|
}
|
|
5941
|
+
// A classified rejection that reached the plain forwarder (no seat to
|
|
5942
|
+
// fail over from) is marked the way the seat branches mark theirs.
|
|
5943
|
+
if (peekedBody !== null && upstream.status >= 400) {
|
|
5944
|
+
responseHeaders['x-dario-upstream-rejection'] = classifyUpstreamRejection(upstream.status, peekedBody).marker;
|
|
5945
|
+
}
|
|
5736
5946
|
requestCount++;
|
|
5737
5947
|
// Log billing classification on first request or in verbose mode.
|
|
5738
5948
|
//
|
|
@@ -5749,7 +5959,10 @@ export async function startProxy(opts = {}) {
|
|
|
5749
5959
|
const billingClaim = upstream.headers.get('anthropic-ratelimit-unified-representative-claim');
|
|
5750
5960
|
const overageUtil = upstream.headers.get('anthropic-ratelimit-unified-overage-utilization');
|
|
5751
5961
|
if (requestCount === 1 || verbose) {
|
|
5752
|
-
if (
|
|
5962
|
+
if (creditsRung) {
|
|
5963
|
+
console.log(`[dario] #${requestCount} billing: credits (${CREDITS_CLAIM}, x-api-key as ${creditsRung.model})`);
|
|
5964
|
+
}
|
|
5965
|
+
else if (billingClaim) {
|
|
5753
5966
|
let overagePct;
|
|
5754
5967
|
if (overageUtil !== null) {
|
|
5755
5968
|
overagePct = `${Math.round(parseFloat(overageUtil) * 100)}%`;
|
|
@@ -5996,19 +6209,19 @@ export async function startProxy(opts = {}) {
|
|
|
5996
6209
|
// would read, and using their (often zero) output_tokens would
|
|
5997
6210
|
// pin think time to baseMs+jitter on the next request needlessly.
|
|
5998
6211
|
if (upstream.status >= 200 && upstream.status < 300) {
|
|
5999
|
-
pacingClocks.noteResponse(
|
|
6212
|
+
pacingClocks.noteResponse(accountKeyNow(), Date.now(), streamOutputTokens);
|
|
6000
6213
|
}
|
|
6001
6214
|
{
|
|
6002
6215
|
const rl = poolAccount?.rateLimit ?? parseRateLimits(upstream.headers);
|
|
6003
6216
|
analytics.record({
|
|
6004
6217
|
timestamp: Date.now(),
|
|
6005
6218
|
consumer,
|
|
6006
|
-
account:
|
|
6219
|
+
account: accountKeyNow(),
|
|
6007
6220
|
model: requestModel,
|
|
6008
6221
|
inputTokens: streamInputTokens, outputTokens: streamOutputTokens,
|
|
6009
6222
|
cacheReadTokens: streamCacheReadTokens, cacheCreateTokens: streamCacheCreateTokens,
|
|
6010
6223
|
thinkingTokens: Math.round(streamThinkingChars / 4),
|
|
6011
|
-
claim: rl
|
|
6224
|
+
claim: claimNow(rl), util5h: rl.util5h, util7d: rl.util7d, overageUtil: rl.overageUtil,
|
|
6012
6225
|
latencyMs: Date.now() - startTime, status: upstream.status, isStream: true, isOpenAI, timing: timingNow(),
|
|
6013
6226
|
continuation: continuationOf(guard, requestDepth),
|
|
6014
6227
|
});
|
|
@@ -6020,9 +6233,9 @@ export async function startProxy(opts = {}) {
|
|
|
6020
6233
|
status: upstream.status, latency_ms: Date.now() - startTime, ...timingLogFields(timingNow()),
|
|
6021
6234
|
in_tokens: streamInputTokens, out_tokens: streamOutputTokens,
|
|
6022
6235
|
cache_read: streamCacheReadTokens, cache_create: streamCacheCreateTokens,
|
|
6023
|
-
claim: poolAccount?.rateLimit.claim,
|
|
6024
|
-
bucket: poolAccount ? billingBucketFromClaim(poolAccount.rateLimit.claim) : undefined,
|
|
6025
|
-
account: poolAccount?.alias,
|
|
6236
|
+
claim: creditsRung ? CREDITS_CLAIM : poolAccount?.rateLimit.claim,
|
|
6237
|
+
bucket: creditsRung ? 'credits' : (poolAccount ? billingBucketFromClaim(poolAccount.rateLimit.claim) : undefined),
|
|
6238
|
+
account: creditsRung ? ACCOUNT_KEY_CREDITS : poolAccount?.alias,
|
|
6026
6239
|
consumer,
|
|
6027
6240
|
client: detectedClientForLog,
|
|
6028
6241
|
preserve_tools: preserveToolsEffective,
|
|
@@ -6037,8 +6250,13 @@ export async function startProxy(opts = {}) {
|
|
|
6037
6250
|
}, consumer));
|
|
6038
6251
|
}
|
|
6039
6252
|
else {
|
|
6040
|
-
// Buffer and forward
|
|
6041
|
-
|
|
6253
|
+
// Buffer and forward. A 400/402/403 body the rejection classifier
|
|
6254
|
+
// already read is in peekedBody and the stream is consumed; reading
|
|
6255
|
+
// it again would hand the client an empty body with the right
|
|
6256
|
+
// status. Only responses no branch above handled reach here: a
|
|
6257
|
+
// non-billing 403 with no seat to fail over from (the credits rung,
|
|
6258
|
+
// ANTHROPIC_UPSTREAM_API_KEY), a generic 400 or 402.
|
|
6259
|
+
let responseBody = peekedBody ?? await upstream.text();
|
|
6042
6260
|
upstreamDoneAt = Date.now();
|
|
6043
6261
|
// Reverse tool name mapping so client sees original names
|
|
6044
6262
|
if (ccToolMap)
|
|
@@ -6066,7 +6284,7 @@ export async function startProxy(opts = {}) {
|
|
|
6066
6284
|
// tokens when the body wasn't JSON or had no usage block — base +
|
|
6067
6285
|
// jitter still apply but the per-token component is 0.
|
|
6068
6286
|
if (upstream.status >= 200 && upstream.status < 300) {
|
|
6069
|
-
pacingClocks.noteResponse(
|
|
6287
|
+
pacingClocks.noteResponse(accountKeyNow(), Date.now(), bufferedUsage?.outputTokens ?? 0);
|
|
6070
6288
|
}
|
|
6071
6289
|
if (bufferedUsage) {
|
|
6072
6290
|
try {
|
|
@@ -6074,12 +6292,12 @@ export async function startProxy(opts = {}) {
|
|
|
6074
6292
|
analytics.record({
|
|
6075
6293
|
timestamp: Date.now(),
|
|
6076
6294
|
consumer,
|
|
6077
|
-
account:
|
|
6295
|
+
account: accountKeyNow(),
|
|
6078
6296
|
model: bufferedUsage.model || requestModel,
|
|
6079
6297
|
inputTokens: bufferedUsage.inputTokens, outputTokens: bufferedUsage.outputTokens,
|
|
6080
6298
|
cacheReadTokens: bufferedUsage.cacheReadTokens, cacheCreateTokens: bufferedUsage.cacheCreateTokens,
|
|
6081
6299
|
thinkingTokens: bufferedUsage.thinkingTokens,
|
|
6082
|
-
claim: rl
|
|
6300
|
+
claim: claimNow(rl), util5h: rl.util5h, util7d: rl.util7d, overageUtil: rl.overageUtil,
|
|
6083
6301
|
latencyMs: Date.now() - startTime, status: upstream.status, isStream: false, isOpenAI, timing: timingNow(),
|
|
6084
6302
|
});
|
|
6085
6303
|
}
|
|
@@ -6092,9 +6310,9 @@ export async function startProxy(opts = {}) {
|
|
|
6092
6310
|
status: upstream.status, latency_ms: Date.now() - startTime, ...timingLogFields(timingNow()),
|
|
6093
6311
|
in_tokens: bufferedUsage?.inputTokens, out_tokens: bufferedUsage?.outputTokens,
|
|
6094
6312
|
cache_read: bufferedUsage?.cacheReadTokens, cache_create: bufferedUsage?.cacheCreateTokens,
|
|
6095
|
-
claim: poolAccount?.rateLimit.claim,
|
|
6096
|
-
bucket: poolAccount ? billingBucketFromClaim(poolAccount.rateLimit.claim) : undefined,
|
|
6097
|
-
account: poolAccount?.alias,
|
|
6313
|
+
claim: creditsRung ? CREDITS_CLAIM : poolAccount?.rateLimit.claim,
|
|
6314
|
+
bucket: creditsRung ? 'credits' : (poolAccount ? billingBucketFromClaim(poolAccount.rateLimit.claim) : undefined),
|
|
6315
|
+
account: creditsRung ? ACCOUNT_KEY_CREDITS : poolAccount?.alias,
|
|
6098
6316
|
consumer,
|
|
6099
6317
|
client: detectedClientForLog,
|
|
6100
6318
|
preserve_tools: preserveToolsEffective,
|