@inferhub/usage 0.1.3 → 0.1.5

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,16 +3,15 @@ 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
- * Compact one-liner for Claude statusline / Codex Stop summary.
6
+ * One-line Claude statusline keep short (Claude only shows one row).
7
7
  *
8
- * Plain-language labels. Free models correctly show $0 billed while still
9
- * reporting request/token activity.
8
+ * Example:
9
+ * InferHub · sess 19r · day 129r · all $0.41 · bal $1.31 · top gpt-5.5
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 local list-price estimate. Default false. */
16
15
  showEstimate?: boolean;
17
16
  }): string;
18
17
  /** Multi-line human report for /usage skills. */
package/dist/format.js CHANGED
@@ -1,10 +1,14 @@
1
1
  export function money(usdc, digits = 2) {
2
2
  const n = Number(usdc ?? 0);
3
3
  if (!Number.isFinite(n))
4
- return "$0.00";
5
- if (Math.abs(n) > 0 && Math.abs(n) < 0.01)
4
+ return "$0";
5
+ if (n === 0)
6
+ return "$0";
7
+ if (Math.abs(n) < 0.01)
6
8
  return `$${n.toFixed(4)}`;
7
- return `$${n.toFixed(digits)}`;
9
+ if (Math.abs(n) < 1)
10
+ return `$${n.toFixed(2)}`;
11
+ return `$${n.toFixed(2)}`;
8
12
  }
9
13
  export function tokens(n) {
10
14
  const v = Number(n ?? 0);
@@ -20,69 +24,40 @@ export function shortModel(model) {
20
24
  const parts = model.split("/");
21
25
  return parts[parts.length - 1] || model;
22
26
  }
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`);
27
+ /** Compact metric: "$0.12" if billed, else "19r" (optionally with tokens if tiny). */
28
+ function compact(spendUsdc, requests, totalTokens) {
29
+ const spend = Number(spendUsdc ?? 0);
30
+ 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);
32
34
  }
33
- else {
34
- bits.push("no traffic");
35
- }
36
- if (spend > 0)
37
- bits.push(money(spend));
38
- else
39
- bits.push("$0");
40
- return `${label} ${bits.join(" / ")}`;
35
+ // free / zero spend: show traffic only
36
+ if (req <= 0 && Number(totalTokens ?? 0) <= 0)
37
+ return "—";
38
+ return `${req}r`;
41
39
  }
42
40
  /**
43
- * Compact one-liner for Claude statusline / Codex Stop summary.
41
+ * One-line Claude statusline keep short (Claude only shows one row).
44
42
  *
45
- * Plain-language labels. Free models correctly show $0 billed while still
46
- * reporting request/token activity.
43
+ * Example:
44
+ * InferHub · sess 19r · day 129r · all $0.41 · bal $1.31 · top gpt-5.5
47
45
  */
48
46
  export function formatStatusLine(usage, extras) {
49
- const model = extras?.model ? shortModel(extras.model) : null;
50
- const bal = money(usage.balance.amount_usdc);
51
- const top = shortModel(usage.top_model?.model);
52
- const ctx = extras?.contextPct != null && Number.isFinite(extras.contextPct)
53
- ? `${Math.round(extras.contextPct)}% context`
54
- : null;
55
47
  const parts = ["InferHub"];
56
- if (model)
57
- parts.push(model);
58
- // Latest conversation on InferHub (server-derived session), if present and useful
48
+ // Session (latest IH conversation) — omit if empty
59
49
  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
- }));
50
+ parts.push(`sess ${compact(usage.session.spend_usdc, usage.session.requests, usage.session.total_tokens)}`);
65
51
  }
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}`);
52
+ 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)}`);
54
+ parts.push(`bal ${money(usage.balance.amount_usdc)}`);
55
+ const top = shortModel(usage.top_model?.model);
77
56
  if (top && top !== "—")
78
57
  parts.push(`top ${top}`);
79
- if (ctx)
80
- parts.push(ctx);
81
- if (extras?.showEstimate &&
82
- extras.sessionCostUsd != null &&
83
- Number.isFinite(extras.sessionCostUsd) &&
84
- extras.sessionCostUsd > 0) {
85
- parts.push(`claude-est ${money(extras.sessionCostUsd)}`);
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)}%`);
86
61
  }
87
62
  return parts.join(" · ");
88
63
  }
