pi-harness-runtime 1.1.430 → 1.1.432
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 +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-sink/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/logging/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/tooling-preference/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/two-phase-review/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/voice-api/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/packages/notification/adapters/email-adapter.ts +0 -126
- package/packages/notification/adapters/ntfy-adapter.ts +0 -100
- package/packages/notification/adapters/telegram-adapter.ts +0 -107
- package/packages/notification/adapters/webhook-adapter.ts +0 -75
- package/packages/notification/base-adapter.ts +0 -75
- package/packages/notification/index.ts +0 -51
- package/packages/notification/notification-center.ts +0 -286
- package/packages/notification/types.ts +0 -89
- package/packages/quota-manager/index.ts +0 -28
- package/packages/quota-manager/quota-manager.ts +0 -339
- package/packages/quota-manager/tui-usage-monitor.ts +0 -355
- package/packages/types/src/ai-providers.d.ts +0 -36
- package/packages/types/src/runtime-types.d.ts +0 -418
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Quota Manager — RFC-0003
|
|
3
|
-
*
|
|
4
|
-
* Collects quota signals from API responses, provider status,
|
|
5
|
-
* Playwright, and local estimates. Produces provider availability state.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type {
|
|
9
|
-
QuotaSignal,
|
|
10
|
-
QuotaState,
|
|
11
|
-
} from "../../packages/types/src/runtime-types.js";
|
|
12
|
-
|
|
13
|
-
export interface QuotaSignalInput {
|
|
14
|
-
provider: string;
|
|
15
|
-
source:
|
|
16
|
-
| "api_response"
|
|
17
|
-
| "provider_status"
|
|
18
|
-
| "playwright"
|
|
19
|
-
| "local_estimate"
|
|
20
|
-
| "tui_message";
|
|
21
|
-
windowType: "5h" | "daily" | "weekly" | "monthly";
|
|
22
|
-
usedPct?: number;
|
|
23
|
-
exhausted?: boolean;
|
|
24
|
-
resetsAt?: string;
|
|
25
|
-
retryAfterMs?: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export class QuotaManager {
|
|
29
|
-
private signals: Map<string, QuotaSignal[]> = new Map();
|
|
30
|
-
private readonly staleThresholdMs: number;
|
|
31
|
-
|
|
32
|
-
constructor(staleThresholdMs: number = 30 * 60 * 1000) {
|
|
33
|
-
this.staleThresholdMs = staleThresholdMs;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Record a quota signal
|
|
38
|
-
*/
|
|
39
|
-
recordSignal(input: QuotaSignalInput): void {
|
|
40
|
-
const signal: QuotaSignal = {
|
|
41
|
-
provider: input.provider,
|
|
42
|
-
windowType: input.windowType,
|
|
43
|
-
usedPct: input.usedPct ?? 0,
|
|
44
|
-
remainingPct: 100 - (input.usedPct ?? 0),
|
|
45
|
-
exhausted: input.exhausted ?? false,
|
|
46
|
-
source: input.source,
|
|
47
|
-
capturedAt: new Date().toISOString(),
|
|
48
|
-
resetsAt: input.resetsAt,
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const key = `${input.provider}:${input.windowType}`;
|
|
52
|
-
const existing = this.signals.get(key) ?? [];
|
|
53
|
-
this.signals.set(key, [...existing, signal]);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Get the latest signal for a provider
|
|
58
|
-
*/
|
|
59
|
-
getLatestSignal(
|
|
60
|
-
provider: string,
|
|
61
|
-
windowType?: "5h" | "daily" | "weekly" | "monthly",
|
|
62
|
-
): QuotaSignal | null {
|
|
63
|
-
const key = windowType ? `${provider}:${windowType}` : provider;
|
|
64
|
-
|
|
65
|
-
if (windowType) {
|
|
66
|
-
const signals = this.signals.get(key) ?? [];
|
|
67
|
-
return (
|
|
68
|
-
signals.sort(
|
|
69
|
-
(a, b) => Date.parse(b.capturedAt) - Date.parse(a.capturedAt),
|
|
70
|
-
)[0] ?? null
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Return most recent across all window types
|
|
75
|
-
let latest: QuotaSignal | null = null;
|
|
76
|
-
let latestTs = 0;
|
|
77
|
-
|
|
78
|
-
for (const entry of Array.from(this.signals.entries())) {
|
|
79
|
-
const [k, sigs] = entry;
|
|
80
|
-
if (k.startsWith(`${provider}:`)) {
|
|
81
|
-
const mostRecent = sigs.sort(
|
|
82
|
-
(a: QuotaSignal, b: QuotaSignal) =>
|
|
83
|
-
Date.parse(b.capturedAt) - Date.parse(a.capturedAt),
|
|
84
|
-
)[0];
|
|
85
|
-
if (mostRecent) {
|
|
86
|
-
const ts = Date.parse(mostRecent.capturedAt);
|
|
87
|
-
if (ts > latestTs) {
|
|
88
|
-
latestTs = ts;
|
|
89
|
-
latest = mostRecent;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return latest;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Get quota state for a provider
|
|
100
|
-
*/
|
|
101
|
-
getProviderState(provider: string): QuotaState {
|
|
102
|
-
const signals = this.getSignals(provider);
|
|
103
|
-
const exhausted = signals.some(
|
|
104
|
-
(s) => s.exhausted || (s.usedPct !== undefined && s.usedPct >= 100),
|
|
105
|
-
);
|
|
106
|
-
const limited = signals.some(
|
|
107
|
-
(s) => s.usedPct !== undefined && s.usedPct >= 90,
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
// Find next reset time
|
|
111
|
-
let nextAvailableAt: string | undefined;
|
|
112
|
-
for (const signal of signals) {
|
|
113
|
-
if (signal.resetsAt) {
|
|
114
|
-
if (
|
|
115
|
-
!nextAvailableAt ||
|
|
116
|
-
Date.parse(signal.resetsAt) < Date.parse(nextAvailableAt)
|
|
117
|
-
) {
|
|
118
|
-
nextAvailableAt = signal.resetsAt;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
provider,
|
|
125
|
-
available: !exhausted && !limited,
|
|
126
|
-
limited,
|
|
127
|
-
exhausted,
|
|
128
|
-
signals,
|
|
129
|
-
nextAvailableAt,
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Get signals for a provider
|
|
135
|
-
*/
|
|
136
|
-
getSignals(provider: string): QuotaSignal[] {
|
|
137
|
-
const result: QuotaSignal[] = [];
|
|
138
|
-
const cutoff = Date.now() - this.staleThresholdMs;
|
|
139
|
-
|
|
140
|
-
for (const entry of Array.from(this.signals.entries())) {
|
|
141
|
-
const [key, sigs] = entry;
|
|
142
|
-
if (key.startsWith(`${provider}:`)) {
|
|
143
|
-
for (const signal of sigs) {
|
|
144
|
-
if (Date.parse(signal.capturedAt) >= cutoff) {
|
|
145
|
-
result.push(signal);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return result;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Check if provider is available
|
|
156
|
-
*/
|
|
157
|
-
isAvailable(provider: string): boolean {
|
|
158
|
-
return this.getProviderState(provider).available;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Check if provider is exhausted
|
|
163
|
-
*/
|
|
164
|
-
isExhausted(provider: string): boolean {
|
|
165
|
-
return this.getProviderState(provider).exhausted;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Get time until provider is available again
|
|
170
|
-
*/
|
|
171
|
-
getWaitTime(provider: string): number | null {
|
|
172
|
-
const state = this.getProviderState(provider);
|
|
173
|
-
if (!state.nextAvailableAt) return null;
|
|
174
|
-
|
|
175
|
-
const waitMs = Date.parse(state.nextAvailableAt) - Date.now();
|
|
176
|
-
return waitMs > 0 ? waitMs : null;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Get best available provider from a list
|
|
181
|
-
*/
|
|
182
|
-
selectBestProvider(providers: string[]): string | null {
|
|
183
|
-
const available = providers.filter((p) => this.isAvailable(p));
|
|
184
|
-
|
|
185
|
-
if (available.length === 0) return null;
|
|
186
|
-
|
|
187
|
-
// Sort by remaining quota (prefer those with more remaining)
|
|
188
|
-
available.sort((a, b) => {
|
|
189
|
-
const signalA = this.getLatestSignal(a);
|
|
190
|
-
const signalB = this.getLatestSignal(b);
|
|
191
|
-
const remA = signalA?.remainingPct ?? 100;
|
|
192
|
-
const remB = signalB?.remainingPct ?? 100;
|
|
193
|
-
return remB - remA;
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
return available[0];
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Clear stale signals
|
|
201
|
-
*/
|
|
202
|
-
clearStale(): void {
|
|
203
|
-
const cutoff = Date.now() - this.staleThresholdMs;
|
|
204
|
-
|
|
205
|
-
for (const entry of Array.from(this.signals.entries())) {
|
|
206
|
-
const [key, sigs] = entry;
|
|
207
|
-
const fresh = sigs.filter(
|
|
208
|
-
(s: QuotaSignal) => Date.parse(s.capturedAt) >= cutoff,
|
|
209
|
-
);
|
|
210
|
-
if (fresh.length === 0) {
|
|
211
|
-
this.signals.delete(key);
|
|
212
|
-
} else {
|
|
213
|
-
this.signals.set(key, fresh);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Generate quota report
|
|
220
|
-
*/
|
|
221
|
-
generateReport(providers: string[]): string {
|
|
222
|
-
const lines = ["Quota Status Report", "=".repeat(40), ""];
|
|
223
|
-
|
|
224
|
-
for (const provider of providers) {
|
|
225
|
-
const state = this.getProviderState(provider);
|
|
226
|
-
const latest = this.getLatestSignal(provider);
|
|
227
|
-
|
|
228
|
-
const status = state.exhausted
|
|
229
|
-
? "EXHAUSTED"
|
|
230
|
-
: state.limited
|
|
231
|
-
? "LIMITED"
|
|
232
|
-
: "AVAILABLE";
|
|
233
|
-
|
|
234
|
-
lines.push(`${provider}: ${status}`);
|
|
235
|
-
|
|
236
|
-
if (latest) {
|
|
237
|
-
lines.push(` Latest: ${latest.usedPct ?? 0}% used`);
|
|
238
|
-
if (latest.resetsAt) {
|
|
239
|
-
const wait = Date.parse(latest.resetsAt) - Date.now();
|
|
240
|
-
if (wait > 0) {
|
|
241
|
-
lines.push(` Resets: in ${Math.ceil(wait / 60000)} minutes`);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
lines.push(` Source: ${latest.source}`);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
lines.push("");
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return lines.join("\n");
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Parse quota signal from MiniMax error
|
|
256
|
-
*/
|
|
257
|
-
export function parseMiniMaxError(error: unknown): QuotaSignal | null {
|
|
258
|
-
const e = error as Record<string, unknown>;
|
|
259
|
-
const msg = String(e.message ?? e.error ?? "");
|
|
260
|
-
|
|
261
|
-
// MiniMax quota error: "Error code: 2056 - Rate limit exceeded. Retry after..."
|
|
262
|
-
if (
|
|
263
|
-
msg.includes("2056") ||
|
|
264
|
-
msg.includes("quota") ||
|
|
265
|
-
msg.includes("rate limit")
|
|
266
|
-
) {
|
|
267
|
-
// Try to extract retry time
|
|
268
|
-
const retryMatch = msg.match(/retry after (\d+) (seconds?|minutes?)/i);
|
|
269
|
-
let retryAfterMs: number | undefined;
|
|
270
|
-
|
|
271
|
-
if (retryMatch) {
|
|
272
|
-
const value = parseInt(retryMatch[1], 10);
|
|
273
|
-
const unit = retryMatch[2].toLowerCase();
|
|
274
|
-
retryAfterMs = unit.startsWith("minute")
|
|
275
|
-
? value * 60 * 1000
|
|
276
|
-
: value * 1000;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return {
|
|
280
|
-
provider: "minimax",
|
|
281
|
-
windowType: "5h",
|
|
282
|
-
usedPct: 100,
|
|
283
|
-
remainingPct: 0,
|
|
284
|
-
exhausted: true,
|
|
285
|
-
source: "api_response",
|
|
286
|
-
capturedAt: new Date().toISOString(),
|
|
287
|
-
retryAfterMs,
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return null;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Parse quota signal from OpenAI error
|
|
296
|
-
*/
|
|
297
|
-
export function parseOpenAIError(error: unknown): QuotaSignal | null {
|
|
298
|
-
const e = error as Record<string, unknown>;
|
|
299
|
-
const code = String(e.code ?? "");
|
|
300
|
-
const msg = String(e.message ?? e.error ?? "");
|
|
301
|
-
|
|
302
|
-
if (code === "insufficient_quota" || code === "context_length_exceeded") {
|
|
303
|
-
return {
|
|
304
|
-
provider: "openai",
|
|
305
|
-
windowType: "daily",
|
|
306
|
-
usedPct: 100,
|
|
307
|
-
remainingPct: 0,
|
|
308
|
-
exhausted: true,
|
|
309
|
-
source: "api_response",
|
|
310
|
-
capturedAt: new Date().toISOString(),
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (msg.includes("429")) {
|
|
315
|
-
const retryMatch = msg.match(/retry after (\d+) (seconds?|minutes?)/i);
|
|
316
|
-
let retryAfterMs: number | undefined;
|
|
317
|
-
|
|
318
|
-
if (retryMatch) {
|
|
319
|
-
const value = parseInt(retryMatch[1], 10);
|
|
320
|
-
const unit = retryMatch[2].toLowerCase();
|
|
321
|
-
retryAfterMs = unit.startsWith("minute")
|
|
322
|
-
? value * 60 * 1000
|
|
323
|
-
: value * 1000;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
return {
|
|
327
|
-
provider: "openai",
|
|
328
|
-
windowType: "5h",
|
|
329
|
-
usedPct: 100,
|
|
330
|
-
remainingPct: 0,
|
|
331
|
-
exhausted: true,
|
|
332
|
-
source: "api_response",
|
|
333
|
-
capturedAt: new Date().toISOString(),
|
|
334
|
-
retryAfterMs,
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
return null;
|
|
339
|
-
}
|
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TUI Usage Monitor — Unified TUI Message Parser
|
|
3
|
-
*
|
|
4
|
-
* Hooks into pi's TUI messages to capture quota signals from all providers.
|
|
5
|
-
* Handles OpenAI, GLM, and any other provider that reports via TUI.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* ```typescript
|
|
9
|
-
* const monitor = new TUIUsageMonitor(quotaManager);
|
|
10
|
-
*
|
|
11
|
-
* // Hook into pi
|
|
12
|
-
* pi.on("error", (event) => monitor.processMessage(event.message));
|
|
13
|
-
* pi.on("message", (event) => monitor.processMessage(event.message));
|
|
14
|
-
* ```
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { EventEmitter } from "node:events";
|
|
18
|
-
import type { QuotaManager } from "./quota-manager.js";
|
|
19
|
-
|
|
20
|
-
export interface TUIUsageSignal {
|
|
21
|
-
provider: "openai" | "glm" | "anthropic" | "openrouter" | "unknown";
|
|
22
|
-
timestamp: string;
|
|
23
|
-
/** Usage percentage (0-100), if available */
|
|
24
|
-
usedPct?: number;
|
|
25
|
-
/** Remaining percentage (0-100) */
|
|
26
|
-
remainingPct: number;
|
|
27
|
-
/** If exhausted */
|
|
28
|
-
exhausted: boolean;
|
|
29
|
-
/** Reset time */
|
|
30
|
-
resetsAt?: string;
|
|
31
|
-
/** Which limit was hit */
|
|
32
|
-
limitType: "tokens" | "context_window" | "rate_limit" | "unknown";
|
|
33
|
-
/** Original message */
|
|
34
|
-
originalMessage: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface TUIUsageMonitorConfig {
|
|
38
|
-
/** QuotaManager instance to feed signals into */
|
|
39
|
-
quotaManager: QuotaManager;
|
|
40
|
-
/** Enable debug logging */
|
|
41
|
-
debug?: boolean;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Provider-specific patterns
|
|
46
|
-
*/
|
|
47
|
-
const PROVIDER_PATTERNS = {
|
|
48
|
-
openai: [
|
|
49
|
-
/(?:^|\s)OpenAI(?:$|\s)/i,
|
|
50
|
-
/(?:^|\s)GPT(?:$|\s)/i,
|
|
51
|
-
/openai.*(?:quota|limit|exhausted)/i,
|
|
52
|
-
/gpt.*(?:quota|limit|exhausted)/i,
|
|
53
|
-
/insufficient_quota/i,
|
|
54
|
-
/context_length_exceeded/i,
|
|
55
|
-
/openai.*rate.?limit/i,
|
|
56
|
-
/429.*openai/i,
|
|
57
|
-
/openai.*context.*(?:window|length)/i,
|
|
58
|
-
],
|
|
59
|
-
glm: [
|
|
60
|
-
/(?:^|\s)GLM(?:$|\s)/i,
|
|
61
|
-
/(?:^|\s)Zhipu(?:$|\s)/i,
|
|
62
|
-
/glm.*(?:quota|limit|exhausted)/i,
|
|
63
|
-
/zhipu.*(?:quota|limit|exhausted)/i,
|
|
64
|
-
/glm.*context.*(?:window|length)/i,
|
|
65
|
-
/glm.*rate.?limit/i,
|
|
66
|
-
],
|
|
67
|
-
anthropic: [
|
|
68
|
-
/(?:^|\s)Claude(?:$|\s)/i,
|
|
69
|
-
/(?:^|\s)Anthropic(?:$|\s)/i,
|
|
70
|
-
/anthropic.*(?:quota|limit|exhausted)/i,
|
|
71
|
-
/claude.*(?:quota|limit|exhausted)/i,
|
|
72
|
-
/overloaded_error/i,
|
|
73
|
-
/rate_limit_error/i,
|
|
74
|
-
],
|
|
75
|
-
openrouter: [
|
|
76
|
-
/(?:^|\s)OpenRouter(?:$|\s)/i,
|
|
77
|
-
/openrouter.*(?:quota|limit|exhausted)/i,
|
|
78
|
-
],
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const RESET_TIME_PATTERNS = [
|
|
82
|
-
/reset(?:s)?\s+(?:at|in)\s+(\d{1,2}):(\d{2})/i,
|
|
83
|
-
/(?:at|in)\s+(\d{1,2}):(\d{2})/i,
|
|
84
|
-
/retry\s+after\s+(\d{1,2}):(\d{2})/i,
|
|
85
|
-
/(\d{1,2}):(\d{2})\s*(?:am|pm)/i,
|
|
86
|
-
];
|
|
87
|
-
|
|
88
|
-
const PERCENTAGE_PATTERN = /(\d+)%/;
|
|
89
|
-
|
|
90
|
-
export class TUIUsageMonitor extends EventEmitter {
|
|
91
|
-
private readonly quotaManager: QuotaManager;
|
|
92
|
-
private readonly debug: boolean;
|
|
93
|
-
private lastSignals: Map<string, TUIUsageSignal> = new Map();
|
|
94
|
-
|
|
95
|
-
constructor(config: TUIUsageMonitorConfig) {
|
|
96
|
-
super();
|
|
97
|
-
this.quotaManager = config.quotaManager;
|
|
98
|
-
this.debug = config.debug ?? false;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Process a TUI message and extract quota signals
|
|
103
|
-
*/
|
|
104
|
-
processMessage(message: string): TUIUsageSignal | null {
|
|
105
|
-
// Try to identify provider
|
|
106
|
-
const provider = this.detectProvider(message);
|
|
107
|
-
if (!provider) {
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Parse the signal
|
|
112
|
-
const signal = this.parseSignal(message, provider);
|
|
113
|
-
if (!signal) {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Store last signal
|
|
118
|
-
this.lastSignals.set(provider, signal);
|
|
119
|
-
|
|
120
|
-
// Record to QuotaManager
|
|
121
|
-
this.recordToQuotaManager(signal);
|
|
122
|
-
|
|
123
|
-
// Emit event
|
|
124
|
-
this.emit("signal", signal);
|
|
125
|
-
|
|
126
|
-
// Debug log
|
|
127
|
-
if (this.debug) {
|
|
128
|
-
console.log(`[TUIUsageMonitor] Detected ${provider}:`, signal);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return signal;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Detect which provider the message is about
|
|
136
|
-
*/
|
|
137
|
-
detectProvider(
|
|
138
|
-
message: string,
|
|
139
|
-
): "openai" | "glm" | "anthropic" | "openrouter" | null {
|
|
140
|
-
for (const [provider, patterns] of Object.entries(PROVIDER_PATTERNS)) {
|
|
141
|
-
for (const pattern of patterns) {
|
|
142
|
-
if (pattern.test(message)) {
|
|
143
|
-
return provider as "openai" | "glm" | "anthropic" | "openrouter";
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Get the last signal for a provider
|
|
152
|
-
*/
|
|
153
|
-
getLastSignal(
|
|
154
|
-
provider: "openai" | "glm" | "anthropic" | "openrouter",
|
|
155
|
-
): TUIUsageSignal | null {
|
|
156
|
-
return this.lastSignals.get(provider) ?? null;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Get all last signals
|
|
161
|
-
*/
|
|
162
|
-
getAllLastSignals(): Map<string, TUIUsageSignal> {
|
|
163
|
-
return new Map(this.lastSignals);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Clear stored signals
|
|
168
|
-
*/
|
|
169
|
-
clear(): void {
|
|
170
|
-
this.lastSignals.clear();
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Parse the quota signal from message
|
|
175
|
-
*/
|
|
176
|
-
private parseSignal(
|
|
177
|
-
message: string,
|
|
178
|
-
provider: "openai" | "glm" | "anthropic" | "openrouter",
|
|
179
|
-
): TUIUsageSignal | null {
|
|
180
|
-
// Determine limit type
|
|
181
|
-
const limitType = this.detectLimitType(message);
|
|
182
|
-
|
|
183
|
-
// Parse reset time
|
|
184
|
-
const resetsAt = this.parseResetTime(message);
|
|
185
|
-
|
|
186
|
-
// Parse percentage if available
|
|
187
|
-
const pctMatch = message.match(PERCENTAGE_PATTERN);
|
|
188
|
-
const usedPct = pctMatch ? parseInt(pctMatch[1], 10) : undefined;
|
|
189
|
-
|
|
190
|
-
// Determine if exhausted
|
|
191
|
-
const exhausted =
|
|
192
|
-
usedPct !== undefined
|
|
193
|
-
? usedPct >= 100
|
|
194
|
-
: this.containsExhaustedKeywords(message);
|
|
195
|
-
|
|
196
|
-
if (!exhausted && !resetsAt) {
|
|
197
|
-
// Not a quota signal
|
|
198
|
-
return null;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return {
|
|
202
|
-
provider,
|
|
203
|
-
timestamp: new Date().toISOString(),
|
|
204
|
-
usedPct,
|
|
205
|
-
remainingPct: usedPct !== undefined ? 100 - usedPct : 0,
|
|
206
|
-
exhausted,
|
|
207
|
-
resetsAt,
|
|
208
|
-
limitType,
|
|
209
|
-
originalMessage: message,
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Detect the type of limit that was hit
|
|
215
|
-
*/
|
|
216
|
-
private detectLimitType(
|
|
217
|
-
message: string,
|
|
218
|
-
): "tokens" | "context_window" | "rate_limit" | "unknown" {
|
|
219
|
-
const lowerMsg = message.toLowerCase();
|
|
220
|
-
|
|
221
|
-
// Context window
|
|
222
|
-
if (
|
|
223
|
-
lowerMsg.includes("context") &&
|
|
224
|
-
(lowerMsg.includes("window") ||
|
|
225
|
-
lowerMsg.includes("length") ||
|
|
226
|
-
lowerMsg.includes("full") ||
|
|
227
|
-
lowerMsg.includes("exceeded"))
|
|
228
|
-
) {
|
|
229
|
-
return "context_window";
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Rate limit
|
|
233
|
-
if (
|
|
234
|
-
lowerMsg.includes("rate") ||
|
|
235
|
-
lowerMsg.includes("429") ||
|
|
236
|
-
lowerMsg.includes("too many requests")
|
|
237
|
-
) {
|
|
238
|
-
return "rate_limit";
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Tokens/quota exhausted
|
|
242
|
-
if (
|
|
243
|
-
lowerMsg.includes("quota") ||
|
|
244
|
-
lowerMsg.includes("exhausted") ||
|
|
245
|
-
lowerMsg.includes("ran out") ||
|
|
246
|
-
lowerMsg.includes("limit")
|
|
247
|
-
) {
|
|
248
|
-
return "tokens";
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return "unknown";
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Check if message contains exhausted keywords
|
|
256
|
-
*/
|
|
257
|
-
private containsExhaustedKeywords(message: string): boolean {
|
|
258
|
-
const keywords = [
|
|
259
|
-
"exhausted",
|
|
260
|
-
"quota",
|
|
261
|
-
"limit",
|
|
262
|
-
"rate limit",
|
|
263
|
-
"429",
|
|
264
|
-
"exceeded",
|
|
265
|
-
"ran out",
|
|
266
|
-
];
|
|
267
|
-
const lowerMsg = message.toLowerCase();
|
|
268
|
-
return keywords.some((kw) => lowerMsg.includes(kw));
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Parse reset time from message
|
|
273
|
-
*/
|
|
274
|
-
private parseResetTime(message: string): string | undefined {
|
|
275
|
-
for (const pattern of RESET_TIME_PATTERNS) {
|
|
276
|
-
const match = message.match(pattern);
|
|
277
|
-
if (match) {
|
|
278
|
-
let hour = parseInt(match[1], 10);
|
|
279
|
-
const minute = parseInt(match[2], 10);
|
|
280
|
-
|
|
281
|
-
// Handle AM/PM
|
|
282
|
-
const matchIndex = message.indexOf(match[0]);
|
|
283
|
-
const afterMatch = message.slice(matchIndex, matchIndex + 20);
|
|
284
|
-
const isPM = /pm/i.test(afterMatch);
|
|
285
|
-
const isAM = /am/i.test(afterMatch);
|
|
286
|
-
|
|
287
|
-
if (isPM && hour < 12) hour += 12;
|
|
288
|
-
if (isAM && hour === 12) hour = 0;
|
|
289
|
-
|
|
290
|
-
// Convert to ISO string
|
|
291
|
-
const now = new Date();
|
|
292
|
-
const resetTime = new Date(now);
|
|
293
|
-
resetTime.setHours(hour, minute, 0, 0);
|
|
294
|
-
|
|
295
|
-
// If time has passed today, assume tomorrow
|
|
296
|
-
if (resetTime <= now) {
|
|
297
|
-
resetTime.setDate(resetTime.getDate() + 1);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
return resetTime.toISOString();
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
return undefined;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Record signal to QuotaManager
|
|
308
|
-
*/
|
|
309
|
-
private recordToQuotaManager(signal: TUIUsageSignal): void {
|
|
310
|
-
// Map limit type to window type
|
|
311
|
-
let windowType: "5h" | "daily" | "weekly" | "monthly" = "5h";
|
|
312
|
-
|
|
313
|
-
if (signal.limitType === "context_window") {
|
|
314
|
-
// Context window is usually daily or 5h depending on provider
|
|
315
|
-
windowType = "daily";
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Record to QuotaManager
|
|
319
|
-
this.quotaManager.recordSignal({
|
|
320
|
-
provider: signal.provider,
|
|
321
|
-
source: "tui_message",
|
|
322
|
-
windowType,
|
|
323
|
-
usedPct: signal.usedPct,
|
|
324
|
-
exhausted: signal.exhausted,
|
|
325
|
-
resetsAt: signal.resetsAt,
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Create a TUI usage monitor and automatically hook into pi
|
|
332
|
-
*/
|
|
333
|
-
export function createTUIUsageMonitor(
|
|
334
|
-
quotaManager: QuotaManager,
|
|
335
|
-
pi: {
|
|
336
|
-
on: (event: string, handler: (event: { message: string }) => void) => void;
|
|
337
|
-
},
|
|
338
|
-
config?: { debug?: boolean },
|
|
339
|
-
): TUIUsageMonitor {
|
|
340
|
-
const monitor = new TUIUsageMonitor({
|
|
341
|
-
quotaManager,
|
|
342
|
-
debug: config?.debug,
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
// Hook into pi events
|
|
346
|
-
pi.on("error", (event: { message: string }) => {
|
|
347
|
-
monitor.processMessage(event.message);
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
pi.on("message", (event: { message: string }) => {
|
|
351
|
-
monitor.processMessage(event.message);
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
return monitor;
|
|
355
|
-
}
|