@inferhub/usage 0.1.5 → 0.1.7

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/format.d.ts CHANGED
@@ -3,10 +3,12 @@ export declare function money(usdc: string | number | null | undefined, digits?:
3
3
  export declare function tokens(n: number | null | undefined): string;
4
4
  export declare function shortModel(model: string | null | undefined): string;
5
5
  /**
6
- * One-line Claude statusline — keep short (Claude only shows one row).
6
+ * One-line Claude statusline.
7
7
  *
8
8
  * Example:
9
- * InferHub · sess 19r · day 129r · all $0.41 · bal $1.31 · top gpt-5.5
9
+ * InferHub · sess 4r/1.2M · day 135r/26.4M · all $0.41/3.9k r/354M · bal $1.31 · top gpt-5.5
10
+ *
11
+ * Keep under ~90 chars when possible; tokens use k/M/B.
10
12
  */
11
13
  export declare function formatStatusLine(usage: AccountUsage, extras?: {
12
14
  sessionCostUsd?: number | null;
package/dist/format.js CHANGED
@@ -6,12 +6,12 @@ export function money(usdc, digits = 2) {
6
6
  return "$0";
7
7
  if (Math.abs(n) < 0.01)
8
8
  return `$${n.toFixed(4)}`;
9
- if (Math.abs(n) < 1)
10
- return `$${n.toFixed(2)}`;
11
9
  return `$${n.toFixed(2)}`;
12
10
  }
13
11
  export function tokens(n) {
14
12
  const v = Number(n ?? 0);
13
+ if (v >= 1_000_000_000)
14
+ return `${(v / 1_000_000_000).toFixed(1)}B`;
15
15
  if (v >= 1_000_000)
16
16
  return `${(v / 1_000_000).toFixed(1)}M`;
17
17
  if (v >= 1_000)
@@ -24,41 +24,44 @@ export function shortModel(model) {
24
24
  const parts = model.split("/");
25
25
  return parts[parts.length - 1] || model;
26
26
  }
27
- /** Compact metric: "$0.12" if billed, else "19r" (optionally with tokens if tiny). */
27
+ /**
28
+ * Compact metric: "4r/1.2M" free, or "$0.12/5r/80k" when billed.
29
+ * Always includes tokens when > 0.
30
+ */
28
31
  function compact(spendUsdc, requests, totalTokens) {
29
32
  const spend = Number(spendUsdc ?? 0);
30
33
  const req = Number(requests ?? 0);
31
- if (spend > 0) {
32
- // billed: show money + req if space-ish useful
33
- return req > 0 ? `${money(spend)}/${req}r` : money(spend);
34
- }
35
- // free / zero spend: show traffic only
36
- if (req <= 0 && Number(totalTokens ?? 0) <= 0)
34
+ const tok = Number(totalTokens ?? 0);
35
+ if (req <= 0 && tok <= 0 && spend <= 0)
37
36
  return "—";
38
- return `${req}r`;
37
+ const bits = [];
38
+ if (spend > 0)
39
+ bits.push(money(spend));
40
+ if (req > 0)
41
+ bits.push(`${req}r`);
42
+ if (tok > 0)
43
+ bits.push(tokens(tok));
44
+ return bits.join("/") || "—";
39
45
  }
40
46
  /**
41
- * One-line Claude statusline — keep short (Claude only shows one row).
47
+ * One-line Claude statusline.
42
48
  *
43
49
  * Example:
44
- * InferHub · sess 19r · day 129r · all $0.41 · bal $1.31 · top gpt-5.5
50
+ * InferHub · sess 4r/1.2M · day 135r/26.4M · all $0.41/3.9k r/354M · bal $1.31 · top gpt-5.5
51
+ *
52
+ * Keep under ~90 chars when possible; tokens use k/M/B.
45
53
  */
46
54
  export function formatStatusLine(usage, extras) {
47
55
  const parts = ["InferHub"];
48
- // Session (latest IH conversation) omit if empty
49
- if (usage.session && (usage.session.requests > 0 || Number(usage.session.spend_usdc) > 0)) {
56
+ if (usage.session && (usage.session.requests > 0 || Number(usage.session.spend_usdc) > 0 || usage.session.total_tokens > 0)) {
50
57
  parts.push(`sess ${compact(usage.session.spend_usdc, usage.session.requests, usage.session.total_tokens)}`);
51
58
  }
52
59
  parts.push(`day ${compact(usage.window.spend_usdc, usage.window.requests, usage.window.total_tokens)}`);
53
- parts.push(`all ${compact(usage.all_time.spend_usdc, usage.all_time.requests)}`);
60
+ parts.push(`all ${compact(usage.all_time.spend_usdc, usage.all_time.requests, usage.all_time.total_tokens)}`);
54
61
  parts.push(`bal ${money(usage.balance.amount_usdc)}`);
55
62
  const top = shortModel(usage.top_model?.model);
56
63
  if (top && top !== "—")
57
64
  parts.push(`top ${top}`);
58
- // context % is useful but optional — only if provided and non-zero space
59
- if (extras?.contextPct != null && Number.isFinite(extras.contextPct)) {
60
- parts.push(`${Math.round(extras.contextPct)}%`);
61
- }
62
65
  return parts.join(" · ");
63
66
  }
64
67
  /** Multi-line human report for /usage skills. */
@@ -7,7 +7,7 @@ test("normalizeBaseUrl adds /v1", () => {
7
7
  test("usageUrl", () => {
8
8
  assert.equal(usageUrl("https://api.inferhub.dev", "day", "UTC", "abc"), "https://api.inferhub.dev/v1/me/usage?window=day&tz=UTC&session_id=abc");
9
9
  });
10
- test("formatStatusLine is compact one-liner", () => {
10
+ test("formatStatusLine includes tokens compactly", () => {
11
11
  const usage = {
12
12
  object: "account.usage",
13
13
  currency: "USDC",
@@ -48,22 +48,14 @@ test("formatStatusLine is compact one-liner", () => {
48
48
  },
49
49
  cache: { cached: false, ttl_seconds: 30 },
50
50
  };
51
- const line = formatStatusLine(usage, {
52
- model: "grok-4.5",
53
- contextPct: 8,
54
- });
55
- // Must be short enough for one Claude status row
56
- assert.ok(line.length < 100, `too long: ${line.length} ${line}`);
57
- assert.equal(line, "InferHub · sess 19r · day 129r · all $0.41 · bal $1.31 · top gpt-5.5 · 8%");
58
- assert.doesNotMatch(line, /req \/|tokens|context|today|all-time|balance /);
59
- // paid day shows money
60
- const paid = {
61
- ...usage,
62
- window: { ...usage.window, spend_usdc: "0.12", requests: 5 },
63
- session: null,
64
- };
65
- const paidLine = formatStatusLine(paid);
66
- assert.match(paidLine, /day \$0\.12\/5r/);
67
- assert.doesNotMatch(paidLine, /sess /);
51
+ const line = formatStatusLine(usage, { model: "grok-4.5", contextPct: 8 });
52
+ assert.equal(line, "InferHub · sess 19r/6.0M · day 129r/26.4M · all $0.41/3933r/354.2M · bal $1.31 · top gpt-5.5");
53
+ assert.ok(line.length < 110, `too long: ${line.length} ${line}`);
54
+ assert.match(line, /6\.0M/);
55
+ assert.match(line, /26\.4M/);
56
+ assert.match(line, /354\.2M/);
57
+ // empty session omitted
58
+ const noSess = { ...usage, session: null };
59
+ assert.doesNotMatch(formatStatusLine(noSess), /sess /);
68
60
  assert.equal(money("0"), "$0");
69
61
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inferhub/usage",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Shared InferHub account usage client for coding-agent plugins",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -14,7 +14,7 @@ test("usageUrl", () => {
14
14
  );
15
15
  });
16
16
 
17
- test("formatStatusLine is compact one-liner", () => {
17
+ test("formatStatusLine includes tokens compactly", () => {
18
18
  const usage: AccountUsage = {
19
19
  object: "account.usage",
20
20
  currency: "USDC",
@@ -56,28 +56,19 @@ test("formatStatusLine is compact one-liner", () => {
56
56
  cache: { cached: false, ttl_seconds: 30 },
57
57
  };
58
58
 
59
- const line = formatStatusLine(usage, {
60
- model: "grok-4.5",
61
- contextPct: 8,
62
- });
63
-
64
- // Must be short enough for one Claude status row
65
- assert.ok(line.length < 100, `too long: ${line.length} ${line}`);
59
+ const line = formatStatusLine(usage, { model: "grok-4.5", contextPct: 8 });
66
60
  assert.equal(
67
61
  line,
68
- "InferHub · sess 19r · day 129r · all $0.41 · bal $1.31 · top gpt-5.5 · 8%",
62
+ "InferHub · sess 19r/6.0M · day 129r/26.4M · all $0.41/3933r/354.2M · bal $1.31 · top gpt-5.5",
69
63
  );
70
- assert.doesNotMatch(line, /req \/|tokens|context|today|all-time|balance /);
64
+ assert.ok(line.length < 110, `too long: ${line.length} ${line}`);
65
+ assert.match(line, /6\.0M/);
66
+ assert.match(line, /26\.4M/);
67
+ assert.match(line, /354\.2M/);
71
68
 
72
- // paid day shows money
73
- const paid: AccountUsage = {
74
- ...usage,
75
- window: { ...usage.window, spend_usdc: "0.12", requests: 5 },
76
- session: null,
77
- };
78
- const paidLine = formatStatusLine(paid);
79
- assert.match(paidLine, /day \$0\.12\/5r/);
80
- assert.doesNotMatch(paidLine, /sess /);
69
+ // empty session omitted
70
+ const noSess = { ...usage, session: null };
71
+ assert.doesNotMatch(formatStatusLine(noSess), /sess /);
81
72
 
82
73
  assert.equal(money("0"), "$0");
83
74
  });
package/src/format.ts CHANGED
@@ -5,12 +5,12 @@ export function money(usdc: string | number | null | undefined, digits = 2): str
5
5
  if (!Number.isFinite(n)) return "$0";
6
6
  if (n === 0) return "$0";
7
7
  if (Math.abs(n) < 0.01) return `$${n.toFixed(4)}`;
8
- if (Math.abs(n) < 1) return `$${n.toFixed(2)}`;
9
8
  return `$${n.toFixed(2)}`;
10
9
  }
11
10
 
12
11
  export function tokens(n: number | null | undefined): string {
13
12
  const v = Number(n ?? 0);
13
+ if (v >= 1_000_000_000) return `${(v / 1_000_000_000).toFixed(1)}B`;
14
14
  if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`;
15
15
  if (v >= 1_000) return `${(v / 1_000).toFixed(1)}k`;
16
16
  return String(Math.round(v));
@@ -22,28 +22,34 @@ export function shortModel(model: string | null | undefined): string {
22
22
  return parts[parts.length - 1] || model;
23
23
  }
24
24
 
25
- /** Compact metric: "$0.12" if billed, else "19r" (optionally with tokens if tiny). */
25
+ /**
26
+ * Compact metric: "4r/1.2M" free, or "$0.12/5r/80k" when billed.
27
+ * Always includes tokens when > 0.
28
+ */
26
29
  function compact(
27
30
  spendUsdc: string | number | null | undefined,
28
31
  requests: number | null | undefined,
29
- totalTokens?: number | null,
32
+ totalTokens: number | null | undefined,
30
33
  ): string {
31
34
  const spend = Number(spendUsdc ?? 0);
32
35
  const req = Number(requests ?? 0);
33
- if (spend > 0) {
34
- // billed: show money + req if space-ish useful
35
- return req > 0 ? `${money(spend)}/${req}r` : money(spend);
36
- }
37
- // free / zero spend: show traffic only
38
- if (req <= 0 && Number(totalTokens ?? 0) <= 0) return "—";
39
- return `${req}r`;
36
+ const tok = Number(totalTokens ?? 0);
37
+ if (req <= 0 && tok <= 0 && spend <= 0) return "—";
38
+
39
+ const bits: string[] = [];
40
+ if (spend > 0) bits.push(money(spend));
41
+ if (req > 0) bits.push(`${req}r`);
42
+ if (tok > 0) bits.push(tokens(tok));
43
+ return bits.join("/") || "—";
40
44
  }
41
45
 
42
46
  /**
43
- * One-line Claude statusline — keep short (Claude only shows one row).
47
+ * One-line Claude statusline.
44
48
  *
45
49
  * Example:
46
- * InferHub · sess 19r · day 129r · all $0.41 · bal $1.31 · top gpt-5.5
50
+ * InferHub · sess 4r/1.2M · day 135r/26.4M · all $0.41/3.9k r/354M · bal $1.31 · top gpt-5.5
51
+ *
52
+ * Keep under ~90 chars when possible; tokens use k/M/B.
47
53
  */
48
54
  export function formatStatusLine(
49
55
  usage: AccountUsage,
@@ -56,23 +62,23 @@ export function formatStatusLine(
56
62
  ): string {
57
63
  const parts: string[] = ["InferHub"];
58
64
 
59
- // Session (latest IH conversation) omit if empty
60
- if (usage.session && (usage.session.requests > 0 || Number(usage.session.spend_usdc) > 0)) {
61
- parts.push(`sess ${compact(usage.session.spend_usdc, usage.session.requests, usage.session.total_tokens)}`);
65
+ if (usage.session && (usage.session.requests > 0 || Number(usage.session.spend_usdc) > 0 || usage.session.total_tokens > 0)) {
66
+ parts.push(
67
+ `sess ${compact(usage.session.spend_usdc, usage.session.requests, usage.session.total_tokens)}`,
68
+ );
62
69
  }
63
70
 
64
- parts.push(`day ${compact(usage.window.spend_usdc, usage.window.requests, usage.window.total_tokens)}`);
65
- parts.push(`all ${compact(usage.all_time.spend_usdc, usage.all_time.requests)}`);
71
+ parts.push(
72
+ `day ${compact(usage.window.spend_usdc, usage.window.requests, usage.window.total_tokens)}`,
73
+ );
74
+ parts.push(
75
+ `all ${compact(usage.all_time.spend_usdc, usage.all_time.requests, usage.all_time.total_tokens)}`,
76
+ );
66
77
  parts.push(`bal ${money(usage.balance.amount_usdc)}`);
67
78
 
68
79
  const top = shortModel(usage.top_model?.model);
69
80
  if (top && top !== "—") parts.push(`top ${top}`);
70
81
 
71
- // context % is useful but optional — only if provided and non-zero space
72
- if (extras?.contextPct != null && Number.isFinite(extras.contextPct)) {
73
- parts.push(`${Math.round(extras.contextPct)}%`);
74
- }
75
-
76
82
  return parts.join(" · ");
77
83
  }
78
84