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