@inferhub/usage 0.1.6 → 0.1.8

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
@@ -1,12 +1,16 @@
1
1
  import type { AccountUsage } from "./types.js";
2
2
  export declare function money(usdc: string | number | null | undefined, digits?: number): string;
3
3
  export declare function tokens(n: number | null | undefined): string;
4
+ /** Compact integer for statusline: 3943 → 3.9k, 28000000 → 28.0M */
5
+ export declare function compactCount(n: number | null | undefined): string;
4
6
  export declare function shortModel(model: string | null | undefined): string;
5
7
  /**
6
- * One-line Claude statusline — keep short (Claude only shows one row).
8
+ * One-line Claude statusline.
7
9
  *
8
10
  * Example:
9
- * InferHub · sess 19r · day 129r · all $0.41 · bal $1.31 · top gpt-5.5
11
+ * InferHub · sess 4r/1.2M · day 135r/26.4M · all $0.41/3.9k r/354M · bal $1.31 · top gpt-5.5
12
+ *
13
+ * Keep under ~90 chars when possible; tokens use k/M/B.
10
14
  */
11
15
  export declare function formatStatusLine(usage: AccountUsage, extras?: {
12
16
  sessionCostUsd?: number | null;
package/dist/format.js CHANGED
@@ -6,15 +6,22 @@ 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) {
12
+ return compactCount(n);
13
+ }
14
+ /** Compact integer for statusline: 3943 → 3.9k, 28000000 → 28.0M */
15
+ export function compactCount(n) {
14
16
  const v = Number(n ?? 0);
15
- if (v >= 1_000_000)
17
+ if (!Number.isFinite(v))
18
+ return "0";
19
+ const abs = Math.abs(v);
20
+ if (abs >= 1_000_000_000)
21
+ return `${(v / 1_000_000_000).toFixed(1)}B`;
22
+ if (abs >= 1_000_000)
16
23
  return `${(v / 1_000_000).toFixed(1)}M`;
17
- if (v >= 1_000)
24
+ if (abs >= 1_000)
18
25
  return `${(v / 1_000).toFixed(1)}k`;
19
26
  return String(Math.round(v));
20
27
  }
@@ -24,36 +31,40 @@ export function shortModel(model) {
24
31
  const parts = model.split("/");
25
32
  return parts[parts.length - 1] || model;
26
33
  }
27
- /** Compact metric for one-line status.
28
- * - billed: "$0.41" (no req clutter on all-time)
29
- * - free/zero: "19r"
30
- * - billed + wantReq: "$0.12/5r" when showReq=true
34
+ /**
35
+ * Compact metric: "4r/1.2M" free, or "$0.41/3.9kr/355.7M" when billed.
36
+ * Both requests and tokens use k/M/B.
31
37
  */
32
- function compact(spendUsdc, requests, opts) {
38
+ function compact(spendUsdc, requests, totalTokens) {
33
39
  const spend = Number(spendUsdc ?? 0);
34
40
  const req = Number(requests ?? 0);
35
- if (spend > 0) {
36
- if (opts?.showReq && req > 0)
37
- return `${money(spend)}/${req}r`;
38
- return money(spend);
39
- }
40
- if (req <= 0)
41
+ const tok = Number(totalTokens ?? 0);
42
+ if (req <= 0 && tok <= 0 && spend <= 0)
41
43
  return "—";
42
- return `${req}r`;
44
+ const bits = [];
45
+ if (spend > 0)
46
+ bits.push(money(spend));
47
+ if (req > 0)
48
+ bits.push(`${compactCount(req)}r`);
49
+ if (tok > 0)
50
+ bits.push(compactCount(tok));
51
+ return bits.join("/") || "—";
43
52
  }
44
53
  /**
45
- * One-line Claude statusline — keep short (Claude only shows one row).
54
+ * One-line Claude statusline.
46
55
  *
47
56
  * Example:
48
- * InferHub · sess 19r · day 129r · all $0.41 · bal $1.31 · top gpt-5.5
57
+ * InferHub · sess 4r/1.2M · day 135r/26.4M · all $0.41/3.9k r/354M · bal $1.31 · top gpt-5.5
58
+ *
59
+ * Keep under ~90 chars when possible; tokens use k/M/B.
49
60
  */
50
61
  export function formatStatusLine(usage, extras) {
51
62
  const parts = ["InferHub"];
52
- if (usage.session && (usage.session.requests > 0 || Number(usage.session.spend_usdc) > 0)) {
53
- parts.push(`sess ${compact(usage.session.spend_usdc, usage.session.requests)}`);
63
+ if (usage.session && (usage.session.requests > 0 || Number(usage.session.spend_usdc) > 0 || usage.session.total_tokens > 0)) {
64
+ parts.push(`sess ${compact(usage.session.spend_usdc, usage.session.requests, usage.session.total_tokens)}`);
54
65
  }
55
- parts.push(`day ${compact(usage.window.spend_usdc, usage.window.requests)}`);
56
- parts.push(`all ${compact(usage.all_time.spend_usdc, usage.all_time.requests)}`);
66
+ parts.push(`day ${compact(usage.window.spend_usdc, usage.window.requests, usage.window.total_tokens)}`);
67
+ parts.push(`all ${compact(usage.all_time.spend_usdc, usage.all_time.requests, usage.all_time.total_tokens)}`);
57
68
  parts.push(`bal ${money(usage.balance.amount_usdc)}`);
58
69
  const top = shortModel(usage.top_model?.model);
59
70
  if (top && top !== "—")
@@ -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,23 +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
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/3.9kr/354.2M · bal $1.31 · top gpt-5.5");
56
53
  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");
58
- assert.ok(line.length <= 72, `too long: ${line.length}`);
59
- // paid day shows money only (no req clutter)
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/);
67
- assert.doesNotMatch(paidLine, /sess /);
68
- assert.doesNotMatch(paidLine, /\/5r/);
54
+ assert.match(line, /3\.9kr/);
55
+ assert.match(line, /354\.2M/);
56
+ assert.doesNotMatch(line, /3933r/);
57
+ // empty session omitted
58
+ const noSess = { ...usage, session: null };
59
+ assert.doesNotMatch(formatStatusLine(noSess), /sess /);
69
60
  assert.equal(money("0"), "$0");
70
61
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inferhub/usage",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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,29 +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",
62
+ "InferHub · sess 19r/6.0M · day 129r/26.4M · all $0.41/3.9kr/354.2M · bal $1.31 · top gpt-5.5",
69
63
  );
70
- assert.ok(line.length <= 72, `too long: ${line.length}`);
64
+ assert.ok(line.length < 100, `too long: ${line.length} ${line}`);
65
+ assert.match(line, /3\.9kr/);
66
+ assert.match(line, /354\.2M/);
67
+ assert.doesNotMatch(line, /3933r/);
71
68
 
72
- // paid day shows money only (no req clutter)
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/);
80
- assert.doesNotMatch(paidLine, /sess /);
81
- assert.doesNotMatch(paidLine, /\/5r/);
69
+ // empty session omitted
70
+ const noSess = { ...usage, session: null };
71
+ assert.doesNotMatch(formatStatusLine(noSess), /sess /);
82
72
 
83
73
  assert.equal(money("0"), "$0");
84
74
  });