@@ -3,41 +3,39 @@ import test from "node:test";
3
3
  import { formatStatusLine, money, normalizeBaseUrl, usageUrl } from "./index.js";
4
4
  test("normalizeBaseUrl adds /v1", () => {
5
5
  assert.equal(normalizeBaseUrl("https://api.inferhub.dev"), "https://api.inferhub.dev/v1");
6
- assert.equal(normalizeBaseUrl("https://api.inferhub.dev/v1/"), "https://api.inferhub.dev/v1");
7
6
  });
8
7
  test("usageUrl", () => {
9
- assert.equal(usageUrl("https://api.inferhub.dev", "day", "UTC"), "https://api.inferhub.dev/v1/me/usage?window=day&tz=UTC");
10
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");
11
9
  });
12
- test("formatStatusLine is plain language", () => {
10
+ test("formatStatusLine is compact one-liner", () => {
13
11
  const usage = {
14
12
  object: "account.usage",
15
13
  currency: "USDC",
16
- balance: { amount_usdc: "1.250000", updated_at: null },
14
+ balance: { amount_usdc: "1.312816", updated_at: null },
17
15
  window: {
18
16
  kind: "day",
19
17
  tz: "UTC",
20
18
  since: "",
21
19
  until: "",
22
- requests: 86,
23
- prompt_tokens: 14_000_000,
24
- completion_tokens: 100_000,
25
- total_tokens: 14_100_000,
20
+ requests: 129,
21
+ prompt_tokens: 0,
22
+ completion_tokens: 0,
23
+ total_tokens: 26_400_000,
26
24
  spend_usdc: "0",
27
25
  },
28
26
  all_time: {
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",
27
+ requests: 3933,
28
+ prompt_tokens: 0,
29
+ completion_tokens: 0,
30
+ total_tokens: 354_200_000,
31
+ spend_usdc: "0.41",
34
32
  },
35
33
  session: {
36
- id: "sess-1",
37
- requests: 12,
38
- prompt_tokens: 200_000,
39
- completion_tokens: 5_000,
40
- total_tokens: 205_000,
34
+ id: "s",
35
+ requests: 19,
36
+ prompt_tokens: 0,
37
+ completion_tokens: 0,
38
+ total_tokens: 6_000_000,
41
39
  spend_usdc: "0",
42
40
  since: null,
43
41
  until: null,
@@ -45,41 +43,27 @@ test("formatStatusLine is plain language", () => {
45
43
  top_model: {
46
44
  model: "cx/gpt-5.5",
47
45
  requests: 212,
48
- tokens: 239830,
49
- spend_usdc: "0.137155",
46
+ tokens: 1,
47
+ spend_usdc: "0.14",
50
48
  },
51
49
  cache: { cached: false, ttl_seconds: 30 },
52
50
  };
53
51
  const line = formatStatusLine(usage, {
54
- model: "free/grok/grok-4.5",
52
+ model: "grok-4.5",
55
53
  contextPct: 8,
56
- sessionCostUsd: 2.69,
57
54
  });
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/);
65
- assert.doesNotMatch(line, /\bIH\b/);
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 = {
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 = {
70
61
  ...usage,
71
- session: {
72
- id: "empty",
73
- requests: 0,
74
- prompt_tokens: 0,
75
- completion_tokens: 0,
76
- total_tokens: 0,
77
- spend_usdc: "0",
78
- since: null,
79
- until: null,
80
- },
62
+ window: { ...usage.window, spend_usdc: "0.12", requests: 5 },
63
+ session: null,
81
64
  };
82
- const line2 = formatStatusLine(noSess, { model: "Grok" });
83
- assert.doesNotMatch(line2, /session /);
84
- assert.equal(money("0.001"), "$0.0010");
65
+ const paidLine = formatStatusLine(paid);
66
+ assert.match(paidLine, /day \$0\.12\/5r/);
67
+ assert.doesNotMatch(paidLine, /sess /);
68
+ assert.equal(money("0"), "$0");
85
69
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inferhub/usage",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Shared InferHub account usage client for coding-agent plugins",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -5,49 +5,44 @@ import type { AccountUsage } from "./types.js";
5
5
 
6
6
  test("normalizeBaseUrl adds /v1", () => {
7
7
  assert.equal(normalizeBaseUrl("https://api.inferhub.dev"), "https://api.inferhub.dev/v1");
8
- assert.equal(normalizeBaseUrl("https://api.inferhub.dev/v1/"), "https://api.inferhub.dev/v1");
9
8
  });
10
9
 
11
10
  test("usageUrl", () => {
12
- assert.equal(
13
- usageUrl("https://api.inferhub.dev", "day", "UTC"),
14
- "https://api.inferhub.dev/v1/me/usage?window=day&tz=UTC",
15
- );
16
11
  assert.equal(
17
12
  usageUrl("https://api.inferhub.dev", "day", "UTC", "abc"),
18
13
  "https://api.inferhub.dev/v1/me/usage?window=day&tz=UTC&session_id=abc",
19
14
  );
20
15
  });
21
16
 
22
- test("formatStatusLine is plain language", () => {
17
+ test("formatStatusLine is compact one-liner", () => {
23
18
  const usage: AccountUsage = {
24
19
  object: "account.usage",
25
20
  currency: "USDC",
26
- balance: { amount_usdc: "1.250000", updated_at: null },
21
+ balance: { amount_usdc: "1.312816", updated_at: null },
27
22
  window: {
28
23
  kind: "day",
29
24
  tz: "UTC",
30
25
  since: "",
31
26
  until: "",
32
- requests: 86,
33
- prompt_tokens: 14_000_000,
34
- completion_tokens: 100_000,
35
- total_tokens: 14_100_000,
27
+ requests: 129,
28
+ prompt_tokens: 0,
29
+ completion_tokens: 0,
30
+ total_tokens: 26_400_000,
36
31
  spend_usdc: "0",
37
32
  },
38
33
  all_time: {
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",
34
+ requests: 3933,
35
+ prompt_tokens: 0,
36
+ completion_tokens: 0,
37
+ total_tokens: 354_200_000,
38
+ spend_usdc: "0.41",
44
39
  },
45
40
  session: {
46
- id: "sess-1",
47
- requests: 12,
48
- prompt_tokens: 200_000,
49
- completion_tokens: 5_000,
50
- total_tokens: 205_000,
41
+ id: "s",
42
+ requests: 19,
43
+ prompt_tokens: 0,
44
+ completion_tokens: 0,
45
+ total_tokens: 6_000_000,
51
46
  spend_usdc: "0",
52
47
  since: null,
53
48
  until: null,
@@ -55,45 +50,34 @@ test("formatStatusLine is plain language", () => {
55
50
  top_model: {
56
51
  model: "cx/gpt-5.5",
57
52
  requests: 212,
58
- tokens: 239830,
59
- spend_usdc: "0.137155",
53
+ tokens: 1,
54
+ spend_usdc: "0.14",
60
55
  },
61
56
  cache: { cached: false, ttl_seconds: 30 },
62
57
  };
63
58
 
64
59
  const line = formatStatusLine(usage, {
65
- model: "free/grok/grok-4.5",
60
+ model: "grok-4.5",
66
61
  contextPct: 8,
67
- sessionCostUsd: 2.69,
68
62
  });
69
63
 
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/);
77
- assert.doesNotMatch(line, /\bIH\b/);
78
- assert.doesNotMatch(line, /0r\/0/);
79
- assert.doesNotMatch(line, /claude-est/); // estimate off by default
64
+ // Must be short enough for one Claude status row
65
+ assert.ok(line.length < 100, `too long: ${line.length} ${line}`);
66
+ assert.equal(
67
+ line,
68
+ "InferHub · sess 19r · day 129r · all $0.41 · bal $1.31 · top gpt-5.5 · 8%",
69
+ );
70
+ assert.doesNotMatch(line, /req \/|tokens|context|today|all-time|balance /);
80
71
 
81
- // empty/zero session should be omitted (not "sess 0r/0")
82
- const noSess: AccountUsage = {
72
+ // paid day shows money
73
+ const paid: AccountUsage = {
83
74
  ...usage,
84
- session: {
85
- id: "empty",
86
- requests: 0,
87
- prompt_tokens: 0,
88
- completion_tokens: 0,
89
- total_tokens: 0,
90
- spend_usdc: "0",
91
- since: null,
92
- until: null,
93
- },
75
+ window: { ...usage.window, spend_usdc: "0.12", requests: 5 },
76
+ session: null,
94
77
  };
95
- const line2 = formatStatusLine(noSess, { model: "Grok" });
96
- assert.doesNotMatch(line2, /session /);
78
+ const paidLine = formatStatusLine(paid);
79
+ assert.match(paidLine, /day \$0\.12\/5r/);
80
+ assert.doesNotMatch(paidLine, /sess /);
97
81
 
98
- assert.equal(money("0.001"), "$0.0010");
82
+ assert.equal(money("0"), "$0");
99
83
  });
package/src/format.ts CHANGED
@@ -2,9 +2,11 @@ import type { AccountUsage } from "./types.js";
2
2
 
3
3
  export function money(usdc: string | number | null | undefined, digits = 2): string {
4
4
  const n = Number(usdc ?? 0);
5
- if (!Number.isFinite(n)) return "$0.00";
6
- if (Math.abs(n) > 0 && Math.abs(n) < 0.01) return `$${n.toFixed(4)}`;
7
- return `$${n.toFixed(digits)}`;
5
+ if (!Number.isFinite(n)) return "$0";
6
+ if (n === 0) return "$0";
7
+ if (Math.abs(n) < 0.01) return `$${n.toFixed(4)}`;
8
+ if (Math.abs(n) < 1) return `$${n.toFixed(2)}`;
9
+ return `$${n.toFixed(2)}`;
8
10
  }
9
11
 
10
12
  export function tokens(n: number | null | undefined): string {
@@ -20,35 +22,28 @@ export function shortModel(model: string | null | undefined): string {
20
22
  return parts[parts.length - 1] || model;
21
23
  }
22
24
 
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
- },
25
+ /** Compact metric: "$0.12" if billed, else "19r" (optionally with tokens if tiny). */
26
+ function compact(
27
+ spendUsdc: string | number | null | undefined,
28
+ requests: number | null | undefined,
29
+ totalTokens?: number | null,
31
30
  ): string {
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");
31
+ const spend = Number(spendUsdc ?? 0);
32
+ 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);
41
36
  }
42
- if (spend > 0) bits.push(money(spend));
43
- else bits.push("$0");
44
- return `${label} ${bits.join(" / ")}`;
37
+ // free / zero spend: show traffic only
38
+ if (req <= 0 && Number(totalTokens ?? 0) <= 0) return "—";
39
+ return `${req}r`;
45
40
  }
46
41
 
47
42
  /**
48
- * Compact one-liner for Claude statusline / Codex Stop summary.
43
+ * One-line Claude statusline keep short (Claude only shows one row).
49
44
  *
50
- * Plain-language labels. Free models correctly show $0 billed while still
51
- * reporting request/token activity.
45
+ * Example:
46
+ * InferHub · sess 19r · day 129r · all $0.41 · bal $1.31 · top gpt-5.5
52
47
  */
53
48
  export function formatStatusLine(
54
49
  usage: AccountUsage,
@@ -56,57 +51,26 @@ export function formatStatusLine(
56
51
  sessionCostUsd?: number | null;
57
52
  contextPct?: number | null;
58
53
  model?: string | null;
59
- /** When true, include Claude local list-price estimate. Default false. */
60
54
  showEstimate?: boolean;
61
55
  },
62
56
  ): string {
63
- const model = extras?.model ? shortModel(extras.model) : null;
64
- const bal = money(usage.balance.amount_usdc);
65
- const top = shortModel(usage.top_model?.model);
66
- const ctx =
67
- extras?.contextPct != null && Number.isFinite(extras.contextPct)
68
- ? `${Math.round(extras.contextPct)}% context`
69
- : null;
70
-
71
57
  const parts: string[] = ["InferHub"];
72
- if (model) parts.push(model);
73
58
 
74
- // Latest conversation on InferHub (server-derived session), if present and useful
59
+ // Session (latest IH conversation) omit if empty
75
60
  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
- );
61
+ parts.push(`sess ${compact(usage.session.spend_usdc, usage.session.requests, usage.session.total_tokens)}`);
83
62
  }
84
63
 
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}`);
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)}`);
66
+ parts.push(`bal ${money(usage.balance.amount_usdc)}`);
67
+
68
+ const top = shortModel(usage.top_model?.model);
100
69
  if (top && top !== "—") parts.push(`top ${top}`);
101
- if (ctx) parts.push(ctx);
102
70
 
103
- if (
104
- extras?.showEstimate &&
105
- extras.sessionCostUsd != null &&
106
- Number.isFinite(extras.sessionCostUsd) &&
107
- extras.sessionCostUsd > 0
108
- ) {
109
- parts.push(`claude-est ${money(extras.sessionCostUsd)}`);
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)}%`);
110
74
  }
111
75
 
112
76
  return parts.join(" · ");