@zhangferry-dev/tokendash 1.6.1 → 1.7.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.
Files changed (61) hide show
  1. package/README.md +148 -84
  2. package/dist/client/assets/index-Bw503sNp.css +1 -0
  3. package/dist/client/index.html +2 -2
  4. package/dist/daemon.cjs +3411 -0
  5. package/dist/daemon.cjs.map +7 -0
  6. package/dist/electron-server.cjs +1124 -28
  7. package/dist/electron-server.cjs.map +4 -4
  8. package/dist/server/ccusage.d.ts +7 -0
  9. package/dist/server/ccusage.js +69 -0
  10. package/dist/server/daemon.d.ts +12 -0
  11. package/dist/server/daemon.js +176 -0
  12. package/dist/server/index.js +23 -11
  13. package/dist/server/insightsCalculator.d.ts +15 -0
  14. package/dist/server/insightsCalculator.js +276 -0
  15. package/dist/server/quota/adapter.d.ts +49 -0
  16. package/dist/server/quota/adapter.js +41 -0
  17. package/dist/server/quota/adapters/claude.d.ts +4 -0
  18. package/dist/server/quota/adapters/claude.js +152 -0
  19. package/dist/server/quota/adapters/codex.d.ts +16 -0
  20. package/dist/server/quota/adapters/codex.js +226 -0
  21. package/dist/server/quota/adapters/glm.d.ts +2 -0
  22. package/dist/server/quota/adapters/glm.js +139 -0
  23. package/dist/server/quota/adapters/kimi.d.ts +2 -0
  24. package/dist/server/quota/adapters/kimi.js +186 -0
  25. package/dist/server/quota/adapters/minimax.d.ts +2 -0
  26. package/dist/server/quota/adapters/minimax.js +82 -0
  27. package/dist/server/quota/cache.d.ts +20 -0
  28. package/dist/server/quota/cache.js +44 -0
  29. package/dist/server/quota/credentialsFile.d.ts +13 -0
  30. package/dist/server/quota/credentialsFile.js +23 -0
  31. package/dist/server/quota/helpers.d.ts +39 -0
  32. package/dist/server/quota/helpers.js +93 -0
  33. package/dist/server/quota/index.d.ts +5 -0
  34. package/dist/server/quota/index.js +23 -0
  35. package/dist/server/quota/quotaService.d.ts +43 -0
  36. package/dist/server/quota/quotaService.js +163 -0
  37. package/dist/server/quota/schemas.d.ts +358 -0
  38. package/dist/server/quota/schemas.js +53 -0
  39. package/dist/server/quota/types.d.ts +76 -0
  40. package/dist/server/quota/types.js +10 -0
  41. package/dist/server/routes/api.js +34 -0
  42. package/dist/server/routes/insights.d.ts +2 -0
  43. package/dist/server/routes/insights.js +155 -0
  44. package/package.json +9 -11
  45. package/resources/entitlements.mac.plist +10 -0
  46. package/resources/icon-1024.png +0 -0
  47. package/resources/icon.icns +0 -0
  48. package/resources/icon.png +0 -0
  49. package/resources/product_menu.png +0 -0
  50. package/resources/readme-hero.png +0 -0
  51. package/dist/client/assets/index-_yA9tOzZ.css +0 -1
  52. package/electron/main.cjs +0 -516
  53. package/electron/npmSync.cjs +0 -62
  54. package/electron/preload.cjs +0 -36
  55. package/electron/serverReuse.cjs +0 -59
  56. package/electron/trayBadge.cjs +0 -27
  57. package/electron/trayHelper +0 -0
  58. package/electron/trayHelper.swift +0 -152
  59. package/electron/updateService.cjs +0 -220
  60. package/electron-builder.yml +0 -20
  61. /package/dist/client/assets/{index-CY4G_b0x.js → index-C913wKtU.js} +0 -0