package/src/format.ts CHANGED
@@ -5,14 +5,21 @@ 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 {
12
+ return compactCount(n);
13
+ }
14
+
15
+ /** Compact integer for statusline: 3943 → 3.9k, 28000000 → 28.0M */
16
+ export function compactCount(n: number | null | undefined): string {
13
17
  const v = Number(n ?? 0);
14
- if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`;
15
- if (v >= 1_000) return `${(v / 1_000).toFixed(1)}k`;
18
+ if (!Number.isFinite(v)) return "0";
19
+ const abs = Math.abs(v);
20
+ if (abs >= 1_000_000_000) return `${(v / 1_000_000_000).toFixed(1)}B`;
21
+ if (abs >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`;
22
+ if (abs >= 1_000) return `${(v / 1_000).toFixed(1)}k`;
16
23
  return String(Math.round(v));
17
24
  }
18
25
 
@@ -22,31 +29,34 @@ export function shortModel(model: string | null | undefined): string {
22
29
  return parts[parts.length - 1] || model;
23
30
  }
24
31
 
25
- /** Compact metric for one-line status.
26
- * - billed: "$0.41" (no req clutter on all-time)
27
- * - free/zero: "19r"
28
- * - billed + wantReq: "$0.12/5r" when showReq=true
32
+ /**
33
+ * Compact metric: "4r/1.2M" free, or "$0.41/3.9kr/355.7M" when billed.
34
+ * Both requests and tokens use k/M/B.
29
35
  */
