@aliou/pi-synthetic 0.9.0 → 0.10.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-synthetic",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -20,16 +20,77 @@ interface QuotaWindow {
20
20
  windowSeconds: number;
21
21
  usedValue: number;
22
22
  limitValue: number;
23
+ isCredits?: boolean;
24
+ isLimited?: boolean;
25
+ tickPercent?: number;
26
+ nextRegenCredits?: string;
27
+ }
28
+
29
+ /** Safely compute percentage, guarding against division by zero */
30
+ function safePercent(used: number, limit: number): number {
31
+ if (!Number.isFinite(used) || !Number.isFinite(limit) || limit <= 0) return 0;
32
+ return Math.max(0, Math.min(100, (used / limit) * 100));
33
+ }
34
+
35
+ /** Parse currency string like "$1,234.56" to number */
36
+ function parseCurrency(value: string): number {
37
+ const n = Number(value.replace(/[^0-9.-]/g, ""));
38
+ return Number.isFinite(n) ? n : 0;
23
39
  }
24
40
 
25
41
  function toWindows(quotas: QuotasResponse): QuotaWindow[] {
26
42
  const windows: QuotaWindow[] = [];
27
43
 
28
- if (quotas.subscription.limit > 0) {
44
+ // Weekly token limit (credits-based)
45
+ if (quotas.weeklyTokenLimit) {
46
+ const { weeklyTokenLimit } = quotas;
47
+ const limitValue = parseCurrency(weeklyTokenLimit.maxCredits);
48
+ const remainingValue = parseCurrency(weeklyTokenLimit.remainingCredits);
49
+ windows.push({
50
+ label: "Credits",
51
+ usedPercent: Math.max(
52
+ 0,
53
+ Math.min(100, 100 - weeklyTokenLimit.percentRemaining),
54
+ ),
55
+ resetsAt: new Date(weeklyTokenLimit.nextRegenAt),
56
+ windowSeconds: 7 * 24 * 60 * 60,
57
+ usedValue: limitValue - remainingValue,
58
+ limitValue,
59
+ isCredits: true,
60
+ nextRegenCredits: weeklyTokenLimit.nextRegenCredits,
61
+ });
62
+ }
63
+
64
+ // Rolling 5-hour limit (request-based)
65
+ if (quotas.rollingFiveHourLimit && quotas.rollingFiveHourLimit.max > 0) {
66
+ const { rollingFiveHourLimit } = quotas;
67
+ windows.push({
68
+ label: "5h",
69
+ usedPercent: safePercent(
70
+ rollingFiveHourLimit.max - rollingFiveHourLimit.remaining,
71
+ rollingFiveHourLimit.max,
72
+ ),
73
+ resetsAt: new Date(rollingFiveHourLimit.nextTickAt),
74
+ windowSeconds: 5 * 60 * 60,
75
+ usedValue: rollingFiveHourLimit.max - rollingFiveHourLimit.remaining,
76
+ limitValue: rollingFiveHourLimit.max,
77
+ isLimited: rollingFiveHourLimit.limited,
78
+ tickPercent: rollingFiveHourLimit.tickPercent,
79
+ });
80
+ }
81
+
82
+ // Legacy subscription (fallback if rollingFiveHourLimit not available)
83
+ if (
84
+ !quotas.rollingFiveHourLimit &&
85
+ quotas.subscription?.limit &&
86
+ quotas.subscription.limit > 0
87
+ ) {
29
88
  windows.push({
30
89
  label: "Completions",
31
- usedPercent:
32
- (quotas.subscription.requests / quotas.subscription.limit) * 100,
90
+ usedPercent: safePercent(
91
+ quotas.subscription.requests,
92
+ quotas.subscription.limit,
93
+ ),
33
94
  resetsAt: new Date(quotas.subscription.renewsAt),
34
95
  windowSeconds: 5 * 60 * 60,
35
96
  usedValue: quotas.subscription.requests,
@@ -37,11 +98,13 @@ function toWindows(quotas: QuotasResponse): QuotaWindow[] {
37
98
  });
38
99
  }
39
100
 
40
- if (quotas.search.hourly.limit > 0) {
101
+ if (quotas.search?.hourly?.limit && quotas.search.hourly.limit > 0) {
41
102
  windows.push({
42
103
  label: "Search",
43
- usedPercent:
44
- (quotas.search.hourly.requests / quotas.search.hourly.limit) * 100,
104
+ usedPercent: safePercent(
105
+ quotas.search.hourly.requests,
106
+ quotas.search.hourly.limit,
107
+ ),
45
108
  resetsAt: new Date(quotas.search.hourly.renewsAt),
46
109
  windowSeconds: 60 * 60,
47
110
  usedValue: quotas.search.hourly.requests,
@@ -49,11 +112,13 @@ function toWindows(quotas: QuotasResponse): QuotaWindow[] {
49
112
  });
50
113
  }
51
114
 
52
- if (quotas.freeToolCalls.limit > 0) {
115
+ if (quotas.freeToolCalls?.limit && quotas.freeToolCalls.limit > 0) {
53
116
  windows.push({
54
117
  label: "Free Tool Calls",
55
- usedPercent:
56
- (quotas.freeToolCalls.requests / quotas.freeToolCalls.limit) * 100,
118
+ usedPercent: safePercent(
119
+ quotas.freeToolCalls.requests,
120
+ quotas.freeToolCalls.limit,
121
+ ),
57
122
  resetsAt: new Date(quotas.freeToolCalls.renewsAt),
58
123
  windowSeconds: 24 * 60 * 60,
59
124
  usedValue: quotas.freeToolCalls.requests,
@@ -155,6 +220,34 @@ function renderProgressBar(
155
220
  return parts.join("");
156
221
  }
157
222
 
223
+ function renderSimpleIndicatorBar(
224
+ usedPercent: number,
225
+ width: number,
226
+ theme: Theme,
227
+ severity: "success" | "warning" | "error",
228
+ ): string {
229
+ const clampedPercent = Math.max(0, Math.min(100, usedPercent));
230
+ // Clamp to width - 1 to avoid off-by-one when usedPercent === 100
231
+ const usedIndex = Math.min(
232
+ Math.round((clampedPercent / 100) * width),
233
+ width - 1,
234
+ );
235
+ const parts: string[] = [];
236
+
237
+ // Hide marker when within 5% of edges
238
+ const showMarker = clampedPercent >= 5 && clampedPercent <= 95;
239
+
240
+ for (let idx = 0; idx < width; idx++) {
241
+ if (showMarker && idx === usedIndex) {
242
+ parts.push(theme.fg(severity, "|"));
243
+ } else {
244
+ parts.push(theme.fg("dim", "░"));
245
+ }
246
+ }
247
+
248
+ return parts.join("");
249
+ }
250
+
158
251
  export class QuotasComponent implements Component {
159
252
  private state: QuotasState = { type: "loading" };
160
253
  private theme: Theme;
@@ -254,22 +347,83 @@ export class QuotasComponent implements Component {
254
347
  truncateToWidth(` ${theme.fg("accent", window.label)}`, maxWidth),
255
348
  );
256
349
 
257
- // Progress bar + usage
258
- const bar = renderProgressBar(
259
- window.usedPercent,
260
- barWidth,
261
- theme,
262
- severity,
263
- pacePercent,
264
- );
265
- const usedStr = `${window.usedValue.toLocaleString()}/${window.limitValue.toLocaleString()} (${Math.round(window.usedPercent)}%)`;
266
- lines.push(
267
- truncateToWidth(` ${bar} ${theme.fg(severity, usedStr)}`, maxWidth),
268
- );
350
+ // Progress bar + usage (or indicator for new quota types)
351
+ if (window.isCredits || window.tickPercent !== undefined) {
352
+ // Show simple indicator bar for new quota types
353
+ const bar = renderSimpleIndicatorBar(
354
+ window.usedPercent,
355
+ barWidth,
356
+ theme,
357
+ severity,
358
+ );
359
+ const usedStr = window.isCredits
360
+ ? `$${window.usedValue.toFixed(2)}/$${window.limitValue.toFixed(2)} (${Math.round(window.usedPercent)}%)`
361
+ : `${window.usedValue.toFixed(0)}/${window.limitValue.toFixed(0)} (${Math.round(window.usedPercent)}%)`;
362
+ const limitedBadge = window.isLimited
363
+ ? theme.fg("error", " LIMITED")
364
+ : "";
365
+ lines.push(
366
+ truncateToWidth(
367
+ ` ${bar} ${theme.fg(severity, usedStr)}${limitedBadge}`,
368
+ maxWidth,
369
+ ),
370
+ );
371
+ } else {
372
+ // Traditional progress bar for legacy quota types
373
+ const bar = renderProgressBar(
374
+ window.usedPercent,
375
+ barWidth,
376
+ theme,
377
+ severity,
378
+ pacePercent,
379
+ );
380
+ const usedStr = `${window.usedValue.toLocaleString()}/${window.limitValue.toLocaleString()} (${Math.round(window.usedPercent)}%)`;
381
+ lines.push(
382
+ truncateToWidth(` ${bar} ${theme.fg(severity, usedStr)}`, maxWidth),
383
+ );
384
+ }
269
385
 
270
386
  // Metadata: estimated + pace left, reset time right
271
387
  const leftParts: string[] = [];
272
- if (projectedPercent > 0) {
388
+
389
+ // Show tick info for rolling window
390
+ if (window.tickPercent !== undefined) {
391
+ const now = Date.now();
392
+ const remainingMs = window.resetsAt.getTime() - now;
393
+ const remainingMins = Math.ceil(remainingMs / (1000 * 60));
394
+ const remainingSecs = Math.ceil(remainingMs / 1000);
395
+ const timeStr =
396
+ remainingMs <= 0
397
+ ? "now"
398
+ : remainingMins >= 1
399
+ ? `${remainingMins}m`
400
+ : `${remainingSecs}s`;
401
+ const tickValue = (window.tickPercent / 100) * window.limitValue;
402
+ const tickStr = `+${tickValue.toFixed(1)} in ${timeStr}`;
403
+ leftParts.push(theme.fg("dim", tickStr));
404
+ }
405
+
406
+ // Show next regen credits for weekly token limit
407
+ if (window.nextRegenCredits !== undefined) {
408
+ const now = Date.now();
409
+ const remainingMs = window.resetsAt.getTime() - now;
410
+ const remainingHours = Math.ceil(remainingMs / (1000 * 60 * 60));
411
+ const remainingMins = Math.ceil(remainingMs / (1000 * 60));
412
+ const timeStr =
413
+ remainingMs <= 0
414
+ ? "now"
415
+ : remainingHours >= 1
416
+ ? `${remainingHours}h`
417
+ : `${remainingMins}m`;
418
+ const regenStr = `+${window.nextRegenCredits} in ${timeStr}`;
419
+ leftParts.push(theme.fg("dim", regenStr));
420
+ }
421
+
422
+ if (
423
+ projectedPercent > 0 &&
424
+ window.tickPercent === undefined &&
425
+ window.nextRegenCredits === undefined
426
+ ) {
273
427
  const estStr = `est ${Math.round(projectedPercent)}%`;
274
428
  leftParts.push(
275
429
  severity !== "success"
@@ -278,7 +432,11 @@ export class QuotasComponent implements Component {
278
432
  );
279
433
  }
280
434
 
281
- if (pacePercent !== null) {
435
+ if (
436
+ pacePercent !== null &&
437
+ window.tickPercent === undefined &&
438
+ window.nextRegenCredits === undefined
439
+ ) {
282
440
  const paceDiff = window.usedPercent - pacePercent;
283
441
  if (Math.abs(paceDiff) > 5) {
284
442
  if (paceDiff > 0) {
@@ -28,34 +28,66 @@ interface SubCoreSettingsPayload {
28
28
  function toUsageSnapshot(quotas: QuotasResponse): UsageSnapshot {
29
29
  const windows: RateWindow[] = [];
30
30
 
31
- if (quotas.subscription) {
31
+ // Weekly token limit (credits-based)
32
+ if (quotas.weeklyTokenLimit) {
33
+ const { weeklyTokenLimit } = quotas;
34
+ windows.push({
35
+ label: "Credits",
36
+ usedPercent: Math.round(
37
+ Math.max(0, Math.min(100, 100 - weeklyTokenLimit.percentRemaining)),
38
+ ),
39
+ resetDescription: formatResetTime(weeklyTokenLimit.nextRegenAt),
40
+ resetAt: weeklyTokenLimit.nextRegenAt,
41
+ });
42
+ }
43
+
44
+ // Rolling 5-hour limit (request-based)
45
+ if (quotas.rollingFiveHourLimit && quotas.rollingFiveHourLimit.max > 0) {
46
+ const { rollingFiveHourLimit } = quotas;
47
+ const used = rollingFiveHourLimit.max - rollingFiveHourLimit.remaining;
48
+ windows.push({
49
+ label: "5h",
50
+ usedPercent: Math.round(
51
+ Math.max(0, Math.min(100, (used / rollingFiveHourLimit.max) * 100)),
52
+ ),
53
+ resetDescription: formatResetTime(rollingFiveHourLimit.nextTickAt),
54
+ resetAt: rollingFiveHourLimit.nextTickAt,
55
+ });
56
+ }
57
+
58
+ // Legacy subscription (fallback if rollingFiveHourLimit not available)
59
+ if (
60
+ !quotas.rollingFiveHourLimit &&
61
+ quotas.subscription?.limit &&
62
+ quotas.subscription.limit > 0
63
+ ) {
32
64
  const pct =
33
65
  (quotas.subscription.requests / quotas.subscription.limit) * 100;
34
66
  windows.push({
35
67
  label: "5h",
36
- usedPercent: Math.round(pct),
68
+ usedPercent: Math.round(Math.max(0, Math.min(100, pct))),
37
69
  resetDescription: formatResetTime(quotas.subscription.renewsAt),
38
70
  resetAt: quotas.subscription.renewsAt,
39
71
  });
40
72
  }
41
73
 
42
- if (quotas.search?.hourly) {
74
+ if (quotas.search?.hourly?.limit && quotas.search.hourly.limit > 0) {
43
75
  const pct =
44
76
  (quotas.search.hourly.requests / quotas.search.hourly.limit) * 100;
45
77
  windows.push({
46
78
  label: "Search",
47
- usedPercent: Math.round(pct),
79
+ usedPercent: Math.round(Math.max(0, Math.min(100, pct))),
48
80
  resetDescription: formatResetTime(quotas.search.hourly.renewsAt),
49
81
  resetAt: quotas.search.hourly.renewsAt,
50
82
  });
51
83
  }
52
84
 
53
- if (quotas.freeToolCalls) {
85
+ if (quotas.freeToolCalls?.limit && quotas.freeToolCalls.limit > 0) {
54
86
  const pct =
55
87
  (quotas.freeToolCalls.requests / quotas.freeToolCalls.limit) * 100;
56
88
  windows.push({
57
89
  label: "Tools",
58
- usedPercent: Math.round(pct),
90
+ usedPercent: Math.round(Math.max(0, Math.min(100, pct))),
59
91
  resetDescription: formatResetTime(quotas.freeToolCalls.renewsAt),
60
92
  resetAt: quotas.freeToolCalls.renewsAt,
61
93
  });
@@ -36,7 +36,7 @@ const SYNTHETIC_REASONING_EFFORT_MAP = {
36
36
  } as const;
37
37
 
38
38
  export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
39
- // API: hf:zai-org/GLM-4.7 → ctx=202752, out=65536
39
+ // API: hf:zai-org/GLM-4.7 → ctx=202752, multimodal
40
40
  {
41
41
  id: "hf:zai-org/GLM-4.7",
42
42
  name: "zai-org/GLM-4.7",
@@ -45,11 +45,11 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
45
45
  supportsReasoningEffort: true,
46
46
  reasoningEffortMap: SYNTHETIC_REASONING_EFFORT_MAP,
47
47
  },
48
- input: ["text"],
48
+ input: ["text", "image"],
49
49
  cost: {
50
- input: 0.45,
50
+ input: 2.19,
51
51
  output: 2.19,
52
- cacheRead: 0.45,
52
+ cacheRead: 2.19,
53
53
  cacheWrite: 0,
54
54
  },
55
55
  contextWindow: 202752,
@@ -74,6 +74,26 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
74
74
  contextWindow: 196608,
75
75
  maxTokens: 65536,
76
76
  },
77
+ // API: hf:zai-org/GLM-5.1 → ctx=196608, out=65536
78
+ {
79
+ id: "hf:zai-org/GLM-5.1",
80
+ name: "zai-org/GLM-5.1",
81
+ reasoning: true,
82
+ compat: {
83
+ supportsReasoningEffort: true,
84
+ reasoningEffortMap: SYNTHETIC_REASONING_EFFORT_MAP,
85
+ supportsDeveloperRole: false,
86
+ },
87
+ input: ["text"],
88
+ cost: {
89
+ input: 1,
90
+ output: 3,
91
+ cacheRead: 1,
92
+ cacheWrite: 0,
93
+ },
94
+ contextWindow: 196608,
95
+ maxTokens: 65536,
96
+ },
77
97
  // API: hf:zai-org/GLM-4.7-Flash → ctx=196608
78
98
  {
79
99
  id: "hf:zai-org/GLM-4.7-Flash",
@@ -1,19 +1,33 @@
1
1
  export interface QuotasResponse {
2
- subscription: {
2
+ subscription?: {
3
3
  limit: number;
4
4
  requests: number;
5
5
  renewsAt: string;
6
6
  };
7
- search: {
8
- hourly: {
7
+ search?: {
8
+ hourly?: {
9
9
  limit: number;
10
10
  requests: number;
11
11
  renewsAt: string;
12
12
  };
13
13
  };
14
- freeToolCalls: {
14
+ freeToolCalls?: {
15
15
  limit: number;
16
16
  requests: number;
17
17
  renewsAt: string;
18
18
  };
19
+ weeklyTokenLimit?: {
20
+ nextRegenAt: string;
21
+ percentRemaining: number;
22
+ maxCredits: string;
23
+ remainingCredits: string;
24
+ nextRegenCredits: string;
25
+ };
26
+ rollingFiveHourLimit?: {
27
+ nextTickAt: string;
28
+ tickPercent: number;
29
+ remaining: number;
30
+ max: number;
31
+ limited: boolean;
32
+ };
19
33
  }