@askalf/dario 3.31.17 → 3.31.18
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.js +8 -1
- package/dist/doctor.d.ts +10 -0
- package/dist/doctor.js +129 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -652,6 +652,12 @@ async function help() {
|
|
|
652
652
|
the server's verdict — the single reliable
|
|
653
653
|
signal for scope-policy drift (dario#42/#71
|
|
654
654
|
class). One GET to claude.ai; no PII.
|
|
655
|
+
dario doctor --usage Fire one minimal Haiku request through your
|
|
656
|
+
OAuth and surface the rate-limit snapshot:
|
|
657
|
+
All-models 5h/7d, per-model 7d buckets
|
|
658
|
+
(Sonnet only, Opus only when Anthropic ships
|
|
659
|
+
them), overage. Mirrors the user-dashboard
|
|
660
|
+
usage page. Costs ~1 subscription request.
|
|
655
661
|
dario doctor --json Emit the check report as structured JSON
|
|
656
662
|
for machine consumption (claude-bridge
|
|
657
663
|
/status, CI scripts, etc.) instead of the
|
|
@@ -972,6 +978,7 @@ async function mcp() {
|
|
|
972
978
|
async function doctor() {
|
|
973
979
|
const { runChecks, formatChecks, formatChecksJson, exitCodeFor, runAuthCheck } = await import('./doctor.js');
|
|
974
980
|
const probe = args.includes('--probe');
|
|
981
|
+
const usage = args.includes('--usage');
|
|
975
982
|
const asJson = args.includes('--json');
|
|
976
983
|
const authCheck = args.includes('--auth-check');
|
|
977
984
|
if (authCheck) {
|
|
@@ -1008,7 +1015,7 @@ async function doctor() {
|
|
|
1008
1015
|
console.log('');
|
|
1009
1016
|
process.exit(result.verdict === 'match' ? 0 : 1);
|
|
1010
1017
|
}
|
|
1011
|
-
const checks = await runChecks({ probe });
|
|
1018
|
+
const checks = await runChecks({ probe, usage });
|
|
1012
1019
|
if (asJson) {
|
|
1013
1020
|
// JSON mode is meant for machine consumption (claude-bridge /status,
|
|
1014
1021
|
// deepdive health checks, CI scripts) — no decorative header, no
|
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.18",
|
|
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": {
|