@inferhub/usage 0.1.1 → 0.1.3

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.
@@ -22,15 +22,13 @@ try {
22
22
  }
23
23
 
24
24
  try {
25
- // Prefer Claude's real session_id so /v1/me/usage can aggregate this conversation.
26
- const sessionId =
27
- (typeof session?.session_id === "string" && session.session_id) ||
28
- (typeof session?.sessionId === "string" && session.sessionId) ||
29
- undefined;
30
-
25
+ // Do NOT pass Claude's session_id through: InferHub derives its own conversation
26
+ // key server-side (hash of system+first user message / X-Session-Id). Claude's
27
+ // UUID almost never matches, which produced "sess 0r/0". Default API path uses
28
+ // the user's latest InferHub session.
31
29
  const { usage } = await client.fetchAccountUsageAuto({
32
30
  window: "day",
33
- sessionId,
31
+ force: false,
34
32
  });
35
33
  const contextPct =
36
34
  typeof session?.context_window?.used_percentage === "number"
package/dist/format.d.ts CHANGED
@@ -5,14 +5,14 @@ export declare function shortModel(model: string | null | undefined): string;
5
5
  /**
6
6
  * Compact one-liner for Claude statusline / Codex Stop summary.
7
7
  *
8
- * Shows InferHub-billed figures only. Claude's local `cost.total_cost_usd` is
9
- * official list-price estimate and is NOT shown unless showEstimate=true.
8
+ * Plain-language labels. Free models correctly show $0 billed while still
9
+ * reporting request/token activity.
10
10
  */
11
11
  export declare function formatStatusLine(usage: AccountUsage, extras?: {
12
12
  sessionCostUsd?: number | null;
13
13
  contextPct?: number | null;
14
14
  model?: string | null;
15
- /** When true, include Claude's local cost estimate as `est $…`. Default false. */
15
+ /** When true, include Claude local list-price estimate. Default false. */
16
16
  showEstimate?: boolean;
17
17
  }): string;
18
18
  /** Multi-line human report for /usage skills. */
package/dist/format.js CHANGED
@@ -20,38 +20,60 @@ export function shortModel(model) {
20
20
  const parts = model.split("/");
21
21
  return parts[parts.length - 1] || model;
22
22
  }
23
- function spendOrActivity(spendUsdc, requests, totalTokens) {
24
- const spend = Number(spendUsdc ?? 0);
23
+ /** Human activity + optional billed spend. */
24
+ function activity(label, opts) {
25
+ const req = Number(opts.requests ?? 0);
26
+ const tok = Number(opts.totalTokens ?? 0);
27
+ const spend = Number(opts.spendUsdc ?? 0);
28
+ const bits = [];
29
+ if (req > 0 || tok > 0) {
30
+ bits.push(`${req.toLocaleString()} req`);
31
+ bits.push(`${tokens(tok)} tok`);
32
+ }
33
+ else {
34
+ bits.push("no traffic");
35
+ }
25
36
  if (spend > 0)
26
- return money(spend);
27
- return `${requests}r/${tokens(totalTokens)}`;
37
+ bits.push(money(spend));
38
+ else
39
+ bits.push("$0");
40
+ return `${label} ${bits.join(" / ")}`;
28
41
  }
29
42
  /**
30
43
  * Compact one-liner for Claude statusline / Codex Stop summary.
31
44
  *
32
- * Shows InferHub-billed figures only. Claude's local `cost.total_cost_usd` is
33
- * official list-price estimate and is NOT shown unless showEstimate=true.
45
+ * Plain-language labels. Free models correctly show $0 billed while still
46
+ * reporting request/token activity.
34
47
  */
35
48
  export function formatStatusLine(usage, extras) {
36
- const sess = usage.session
37
- ? spendOrActivity(usage.session.spend_usdc, usage.session.requests, usage.session.total_tokens)
38
- : null;
39
- const day = spendOrActivity(usage.window.spend_usdc, usage.window.requests, usage.window.total_tokens);
40
- const all = money(usage.all_time.spend_usdc);
49
+ const model = extras?.model ? shortModel(extras.model) : null;
41
50
  const bal = money(usage.balance.amount_usdc);
42
51
  const top = shortModel(usage.top_model?.model);
43
- const model = extras?.model ? shortModel(extras.model) : null;
44
52
  const ctx = extras?.contextPct != null && Number.isFinite(extras.contextPct)
45
- ? `${Math.round(extras.contextPct)}%ctx`
53
+ ? `${Math.round(extras.contextPct)}% context`
46
54
  : null;
47
55
  const parts = ["InferHub"];
48
56
  if (model)
49
57
  parts.push(model);
50
- if (sess)
51
- parts.push(`sess ${sess}`);
52
- parts.push(`${usage.window.kind} ${day}`);
53
- parts.push(`all ${all}`);
54
- parts.push(`bal ${bal}`);
58
+ // Latest conversation on InferHub (server-derived session), if present and useful
59
+ if (usage.session && (usage.session.requests > 0 || Number(usage.session.spend_usdc) > 0)) {
60
+ parts.push(activity("session", {
61
+ spendUsdc: usage.session.spend_usdc,
62
+ requests: usage.session.requests,
63
+ totalTokens: usage.session.total_tokens,
64
+ }));
65
+ }
66
+ parts.push(activity("today", {
67
+ spendUsdc: usage.window.spend_usdc,
68
+ requests: usage.window.requests,
69
+ totalTokens: usage.window.total_tokens,
70
+ }));
71
+ parts.push(activity("all-time", {
72
+ spendUsdc: usage.all_time.spend_usdc,
73
+ requests: usage.all_time.requests,
74
+ totalTokens: usage.all_time.total_tokens,
75
+ }));
76
+ parts.push(`balance ${bal}`);
55
77
  if (top && top !== "—")
56
78
  parts.push(`top ${top}`);
57
79
  if (ctx)
@@ -60,21 +82,21 @@ export function formatStatusLine(usage, extras) {
60
82
  extras.sessionCostUsd != null &&
61
83
  Number.isFinite(extras.sessionCostUsd) &&
62
84
  extras.sessionCostUsd > 0) {
63
- parts.push(`est ${money(extras.sessionCostUsd)}`);
85
+ parts.push(`claude-est ${money(extras.sessionCostUsd)}`);
64
86
  }
65
87
  return parts.join(" · ");
66
88
  }
67
89
  /** Multi-line human report for /usage skills. */
68
90
  export function formatReport(usage) {
69
91
  const lines = [
70
- "InferHub usage",
71
- ` Balance: ${money(usage.balance.amount_usdc)} USDC`,
92
+ "InferHub usage (billed USDC)",
93
+ ` Balance: ${money(usage.balance.amount_usdc)} remaining`,
72
94
  ];
73
95
  if (usage.session) {
74
- lines.push(` Session: ${money(usage.session.spend_usdc)} · ${usage.session.requests} req · ${tokens(usage.session.total_tokens)} tok (${usage.session.id.slice(0, 12)}…)`);
96
+ lines.push(` Session: ${usage.session.requests} requests · ${tokens(usage.session.total_tokens)} tokens · ${money(usage.session.spend_usdc)} billed`);
75
97
  }
76
- lines.push(` ${usage.window.kind} window (${usage.window.tz}): ${money(usage.window.spend_usdc)} · ${usage.window.requests} req · ${tokens(usage.window.total_tokens)} tok`, ` All-time: ${money(usage.all_time.spend_usdc)} · ${usage.all_time.requests} req · ${tokens(usage.all_time.total_tokens)} tok`, ` Top model: ${usage.top_model?.model ?? "—"} (${money(usage.top_model?.spend_usdc ?? 0)}, ${usage.top_model?.requests ?? 0} req)`);
98
+ lines.push(` Today: ${usage.window.requests} requests · ${tokens(usage.window.total_tokens)} tokens · ${money(usage.window.spend_usdc)} billed`, ` All-time: ${usage.all_time.requests} requests · ${tokens(usage.all_time.total_tokens)} tokens · ${money(usage.all_time.spend_usdc)} billed`, ` Top model: ${usage.top_model?.model ?? "—"} (${usage.top_model?.requests ?? 0} req · ${money(usage.top_model?.spend_usdc ?? 0)})`);
77
99
  if (usage.cache?.cached)
78
- lines.push(` (cached, ttl ${usage.cache.ttl_seconds}s)`);
100
+ lines.push(` (cached ${usage.cache.ttl_seconds}s)`);
79
101
  return lines.join("\n");
80
102
  }
@@ -9,7 +9,7 @@ test("usageUrl", () => {
9
9
  assert.equal(usageUrl("https://api.inferhub.dev", "day", "UTC"), "https://api.inferhub.dev/v1/me/usage?window=day&tz=UTC");
10
10
  assert.equal(usageUrl("https://api.inferhub.dev", "day", "UTC", "abc"), "https://api.inferhub.dev/v1/me/usage?window=day&tz=UTC&session_id=abc");
11
11
  });
12
- test("formatStatusLine", () => {
12
+ test("formatStatusLine is plain language", () => {
13
13
  const usage = {
14
14
  object: "account.usage",
15
15
  currency: "USDC",
@@ -19,64 +19,67 @@ test("formatStatusLine", () => {
19
19
  tz: "UTC",
20
20
  since: "",
21
21
  until: "",
22
- requests: 3,
23
- prompt_tokens: 100,
24
- completion_tokens: 50,
25
- total_tokens: 150,
26
- spend_usdc: "0.012000",
22
+ requests: 86,
23
+ prompt_tokens: 14_000_000,
24
+ completion_tokens: 100_000,
25
+ total_tokens: 14_100_000,
26
+ spend_usdc: "0",
27
27
  },
28
28
  all_time: {
29
- requests: 10,
30
- prompt_tokens: 1000,
31
- completion_tokens: 500,
32
- total_tokens: 1500,
33
- spend_usdc: "0.500000",
29
+ requests: 3890,
30
+ prompt_tokens: 300_000_000,
31
+ completion_tokens: 1_000_000,
32
+ total_tokens: 301_000_000,
33
+ spend_usdc: "0.410000",
34
34
  },
35
35
  session: {
36
36
  id: "sess-1",
37
- requests: 2,
38
- prompt_tokens: 80,
39
- completion_tokens: 20,
40
- total_tokens: 100,
41
- spend_usdc: "0.004000",
37
+ requests: 12,
38
+ prompt_tokens: 200_000,
39
+ completion_tokens: 5_000,
40
+ total_tokens: 205_000,
41
+ spend_usdc: "0",
42
42
  since: null,
43
43
  until: null,
44
44
  },
45
45
  top_model: {
46
- model: "free/grok/grok-4.5",
47
- requests: 8,
48
- tokens: 1200,
49
- spend_usdc: "0.400000",
46
+ model: "cx/gpt-5.5",
47
+ requests: 212,
48
+ tokens: 239830,
49
+ spend_usdc: "0.137155",
50
50
  },
51
51
  cache: { cached: false, ttl_seconds: 30 },
52
52
  };
53
53
  const line = formatStatusLine(usage, {
54
- sessionCostUsd: 2.69,
55
- contextPct: 12,
56
54
  model: "free/grok/grok-4.5",
55
+ contextPct: 8,
56
+ sessionCostUsd: 2.69,
57
57
  });
58
58
  assert.match(line, /^InferHub/);
59
+ assert.match(line, /session 12 req \/ 205\.0k tok \/ \$0/);
60
+ assert.match(line, /today 86 req \/ 14\.1M tok \/ \$0/);
61
+ assert.match(line, /all-time 3,890 req \/ 301\.0M tok \/ \$0\.41/);
62
+ assert.match(line, /balance \$1\.25/);
63
+ assert.match(line, /top gpt-5\.5/);
64
+ assert.match(line, /8% context/);
59
65
  assert.doesNotMatch(line, /\bIH\b/);
60
- assert.match(line, /bal \$1\.25/);
61
- assert.match(line, /sess \$0\.0040/);
62
- assert.match(line, /top grok-4\.5/);
63
- assert.doesNotMatch(line, /est /);
64
- const freeDay = {
66
+ assert.doesNotMatch(line, /0r\/0/);
67
+ assert.doesNotMatch(line, /claude-est/); // estimate off by default
68
+ // empty/zero session should be omitted (not "sess 0r/0")
69
+ const noSess = {
65
70
  ...usage,
66
71
  session: {
67
- id: "s2",
68
- requests: 5,
69
- prompt_tokens: 1000,
72
+ id: "empty",
73
+ requests: 0,
74
+ prompt_tokens: 0,
70
75
  completion_tokens: 0,
71
- total_tokens: 1000,
76
+ total_tokens: 0,
72
77
  spend_usdc: "0",
73
78
  since: null,
74
79
  until: null,
75
80
  },
76
- window: { ...usage.window, spend_usdc: "0", requests: 86, total_tokens: 14_100_000 },
77
81
  };
78
- const freeLine = formatStatusLine(freeDay, { model: "grok-4.5" });
79
- assert.match(freeLine, /sess 5r\/1\.0k/);
80
- assert.match(freeLine, /day 86r\/14\.1M/);
82
+ const line2 = formatStatusLine(noSess, { model: "Grok" });
83
+ assert.doesNotMatch(line2, /session /);
81
84
  assert.equal(money("0.001"), "$0.0010");
82
85
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inferhub/usage",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Shared InferHub account usage client for coding-agent plugins",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -19,7 +19,7 @@ test("usageUrl", () => {
19
19
  );
20
20
  });
21
21
 
22
- test("formatStatusLine", () => {
22
+ test("formatStatusLine is plain language", () => {
23
23
  const usage: AccountUsage = {
24
24
  object: "account.usage",
25
25
  currency: "USDC",
@@ -29,65 +29,71 @@ test("formatStatusLine", () => {
29
29
  tz: "UTC",
30
30
  since: "",
31
31
  until: "",
32
- requests: 3,
33
- prompt_tokens: 100,
34
- completion_tokens: 50,
35
- total_tokens: 150,
36
- spend_usdc: "0.012000",
32
+ requests: 86,
33
+ prompt_tokens: 14_000_000,
34
+ completion_tokens: 100_000,
35
+ total_tokens: 14_100_000,
36
+ spend_usdc: "0",
37
37
  },
38
38
  all_time: {
39
- requests: 10,
40
- prompt_tokens: 1000,
41
- completion_tokens: 500,
42
- total_tokens: 1500,
43
- spend_usdc: "0.500000",
39
+ requests: 3890,
40
+ prompt_tokens: 300_000_000,
41
+ completion_tokens: 1_000_000,
42
+ total_tokens: 301_000_000,
43
+ spend_usdc: "0.410000",
44
44
  },
45
45
  session: {
46
46
  id: "sess-1",
47
- requests: 2,
48
- prompt_tokens: 80,
49
- completion_tokens: 20,
50
- total_tokens: 100,
51
- spend_usdc: "0.004000",
47
+ requests: 12,
48
+ prompt_tokens: 200_000,
49
+ completion_tokens: 5_000,
50
+ total_tokens: 205_000,
51
+ spend_usdc: "0",
52
52
  since: null,
53
53
  until: null,
54
54
  },
55
55
  top_model: {
56
- model: "free/grok/grok-4.5",
57
- requests: 8,
58
- tokens: 1200,
59
- spend_usdc: "0.400000",
56
+ model: "cx/gpt-5.5",
57
+ requests: 212,
58
+ tokens: 239830,
59
+ spend_usdc: "0.137155",
60
60
  },
61
61
  cache: { cached: false, ttl_seconds: 30 },
62
62
  };
63
+
63
64
  const line = formatStatusLine(usage, {
64
- sessionCostUsd: 2.69,
65
- contextPct: 12,
66
65
  model: "free/grok/grok-4.5",
66
+ contextPct: 8,
67
+ sessionCostUsd: 2.69,
67
68
  });
69
+
68
70
  assert.match(line, /^InferHub/);
71
+ assert.match(line, /session 12 req \/ 205\.0k tok \/ \$0/);
72
+ assert.match(line, /today 86 req \/ 14\.1M tok \/ \$0/);
73
+ assert.match(line, /all-time 3,890 req \/ 301\.0M tok \/ \$0\.41/);
74
+ assert.match(line, /balance \$1\.25/);
75
+ assert.match(line, /top gpt-5\.5/);
76
+ assert.match(line, /8% context/);
69
77
  assert.doesNotMatch(line, /\bIH\b/);
70
- assert.match(line, /bal \$1\.25/);
71
- assert.match(line, /sess \$0\.0040/);
72
- assert.match(line, /top grok-4\.5/);
73
- assert.doesNotMatch(line, /est /);
78
+ assert.doesNotMatch(line, /0r\/0/);
79
+ assert.doesNotMatch(line, /claude-est/); // estimate off by default
74
80
 
75
- const freeDay: AccountUsage = {
81
+ // empty/zero session should be omitted (not "sess 0r/0")
82
+ const noSess: AccountUsage = {
76
83
  ...usage,
77
84
  session: {
78
- id: "s2",
79
- requests: 5,
80
- prompt_tokens: 1000,
85
+ id: "empty",
86
+ requests: 0,
87
+ prompt_tokens: 0,
81
88
  completion_tokens: 0,
82
- total_tokens: 1000,
89
+ total_tokens: 0,
83
90
  spend_usdc: "0",
84
91
  since: null,
85
92
  until: null,
86
93
  },
87
- window: { ...usage.window, spend_usdc: "0", requests: 86, total_tokens: 14_100_000 },
88
94
  };
89
- const freeLine = formatStatusLine(freeDay, { model: "grok-4.5" });
90
- assert.match(freeLine, /sess 5r\/1\.0k/);
91
- assert.match(freeLine, /day 86r\/14\.1M/);
95
+ const line2 = formatStatusLine(noSess, { model: "Grok" });
96
+ assert.doesNotMatch(line2, /session /);
97
+
92
98
  assert.equal(money("0.001"), "$0.0010");
93
99
  });
package/src/format.ts CHANGED
@@ -20,21 +20,35 @@ export function shortModel(model: string | null | undefined): string {
20
20
  return parts[parts.length - 1] || model;
21
21
  }
22
22
 
23
- function spendOrActivity(
24
- spendUsdc: string | number | null | undefined,
25
- requests: number,
26
- totalTokens: number,
23
+ /** Human activity + optional billed spend. */
24
+ function activity(
25
+ label: string,
26
+ opts: {
27
+ spendUsdc?: string | number | null;
28
+ requests?: number | null;
29
+ totalTokens?: number | null;
30
+ },
27
31
  ): string {
28
- const spend = Number(spendUsdc ?? 0);
29
- if (spend > 0) return money(spend);
30
- return `${requests}r/${tokens(totalTokens)}`;
32
+ const req = Number(opts.requests ?? 0);
33
+ const tok = Number(opts.totalTokens ?? 0);
34
+ const spend = Number(opts.spendUsdc ?? 0);
35
+ const bits: string[] = [];
36
+ if (req > 0 || tok > 0) {
37
+ bits.push(`${req.toLocaleString()} req`);
38
+ bits.push(`${tokens(tok)} tok`);
39
+ } else {
40
+ bits.push("no traffic");
41
+ }
42
+ if (spend > 0) bits.push(money(spend));
43
+ else bits.push("$0");
44
+ return `${label} ${bits.join(" / ")}`;
31
45
  }
32
46
 
33
47
  /**
34
48
  * Compact one-liner for Claude statusline / Codex Stop summary.
35
49
  *
36
- * Shows InferHub-billed figures only. Claude's local `cost.total_cost_usd` is
37
- * official list-price estimate and is NOT shown unless showEstimate=true.
50
+ * Plain-language labels. Free models correctly show $0 billed while still
51
+ * reporting request/token activity.
38
52
  */
39
53
  export function formatStatusLine(
40
54
  usage: AccountUsage,
@@ -42,37 +56,47 @@ export function formatStatusLine(
42
56
  sessionCostUsd?: number | null;
43
57
  contextPct?: number | null;
44
58
  model?: string | null;
45
- /** When true, include Claude's local cost estimate as `est $…`. Default false. */
59
+ /** When true, include Claude local list-price estimate. Default false. */
46
60
  showEstimate?: boolean;
47
61
  },
48
62
  ): string {
49
- const sess = usage.session
50
- ? spendOrActivity(
51
- usage.session.spend_usdc,
52
- usage.session.requests,
53
- usage.session.total_tokens,
54
- )
55
- : null;
56
- const day = spendOrActivity(
57
- usage.window.spend_usdc,
58
- usage.window.requests,
59
- usage.window.total_tokens,
60
- );
61
- const all = money(usage.all_time.spend_usdc);
63
+ const model = extras?.model ? shortModel(extras.model) : null;
62
64
  const bal = money(usage.balance.amount_usdc);
63
65
  const top = shortModel(usage.top_model?.model);
64
- const model = extras?.model ? shortModel(extras.model) : null;
65
66
  const ctx =
66
67
  extras?.contextPct != null && Number.isFinite(extras.contextPct)
67
- ? `${Math.round(extras.contextPct)}%ctx`
68
+ ? `${Math.round(extras.contextPct)}% context`
68
69
  : null;
69
70
 
70
71
  const parts: string[] = ["InferHub"];
71
72
  if (model) parts.push(model);
72
- if (sess) parts.push(`sess ${sess}`);
73
- parts.push(`${usage.window.kind} ${day}`);
74
- parts.push(`all ${all}`);
75
- parts.push(`bal ${bal}`);
73
+
74
+ // Latest conversation on InferHub (server-derived session), if present and useful
75
+ if (usage.session && (usage.session.requests > 0 || Number(usage.session.spend_usdc) > 0)) {
76
+ parts.push(
77
+ activity("session", {
78
+ spendUsdc: usage.session.spend_usdc,
79
+ requests: usage.session.requests,
80
+ totalTokens: usage.session.total_tokens,
81
+ }),
82
+ );
83
+ }
84
+
85
+ parts.push(
86
+ activity("today", {
87
+ spendUsdc: usage.window.spend_usdc,
88
+ requests: usage.window.requests,
89
+ totalTokens: usage.window.total_tokens,
90
+ }),
91
+ );
92
+ parts.push(
93
+ activity("all-time", {
94
+ spendUsdc: usage.all_time.spend_usdc,
95
+ requests: usage.all_time.requests,
96
+ totalTokens: usage.all_time.total_tokens,
97
+ }),
98
+ );
99
+ parts.push(`balance ${bal}`);
76
100
  if (top && top !== "—") parts.push(`top ${top}`);
77
101
  if (ctx) parts.push(ctx);
78
102
 
@@ -82,7 +106,7 @@ export function formatStatusLine(
82
106
  Number.isFinite(extras.sessionCostUsd) &&
83
107
  extras.sessionCostUsd > 0
84
108
  ) {
85
- parts.push(`est ${money(extras.sessionCostUsd)}`);
109
+ parts.push(`claude-est ${money(extras.sessionCostUsd)}`);
86
110
  }
87
111
 
88
112
  return parts.join(" · ");
@@ -91,19 +115,19 @@ export function formatStatusLine(
91
115
  /** Multi-line human report for /usage skills. */
92
116
  export function formatReport(usage: AccountUsage): string {
93
117
  const lines = [
94
- "InferHub usage",
95
- ` Balance: ${money(usage.balance.amount_usdc)} USDC`,
118
+ "InferHub usage (billed USDC)",
119
+ ` Balance: ${money(usage.balance.amount_usdc)} remaining`,
96
120
  ];
97
121
  if (usage.session) {
98
122
  lines.push(
99
- ` Session: ${money(usage.session.spend_usdc)} · ${usage.session.requests} req · ${tokens(usage.session.total_tokens)} tok (${usage.session.id.slice(0, 12)}…)`,
123
+ ` Session: ${usage.session.requests} requests · ${tokens(usage.session.total_tokens)} tokens · ${money(usage.session.spend_usdc)} billed`,
100
124
  );
101
125
  }
102
126
  lines.push(
103
- ` ${usage.window.kind} window (${usage.window.tz}): ${money(usage.window.spend_usdc)} · ${usage.window.requests} req · ${tokens(usage.window.total_tokens)} tok`,
104
- ` All-time: ${money(usage.all_time.spend_usdc)} · ${usage.all_time.requests} req · ${tokens(usage.all_time.total_tokens)} tok`,
105
- ` Top model: ${usage.top_model?.model ?? "—"} (${money(usage.top_model?.spend_usdc ?? 0)}, ${usage.top_model?.requests ?? 0} req)`,
127
+ ` Today: ${usage.window.requests} requests · ${tokens(usage.window.total_tokens)} tokens · ${money(usage.window.spend_usdc)} billed`,
128
+ ` All-time: ${usage.all_time.requests} requests · ${tokens(usage.all_time.total_tokens)} tokens · ${money(usage.all_time.spend_usdc)} billed`,
129
+ ` Top model: ${usage.top_model?.model ?? "—"} (${usage.top_model?.requests ?? 0} req · ${money(usage.top_model?.spend_usdc ?? 0)})`,
106
130
  );
107
- if (usage.cache?.cached) lines.push(` (cached, ttl ${usage.cache.ttl_seconds}s)`);
131
+ if (usage.cache?.cached) lines.push(` (cached ${usage.cache.ttl_seconds}s)`);
108
132
  return lines.join("\n");
109
133
  }