@askalf/dario 6.5.0 → 6.6.1
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 +17 -3
- package/dist/analytics.d.ts +72 -3
- package/dist/analytics.js +82 -8
- package/dist/cli.js +74 -4
- package/dist/ledger.d.ts +175 -0
- package/dist/ledger.js +411 -0
- package/dist/midstream.d.ts +21 -0
- package/dist/midstream.js +29 -4
- package/dist/proxy.d.ts +17 -0
- package/dist/proxy.js +53 -3
- package/dist/tui/tabs/analytics.d.ts +8 -0
- package/dist/tui/tabs/analytics.js +11 -2
- package/docs/api-equivalent-spend.md +116 -0
- package/docs/midstream-continuation.md +26 -0
- package/package.json +1 -1
package/dist/proxy.js
CHANGED
|
@@ -16,6 +16,7 @@ import { AccountPool, computeStickyKey, parseRateLimits, modelFamily, isInAuthCo
|
|
|
16
16
|
import { backfillIdentity } from './accounts.js';
|
|
17
17
|
import { PoolSync, DEFAULT_POOL_SYNC_INTERVAL_MS } from './pool-sync.js';
|
|
18
18
|
import { Analytics, billingBucketFromClaim, formatUsageLogLine, SUBSCRIPTION_CLAIMS, consumerFromHeader, consumerFromBody, CONSUMER_HEADER, CODEX_CLAIM } from './analytics.js';
|
|
19
|
+
import { Ledger, resolveLedgerPath, ledgerDisabledByEnv } from './ledger.js';
|
|
19
20
|
import { OverageGuard, buildHaltErrorBody } from './overage-guard.js';
|
|
20
21
|
import { notify as osNotify } from './notify.js';
|
|
21
22
|
import { grantAge, grantThresholds, worstGrantLevel, describeGrantAge } from './refresh-grant.js';
|
|
@@ -26,7 +27,19 @@ import { createTokenBucket } from './rate-limit.js';
|
|
|
26
27
|
import { getOpenAIBackend, isOpenAIModel, forwardToOpenAI } from './openai-backend.js';
|
|
27
28
|
import { forwardToCodex, forwardResponsesToCodex, getCodexModelSlugs, peekCodexModelSlugs, isCodexModel, pickCodexFallback, pickClaudeTarget, CODEX_BACKEND_BASE_URL } from './codex-backend.js';
|
|
28
29
|
import { effortForCodex } from './effort.js';
|
|
29
|
-
import { MidstreamGuard, guardFor, loopbackBaseFor, chaosCutFetch, chaosCutState, CONTINUATION_HEADER, MAX_CONTINUATION_DEPTH, continuationDepth } from './midstream.js';
|
|
30
|
+
import { MidstreamGuard, guardFor, loopbackBaseFor, chaosCutFetch, chaosCutState, CONTINUATION_HEADER, CONTINUATION_OF_HEADER, MAX_CONTINUATION_DEPTH, continuationDepth, continuationOfRequest } from './midstream.js';
|
|
31
|
+
/**
|
|
32
|
+
* The continuation fields for a request's analytics row, from its guard.
|
|
33
|
+
* Only the client's own request (depth 0) carries them: a resume leg is a
|
|
34
|
+
* loopback request with a guard of its own, and counting its attempt too
|
|
35
|
+
* would show one dying client stream as two. The log line still carries
|
|
36
|
+
* every leg's outcome — that is where the hop-by-hop story is read.
|
|
37
|
+
*/
|
|
38
|
+
function continuationOf(guard, depth) {
|
|
39
|
+
if (!guard || !guard.outcome || depth > 0)
|
|
40
|
+
return undefined;
|
|
41
|
+
return { outcome: guard.outcome, ...(guard.continuedBy ? { by: guard.continuedBy } : {}), partialChars: guard.partialChars };
|
|
42
|
+
}
|
|
30
43
|
import { responsesRequestToAnthropic, unsupportedOnClaudeError, ResponsesRequestError, ResponsesOut, wrapResponsesClient } from './responses-inbound.js';
|
|
31
44
|
import { isClaudeServableModel } from './claude-model.js';
|
|
32
45
|
import { MODEL_UNROUTABLE } from './upstream-rejection.js';
|
|
@@ -1445,6 +1458,19 @@ export async function startProxy(opts = {}) {
|
|
|
1445
1458
|
// : null` — that gated the /analytics endpoint, but burn-rate /
|
|
1446
1459
|
// per-request visibility is useful for a pool of one too.
|
|
1447
1460
|
const analytics = new Analytics();
|
|
1461
|
+
// The lifetime ledger rides the same record stream analytics emits, so
|
|
1462
|
+
// every site that records a request feeds it without a second call. Off,
|
|
1463
|
+
// /analytics reports `lifetime: null`.
|
|
1464
|
+
const ledgerOn = opts.ledger !== false && !ledgerDisabledByEnv();
|
|
1465
|
+
const ledger = ledgerOn ? await Ledger.open(resolveLedgerPath(port), (line) => console.log(line)) : null;
|
|
1466
|
+
if (ledger) {
|
|
1467
|
+
analytics.on('record', (r) => { ledger.add(r); });
|
|
1468
|
+
if (verbose)
|
|
1469
|
+
console.log(`[dario] ledger: ${ledger.path}`);
|
|
1470
|
+
}
|
|
1471
|
+
else {
|
|
1472
|
+
console.log('[dario] ledger: disabled (--no-ledger)');
|
|
1473
|
+
}
|
|
1448
1474
|
// Per-alias request counts for GET /codex — the pool has requestCount per
|
|
1449
1475
|
// account; the codex accounts had nothing until now.
|
|
1450
1476
|
const codexRequestCounts = new Map();
|
|
@@ -2569,7 +2595,19 @@ export async function startProxy(opts = {}) {
|
|
|
2569
2595
|
// `queue` rides along the summary (dario#905): request-queue.ts always
|
|
2570
2596
|
// documented snapshot() as "exposed for /analytics", but it was never
|
|
2571
2597
|
// actually wired in, so slot exhaustion was invisible from outside.
|
|
2572
|
-
res.end(JSON.stringify({ ...analytics.summary(), queue: queue.snapshot() }));
|
|
2598
|
+
res.end(JSON.stringify({ ...analytics.summary(), queue: queue.snapshot(), lifetime: ledger ? ledger.summary() : null }));
|
|
2599
|
+
return;
|
|
2600
|
+
}
|
|
2601
|
+
// The ledger's per-day table, for anyone charting it. `lifetime` on
|
|
2602
|
+
// /analytics is the summary; this is the data behind it.
|
|
2603
|
+
if (urlPath === '/analytics/ledger' && req.method === 'GET') {
|
|
2604
|
+
if (!ledger) {
|
|
2605
|
+
res.writeHead(404, JSON_HEADERS);
|
|
2606
|
+
res.end(JSON.stringify({ error: 'ledger disabled', hint: 'start without --no-ledger / DARIO_LEDGER=0' }));
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2609
|
+
res.writeHead(200, JSON_HEADERS);
|
|
2610
|
+
res.end(JSON.stringify({ path: ledger.path, ...ledger.snapshot() }));
|
|
2573
2611
|
return;
|
|
2574
2612
|
}
|
|
2575
2613
|
// Analytics live stream — SSE of new RequestRecord JSON, one event
|
|
@@ -3103,6 +3141,12 @@ export async function startProxy(opts = {}) {
|
|
|
3103
3141
|
// never continued itself (MAX_CONTINUATION_DEPTH).
|
|
3104
3142
|
const requestDepth = continuationDepth(req.headers[CONTINUATION_HEADER]);
|
|
3105
3143
|
const isContinuation = requestDepth >= MAX_CONTINUATION_DEPTH;
|
|
3144
|
+
// A resume leg's log row points back at the request whose stream died
|
|
3145
|
+
// (its guard's number) and says how deep it sits, so the hop-by-hop
|
|
3146
|
+
// story is readable from the log alone. Empty on a client request.
|
|
3147
|
+
const continuationLeg = requestDepth > 0
|
|
3148
|
+
? { continuation_depth: requestDepth, continuation_of: continuationOfRequest(req.headers[CONTINUATION_OF_HEADER]) }
|
|
3149
|
+
: {};
|
|
3106
3150
|
/**
|
|
3107
3151
|
* First hop: the SAME model again, through the front door. The pool
|
|
3108
3152
|
* picks a seat (sticky binding keeps the prompt cache warm), and if the
|
|
@@ -3624,6 +3668,7 @@ export async function startProxy(opts = {}) {
|
|
|
3624
3668
|
// overage guard (#288) leaves it alone.
|
|
3625
3669
|
claim: CODEX_CLAIM, util5h: 0, util7d: 0, overageUtil: 0,
|
|
3626
3670
|
latencyMs: o.latencyMs, status: o.status, isStream: o.stream, isOpenAI,
|
|
3671
|
+
continuation: continuationOf(codexGuard, requestDepth),
|
|
3627
3672
|
});
|
|
3628
3673
|
writeLogLine(logFileStream, {
|
|
3629
3674
|
ts: new Date().toISOString(), req: codexReq,
|
|
@@ -3631,6 +3676,8 @@ export async function startProxy(opts = {}) {
|
|
|
3631
3676
|
status: o.status, latency_ms: o.latencyMs, in_tokens: o.inputTokens, out_tokens: o.outputTokens,
|
|
3632
3677
|
cache_read: o.cacheReadTokens, cache_create: o.cacheCreateTokens,
|
|
3633
3678
|
claim: CODEX_CLAIM, bucket: 'subscription', account: o.alias, consumer, stream: o.stream,
|
|
3679
|
+
...(codexGuard?.outcome ? { continued: codexGuard.outcome, continued_by: codexGuard.continuedBy ?? undefined, continued_after: codexGuard.partialChars } : {}),
|
|
3680
|
+
...continuationLeg,
|
|
3634
3681
|
});
|
|
3635
3682
|
if (verbose)
|
|
3636
3683
|
console.log(formatUsageLogLine(codexReq, {
|
|
@@ -5156,6 +5203,7 @@ export async function startProxy(opts = {}) {
|
|
|
5156
5203
|
thinkingTokens: Math.round(streamThinkingChars / 4),
|
|
5157
5204
|
claim: rl.claim, util5h: rl.util5h, util7d: rl.util7d, overageUtil: rl.overageUtil,
|
|
5158
5205
|
latencyMs: Date.now() - startTime, status: upstream.status, isStream: true, isOpenAI,
|
|
5206
|
+
continuation: continuationOf(guard, requestDepth),
|
|
5159
5207
|
});
|
|
5160
5208
|
}
|
|
5161
5209
|
writeLogLine(logFileStream, {
|
|
@@ -5172,6 +5220,8 @@ export async function startProxy(opts = {}) {
|
|
|
5172
5220
|
client: detectedClientForLog,
|
|
5173
5221
|
preserve_tools: preserveToolsEffective,
|
|
5174
5222
|
stream: true,
|
|
5223
|
+
...(guard?.outcome ? { continued: guard.outcome, continued_by: guard.continuedBy ?? undefined, continued_after: guard.partialChars } : {}),
|
|
5224
|
+
...continuationLeg,
|
|
5175
5225
|
});
|
|
5176
5226
|
if (verbose)
|
|
5177
5227
|
console.log(formatUsageLogLine(requestCount, {
|
|
@@ -5532,7 +5582,7 @@ export async function startProxy(opts = {}) {
|
|
|
5532
5582
|
// Flush tokens first (best-effort, bounded), then close the server. The
|
|
5533
5583
|
// flush is fire-and-forget under the same 5s force-exit guard below so a
|
|
5534
5584
|
// hung fsync can't wedge shutdown.
|
|
5535
|
-
void flushPoolTokens().finally(() => {
|
|
5585
|
+
void Promise.all([flushPoolTokens(), ledger?.close()]).finally(() => {
|
|
5536
5586
|
server.close(() => process.exit(0));
|
|
5537
5587
|
});
|
|
5538
5588
|
// Force exit after 5s if connections (or the flush) don't complete.
|
|
@@ -43,6 +43,14 @@ interface SummaryShape {
|
|
|
43
43
|
currentUtil7d: number;
|
|
44
44
|
lastClaim: string;
|
|
45
45
|
}>;
|
|
46
|
+
/** The ledger's lifetime view (v6.6); null when the proxy runs --no-ledger, absent on older proxies. */
|
|
47
|
+
lifetime?: {
|
|
48
|
+
apiEquivalentCost: number;
|
|
49
|
+
since: string;
|
|
50
|
+
recent: {
|
|
51
|
+
today: number;
|
|
52
|
+
};
|
|
53
|
+
} | null;
|
|
46
54
|
}
|
|
47
55
|
export interface AnalyticsState {
|
|
48
56
|
summary: SummaryShape | null;
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { fg, dim, brand, progressBar, pad, truncate } from '../render.js';
|
|
15
15
|
import { renderKvRow } from '../layout.js';
|
|
16
16
|
import { fitPanels } from '../panels.js';
|
|
17
|
+
import { formatUsd } from '../../ledger.js';
|
|
17
18
|
const POLL_INTERVAL_MS = 2000;
|
|
18
19
|
/**
|
|
19
20
|
* Label column for the gauge rows (5h / 7d / Overage). Was 6, which is
|
|
@@ -97,13 +98,21 @@ export const AnalyticsTab = {
|
|
|
97
98
|
counters.push(' ' + renderKvRow('Thinking tokens', formatNumber(s.window.totalThinkingTokens), w - 4));
|
|
98
99
|
counters.push(' ' + renderKvRow('Avg latency', `${Math.round(s.window.avgLatencyMs)}ms`, w - 4));
|
|
99
100
|
counters.push(' ' + renderKvRow('Subscription %', `${s.window.subscriptionPercent.toFixed(0)}%`, w - 4));
|
|
100
|
-
//
|
|
101
|
+
// The ledger's number: what everything since the first request would
|
|
102
|
+
// have been billed on the metered API. Lifetime, not the window.
|
|
103
|
+
const lifetimeRow = s.lifetime
|
|
104
|
+
? ' ' + renderKvRow('API-equivalent', `${formatUsd(s.lifetime.apiEquivalentCost)} ${dim(`lifetime, ${formatUsd(s.lifetime.recent.today)} today, since ${s.lifetime.since.slice(0, 10)}`)}`, w - 4)
|
|
105
|
+
: null;
|
|
106
|
+
if (lifetimeRow)
|
|
107
|
+
counters.push(lifetimeRow);
|
|
108
|
+
// Headline numbers — the ones that answer "is this costing me money?"
|
|
101
109
|
// survive as the collapsed form.
|
|
102
110
|
panels.push({
|
|
103
111
|
lines: counters,
|
|
104
112
|
collapsed: ['',
|
|
105
113
|
' ' + renderKvRow('Requests', `${s.window.requests} ${dim(`(${rpm.toFixed(1)}/min)`)}`, w - 4),
|
|
106
|
-
' ' + renderKvRow('Subscription %', `${s.window.subscriptionPercent.toFixed(0)}%`, w - 4)
|
|
114
|
+
' ' + renderKvRow('Subscription %', `${s.window.subscriptionPercent.toFixed(0)}%`, w - 4),
|
|
115
|
+
...(lifetimeRow ? [lifetimeRow] : [])],
|
|
107
116
|
priority: 1,
|
|
108
117
|
});
|
|
109
118
|
// ── Per-model bars ─────────────────────────────────────────
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# API-equivalent spend — the ledger
|
|
2
|
+
|
|
3
|
+
What the traffic dario has served would have cost on the metered API, kept
|
|
4
|
+
across restarts. `dario usage` opens with it; `/analytics` carries it as
|
|
5
|
+
`lifetime`; the TUI's Analytics tab shows it as **API-equivalent**.
|
|
6
|
+
|
|
7
|
+
## Why a file
|
|
8
|
+
|
|
9
|
+
`/analytics` is a rolling in-memory window: 10k records, gone on restart. It
|
|
10
|
+
answers "what is this costing me right now" and could never answer the
|
|
11
|
+
question a subscription user actually has — what has this saved me since I
|
|
12
|
+
set it up. The ledger is the persistent half. It is deliberately small: one
|
|
13
|
+
row per UTC day, per model, per billing bucket, holding a request count and
|
|
14
|
+
the four token buckets (input, output, cache read, cache write). Nothing else.
|
|
15
|
+
|
|
16
|
+
It never stores a price. Rows are priced when read, at the rate in effect on
|
|
17
|
+
the row's day, from the same tables the rolling window uses (`PRICING` for
|
|
18
|
+
Claude, `OPENAI_PRICING` for the ChatGPT leg, both in `src/analytics.ts`).
|
|
19
|
+
Pricing has been wrong twice in this repo (#1047, #1048); a stored dollar
|
|
20
|
+
figure would have frozen the wrong number in, a stored token count gets
|
|
21
|
+
repriced the moment the table is fixed.
|
|
22
|
+
|
|
23
|
+
## What counts
|
|
24
|
+
|
|
25
|
+
Only responses with a 2xx status. A 429 carries no tokens and a 5xx bills
|
|
26
|
+
nothing.
|
|
27
|
+
|
|
28
|
+
Two columns per row:
|
|
29
|
+
|
|
30
|
+
- **covered** — served against a subscription: every Anthropic subscription
|
|
31
|
+
claim (`five_hour`, `seven_day`, their `_fallback` and `_overage_included`
|
|
32
|
+
forms), the ChatGPT leg, and a 2xx that carried no claim at all (a stream
|
|
33
|
+
cut before the headers were read, api-key mode without the header). The
|
|
34
|
+
API-equivalent cost of this column is the headline — the invoice that never
|
|
35
|
+
arrived.
|
|
36
|
+
- **metered** — billed per token anyway: an API key upstream (`api`) or
|
|
37
|
+
Anthropic's paid overage (`overage` → `extra_usage`). That money was spent.
|
|
38
|
+
It is reported on its own line ("Paid per token on top") and never counted
|
|
39
|
+
as saved.
|
|
40
|
+
|
|
41
|
+
A mid-stream continuation (6.1) is two upstream requests and records as two
|
|
42
|
+
rows, one per provider, each with the tokens that leg actually consumed.
|
|
43
|
+
|
|
44
|
+
## Where it lives
|
|
45
|
+
|
|
46
|
+
`~/.dario/ledger.json` for a proxy on the default port; `ledger-<port>.json`
|
|
47
|
+
for any other, so two instances sharing a home (a live-test rig next to
|
|
48
|
+
production) do not overwrite each other. `DARIO_LEDGER_PATH=<file>` moves it;
|
|
49
|
+
`--no-ledger` / `DARIO_LEDGER=0` turns it off, after which `/analytics`
|
|
50
|
+
reports `lifetime: null`, `/analytics/ledger` is a 404 and `dario usage` says
|
|
51
|
+
so.
|
|
52
|
+
|
|
53
|
+
Writes are debounced (3 s after the last record) and durable — temp file,
|
|
54
|
+
fsync, rename, directory fsync, the same path the credential store uses
|
|
55
|
+
(#790). The shutdown hook flushes what the debounce still holds, so a
|
|
56
|
+
`docker rm -f` loses at most the last few seconds. A file that will not parse
|
|
57
|
+
is moved aside as `ledger.json.corrupt-<ts>` and the ledger starts fresh; it
|
|
58
|
+
is never overwritten in place. Days past 730 roll off the front.
|
|
59
|
+
|
|
60
|
+
The test suite pins `DARIO_LEDGER_PATH` to a temp directory, so a suite run
|
|
61
|
+
does not add stub traffic to the operator's file.
|
|
62
|
+
|
|
63
|
+
## Reading it
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
$ dario usage
|
|
67
|
+
API-equivalent spend (since 2026-09-11, 3 days, 1,515 requests):
|
|
68
|
+
$413 would have been billed on the metered API — covered by subscriptions
|
|
69
|
+
Claude $388 1,204 reqs (Opus 5 $301 · Sonnet 5 $86.68)
|
|
70
|
+
ChatGPT $24.75 311 reqs (gpt-5.6-terra $24.75)
|
|
71
|
+
Today $48.20 · Last 7d $413 · Last 30d $413
|
|
72
|
+
Paid per token on top (API key / extra usage): $1.10
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The proxy's view is preferred (it holds records the debounce has not flushed
|
|
76
|
+
yet); with no proxy on the port, the command reads the file the proxy on that
|
|
77
|
+
port would write. `--card[=file.svg]` renders the headline as a 640×320 SVG
|
|
78
|
+
(default `dario-api-equivalent.svg`) — plain system monospace, nothing to
|
|
79
|
+
fetch, so it looks the same in a README and a screenshot. `--json` is the raw
|
|
80
|
+
`/analytics` payload, `lifetime` included.
|
|
81
|
+
|
|
82
|
+
`GET /analytics` → `lifetime`:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"path": "/root/.dario/ledger.json",
|
|
87
|
+
"since": "2026-09-11T02:14:09.000Z",
|
|
88
|
+
"days": 3,
|
|
89
|
+
"requests": 1515,
|
|
90
|
+
"apiEquivalentCost": 412.87,
|
|
91
|
+
"meteredCost": 1.1,
|
|
92
|
+
"tokens": { "input": 1204000, "output": 388000, "cacheRead": 91200000, "cacheCreate": 4100000 },
|
|
93
|
+
"perProvider": { "anthropic": { "requests": 1204, "apiEquivalentCost": 388.12 }, "openai": { "requests": 311, "apiEquivalentCost": 24.75 } },
|
|
94
|
+
"perModel": { "claude-opus-5": { "provider": "anthropic", "requests": 900, "apiEquivalentCost": 301.44, "meteredCost": 1.1, "...": "token totals" } },
|
|
95
|
+
"recent": { "today": 48.2, "last7d": 412.87, "last30d": 412.87 }
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`GET /analytics/ledger` is the file itself: `{ path, version, since, updated,
|
|
100
|
+
days: { "YYYY-MM-DD": { "<model>": { covered: {…}, metered: {…} } } } }`.
|
|
101
|
+
|
|
102
|
+
## Pricing on the ChatGPT leg
|
|
103
|
+
|
|
104
|
+
Before 6.6 a `gpt-*` row fell through to the Claude fallback rate and the
|
|
105
|
+
"would-be API cost" of a ChatGPT-plan request was Anthropic's Sonnet 4.6
|
|
106
|
+
price for a model Anthropic does not sell. `OPENAI_PRICING` carries OpenAI's
|
|
107
|
+
published standard-tier rates for the models the codex backend serves, read
|
|
108
|
+
off `developers.openai.com/api/docs/pricing` on 2026-09-11. OpenAI charges
|
|
109
|
+
nothing to write a cache entry, so cache-write tokens (which the codex path
|
|
110
|
+
never reports anyway) are priced at the input rate. Unknown `gpt-*` ids take
|
|
111
|
+
gpt-5.6-terra's rate, dario's default codex model.
|
|
112
|
+
|
|
113
|
+
`scripts/check-pricing-drift.mjs` watches the Claude table against
|
|
114
|
+
Anthropic's page. Nothing watches the OpenAI table yet — an entry that is
|
|
115
|
+
correct today goes wrong the moment OpenAI changes it, and the only signal
|
|
116
|
+
would be this number moving.
|
|
@@ -161,6 +161,32 @@ its own answer; the other subscription takes over only when that model cannot
|
|
|
161
161
|
serve the resume. dario warns loudly at startup while the tap is set; it is a
|
|
162
162
|
demo and test affordance, never a default.
|
|
163
163
|
|
|
164
|
+
## Seeing it after the fact
|
|
165
|
+
|
|
166
|
+
A continuation leaves three traces. The SSE comment on the wire
|
|
167
|
+
(`: dario continuation gpt-5.6-terra (codex live) after 1204 chars`) is the
|
|
168
|
+
one a raw capture shows; every SSE parser ignores it. The request log
|
|
169
|
+
(`--log-file`) tells the hop-by-hop story: the row of a request whose stream
|
|
170
|
+
died carries `continued` (`continued`, `continued-unfinished`,
|
|
171
|
+
`resume-failed`, `no-target`), `continued_by` (the leg that served the rest)
|
|
172
|
+
and `continued_after` (characters the client already had); every resume leg
|
|
173
|
+
has a row of its own with `continuation_depth` (1, or 2 for the resume of a
|
|
174
|
+
resume) and `continuation_of`, the number of the request it resumed — and,
|
|
175
|
+
when it died too, its own `continued_*` fields. A two-hop resume is three
|
|
176
|
+
rows that point at each other. `/analytics` tallies per window under
|
|
177
|
+
`continuations`: `attempted`, split into `finished`, `unfinished`, `failed`
|
|
178
|
+
and `noTarget`. Only the client's own request counts there — a resume leg is
|
|
179
|
+
a loopback request with a guard of its own, and counting its attempt too
|
|
180
|
+
would show one dying stream as two. `dario usage` prints the tally as one
|
|
181
|
+
line when anything died:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
Continuations: 3 streams died mid-answer: 2 finished, 1 unfinished
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`noTarget` above zero is the line to act on: streams are dying and there is
|
|
188
|
+
no `--pool-fallback` entry for the other provider to finish them.
|
|
189
|
+
|
|
164
190
|
## How it was proven
|
|
165
191
|
|
|
166
192
|
`test/midstream-continuation-wiring.mjs` runs a real proxy against a fake
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.6.1",
|
|
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": {
|