@@ -0,0 +1,163 @@
1
+ import { QuotaError } from './adapter.js';
2
+ import { QuotaCache } from './cache.js';
3
+ import { validateQuotaSnapshot } from './schemas.js';
4
+ /**
5
+ * Deep quota service. Owns discovery, concurrency, deduplication, caching,
6
+ * stale-while-revalidate, timeouts, and error classification. Adapters only
7
+ * detect + fetch + normalize; everything cross-cutting lives here.
8
+ */
9
+ export class QuotaService {
10
+ registry;
11
+ cache;
12
+ configuredCache;
13
+ /** Cap concurrent upstream calls so a slow provider can't block the others. */
14
+ fetchTimeoutMs;
15
+ /** In-flight promises keyed by provider id, to dedupe concurrent requests. */
16
+ inflight = new Map();
17
+ constructor(registry, cache = new QuotaCache(), configuredCache = null, fetchTimeoutMs = 8_000) {
18
+ this.registry = registry;
19
+ this.cache = cache;
20
+ this.configuredCache = configuredCache;
21
+ this.fetchTimeoutMs = fetchTimeoutMs;
22
+ }
23
+ /**
24
+ * List provider ids that are configured locally. Cheap (no network).
25
+ * The dashboard only shows these — not-configured providers are excluded.
26
+ */
27
+ async discover() {
28
+ const all = this.registry.list();
29
+ const checks = await Promise.all(all.map(async (a) => ({ id: a.provider, configured: await safeIsConfigured(a) })));
30
+ return checks.filter((c) => c.configured).map((c) => c.id);
31
+ }
32
+ /**
33
+ * Fetch one provider's snapshot. Fresh if available; stale-but-retained
34
+ * on failure; never throws (errors become structured statuses).
35
+ */
36
+ async fetchOne(provider) {
37
+ // 1. Fresh cache hit
38
+ const fresh = this.cache.getFresh(provider);
39
+ if (fresh)
40
+ return fresh;
41
+ const adapter = this.registry.get(provider);
42
+ if (!adapter)
43
+ return null;
44
+ // 2. Dedupe concurrent requests for the same provider
45
+ let p = this.inflight.get(provider);
46
+ if (!p) {
47
+ p = this.fetchWithTimeout(adapter).finally(() => this.inflight.delete(provider));
48
+ this.inflight.set(provider, p);
49
+ }
50
+ return p;
51
+ }
52
+ /**
53
+ * Fetch all configured providers concurrently. Partial success: one
54
+ * provider's failure never breaks the others. Order = registry order.
55
+ */
56
+ async fetchAll() {
57
+ const ids = this.configuredCache ?? (await this.discover());
58
+ const byId = new Map();
59
+ const snapshots = await Promise.all(ids.map((id) => this.fetchOne(id)));
60
+ // Preserve registry order regardless of completion order.
61
+ for (const adapter of this.registry.list()) {
62
+ const snap = snapshots.find((s) => s?.provider === adapter.provider);
63
+ if (snap)
64
+ byId.set(adapter.provider, snap);
65
+ }
66
+ return { providers: this.registry.list().map((a) => byId.get(a.provider)).filter((s) => !!s) };
67
+ }
68
+ /** Force a refresh of all configured providers, bypassing the cache. */
69
+ async refreshAll() {
70
+ // Cache-clear only the freshness gate; stale data is still retained by fetchOne on failure.
71
+ this.configuredCache?.forEach(() => { });
72
+ for (const adapter of this.registry.list()) {
73
+ this.cache.clear(adapter.provider);
74
+ }
75
+ return this.fetchAll();
76
+ }
77
+ /**
78
+ * Validate a credential without caching it or writing it to disk. This keeps
79
+ * the settings form transactional: only credentials accepted upstream are
80
+ * persisted by the native app.
81
+ */
82
+ async validateCredential(provider, credential) {
83
+ const adapter = this.registry.get(provider);
84
+ if (!adapter) {
85
+ return {
86
+ provider,
87
+ valid: false,
88
+ status: { state: 'not_configured', message: 'Unsupported provider' },
89
+ };
90
+ }
91
+ try {
92
+ const snapshot = await withTimeout(adapter.fetch({ credential }), this.fetchTimeoutMs, provider);
93
+ const validated = validateQuotaSnapshot(snapshot);
94
+ return { provider, valid: validated.status.state === 'ok', status: validated.status };
95
+ }
96
+ catch (err) {
97
+ return { provider, valid: false, status: statusForError(err, this.fetchTimeoutMs) };
98
+ }
99
+ }
100
+ async fetchWithTimeout(adapter) {
101
+ try {
102
+ const snapshot = await withTimeout(adapter.fetch(), this.fetchTimeoutMs, adapter.provider);
103
+ const validated = validateQuotaSnapshot(snapshot);
104
+ this.cache.set(validated);
105
+ return validated;
106
+ }
107
+ catch (err) {
108
+ return this.handleFailure(adapter, err);
109
+ }
110
+ }
111
+ handleFailure(adapter, err) {
112
+ const status = statusForError(err, this.fetchTimeoutMs);
113
+ // Retain last good snapshot as stale.
114
+ const stale = this.cache.getStale(adapter.provider);
115
+ if (stale) {
116
+ return { ...stale, freshness: 'stale', status };
117
+ }
118
+ // No prior data — surface the structured error so the user can act on it.
119
+ return {
120
+ provider: adapter.provider,
121
+ displayName: adapter.displayName,
122
+ fetchedAt: new Date().toISOString(),
123
+ freshness: 'stale',
124
+ windows: [],
125
+ status,
126
+ };
127
+ }
128
+ }
129
+ function statusForError(err, timeoutMs) {
130
+ if (err instanceof QuotaError)
131
+ return err.status;
132
+ if (err instanceof TimeoutError) {
133
+ return { state: 'timed_out', message: `upstream did not respond within ${timeoutMs}ms` };
134
+ }
135
+ return { state: 'error', message: redact(err), category: 'unexpected' };
136
+ }
137
+ class TimeoutError extends Error {
138
+ constructor(provider) {
139
+ super(`quota fetch timed out: ${provider}`);
140
+ this.name = 'TimeoutError';
141
+ }
142
+ }
143
+ function withTimeout(p, ms, provider) {
144
+ return new Promise((resolve, reject) => {
145
+ const timer = setTimeout(() => reject(new TimeoutError(provider)), ms);
146
+ p.then((v) => { clearTimeout(timer); resolve(v); }, (e) => { clearTimeout(timer); reject(e); });
147
+ });
148
+ }
149
+ async function safeIsConfigured(adapter) {
150
+ try {
151
+ return await adapter.isConfigured();
152
+ }
153
+ catch {
154
+ return false;
155
+ }
156
+ }
157
+ /** Strip anything that looks like a token/key from an error before it surfaces. */
158
+ function redact(err) {
159
+ const msg = err instanceof Error ? err.message : String(err);
160
+ return msg.replace(/(sk-[A-Za-z0-9_-]{6,})[A-Za-z0-9_-]*/g, '$1…')
161
+ .replace(/(Bearer\s+)[A-Za-z0-9._-]+/gi, '$1…')
162
+ .slice(0, 200);
163
+ }
@@ -0,0 +1,358 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Zod schemas for the normalized quota contract.
4
+ *
5
+ * Provider adapter OUTPUT is validated here before it leaves the service,
6
+ * so the API response (and the Swift client) can always trust the shape.
7
+ * Validation is tolerant of additive upstream fields — adapters strip those —
8
+ * but strict on fields used in calculations.
9
+ */
10
+ export declare const QuotaWindowSchema: z.ZodObject<{
11
+ id: z.ZodString;
12
+ label: z.ZodString;
13
+ usedPercent: z.ZodDefault<z.ZodNumber>;
14
+ remainingPercent: z.ZodDefault<z.ZodNumber>;
15
+ used: z.ZodOptional<z.ZodNumber>;
16
+ limit: z.ZodOptional<z.ZodNumber>;
17
+ durationMins: z.ZodOptional<z.ZodNumber>;
18
+ resetsAt: z.ZodOptional<z.ZodString>;
19
+ isUnlimited: z.ZodOptional<z.ZodBoolean>;
20
+ modelName: z.ZodOptional<z.ZodString>;
21
+ }, "strip", z.ZodTypeAny, {
22
+ id: string;
23
+ label: string;
24
+ usedPercent: number;
25
+ remainingPercent: number;
26
+ modelName?: string | undefined;
27
+ used?: number | undefined;
28
+ limit?: number | undefined;
29
+ durationMins?: number | undefined;
30
+ resetsAt?: string | undefined;
31
+ isUnlimited?: boolean | undefined;
32
+ }, {
33
+ id: string;
34
+ label: string;
35
+ modelName?: string | undefined;
36
+ usedPercent?: number | undefined;
37
+ remainingPercent?: number | undefined;
38
+ used?: number | undefined;
39
+ limit?: number | undefined;
40
+ durationMins?: number | undefined;
41
+ resetsAt?: string | undefined;
42
+ isUnlimited?: boolean | undefined;
43
+ }>;
44
+ export declare const QuotaProviderStatusSchema: z.ZodObject<{
45
+ state: z.ZodEnum<["ok", "auth_failed", "not_configured", "upstream_unavailable", "rate_limited", "malformed_response", "timed_out", "error"]>;
46
+ message: z.ZodOptional<z.ZodString>;
47
+ category: z.ZodOptional<z.ZodString>;
48
+ }, "strip", z.ZodTypeAny, {
49
+ state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
50
+ message?: string | undefined;
51
+ category?: string | undefined;
52
+ }, {
53
+ state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
54
+ message?: string | undefined;
55
+ category?: string | undefined;
56
+ }>;
57
+ export declare const QuotaSnapshotSchema: z.ZodObject<{
58
+ provider: z.ZodEnum<["codex", "claude", "glm", "minimax", "kimi"]>;
59
+ displayName: z.ZodString;
60
+ planName: z.ZodOptional<z.ZodString>;
61
+ fetchedAt: z.ZodString;
62
+ freshness: z.ZodEnum<["live", "cached", "stale"]>;
63
+ windows: z.ZodDefault<z.ZodArray<z.ZodObject<{
64
+ id: z.ZodString;
65
+ label: z.ZodString;
66
+ usedPercent: z.ZodDefault<z.ZodNumber>;
67
+ remainingPercent: z.ZodDefault<z.ZodNumber>;
68
+ used: z.ZodOptional<z.ZodNumber>;
69
+ limit: z.ZodOptional<z.ZodNumber>;
70
+ durationMins: z.ZodOptional<z.ZodNumber>;
71
+ resetsAt: z.ZodOptional<z.ZodString>;
72
+ isUnlimited: z.ZodOptional<z.ZodBoolean>;
73
+ modelName: z.ZodOptional<z.ZodString>;
74
+ }, "strip", z.ZodTypeAny, {
75
+ id: string;
76
+ label: string;
77
+ usedPercent: number;
78
+ remainingPercent: number;
79
+ modelName?: string | undefined;
80
+ used?: number | undefined;
81
+ limit?: number | undefined;
82
+ durationMins?: number | undefined;
83
+ resetsAt?: string | undefined;
84
+ isUnlimited?: boolean | undefined;
85
+ }, {
86
+ id: string;
87
+ label: string;
88
+ modelName?: string | undefined;
89
+ usedPercent?: number | undefined;
90
+ remainingPercent?: number | undefined;
91
+ used?: number | undefined;
92
+ limit?: number | undefined;
93
+ durationMins?: number | undefined;
94
+ resetsAt?: string | undefined;
95
+ isUnlimited?: boolean | undefined;
96
+ }>, "many">>;
97
+ status: z.ZodObject<{
98
+ state: z.ZodEnum<["ok", "auth_failed", "not_configured", "upstream_unavailable", "rate_limited", "malformed_response", "timed_out", "error"]>;
99
+ message: z.ZodOptional<z.ZodString>;
100
+ category: z.ZodOptional<z.ZodString>;
101
+ }, "strip", z.ZodTypeAny, {
102
+ state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
103
+ message?: string | undefined;
104
+ category?: string | undefined;
105
+ }, {
106
+ state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
107
+ message?: string | undefined;
108
+ category?: string | undefined;
109
+ }>;
110
+ }, "strip", z.ZodTypeAny, {
111
+ provider: "claude" | "codex" | "glm" | "minimax" | "kimi";
112
+ status: {
113
+ state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
114
+ message?: string | undefined;
115
+ category?: string | undefined;
116
+ };
117
+ windows: {
118
+ id: string;
119
+ label: string;
120
+ usedPercent: number;
121
+ remainingPercent: number;
122
+ modelName?: string | undefined;
123
+ used?: number | undefined;
124
+ limit?: number | undefined;
125
+ durationMins?: number | undefined;
126
+ resetsAt?: string | undefined;
127
+ isUnlimited?: boolean | undefined;
128
+ }[];
129
+ displayName: string;
130
+ fetchedAt: string;
131
+ freshness: "live" | "cached" | "stale";
132
+ planName?: string | undefined;
133
+ }, {
134
+ provider: "claude" | "codex" | "glm" | "minimax" | "kimi";
135
+ status: {
136
+ state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
137
+ message?: string | undefined;
138
+ category?: string | undefined;
139
+ };
140
+ displayName: string;
141
+ fetchedAt: string;
142
+ freshness: "live" | "cached" | "stale";
143
+ windows?: {
144
+ id: string;
145
+ label: string;
146
+ modelName?: string | undefined;
147
+ usedPercent?: number | undefined;
148
+ remainingPercent?: number | undefined;
149
+ used?: number | undefined;
150
+ limit?: number | undefined;
151
+ durationMins?: number | undefined;
152
+ resetsAt?: string | undefined;
153
+ isUnlimited?: boolean | undefined;
154
+ }[] | undefined;
155
+ planName?: string | undefined;
156
+ }>;
157
+ export declare const QuotaResponseSchema: z.ZodObject<{
158
+ providers: z.ZodDefault<z.ZodArray<z.ZodObject<{
159
+ provider: z.ZodEnum<["codex", "claude", "glm", "minimax", "kimi"]>;
160
+ displayName: z.ZodString;
161
+ planName: z.ZodOptional<z.ZodString>;
162
+ fetchedAt: z.ZodString;
163
+ freshness: z.ZodEnum<["live", "cached", "stale"]>;
164
+ windows: z.ZodDefault<z.ZodArray<z.ZodObject<{
165
+ id: z.ZodString;
166
+ label: z.ZodString;
167
+ usedPercent: z.ZodDefault<z.ZodNumber>;
168
+ remainingPercent: z.ZodDefault<z.ZodNumber>;
169
+ used: z.ZodOptional<z.ZodNumber>;
170
+ limit: z.ZodOptional<z.ZodNumber>;
171
+ durationMins: z.ZodOptional<z.ZodNumber>;
172
+ resetsAt: z.ZodOptional<z.ZodString>;
173
+ isUnlimited: z.ZodOptional<z.ZodBoolean>;
174
+ modelName: z.ZodOptional<z.ZodString>;
175
+ }, "strip", z.ZodTypeAny, {
176
+ id: string;
177
+ label: string;
178
+ usedPercent: number;
179
+ remainingPercent: number;
180
+ modelName?: string | undefined;
181
+ used?: number | undefined;
182
+ limit?: number | undefined;
183
+ durationMins?: number | undefined;
184
+ resetsAt?: string | undefined;
185
+ isUnlimited?: boolean | undefined;
186
+ }, {
187
+ id: string;
188
+ label: string;
189
+ modelName?: string | undefined;
190
+ usedPercent?: number | undefined;
191
+ remainingPercent?: number | undefined;
192
+ used?: number | undefined;
193
+ limit?: number | undefined;
194
+ durationMins?: number | undefined;
195
+ resetsAt?: string | undefined;
196
+ isUnlimited?: boolean | undefined;
197
+ }>, "many">>;
198
+ status: z.ZodObject<{
199
+ state: z.ZodEnum<["ok", "auth_failed", "not_configured", "upstream_unavailable", "rate_limited", "malformed_response", "timed_out", "error"]>;
200
+ message: z.ZodOptional<z.ZodString>;
201
+ category: z.ZodOptional<z.ZodString>;
202
+ }, "strip", z.ZodTypeAny, {
203
+ state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
204
+ message?: string | undefined;
205
+ category?: string | undefined;
206
+ }, {
207
+ state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
208
+ message?: string | undefined;
209
+ category?: string | undefined;
210
+ }>;
211
+ }, "strip", z.ZodTypeAny, {
212
+ provider: "claude" | "codex" | "glm" | "minimax" | "kimi";
213
+ status: {
214
+ state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
215
+ message?: string | undefined;
216
+ category?: string | undefined;
217
+ };
218
+ windows: {
219
+ id: string;
220
+ label: string;
221
+ usedPercent: number;
222
+ remainingPercent: number;
223
+ modelName?: string | undefined;
224
+ used?: number | undefined;
225
+ limit?: number | undefined;
226
+ durationMins?: number | undefined;
227
+ resetsAt?: string | undefined;
228
+ isUnlimited?: boolean | undefined;
229
+ }[];
230
+ displayName: string;
231
+ fetchedAt: string;
232
+ freshness: "live" | "cached" | "stale";
233
+ planName?: string | undefined;
234
+ }, {
235
+ provider: "claude" | "codex" | "glm" | "minimax" | "kimi";
236
+ status: {
237
+ state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
238
+ message?: string | undefined;
239
+ category?: string | undefined;
240
+ };
241
+ displayName: string;
242
+ fetchedAt: string;
243
+ freshness: "live" | "cached" | "stale";
244
+ windows?: {
245
+ id: string;
246
+ label: string;
247
+ modelName?: string | undefined;
248
+ usedPercent?: number | undefined;
249
+ remainingPercent?: number | undefined;
250
+ used?: number | undefined;
251
+ limit?: number | undefined;
252
+ durationMins?: number | undefined;
253
+ resetsAt?: string | undefined;
254
+ isUnlimited?: boolean | undefined;
255
+ }[] | undefined;
256
+ planName?: string | undefined;
257
+ }>, "many">>;
258
+ }, "strip", z.ZodTypeAny, {
259
+ providers: {
260
+ provider: "claude" | "codex" | "glm" | "minimax" | "kimi";
261
+ status: {
262
+ state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
263
+ message?: string | undefined;
264
+ category?: string | undefined;
265
+ };
266
+ windows: {
267
+ id: string;
268
+ label: string;
269
+ usedPercent: number;
270
+ remainingPercent: number;
271
+ modelName?: string | undefined;
272
+ used?: number | undefined;
273
+ limit?: number | undefined;
274
+ durationMins?: number | undefined;
275
+ resetsAt?: string | undefined;
276
+ isUnlimited?: boolean | undefined;
277
+ }[];
278
+ displayName: string;
279
+ fetchedAt: string;
280
+ freshness: "live" | "cached" | "stale";
281
+ planName?: string | undefined;
282
+ }[];
283
+ }, {
284
+ providers?: {
285
+ provider: "claude" | "codex" | "glm" | "minimax" | "kimi";
286
+ status: {
287
+ state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
288
+ message?: string | undefined;
289
+ category?: string | undefined;
290
+ };
291
+ displayName: string;
292
+ fetchedAt: string;
293
+ freshness: "live" | "cached" | "stale";
294
+ windows?: {
295
+ id: string;
296
+ label: string;
297
+ modelName?: string | undefined;
298
+ usedPercent?: number | undefined;
299
+ remainingPercent?: number | undefined;
300
+ used?: number | undefined;
301
+ limit?: number | undefined;
302
+ durationMins?: number | undefined;
303
+ resetsAt?: string | undefined;
304
+ isUnlimited?: boolean | undefined;
305
+ }[] | undefined;
306
+ planName?: string | undefined;
307
+ }[] | undefined;
308
+ }>;
309
+ export declare function validateQuotaSnapshot(data: unknown): {
310
+ provider: "claude" | "codex" | "glm" | "minimax" | "kimi";
311
+ status: {
312
+ state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
313
+ message?: string | undefined;
314
+ category?: string | undefined;
315
+ };
316
+ windows: {
317
+ id: string;
318
+ label: string;
319
+ usedPercent: number;
320
+ remainingPercent: number;
321
+ modelName?: string | undefined;
322
+ used?: number | undefined;
323
+ limit?: number | undefined;
324
+ durationMins?: number | undefined;
325
+ resetsAt?: string | undefined;
326
+ isUnlimited?: boolean | undefined;
327
+ }[];
328
+ displayName: string;
329
+ fetchedAt: string;
330
+ freshness: "live" | "cached" | "stale";
331
+ planName?: string | undefined;
332
+ };
333
+ export declare function validateQuotaResponse(data: unknown): {
334
+ providers: {
335
+ provider: "claude" | "codex" | "glm" | "minimax" | "kimi";
336
+ status: {
337
+ state: "ok" | "auth_failed" | "not_configured" | "upstream_unavailable" | "rate_limited" | "malformed_response" | "timed_out" | "error";
338
+ message?: string | undefined;
339
+ category?: string | undefined;
340
+ };
341
+ windows: {
342
+ id: string;
343
+ label: string;
344
+ usedPercent: number;
345
+ remainingPercent: number;
346
+ modelName?: string | undefined;
347
+ used?: number | undefined;
348
+ limit?: number | undefined;
349
+ durationMins?: number | undefined;
350
+ resetsAt?: string | undefined;
351
+ isUnlimited?: boolean | undefined;
352
+ }[];
353
+ displayName: string;
354
+ fetchedAt: string;
355
+ freshness: "live" | "cached" | "stale";
356
+ planName?: string | undefined;
357
+ }[];
358
+ };
@@ -0,0 +1,53 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Zod schemas for the normalized quota contract.
4
+ *
5
+ * Provider adapter OUTPUT is validated here before it leaves the service,
6
+ * so the API response (and the Swift client) can always trust the shape.
7
+ * Validation is tolerant of additive upstream fields — adapters strip those —
8
+ * but strict on fields used in calculations.
9
+ */
10
+ export const QuotaWindowSchema = z.object({
11
+ id: z.string(),
12
+ label: z.string(),
13
+ usedPercent: z.number().min(0).max(100).default(0),
14
+ remainingPercent: z.number().min(0).max(100).default(0),
15
+ used: z.number().optional(),
16
+ limit: z.number().optional(),
17
+ durationMins: z.number().optional(),
18
+ resetsAt: z.string().optional(),
19
+ isUnlimited: z.boolean().optional(),
20
+ modelName: z.string().optional(),
21
+ });
22
+ export const QuotaProviderStatusSchema = z.object({
23
+ state: z.enum([
24
+ 'ok',
25
+ 'auth_failed',
26
+ 'not_configured',
27
+ 'upstream_unavailable',
28
+ 'rate_limited',
29
+ 'malformed_response',
30
+ 'timed_out',
31
+ 'error',
32
+ ]),
33
+ message: z.string().optional(),
34
+ category: z.string().optional(),
35
+ });
36
+ export const QuotaSnapshotSchema = z.object({
37
+ provider: z.enum(['codex', 'claude', 'glm', 'minimax', 'kimi']),
38
+ displayName: z.string(),
39
+ planName: z.string().optional(),
40
+ fetchedAt: z.string(),
41
+ freshness: z.enum(['live', 'cached', 'stale']),
42
+ windows: z.array(QuotaWindowSchema).default([]),
43
+ status: QuotaProviderStatusSchema,
44
+ });
45
+ export const QuotaResponseSchema = z.object({
46
+ providers: z.array(QuotaSnapshotSchema).default([]),
47
+ });
48
+ export function validateQuotaSnapshot(data) {
49
+ return QuotaSnapshotSchema.parse(data);
50
+ }
51
+ export function validateQuotaResponse(data) {
52
+ return QuotaResponseSchema.parse(data);
53
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Normalized quota domain types.
3
+ *
4
+ * These are provider-neutral. Every provider adapter converts its own
5
+ * heterogeneous response shape into this contract so the rest of the app
6
+ * never sees provider-specific fields.
7
+ *
8
+ * Mirrored on the Swift side by TokenDashSwift/Sources/TokenDash/Models/APIModels.swift.
9
+ */
10
+ export type QuotaProviderId = 'codex' | 'claude' | 'glm' | 'minimax' | 'kimi';
11
+ /** A normalized snapshot of one provider's current quota state. */
12
+ export interface QuotaSnapshot {
13
+ /** Stable provider id, e.g. "codex" | "claude" | "glm" | "minimax" | "kimi". */
14
+ provider: QuotaProviderId;
15
+ /** Human-facing name, e.g. "OpenAI Codex". */
16
+ displayName: string;
17
+ /** Subscription tier when known, e.g. "Pro" | "Plus" | "LEVEL_INTERMEDIATE". */
18
+ planName?: string;
19
+ /** ISO 8601 timestamp of the most recent successful fetch. */
20
+ fetchedAt: string;
21
+ /** "live" = fresh this cycle, "cached" = within ttl, "stale" = last good after a failure. */
22
+ freshness: 'live' | 'cached' | 'stale';
23
+ /** Independent quota windows. Never merged into one synthetic number. */
24
+ windows: QuotaWindow[];
25
+ /** Structured status — never carries secrets, only redacted messages. */
26
+ status: QuotaProviderStatus;
27
+ }
28
+ /** A single independent quota window (e.g. 5-hour, weekly, MCP-monthly). */
29
+ export interface QuotaWindow {
30
+ /** Stable per-snapshot id, e.g. "five_hour" | "weekly" | "codex_primary". */
31
+ id: string;
32
+ /** Human label, e.g. "5-Hour Window" | "Weekly". */
33
+ label: string;
34
+ /** Consumed percentage 0-100. */
35
+ usedPercent: number;
36
+ /** Remaining percentage 0-100 (100 - usedPercent). */
37
+ remainingPercent: number;
38
+ /** Absolute used value when the provider reports one. */
39
+ used?: number;
40
+ /** Absolute limit value when the provider reports one. */
41
+ limit?: number;
42
+ /** Window length in minutes (300 = 5h, 10080 = 7d). */
43
+ durationMins?: number;
44
+ /** ISO 8601 timestamp when this window resets. */
45
+ resetsAt?: string;
46
+ /** True for unlimited / boosted windows. */
47
+ isUnlimited?: boolean;
48
+ /** Per-model windows (MiniMax returns per-model buckets). */
49
+ modelName?: string;
50
+ }
51
+ export type QuotaProviderState = 'ok' | 'auth_failed' | 'not_configured' | 'upstream_unavailable' | 'rate_limited' | 'malformed_response' | 'timed_out' | 'error';
52
+ /**
53
+ * Structured provider status. Flat shape (not a discriminated union) so it
54
+ * round-trips cleanly through the Zod schema. Adapters populate `message`
55
+ * only when it carries actionable detail; `state: 'ok'` omits it.
56
+ */
57
+ export interface QuotaProviderStatus {
58
+ state: QuotaProviderState;
59
+ message?: string;
60
+ category?: string;
61
+ }
62
+ /** Full API response for GET /api/quota — only configured providers appear. */
63
+ export interface QuotaResponse {
64
+ providers: QuotaSnapshot[];
65
+ }
66
+ /** A credential proposed by the settings UI but not persisted yet. */
67
+ export interface QuotaCredentialInput {
68
+ apiKey: string;
69
+ baseUrl?: string;
70
+ }
71
+ /** Result of checking a proposed credential against its upstream provider. */
72
+ export interface QuotaCredentialValidation {
73
+ provider: QuotaProviderId;
74
+ valid: boolean;
75
+ status: QuotaProviderStatus;
76
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Normalized quota domain types.
3
+ *
4
+ * These are provider-neutral. Every provider adapter converts its own
5
+ * heterogeneous response shape into this contract so the rest of the app
6
+ * never sees provider-specific fields.
7
+ *
8
+ * Mirrored on the Swift side by TokenDashSwift/Sources/TokenDash/Models/APIModels.swift.
9
+ */
10
+ export {};