@diousk/pi-usage-fast 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/LICENSE +21 -0
- package/README.md +259 -0
- package/dist/index.ts +5478 -0
- package/dist/index.ts.map +7 -0
- package/docs/operations.md +31 -0
- package/docs/providers.md +273 -0
- package/package.json +82 -0
- package/src/codex-fast-runtime.ts +184 -0
- package/src/codex-fast.ts +118 -0
- package/src/codex-resets.ts +347 -0
- package/src/core.ts +245 -0
- package/src/format.ts +678 -0
- package/src/index.ts +116 -0
- package/src/oauth-credential-source.ts +117 -0
- package/src/providers/baseten.ts +51 -0
- package/src/providers/codex.ts +139 -0
- package/src/providers/deepseek.ts +80 -0
- package/src/providers/fireworks.ts +291 -0
- package/src/providers/github-copilot.ts +114 -0
- package/src/providers/kimi-coding.ts +265 -0
- package/src/providers/minimax.ts +280 -0
- package/src/providers/moonshot.ts +59 -0
- package/src/providers/opencode-zen.ts +80 -0
- package/src/providers/openrouter.ts +70 -0
- package/src/providers/vercel-ai-gateway.ts +43 -0
- package/src/providers/xai.ts +179 -0
- package/src/providers/zai-errors.ts +94 -0
- package/src/providers/zai.ts +239 -0
- package/src/query.ts +1073 -0
- package/src/settings.ts +366 -0
- package/src/types.ts +235 -0
- package/src/usage-helpers.ts +40 -0
- package/src/usage-settings-ui.ts +152 -0
- package/src/usage-targets.ts +127 -0
- package/src/usage.ts +1335 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { sanitizeDisplayText } from "../core.js";
|
|
2
|
+
import type { MiniMaxUsagePayload, UsageBucket, UsageMetric, UsageReport } from "../types.js";
|
|
3
|
+
|
|
4
|
+
export type MiniMaxProviderId = "minimax" | "minimax-cn";
|
|
5
|
+
export type MiniMaxUsageKind = "token-plan" | "account-balance";
|
|
6
|
+
|
|
7
|
+
const PROVIDERS = {
|
|
8
|
+
minimax: { name: "MiniMax", currency: "USD" },
|
|
9
|
+
"minimax-cn": { name: "MiniMax CN", currency: "CNY" },
|
|
10
|
+
} as const;
|
|
11
|
+
const DECIMAL_AMOUNT = /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/u;
|
|
12
|
+
const PERCENT_TOLERANCE = 1;
|
|
13
|
+
|
|
14
|
+
export function miniMaxUsageKind(apiKey: string): MiniMaxUsageKind {
|
|
15
|
+
return apiKey.startsWith("sk-api-") ? "account-balance" : "token-plan";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizeMiniMaxUsagePayload(
|
|
19
|
+
providerId: MiniMaxProviderId,
|
|
20
|
+
kind: MiniMaxUsageKind,
|
|
21
|
+
payload: MiniMaxUsagePayload,
|
|
22
|
+
capturedAt: number,
|
|
23
|
+
): UsageReport {
|
|
24
|
+
return kind === "account-balance"
|
|
25
|
+
? normalizeBalance(providerId, payload, capturedAt)
|
|
26
|
+
: normalizeTokenPlan(providerId, payload, capturedAt);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeBalance(
|
|
30
|
+
providerId: MiniMaxProviderId,
|
|
31
|
+
payload: MiniMaxUsagePayload,
|
|
32
|
+
capturedAt: number,
|
|
33
|
+
): UsageReport {
|
|
34
|
+
assertSuccess(payload);
|
|
35
|
+
const provider = PROVIDERS[providerId];
|
|
36
|
+
const metrics: UsageMetric[] = [
|
|
37
|
+
balanceMetric("available-balance", "Available balance", payload.available_amount, provider.currency),
|
|
38
|
+
balanceMetric("cash-balance", "Cash balance", payload.cash_balance, provider.currency, true),
|
|
39
|
+
balanceMetric("voucher-balance", "Voucher balance", payload.voucher_balance, provider.currency),
|
|
40
|
+
balanceMetric("credit-balance", "Credit balance", payload.credit_balance, provider.currency),
|
|
41
|
+
balanceMetric("owed-amount", "Owed amount", payload.owed_amount, provider.currency),
|
|
42
|
+
];
|
|
43
|
+
return {
|
|
44
|
+
providerId,
|
|
45
|
+
providerName: provider.name,
|
|
46
|
+
capturedAt,
|
|
47
|
+
source: "minimax-account-balance",
|
|
48
|
+
semantics: { kind: "api-key", label: "MiniMax pay-as-you-go account balance" },
|
|
49
|
+
buckets: [],
|
|
50
|
+
metrics,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeTokenPlan(
|
|
55
|
+
providerId: MiniMaxProviderId,
|
|
56
|
+
payload: MiniMaxUsagePayload,
|
|
57
|
+
capturedAt: number,
|
|
58
|
+
): UsageReport {
|
|
59
|
+
assertSuccess(payload);
|
|
60
|
+
if (!Array.isArray(payload.model_remains) || payload.model_remains.length === 0) {
|
|
61
|
+
throw new Error("MiniMax Token Plan returned no quota rows.");
|
|
62
|
+
}
|
|
63
|
+
const provider = PROVIDERS[providerId];
|
|
64
|
+
const buckets: UsageBucket[] = [];
|
|
65
|
+
const groups = new Set<string>();
|
|
66
|
+
for (const [index, raw] of payload.model_remains.entries()) {
|
|
67
|
+
const row = asObject(raw);
|
|
68
|
+
if (!row) throw new Error("MiniMax Token Plan quota row was not an object.");
|
|
69
|
+
const groupLabel = safeLabel(row.model_name, `Quota ${index + 1}`);
|
|
70
|
+
const groupId = uniqueGroupId(groupLabel, index, groups);
|
|
71
|
+
buckets.push(
|
|
72
|
+
normalizeWindow(row, {
|
|
73
|
+
id: `${groupId}:interval`,
|
|
74
|
+
label: "Rolling window",
|
|
75
|
+
groupId,
|
|
76
|
+
groupLabel,
|
|
77
|
+
countField: "current_interval_usage_count",
|
|
78
|
+
totalField: "current_interval_total_count",
|
|
79
|
+
percentField: "current_interval_remaining_percent",
|
|
80
|
+
statusField: "current_interval_status",
|
|
81
|
+
startField: "start_time",
|
|
82
|
+
endField: "end_time",
|
|
83
|
+
}),
|
|
84
|
+
normalizeWindow(row, {
|
|
85
|
+
id: `${groupId}:weekly`,
|
|
86
|
+
label: "Weekly window",
|
|
87
|
+
groupId,
|
|
88
|
+
groupLabel,
|
|
89
|
+
countField: "current_weekly_usage_count",
|
|
90
|
+
totalField: "current_weekly_total_count",
|
|
91
|
+
percentField: "current_weekly_remaining_percent",
|
|
92
|
+
statusField: "current_weekly_status",
|
|
93
|
+
startField: "weekly_start_time",
|
|
94
|
+
endField: "weekly_end_time",
|
|
95
|
+
boostPermille: row.weekly_boost_permille,
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
providerId,
|
|
101
|
+
providerName: provider.name,
|
|
102
|
+
capturedAt,
|
|
103
|
+
source: "minimax-token-plan",
|
|
104
|
+
semantics: { kind: "consumer-subscription", label: "MiniMax Token Plan quota" },
|
|
105
|
+
buckets,
|
|
106
|
+
metrics: [],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
type WindowFields = {
|
|
111
|
+
id: string;
|
|
112
|
+
label: string;
|
|
113
|
+
groupId: string;
|
|
114
|
+
groupLabel: string;
|
|
115
|
+
countField: string;
|
|
116
|
+
totalField: string;
|
|
117
|
+
percentField: string;
|
|
118
|
+
statusField: string;
|
|
119
|
+
startField: string;
|
|
120
|
+
endField: string;
|
|
121
|
+
boostPermille?: unknown;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
function normalizeWindow(row: Record<string, unknown>, fields: WindowFields): UsageBucket {
|
|
125
|
+
const status = optionalInteger(row[fields.statusField], fields.statusField);
|
|
126
|
+
if (status !== undefined && ![1, 2, 3].includes(status)) {
|
|
127
|
+
throw new Error(`MiniMax Token Plan ${fields.label} status was unsupported.`);
|
|
128
|
+
}
|
|
129
|
+
const percent = optionalPercent(row[fields.percentField], fields.percentField);
|
|
130
|
+
validateBoost(fields.boostPermille);
|
|
131
|
+
const start = timestamp(row[fields.startField], fields.startField);
|
|
132
|
+
const end = timestamp(row[fields.endField], fields.endField);
|
|
133
|
+
if (end < start) throw new Error(`MiniMax Token Plan ${fields.label} timestamps were reversed.`);
|
|
134
|
+
const resetsAt = Math.floor(end / 1_000);
|
|
135
|
+
const windowMinutes = Math.max(1, Math.round((end - start) / 60_000));
|
|
136
|
+
if (status === 3) {
|
|
137
|
+
return {
|
|
138
|
+
id: fields.id,
|
|
139
|
+
label: fields.label,
|
|
140
|
+
groupId: fields.groupId,
|
|
141
|
+
groupLabel: fields.groupLabel,
|
|
142
|
+
remaining: 100,
|
|
143
|
+
unit: "percent",
|
|
144
|
+
period: "unlimited",
|
|
145
|
+
windowMinutes,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const total = nonnegativeInteger(row[fields.totalField], fields.totalField);
|
|
149
|
+
const count = nonnegativeInteger(row[fields.countField], fields.countField);
|
|
150
|
+
if (total === 0) {
|
|
151
|
+
if (count !== 0) {
|
|
152
|
+
throw new Error(`MiniMax Token Plan ${fields.label} counts were inconsistent.`);
|
|
153
|
+
}
|
|
154
|
+
if (percent === undefined) {
|
|
155
|
+
throw new Error(`MiniMax Token Plan ${fields.label} returned no quota and no percent.`);
|
|
156
|
+
}
|
|
157
|
+
// Token Plan reports remaining_percent as the quota when both counts are 0.
|
|
158
|
+
return {
|
|
159
|
+
id: fields.id,
|
|
160
|
+
label: fields.label,
|
|
161
|
+
groupId: fields.groupId,
|
|
162
|
+
groupLabel: fields.groupLabel,
|
|
163
|
+
remaining: percent,
|
|
164
|
+
used: 100 - percent,
|
|
165
|
+
limit: 0,
|
|
166
|
+
unit: "percent",
|
|
167
|
+
windowMinutes,
|
|
168
|
+
resetsAt,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const resolved = resolveQuotaCounts(count, total, percent);
|
|
172
|
+
if (!resolved) throw new Error(`MiniMax Token Plan ${fields.label} counts were inconsistent.`);
|
|
173
|
+
return {
|
|
174
|
+
id: fields.id,
|
|
175
|
+
label: fields.label,
|
|
176
|
+
groupId: fields.groupId,
|
|
177
|
+
groupLabel: fields.groupLabel,
|
|
178
|
+
...resolved,
|
|
179
|
+
unit: "count",
|
|
180
|
+
windowMinutes,
|
|
181
|
+
resetsAt,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function resolveQuotaCounts(
|
|
186
|
+
reportedCount: number,
|
|
187
|
+
total: number,
|
|
188
|
+
remainingPercent: number | undefined,
|
|
189
|
+
): Pick<UsageBucket, "used" | "remaining" | "limit"> | undefined {
|
|
190
|
+
if (total <= 0 || reportedCount > total) return undefined;
|
|
191
|
+
let remaining = reportedCount;
|
|
192
|
+
if (remainingPercent !== undefined) {
|
|
193
|
+
const asRemaining = (reportedCount / total) * 100;
|
|
194
|
+
const asUsed = ((total - reportedCount) / total) * 100;
|
|
195
|
+
const remainingDistance = Math.abs(asRemaining - remainingPercent);
|
|
196
|
+
const usedDistance = Math.abs(asUsed - remainingPercent);
|
|
197
|
+
if (Math.min(remainingDistance, usedDistance) > PERCENT_TOLERANCE) return undefined;
|
|
198
|
+
if (usedDistance < remainingDistance) remaining = total - reportedCount;
|
|
199
|
+
}
|
|
200
|
+
return { used: total - remaining, remaining, limit: total };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function assertSuccess(payload: MiniMaxUsagePayload): void {
|
|
204
|
+
const base = asObject(payload.base_resp);
|
|
205
|
+
if (base?.status_code !== 0) {
|
|
206
|
+
throw new Error("MiniMax usage response did not report success.");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function balanceMetric(
|
|
211
|
+
id: string,
|
|
212
|
+
label: string,
|
|
213
|
+
value: unknown,
|
|
214
|
+
currency: "CNY" | "USD",
|
|
215
|
+
allowNegative = false,
|
|
216
|
+
): UsageMetric {
|
|
217
|
+
if (
|
|
218
|
+
typeof value !== "string" ||
|
|
219
|
+
value.length > 64 ||
|
|
220
|
+
!DECIMAL_AMOUNT.test(value) ||
|
|
221
|
+
(!allowNegative && value.startsWith("-"))
|
|
222
|
+
) {
|
|
223
|
+
throw new Error(`MiniMax ${label.toLowerCase()} was not a valid amount.`);
|
|
224
|
+
}
|
|
225
|
+
return { id, label, value, unit: "currency", currency };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function asObject(value: unknown): Record<string, unknown> | undefined {
|
|
229
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
230
|
+
return value as Record<string, unknown>;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function safeLabel(value: unknown, fallback: string): string {
|
|
234
|
+
if (typeof value !== "string") throw new Error("MiniMax Token Plan model name was not a string.");
|
|
235
|
+
return sanitizeDisplayText(value, 80) || fallback;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function uniqueGroupId(label: string, index: number, groups: Set<string>): string {
|
|
239
|
+
const base =
|
|
240
|
+
label
|
|
241
|
+
.toLowerCase()
|
|
242
|
+
.replace(/[^a-z0-9]+/gu, "-")
|
|
243
|
+
.replace(/^-|-$/gu, "") || "quota";
|
|
244
|
+
const id = groups.has(base) ? `${base}-${index + 1}` : base;
|
|
245
|
+
groups.add(id);
|
|
246
|
+
return id;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function nonnegativeInteger(value: unknown, field: string): number {
|
|
250
|
+
if (!Number.isSafeInteger(value) || (value as number) < 0) {
|
|
251
|
+
throw new Error(`MiniMax Token Plan ${field} was not a nonnegative safe integer.`);
|
|
252
|
+
}
|
|
253
|
+
return value as number;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function optionalInteger(value: unknown, field: string): number | undefined {
|
|
257
|
+
if (value === undefined || value === null) return undefined;
|
|
258
|
+
return nonnegativeInteger(value, field);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function optionalPercent(value: unknown, field: string): number | undefined {
|
|
262
|
+
if (value === undefined || value === null) return undefined;
|
|
263
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 100) {
|
|
264
|
+
throw new Error(`MiniMax Token Plan ${field} was not a percentage.`);
|
|
265
|
+
}
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function validateBoost(boost: unknown): void {
|
|
270
|
+
if (boost === undefined || boost === null) return;
|
|
271
|
+
const permille = nonnegativeInteger(boost, "weekly_boost_permille");
|
|
272
|
+
if (permille > 10_000) throw new Error("MiniMax Token Plan weekly boost was unreasonable.");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function timestamp(value: unknown, field: string): number {
|
|
276
|
+
if (!Number.isSafeInteger(value) || (value as number) <= 0) {
|
|
277
|
+
throw new Error(`MiniMax Token Plan ${field} was not a valid timestamp.`);
|
|
278
|
+
}
|
|
279
|
+
return value as number;
|
|
280
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { MoonshotBalancePayload, UsageMetric, UsageReport } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export type MoonshotProviderId = "moonshotai" | "moonshotai-cn";
|
|
4
|
+
|
|
5
|
+
const PROVIDERS = {
|
|
6
|
+
moonshotai: { name: "Moonshot AI", currency: "USD" },
|
|
7
|
+
"moonshotai-cn": { name: "Moonshot AI CN", currency: "CNY" },
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
export function normalizeMoonshotBalancePayload(
|
|
11
|
+
providerId: MoonshotProviderId,
|
|
12
|
+
payload: MoonshotBalancePayload,
|
|
13
|
+
capturedAt: number,
|
|
14
|
+
): UsageReport {
|
|
15
|
+
if (payload.code !== 0 || payload.status !== true) {
|
|
16
|
+
throw new Error("Moonshot AI balance response did not report success.");
|
|
17
|
+
}
|
|
18
|
+
const data = asObject(payload.data);
|
|
19
|
+
if (!data) throw new Error("Moonshot AI balance response data was not an object.");
|
|
20
|
+
const provider = PROVIDERS[providerId];
|
|
21
|
+
const available = amount(data.available_balance, "available balance", false);
|
|
22
|
+
const voucher = amount(data.voucher_balance, "voucher balance", false);
|
|
23
|
+
const cash = amount(data.cash_balance, "cash balance", true);
|
|
24
|
+
const metrics: UsageMetric[] = [
|
|
25
|
+
currencyMetric("available-balance", "Available balance", available, provider.currency),
|
|
26
|
+
currencyMetric("voucher-balance", "Voucher balance", voucher, provider.currency),
|
|
27
|
+
currencyMetric("cash-balance", "Cash balance", cash, provider.currency),
|
|
28
|
+
];
|
|
29
|
+
return {
|
|
30
|
+
providerId,
|
|
31
|
+
providerName: provider.name,
|
|
32
|
+
capturedAt,
|
|
33
|
+
source: "moonshot-balance",
|
|
34
|
+
semantics: { kind: "api-key", label: "Moonshot API account balance" },
|
|
35
|
+
buckets: [],
|
|
36
|
+
metrics,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function currencyMetric(id: string, label: string, value: string, currency: "CNY" | "USD"): UsageMetric {
|
|
41
|
+
return { id, label, value, unit: "currency", currency };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function asObject(value: unknown): Record<string, unknown> | undefined {
|
|
45
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
46
|
+
return value as Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function amount(value: unknown, label: string, allowNegative: boolean): string {
|
|
50
|
+
if (
|
|
51
|
+
typeof value !== "number" ||
|
|
52
|
+
!Number.isFinite(value) ||
|
|
53
|
+
(!allowNegative && value < 0) ||
|
|
54
|
+
Math.abs(value) > Number.MAX_SAFE_INTEGER
|
|
55
|
+
) {
|
|
56
|
+
throw new Error(`Moonshot AI ${label} was not a valid amount.`);
|
|
57
|
+
}
|
|
58
|
+
return String(value);
|
|
59
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { sanitizeDisplayText } from "../core.js";
|
|
2
|
+
import type { OpenCodeZenPayload, UsageBucket, UsageReport } from "../types.js";
|
|
3
|
+
|
|
4
|
+
const ZEN_WINDOWS = [
|
|
5
|
+
{ key: "rolling", label: "Rolling" },
|
|
6
|
+
{ key: "weekly", label: "Weekly" },
|
|
7
|
+
{ key: "monthly", label: "Monthly" },
|
|
8
|
+
] as const;
|
|
9
|
+
|
|
10
|
+
export function normalizeOpenCodeZenPayload(payload: OpenCodeZenPayload, capturedAt: number): UsageReport {
|
|
11
|
+
const usage = asObject(payload.usage);
|
|
12
|
+
if (!usage) throw new Error("OpenCode Zen usage response was not an object.");
|
|
13
|
+
|
|
14
|
+
const buckets: UsageBucket[] = [];
|
|
15
|
+
const notes: string[] = [];
|
|
16
|
+
for (const window of ZEN_WINDOWS) {
|
|
17
|
+
const raw = asObject(usage[window.key]);
|
|
18
|
+
if (!raw) continue;
|
|
19
|
+
const status = asString(raw.status);
|
|
20
|
+
if (status !== "ok" && status !== "rate-limited") {
|
|
21
|
+
notes.push(`${window.label} window unavailable (${status ?? "unknown status"}).`);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const used = asNonnegativeNumber(raw.percent);
|
|
25
|
+
if (used === undefined) continue;
|
|
26
|
+
const resetsAt = asEpochSeconds(raw.resetsAt);
|
|
27
|
+
buckets.push({
|
|
28
|
+
id: window.key,
|
|
29
|
+
label: `${window.label} window`,
|
|
30
|
+
used,
|
|
31
|
+
remaining: 100 - clampPercent(used),
|
|
32
|
+
limit: 100,
|
|
33
|
+
unit: "percent",
|
|
34
|
+
...(resetsAt !== undefined ? { resetsAt } : {}),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (buckets.length === 0) {
|
|
38
|
+
throw new Error("OpenCode Zen usage endpoint returned no displayable usage data.");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
providerId: "opencode-go",
|
|
43
|
+
providerName: "OpenCode Go",
|
|
44
|
+
capturedAt,
|
|
45
|
+
source: "opencode-zen-usage",
|
|
46
|
+
semantics: {
|
|
47
|
+
kind: "consumer-subscription",
|
|
48
|
+
label: "OpenCode Zen plan usage",
|
|
49
|
+
},
|
|
50
|
+
buckets,
|
|
51
|
+
metrics: [],
|
|
52
|
+
...(notes.length > 0 ? { notes } : {}),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function asObject(value: unknown): Record<string, unknown> | undefined {
|
|
57
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
58
|
+
return value as Record<string, unknown>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function asString(value: unknown): string | undefined {
|
|
62
|
+
if (typeof value !== "string") return undefined;
|
|
63
|
+
return sanitizeDisplayText(value, 80) || undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function asNonnegativeNumber(value: unknown): number | undefined {
|
|
67
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return undefined;
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function asEpochSeconds(value: unknown): number | undefined {
|
|
72
|
+
if (typeof value !== "string" || !value.trim()) return undefined;
|
|
73
|
+
const parsed = Date.parse(value);
|
|
74
|
+
if (Number.isNaN(parsed)) return undefined;
|
|
75
|
+
return Math.floor(parsed / 1000);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function clampPercent(value: number): number {
|
|
79
|
+
return Math.min(100, Math.max(0, value));
|
|
80
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { sanitizeDisplayText } from "../core.js";
|
|
2
|
+
import type { OpenRouterKeyPayload, UsageBucket, UsageMetric, UsageReport } from "../types.js";
|
|
3
|
+
|
|
4
|
+
export function normalizeOpenRouterKeyPayload(payload: OpenRouterKeyPayload, capturedAt: number): UsageReport {
|
|
5
|
+
const data = asObject(payload.data);
|
|
6
|
+
if (!data) throw new Error("OpenRouter key response data was not an object.");
|
|
7
|
+
|
|
8
|
+
const limit = asNonnegativeNumber(data.limit);
|
|
9
|
+
const remaining = asNonnegativeNumber(data.limit_remaining);
|
|
10
|
+
const period = asString(data.limit_reset);
|
|
11
|
+
const totalUsage = asNonnegativeNumber(data.usage);
|
|
12
|
+
const buckets: UsageBucket[] = [];
|
|
13
|
+
if (limit !== undefined) {
|
|
14
|
+
buckets.push({
|
|
15
|
+
id: "key-limit",
|
|
16
|
+
label: "Key limit",
|
|
17
|
+
...(remaining !== undefined ? { used: Math.max(0, limit - remaining), remaining } : {}),
|
|
18
|
+
limit,
|
|
19
|
+
unit: "usd",
|
|
20
|
+
...(period ? { period } : {}),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const metrics: UsageMetric[] = [];
|
|
25
|
+
addUsageMetric(metrics, "usage-daily", "Usage today", data.usage_daily);
|
|
26
|
+
addUsageMetric(metrics, "usage-weekly", "Usage this week", data.usage_weekly);
|
|
27
|
+
addUsageMetric(metrics, "usage-monthly", "Usage this month", data.usage_monthly);
|
|
28
|
+
addUsageMetric(metrics, "usage-total", "All-time usage", totalUsage);
|
|
29
|
+
if (buckets.length === 0 && metrics.length === 0) {
|
|
30
|
+
throw new Error("OpenRouter key response returned no displayable usage data.");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const notes: string[] = [];
|
|
34
|
+
if (data.limit === null) notes.push("No per-key spend cap");
|
|
35
|
+
else if (limit === undefined) notes.push("Per-key spend cap unavailable");
|
|
36
|
+
if (data.is_free_tier === true) notes.push("Free-tier API key");
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
providerId: "openrouter",
|
|
40
|
+
providerName: "OpenRouter",
|
|
41
|
+
capturedAt,
|
|
42
|
+
source: "openrouter-key",
|
|
43
|
+
semantics: { kind: "api-key", label: "API-key spend limits" },
|
|
44
|
+
accountLabel: asString(data.label),
|
|
45
|
+
buckets,
|
|
46
|
+
metrics,
|
|
47
|
+
...(notes.length > 0 ? { notes } : {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function addUsageMetric(metrics: UsageMetric[], id: string, label: string, value: unknown): void {
|
|
52
|
+
const amount = typeof value === "number" ? asNonnegativeNumber(value) : undefined;
|
|
53
|
+
if (amount === undefined) return;
|
|
54
|
+
metrics.push({ id, label, value: amount, unit: "usd" });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function asObject(value: unknown): Record<string, unknown> | undefined {
|
|
58
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
59
|
+
return value as Record<string, unknown>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function asString(value: unknown): string | undefined {
|
|
63
|
+
if (typeof value !== "string") return undefined;
|
|
64
|
+
return sanitizeDisplayText(value, 80) || undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function asNonnegativeNumber(value: unknown): number | undefined {
|
|
68
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return undefined;
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { UsageMetric, UsageReport, VercelAIGatewayCreditsPayload } from "../types.js";
|
|
2
|
+
|
|
3
|
+
const DECIMAL_AMOUNT = /^(?:0|[1-9]\d*)(?:\.\d+)?$/u;
|
|
4
|
+
|
|
5
|
+
export function normalizeVercelAIGatewayCreditsPayload(
|
|
6
|
+
payload: VercelAIGatewayCreditsPayload,
|
|
7
|
+
capturedAt: number,
|
|
8
|
+
): UsageReport {
|
|
9
|
+
const balance = decimalAmount(payload.balance, "balance");
|
|
10
|
+
const totalUsed = decimalAmount(payload.total_used, "total used");
|
|
11
|
+
const metrics: UsageMetric[] = [
|
|
12
|
+
{
|
|
13
|
+
id: "credit-balance",
|
|
14
|
+
label: "Credit balance",
|
|
15
|
+
value: balance,
|
|
16
|
+
unit: "currency",
|
|
17
|
+
currency: "USD",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: "lifetime-spend",
|
|
21
|
+
label: "Lifetime spend",
|
|
22
|
+
value: totalUsed,
|
|
23
|
+
unit: "currency",
|
|
24
|
+
currency: "USD",
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
return {
|
|
28
|
+
providerId: "vercel-ai-gateway",
|
|
29
|
+
providerName: "Vercel AI Gateway",
|
|
30
|
+
capturedAt,
|
|
31
|
+
source: "vercel-ai-gateway-credits",
|
|
32
|
+
semantics: { kind: "api-key", label: "AI Gateway credits and lifetime spend" },
|
|
33
|
+
buckets: [],
|
|
34
|
+
metrics,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function decimalAmount(value: unknown, label: string): string {
|
|
39
|
+
if (typeof value !== "string" || value.length > 64 || !DECIMAL_AMOUNT.test(value)) {
|
|
40
|
+
throw new Error(`Vercel AI Gateway ${label} was not a valid nonnegative amount.`);
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|