30
36
  function compact(
31
37
  spendUsdc: string | number | null | undefined,
32
38
  requests: number | null | undefined,
33
- opts?: { showReq?: boolean },
39
+ totalTokens: number | null | undefined,
34
40
  ): string {
35
41
  const spend = Number(spendUsdc ?? 0);
36
42
  const req = Number(requests ?? 0);
37
- if (spend > 0) {
38
- if (opts?.showReq && req > 0) return `${money(spend)}/${req}r`;
39
- return money(spend);
40
- }
41
- if (req <= 0) return "—";
42
- return `${req}r`;
43
+ const tok = Number(totalTokens ?? 0);
44
+ if (req <= 0 && tok <= 0 && spend <= 0) return "—";
45
+
46
+ const bits: string[] = [];
47
+ if (spend > 0) bits.push(money(spend));
48
+ if (req > 0) bits.push(`${compactCount(req)}r`);
49
+ if (tok > 0) bits.push(compactCount(tok));
50
+ return bits.join("/") || "—";
43
51
  }
44
52
 
45
53
  /**
46
- * One-line Claude statusline — keep short (Claude only shows one row).
54
+ * One-line Claude statusline.
47
55
  *
48
56
  * Example:
49
- * InferHub · sess 19r · day 129r · all $0.41 · bal $1.31 · top gpt-5.5
57
+ * InferHub · sess 4r/1.2M · day 135r/26.4M · all $0.41/3.9k r/354M · bal $1.31 · top gpt-5.5
58
+ *
59
+ * Keep under ~90 chars when possible; tokens use k/M/B.
50
60
  */
51
61
  export function formatStatusLine(
52
62
  usage: AccountUsage,
@@ -59,12 +69,18 @@ export function formatStatusLine(
59
69
  ): string {
60
70
  const parts: string[] = ["InferHub"];
61
71
 
62
- if (usage.session && (usage.session.requests > 0 || Number(usage.session.spend_usdc) > 0)) {
63
- parts.push(`sess ${compact(usage.session.spend_usdc, usage.session.requests)}`);
72
+ if (usage.session && (usage.session.requests > 0 || Number(usage.session.spend_usdc) > 0 || usage.session.total_tokens > 0)) {
73
+ parts.push(
74
+ `sess ${compact(usage.session.spend_usdc, usage.session.requests, usage.session.total_tokens)}`,
75
+ );
64
76
  }
65
77
 
66
- parts.push(`day ${compact(usage.window.spend_usdc, usage.window.requests)}`);
67
- parts.push(`all ${compact(usage.all_time.spend_usdc, usage.all_time.requests)}`);
78
+ parts.push(
79
+ `day ${compact(usage.window.spend_usdc, usage.window.requests, usage.window.total_tokens)}`,
80
+ );
81
+ parts.push(
82
+ `all ${compact(usage.all_time.spend_usdc, usage.all_time.requests, usage.all_time.total_tokens)}`,
83
+ );
68
84
  parts.push(`bal ${money(usage.balance.amount_usdc)}`);
69
85
 
70
86
  const top = shortModel(usage.top_model?.model);