@oh-my-pi/pi-ai 16.5.0 → 16.5.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/CHANGELOG.md +37 -0
- package/dist/types/auth-broker/remote-store.d.ts +2 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +28 -0
- package/dist/types/auth-retry.d.ts +44 -20
- package/dist/types/auth-storage.d.ts +51 -10
- package/dist/types/error/flags.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/providers/openai-chat-server-schema.d.ts +3 -3
- package/dist/types/providers/openai-shared.d.ts +2 -0
- package/dist/types/registry/oauth/types.d.ts +8 -0
- package/dist/types/usage/cursor.d.ts +3 -0
- package/dist/types/usage/openai-codex-reset.d.ts +1 -1
- package/dist/types/usage/openai-codex.d.ts +8 -1
- package/dist/types/usage/zai.d.ts +2 -1
- package/dist/types/usage.d.ts +4 -0
- package/dist/types/utils/idle-iterator.d.ts +6 -6
- package/package.json +4 -4
- package/src/auth-broker/discover.ts +22 -3
- package/src/auth-broker/remote-store.ts +103 -10
- package/src/auth-broker/wire-schemas.ts +4 -0
- package/src/auth-gateway/server.ts +3 -1
- package/src/auth-retry.ts +191 -53
- package/src/auth-storage.ts +668 -198
- package/src/error/auth-classify.ts +2 -1
- package/src/error/flags.ts +10 -0
- package/src/error/provider.ts +2 -2
- package/src/index.ts +1 -0
- package/src/providers/cursor.ts +10 -1
- package/src/providers/openai-chat-server-schema.ts +2 -1
- package/src/providers/openai-codex-responses.ts +74 -36
- package/src/providers/openai-shared.ts +6 -3
- package/src/registry/oauth/anthropic.ts +57 -24
- package/src/registry/oauth/oauth.html +1 -1
- package/src/registry/oauth/types.ts +8 -0
- package/src/stream.ts +19 -30
- package/src/usage/cursor.ts +192 -0
- package/src/usage/openai-codex-reset.ts +6 -2
- package/src/usage/openai-codex.ts +33 -0
- package/src/usage/zai.ts +49 -6
- package/src/usage.ts +4 -0
- package/src/utils/idle-iterator.ts +6 -6
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
UsageAmount,
|
|
3
|
+
UsageFetchContext,
|
|
4
|
+
UsageFetchParams,
|
|
5
|
+
UsageLimit,
|
|
6
|
+
UsageProvider,
|
|
7
|
+
UsageReport,
|
|
8
|
+
UsageStatus,
|
|
9
|
+
UsageWindow,
|
|
10
|
+
} from "../usage";
|
|
11
|
+
import { toNumber } from "./shared";
|
|
12
|
+
|
|
13
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
14
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseTimestamp(value: unknown): number | undefined {
|
|
18
|
+
const numeric = toNumber(value);
|
|
19
|
+
if (numeric !== undefined) return numeric < 1_000_000_000_000 ? numeric * 1000 : numeric;
|
|
20
|
+
if (typeof value !== "string" || !value.trim()) return undefined;
|
|
21
|
+
const parsed = Date.parse(value);
|
|
22
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeCursorBaseUrl(baseUrl?: string): string {
|
|
26
|
+
if (!baseUrl) return "https://api2.cursor.sh";
|
|
27
|
+
return baseUrl.replace(/\/+$/, "");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function deriveResetsAt(payload: Record<string, unknown>): number | undefined {
|
|
31
|
+
const endKeys = ["billingCycleEnd", "endOfMonth", "resetsAt", "nextReset"];
|
|
32
|
+
for (const key of endKeys) {
|
|
33
|
+
const parsed = parseTimestamp(payload[key]);
|
|
34
|
+
if (parsed !== undefined) return parsed;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const startKeys = ["startOfMonth", "billingCycleStart", "startOfBillingCycle"];
|
|
38
|
+
for (const key of startKeys) {
|
|
39
|
+
const parsed = parseTimestamp(payload[key]);
|
|
40
|
+
if (parsed !== undefined) {
|
|
41
|
+
const date = new Date(parsed);
|
|
42
|
+
date.setUTCMonth(date.getUTCMonth() + 1);
|
|
43
|
+
return date.getTime();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parseCursorUsage(payload: unknown, fetchedAt = Date.now()): UsageReport | null {
|
|
50
|
+
if (!isRecord(payload)) return null;
|
|
51
|
+
const limits: UsageLimit[] = [];
|
|
52
|
+
const resetsAt = deriveResetsAt(payload);
|
|
53
|
+
|
|
54
|
+
const window: UsageWindow = {
|
|
55
|
+
id: "monthly",
|
|
56
|
+
label: "Monthly",
|
|
57
|
+
...(resetsAt !== undefined ? { resetsAt } : {}),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
61
|
+
if (!isRecord(value)) continue;
|
|
62
|
+
|
|
63
|
+
// used can be: numRequests, used, amountUsed, usdUsed
|
|
64
|
+
const usedVal =
|
|
65
|
+
toNumber(value.numRequests) ?? toNumber(value.used) ?? toNumber(value.amountUsed) ?? toNumber(value.usdUsed);
|
|
66
|
+
|
|
67
|
+
// limit can be: maxRequestUsage, limit, amountLimit, usdLimit
|
|
68
|
+
const limitVal =
|
|
69
|
+
toNumber(value.maxRequestUsage) ??
|
|
70
|
+
toNumber(value.limit) ??
|
|
71
|
+
toNumber(value.amountLimit) ??
|
|
72
|
+
toNumber(value.usdLimit);
|
|
73
|
+
|
|
74
|
+
if (usedVal !== undefined && limitVal !== undefined) {
|
|
75
|
+
const isUsd =
|
|
76
|
+
key === "planUsage" ||
|
|
77
|
+
key.toLowerCase().includes("usd") ||
|
|
78
|
+
key.toLowerCase().includes("billing") ||
|
|
79
|
+
key.toLowerCase().includes("stripe");
|
|
80
|
+
|
|
81
|
+
const unit = isUsd ? "usd" : "requests";
|
|
82
|
+
const cleanBucket = key.toLowerCase().trim();
|
|
83
|
+
const limitId = isUsd ? `cursor:usd:${cleanBucket}` : `cursor:requests:${cleanBucket}`;
|
|
84
|
+
|
|
85
|
+
const label = isUsd ? `${key} spend` : `${key} requests`;
|
|
86
|
+
|
|
87
|
+
const amount: UsageAmount = {
|
|
88
|
+
used: usedVal,
|
|
89
|
+
limit: limitVal,
|
|
90
|
+
remaining: Math.max(0, limitVal - usedVal),
|
|
91
|
+
usedFraction: limitVal > 0 ? usedVal / limitVal : 0,
|
|
92
|
+
remainingFraction: limitVal > 0 ? Math.max(0, limitVal - usedVal) / limitVal : 0,
|
|
93
|
+
unit,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const usedFraction = amount.usedFraction;
|
|
97
|
+
let status: UsageStatus = "unknown";
|
|
98
|
+
if (usedFraction !== undefined) {
|
|
99
|
+
if (usedFraction >= 1) {
|
|
100
|
+
status = "exhausted";
|
|
101
|
+
} else if (usedFraction >= 0.9) {
|
|
102
|
+
status = "warning";
|
|
103
|
+
} else {
|
|
104
|
+
status = "ok";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
limits.push({
|
|
109
|
+
id: limitId,
|
|
110
|
+
label,
|
|
111
|
+
scope: {
|
|
112
|
+
provider: "cursor",
|
|
113
|
+
...(window ? { windowId: window.id } : {}),
|
|
114
|
+
},
|
|
115
|
+
...(window ? { window } : {}),
|
|
116
|
+
amount,
|
|
117
|
+
status,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (limits.length === 0) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
provider: "cursor",
|
|
128
|
+
fetchedAt,
|
|
129
|
+
limits,
|
|
130
|
+
raw: payload,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const cursorUsageProvider: UsageProvider = {
|
|
135
|
+
id: "cursor",
|
|
136
|
+
supports(params: UsageFetchParams): boolean {
|
|
137
|
+
if (params.provider !== "cursor") return false;
|
|
138
|
+
const { credential } = params;
|
|
139
|
+
if (credential.type === "oauth") {
|
|
140
|
+
return Boolean(credential.accessToken);
|
|
141
|
+
}
|
|
142
|
+
if (credential.type === "api_key") {
|
|
143
|
+
return Boolean(credential.apiKey);
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
},
|
|
147
|
+
async fetchUsage(params: UsageFetchParams, ctx: UsageFetchContext): Promise<UsageReport | null> {
|
|
148
|
+
if (params.provider !== "cursor") return null;
|
|
149
|
+
const { credential } = params;
|
|
150
|
+
const token = credential.type === "oauth" ? credential.accessToken : credential.apiKey;
|
|
151
|
+
if (!token) return null;
|
|
152
|
+
|
|
153
|
+
const baseUrl = normalizeCursorBaseUrl(params.baseUrl ?? credential.apiEndpoint);
|
|
154
|
+
const url = `${baseUrl}/auth/usage`;
|
|
155
|
+
|
|
156
|
+
const headers: Record<string, string> = {
|
|
157
|
+
Accept: "application/json",
|
|
158
|
+
Authorization: `Bearer ${token}`,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const response = await ctx.fetch(url, {
|
|
163
|
+
headers,
|
|
164
|
+
signal: params.signal,
|
|
165
|
+
});
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
ctx.logger?.warn("Cursor usage request failed", {
|
|
168
|
+
status: response.status,
|
|
169
|
+
provider: params.provider,
|
|
170
|
+
});
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const payload = await response.json();
|
|
174
|
+
const report = parseCursorUsage(payload);
|
|
175
|
+
if (report) {
|
|
176
|
+
const metadata = {
|
|
177
|
+
...(credential.email ? { email: credential.email } : {}),
|
|
178
|
+
...(credential.accountId ? { accountId: credential.accountId } : {}),
|
|
179
|
+
...(credential.projectId ? { projectId: credential.projectId } : {}),
|
|
180
|
+
};
|
|
181
|
+
if (Object.keys(metadata).length > 0) report.metadata = metadata;
|
|
182
|
+
}
|
|
183
|
+
return report;
|
|
184
|
+
} catch (error) {
|
|
185
|
+
ctx.logger?.warn("Cursor usage request error", {
|
|
186
|
+
provider: params.provider,
|
|
187
|
+
error: String(error),
|
|
188
|
+
});
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
};
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* GET /wham/rate-limit-reset-credits → list redeemable credits
|
|
10
10
|
* POST /wham/rate-limit-reset-credits/consume → spend one credit
|
|
11
|
-
* body: { credit_id, redeem_request_id }
|
|
11
|
+
* body: { credit_id, redeem_request_id, account_id? }
|
|
12
12
|
*
|
|
13
13
|
* `redeem_request_id` is a client-generated idempotency key (UUID). The consume
|
|
14
14
|
* response carries a `code`: `"reset"` on success, otherwise a business reason
|
|
@@ -159,7 +159,11 @@ export async function consumeCodexResetCredit(
|
|
|
159
159
|
const response = await auth.fetch(url, {
|
|
160
160
|
method: "POST",
|
|
161
161
|
headers: buildHeaders(auth, true),
|
|
162
|
-
body: JSON.stringify({
|
|
162
|
+
body: JSON.stringify({
|
|
163
|
+
credit_id: auth.creditId,
|
|
164
|
+
redeem_request_id: redeemRequestId,
|
|
165
|
+
account_id: auth.accountId,
|
|
166
|
+
}),
|
|
163
167
|
signal: auth.signal,
|
|
164
168
|
});
|
|
165
169
|
let body: unknown;
|
|
@@ -344,11 +344,44 @@ function buildAdditionalUsageLimit(args: {
|
|
|
344
344
|
};
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Parse Codex `x-codex-{primary,secondary}-*` rate-limit response headers into
|
|
349
|
+
* a usage report. The backend attaches these snapshots to every response, so
|
|
350
|
+
* ingesting them lets credential selection block an exhausted account before
|
|
351
|
+
* the next request burns a wire 429.
|
|
352
|
+
*/
|
|
353
|
+
export function parseCodexRateLimitHeaders(headers: Record<string, string>, now = Date.now()): UsageReport | null {
|
|
354
|
+
const parseWindow = (key: "primary" | "secondary"): ParsedUsageWindow | undefined => {
|
|
355
|
+
const usedPercent = toNumber(headers[`x-codex-${key}-used-percent`]);
|
|
356
|
+
if (usedPercent === undefined) return undefined;
|
|
357
|
+
const windowMinutes = toNumber(headers[`x-codex-${key}-window-minutes`]);
|
|
358
|
+
const resetAt = toNumber(headers[`x-codex-${key}-reset-at`]);
|
|
359
|
+
return {
|
|
360
|
+
usedPercent,
|
|
361
|
+
limitWindowSeconds: windowMinutes === undefined ? undefined : windowMinutes * 60,
|
|
362
|
+
resetAt,
|
|
363
|
+
};
|
|
364
|
+
};
|
|
365
|
+
const primary = parseWindow("primary");
|
|
366
|
+
const secondary = parseWindow("secondary");
|
|
367
|
+
if (!primary && !secondary) return null;
|
|
368
|
+
const limits: UsageLimit[] = [];
|
|
369
|
+
if (primary) limits.push(buildUsageLimit({ key: "primary", window: primary, nowMs: now }));
|
|
370
|
+
if (secondary) limits.push(buildUsageLimit({ key: "secondary", window: secondary, nowMs: now }));
|
|
371
|
+
return {
|
|
372
|
+
provider: "openai-codex",
|
|
373
|
+
fetchedAt: now,
|
|
374
|
+
limits,
|
|
375
|
+
metadata: { source: "ratelimit-headers" },
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
347
379
|
export const openaiCodexUsageProvider: UsageProvider = {
|
|
348
380
|
id: "openai-codex",
|
|
349
381
|
supports(params: UsageFetchParams): boolean {
|
|
350
382
|
return params.provider === "openai-codex" && params.credential.type === "oauth";
|
|
351
383
|
},
|
|
384
|
+
parseRateLimitHeaders: parseCodexRateLimitHeaders,
|
|
352
385
|
async fetchUsage(params: UsageFetchParams, ctx: UsageFetchContext): Promise<UsageReport | null> {
|
|
353
386
|
if (params.provider !== "openai-codex") return null;
|
|
354
387
|
const { credential } = params;
|
package/src/usage/zai.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { toNumber } from "@oh-my-pi/pi-catalog/utils";
|
|
2
2
|
import type {
|
|
3
|
+
CredentialRankingStrategy,
|
|
3
4
|
UsageAmount,
|
|
4
5
|
UsageFetchContext,
|
|
5
6
|
UsageFetchParams,
|
|
@@ -175,12 +176,14 @@ function buildZaiWindow(parsed: ZaiUsageLimitItem): UsageWindow {
|
|
|
175
176
|
};
|
|
176
177
|
}
|
|
177
178
|
|
|
178
|
-
function
|
|
179
|
+
function isZaiFeatureRequestLimit(parsed: ZaiUsageLimitItem): boolean {
|
|
179
180
|
const detailCodes =
|
|
180
181
|
parsed.usageDetails?.map(detail => detail.modelCode).filter((code): code is string => !!code) ?? [];
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
return detailCodes.includes("search-prime") && detailCodes.includes("web-reader") && detailCodes.includes("zread");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function requestQuotaLabel(parsed: ZaiUsageLimitItem): string {
|
|
186
|
+
if (isZaiFeatureRequestLimit(parsed)) return "ZAI Web Search / Reader / Zread Quota";
|
|
184
187
|
return "ZAI Request Quota";
|
|
185
188
|
}
|
|
186
189
|
|
|
@@ -191,6 +194,29 @@ function buildModelUsageUrl(baseUrl: string, now: Date): string {
|
|
|
191
194
|
return `${baseUrl}${MODEL_USAGE_PATH}?startTime=${encodeURIComponent(startTime)}&endTime=${encodeURIComponent(endTime)}`;
|
|
192
195
|
}
|
|
193
196
|
|
|
197
|
+
function getZaiCredentialLimits(report: UsageReport): UsageLimit[] {
|
|
198
|
+
const limits = report.limits.filter(
|
|
199
|
+
limit => limit.id.startsWith("zai:requests:") || limit.id.startsWith("zai:tokens:"),
|
|
200
|
+
);
|
|
201
|
+
return limits;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function rankZaiRequestLimits(report: UsageReport): UsageLimit[] {
|
|
205
|
+
const requestLimits = report.limits.filter(limit => limit.id.startsWith("zai:requests:"));
|
|
206
|
+
const credentialLimits = getZaiCredentialLimits(report);
|
|
207
|
+
const limits = requestLimits.length > 0 ? requestLimits : credentialLimits;
|
|
208
|
+
const ranked = [...limits];
|
|
209
|
+
ranked.sort((left, right) => {
|
|
210
|
+
const leftDuration = left.window?.durationMs ?? Number.POSITIVE_INFINITY;
|
|
211
|
+
const rightDuration = right.window?.durationMs ?? Number.POSITIVE_INFINITY;
|
|
212
|
+
if (leftDuration !== rightDuration) return leftDuration - rightDuration;
|
|
213
|
+
const leftReset = left.window?.resetsAt ?? Number.POSITIVE_INFINITY;
|
|
214
|
+
const rightReset = right.window?.resetsAt ?? Number.POSITIVE_INFINITY;
|
|
215
|
+
return leftReset - rightReset;
|
|
216
|
+
});
|
|
217
|
+
return ranked;
|
|
218
|
+
}
|
|
219
|
+
|
|
194
220
|
async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext): Promise<UsageReport | null> {
|
|
195
221
|
if (params.provider !== "zai") return null;
|
|
196
222
|
const credential = params.credential;
|
|
@@ -263,13 +289,15 @@ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext):
|
|
|
263
289
|
percentage: parsed.percentage,
|
|
264
290
|
unit: "requests",
|
|
265
291
|
});
|
|
292
|
+
const featureLimit = isZaiFeatureRequestLimit(parsed);
|
|
266
293
|
limits.push({
|
|
267
|
-
id: `zai:requests:${window.id}`,
|
|
294
|
+
id: featureLimit ? `zai:features:web-search-reader-zread:${window.id}` : `zai:requests:${window.id}`,
|
|
268
295
|
label: requestQuotaLabel(parsed),
|
|
269
296
|
scope: {
|
|
270
297
|
provider: params.provider,
|
|
271
298
|
windowId: window.id,
|
|
272
|
-
shared:
|
|
299
|
+
shared: !featureLimit,
|
|
300
|
+
...(featureLimit ? { tier: "web-search-reader-zread" } : {}),
|
|
273
301
|
},
|
|
274
302
|
window,
|
|
275
303
|
amount,
|
|
@@ -319,3 +347,18 @@ export const zaiUsageProvider: UsageProvider = {
|
|
|
319
347
|
fetchUsage: fetchZaiUsage,
|
|
320
348
|
supports: params => params.provider === "zai" && params.credential.type === "api_key",
|
|
321
349
|
};
|
|
350
|
+
|
|
351
|
+
export const zaiRankingStrategy: CredentialRankingStrategy = {
|
|
352
|
+
findWindowLimits(report) {
|
|
353
|
+
const ranked = rankZaiRequestLimits(report);
|
|
354
|
+
return { primary: ranked[0], secondary: ranked[1] };
|
|
355
|
+
},
|
|
356
|
+
scopeLimits(report) {
|
|
357
|
+
const limits = getZaiCredentialLimits(report);
|
|
358
|
+
return limits;
|
|
359
|
+
},
|
|
360
|
+
windowDefaults: {
|
|
361
|
+
primaryMs: 5 * HOUR_MS,
|
|
362
|
+
secondaryMs: WEEK_MS,
|
|
363
|
+
},
|
|
364
|
+
};
|
package/src/usage.ts
CHANGED
|
@@ -260,6 +260,10 @@ export interface UsageCredential {
|
|
|
260
260
|
accountId?: string;
|
|
261
261
|
projectId?: string;
|
|
262
262
|
email?: string;
|
|
263
|
+
/** Organization/workspace the credential is scoped to (see OAuthCredentials.orgId). */
|
|
264
|
+
orgId?: string;
|
|
265
|
+
/** Human-readable organization name for display. */
|
|
266
|
+
orgName?: string;
|
|
263
267
|
enterpriseUrl?: string;
|
|
264
268
|
metadata?: Record<string, unknown>;
|
|
265
269
|
apiEndpoint?: string;
|
|
@@ -98,12 +98,12 @@ export function getOpenAIStreamFirstEventTimeoutMs(
|
|
|
98
98
|
* pre-response request (issue #2422 regression: large `write` tool-call streams
|
|
99
99
|
* died at the budget with `TimeoutError: The operation timed out.` despite
|
|
100
100
|
* deltas actively flowing). This arms a `clearTimeout`-able timer instead;
|
|
101
|
-
* callers MUST `clear()` as soon as
|
|
102
|
-
* the body stream is left to the iterator-level idle watchdog.
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
101
|
+
* callers MUST `clear()` as soon as the guarded transport attempt settles so
|
|
102
|
+
* the body stream is left to the iterator-level idle watchdog.
|
|
103
|
+
*
|
|
104
|
+
* Retrying callers MUST arm a fresh guard for each transport attempt and keep
|
|
105
|
+
* the retry loop's base signal reserved for caller cancellation. Reusing the
|
|
106
|
+
* guard as the loop signal makes its timeout indistinguishable from cancellation.
|
|
107
107
|
*
|
|
108
108
|
* Returns the caller signal unchanged (and a no-op `clear`) when no positive
|
|
109
109
|
* timeout is configured.
|