@inferhub/usage 0.1.0 → 0.1.2
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/bin/statusline.mjs +11 -7
- package/dist/client.d.ts +1 -1
- package/dist/client.js +9 -5
- package/dist/format.d.ts +8 -1
- package/dist/format.js +28 -9
- package/dist/format.test.js +54 -20
- package/dist/types.d.ts +12 -0
- package/package.json +1 -1
- package/src/client.ts +213 -205
- package/src/format.test.ts +61 -20
- package/src/format.ts +50 -9
- package/src/types.ts +12 -0
package/bin/statusline.mjs
CHANGED
|
@@ -22,11 +22,16 @@ try {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
try {
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
typeof session?.
|
|
28
|
-
|
|
29
|
-
|
|
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
|
+
|
|
31
|
+
const { usage } = await client.fetchAccountUsageAuto({
|
|
32
|
+
window: "day",
|
|
33
|
+
sessionId,
|
|
34
|
+
});
|
|
30
35
|
const contextPct =
|
|
31
36
|
typeof session?.context_window?.used_percentage === "number"
|
|
32
37
|
? session.context_window.used_percentage
|
|
@@ -34,13 +39,12 @@ try {
|
|
|
34
39
|
const model = session?.model?.display_name || session?.model?.id || null;
|
|
35
40
|
process.stdout.write(
|
|
36
41
|
client.formatStatusLine(usage, {
|
|
37
|
-
sessionCostUsd: sessionCost,
|
|
38
42
|
contextPct,
|
|
39
43
|
model,
|
|
40
44
|
}) + "\n",
|
|
41
45
|
);
|
|
42
46
|
} catch (e) {
|
|
43
47
|
const msg = e instanceof Error ? e.message : String(e);
|
|
44
|
-
process.stdout.write("
|
|
48
|
+
process.stdout.write("InferHub · offline (" + msg.slice(0, 48) + ")\n");
|
|
45
49
|
process.exitCode = 0;
|
|
46
50
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AccountUsage, FetchUsageOptions, ResolvedAuth, UsageWindowKind } from "./types.js";
|
|
2
2
|
export declare const DEFAULT_BASE_URL = "https://api.inferhub.dev/v1";
|
|
3
3
|
export declare function normalizeBaseUrl(input?: string | null): string;
|
|
4
|
-
export declare function usageUrl(baseUrl: string, window?: UsageWindowKind, tz?: string): string;
|
|
4
|
+
export declare function usageUrl(baseUrl: string, window?: UsageWindowKind, tz?: string, sessionId?: string): string;
|
|
5
5
|
/** Resolve API key + base URL from env / agent config files. */
|
|
6
6
|
export declare function resolveAuth(overrides?: Partial<ResolvedAuth>): ResolvedAuth | null;
|
|
7
7
|
export declare function fetchAccountUsage(opts: FetchUsageOptions): Promise<AccountUsage>;
|
package/dist/client.js
CHANGED
|
@@ -12,9 +12,11 @@ export function normalizeBaseUrl(input) {
|
|
|
12
12
|
}
|
|
13
13
|
return u;
|
|
14
14
|
}
|
|
15
|
-
export function usageUrl(baseUrl, window = "day", tz = "UTC") {
|
|
15
|
+
export function usageUrl(baseUrl, window = "day", tz = "UTC", sessionId) {
|
|
16
16
|
const base = normalizeBaseUrl(baseUrl);
|
|
17
17
|
const q = new URLSearchParams({ window, tz });
|
|
18
|
+
if (sessionId)
|
|
19
|
+
q.set("session_id", sessionId);
|
|
18
20
|
return `${base}/me/usage?${q.toString()}`;
|
|
19
21
|
}
|
|
20
22
|
/** Resolve API key + base URL from env / agent config files. */
|
|
@@ -86,9 +88,9 @@ function tryReadJson(path) {
|
|
|
86
88
|
return null;
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
|
-
function cacheKey(apiKey, window, tz, baseUrl) {
|
|
91
|
+
function cacheKey(apiKey, window, tz, baseUrl, sessionId = "") {
|
|
90
92
|
const h = createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
|
|
91
|
-
return `${h}|${normalizeBaseUrl(baseUrl)}|${window}|${tz}`;
|
|
93
|
+
return `${h}|${normalizeBaseUrl(baseUrl)}|${window}|${tz}|${sessionId}`;
|
|
92
94
|
}
|
|
93
95
|
function diskCachePath(key) {
|
|
94
96
|
const dir = process.env.CLAUDE_PLUGIN_DATA ||
|
|
@@ -106,9 +108,10 @@ function diskCachePath(key) {
|
|
|
106
108
|
export async function fetchAccountUsage(opts) {
|
|
107
109
|
const window = opts.window ?? "day";
|
|
108
110
|
const tz = opts.tz ?? "UTC";
|
|
111
|
+
const sessionId = opts.sessionId ?? "";
|
|
109
112
|
const baseUrl = normalizeBaseUrl(opts.baseUrl);
|
|
110
113
|
const ttlMs = (opts.cacheTtlSeconds ?? 30) * 1000;
|
|
111
|
-
const key = cacheKey(opts.apiKey, window, tz, baseUrl);
|
|
114
|
+
const key = cacheKey(opts.apiKey, window, tz, baseUrl, sessionId);
|
|
112
115
|
const now = Date.now();
|
|
113
116
|
if (!opts.force) {
|
|
114
117
|
const hit = mem.get(key);
|
|
@@ -128,7 +131,7 @@ export async function fetchAccountUsage(opts) {
|
|
|
128
131
|
/* ignore disk cache */
|
|
129
132
|
}
|
|
130
133
|
}
|
|
131
|
-
const url = usageUrl(baseUrl, window, tz);
|
|
134
|
+
const url = usageUrl(baseUrl, window, tz, sessionId || undefined);
|
|
132
135
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
133
136
|
if (!fetchImpl) {
|
|
134
137
|
throw new Error("fetch is not available in this runtime");
|
|
@@ -139,6 +142,7 @@ export async function fetchAccountUsage(opts) {
|
|
|
139
142
|
Authorization: `Bearer ${opts.apiKey}`,
|
|
140
143
|
"x-api-key": opts.apiKey,
|
|
141
144
|
Accept: "application/json",
|
|
145
|
+
...(sessionId ? { "X-Session-Id": sessionId } : {}),
|
|
142
146
|
},
|
|
143
147
|
signal: opts.signal,
|
|
144
148
|
});
|
package/dist/format.d.ts
CHANGED
|
@@ -2,11 +2,18 @@ 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
4
|
export declare function shortModel(model: string | null | undefined): string;
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Compact one-liner for Claude statusline / Codex Stop summary.
|
|
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.
|
|
10
|
+
*/
|
|
6
11
|
export declare function formatStatusLine(usage: AccountUsage, extras?: {
|
|
7
12
|
sessionCostUsd?: number | null;
|
|
8
13
|
contextPct?: number | null;
|
|
9
14
|
model?: string | null;
|
|
15
|
+
/** When true, include Claude's local cost estimate as `est $…`. Default false. */
|
|
16
|
+
showEstimate?: boolean;
|
|
10
17
|
}): string;
|
|
11
18
|
/** Multi-line human report for /usage skills. */
|
|
12
19
|
export declare function formatReport(usage: AccountUsage): string;
|
package/dist/format.js
CHANGED
|
@@ -20,12 +20,23 @@ export function shortModel(model) {
|
|
|
20
20
|
const parts = model.split("/");
|
|
21
21
|
return parts[parts.length - 1] || model;
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
function spendOrActivity(spendUsdc, requests, totalTokens) {
|
|
24
|
+
const spend = Number(spendUsdc ?? 0);
|
|
25
|
+
if (spend > 0)
|
|
26
|
+
return money(spend);
|
|
27
|
+
return `${requests}r/${tokens(totalTokens)}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Compact one-liner for Claude statusline / Codex Stop summary.
|
|
31
|
+
*
|
|
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.
|
|
34
|
+
*/
|
|
24
35
|
export function formatStatusLine(usage, extras) {
|
|
25
|
-
const sess =
|
|
26
|
-
?
|
|
36
|
+
const sess = usage.session
|
|
37
|
+
? spendOrActivity(usage.session.spend_usdc, usage.session.requests, usage.session.total_tokens)
|
|
27
38
|
: null;
|
|
28
|
-
const day =
|
|
39
|
+
const day = spendOrActivity(usage.window.spend_usdc, usage.window.requests, usage.window.total_tokens);
|
|
29
40
|
const all = money(usage.all_time.spend_usdc);
|
|
30
41
|
const bal = money(usage.balance.amount_usdc);
|
|
31
42
|
const top = shortModel(usage.top_model?.model);
|
|
@@ -33,7 +44,7 @@ export function formatStatusLine(usage, extras) {
|
|
|
33
44
|
const ctx = extras?.contextPct != null && Number.isFinite(extras.contextPct)
|
|
34
45
|
? `${Math.round(extras.contextPct)}%ctx`
|
|
35
46
|
: null;
|
|
36
|
-
const parts = ["
|
|
47
|
+
const parts = ["InferHub"];
|
|
37
48
|
if (model)
|
|
38
49
|
parts.push(model);
|
|
39
50
|
if (sess)
|
|
@@ -41,9 +52,16 @@ export function formatStatusLine(usage, extras) {
|
|
|
41
52
|
parts.push(`${usage.window.kind} ${day}`);
|
|
42
53
|
parts.push(`all ${all}`);
|
|
43
54
|
parts.push(`bal ${bal}`);
|
|
44
|
-
|
|
55
|
+
if (top && top !== "—")
|
|
56
|
+
parts.push(`top ${top}`);
|
|
45
57
|
if (ctx)
|
|
46
58
|
parts.push(ctx);
|
|
59
|
+
if (extras?.showEstimate &&
|
|
60
|
+
extras.sessionCostUsd != null &&
|
|
61
|
+
Number.isFinite(extras.sessionCostUsd) &&
|
|
62
|
+
extras.sessionCostUsd > 0) {
|
|
63
|
+
parts.push(`est ${money(extras.sessionCostUsd)}`);
|
|
64
|
+
}
|
|
47
65
|
return parts.join(" · ");
|
|
48
66
|
}
|
|
49
67
|
/** Multi-line human report for /usage skills. */
|
|
@@ -51,10 +69,11 @@ export function formatReport(usage) {
|
|
|
51
69
|
const lines = [
|
|
52
70
|
"InferHub usage",
|
|
53
71
|
` Balance: ${money(usage.balance.amount_usdc)} USDC`,
|
|
54
|
-
` ${usage.window.kind} window (${usage.window.tz}): ${money(usage.window.spend_usdc)} · ${usage.window.requests} req · ${tokens(usage.window.total_tokens)} tok`,
|
|
55
|
-
` All-time: ${money(usage.all_time.spend_usdc)} · ${usage.all_time.requests} req · ${tokens(usage.all_time.total_tokens)} tok`,
|
|
56
|
-
` Top model: ${usage.top_model?.model ?? "—"} (${money(usage.top_model?.spend_usdc ?? 0)}, ${usage.top_model?.requests ?? 0} req)`,
|
|
57
72
|
];
|
|
73
|
+
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)}…)`);
|
|
75
|
+
}
|
|
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)`);
|
|
58
77
|
if (usage.cache?.cached)
|
|
59
78
|
lines.push(` (cached, ttl ${usage.cache.ttl_seconds}s)`);
|
|
60
79
|
return lines.join("\n");
|
package/dist/format.test.js
CHANGED
|
@@ -7,8 +7,9 @@ test("normalizeBaseUrl adds /v1", () => {
|
|
|
7
7
|
});
|
|
8
8
|
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
|
+
assert.equal(usageUrl("https://api.inferhub.dev", "day", "UTC", "abc"), "https://api.inferhub.dev/v1/me/usage?window=day&tz=UTC&session_id=abc");
|
|
10
11
|
});
|
|
11
|
-
test("formatStatusLine", () => {
|
|
12
|
+
test("formatStatusLine is plain language", () => {
|
|
12
13
|
const usage = {
|
|
13
14
|
object: "account.usage",
|
|
14
15
|
currency: "USDC",
|
|
@@ -18,34 +19,67 @@ test("formatStatusLine", () => {
|
|
|
18
19
|
tz: "UTC",
|
|
19
20
|
since: "",
|
|
20
21
|
until: "",
|
|
21
|
-
requests:
|
|
22
|
-
prompt_tokens:
|
|
23
|
-
completion_tokens:
|
|
24
|
-
total_tokens:
|
|
25
|
-
spend_usdc: "0
|
|
22
|
+
requests: 86,
|
|
23
|
+
prompt_tokens: 14_000_000,
|
|
24
|
+
completion_tokens: 100_000,
|
|
25
|
+
total_tokens: 14_100_000,
|
|
26
|
+
spend_usdc: "0",
|
|
26
27
|
},
|
|
27
28
|
all_time: {
|
|
28
|
-
requests:
|
|
29
|
-
prompt_tokens:
|
|
30
|
-
completion_tokens:
|
|
31
|
-
total_tokens:
|
|
32
|
-
spend_usdc: "0.
|
|
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
|
+
},
|
|
35
|
+
session: {
|
|
36
|
+
id: "sess-1",
|
|
37
|
+
requests: 12,
|
|
38
|
+
prompt_tokens: 200_000,
|
|
39
|
+
completion_tokens: 5_000,
|
|
40
|
+
total_tokens: 205_000,
|
|
41
|
+
spend_usdc: "0",
|
|
42
|
+
since: null,
|
|
43
|
+
until: null,
|
|
33
44
|
},
|
|
34
45
|
top_model: {
|
|
35
|
-
model: "
|
|
36
|
-
requests:
|
|
37
|
-
tokens:
|
|
38
|
-
spend_usdc: "0.
|
|
46
|
+
model: "cx/gpt-5.5",
|
|
47
|
+
requests: 212,
|
|
48
|
+
tokens: 239830,
|
|
49
|
+
spend_usdc: "0.137155",
|
|
39
50
|
},
|
|
40
51
|
cache: { cached: false, ttl_seconds: 30 },
|
|
41
52
|
};
|
|
42
53
|
const line = formatStatusLine(usage, {
|
|
43
|
-
sessionCostUsd: 0.03,
|
|
44
|
-
contextPct: 12,
|
|
45
54
|
model: "free/grok/grok-4.5",
|
|
55
|
+
contextPct: 8,
|
|
56
|
+
sessionCostUsd: 2.69,
|
|
46
57
|
});
|
|
47
|
-
assert.match(line, /
|
|
48
|
-
assert.match(line, /
|
|
49
|
-
assert.match(line, /
|
|
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 = {
|
|
70
|
+
...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
|
+
},
|
|
81
|
+
};
|
|
82
|
+
const line2 = formatStatusLine(noSess, { model: "Grok" });
|
|
83
|
+
assert.doesNotMatch(line2, /session /);
|
|
50
84
|
assert.equal(money("0.001"), "$0.0010");
|
|
51
85
|
});
|
package/dist/types.d.ts
CHANGED
|
@@ -24,6 +24,16 @@ export type AccountUsage = {
|
|
|
24
24
|
total_tokens: number;
|
|
25
25
|
spend_usdc: string;
|
|
26
26
|
};
|
|
27
|
+
session: {
|
|
28
|
+
id: string;
|
|
29
|
+
requests: number;
|
|
30
|
+
prompt_tokens: number;
|
|
31
|
+
completion_tokens: number;
|
|
32
|
+
total_tokens: number;
|
|
33
|
+
spend_usdc: string;
|
|
34
|
+
since: string | null;
|
|
35
|
+
until: string | null;
|
|
36
|
+
} | null;
|
|
27
37
|
top_model: {
|
|
28
38
|
model: string;
|
|
29
39
|
requests: number;
|
|
@@ -41,6 +51,8 @@ export type FetchUsageOptions = {
|
|
|
41
51
|
baseUrl?: string;
|
|
42
52
|
window?: UsageWindowKind;
|
|
43
53
|
tz?: string;
|
|
54
|
+
/** Conversation session id (X-Session-Id / sessionid.Derive). */
|
|
55
|
+
sessionId?: string;
|
|
44
56
|
/** Force network; ignore process cache */
|
|
45
57
|
force?: boolean;
|
|
46
58
|
/** Client-side process cache TTL seconds (default 30) */
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -1,205 +1,213 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { homedir, tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import type { AccountUsage, FetchUsageOptions, ResolvedAuth, UsageWindowKind } from "./types.js";
|
|
6
|
-
|
|
7
|
-
export const DEFAULT_BASE_URL = "https://api.inferhub.dev/v1";
|
|
8
|
-
|
|
9
|
-
const mem = new Map<string, { expires: number; value: AccountUsage }>();
|
|
10
|
-
|
|
11
|
-
export function normalizeBaseUrl(input?: string | null): string {
|
|
12
|
-
let u = (input || DEFAULT_BASE_URL).trim().replace(/\/+$/, "");
|
|
13
|
-
// Accept API root without /v1
|
|
14
|
-
if (!u.endsWith("/v1")) {
|
|
15
|
-
u = `${u}/v1`;
|
|
16
|
-
}
|
|
17
|
-
return u;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function usageUrl(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
process.env.
|
|
44
|
-
process.env.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir, tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { AccountUsage, FetchUsageOptions, ResolvedAuth, UsageWindowKind } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_BASE_URL = "https://api.inferhub.dev/v1";
|
|
8
|
+
|
|
9
|
+
const mem = new Map<string, { expires: number; value: AccountUsage }>();
|
|
10
|
+
|
|
11
|
+
export function normalizeBaseUrl(input?: string | null): string {
|
|
12
|
+
let u = (input || DEFAULT_BASE_URL).trim().replace(/\/+$/, "");
|
|
13
|
+
// Accept API root without /v1
|
|
14
|
+
if (!u.endsWith("/v1")) {
|
|
15
|
+
u = `${u}/v1`;
|
|
16
|
+
}
|
|
17
|
+
return u;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function usageUrl(
|
|
21
|
+
baseUrl: string,
|
|
22
|
+
window: UsageWindowKind = "day",
|
|
23
|
+
tz = "UTC",
|
|
24
|
+
sessionId?: string,
|
|
25
|
+
): string {
|
|
26
|
+
const base = normalizeBaseUrl(baseUrl);
|
|
27
|
+
const q = new URLSearchParams({ window, tz });
|
|
28
|
+
if (sessionId) q.set("session_id", sessionId);
|
|
29
|
+
return `${base}/me/usage?${q.toString()}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Resolve API key + base URL from env / agent config files. */
|
|
33
|
+
export function resolveAuth(overrides: Partial<ResolvedAuth> = {}): ResolvedAuth | null {
|
|
34
|
+
if (overrides.apiKey) {
|
|
35
|
+
return {
|
|
36
|
+
apiKey: overrides.apiKey,
|
|
37
|
+
baseUrl: normalizeBaseUrl(overrides.baseUrl),
|
|
38
|
+
source: "override",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const envKey =
|
|
43
|
+
process.env.INFERHUB_API_KEY ||
|
|
44
|
+
process.env.ANTHROPIC_AUTH_TOKEN ||
|
|
45
|
+
process.env.ANTHROPIC_API_KEY ||
|
|
46
|
+
process.env.OPENAI_API_KEY;
|
|
47
|
+
const envBase =
|
|
48
|
+
process.env.INFERHUB_BASE_URL ||
|
|
49
|
+
process.env.ANTHROPIC_BASE_URL ||
|
|
50
|
+
process.env.OPENAI_BASE_URL;
|
|
51
|
+
|
|
52
|
+
if (envKey) {
|
|
53
|
+
return {
|
|
54
|
+
apiKey: envKey,
|
|
55
|
+
baseUrl: normalizeBaseUrl(envBase || DEFAULT_BASE_URL),
|
|
56
|
+
source: "env",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// OpenCode provider config
|
|
61
|
+
const oc = tryReadJson(join(homedir(), ".config", "opencode", "opencode.json"));
|
|
62
|
+
const ocKey = oc?.provider?.inferhub?.options?.apiKey;
|
|
63
|
+
const ocBase = oc?.provider?.inferhub?.options?.baseURL;
|
|
64
|
+
if (typeof ocKey === "string" && ocKey) {
|
|
65
|
+
return {
|
|
66
|
+
apiKey: ocKey,
|
|
67
|
+
baseUrl: normalizeBaseUrl(typeof ocBase === "string" ? ocBase : DEFAULT_BASE_URL),
|
|
68
|
+
source: "opencode.json",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Claude settings env
|
|
73
|
+
const claude = tryReadJson(join(homedir(), ".claude", "settings.json"));
|
|
74
|
+
const cKey =
|
|
75
|
+
claude?.env?.ANTHROPIC_AUTH_TOKEN ||
|
|
76
|
+
claude?.env?.ANTHROPIC_API_KEY ||
|
|
77
|
+
claude?.env?.INFERHUB_API_KEY;
|
|
78
|
+
const cBase = claude?.env?.ANTHROPIC_BASE_URL || claude?.env?.INFERHUB_BASE_URL;
|
|
79
|
+
if (typeof cKey === "string" && cKey) {
|
|
80
|
+
return {
|
|
81
|
+
apiKey: cKey,
|
|
82
|
+
baseUrl: normalizeBaseUrl(typeof cBase === "string" ? cBase : DEFAULT_BASE_URL),
|
|
83
|
+
source: "claude settings",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Codex auth.json
|
|
88
|
+
const codexAuth = tryReadJson(join(homedir(), ".codex", "auth.json"));
|
|
89
|
+
const codexKey = codexAuth?.OPENAI_API_KEY;
|
|
90
|
+
if (typeof codexKey === "string" && codexKey) {
|
|
91
|
+
return {
|
|
92
|
+
apiKey: codexKey,
|
|
93
|
+
baseUrl: normalizeBaseUrl(DEFAULT_BASE_URL),
|
|
94
|
+
source: "codex auth.json",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function tryReadJson(path: string): any | null {
|
|
102
|
+
try {
|
|
103
|
+
if (!existsSync(path)) return null;
|
|
104
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function cacheKey(apiKey: string, window: string, tz: string, baseUrl: string, sessionId = ""): string {
|
|
111
|
+
const h = createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
|
|
112
|
+
return `${h}|${normalizeBaseUrl(baseUrl)}|${window}|${tz}|${sessionId}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function diskCachePath(key: string): string {
|
|
116
|
+
const dir =
|
|
117
|
+
process.env.CLAUDE_PLUGIN_DATA ||
|
|
118
|
+
process.env.INFERHUB_CACHE_DIR ||
|
|
119
|
+
join(tmpdir(), "inferhub-usage");
|
|
120
|
+
try {
|
|
121
|
+
mkdirSync(dir, { recursive: true });
|
|
122
|
+
} catch {
|
|
123
|
+
/* ignore */
|
|
124
|
+
}
|
|
125
|
+
const safe = createHash("sha256").update(key).digest("hex").slice(0, 24);
|
|
126
|
+
return join(dir, `usage-${safe}.json`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function fetchAccountUsage(opts: FetchUsageOptions): Promise<AccountUsage> {
|
|
130
|
+
const window = opts.window ?? "day";
|
|
131
|
+
const tz = opts.tz ?? "UTC";
|
|
132
|
+
const sessionId = opts.sessionId ?? "";
|
|
133
|
+
const baseUrl = normalizeBaseUrl(opts.baseUrl);
|
|
134
|
+
const ttlMs = (opts.cacheTtlSeconds ?? 30) * 1000;
|
|
135
|
+
const key = cacheKey(opts.apiKey, window, tz, baseUrl, sessionId);
|
|
136
|
+
const now = Date.now();
|
|
137
|
+
|
|
138
|
+
if (!opts.force) {
|
|
139
|
+
const hit = mem.get(key);
|
|
140
|
+
if (hit && hit.expires > now) return hit.value;
|
|
141
|
+
try {
|
|
142
|
+
const p = diskCachePath(key);
|
|
143
|
+
if (existsSync(p)) {
|
|
144
|
+
const raw = JSON.parse(readFileSync(p, "utf8")) as {
|
|
145
|
+
expires: number;
|
|
146
|
+
value: AccountUsage;
|
|
147
|
+
};
|
|
148
|
+
if (raw.expires > now && raw.value?.object === "account.usage") {
|
|
149
|
+
mem.set(key, raw);
|
|
150
|
+
return raw.value;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
/* ignore disk cache */
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const url = usageUrl(baseUrl, window, tz, sessionId || undefined);
|
|
159
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
160
|
+
if (!fetchImpl) {
|
|
161
|
+
throw new Error("fetch is not available in this runtime");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const res = await fetchImpl(url, {
|
|
165
|
+
method: "GET",
|
|
166
|
+
headers: {
|
|
167
|
+
Authorization: `Bearer ${opts.apiKey}`,
|
|
168
|
+
"x-api-key": opts.apiKey,
|
|
169
|
+
Accept: "application/json",
|
|
170
|
+
...(sessionId ? { "X-Session-Id": sessionId } : {}),
|
|
171
|
+
},
|
|
172
|
+
signal: opts.signal,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (!res.ok) {
|
|
176
|
+
const body = await res.text().catch(() => "");
|
|
177
|
+
throw new Error(`InferHub usage HTTP ${res.status}: ${body.slice(0, 300)}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const value = (await res.json()) as AccountUsage;
|
|
181
|
+
if (!value || value.object !== "account.usage") {
|
|
182
|
+
throw new Error("unexpected usage response shape");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const entry = { expires: now + ttlMs, value };
|
|
186
|
+
mem.set(key, entry);
|
|
187
|
+
try {
|
|
188
|
+
writeFileSync(diskCachePath(key), JSON.stringify(entry));
|
|
189
|
+
} catch {
|
|
190
|
+
/* ignore */
|
|
191
|
+
}
|
|
192
|
+
return value;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function fetchAccountUsageAuto(
|
|
196
|
+
opts: Omit<FetchUsageOptions, "apiKey" | "baseUrl"> & {
|
|
197
|
+
apiKey?: string;
|
|
198
|
+
baseUrl?: string;
|
|
199
|
+
} = {},
|
|
200
|
+
): Promise<{ usage: AccountUsage; auth: ResolvedAuth }> {
|
|
201
|
+
const auth = resolveAuth({ apiKey: opts.apiKey, baseUrl: opts.baseUrl });
|
|
202
|
+
if (!auth) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
"No InferHub API key found. Set INFERHUB_API_KEY or configure Claude/Codex/OpenCode via @inferhub/helper.",
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const usage = await fetchAccountUsage({
|
|
208
|
+
...opts,
|
|
209
|
+
apiKey: auth.apiKey,
|
|
210
|
+
baseUrl: auth.baseUrl,
|
|
211
|
+
});
|
|
212
|
+
return { usage, auth };
|
|
213
|
+
}
|
package/src/format.test.ts
CHANGED
|
@@ -13,9 +13,13 @@ test("usageUrl", () => {
|
|
|
13
13
|
usageUrl("https://api.inferhub.dev", "day", "UTC"),
|
|
14
14
|
"https://api.inferhub.dev/v1/me/usage?window=day&tz=UTC",
|
|
15
15
|
);
|
|
16
|
+
assert.equal(
|
|
17
|
+
usageUrl("https://api.inferhub.dev", "day", "UTC", "abc"),
|
|
18
|
+
"https://api.inferhub.dev/v1/me/usage?window=day&tz=UTC&session_id=abc",
|
|
19
|
+
);
|
|
16
20
|
});
|
|
17
21
|
|
|
18
|
-
test("formatStatusLine", () => {
|
|
22
|
+
test("formatStatusLine is plain language", () => {
|
|
19
23
|
const usage: AccountUsage = {
|
|
20
24
|
object: "account.usage",
|
|
21
25
|
currency: "USDC",
|
|
@@ -25,34 +29,71 @@ test("formatStatusLine", () => {
|
|
|
25
29
|
tz: "UTC",
|
|
26
30
|
since: "",
|
|
27
31
|
until: "",
|
|
28
|
-
requests:
|
|
29
|
-
prompt_tokens:
|
|
30
|
-
completion_tokens:
|
|
31
|
-
total_tokens:
|
|
32
|
-
spend_usdc: "0
|
|
32
|
+
requests: 86,
|
|
33
|
+
prompt_tokens: 14_000_000,
|
|
34
|
+
completion_tokens: 100_000,
|
|
35
|
+
total_tokens: 14_100_000,
|
|
36
|
+
spend_usdc: "0",
|
|
33
37
|
},
|
|
34
38
|
all_time: {
|
|
35
|
-
requests:
|
|
36
|
-
prompt_tokens:
|
|
37
|
-
completion_tokens:
|
|
38
|
-
total_tokens:
|
|
39
|
-
spend_usdc: "0.
|
|
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
|
+
},
|
|
45
|
+
session: {
|
|
46
|
+
id: "sess-1",
|
|
47
|
+
requests: 12,
|
|
48
|
+
prompt_tokens: 200_000,
|
|
49
|
+
completion_tokens: 5_000,
|
|
50
|
+
total_tokens: 205_000,
|
|
51
|
+
spend_usdc: "0",
|
|
52
|
+
since: null,
|
|
53
|
+
until: null,
|
|
40
54
|
},
|
|
41
55
|
top_model: {
|
|
42
|
-
model: "
|
|
43
|
-
requests:
|
|
44
|
-
tokens:
|
|
45
|
-
spend_usdc: "0.
|
|
56
|
+
model: "cx/gpt-5.5",
|
|
57
|
+
requests: 212,
|
|
58
|
+
tokens: 239830,
|
|
59
|
+
spend_usdc: "0.137155",
|
|
46
60
|
},
|
|
47
61
|
cache: { cached: false, ttl_seconds: 30 },
|
|
48
62
|
};
|
|
63
|
+
|
|
49
64
|
const line = formatStatusLine(usage, {
|
|
50
|
-
sessionCostUsd: 0.03,
|
|
51
|
-
contextPct: 12,
|
|
52
65
|
model: "free/grok/grok-4.5",
|
|
66
|
+
contextPct: 8,
|
|
67
|
+
sessionCostUsd: 2.69,
|
|
53
68
|
});
|
|
54
|
-
|
|
55
|
-
assert.match(line, /
|
|
56
|
-
assert.match(line, /
|
|
69
|
+
|
|
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
|
|
80
|
+
|
|
81
|
+
// empty/zero session should be omitted (not "sess 0r/0")
|
|
82
|
+
const noSess: AccountUsage = {
|
|
83
|
+
...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
|
+
},
|
|
94
|
+
};
|
|
95
|
+
const line2 = formatStatusLine(noSess, { model: "Grok" });
|
|
96
|
+
assert.doesNotMatch(line2, /session /);
|
|
97
|
+
|
|
57
98
|
assert.equal(money("0.001"), "$0.0010");
|
|
58
99
|
});
|
package/src/format.ts
CHANGED
|
@@ -20,20 +20,44 @@ export function shortModel(model: string | null | undefined): string {
|
|
|
20
20
|
return parts[parts.length - 1] || model;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
function spendOrActivity(
|
|
24
|
+
spendUsdc: string | number | null | undefined,
|
|
25
|
+
requests: number,
|
|
26
|
+
totalTokens: number,
|
|
27
|
+
): string {
|
|
28
|
+
const spend = Number(spendUsdc ?? 0);
|
|
29
|
+
if (spend > 0) return money(spend);
|
|
30
|
+
return `${requests}r/${tokens(totalTokens)}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Compact one-liner for Claude statusline / Codex Stop summary.
|
|
35
|
+
*
|
|
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.
|
|
38
|
+
*/
|
|
24
39
|
export function formatStatusLine(
|
|
25
40
|
usage: AccountUsage,
|
|
26
41
|
extras?: {
|
|
27
42
|
sessionCostUsd?: number | null;
|
|
28
43
|
contextPct?: number | null;
|
|
29
44
|
model?: string | null;
|
|
45
|
+
/** When true, include Claude's local cost estimate as `est $…`. Default false. */
|
|
46
|
+
showEstimate?: boolean;
|
|
30
47
|
},
|
|
31
48
|
): string {
|
|
32
|
-
const sess =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
+
);
|
|
37
61
|
const all = money(usage.all_time.spend_usdc);
|
|
38
62
|
const bal = money(usage.balance.amount_usdc);
|
|
39
63
|
const top = shortModel(usage.top_model?.model);
|
|
@@ -43,14 +67,24 @@ export function formatStatusLine(
|
|
|
43
67
|
? `${Math.round(extras.contextPct)}%ctx`
|
|
44
68
|
: null;
|
|
45
69
|
|
|
46
|
-
const parts: string[] = ["
|
|
70
|
+
const parts: string[] = ["InferHub"];
|
|
47
71
|
if (model) parts.push(model);
|
|
48
72
|
if (sess) parts.push(`sess ${sess}`);
|
|
49
73
|
parts.push(`${usage.window.kind} ${day}`);
|
|
50
74
|
parts.push(`all ${all}`);
|
|
51
75
|
parts.push(`bal ${bal}`);
|
|
52
|
-
parts.push(`top ${top}`);
|
|
76
|
+
if (top && top !== "—") parts.push(`top ${top}`);
|
|
53
77
|
if (ctx) parts.push(ctx);
|
|
78
|
+
|
|
79
|
+
if (
|
|
80
|
+
extras?.showEstimate &&
|
|
81
|
+
extras.sessionCostUsd != null &&
|
|
82
|
+
Number.isFinite(extras.sessionCostUsd) &&
|
|
83
|
+
extras.sessionCostUsd > 0
|
|
84
|
+
) {
|
|
85
|
+
parts.push(`est ${money(extras.sessionCostUsd)}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
54
88
|
return parts.join(" · ");
|
|
55
89
|
}
|
|
56
90
|
|
|
@@ -59,10 +93,17 @@ export function formatReport(usage: AccountUsage): string {
|
|
|
59
93
|
const lines = [
|
|
60
94
|
"InferHub usage",
|
|
61
95
|
` Balance: ${money(usage.balance.amount_usdc)} USDC`,
|
|
96
|
+
];
|
|
97
|
+
if (usage.session) {
|
|
98
|
+
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)}…)`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
lines.push(
|
|
62
103
|
` ${usage.window.kind} window (${usage.window.tz}): ${money(usage.window.spend_usdc)} · ${usage.window.requests} req · ${tokens(usage.window.total_tokens)} tok`,
|
|
63
104
|
` All-time: ${money(usage.all_time.spend_usdc)} · ${usage.all_time.requests} req · ${tokens(usage.all_time.total_tokens)} tok`,
|
|
64
105
|
` Top model: ${usage.top_model?.model ?? "—"} (${money(usage.top_model?.spend_usdc ?? 0)}, ${usage.top_model?.requests ?? 0} req)`,
|
|
65
|
-
|
|
106
|
+
);
|
|
66
107
|
if (usage.cache?.cached) lines.push(` (cached, ttl ${usage.cache.ttl_seconds}s)`);
|
|
67
108
|
return lines.join("\n");
|
|
68
109
|
}
|
package/src/types.ts
CHANGED
|
@@ -25,6 +25,16 @@ export type AccountUsage = {
|
|
|
25
25
|
total_tokens: number;
|
|
26
26
|
spend_usdc: string;
|
|
27
27
|
};
|
|
28
|
+
session: {
|
|
29
|
+
id: string;
|
|
30
|
+
requests: number;
|
|
31
|
+
prompt_tokens: number;
|
|
32
|
+
completion_tokens: number;
|
|
33
|
+
total_tokens: number;
|
|
34
|
+
spend_usdc: string;
|
|
35
|
+
since: string | null;
|
|
36
|
+
until: string | null;
|
|
37
|
+
} | null;
|
|
28
38
|
top_model: {
|
|
29
39
|
model: string;
|
|
30
40
|
requests: number;
|
|
@@ -43,6 +53,8 @@ export type FetchUsageOptions = {
|
|
|
43
53
|
baseUrl?: string;
|
|
44
54
|
window?: UsageWindowKind;
|
|
45
55
|
tz?: string;
|
|
56
|
+
/** Conversation session id (X-Session-Id / sessionid.Derive). */
|
|
57
|
+
sessionId?: string;
|
|
46
58
|
/** Force network; ignore process cache */
|
|
47
59
|
force?: boolean;
|
|
48
60
|
/** Client-side process cache TTL seconds (default 30) */
|