@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 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.17",
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": {