@inferhub/usage 0.1.0
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 +46 -0
- package/bin/usage.mjs +33 -0
- package/dist/client.d.ts +14 -0
- package/dist/client.js +174 -0
- package/dist/format.d.ts +12 -0
- package/dist/format.js +61 -0
- package/dist/format.test.d.ts +1 -0
- package/dist/format.test.js +51 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/types.d.ts +55 -0
- package/dist/types.js +1 -0
- package/package.json +53 -0
- package/src/client.ts +205 -0
- package/src/format.test.ts +58 -0
- package/src/format.ts +68 -0
- package/src/index.ts +15 -0
- package/src/types.ts +58 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
|
|
6
|
+
const root = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const client = await import(pathToFileURL(join(root, "../dist/index.js")).href);
|
|
8
|
+
|
|
9
|
+
function readStdin() {
|
|
10
|
+
try {
|
|
11
|
+
return readFileSync(0, "utf8");
|
|
12
|
+
} catch {
|
|
13
|
+
return "{}";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let session = {};
|
|
18
|
+
try {
|
|
19
|
+
session = JSON.parse(readStdin() || "{}");
|
|
20
|
+
} catch {
|
|
21
|
+
session = {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const { usage } = await client.fetchAccountUsageAuto({ window: "day" });
|
|
26
|
+
const sessionCost =
|
|
27
|
+
typeof session?.cost?.total_cost_usd === "number"
|
|
28
|
+
? session.cost.total_cost_usd
|
|
29
|
+
: null;
|
|
30
|
+
const contextPct =
|
|
31
|
+
typeof session?.context_window?.used_percentage === "number"
|
|
32
|
+
? session.context_window.used_percentage
|
|
33
|
+
: null;
|
|
34
|
+
const model = session?.model?.display_name || session?.model?.id || null;
|
|
35
|
+
process.stdout.write(
|
|
36
|
+
client.formatStatusLine(usage, {
|
|
37
|
+
sessionCostUsd: sessionCost,
|
|
38
|
+
contextPct,
|
|
39
|
+
model,
|
|
40
|
+
}) + "\n",
|
|
41
|
+
);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
44
|
+
process.stdout.write("IH · offline (" + msg.slice(0, 48) + ")\n");
|
|
45
|
+
process.exitCode = 0;
|
|
46
|
+
}
|
package/bin/usage.mjs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
const root = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const client = await import(pathToFileURL(join(root, "../dist/index.js")).href);
|
|
7
|
+
|
|
8
|
+
const json = process.argv.includes("--json");
|
|
9
|
+
const force = process.argv.includes("--force");
|
|
10
|
+
const windowArg = process.argv.find((a) => a.startsWith("--window="));
|
|
11
|
+
const window = windowArg?.split("=")[1] || "day";
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const { usage, auth } = await client.fetchAccountUsageAuto({
|
|
15
|
+
window,
|
|
16
|
+
force,
|
|
17
|
+
});
|
|
18
|
+
if (json) {
|
|
19
|
+
process.stdout.write(
|
|
20
|
+
JSON.stringify(
|
|
21
|
+
{ auth: { source: auth.source, baseUrl: auth.baseUrl }, usage },
|
|
22
|
+
null,
|
|
23
|
+
2,
|
|
24
|
+
) + "\n",
|
|
25
|
+
);
|
|
26
|
+
} else {
|
|
27
|
+
process.stdout.write(client.formatReport(usage) + "\n");
|
|
28
|
+
process.stdout.write(" (key source: " + auth.source + ")\n");
|
|
29
|
+
}
|
|
30
|
+
} catch (e) {
|
|
31
|
+
console.error(e instanceof Error ? e.message : e);
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AccountUsage, FetchUsageOptions, ResolvedAuth, UsageWindowKind } from "./types.js";
|
|
2
|
+
export declare const DEFAULT_BASE_URL = "https://api.inferhub.dev/v1";
|
|
3
|
+
export declare function normalizeBaseUrl(input?: string | null): string;
|
|
4
|
+
export declare function usageUrl(baseUrl: string, window?: UsageWindowKind, tz?: string): string;
|
|
5
|
+
/** Resolve API key + base URL from env / agent config files. */
|
|
6
|
+
export declare function resolveAuth(overrides?: Partial<ResolvedAuth>): ResolvedAuth | null;
|
|
7
|
+
export declare function fetchAccountUsage(opts: FetchUsageOptions): Promise<AccountUsage>;
|
|
8
|
+
export declare function fetchAccountUsageAuto(opts?: Omit<FetchUsageOptions, "apiKey" | "baseUrl"> & {
|
|
9
|
+
apiKey?: string;
|
|
10
|
+
baseUrl?: string;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
usage: AccountUsage;
|
|
13
|
+
auth: ResolvedAuth;
|
|
14
|
+
}>;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
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
|
+
export const DEFAULT_BASE_URL = "https://api.inferhub.dev/v1";
|
|
6
|
+
const mem = new Map();
|
|
7
|
+
export function normalizeBaseUrl(input) {
|
|
8
|
+
let u = (input || DEFAULT_BASE_URL).trim().replace(/\/+$/, "");
|
|
9
|
+
// Accept API root without /v1
|
|
10
|
+
if (!u.endsWith("/v1")) {
|
|
11
|
+
u = `${u}/v1`;
|
|
12
|
+
}
|
|
13
|
+
return u;
|
|
14
|
+
}
|
|
15
|
+
export function usageUrl(baseUrl, window = "day", tz = "UTC") {
|
|
16
|
+
const base = normalizeBaseUrl(baseUrl);
|
|
17
|
+
const q = new URLSearchParams({ window, tz });
|
|
18
|
+
return `${base}/me/usage?${q.toString()}`;
|
|
19
|
+
}
|
|
20
|
+
/** Resolve API key + base URL from env / agent config files. */
|
|
21
|
+
export function resolveAuth(overrides = {}) {
|
|
22
|
+
if (overrides.apiKey) {
|
|
23
|
+
return {
|
|
24
|
+
apiKey: overrides.apiKey,
|
|
25
|
+
baseUrl: normalizeBaseUrl(overrides.baseUrl),
|
|
26
|
+
source: "override",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const envKey = process.env.INFERHUB_API_KEY ||
|
|
30
|
+
process.env.ANTHROPIC_AUTH_TOKEN ||
|
|
31
|
+
process.env.ANTHROPIC_API_KEY ||
|
|
32
|
+
process.env.OPENAI_API_KEY;
|
|
33
|
+
const envBase = process.env.INFERHUB_BASE_URL ||
|
|
34
|
+
process.env.ANTHROPIC_BASE_URL ||
|
|
35
|
+
process.env.OPENAI_BASE_URL;
|
|
36
|
+
if (envKey) {
|
|
37
|
+
return {
|
|
38
|
+
apiKey: envKey,
|
|
39
|
+
baseUrl: normalizeBaseUrl(envBase || DEFAULT_BASE_URL),
|
|
40
|
+
source: "env",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// OpenCode provider config
|
|
44
|
+
const oc = tryReadJson(join(homedir(), ".config", "opencode", "opencode.json"));
|
|
45
|
+
const ocKey = oc?.provider?.inferhub?.options?.apiKey;
|
|
46
|
+
const ocBase = oc?.provider?.inferhub?.options?.baseURL;
|
|
47
|
+
if (typeof ocKey === "string" && ocKey) {
|
|
48
|
+
return {
|
|
49
|
+
apiKey: ocKey,
|
|
50
|
+
baseUrl: normalizeBaseUrl(typeof ocBase === "string" ? ocBase : DEFAULT_BASE_URL),
|
|
51
|
+
source: "opencode.json",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// Claude settings env
|
|
55
|
+
const claude = tryReadJson(join(homedir(), ".claude", "settings.json"));
|
|
56
|
+
const cKey = claude?.env?.ANTHROPIC_AUTH_TOKEN ||
|
|
57
|
+
claude?.env?.ANTHROPIC_API_KEY ||
|
|
58
|
+
claude?.env?.INFERHUB_API_KEY;
|
|
59
|
+
const cBase = claude?.env?.ANTHROPIC_BASE_URL || claude?.env?.INFERHUB_BASE_URL;
|
|
60
|
+
if (typeof cKey === "string" && cKey) {
|
|
61
|
+
return {
|
|
62
|
+
apiKey: cKey,
|
|
63
|
+
baseUrl: normalizeBaseUrl(typeof cBase === "string" ? cBase : DEFAULT_BASE_URL),
|
|
64
|
+
source: "claude settings",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// Codex auth.json
|
|
68
|
+
const codexAuth = tryReadJson(join(homedir(), ".codex", "auth.json"));
|
|
69
|
+
const codexKey = codexAuth?.OPENAI_API_KEY;
|
|
70
|
+
if (typeof codexKey === "string" && codexKey) {
|
|
71
|
+
return {
|
|
72
|
+
apiKey: codexKey,
|
|
73
|
+
baseUrl: normalizeBaseUrl(DEFAULT_BASE_URL),
|
|
74
|
+
source: "codex auth.json",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
function tryReadJson(path) {
|
|
80
|
+
try {
|
|
81
|
+
if (!existsSync(path))
|
|
82
|
+
return null;
|
|
83
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function cacheKey(apiKey, window, tz, baseUrl) {
|
|
90
|
+
const h = createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
|
|
91
|
+
return `${h}|${normalizeBaseUrl(baseUrl)}|${window}|${tz}`;
|
|
92
|
+
}
|
|
93
|
+
function diskCachePath(key) {
|
|
94
|
+
const dir = process.env.CLAUDE_PLUGIN_DATA ||
|
|
95
|
+
process.env.INFERHUB_CACHE_DIR ||
|
|
96
|
+
join(tmpdir(), "inferhub-usage");
|
|
97
|
+
try {
|
|
98
|
+
mkdirSync(dir, { recursive: true });
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
/* ignore */
|
|
102
|
+
}
|
|
103
|
+
const safe = createHash("sha256").update(key).digest("hex").slice(0, 24);
|
|
104
|
+
return join(dir, `usage-${safe}.json`);
|
|
105
|
+
}
|
|
106
|
+
export async function fetchAccountUsage(opts) {
|
|
107
|
+
const window = opts.window ?? "day";
|
|
108
|
+
const tz = opts.tz ?? "UTC";
|
|
109
|
+
const baseUrl = normalizeBaseUrl(opts.baseUrl);
|
|
110
|
+
const ttlMs = (opts.cacheTtlSeconds ?? 30) * 1000;
|
|
111
|
+
const key = cacheKey(opts.apiKey, window, tz, baseUrl);
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
if (!opts.force) {
|
|
114
|
+
const hit = mem.get(key);
|
|
115
|
+
if (hit && hit.expires > now)
|
|
116
|
+
return hit.value;
|
|
117
|
+
try {
|
|
118
|
+
const p = diskCachePath(key);
|
|
119
|
+
if (existsSync(p)) {
|
|
120
|
+
const raw = JSON.parse(readFileSync(p, "utf8"));
|
|
121
|
+
if (raw.expires > now && raw.value?.object === "account.usage") {
|
|
122
|
+
mem.set(key, raw);
|
|
123
|
+
return raw.value;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
/* ignore disk cache */
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const url = usageUrl(baseUrl, window, tz);
|
|
132
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
133
|
+
if (!fetchImpl) {
|
|
134
|
+
throw new Error("fetch is not available in this runtime");
|
|
135
|
+
}
|
|
136
|
+
const res = await fetchImpl(url, {
|
|
137
|
+
method: "GET",
|
|
138
|
+
headers: {
|
|
139
|
+
Authorization: `Bearer ${opts.apiKey}`,
|
|
140
|
+
"x-api-key": opts.apiKey,
|
|
141
|
+
Accept: "application/json",
|
|
142
|
+
},
|
|
143
|
+
signal: opts.signal,
|
|
144
|
+
});
|
|
145
|
+
if (!res.ok) {
|
|
146
|
+
const body = await res.text().catch(() => "");
|
|
147
|
+
throw new Error(`InferHub usage HTTP ${res.status}: ${body.slice(0, 300)}`);
|
|
148
|
+
}
|
|
149
|
+
const value = (await res.json());
|
|
150
|
+
if (!value || value.object !== "account.usage") {
|
|
151
|
+
throw new Error("unexpected usage response shape");
|
|
152
|
+
}
|
|
153
|
+
const entry = { expires: now + ttlMs, value };
|
|
154
|
+
mem.set(key, entry);
|
|
155
|
+
try {
|
|
156
|
+
writeFileSync(diskCachePath(key), JSON.stringify(entry));
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
/* ignore */
|
|
160
|
+
}
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
export async function fetchAccountUsageAuto(opts = {}) {
|
|
164
|
+
const auth = resolveAuth({ apiKey: opts.apiKey, baseUrl: opts.baseUrl });
|
|
165
|
+
if (!auth) {
|
|
166
|
+
throw new Error("No InferHub API key found. Set INFERHUB_API_KEY or configure Claude/Codex/OpenCode via @inferhub/helper.");
|
|
167
|
+
}
|
|
168
|
+
const usage = await fetchAccountUsage({
|
|
169
|
+
...opts,
|
|
170
|
+
apiKey: auth.apiKey,
|
|
171
|
+
baseUrl: auth.baseUrl,
|
|
172
|
+
});
|
|
173
|
+
return { usage, auth };
|
|
174
|
+
}
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AccountUsage } from "./types.js";
|
|
2
|
+
export declare function money(usdc: string | number | null | undefined, digits?: number): string;
|
|
3
|
+
export declare function tokens(n: number | null | undefined): string;
|
|
4
|
+
export declare function shortModel(model: string | null | undefined): string;
|
|
5
|
+
/** Compact one-liner for Claude statusline / Codex Stop summary. */
|
|
6
|
+
export declare function formatStatusLine(usage: AccountUsage, extras?: {
|
|
7
|
+
sessionCostUsd?: number | null;
|
|
8
|
+
contextPct?: number | null;
|
|
9
|
+
model?: string | null;
|
|
10
|
+
}): string;
|
|
11
|
+
/** Multi-line human report for /usage skills. */
|
|
12
|
+
export declare function formatReport(usage: AccountUsage): string;
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export function money(usdc, digits = 2) {
|
|
2
|
+
const n = Number(usdc ?? 0);
|
|
3
|
+
if (!Number.isFinite(n))
|
|
4
|
+
return "$0.00";
|
|
5
|
+
if (Math.abs(n) > 0 && Math.abs(n) < 0.01)
|
|
6
|
+
return `$${n.toFixed(4)}`;
|
|
7
|
+
return `$${n.toFixed(digits)}`;
|
|
8
|
+
}
|
|
9
|
+
export function tokens(n) {
|
|
10
|
+
const v = Number(n ?? 0);
|
|
11
|
+
if (v >= 1_000_000)
|
|
12
|
+
return `${(v / 1_000_000).toFixed(1)}M`;
|
|
13
|
+
if (v >= 1_000)
|
|
14
|
+
return `${(v / 1_000).toFixed(1)}k`;
|
|
15
|
+
return String(Math.round(v));
|
|
16
|
+
}
|
|
17
|
+
export function shortModel(model) {
|
|
18
|
+
if (!model)
|
|
19
|
+
return "—";
|
|
20
|
+
const parts = model.split("/");
|
|
21
|
+
return parts[parts.length - 1] || model;
|
|
22
|
+
}
|
|
23
|
+
/** Compact one-liner for Claude statusline / Codex Stop summary. */
|
|
24
|
+
export function formatStatusLine(usage, extras) {
|
|
25
|
+
const sess = extras?.sessionCostUsd != null && Number.isFinite(extras.sessionCostUsd)
|
|
26
|
+
? money(extras.sessionCostUsd)
|
|
27
|
+
: null;
|
|
28
|
+
const day = money(usage.window.spend_usdc);
|
|
29
|
+
const all = money(usage.all_time.spend_usdc);
|
|
30
|
+
const bal = money(usage.balance.amount_usdc);
|
|
31
|
+
const top = shortModel(usage.top_model?.model);
|
|
32
|
+
const model = extras?.model ? shortModel(extras.model) : null;
|
|
33
|
+
const ctx = extras?.contextPct != null && Number.isFinite(extras.contextPct)
|
|
34
|
+
? `${Math.round(extras.contextPct)}%ctx`
|
|
35
|
+
: null;
|
|
36
|
+
const parts = ["IH"];
|
|
37
|
+
if (model)
|
|
38
|
+
parts.push(model);
|
|
39
|
+
if (sess)
|
|
40
|
+
parts.push(`sess ${sess}`);
|
|
41
|
+
parts.push(`${usage.window.kind} ${day}`);
|
|
42
|
+
parts.push(`all ${all}`);
|
|
43
|
+
parts.push(`bal ${bal}`);
|
|
44
|
+
parts.push(`top ${top}`);
|
|
45
|
+
if (ctx)
|
|
46
|
+
parts.push(ctx);
|
|
47
|
+
return parts.join(" · ");
|
|
48
|
+
}
|
|
49
|
+
/** Multi-line human report for /usage skills. */
|
|
50
|
+
export function formatReport(usage) {
|
|
51
|
+
const lines = [
|
|
52
|
+
"InferHub usage",
|
|
53
|
+
` 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
|
+
];
|
|
58
|
+
if (usage.cache?.cached)
|
|
59
|
+
lines.push(` (cached, ttl ${usage.cache.ttl_seconds}s)`);
|
|
60
|
+
return lines.join("\n");
|
|
61
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { formatStatusLine, money, normalizeBaseUrl, usageUrl } from "./index.js";
|
|
4
|
+
test("normalizeBaseUrl adds /v1", () => {
|
|
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
|
+
});
|
|
8
|
+
test("usageUrl", () => {
|
|
9
|
+
assert.equal(usageUrl("https://api.inferhub.dev", "day", "UTC"), "https://api.inferhub.dev/v1/me/usage?window=day&tz=UTC");
|
|
10
|
+
});
|
|
11
|
+
test("formatStatusLine", () => {
|
|
12
|
+
const usage = {
|
|
13
|
+
object: "account.usage",
|
|
14
|
+
currency: "USDC",
|
|
15
|
+
balance: { amount_usdc: "1.250000", updated_at: null },
|
|
16
|
+
window: {
|
|
17
|
+
kind: "day",
|
|
18
|
+
tz: "UTC",
|
|
19
|
+
since: "",
|
|
20
|
+
until: "",
|
|
21
|
+
requests: 3,
|
|
22
|
+
prompt_tokens: 100,
|
|
23
|
+
completion_tokens: 50,
|
|
24
|
+
total_tokens: 150,
|
|
25
|
+
spend_usdc: "0.012000",
|
|
26
|
+
},
|
|
27
|
+
all_time: {
|
|
28
|
+
requests: 10,
|
|
29
|
+
prompt_tokens: 1000,
|
|
30
|
+
completion_tokens: 500,
|
|
31
|
+
total_tokens: 1500,
|
|
32
|
+
spend_usdc: "0.500000",
|
|
33
|
+
},
|
|
34
|
+
top_model: {
|
|
35
|
+
model: "free/grok/grok-4.5",
|
|
36
|
+
requests: 8,
|
|
37
|
+
tokens: 1200,
|
|
38
|
+
spend_usdc: "0.400000",
|
|
39
|
+
},
|
|
40
|
+
cache: { cached: false, ttl_seconds: 30 },
|
|
41
|
+
};
|
|
42
|
+
const line = formatStatusLine(usage, {
|
|
43
|
+
sessionCostUsd: 0.03,
|
|
44
|
+
contextPct: 12,
|
|
45
|
+
model: "free/grok/grok-4.5",
|
|
46
|
+
});
|
|
47
|
+
assert.match(line, /IH/);
|
|
48
|
+
assert.match(line, /bal \$1\.25/);
|
|
49
|
+
assert.match(line, /top grok-4\.5/);
|
|
50
|
+
assert.equal(money("0.001"), "$0.0010");
|
|
51
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export type { AccountUsage, FetchUsageOptions, ResolvedAuth, UsageWindowKind, } from "./types.js";
|
|
2
|
+
export { DEFAULT_BASE_URL, fetchAccountUsage, fetchAccountUsageAuto, normalizeBaseUrl, resolveAuth, usageUrl, } from "./client.js";
|
|
3
|
+
export { formatReport, formatStatusLine, money, shortModel, tokens } from "./format.js";
|
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type UsageWindowKind = "day" | "24h" | "7d" | "30d";
|
|
2
|
+
export type AccountUsage = {
|
|
3
|
+
object: "account.usage";
|
|
4
|
+
currency: string;
|
|
5
|
+
balance: {
|
|
6
|
+
amount_usdc: string;
|
|
7
|
+
updated_at: string | null;
|
|
8
|
+
};
|
|
9
|
+
window: {
|
|
10
|
+
kind: UsageWindowKind | string;
|
|
11
|
+
tz: string;
|
|
12
|
+
since: string;
|
|
13
|
+
until: string;
|
|
14
|
+
requests: number;
|
|
15
|
+
prompt_tokens: number;
|
|
16
|
+
completion_tokens: number;
|
|
17
|
+
total_tokens: number;
|
|
18
|
+
spend_usdc: string;
|
|
19
|
+
};
|
|
20
|
+
all_time: {
|
|
21
|
+
requests: number;
|
|
22
|
+
prompt_tokens: number;
|
|
23
|
+
completion_tokens: number;
|
|
24
|
+
total_tokens: number;
|
|
25
|
+
spend_usdc: string;
|
|
26
|
+
};
|
|
27
|
+
top_model: {
|
|
28
|
+
model: string;
|
|
29
|
+
requests: number;
|
|
30
|
+
tokens: number;
|
|
31
|
+
spend_usdc: string;
|
|
32
|
+
} | null;
|
|
33
|
+
cache: {
|
|
34
|
+
cached: boolean;
|
|
35
|
+
ttl_seconds: number;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
export type FetchUsageOptions = {
|
|
39
|
+
apiKey: string;
|
|
40
|
+
/** OpenAI-style base ending in /v1, or API root. Default https://api.inferhub.dev/v1 */
|
|
41
|
+
baseUrl?: string;
|
|
42
|
+
window?: UsageWindowKind;
|
|
43
|
+
tz?: string;
|
|
44
|
+
/** Force network; ignore process cache */
|
|
45
|
+
force?: boolean;
|
|
46
|
+
/** Client-side process cache TTL seconds (default 30) */
|
|
47
|
+
cacheTtlSeconds?: number;
|
|
48
|
+
fetchImpl?: typeof fetch;
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
};
|
|
51
|
+
export type ResolvedAuth = {
|
|
52
|
+
apiKey: string;
|
|
53
|
+
baseUrl: string;
|
|
54
|
+
source: string;
|
|
55
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@inferhub/usage",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared InferHub account usage client for coding-agent plugins",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src",
|
|
17
|
+
"bin"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json",
|
|
21
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
22
|
+
"test": "node --test dist/*.test.js 2>nul || node --import tsx --test src/*.test.ts",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^22.15.0",
|
|
27
|
+
"tsx": "^4.19.0",
|
|
28
|
+
"typescript": "^5.8.2"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"bin": {
|
|
35
|
+
"inferhub-statusline": "bin/statusline.mjs",
|
|
36
|
+
"inferhub-usage": "bin/usage.mjs"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/inferhub/plugins.git",
|
|
44
|
+
"directory": "packages/client"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://inferhub.dev",
|
|
47
|
+
"keywords": [
|
|
48
|
+
"inferhub",
|
|
49
|
+
"usage",
|
|
50
|
+
"statusline",
|
|
51
|
+
"billing"
|
|
52
|
+
]
|
|
53
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,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(baseUrl: string, window: UsageWindowKind = "day", tz = "UTC"): string {
|
|
21
|
+
const base = normalizeBaseUrl(baseUrl);
|
|
22
|
+
const q = new URLSearchParams({ window, tz });
|
|
23
|
+
return `${base}/me/usage?${q.toString()}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Resolve API key + base URL from env / agent config files. */
|
|
27
|
+
export function resolveAuth(overrides: Partial<ResolvedAuth> = {}): ResolvedAuth | null {
|
|
28
|
+
if (overrides.apiKey) {
|
|
29
|
+
return {
|
|
30
|
+
apiKey: overrides.apiKey,
|
|
31
|
+
baseUrl: normalizeBaseUrl(overrides.baseUrl),
|
|
32
|
+
source: "override",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const envKey =
|
|
37
|
+
process.env.INFERHUB_API_KEY ||
|
|
38
|
+
process.env.ANTHROPIC_AUTH_TOKEN ||
|
|
39
|
+
process.env.ANTHROPIC_API_KEY ||
|
|
40
|
+
process.env.OPENAI_API_KEY;
|
|
41
|
+
const envBase =
|
|
42
|
+
process.env.INFERHUB_BASE_URL ||
|
|
43
|
+
process.env.ANTHROPIC_BASE_URL ||
|
|
44
|
+
process.env.OPENAI_BASE_URL;
|
|
45
|
+
|
|
46
|
+
if (envKey) {
|
|
47
|
+
return {
|
|
48
|
+
apiKey: envKey,
|
|
49
|
+
baseUrl: normalizeBaseUrl(envBase || DEFAULT_BASE_URL),
|
|
50
|
+
source: "env",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// OpenCode provider config
|
|
55
|
+
const oc = tryReadJson(join(homedir(), ".config", "opencode", "opencode.json"));
|
|
56
|
+
const ocKey = oc?.provider?.inferhub?.options?.apiKey;
|
|
57
|
+
const ocBase = oc?.provider?.inferhub?.options?.baseURL;
|
|
58
|
+
if (typeof ocKey === "string" && ocKey) {
|
|
59
|
+
return {
|
|
60
|
+
apiKey: ocKey,
|
|
61
|
+
baseUrl: normalizeBaseUrl(typeof ocBase === "string" ? ocBase : DEFAULT_BASE_URL),
|
|
62
|
+
source: "opencode.json",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Claude settings env
|
|
67
|
+
const claude = tryReadJson(join(homedir(), ".claude", "settings.json"));
|
|
68
|
+
const cKey =
|
|
69
|
+
claude?.env?.ANTHROPIC_AUTH_TOKEN ||
|
|
70
|
+
claude?.env?.ANTHROPIC_API_KEY ||
|
|
71
|
+
claude?.env?.INFERHUB_API_KEY;
|
|
72
|
+
const cBase = claude?.env?.ANTHROPIC_BASE_URL || claude?.env?.INFERHUB_BASE_URL;
|
|
73
|
+
if (typeof cKey === "string" && cKey) {
|
|
74
|
+
return {
|
|
75
|
+
apiKey: cKey,
|
|
76
|
+
baseUrl: normalizeBaseUrl(typeof cBase === "string" ? cBase : DEFAULT_BASE_URL),
|
|
77
|
+
source: "claude settings",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Codex auth.json
|
|
82
|
+
const codexAuth = tryReadJson(join(homedir(), ".codex", "auth.json"));
|
|
83
|
+
const codexKey = codexAuth?.OPENAI_API_KEY;
|
|
84
|
+
if (typeof codexKey === "string" && codexKey) {
|
|
85
|
+
return {
|
|
86
|
+
apiKey: codexKey,
|
|
87
|
+
baseUrl: normalizeBaseUrl(DEFAULT_BASE_URL),
|
|
88
|
+
source: "codex auth.json",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function tryReadJson(path: string): any | null {
|
|
96
|
+
try {
|
|
97
|
+
if (!existsSync(path)) return null;
|
|
98
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function cacheKey(apiKey: string, window: string, tz: string, baseUrl: string): string {
|
|
105
|
+
const h = createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
|
|
106
|
+
return `${h}|${normalizeBaseUrl(baseUrl)}|${window}|${tz}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function diskCachePath(key: string): string {
|
|
110
|
+
const dir =
|
|
111
|
+
process.env.CLAUDE_PLUGIN_DATA ||
|
|
112
|
+
process.env.INFERHUB_CACHE_DIR ||
|
|
113
|
+
join(tmpdir(), "inferhub-usage");
|
|
114
|
+
try {
|
|
115
|
+
mkdirSync(dir, { recursive: true });
|
|
116
|
+
} catch {
|
|
117
|
+
/* ignore */
|
|
118
|
+
}
|
|
119
|
+
const safe = createHash("sha256").update(key).digest("hex").slice(0, 24);
|
|
120
|
+
return join(dir, `usage-${safe}.json`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function fetchAccountUsage(opts: FetchUsageOptions): Promise<AccountUsage> {
|
|
124
|
+
const window = opts.window ?? "day";
|
|
125
|
+
const tz = opts.tz ?? "UTC";
|
|
126
|
+
const baseUrl = normalizeBaseUrl(opts.baseUrl);
|
|
127
|
+
const ttlMs = (opts.cacheTtlSeconds ?? 30) * 1000;
|
|
128
|
+
const key = cacheKey(opts.apiKey, window, tz, baseUrl);
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
|
|
131
|
+
if (!opts.force) {
|
|
132
|
+
const hit = mem.get(key);
|
|
133
|
+
if (hit && hit.expires > now) return hit.value;
|
|
134
|
+
try {
|
|
135
|
+
const p = diskCachePath(key);
|
|
136
|
+
if (existsSync(p)) {
|
|
137
|
+
const raw = JSON.parse(readFileSync(p, "utf8")) as {
|
|
138
|
+
expires: number;
|
|
139
|
+
value: AccountUsage;
|
|
140
|
+
};
|
|
141
|
+
if (raw.expires > now && raw.value?.object === "account.usage") {
|
|
142
|
+
mem.set(key, raw);
|
|
143
|
+
return raw.value;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
/* ignore disk cache */
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const url = usageUrl(baseUrl, window, tz);
|
|
152
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
153
|
+
if (!fetchImpl) {
|
|
154
|
+
throw new Error("fetch is not available in this runtime");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const res = await fetchImpl(url, {
|
|
158
|
+
method: "GET",
|
|
159
|
+
headers: {
|
|
160
|
+
Authorization: `Bearer ${opts.apiKey}`,
|
|
161
|
+
"x-api-key": opts.apiKey,
|
|
162
|
+
Accept: "application/json",
|
|
163
|
+
},
|
|
164
|
+
signal: opts.signal,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (!res.ok) {
|
|
168
|
+
const body = await res.text().catch(() => "");
|
|
169
|
+
throw new Error(`InferHub usage HTTP ${res.status}: ${body.slice(0, 300)}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const value = (await res.json()) as AccountUsage;
|
|
173
|
+
if (!value || value.object !== "account.usage") {
|
|
174
|
+
throw new Error("unexpected usage response shape");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const entry = { expires: now + ttlMs, value };
|
|
178
|
+
mem.set(key, entry);
|
|
179
|
+
try {
|
|
180
|
+
writeFileSync(diskCachePath(key), JSON.stringify(entry));
|
|
181
|
+
} catch {
|
|
182
|
+
/* ignore */
|
|
183
|
+
}
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function fetchAccountUsageAuto(
|
|
188
|
+
opts: Omit<FetchUsageOptions, "apiKey" | "baseUrl"> & {
|
|
189
|
+
apiKey?: string;
|
|
190
|
+
baseUrl?: string;
|
|
191
|
+
} = {},
|
|
192
|
+
): Promise<{ usage: AccountUsage; auth: ResolvedAuth }> {
|
|
193
|
+
const auth = resolveAuth({ apiKey: opts.apiKey, baseUrl: opts.baseUrl });
|
|
194
|
+
if (!auth) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
"No InferHub API key found. Set INFERHUB_API_KEY or configure Claude/Codex/OpenCode via @inferhub/helper.",
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const usage = await fetchAccountUsage({
|
|
200
|
+
...opts,
|
|
201
|
+
apiKey: auth.apiKey,
|
|
202
|
+
baseUrl: auth.baseUrl,
|
|
203
|
+
});
|
|
204
|
+
return { usage, auth };
|
|
205
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { formatStatusLine, money, normalizeBaseUrl, usageUrl } from "./index.js";
|
|
4
|
+
import type { AccountUsage } from "./types.js";
|
|
5
|
+
|
|
6
|
+
test("normalizeBaseUrl adds /v1", () => {
|
|
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
|
+
});
|
|
10
|
+
|
|
11
|
+
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
|
+
});
|
|
17
|
+
|
|
18
|
+
test("formatStatusLine", () => {
|
|
19
|
+
const usage: AccountUsage = {
|
|
20
|
+
object: "account.usage",
|
|
21
|
+
currency: "USDC",
|
|
22
|
+
balance: { amount_usdc: "1.250000", updated_at: null },
|
|
23
|
+
window: {
|
|
24
|
+
kind: "day",
|
|
25
|
+
tz: "UTC",
|
|
26
|
+
since: "",
|
|
27
|
+
until: "",
|
|
28
|
+
requests: 3,
|
|
29
|
+
prompt_tokens: 100,
|
|
30
|
+
completion_tokens: 50,
|
|
31
|
+
total_tokens: 150,
|
|
32
|
+
spend_usdc: "0.012000",
|
|
33
|
+
},
|
|
34
|
+
all_time: {
|
|
35
|
+
requests: 10,
|
|
36
|
+
prompt_tokens: 1000,
|
|
37
|
+
completion_tokens: 500,
|
|
38
|
+
total_tokens: 1500,
|
|
39
|
+
spend_usdc: "0.500000",
|
|
40
|
+
},
|
|
41
|
+
top_model: {
|
|
42
|
+
model: "free/grok/grok-4.5",
|
|
43
|
+
requests: 8,
|
|
44
|
+
tokens: 1200,
|
|
45
|
+
spend_usdc: "0.400000",
|
|
46
|
+
},
|
|
47
|
+
cache: { cached: false, ttl_seconds: 30 },
|
|
48
|
+
};
|
|
49
|
+
const line = formatStatusLine(usage, {
|
|
50
|
+
sessionCostUsd: 0.03,
|
|
51
|
+
contextPct: 12,
|
|
52
|
+
model: "free/grok/grok-4.5",
|
|
53
|
+
});
|
|
54
|
+
assert.match(line, /IH/);
|
|
55
|
+
assert.match(line, /bal \$1\.25/);
|
|
56
|
+
assert.match(line, /top grok-4\.5/);
|
|
57
|
+
assert.equal(money("0.001"), "$0.0010");
|
|
58
|
+
});
|
package/src/format.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { AccountUsage } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export function money(usdc: string | number | null | undefined, digits = 2): string {
|
|
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)}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function tokens(n: number | null | undefined): string {
|
|
11
|
+
const v = Number(n ?? 0);
|
|
12
|
+
if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`;
|
|
13
|
+
if (v >= 1_000) return `${(v / 1_000).toFixed(1)}k`;
|
|
14
|
+
return String(Math.round(v));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function shortModel(model: string | null | undefined): string {
|
|
18
|
+
if (!model) return "—";
|
|
19
|
+
const parts = model.split("/");
|
|
20
|
+
return parts[parts.length - 1] || model;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Compact one-liner for Claude statusline / Codex Stop summary. */
|
|
24
|
+
export function formatStatusLine(
|
|
25
|
+
usage: AccountUsage,
|
|
26
|
+
extras?: {
|
|
27
|
+
sessionCostUsd?: number | null;
|
|
28
|
+
contextPct?: number | null;
|
|
29
|
+
model?: string | null;
|
|
30
|
+
},
|
|
31
|
+
): string {
|
|
32
|
+
const sess =
|
|
33
|
+
extras?.sessionCostUsd != null && Number.isFinite(extras.sessionCostUsd)
|
|
34
|
+
? money(extras.sessionCostUsd)
|
|
35
|
+
: null;
|
|
36
|
+
const day = money(usage.window.spend_usdc);
|
|
37
|
+
const all = money(usage.all_time.spend_usdc);
|
|
38
|
+
const bal = money(usage.balance.amount_usdc);
|
|
39
|
+
const top = shortModel(usage.top_model?.model);
|
|
40
|
+
const model = extras?.model ? shortModel(extras.model) : null;
|
|
41
|
+
const ctx =
|
|
42
|
+
extras?.contextPct != null && Number.isFinite(extras.contextPct)
|
|
43
|
+
? `${Math.round(extras.contextPct)}%ctx`
|
|
44
|
+
: null;
|
|
45
|
+
|
|
46
|
+
const parts: string[] = ["IH"];
|
|
47
|
+
if (model) parts.push(model);
|
|
48
|
+
if (sess) parts.push(`sess ${sess}`);
|
|
49
|
+
parts.push(`${usage.window.kind} ${day}`);
|
|
50
|
+
parts.push(`all ${all}`);
|
|
51
|
+
parts.push(`bal ${bal}`);
|
|
52
|
+
parts.push(`top ${top}`);
|
|
53
|
+
if (ctx) parts.push(ctx);
|
|
54
|
+
return parts.join(" · ");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Multi-line human report for /usage skills. */
|
|
58
|
+
export function formatReport(usage: AccountUsage): string {
|
|
59
|
+
const lines = [
|
|
60
|
+
"InferHub usage",
|
|
61
|
+
` Balance: ${money(usage.balance.amount_usdc)} USDC`,
|
|
62
|
+
` ${usage.window.kind} window (${usage.window.tz}): ${money(usage.window.spend_usdc)} · ${usage.window.requests} req · ${tokens(usage.window.total_tokens)} tok`,
|
|
63
|
+
` All-time: ${money(usage.all_time.spend_usdc)} · ${usage.all_time.requests} req · ${tokens(usage.all_time.total_tokens)} tok`,
|
|
64
|
+
` Top model: ${usage.top_model?.model ?? "—"} (${money(usage.top_model?.spend_usdc ?? 0)}, ${usage.top_model?.requests ?? 0} req)`,
|
|
65
|
+
];
|
|
66
|
+
if (usage.cache?.cached) lines.push(` (cached, ttl ${usage.cache.ttl_seconds}s)`);
|
|
67
|
+
return lines.join("\n");
|
|
68
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
AccountUsage,
|
|
3
|
+
FetchUsageOptions,
|
|
4
|
+
ResolvedAuth,
|
|
5
|
+
UsageWindowKind,
|
|
6
|
+
} from "./types.js";
|
|
7
|
+
export {
|
|
8
|
+
DEFAULT_BASE_URL,
|
|
9
|
+
fetchAccountUsage,
|
|
10
|
+
fetchAccountUsageAuto,
|
|
11
|
+
normalizeBaseUrl,
|
|
12
|
+
resolveAuth,
|
|
13
|
+
usageUrl,
|
|
14
|
+
} from "./client.js";
|
|
15
|
+
export { formatReport, formatStatusLine, money, shortModel, tokens } from "./format.js";
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export type UsageWindowKind = "day" | "24h" | "7d" | "30d";
|
|
2
|
+
|
|
3
|
+
export type AccountUsage = {
|
|
4
|
+
object: "account.usage";
|
|
5
|
+
currency: string;
|
|
6
|
+
balance: {
|
|
7
|
+
amount_usdc: string;
|
|
8
|
+
updated_at: string | null;
|
|
9
|
+
};
|
|
10
|
+
window: {
|
|
11
|
+
kind: UsageWindowKind | string;
|
|
12
|
+
tz: string;
|
|
13
|
+
since: string;
|
|
14
|
+
until: string;
|
|
15
|
+
requests: number;
|
|
16
|
+
prompt_tokens: number;
|
|
17
|
+
completion_tokens: number;
|
|
18
|
+
total_tokens: number;
|
|
19
|
+
spend_usdc: string;
|
|
20
|
+
};
|
|
21
|
+
all_time: {
|
|
22
|
+
requests: number;
|
|
23
|
+
prompt_tokens: number;
|
|
24
|
+
completion_tokens: number;
|
|
25
|
+
total_tokens: number;
|
|
26
|
+
spend_usdc: string;
|
|
27
|
+
};
|
|
28
|
+
top_model: {
|
|
29
|
+
model: string;
|
|
30
|
+
requests: number;
|
|
31
|
+
tokens: number;
|
|
32
|
+
spend_usdc: string;
|
|
33
|
+
} | null;
|
|
34
|
+
cache: {
|
|
35
|
+
cached: boolean;
|
|
36
|
+
ttl_seconds: number;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type FetchUsageOptions = {
|
|
41
|
+
apiKey: string;
|
|
42
|
+
/** OpenAI-style base ending in /v1, or API root. Default https://api.inferhub.dev/v1 */
|
|
43
|
+
baseUrl?: string;
|
|
44
|
+
window?: UsageWindowKind;
|
|
45
|
+
tz?: string;
|
|
46
|
+
/** Force network; ignore process cache */
|
|
47
|
+
force?: boolean;
|
|
48
|
+
/** Client-side process cache TTL seconds (default 30) */
|
|
49
|
+
cacheTtlSeconds?: number;
|
|
50
|
+
fetchImpl?: typeof fetch;
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type ResolvedAuth = {
|
|
55
|
+
apiKey: string;
|
|
56
|
+
baseUrl: string;
|
|
57
|
+
source: string;
|
|
58
|
+
};
|