@askalf/dario 3.11.0 → 3.11.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.
@@ -23,6 +23,26 @@ export interface RequestRecord {
23
23
  isStream: boolean;
24
24
  isOpenAI: boolean;
25
25
  }
26
+ /**
27
+ * The four billing buckets a request can land in, derived from the
28
+ * `anthropic-ratelimit-unified-representative-claim` response header.
29
+ *
30
+ * - `subscription` — request billed against the user's 5h subscription window (Max/Pro)
31
+ * - `subscription_fallback` — server-side fallback subscription bucket (rare, still covered)
32
+ * - `extra_usage` — overage / pay-as-you-go, paid on top of subscription
33
+ * - `api` — pure API key billing, no subscription involved
34
+ * - `unknown` — header absent or unparseable (non-200 responses, stream aborts)
35
+ *
36
+ * Exposed in `/analytics` summaries and in verbose per-request logs so
37
+ * users can see at a glance which bucket their traffic is actually hitting.
38
+ * See #34 for background.
39
+ */
40
+ export type BillingBucket = 'subscription' | 'subscription_fallback' | 'extra_usage' | 'api' | 'unknown';
41
+ /**
42
+ * Map the raw `representative-claim` header value to a human-friendly
43
+ * billing bucket. Pure function; no state; safe to call from any context.
44
+ */
45
+ export declare function billingBucketFromClaim(claim: string | null | undefined): BillingBucket;
26
46
  export declare class Analytics {
27
47
  private records;
28
48
  private maxRecords;
@@ -60,27 +80,30 @@ interface PerModelStat {
60
80
  avgThinkingTokens: number;
61
81
  estimatedCost: number;
62
82
  }
83
+ interface WindowStats {
84
+ totalInputTokens: number;
85
+ totalOutputTokens: number;
86
+ totalThinkingTokens: number;
87
+ estimatedCost: number;
88
+ avgLatencyMs: number;
89
+ errorRate: number;
90
+ claimBreakdown: Record<string, number>;
91
+ /** Count of requests in each derived billing bucket. See #34. */
92
+ billingBucketBreakdown: Record<BillingBucket, number>;
93
+ /**
94
+ * Percentage of *classified* requests (non-unknown) that hit a
95
+ * subscription bucket. The headline number for "is dario routing me
96
+ * through my subscription?" — should be 100% for a clean setup. See #34.
97
+ */
98
+ subscriptionPercent: number;
99
+ }
63
100
  export interface AnalyticsSummary {
64
- window: {
101
+ window: WindowStats & {
65
102
  minutes: number;
66
103
  requests: number;
67
- totalInputTokens: number;
68
- totalOutputTokens: number;
69
- totalThinkingTokens: number;
70
- estimatedCost: number;
71
- avgLatencyMs: number;
72
- errorRate: number;
73
- claimBreakdown: Record<string, number>;
74
104
  };
75
- allTime: {
105
+ allTime: WindowStats & {
76
106
  requests: number;
77
- totalInputTokens: number;
78
- totalOutputTokens: number;
79
- totalThinkingTokens: number;
80
- estimatedCost: number;
81
- avgLatencyMs: number;
82
- errorRate: number;
83
- claimBreakdown: Record<string, number>;
84
107
  };
85
108
  perAccount: Record<string, PerAccountStat>;
86
109
  perModel: Record<string, PerModelStat>;
package/dist/analytics.js CHANGED
@@ -5,6 +5,24 @@
5
5
  * In-memory rolling window; exposed via the /analytics endpoint when
6
6
  * pool mode is active.
7
7
  */
8
+ /**
9
+ * Map the raw `representative-claim` header value to a human-friendly
10
+ * billing bucket. Pure function; no state; safe to call from any context.
11
+ */
12
+ export function billingBucketFromClaim(claim) {
13
+ switch (claim) {
14
+ case 'five_hour':
15
+ return 'subscription';
16
+ case 'five_hour_fallback':
17
+ return 'subscription_fallback';
18
+ case 'overage':
19
+ return 'extra_usage';
20
+ case 'api':
21
+ return 'api';
22
+ default:
23
+ return 'unknown';
24
+ }
25
+ }
8
26
  // Anthropic pricing (per 1M tokens, USD). Not authoritative — used for
9
27
  // rough burn-rate display in the /analytics summary.
10
28
  const PRICING = {
@@ -74,6 +92,14 @@ export class Analytics {
74
92
  totalInputTokens: 0, totalOutputTokens: 0, totalThinkingTokens: 0,
75
93
  estimatedCost: 0, avgLatencyMs: 0, errorRate: 0,
76
94
  claimBreakdown: {},
95
+ billingBucketBreakdown: {
96
+ subscription: 0,
97
+ subscription_fallback: 0,
98
+ extra_usage: 0,
99
+ api: 0,
100
+ unknown: 0,
101
+ },
102
+ subscriptionPercent: 0,
77
103
  };
78
104
  }
79
105
  const totalInput = records.reduce((s, r) => s + r.inputTokens, 0);
@@ -83,9 +109,22 @@ export class Analytics {
83
109
  const avgLatency = records.reduce((s, r) => s + r.latencyMs, 0) / records.length;
84
110
  const errors = records.filter(r => r.status >= 400).length;
85
111
  const claims = {};
112
+ const buckets = {
113
+ subscription: 0,
114
+ subscription_fallback: 0,
115
+ extra_usage: 0,
116
+ api: 0,
117
+ unknown: 0,
118
+ };
86
119
  for (const r of records) {
87
120
  claims[r.claim] = (claims[r.claim] ?? 0) + 1;
121
+ buckets[billingBucketFromClaim(r.claim)]++;
88
122
  }
123
+ const subscriptionHits = buckets.subscription + buckets.subscription_fallback;
124
+ const billedRequests = records.length - buckets.unknown;
125
+ const subscriptionPct = billedRequests > 0
126
+ ? Math.round((subscriptionHits / billedRequests) * 10000) / 100
127
+ : 0;
89
128
  return {
90
129
  totalInputTokens: totalInput,
91
130
  totalOutputTokens: totalOutput,
@@ -94,6 +133,8 @@ export class Analytics {
94
133
  avgLatencyMs: Math.round(avgLatency),
95
134
  errorRate: Math.round((errors / records.length) * 10000) / 10000,
96
135
  claimBreakdown: claims,
136
+ billingBucketBreakdown: buckets,
137
+ subscriptionPercent: subscriptionPct,
97
138
  };
98
139
  }
99
140
  perAccountStats(records) {
package/dist/proxy.js CHANGED
@@ -8,7 +8,7 @@ import { arch, platform } from 'node:process';
8
8
  import { getAccessToken, getStatus } from './oauth.js';
9
9
  import { buildCCRequest, reverseMapResponse, createStreamingReverseMapper } from './cc-template.js';
10
10
  import { AccountPool, parseRateLimits } from './pool.js';
11
- import { Analytics } from './analytics.js';
11
+ import { Analytics, billingBucketFromClaim } from './analytics.js';
12
12
  import { loadAllAccounts, loadAccount, refreshAccountToken } from './accounts.js';
13
13
  import { getOpenAIBackend, isOpenAIModel, forwardToOpenAI } from './openai-backend.js';
14
14
  const ANTHROPIC_API = 'https://api.anthropic.com';
@@ -1039,7 +1039,13 @@ export async function startProxy(opts = {}) {
1039
1039
  else {
1040
1040
  overagePct = 'n/a';
1041
1041
  }
1042
- console.log(`[dario] #${requestCount} billing: ${billingClaim} (overage: ${overagePct})`);
1042
+ // Show the derived billing bucket as the headline, with the raw
1043
+ // claim value in parens so power users still see the header as-is.
1044
+ // See #34 — users want "am I actually on subscription?" answered
1045
+ // at a glance instead of having to memorize that `five_hour` means
1046
+ // "yes, subscription."
1047
+ const bucket = billingBucketFromClaim(billingClaim);
1048
+ console.log(`[dario] #${requestCount} billing: ${bucket} (${billingClaim}, overage: ${overagePct})`);
1043
1049
  }
1044
1050
  else if (verbose) {
1045
1051
  console.log(`[dario] #${requestCount} billing: headers absent (status=${upstream.status})`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "3.11.0",
3
+ "version": "3.11.1",
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": {
@@ -21,7 +21,7 @@
21
21
  ],
22
22
  "scripts": {
23
23
  "build": "tsc && cp src/cc-template-data.json dist/",
24
- "test": "node test/issue-29-tool-translation.mjs && node test/hybrid-tools.mjs && node test/scrub-paths.mjs && node test/provider-prefix.mjs && node test/analytics-recording.mjs && node test/failover-429.mjs && node test/live-fingerprint.mjs",
24
+ "test": "node test/issue-29-tool-translation.mjs && node test/hybrid-tools.mjs && node test/scrub-paths.mjs && node test/provider-prefix.mjs && node test/analytics-recording.mjs && node test/analytics-billing-bucket.mjs && node test/failover-429.mjs && node test/live-fingerprint.mjs",
25
25
  "audit": "npm audit --production --audit-level=high",
26
26
  "prepublishOnly": "npm run build",
27
27
  "start": "node dist/cli.js",