@hanzo/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.
Files changed (74) hide show
  1. package/assets/providers/abacus.svg +18 -0
  2. package/assets/providers/alibaba.svg +3 -0
  3. package/assets/providers/amp.svg +6 -0
  4. package/assets/providers/antigravity.svg +3 -0
  5. package/assets/providers/augment.svg +16 -0
  6. package/assets/providers/bedrock.svg +8 -0
  7. package/assets/providers/chutes.svg +6 -0
  8. package/assets/providers/claude.svg +3 -0
  9. package/assets/providers/clawrouter.svg +7 -0
  10. package/assets/providers/codebuff.svg +4 -0
  11. package/assets/providers/codex.svg +3 -0
  12. package/assets/providers/commandcode.svg +3 -0
  13. package/assets/providers/copilot.svg +3 -0
  14. package/assets/providers/crof.svg +3 -0
  15. package/assets/providers/crossmodel.svg +5 -0
  16. package/assets/providers/cursor.svg +3 -0
  17. package/assets/providers/deepgram.svg +4 -0
  18. package/assets/providers/deepseek.svg +3 -0
  19. package/assets/providers/devin.svg +3 -0
  20. package/assets/providers/doubao.svg +1 -0
  21. package/assets/providers/elevenlabs.svg +4 -0
  22. package/assets/providers/factory.svg +3 -0
  23. package/assets/providers/gemini.svg +3 -0
  24. package/assets/providers/grok.svg +4 -0
  25. package/assets/providers/groq.svg +4 -0
  26. package/assets/providers/jetbrains.svg +1 -0
  27. package/assets/providers/kilo.svg +6 -0
  28. package/assets/providers/kimi.svg +1 -0
  29. package/assets/providers/kiro.svg +19 -0
  30. package/assets/providers/litellm.svg +16 -0
  31. package/assets/providers/llmproxy.svg +7 -0
  32. package/assets/providers/manus.svg +6 -0
  33. package/assets/providers/mimo.svg +4 -0
  34. package/assets/providers/minimax.svg +1 -0
  35. package/assets/providers/mistral.svg +1 -0
  36. package/assets/providers/ollama.svg +7 -0
  37. package/assets/providers/opencode.svg +3 -0
  38. package/assets/providers/opencodego.svg +3 -0
  39. package/assets/providers/openrouter.svg +13 -0
  40. package/assets/providers/perplexity.svg +1 -0
  41. package/assets/providers/poe.svg +1 -0
  42. package/assets/providers/qoder.svg +3 -0
  43. package/assets/providers/sakana.svg +5 -0
  44. package/assets/providers/stepfun.svg +33 -0
  45. package/assets/providers/synthetic.svg +14 -0
  46. package/assets/providers/t3chat.svg +5 -0
  47. package/assets/providers/venice.svg +4 -0
  48. package/assets/providers/vertexai.svg +6 -0
  49. package/assets/providers/warp.svg +4 -0
  50. package/assets/providers/windsurf.svg +3 -0
  51. package/assets/providers/zai.svg +5 -0
  52. package/assets/providers/zed.svg +3 -0
  53. package/dist/catalog.d.ts +9 -0
  54. package/dist/catalog.js +67 -0
  55. package/dist/index.d.ts +12 -1
  56. package/dist/index.js +33 -1
  57. package/dist/providers/api-token-providers.d.ts +15 -0
  58. package/dist/providers/api-token-providers.js +569 -0
  59. package/dist/providers/api-token.d.ts +36 -0
  60. package/dist/providers/api-token.js +99 -0
  61. package/dist/providers/byo.d.ts +2 -0
  62. package/dist/providers/byo.js +114 -0
  63. package/dist/providers/deepgram.d.ts +2 -0
  64. package/dist/providers/deepgram.js +83 -0
  65. package/dist/providers/groq.d.ts +2 -0
  66. package/dist/providers/groq.js +83 -0
  67. package/package.json +6 -4
  68. package/src/catalog.ts +78 -0
  69. package/src/index.ts +65 -1
  70. package/src/providers/api-token-providers.ts +610 -0
  71. package/src/providers/api-token.ts +138 -0
  72. package/src/providers/byo.ts +132 -0
  73. package/src/providers/deepgram.ts +100 -0
  74. package/src/providers/groq.ts +97 -0
