@askalf/dario 3.31.17 → 3.31.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +24 -0
- package/dist/cli.js +47 -9
- package/dist/doctor.d.ts +10 -0
- package/dist/doctor.js +129 -0
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
|
@@ -48,3 +48,27 @@ export declare function parseBooleanEnv(value: string | undefined): boolean | un
|
|
|
48
48
|
* Set(['thinking','env']) — value "thinking,env" → preserve listed
|
|
49
49
|
*/
|
|
50
50
|
export declare function resolvePreserveOrchestrationTags(args: string[], env: string | undefined): Set<string> | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Decide whether this module is being invoked as the CLI entry point or
|
|
53
|
+
* imported as a library. Pure, exported for tests; the file-bottom uses
|
|
54
|
+
* it with `process.argv[1]` + `import.meta.url` + `fs.realpathSync`.
|
|
55
|
+
*
|
|
56
|
+
* The pre-v3.31.19 implementation was a strict string compare —
|
|
57
|
+
* import.meta.url === pathToFileURL(process.argv[1]).href
|
|
58
|
+
* — which silently failed on every npm-global install because the bin
|
|
59
|
+
* shim path (e.g. `/usr/local/bin/dario`) is a symlink to `dist/cli.js`.
|
|
60
|
+
* `argv[1]` arrived as the *symlink* path while `import.meta.url`
|
|
61
|
+
* resolved through the symlink to the real file. They never matched,
|
|
62
|
+
* the guard returned false, and the entire CLI body was gated out —
|
|
63
|
+
* `dario doctor`, `dario proxy`, every command produced zero output and
|
|
64
|
+
* exited 0. Reported as dario#143 by @tetsuco.
|
|
65
|
+
*
|
|
66
|
+
* The fix: also check the symlink-resolved path. `realpathSync`
|
|
67
|
+
* canonicalizes the argv[1] symlink into the same on-disk path that
|
|
68
|
+
* `import.meta.url` already represents, so a global-install bin-shim
|
|
69
|
+
* invocation matches. Direct invocation (`node dist/cli.js`) still
|
|
70
|
+
* matches via the first leg. Test-side imports of named exports still
|
|
71
|
+
* don't match either leg, which preserves the original purpose of the
|
|
72
|
+
* guard from #137 (v3.31.15).
|
|
73
|
+
*/
|
|
74
|
+
export declare function isMainEntry(argv1: string | undefined | null, moduleHref: string, realpath?: (p: string) => string): boolean;
|
package/dist/cli.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// just want `parsePositiveIntEnv`) doesn't trigger a Bun relaunch or any
|
|
18
18
|
// other startup side effect.
|
|
19
19
|
import { unlink } from 'node:fs/promises';
|
|
20
|
+
import { realpathSync } from 'node:fs';
|
|
20
21
|
import { join } from 'node:path';
|
|
21
22
|
import { homedir } from 'node:os';
|
|
22
23
|
import { pathToFileURL } from 'node:url';
|
|
@@ -652,6 +653,12 @@ async function help() {
|
|
|
652
653
|
the server's verdict — the single reliable
|
|
653
654
|
signal for scope-policy drift (dario#42/#71
|
|
654
655
|
class). One GET to claude.ai; no PII.
|
|
656
|
+
dario doctor --usage Fire one minimal Haiku request through your
|
|
657
|
+
OAuth and surface the rate-limit snapshot:
|
|
658
|
+
All-models 5h/7d, per-model 7d buckets
|
|
659
|
+
(Sonnet only, Opus only when Anthropic ships
|
|
660
|
+
them), overage. Mirrors the user-dashboard
|
|
661
|
+
usage page. Costs ~1 subscription request.
|
|
655
662
|
dario doctor --json Emit the check report as structured JSON
|
|
656
663
|
for machine consumption (claude-bridge
|
|
657
664
|
/status, CI scripts, etc.) instead of the
|
|
@@ -972,6 +979,7 @@ async function mcp() {
|
|
|
972
979
|
async function doctor() {
|
|
973
980
|
const { runChecks, formatChecks, formatChecksJson, exitCodeFor, runAuthCheck } = await import('./doctor.js');
|
|
974
981
|
const probe = args.includes('--probe');
|
|
982
|
+
const usage = args.includes('--usage');
|
|
975
983
|
const asJson = args.includes('--json');
|
|
976
984
|
const authCheck = args.includes('--auth-check');
|
|
977
985
|
if (authCheck) {
|
|
@@ -1008,7 +1016,7 @@ async function doctor() {
|
|
|
1008
1016
|
console.log('');
|
|
1009
1017
|
process.exit(result.verdict === 'match' ? 0 : 1);
|
|
1010
1018
|
}
|
|
1011
|
-
const checks = await runChecks({ probe });
|
|
1019
|
+
const checks = await runChecks({ probe, usage });
|
|
1012
1020
|
if (asJson) {
|
|
1013
1021
|
// JSON mode is meant for machine consumption (claude-bridge /status,
|
|
1014
1022
|
// deepdive health checks, CI scripts) — no decorative header, no
|
|
@@ -1147,14 +1155,44 @@ const commands = {
|
|
|
1147
1155
|
'--version': version,
|
|
1148
1156
|
'-V': version,
|
|
1149
1157
|
};
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
+
/**
|
|
1159
|
+
* Decide whether this module is being invoked as the CLI entry point or
|
|
1160
|
+
* imported as a library. Pure, exported for tests; the file-bottom uses
|
|
1161
|
+
* it with `process.argv[1]` + `import.meta.url` + `fs.realpathSync`.
|
|
1162
|
+
*
|
|
1163
|
+
* The pre-v3.31.19 implementation was a strict string compare —
|
|
1164
|
+
* import.meta.url === pathToFileURL(process.argv[1]).href
|
|
1165
|
+
* — which silently failed on every npm-global install because the bin
|
|
1166
|
+
* shim path (e.g. `/usr/local/bin/dario`) is a symlink to `dist/cli.js`.
|
|
1167
|
+
* `argv[1]` arrived as the *symlink* path while `import.meta.url`
|
|
1168
|
+
* resolved through the symlink to the real file. They never matched,
|
|
1169
|
+
* the guard returned false, and the entire CLI body was gated out —
|
|
1170
|
+
* `dario doctor`, `dario proxy`, every command produced zero output and
|
|
1171
|
+
* exited 0. Reported as dario#143 by @tetsuco.
|
|
1172
|
+
*
|
|
1173
|
+
* The fix: also check the symlink-resolved path. `realpathSync`
|
|
1174
|
+
* canonicalizes the argv[1] symlink into the same on-disk path that
|
|
1175
|
+
* `import.meta.url` already represents, so a global-install bin-shim
|
|
1176
|
+
* invocation matches. Direct invocation (`node dist/cli.js`) still
|
|
1177
|
+
* matches via the first leg. Test-side imports of named exports still
|
|
1178
|
+
* don't match either leg, which preserves the original purpose of the
|
|
1179
|
+
* guard from #137 (v3.31.15).
|
|
1180
|
+
*/
|
|
1181
|
+
export function isMainEntry(argv1, moduleHref, realpath = realpathSync) {
|
|
1182
|
+
if (typeof argv1 !== 'string' || argv1.length === 0)
|
|
1183
|
+
return false;
|
|
1184
|
+
if (moduleHref === pathToFileURL(argv1).href)
|
|
1185
|
+
return true;
|
|
1186
|
+
try {
|
|
1187
|
+
return moduleHref === pathToFileURL(realpath(argv1)).href;
|
|
1188
|
+
}
|
|
1189
|
+
catch {
|
|
1190
|
+
return false;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
// Main-entry guard. Only run the Bun auto-relaunch and handler dispatch
|
|
1194
|
+
// when this module is the direct CLI entry point.
|
|
1195
|
+
const isDirectEntry = isMainEntry(process.argv[1], import.meta.url);
|
|
1158
1196
|
if (isDirectEntry) {
|
|
1159
1197
|
// Bun auto-relaunch for TLS fingerprint fidelity. Only meaningful when
|
|
1160
1198
|
// dario is the direct entry — if we're imported, whoever imported us
|
package/dist/doctor.d.ts
CHANGED
|
@@ -50,6 +50,16 @@ export interface RunChecksOptions {
|
|
|
50
50
|
* GET to `claude.ai` and runs in parallel with the other checks.
|
|
51
51
|
*/
|
|
52
52
|
probe?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Opt-in: fire a minimal `POST /v1/messages` through the user's OAuth
|
|
55
|
+
* (Haiku, `max_tokens=1`) to capture the current rate-limit snapshot,
|
|
56
|
+
* including the unified buckets AND the per-model buckets Anthropic
|
|
57
|
+
* started carving in late April 2026 (`7d_sonnet-utilization` etc).
|
|
58
|
+
* Surfaces "All models X%, Sonnet only Y%" the way the user dashboard
|
|
59
|
+
* does. Enable with `dario doctor --usage`; costs ~1 subscription
|
|
60
|
+
* request.
|
|
61
|
+
*/
|
|
62
|
+
usage?: boolean;
|
|
53
63
|
}
|
|
54
64
|
/**
|
|
55
65
|
* Run every available health check. Never throws — each check is
|
package/dist/doctor.js
CHANGED
|
@@ -282,6 +282,135 @@ export async function runChecks(opts = {}) {
|
|
|
282
282
|
});
|
|
283
283
|
}
|
|
284
284
|
}
|
|
285
|
+
// ---- Usage snapshot (opt-in, --usage).
|
|
286
|
+
// Fires one `POST /v1/messages` via the loaded OAuth (Haiku, max_tokens=1)
|
|
287
|
+
// to capture the current rate-limit snapshot including the per-model
|
|
288
|
+
// buckets Anthropic started carving around 2026-04-25. Surfaces the
|
|
289
|
+
// `All models` vs `Sonnet only` split the way the user dashboard does.
|
|
290
|
+
// Direct-to-Anthropic, not through the proxy — the proxy doesn't need
|
|
291
|
+
// to be running for `dario doctor --usage`.
|
|
292
|
+
if (opts.usage) {
|
|
293
|
+
try {
|
|
294
|
+
const { parseRateLimits } = await import('./pool.js');
|
|
295
|
+
const { billingBucketFromClaim } = await import('./analytics.js');
|
|
296
|
+
// Probe routing decision: Anthropic's subscription path rejects
|
|
297
|
+
// non-CC-shaped requests on Sonnet/Opus (returns 429 with no
|
|
298
|
+
// rate-limit headers). Haiku accepts the raw shape. So:
|
|
299
|
+
// - If a local `dario proxy` is listening, route through it —
|
|
300
|
+
// the proxy injects the full CC template and all three families
|
|
301
|
+
// succeed, giving us the _sonnet / _opus / _haiku per-model
|
|
302
|
+
// bucket headers on a single round trip each.
|
|
303
|
+
// - Else fall back to direct-to-Anthropic with Haiku only.
|
|
304
|
+
// Unified buckets surface but per-model buckets won't.
|
|
305
|
+
const dario_base = process.env.DARIO_TEST_URL || 'http://127.0.0.1:3456';
|
|
306
|
+
let probeEndpoint = `${dario_base}/v1/messages`;
|
|
307
|
+
let probeHeaders = {
|
|
308
|
+
'content-type': 'application/json',
|
|
309
|
+
'anthropic-version': '2023-06-01',
|
|
310
|
+
'authorization': 'Bearer dario',
|
|
311
|
+
};
|
|
312
|
+
let proxyAvailable = false;
|
|
313
|
+
try {
|
|
314
|
+
const healthRes = await fetch(`${dario_base}/health`, { signal: AbortSignal.timeout(800) });
|
|
315
|
+
proxyAvailable = healthRes.ok;
|
|
316
|
+
}
|
|
317
|
+
catch { /* proxy not running */ }
|
|
318
|
+
if (!proxyAvailable) {
|
|
319
|
+
const { getAccessToken } = await import('./oauth.js');
|
|
320
|
+
const token = await getAccessToken();
|
|
321
|
+
probeEndpoint = 'https://api.anthropic.com/v1/messages';
|
|
322
|
+
probeHeaders = {
|
|
323
|
+
'content-type': 'application/json',
|
|
324
|
+
'anthropic-version': '2023-06-01',
|
|
325
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
326
|
+
'authorization': `Bearer ${token}`,
|
|
327
|
+
};
|
|
328
|
+
checks.push({
|
|
329
|
+
status: 'info',
|
|
330
|
+
label: 'Usage probe',
|
|
331
|
+
detail: 'dario proxy not running — probing direct. Per-model buckets visible only when probing through a running proxy (start `dario proxy` in another terminal and re-run).',
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
// Probe each family in parallel. Anthropic only returns the
|
|
335
|
+
// per-model 7d bucket header on a request TO that family.
|
|
336
|
+
const families = [
|
|
337
|
+
{ family: 'haiku', model: 'claude-haiku-4-5' },
|
|
338
|
+
{ family: 'sonnet', model: 'claude-sonnet-4-6' },
|
|
339
|
+
{ family: 'opus', model: 'claude-opus-4-7' },
|
|
340
|
+
];
|
|
341
|
+
const probe = async (model) => {
|
|
342
|
+
const res = await fetch(probeEndpoint, {
|
|
343
|
+
method: 'POST',
|
|
344
|
+
headers: probeHeaders,
|
|
345
|
+
body: JSON.stringify({
|
|
346
|
+
model,
|
|
347
|
+
max_tokens: 1,
|
|
348
|
+
messages: [{ role: 'user', content: 'ok' }],
|
|
349
|
+
}),
|
|
350
|
+
signal: AbortSignal.timeout(15_000),
|
|
351
|
+
});
|
|
352
|
+
// Consume the body so the socket releases; we only care about headers.
|
|
353
|
+
await res.text().catch(() => '');
|
|
354
|
+
// Ignore 429/4xx snapshots without useful rate-limit headers.
|
|
355
|
+
if (!res.headers.get('anthropic-ratelimit-unified-status'))
|
|
356
|
+
return null;
|
|
357
|
+
return parseRateLimits(res.headers);
|
|
358
|
+
};
|
|
359
|
+
const results = await Promise.all(families.map(f => probe(f.model).catch(() => null)));
|
|
360
|
+
// Use the first non-null snapshot for the unified view — they
|
|
361
|
+
// should all agree on the unified buckets (same account, same moment).
|
|
362
|
+
const firstOk = results.find(s => s !== null);
|
|
363
|
+
if (!firstOk)
|
|
364
|
+
throw new Error('all probe requests failed');
|
|
365
|
+
const bucket = billingBucketFromClaim(firstOk.claim);
|
|
366
|
+
const pct = (n) => `${(n * 100).toFixed(1)}%`;
|
|
367
|
+
checks.push({
|
|
368
|
+
status: firstOk.util5h >= 0.90 ? 'warn' : 'ok',
|
|
369
|
+
label: 'Usage 5h (all)',
|
|
370
|
+
detail: `${pct(firstOk.util5h)} used • status=${firstOk.status} • claim=${firstOk.claim} (${bucket})`,
|
|
371
|
+
});
|
|
372
|
+
checks.push({
|
|
373
|
+
status: firstOk.util7d >= 0.90 ? 'warn' : 'ok',
|
|
374
|
+
label: 'Usage 7d (all)',
|
|
375
|
+
detail: `${pct(firstOk.util7d)} used`,
|
|
376
|
+
});
|
|
377
|
+
// Merge per-model buckets across all probes — each probe's response
|
|
378
|
+
// carries at most its own family bucket; union them for display.
|
|
379
|
+
const mergedPerModel = {};
|
|
380
|
+
for (const s of results) {
|
|
381
|
+
if (!s)
|
|
382
|
+
continue;
|
|
383
|
+
for (const [family, util] of Object.entries(s.perModel7d)) {
|
|
384
|
+
mergedPerModel[family] = util;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
for (const [family, util] of Object.entries(mergedPerModel).sort()) {
|
|
388
|
+
const divergence = util - firstOk.util7d;
|
|
389
|
+
const marker = Math.abs(divergence) > 0.05
|
|
390
|
+
? ` • Δ vs 7d(all): ${divergence >= 0 ? '+' : ''}${(divergence * 100).toFixed(1)}pp`
|
|
391
|
+
: '';
|
|
392
|
+
checks.push({
|
|
393
|
+
status: util >= 0.90 ? 'warn' : 'ok',
|
|
394
|
+
label: `Usage 7d (${family} only)`,
|
|
395
|
+
detail: `${pct(util)} used${marker}`,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
if (firstOk.overageUtil > 0) {
|
|
399
|
+
checks.push({
|
|
400
|
+
status: firstOk.overageUtil >= 0.90 ? 'warn' : 'info',
|
|
401
|
+
label: 'Usage overage',
|
|
402
|
+
detail: `${pct(firstOk.overageUtil)} of configured monthly spend`,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
checks.push({
|
|
408
|
+
status: 'warn',
|
|
409
|
+
label: 'Usage snapshot',
|
|
410
|
+
detail: `probe failed: ${err.message}`,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
285
414
|
// ---- Account pool
|
|
286
415
|
try {
|
|
287
416
|
const { listAccountAliases, loadAllAccounts } = await import('./accounts.js');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "3.31.
|
|
3
|
+
"version": "3.31.19",
|
|
4
4
|
"description": "A local LLM router. One endpoint, every provider — Claude subscriptions, OpenAI, OpenRouter, Groq, local LiteLLM, any OpenAI-compat endpoint — your tools don't need to change.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|