@hanzo/usage 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/catalog.js CHANGED
@@ -5,6 +5,7 @@
5
5
  // the full connectable surface for pickers, grids, and marketing.
6
6
  export const providerCatalog = [
7
7
  { id: 'hanzo', name: 'Hanzo', color: '#ff2d55' },
8
+ { id: 'byo', name: 'BYO endpoint', color: '#64748b' },
8
9
  { id: 'abacus', name: 'Abacus AI', icon: 'providers/abacus.svg' },
9
10
  { id: 'alibaba', name: 'Alibaba', icon: 'providers/alibaba.svg' },
10
11
  { id: 'alibabatokenplan', name: 'Alibaba Token Plan' },
package/dist/index.d.ts CHANGED
@@ -5,8 +5,18 @@ export * from './store.js';
5
5
  export { codexProvider } from './providers/codex.js';
6
6
  export { claudeProvider } from './providers/claude.js';
7
7
  export { hanzoProvider } from './providers/hanzo.js';
8
+ export { groqProvider } from './providers/groq.js';
9
+ export { deepgramProvider } from './providers/deepgram.js';
10
+ export { byoProvider } from './providers/byo.js';
11
+ export * from './providers/api-token.js';
12
+ export { openrouterProvider, deepseekProvider, elevenlabsProvider, poeProvider, veniceProvider, chutesProvider, moonshotProvider, kimiProvider, kimik2Provider, zaiProvider, openaiProvider, litellmProvider, llmproxyProvider, minimaxProvider, } from './providers/api-token-providers.js';
8
13
  import type { ProviderDescriptor } from './provider.js';
9
- /** All built-in providers, registry-style like CodexBar's descriptor registry. */
14
+ /** All built-in providers with a live fetch pipeline, registry-style like
15
+ * CodexBar's descriptor registry. Keys are the canonical provider ids. */
10
16
  export declare const providerRegistry: Record<string, ProviderDescriptor>;
11
17
  export declare const allProviders: ProviderDescriptor[];
18
+ /** Provider ids that have a live native pipeline — UIs badge catalog entries
19
+ * as "In-app tracked" when their id is in this set (the rest are connect-only
20
+ * or tracked only via api.hanzo.ai cloud routing). */
21
+ export declare const trackedProviderIds: readonly string[];
12
22
  export * from './catalog.js';
package/dist/index.js CHANGED
@@ -6,14 +6,45 @@ export * from './store.js';
6
6
  export { codexProvider } from './providers/codex.js';
7
7
  export { claudeProvider } from './providers/claude.js';
8
8
  export { hanzoProvider } from './providers/hanzo.js';
9
+ export { groqProvider } from './providers/groq.js';
10
+ export { deepgramProvider } from './providers/deepgram.js';
11
+ export { byoProvider } from './providers/byo.js';
12
+ export * from './providers/api-token.js';
13
+ export { openrouterProvider, deepseekProvider, elevenlabsProvider, poeProvider, veniceProvider, chutesProvider, moonshotProvider, kimiProvider, kimik2Provider, zaiProvider, openaiProvider, litellmProvider, llmproxyProvider, minimaxProvider, } from './providers/api-token-providers.js';
9
14
  import { codexProvider } from './providers/codex.js';
10
15
  import { claudeProvider } from './providers/claude.js';
11
16
  import { hanzoProvider } from './providers/hanzo.js';
12
- /** All built-in providers, registry-style like CodexBar's descriptor registry. */
17
+ import { groqProvider } from './providers/groq.js';
18
+ import { deepgramProvider } from './providers/deepgram.js';
19
+ import { byoProvider } from './providers/byo.js';
20
+ import { openrouterProvider, deepseekProvider, elevenlabsProvider, poeProvider, veniceProvider, chutesProvider, moonshotProvider, kimiProvider, kimik2Provider, zaiProvider, openaiProvider, litellmProvider, llmproxyProvider, minimaxProvider, } from './providers/api-token-providers.js';
21
+ /** All built-in providers with a live fetch pipeline, registry-style like
22
+ * CodexBar's descriptor registry. Keys are the canonical provider ids. */
13
23
  export const providerRegistry = {
14
24
  hanzo: hanzoProvider,
15
25
  codex: codexProvider,
16
26
  claude: claudeProvider,
27
+ openai: openaiProvider,
28
+ openrouter: openrouterProvider,
29
+ deepseek: deepseekProvider,
30
+ elevenlabs: elevenlabsProvider,
31
+ deepgram: deepgramProvider,
32
+ groq: groqProvider,
33
+ poe: poeProvider,
34
+ venice: veniceProvider,
35
+ chutes: chutesProvider,
36
+ moonshot: moonshotProvider,
37
+ kimi: kimiProvider,
38
+ kimik2: kimik2Provider,
39
+ zai: zaiProvider,
40
+ litellm: litellmProvider,
41
+ llmproxy: llmproxyProvider,
42
+ minimax: minimaxProvider,
43
+ byo: byoProvider,
17
44
  };
18
45
  export const allProviders = Object.values(providerRegistry);
46
+ /** Provider ids that have a live native pipeline — UIs badge catalog entries
47
+ * as "In-app tracked" when their id is in this set (the rest are connect-only
48
+ * or tracked only via api.hanzo.ai cloud routing). */
49
+ export const trackedProviderIds = Object.keys(providerRegistry);
19
50
  export * from './catalog.js';
@@ -0,0 +1,15 @@
1
+ import type { ProviderDescriptor } from '../provider.js';
2
+ export declare const openrouterProvider: ProviderDescriptor;
3
+ export declare const deepseekProvider: ProviderDescriptor;
4
+ export declare const elevenlabsProvider: ProviderDescriptor;
5
+ export declare const poeProvider: ProviderDescriptor;
6
+ export declare const veniceProvider: ProviderDescriptor;
7
+ export declare const chutesProvider: ProviderDescriptor;
8
+ export declare const moonshotProvider: ProviderDescriptor;
9
+ export declare const kimiProvider: ProviderDescriptor;
10
+ export declare const kimik2Provider: ProviderDescriptor;
11
+ export declare const zaiProvider: ProviderDescriptor;
12
+ export declare const openaiProvider: ProviderDescriptor;
13
+ export declare const litellmProvider: ProviderDescriptor;
14
+ export declare const llmproxyProvider: ProviderDescriptor;
15
+ export declare const minimaxProvider: ProviderDescriptor;
@@ -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
+ });