@@ -0,0 +1,569 @@
1
+ // Copyright (c) 2026 Hanzo AI Inc. MIT License.
2
+ // API-key providers built on the makeApiTokenProvider factory. Each descriptor is
3
+ // ~30 lines of data: how to build the request (endpoint + auth header) and how to
4
+ // map the JSON body. Every endpoint/shape here is a faithful port of the matching
5
+ // CodexBarCore/Providers/<Id> Swift source (verified, not guessed).
6
+ import { makeApiTokenProvider, bearer, numberOrUndefined } from './api-token.js';
7
+ const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
8
+ /** Parse a value that a provider may encode as number OR numeric string. */
9
+ const loose = (v) => {
10
+ if (typeof v === 'number' && Number.isFinite(v))
11
+ return v;
12
+ if (typeof v === 'string') {
13
+ const n = Number(v);
14
+ return Number.isFinite(n) ? n : undefined;
15
+ }
16
+ return undefined;
17
+ };
18
+ const pct = (used, limit) => limit > 0 ? Math.min(100, Math.max(0, (used / limit) * 100)) : 0;
19
+ const obj = (v) => (v && typeof v === 'object' ? v : {});
20
+ // ---- OpenRouter — GET {base}/credits (Bearer). data.total_credits/total_usage. ----
21
+ export const openrouterProvider = makeApiTokenProvider({
22
+ id: 'openrouter',
23
+ metadata: {
24
+ displayName: 'OpenRouter',
25
+ sessionLabel: 'Credits used',
26
+ weeklyLabel: 'Credit limit',
27
+ supportsCredits: true,
28
+ dashboardUrl: 'https://openrouter.ai/settings/credits',
29
+ color: '#6467f2',
30
+ },
31
+ envKeys: ['OPENROUTER_API_KEY'],
32
+ request: (s, ctx) => ({
33
+ url: `${s.baseUrl ?? ctx.host.env('OPENROUTER_API_URL') ?? 'https://openrouter.ai/api/v1'}/credits`,
34
+ headers: bearer(s.apiKey),
35
+ }),
36
+ map: (json, now) => {
37
+ const d = obj(obj(json).data);
38
+ const total = num(d.total_credits);
39
+ const used = num(d.total_usage);
40
+ return {
41
+ usage: {
42
+ identity: { providerId: 'openrouter', loginMethod: 'api' },
43
+ providerCost: { used, limit: total, currencyCode: 'USD', period: 'total', updatedAt: now.toISOString() },
44
+ dataConfidence: 'exact',
45
+ },
46
+ credits: { remaining: Math.max(0, total - used) },
47
+ };
48
+ },
49
+ });
50
+ // ---- DeepSeek — GET /user/balance (Bearer). balance_infos[].total_balance (string). ----
51
+ export const deepseekProvider = makeApiTokenProvider({
52
+ id: 'deepseek',
53
+ metadata: {
54
+ displayName: 'DeepSeek',
55
+ sessionLabel: 'Balance',
56
+ weeklyLabel: 'Balance',
57
+ supportsCredits: true,
58
+ dashboardUrl: 'https://platform.deepseek.com/usage',
59
+ color: '#527df0',
60
+ },
61
+ envKeys: ['DEEPSEEK_API_KEY', 'DEEPSEEK_KEY'],
62
+ request: (s) => ({ url: 'https://api.deepseek.com/user/balance', headers: bearer(s.apiKey) }),
63
+ map: (json) => {
64
+ const infos = (Array.isArray(obj(json).balance_infos) ? obj(json).balance_infos : []);
65
+ const bal = (r) => loose(r.total_balance) ?? 0;
66
+ const row = infos.find((r) => r.currency === 'USD' && bal(r) > 0) ??
67
+ infos.find((r) => bal(r) > 0) ??
68
+ infos.find((r) => r.currency === 'USD') ??
69
+ infos[0];
70
+ return {
71
+ usage: {
72
+ identity: { providerId: 'deepseek', loginMethod: 'api', plan: typeof row?.currency === 'string' ? row.currency : undefined },
73
+ dataConfidence: 'exact',
74
+ },
75
+ credits: { remaining: row ? bal(row) : 0 },
76
+ };
77
+ },
78
+ });
79
+ // ---- ElevenLabs — GET /v1/user/subscription (xi-api-key). character_count/limit. ----
80
+ export const elevenlabsProvider = makeApiTokenProvider({
81
+ id: 'elevenlabs',
82
+ metadata: {
83
+ displayName: 'ElevenLabs',
84
+ sessionLabel: 'Characters',
85
+ weeklyLabel: 'Character limit',
86
+ dashboardUrl: 'https://elevenlabs.io/app/subscription',
87
+ color: '#ebebe6',
88
+ },
89
+ envKeys: ['ELEVENLABS_API_KEY', 'XI_API_KEY'],
90
+ request: (s, ctx) => ({
91
+ url: `${s.baseUrl ?? ctx.host.env('ELEVENLABS_API_URL') ?? 'https://api.elevenlabs.io'}/v1/user/subscription`,
92
+ headers: { 'xi-api-key': s.apiKey, Accept: 'application/json' },
93
+ }),
94
+ map: (json, now) => {
95
+ const b = obj(json);
96
+ const count = num(b.character_count);
97
+ const limit = num(b.character_limit);
98
+ const resetUnix = numberOrUndefined(b.next_character_count_reset_unix);
99
+ const resetsAt = resetUnix !== undefined ? new Date(resetUnix * 1000).toISOString() : undefined;
100
+ const overage = obj(b.current_overage);
101
+ const overageAmount = loose(overage.amount);
102
+ return {
103
+ usage: {
104
+ primary: { usedPercent: pct(count, limit), resetsAt },
105
+ identity: { providerId: 'elevenlabs', loginMethod: 'api', plan: typeof b.tier === 'string' ? b.tier : undefined },
106
+ subscriptionRenewsAt: resetsAt,
107
+ providerCost: overageAmount !== undefined && overageAmount > 0
108
+ ? {
109
+ used: overageAmount,
110
+ currencyCode: typeof overage.currency === 'string' ? overage.currency : 'USD',
111
+ period: 'overage',
112
+ updatedAt: now.toISOString(),
113
+ }
114
+ : undefined,
115
+ dataConfidence: 'exact',
116
+ },
117
+ };
118
+ },
119
+ });
120
+ // ---- Poe — GET /usage/current_balance (Bearer). current_point_balance (points). ----
121
+ export const poeProvider = makeApiTokenProvider({
122
+ id: 'poe',
123
+ metadata: {
124
+ displayName: 'Poe',
125
+ sessionLabel: 'Points',
126
+ weeklyLabel: 'Points',
127
+ supportsCredits: true,
128
+ dashboardUrl: 'https://poe.com/api/keys',
129
+ color: '#5d5cde',
130
+ },
131
+ envKeys: ['POE_API_KEY'],
132
+ request: (s) => ({ url: 'https://api.poe.com/usage/current_balance', headers: bearer(s.apiKey) }),
133
+ map: (json) => ({
134
+ usage: { identity: { providerId: 'poe', loginMethod: 'api' }, dataConfidence: 'exact' },
135
+ credits: { remaining: num(obj(json).current_point_balance) },
136
+ }),
137
+ });
138
+ // ---- Venice — GET /api/v1/billing/balance (Bearer). balances.usd/diem + alloc. ----
139
+ export const veniceProvider = makeApiTokenProvider({
140
+ id: 'venice',
141
+ metadata: {
142
+ displayName: 'Venice',
143
+ sessionLabel: 'Balance',
144
+ weeklyLabel: 'DIEM allocation',
145
+ supportsCredits: true,
146
+ dashboardUrl: 'https://venice.ai/settings/api',
147
+ color: '#3399ff',
148
+ },
149
+ envKeys: ['VENICE_API_KEY', 'VENICE_KEY'],
150
+ request: (s) => ({ url: 'https://api.venice.ai/api/v1/billing/balance', headers: bearer(s.apiKey) }),
151
+ map: (json) => {
152
+ const b = obj(json);
153
+ const balances = obj(b.balances);
154
+ const canConsume = b.canConsume !== false;
155
+ const currency = (typeof b.consumptionCurrency === 'string' ? b.consumptionCurrency : 'USD').toUpperCase();
156
+ const usd = loose(balances.usd);
157
+ const diem = loose(balances.diem);
158
+ const alloc = loose(b.diemEpochAllocation);
159
+ const isDiem = currency === 'DIEM';
160
+ const remaining = (isDiem ? diem : usd) ?? 0;
161
+ let usedPercent = 0;
162
+ if (!canConsume)
163
+ usedPercent = 100;
164
+ else if (isDiem && alloc && alloc > 0)
165
+ usedPercent = pct(Math.max(0, alloc - remaining), alloc);
166
+ return {
167
+ usage: {
168
+ primary: { usedPercent },
169
+ identity: { providerId: 'venice', loginMethod: 'api', plan: currency },
170
+ dataConfidence: 'exact',
171
+ },
172
+ credits: { remaining },
173
+ };
174
+ },
175
+ });
176
+ // ---- Chutes — GET /users/me/subscription_usage (Bearer). rolling(4h)+monthly. ----
177
+ /** Read a used/limit/remaining/percent meter from a key-tolerant window object. */
178
+ const chutesWindow = (container, windowMinutes) => {
179
+ const w = obj(container);
180
+ const pick = (keys) => {
181
+ for (const k of keys) {
182
+ const v = loose(w[k]);
183
+ if (v !== undefined)
184
+ return v;
185
+ }
186
+ return undefined;
187
+ };
188
+ const explicit = pick(['percent_used', 'usage_percent', 'used_percent', 'utilization', 'utilization_percent']);
189
+ const remainingPct = pick(['percent_remaining', 'remaining_percent']);
190
+ const used = pick(['used', 'usage', 'consumed', 'current', 'requests', 'tokens', 'monthly_usage']);
191
+ const limit = pick(['limit', 'cap', 'max', 'maximum', 'quota', 'quota_limit', 'monthly_limit', 'total']);
192
+ const remaining = pick(['remaining', 'available', 'balance', 'left']);
193
+ let usedPercent = explicit;
194
+ if (usedPercent === undefined && remainingPct !== undefined)
195
+ usedPercent = 100 - remainingPct;
196
+ if (usedPercent !== undefined && usedPercent <= 1 && usedPercent >= 0)
197
+ usedPercent *= 100;
198
+ if (usedPercent === undefined && limit !== undefined) {
199
+ const u = used ?? (remaining !== undefined ? limit - remaining : undefined);
200
+ if (u !== undefined)
201
+ usedPercent = pct(u, limit);
202
+ }
203
+ if (usedPercent === undefined)
204
+ return undefined;
205
+ const resetRaw = pick(['reset_at', 'resets_at', 'next_reset_at', 'period_end', 'current_period_end', 'expires_at', 'window_end']);
206
+ return {
207
+ usedPercent: Math.min(100, Math.max(0, usedPercent)),
208
+ windowMinutes,
209
+ resetsAt: resetRaw !== undefined ? new Date(resetRaw * 1000).toISOString() : undefined,
210
+ };
211
+ };
212
+ export const chutesProvider = makeApiTokenProvider({
213
+ id: 'chutes',
214
+ metadata: {
215
+ displayName: 'Chutes',
216
+ sessionLabel: '4h quota',
217
+ weeklyLabel: 'Monthly quota',
218
+ dashboardUrl: 'https://chutes.ai',
219
+ color: '#eab308',
220
+ },
221
+ envKeys: ['CHUTES_API_KEY'],
222
+ request: (s, ctx) => ({
223
+ url: `${s.baseUrl ?? ctx.host.env('CHUTES_API_URL') ?? 'https://api.chutes.ai'}/users/me/subscription_usage`,
224
+ headers: bearer(s.apiKey),
225
+ }),
226
+ map: (json) => {
227
+ const b = obj(json);
228
+ const rolling = chutesWindow(b.rolling, 240) ??
229
+ chutesWindow(b.rolling_window, 240) ??
230
+ chutesWindow(b.four_hour, 240);
231
+ const monthly = chutesWindow(b.monthly, 43_200) ??
232
+ chutesWindow(b.subscription, 43_200) ??
233
+ chutesWindow(b.subscription_usage, 43_200);
234
+ const plan = b.plan_name ?? b.plan ?? b.tier;
235
+ return {
236
+ usage: {
237
+ primary: rolling,
238
+ secondary: monthly,
239
+ identity: { providerId: 'chutes', loginMethod: 'api', plan: typeof plan === 'string' ? plan : undefined },
240
+ dataConfidence: rolling || monthly ? 'percentOnly' : 'unknown',
241
+ },
242
+ };
243
+ },
244
+ });
245
+ // ---- Moonshot (Kimi API) — GET {base}/v1/users/me/balance (Bearer). ----
246
+ const moonshotBase = (region) => region === 'china' ? 'https://api.moonshot.cn' : 'https://api.moonshot.ai';
247
+ export const moonshotProvider = makeApiTokenProvider({
248
+ id: 'moonshot',
249
+ metadata: {
250
+ displayName: 'Moonshot',
251
+ sessionLabel: 'Balance',
252
+ weeklyLabel: 'Balance',
253
+ supportsCredits: true,
254
+ dashboardUrl: 'https://platform.moonshot.ai/console/info',
255
+ color: '#16a34a',
256
+ },
257
+ envKeys: ['MOONSHOT_API_KEY', 'MOONSHOT_KEY'],
258
+ request: (s, ctx) => ({
259
+ url: `${s.baseUrl ?? moonshotBase(s.region ?? ctx.host.env('MOONSHOT_REGION'))}/v1/users/me/balance`,
260
+ headers: bearer(s.apiKey),
261
+ }),
262
+ map: (json) => {
263
+ const d = obj(obj(json).data);
264
+ return {
265
+ usage: { identity: { providerId: 'moonshot', loginMethod: 'api' }, dataConfidence: 'exact' },
266
+ credits: { remaining: num(d.available_balance) },
267
+ };
268
+ },
269
+ });
270
+ // ---- Kimi (coding API key path) — GET {base}/coding/v1/usages (Bearer). ----
271
+ export const kimiProvider = makeApiTokenProvider({
272
+ id: 'kimi',
273
+ metadata: {
274
+ displayName: 'Kimi',
275
+ sessionLabel: 'Weekly',
276
+ weeklyLabel: 'Rate (5h)',
277
+ dashboardUrl: 'https://www.kimi.com/code/console',
278
+ color: '#111111',
279
+ },
280
+ envKeys: ['KIMI_CODE_API_KEY'],
281
+ request: (s, ctx) => ({
282
+ url: `${s.baseUrl ?? ctx.host.env('KIMI_CODE_BASE_URL') ?? 'https://api.kimi.com'}/coding/v1/usages`,
283
+ headers: bearer(s.apiKey),
284
+ }),
285
+ map: (json) => {
286
+ const b = obj(json);
287
+ const detail = obj(b.usage);
288
+ const limit = loose(detail.limit) ?? 0;
289
+ const used = loose(detail.used) ?? (limit && loose(detail.remaining) !== undefined ? limit - loose(detail.remaining) : 0);
290
+ const resetTime = detail.resetTime ?? detail.resetAt ?? detail.reset_time;
291
+ const rate = obj(obj((Array.isArray(b.limits) ? b.limits[0] : undefined)).detail);
292
+ const rateLimit = loose(rate.limit);
293
+ const rateUsed = loose(rate.used);
294
+ const secondary = rateLimit !== undefined ? { usedPercent: pct(rateUsed ?? 0, rateLimit), windowMinutes: 300 } : undefined;
295
+ return {
296
+ usage: {
297
+ primary: { usedPercent: pct(used, limit), resetsAt: typeof resetTime === 'string' ? resetTime : undefined },
298
+ secondary,
299
+ identity: { providerId: 'kimi', loginMethod: 'api' },
300
+ dataConfidence: 'percentOnly',
301
+ },
302
+ };
303
+ },
304
+ });
305
+ // ---- Kimi K2 (legacy kimi-k2.ai) — GET /api/user/credits (Bearer). ----
306
+ export const kimik2Provider = makeApiTokenProvider({
307
+ id: 'kimik2',
308
+ metadata: {
309
+ displayName: 'Kimi K2',
310
+ sessionLabel: 'Credits',
311
+ weeklyLabel: 'Credits',
312
+ supportsCredits: true,
313
+ dashboardUrl: 'https://kimi-k2.ai',
314
+ color: '#111111',
315
+ },
316
+ envKeys: ['KIMI_K2_API_KEY', 'KIMI_API_KEY', 'KIMI_KEY'],
317
+ request: (s) => ({ url: 'https://kimi-k2.ai/api/user/credits', headers: bearer(s.apiKey) }),
318
+ map: (json) => {
319
+ const b = obj(json);
320
+ const nested = { ...obj(b.data), ...obj(b.result), ...obj(b.usage), ...obj(b.credits) };
321
+ const pick = (keys) => {
322
+ for (const k of keys) {
323
+ const v = loose(b[k]) ?? loose(nested[k]);
324
+ if (v !== undefined)
325
+ return v;
326
+ }
327
+ return undefined;
328
+ };
329
+ const remaining = pick([
330
+ 'credits_remaining',
331
+ 'creditsRemaining',
332
+ 'remaining_credits',
333
+ 'remainingCredits',
334
+ 'available_credits',
335
+ 'availableCredits',
336
+ 'credits_left',
337
+ 'creditsLeft',
338
+ 'remaining',
339
+ ]) ?? 0;
340
+ return {
341
+ usage: { identity: { providerId: 'kimik2', loginMethod: 'api' }, dataConfidence: 'exact' },
342
+ credits: { remaining },
343
+ };
344
+ },
345
+ });
346
+ // ---- Zai (z.ai) — GET {base}/api/monitor/usage/quota/limit (Bearer). ----
347
+ const zaiBase = (region) => region === 'bigmodel-cn' ? 'https://open.bigmodel.cn' : 'https://api.z.ai';
348
+ const ZAI_UNIT_MINUTES = { 1: 1440, 3: 60, 5: 1, 6: 10_080 };
349
+ const zaiWindow = (raw) => {
350
+ const limit = loose(raw.usage) ?? 0;
351
+ const current = loose(raw.currentValue);
352
+ const remaining = loose(raw.remaining);
353
+ const used = current ?? (remaining !== undefined ? Math.max(0, limit - remaining) : undefined);
354
+ const explicit = loose(raw.percentage);
355
+ const usedPercent = used !== undefined && limit > 0 ? pct(used, limit) : explicit ?? 0;
356
+ const unit = loose(raw.unit);
357
+ const number = loose(raw.number);
358
+ const resetMs = loose(raw.nextResetTime);
359
+ return {
360
+ usedPercent,
361
+ windowMinutes: unit !== undefined && number !== undefined ? (ZAI_UNIT_MINUTES[unit] ?? 0) * number : undefined,
362
+ resetsAt: resetMs !== undefined ? new Date(resetMs).toISOString() : undefined,
363
+ };
364
+ };
365
+ export const zaiProvider = makeApiTokenProvider({
366
+ id: 'zai',
367
+ metadata: {
368
+ displayName: 'z.ai',
369
+ sessionLabel: 'Token quota',
370
+ weeklyLabel: 'Time quota',
371
+ dashboardUrl: 'https://z.ai/manage-apikey/coding-plan/personal/my-plan',
372
+ color: '#3b82f6',
373
+ },
374
+ envKeys: ['Z_AI_API_KEY'],
375
+ baseUrlEnv: ['Z_AI_API_HOST'],
376
+ request: (s, ctx) => ({
377
+ url: `${resolveZaiBase(s, ctx.host.env('Z_AI_API_HOST'))}/api/monitor/usage/quota/limit`,
378
+ headers: { ...bearer(s.apiKey), accept: 'application/json' },
379
+ }),
380
+ map: (json) => {
381
+ const data = obj(obj(json).data);
382
+ const limits = (Array.isArray(data.limits) ? data.limits : []);
383
+ const tokens = limits.filter((l) => l.type === 'TOKENS_LIMIT').map(zaiWindow);
384
+ const time = limits.find((l) => l.type === 'TIME_LIMIT');
385
+ const plan = data.planName ?? data.plan ?? data.planType ?? data.packageName;
386
+ return {
387
+ usage: {
388
+ primary: tokens[0],
389
+ secondary: time ? zaiWindow(time) : undefined,
390
+ tertiary: tokens[1],
391
+ identity: { providerId: 'zai', loginMethod: 'api', plan: typeof plan === 'string' ? plan : undefined },
392
+ dataConfidence: tokens.length || time ? 'percentOnly' : 'unknown',
393
+ },
394
+ };
395
+ },
396
+ });
397
+ const resolveZaiBase = (s, hostEnv) => s.baseUrl ?? hostEnv ?? zaiBase(s.region);
398
+ // ---- OpenAI (platform) — GET /v1/dashboard/billing/credit_grants (Bearer). ----
399
+ export const openaiProvider = makeApiTokenProvider({
400
+ id: 'openai',
401
+ metadata: {
402
+ displayName: 'OpenAI',
403
+ sessionLabel: 'Credits used',
404
+ weeklyLabel: 'Credit grant',
405
+ supportsCredits: true,
406
+ dashboardUrl: 'https://platform.openai.com/usage',
407
+ statusPageUrl: 'https://status.openai.com',
408
+ color: '#0f826e',
409
+ },
410
+ envKeys: ['OPENAI_ADMIN_KEY', 'OPENAI_API_KEY'],
411
+ request: (s) => ({
412
+ url: 'https://api.openai.com/v1/dashboard/billing/credit_grants',
413
+ headers: bearer(s.apiKey),
414
+ }),
415
+ map: (json, now) => {
416
+ const b = obj(json);
417
+ const granted = num(b.total_granted);
418
+ const used = num(b.total_used);
419
+ const available = num(b.total_available);
420
+ return {
421
+ usage: {
422
+ primary: { usedPercent: pct(used, granted) },
423
+ identity: { providerId: 'openai', loginMethod: 'api' },
424
+ providerCost: { used, limit: granted, currencyCode: 'USD', period: 'grant', updatedAt: now.toISOString() },
425
+ dataConfidence: 'exact',
426
+ },
427
+ credits: { remaining: available },
428
+ };
429
+ },
430
+ });
431
+ // ---- LiteLLM (self-hosted) — GET {base}/key/info (Bearer). info.spend/expires. ----
432
+ const stripV1 = (base) => base.replace(/\/v1\/?$/, '');
433
+ export const litellmProvider = makeApiTokenProvider({
434
+ id: 'litellm',
435
+ metadata: {
436
+ displayName: 'LiteLLM',
437
+ sessionLabel: 'Key spend',
438
+ weeklyLabel: 'Budget',
439
+ dashboardUrl: 'https://docs.litellm.ai/docs/proxy/cost_tracking',
440
+ color: '#22c55e',
441
+ },
442
+ envKeys: ['LITELLM_API_KEY'],
443
+ baseUrlEnv: ['LITELLM_BASE_URL'],
444
+ requireBaseUrl: true,
445
+ request: (s) => ({ url: `${stripV1(s.baseUrl)}/key/info`, headers: bearer(s.apiKey) }),
446
+ map: (json, now) => {
447
+ const info = obj(obj(json).info);
448
+ const spend = num(info.spend);
449
+ return {
450
+ usage: {
451
+ identity: {
452
+ providerId: 'litellm',
453
+ loginMethod: 'api',
454
+ accountOrganization: typeof info.team_id === 'string' ? info.team_id : undefined,
455
+ accountEmail: typeof info.user_id === 'string' ? info.user_id : undefined,
456
+ plan: typeof info.key_name === 'string' ? info.key_name : undefined,
457
+ },
458
+ providerCost: { used: spend, currencyCode: 'USD', period: 'key', updatedAt: now.toISOString() },
459
+ subscriptionExpiresAt: typeof info.expires === 'string' ? info.expires : undefined,
460
+ dataConfidence: 'exact',
461
+ },
462
+ };
463
+ },
464
+ });
465
+ // ---- LLMProxy (self-hosted) — GET {base}/v1/quota-stats (Bearer). ----
466
+ const llmProxyV1 = (base) => (/\/v1\/?$/.test(base) ? base.replace(/\/$/, '') : `${base.replace(/\/$/, '')}/v1`);
467
+ export const llmproxyProvider = makeApiTokenProvider({
468
+ id: 'llmproxy',
469
+ metadata: {
470
+ displayName: 'LLM Proxy',
471
+ sessionLabel: 'Quota used',
472
+ weeklyLabel: 'Approx. spend',
473
+ dashboardUrl: 'https://github.com/hzruo/LLM-API-Key-Proxy',
474
+ color: '#8b5cf6',
475
+ },
476
+ envKeys: ['LLM_PROXY_API_KEY'],
477
+ baseUrlEnv: ['LLM_PROXY_BASE_URL'],
478
+ requireBaseUrl: true,
479
+ request: (s) => ({ url: `${llmProxyV1(s.baseUrl)}/quota-stats`, headers: bearer(s.apiKey) }),
480
+ map: (json, now) => {
481
+ const b = obj(json);
482
+ const providers = obj(b.providers);
483
+ const summary = obj(b.summary);
484
+ let minRemaining = 100;
485
+ let earliestReset;
486
+ let sumTokens = 0;
487
+ let sumRequests = 0;
488
+ let sumCost = 0;
489
+ for (const p of Object.values(providers)) {
490
+ const stats = obj(p);
491
+ sumRequests += num(stats.total_requests);
492
+ sumCost += num(stats.approx_cost);
493
+ const t = obj(stats.tokens);
494
+ sumTokens += num(t.input_cached) + num(t.input_uncached) + num(t.output);
495
+ const groups = Array.isArray(stats.quota_groups)
496
+ ? stats.quota_groups
497
+ : Object.values(obj(stats.quota_groups));
498
+ for (const g of groups) {
499
+ const grp = obj(g);
500
+ const rem = loose(grp.remaining_percent);
501
+ if (rem !== undefined && rem < minRemaining)
502
+ minRemaining = rem;
503
+ const reset = typeof grp.reset_time === 'string' ? grp.reset_time : undefined;
504
+ if (reset && (!earliestReset || reset < earliestReset))
505
+ earliestReset = reset;
506
+ }
507
+ }
508
+ const totalRequests = numberOrUndefined(summary.total_requests) ?? sumRequests;
509
+ const totalTokens = numberOrUndefined(summary.total_tokens) ?? sumTokens;
510
+ const approxCost = numberOrUndefined(summary.approx_cost) ?? sumCost;
511
+ return {
512
+ usage: {
513
+ primary: { usedPercent: Math.min(100, Math.max(0, 100 - minRemaining)), resetsAt: earliestReset },
514
+ identity: { providerId: 'llmproxy', loginMethod: 'api' },
515
+ totals: { tokens: totalTokens, requests: totalRequests },
516
+ providerCost: approxCost > 0 ? { used: approxCost, currencyCode: 'USD', period: 'approx', updatedAt: now.toISOString() } : undefined,
517
+ dataConfidence: 'exact',
518
+ },
519
+ };
520
+ },
521
+ });
522
+ // ---- MiniMax (coding-plan API key path) — GET {apiBase}/v1/api/openplatform/coding_plan/remains. ----
523
+ // Standard (sk-api-*) keys need a browser cookie (connect-only); the coding-plan
524
+ // (sk-cp-*) key uses this API path. We map the interval/weekly windows honestly.
525
+ const minimaxApiBase = (region) => region === 'cn' || region === 'china' ? 'https://api.minimaxi.com' : 'https://api.minimax.io';
526
+ const minimaxWindow = (m, prefix, windowMinutes) => {
527
+ const remainingPct = loose(m[`${prefix}_remaining_percent`]);
528
+ const total = loose(m[`${prefix}_total_count`]);
529
+ const usedCount = loose(m[`${prefix}_usage_count`]);
530
+ let usedPercent;
531
+ if (remainingPct !== undefined)
532
+ usedPercent = 100 - remainingPct;
533
+ else if (total !== undefined && usedCount !== undefined)
534
+ usedPercent = pct(usedCount, total);
535
+ if (usedPercent === undefined)
536
+ return undefined;
537
+ return { usedPercent: Math.min(100, Math.max(0, usedPercent)), windowMinutes };
538
+ };
539
+ export const minimaxProvider = makeApiTokenProvider({
540
+ id: 'minimax',
541
+ metadata: {
542
+ displayName: 'MiniMax',
543
+ sessionLabel: 'Interval',
544
+ weeklyLabel: 'Weekly',
545
+ dashboardUrl: 'https://platform.minimax.io/user-center/payment/coding-plan',
546
+ color: '#f43f5e',
547
+ },
548
+ envKeys: ['MINIMAX_CODING_API_KEY', 'MINIMAX_API_KEY'],
549
+ request: (s, ctx) => ({
550
+ url: `${s.baseUrl ?? minimaxApiBase(s.region ?? ctx.host.env('MINIMAX_REGION'))}/v1/api/openplatform/coding_plan/remains`,
551
+ headers: { ...bearer(s.apiKey), accept: 'application/json', 'MM-API-Source': 'HanzoUsage' },
552
+ }),
553
+ map: (json) => {
554
+ const data = obj(obj(json).data);
555
+ const models = (Array.isArray(data.model_remains) ? data.model_remains : []);
556
+ const m = obj(models[0]);
557
+ const plan = data.plan_name ?? data.current_plan_title ?? data.current_subscribe_title;
558
+ const points = loose(data.points_balance) ?? loose(data.balance);
559
+ return {
560
+ usage: {
561
+ primary: minimaxWindow(m, 'current_interval', 0),
562
+ secondary: minimaxWindow(m, 'current_weekly', 10_080),
563
+ identity: { providerId: 'minimax', loginMethod: 'api', plan: typeof plan === 'string' ? plan : undefined },
564
+ dataConfidence: models.length ? 'percentOnly' : 'unknown',
565
+ },
566
+ credits: points !== undefined ? { remaining: points } : undefined,
567
+ };
568
+ },
569
+ });
@@ -0,0 +1,36 @@
1
+ import type { ProviderDescriptor, ProviderFetchContext, ProviderFetchStrategy, ProviderMetadata, ProviderSettings } from '../provider.js';
2
+ import type { CreditsSnapshot, UsageSnapshot } from '../types.js';
3
+ import type { HttpRequest } from '../host.js';
4
+ /** Everything a provider's `map` yields except the mechanical providerId/updatedAt. */
5
+ export type ApiTokenUsage = Omit<UsageSnapshot, 'providerId' | 'updatedAt'>;
6
+ export interface ApiTokenResult {
7
+ usage: ApiTokenUsage;
8
+ credits?: Omit<CreditsSnapshot, 'updatedAt'>;
9
+ }
10
+ export interface ApiTokenProviderConfig {
11
+ id: string;
12
+ metadata: ProviderMetadata;
13
+ /** Source label surfaced to the UI (defaults to the display name). */
14
+ sourceLabel?: string;
15
+ /** Env var names to fall back to when settings.apiKey is absent. */
16
+ envKeys?: string[];
17
+ /** Env var names that supply a base URL for self-hosted gateways. */
18
+ baseUrlEnv?: string[];
19
+ /** Require a resolvable base URL (self-hosted gateways) to be available. */
20
+ requireBaseUrl?: boolean;
21
+ /** Build the HTTP request. `settings.apiKey` (and resolved `baseUrl`) are present. */
22
+ request: (settings: ProviderSettings, ctx: ProviderFetchContext) => HttpRequest;
23
+ /** Map a decoded 200 body into a usage/credits result. */
24
+ map: (json: unknown, now: Date, settings: ProviderSettings) => ApiTokenResult;
25
+ }
26
+ /** Resolve a base URL from explicit settings, then from host env fallbacks. */
27
+ export declare const resolveBaseUrl: (ctx: ProviderFetchContext, envKeys?: readonly string[]) => string | undefined;
28
+ /** Resolve an API key from explicit settings, then from host env fallbacks. */
29
+ export declare const resolveApiKey: (ctx: ProviderFetchContext, envKeys?: readonly string[]) => string | undefined;
30
+ export declare const makeApiTokenStrategy: (config: ApiTokenProviderConfig) => ProviderFetchStrategy;
31
+ /** Build a single-strategy apiToken ProviderDescriptor. */
32
+ export declare const makeApiTokenProvider: (config: ApiTokenProviderConfig) => ProviderDescriptor;
33
+ /** Numeric coercion shared by mappers; non-finite/absent → undefined. */
34
+ export declare const numberOrUndefined: (v: unknown) => number | undefined;
35
+ /** Bearer auth header helper. */
36
+ export declare const bearer: (key: string) => Record<string, string>;