@aliou/pi-synthetic 0.11.0 → 0.13.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.
@@ -0,0 +1,272 @@
1
+ import type { QuotasResponse } from "../types/quotas";
2
+
3
+ export type RiskSeverity = "none" | "warning" | "high" | "critical";
4
+
5
+ export interface QuotaWindow {
6
+ label: string;
7
+ usedPercent: number;
8
+ resetsAt: Date;
9
+ windowSeconds: number;
10
+ usedValue: number;
11
+ limitValue: number;
12
+ isCurrency?: boolean;
13
+ showPace?: boolean;
14
+ paceScale?: number;
15
+ limited?: boolean;
16
+ nextAmount?: string;
17
+ nextLabel?: string;
18
+ }
19
+
20
+ export interface WindowProjection {
21
+ pacePercent: number | null;
22
+ progress: number | null; // 0..1
23
+ projectedPercent: number; // 0..+
24
+ usedPercent: number;
25
+ }
26
+
27
+ export interface RiskAssessment extends WindowProjection {
28
+ usedFloorPercent: number | null;
29
+ warnProjectedPercent: number | null;
30
+ highProjectedPercent: number | null;
31
+ criticalProjectedPercent: number | null;
32
+ severity: RiskSeverity;
33
+ }
34
+
35
+ const MIN_PACE_PERCENT = 5;
36
+
37
+ // Threshold interpolation points
38
+ // Early window (0% progress) -> Late window (100% progress)
39
+ const THRESHOLDS = {
40
+ usedFloor: { start: 33, end: 8 },
41
+ warnProjected: { start: 260, end: 120 },
42
+ highProjected: { start: 320, end: 145 },
43
+ criticalProjected: { start: 400, end: 170 },
44
+ };
45
+
46
+ function interpolate(start: number, end: number, progress: number): number {
47
+ const clampedProgress = Math.max(0, Math.min(1, progress));
48
+ return start + (end - start) * clampedProgress;
49
+ }
50
+
51
+ /** Safely compute percentage, guarding against division by zero */
52
+ export function safePercent(used: number, limit: number): number {
53
+ if (!Number.isFinite(used) || !Number.isFinite(limit) || limit <= 0) return 0;
54
+ return Math.max(0, Math.min(100, (used / limit) * 100));
55
+ }
56
+
57
+ /** Parse currency string like "$1,234.56" to number */
58
+ export function parseCurrency(value: string): number {
59
+ const n = Number(value.replace(/[^0-9.-]/g, ""));
60
+ return Number.isFinite(n) ? n : 0;
61
+ }
62
+
63
+ export function toWindows(quotas: QuotasResponse): QuotaWindow[] {
64
+ const windows: QuotaWindow[] = [];
65
+
66
+ if (quotas.weeklyTokenLimit) {
67
+ const { weeklyTokenLimit } = quotas;
68
+ const limitValue = parseCurrency(weeklyTokenLimit.maxCredits);
69
+ const remainingValue = parseCurrency(weeklyTokenLimit.remainingCredits);
70
+ windows.push({
71
+ label: "Credits / week",
72
+ usedPercent: Math.max(
73
+ 0,
74
+ Math.min(100, 100 - weeklyTokenLimit.percentRemaining),
75
+ ),
76
+ resetsAt: new Date(weeklyTokenLimit.nextRegenAt),
77
+ windowSeconds: 24 * 60 * 60,
78
+ usedValue: limitValue - remainingValue,
79
+ limitValue,
80
+ isCurrency: true,
81
+ showPace: true,
82
+ paceScale: 1 / 7,
83
+ nextAmount: `+${weeklyTokenLimit.nextRegenCredits}`,
84
+ nextLabel: "Next regen",
85
+ });
86
+ }
87
+
88
+ if (quotas.rollingFiveHourLimit && quotas.rollingFiveHourLimit.max > 0) {
89
+ const { rollingFiveHourLimit } = quotas;
90
+ const used = rollingFiveHourLimit.max - rollingFiveHourLimit.remaining;
91
+ const tickAmount =
92
+ rollingFiveHourLimit.tickPercent * rollingFiveHourLimit.max;
93
+ windows.push({
94
+ label: "Requests / 5h",
95
+ usedPercent: safePercent(used, rollingFiveHourLimit.max),
96
+ resetsAt: new Date(rollingFiveHourLimit.nextTickAt),
97
+ windowSeconds: 5 * 60 * 60,
98
+ usedValue: Math.round(used),
99
+ limitValue: rollingFiveHourLimit.max,
100
+ showPace: false,
101
+ limited: rollingFiveHourLimit.limited,
102
+ nextAmount: `+${tickAmount.toFixed(1)}`,
103
+ nextLabel: "Next tick",
104
+ });
105
+ }
106
+
107
+ if (quotas.search?.hourly?.limit && quotas.search.hourly.limit > 0) {
108
+ const { hourly } = quotas.search;
109
+ windows.push({
110
+ label: "Search / hour",
111
+ usedPercent: safePercent(hourly.requests, hourly.limit),
112
+ resetsAt: new Date(hourly.renewsAt),
113
+ windowSeconds: 60 * 60,
114
+ usedValue: hourly.requests,
115
+ limitValue: hourly.limit,
116
+ showPace: true,
117
+ paceScale: 1,
118
+ nextLabel: "Resets",
119
+ });
120
+ }
121
+
122
+ if (quotas.freeToolCalls?.limit && quotas.freeToolCalls.limit > 0) {
123
+ windows.push({
124
+ label: "Free Tool Calls / day",
125
+ usedPercent: safePercent(
126
+ quotas.freeToolCalls.requests,
127
+ quotas.freeToolCalls.limit,
128
+ ),
129
+ resetsAt: new Date(quotas.freeToolCalls.renewsAt),
130
+ windowSeconds: 24 * 60 * 60,
131
+ usedValue: quotas.freeToolCalls.requests,
132
+ limitValue: quotas.freeToolCalls.limit,
133
+ showPace: true,
134
+ paceScale: 1,
135
+ nextLabel: "Resets",
136
+ });
137
+ }
138
+
139
+ return windows;
140
+ }
141
+
142
+ export function getPacePercent(window: QuotaWindow): number | null {
143
+ const totalMs = window.windowSeconds * 1000;
144
+ if (totalMs <= 0) return null;
145
+ const remainingMs = window.resetsAt.getTime() - Date.now();
146
+ const elapsedMs = totalMs - remainingMs;
147
+ return Math.max(0, Math.min(100, (elapsedMs / totalMs) * 100));
148
+ }
149
+
150
+ export function getProjectedPercent(
151
+ usedPercent: number,
152
+ pacePercent: number | null,
153
+ ): number {
154
+ if (pacePercent === null) return usedPercent;
155
+ const effectivePace = Math.max(MIN_PACE_PERCENT, pacePercent);
156
+ return Math.max(0, (usedPercent / effectivePace) * 100);
157
+ }
158
+
159
+ export function assessWindow(window: QuotaWindow): RiskAssessment {
160
+ // Respect showPace/paceScale: only compute pace when the window opts in,
161
+ // and apply paceScale to normalize (e.g. weekly windows scale daily pace by 1/7).
162
+ const rawPace = window.showPace ? getPacePercent(window) : null;
163
+ const pacePercent =
164
+ rawPace !== null ? rawPace * (window.paceScale ?? 1) : null;
165
+ const projectedPercent = getProjectedPercent(window.usedPercent, pacePercent);
166
+
167
+ // Calculate progress (0 to 1) through the window
168
+ let progress: number | null = null;
169
+ if (pacePercent !== null) {
170
+ progress = pacePercent / 100;
171
+ }
172
+
173
+ const base: WindowProjection = {
174
+ pacePercent,
175
+ progress,
176
+ projectedPercent,
177
+ usedPercent: window.usedPercent,
178
+ };
179
+
180
+ // Fallback when pace/progress unavailable: use static thresholds on projected only
181
+ if (progress === null) {
182
+ let severity: RiskSeverity = "none";
183
+ if (window.limited) {
184
+ severity = "critical";
185
+ } else if (projectedPercent >= 100) {
186
+ severity = "critical";
187
+ } else if (projectedPercent >= 90) {
188
+ severity = "high";
189
+ } else if (projectedPercent >= 80) {
190
+ severity = "warning";
191
+ }
192
+
193
+ return {
194
+ ...base,
195
+ usedFloorPercent: null,
196
+ warnProjectedPercent: 80,
197
+ highProjectedPercent: 90,
198
+ criticalProjectedPercent: 100,
199
+ severity,
200
+ };
201
+ }
202
+
203
+ // Dynamic thresholds based on window progress
204
+ const usedFloorPercent = interpolate(
205
+ THRESHOLDS.usedFloor.start,
206
+ THRESHOLDS.usedFloor.end,
207
+ progress,
208
+ );
209
+ const warnProjectedPercent = interpolate(
210
+ THRESHOLDS.warnProjected.start,
211
+ THRESHOLDS.warnProjected.end,
212
+ progress,
213
+ );
214
+ const highProjectedPercent = interpolate(
215
+ THRESHOLDS.highProjected.start,
216
+ THRESHOLDS.highProjected.end,
217
+ progress,
218
+ );
219
+ const criticalProjectedPercent = interpolate(
220
+ THRESHOLDS.criticalProjected.start,
221
+ THRESHOLDS.criticalProjected.end,
222
+ progress,
223
+ );
224
+
225
+ // Determine severity (hard-limited windows are always critical)
226
+ let severity: RiskSeverity = "none";
227
+ if (window.limited) {
228
+ severity = "critical";
229
+ } else if (window.usedPercent >= usedFloorPercent) {
230
+ if (projectedPercent >= criticalProjectedPercent) {
231
+ severity = "critical";
232
+ } else if (projectedPercent >= highProjectedPercent) {
233
+ severity = "high";
234
+ } else if (projectedPercent >= warnProjectedPercent) {
235
+ severity = "warning";
236
+ }
237
+ }
238
+
239
+ return {
240
+ ...base,
241
+ usedFloorPercent,
242
+ warnProjectedPercent,
243
+ highProjectedPercent,
244
+ criticalProjectedPercent,
245
+ severity,
246
+ };
247
+ }
248
+
249
+ export function formatTimeRemaining(date: Date): string {
250
+ const ms = date.getTime() - Date.now();
251
+ if (ms <= 0) return "now";
252
+ const totalMins = Math.ceil(ms / (1000 * 60));
253
+ const hours = Math.floor(totalMins / 60);
254
+ const mins = totalMins % 60;
255
+ if (hours >= 1) return mins > 0 ? `${hours}h${mins}m` : `${hours}h`;
256
+ const totalSecs = Math.ceil(ms / 1000);
257
+ return totalMins >= 1 ? `${totalMins}m` : `${totalSecs}s`;
258
+ }
259
+
260
+ export function getSeverityColor(
261
+ severity: RiskSeverity,
262
+ ): "success" | "warning" | "error" {
263
+ switch (severity) {
264
+ case "critical":
265
+ case "high":
266
+ return "error";
267
+ case "warning":
268
+ return "warning";
269
+ default:
270
+ return "success";
271
+ }
272
+ }