@oh-my-pi/pi-ai 18.2.8 → 18.2.9

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 (45) hide show
  1. package/CHANGELOG.md +33 -18
  2. package/dist/types/auth-gateway/types.d.ts +5 -0
  3. package/dist/types/auth-storage.d.ts +35 -32
  4. package/dist/types/index.d.ts +1 -0
  5. package/dist/types/providers/amazon-bedrock.d.ts +7 -0
  6. package/dist/types/providers/claude-code-fingerprint.d.ts +22 -3
  7. package/dist/types/providers/google-gemini-cli.d.ts +0 -2
  8. package/dist/types/providers/openai-chat-server-schema.d.ts +2 -2
  9. package/dist/types/usage/claude-api.d.ts +22 -0
  10. package/dist/types/usage/claude-reset.d.ts +44 -0
  11. package/dist/types/usage.d.ts +111 -5
  12. package/dist/types/utils/schema/json-schema-validator.d.ts +5 -2
  13. package/dist/types/utils/tool-call-loop-guard.d.ts +1 -1
  14. package/package.json +6 -6
  15. package/src/auth/sqlite-credential-store.ts +44 -1
  16. package/src/auth-broker/remote-store.ts +6 -6
  17. package/src/auth-broker/wire-schemas.ts +14 -0
  18. package/src/auth-gateway/server.ts +4 -0
  19. package/src/auth-gateway/types.ts +5 -0
  20. package/src/auth-storage.ts +263 -140
  21. package/src/error/flags.ts +10 -0
  22. package/src/index.ts +1 -0
  23. package/src/providers/amazon-bedrock.ts +55 -5
  24. package/src/providers/anthropic.ts +45 -11
  25. package/src/providers/aws-credentials.ts +124 -11
  26. package/src/providers/claude-code-fingerprint.ts +55 -3
  27. package/src/providers/gitlab-duo.ts +20 -4
  28. package/src/providers/google-gemini-cli.ts +0 -8
  29. package/src/providers/google-shared.ts +1 -18
  30. package/src/providers/openai-chat-server-schema.ts +1 -1
  31. package/src/providers/openai-chat-server.ts +3 -1
  32. package/src/providers/openai-codex-responses.ts +27 -5
  33. package/src/providers/openai-completions.ts +122 -19
  34. package/src/providers/pi-native-server.ts +1 -0
  35. package/src/registry/oauth/anthropic.ts +2 -3
  36. package/src/stream.ts +13 -4
  37. package/src/usage/alibaba-token-plan.ts +7 -1
  38. package/src/usage/claude-api.ts +66 -0
  39. package/src/usage/claude-reset.ts +638 -0
  40. package/src/usage/claude.ts +37 -59
  41. package/src/usage/kimi.ts +32 -1
  42. package/src/usage.ts +52 -5
  43. package/src/utils/schema/json-schema-validator.ts +23 -10
  44. package/src/utils/tool-call-loop-guard.ts +2 -2
  45. package/src/utils/validation.ts +145 -50
@@ -0,0 +1,638 @@
1
+ import type { UsageResetCredit, UsageResetCredits } from "../usage";
2
+ import type { FetchImpl } from "../types";
3
+ import { isRecord } from "../utils";
4
+ import {
5
+ buildClaudeOAuthHeaders,
6
+ claudeApiUrl,
7
+ claudeOAuthBaseUrl,
8
+ claudeOAuthBaseUrls,
9
+ DEFAULT_CLAUDE_OAUTH_BASE_URL,
10
+ } from "./claude-api";
11
+
12
+ const CEDAR_PROGRAM = "cedar_ember";
13
+ const JUNIPER_PROGRAM = "juniper_tide";
14
+ const ENDPOINT_ABSENT_STATUSES: Record<number, true> = { 404: true, 405: true, 410: true, 501: true };
15
+ const CEDAR_GRANT_ID = /^[a-z0-9_-]{1,40}$/;
16
+ const REDEEM_REQUEST_ID = /^[A-Za-z0-9_-]{1,64}$/;
17
+ const ORGANIZATION_ID = /^[A-Za-z0-9_-]{1,128}$/;
18
+ const WINDOW_ID = /^[a-z0-9_-]{1,80}$/;
19
+
20
+ const CLAUDE_RESET_WINDOW_IDS: Readonly<Record<string, string>> = {
21
+ five_hour: "anthropic:5h",
22
+ seven_day: "anthropic:7d",
23
+ seven_day_opus: "anthropic:7d:opus",
24
+ seven_day_sonnet: "anthropic:7d:sonnet",
25
+ };
26
+
27
+ /** OAuth credential and transport used for an account's Claude reset operations. */
28
+ export interface ClaudeResetAuth {
29
+ accessToken: string;
30
+ orgId?: string;
31
+ baseUrl?: string;
32
+ fetch: FetchImpl;
33
+ signal?: AbortSignal;
34
+ }
35
+
36
+ /** Live reset eligibility plus the exact organization and host used for redemption. */
37
+ export interface ClaudeResetCreditList extends UsageResetCredits {
38
+ credits: UsageResetCredit[];
39
+ orgId?: string;
40
+ /** OAuth API base that answered discovery; pass back as consume `baseUrl`. */
41
+ baseUrl?: string;
42
+ }
43
+
44
+ /** Confirmed reset outcome; transport uncertainty never reports a successful spend. */
45
+ export interface ClaudeResetConsumeResult {
46
+ ok: boolean;
47
+ code: string;
48
+ status: number;
49
+ cleared?: string[];
50
+ reason?: string;
51
+ raw?: unknown;
52
+ }
53
+
54
+ interface CedarGrant {
55
+ id: string;
56
+ label?: string;
57
+ remainingCount: number;
58
+ grantedAt?: string;
59
+ expiresAt?: string;
60
+ clears: string[];
61
+ paused: boolean;
62
+ usableNow: boolean;
63
+ requiresLimit: boolean;
64
+ usedFractions: Record<string, number>;
65
+ blocking: string[];
66
+ }
67
+
68
+ interface CedarStatus {
69
+ eligible: boolean;
70
+ reason?: string;
71
+ grants: CedarGrant[];
72
+ nextGrantId?: string;
73
+ cooldownUntil?: string;
74
+ }
75
+
76
+ interface JuniperStatus {
77
+ eligible: boolean;
78
+ reason?: string;
79
+ arm?: "control" | "reset";
80
+ available: boolean;
81
+ nextAvailableAt?: string;
82
+ weeklyResetsAt?: string;
83
+ }
84
+
85
+ type ParsedStatus<T> = { ok: true; status: T | null } | { ok: false };
86
+ type Discovery<T> = { kind: "answered"; status: T | null; baseUrl: string } | { kind: "failed" };
87
+
88
+ function optionalString(value: unknown): string | undefined | null {
89
+ if (value === undefined || value === null) return undefined;
90
+ if (typeof value !== "string" || !value.trim()) return null;
91
+ return value.trim();
92
+ }
93
+
94
+ function optionalIsoTimestamp(value: unknown): string | undefined | null {
95
+ const stringValue = optionalString(value);
96
+ if (stringValue === null || stringValue === undefined) return stringValue;
97
+ const timestamp = Date.parse(stringValue);
98
+ return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
99
+ }
100
+
101
+ function normalizeWindowId(value: unknown): string | null {
102
+ if (typeof value !== "string" || !WINDOW_ID.test(value)) return null;
103
+ return CLAUDE_RESET_WINDOW_IDS[value] ?? `anthropic:reset:${value}`;
104
+ }
105
+
106
+ function normalizeWindowList(value: unknown): string[] | null {
107
+ if (!Array.isArray(value)) return null;
108
+ const normalized: string[] = [];
109
+ for (const item of value) {
110
+ const id = normalizeWindowId(item);
111
+ if (!id) return null;
112
+ if (!normalized.includes(id)) normalized.push(id);
113
+ }
114
+ return normalized;
115
+ }
116
+
117
+ function normalizeUsedFractions(value: unknown): Record<string, number> | null {
118
+ if (value === undefined) return {};
119
+ if (!isRecord(value)) return null;
120
+ const normalized: Record<string, number> = {};
121
+ for (const rawId in value) {
122
+ const percent = value[rawId];
123
+ const id = normalizeWindowId(rawId);
124
+ if (!id || typeof percent !== "number" || !Number.isInteger(percent) || percent < 0 || percent > 100) {
125
+ return null;
126
+ }
127
+ normalized[id] = percent / 100;
128
+ }
129
+ return normalized;
130
+ }
131
+
132
+ function parseCedarGrant(value: unknown): CedarGrant | null {
133
+ if (!isRecord(value) || typeof value.id !== "string" || !CEDAR_GRANT_ID.test(value.id)) return null;
134
+ if (typeof value.resets_left !== "number" || !Number.isInteger(value.resets_left) || value.resets_left < 0) {
135
+ return null;
136
+ }
137
+ if (
138
+ value.resets_total !== undefined &&
139
+ (typeof value.resets_total !== "number" || !Number.isInteger(value.resets_total) || value.resets_total < 0)
140
+ ) {
141
+ return null;
142
+ }
143
+ const label =
144
+ value.label === undefined || value.label === null
145
+ ? undefined
146
+ : typeof value.label === "string"
147
+ ? value.label.trim()
148
+ : null;
149
+ const grantedAt = optionalIsoTimestamp(value.starts_at);
150
+ const expiresAt = optionalIsoTimestamp(value.ends_at);
151
+ const clears = normalizeWindowList(value.clears);
152
+ const blocking = normalizeWindowList(value.blocking);
153
+ const usedFractions = normalizeUsedFractions(value.percent_used);
154
+ if (label === null || grantedAt === null || expiresAt === null || !clears || !blocking || !usedFractions)
155
+ return null;
156
+ if (value.paused !== undefined && typeof value.paused !== "boolean") return null;
157
+ if (value.usable_now !== undefined && typeof value.usable_now !== "boolean") return null;
158
+ if (value.use_requires_limit !== undefined && typeof value.use_requires_limit !== "boolean") return null;
159
+ return {
160
+ id: value.id,
161
+ ...(label ? { label } : {}),
162
+ remainingCount: value.resets_left,
163
+ ...(grantedAt ? { grantedAt } : {}),
164
+ ...(expiresAt ? { expiresAt } : {}),
165
+ clears,
166
+ paused: value.paused ?? false,
167
+ usableNow: value.usable_now ?? false,
168
+ requiresLimit: value.use_requires_limit ?? true,
169
+ usedFractions,
170
+ blocking,
171
+ };
172
+ }
173
+
174
+ function parseCedarStatus(value: unknown): ParsedStatus<CedarStatus> {
175
+ if (value === undefined || value === null) return { ok: true, status: null };
176
+ if (!isRecord(value) || typeof value.eligible !== "boolean") return { ok: false };
177
+ const reason = optionalString(value.ineligible_reason);
178
+ const cooldownUntil = optionalIsoTimestamp(value.cooldown_until);
179
+ if (reason === null || cooldownUntil === null) return { ok: false };
180
+ if (value.at_limit !== undefined && typeof value.at_limit !== "boolean") return { ok: false };
181
+ if (value.exhausted !== undefined && !normalizeWindowList(value.exhausted)) return { ok: false };
182
+ if (value.grants !== undefined && !Array.isArray(value.grants)) return { ok: false };
183
+ const grants: CedarGrant[] = [];
184
+ for (const rawGrant of value.grants ?? []) {
185
+ const grant = parseCedarGrant(rawGrant);
186
+ if (!grant) return { ok: false };
187
+ grants.push(grant);
188
+ }
189
+ const rawNextGrantId = value.next_grant_id;
190
+ if (
191
+ rawNextGrantId !== undefined &&
192
+ rawNextGrantId !== null &&
193
+ (typeof rawNextGrantId !== "string" || !CEDAR_GRANT_ID.test(rawNextGrantId))
194
+ ) {
195
+ return { ok: false };
196
+ }
197
+ const nextGrantId =
198
+ typeof rawNextGrantId === "string" && grants.some(grant => grant.id === rawNextGrantId)
199
+ ? rawNextGrantId
200
+ : undefined;
201
+ return {
202
+ ok: true,
203
+ status: {
204
+ eligible: value.eligible,
205
+ ...(reason ? { reason } : {}),
206
+ grants,
207
+ ...(nextGrantId ? { nextGrantId } : {}),
208
+ ...(cooldownUntil ? { cooldownUntil } : {}),
209
+ },
210
+ };
211
+ }
212
+
213
+ function parseJuniperStatus(value: unknown): ParsedStatus<JuniperStatus> {
214
+ if (value === undefined || value === null) return { ok: true, status: null };
215
+ if (!isRecord(value) || typeof value.eligible !== "boolean") return { ok: false };
216
+ const reason = optionalString(value.ineligible_reason);
217
+ const nextAvailableAt = optionalIsoTimestamp(value.next_available_at);
218
+ const weeklyResetsAt = optionalIsoTimestamp(value.weekly_resets_at);
219
+ if (reason === null || nextAvailableAt === null || weeklyResetsAt === null) return { ok: false };
220
+ if (value.available !== undefined && typeof value.available !== "boolean") return { ok: false };
221
+ if (value.arm !== undefined && value.arm !== null && value.arm !== "control" && value.arm !== "reset") {
222
+ return { ok: false };
223
+ }
224
+ if (
225
+ value.resets_per_week !== undefined &&
226
+ (typeof value.resets_per_week !== "number" ||
227
+ !Number.isInteger(value.resets_per_week) ||
228
+ value.resets_per_week < 0)
229
+ ) {
230
+ return { ok: false };
231
+ }
232
+ return {
233
+ ok: true,
234
+ status: {
235
+ eligible: value.eligible,
236
+ ...(reason ? { reason } : {}),
237
+ ...(value.arm === "control" || value.arm === "reset" ? { arm: value.arm } : {}),
238
+ available: value.available ?? false,
239
+ ...(nextAvailableAt ? { nextAvailableAt } : {}),
240
+ ...(weeklyResetsAt ? { weeklyResetsAt } : {}),
241
+ },
242
+ };
243
+ }
244
+
245
+ function isUsageEnvelope(payload: Record<string, unknown>): boolean {
246
+ return (
247
+ "five_hour" in payload ||
248
+ "seven_day" in payload ||
249
+ "limits" in payload ||
250
+ "extra_usage" in payload ||
251
+ "spend" in payload
252
+ );
253
+ }
254
+
255
+ async function discoverStatus<T>(
256
+ auth: ClaudeResetAuth,
257
+ query: string,
258
+ field: string,
259
+ parse: (value: unknown) => ParsedStatus<T>,
260
+ ): Promise<Discovery<T>> {
261
+ const candidates = claudeOAuthBaseUrls(auth.baseUrl);
262
+ for (const baseUrl of candidates) {
263
+ if (auth.signal?.aborted) return { kind: "failed" };
264
+ let response: Response;
265
+ try {
266
+ response = await auth.fetch(`${baseUrl}/usage?${query}`, {
267
+ headers: buildClaudeOAuthHeaders(auth.accessToken),
268
+ signal: auth.signal ?? AbortSignal.timeout(5_000),
269
+ });
270
+ } catch {
271
+ return { kind: "failed" };
272
+ }
273
+ if (!response.ok) {
274
+ const canFallback =
275
+ ENDPOINT_ABSENT_STATUSES[response.status] === true && baseUrl !== DEFAULT_CLAUDE_OAUTH_BASE_URL;
276
+ if (canFallback) continue;
277
+ return { kind: "failed" };
278
+ }
279
+ let payload: unknown;
280
+ try {
281
+ payload = await response.json();
282
+ } catch {
283
+ return { kind: "failed" };
284
+ }
285
+ if (!isRecord(payload)) return { kind: "failed" };
286
+ if (field in payload) {
287
+ const parsed = parse(payload[field]);
288
+ return parsed.ok ? { kind: "answered", status: parsed.status, baseUrl } : { kind: "failed" };
289
+ }
290
+ if (isUsageEnvelope(payload)) return { kind: "answered", status: null, baseUrl };
291
+ const canFallback = baseUrl !== DEFAULT_CLAUDE_OAUTH_BASE_URL;
292
+ if (!canFallback) return { kind: "failed" };
293
+ }
294
+ return { kind: "failed" };
295
+ }
296
+
297
+ function normalizeCedarList(status: CedarStatus, orgId?: string, baseUrl?: string): ClaudeResetCreditList {
298
+ const now = Date.now();
299
+ const cooldownActive = status.cooldownUntil !== undefined && Date.parse(status.cooldownUntil) > now;
300
+ let availableCount = 0;
301
+ let selectedCredit: UsageResetCredit | undefined;
302
+ const credits = status.grants.map<UsageResetCredit>(grant => {
303
+ const expired = grant.expiresAt !== undefined && Date.parse(grant.expiresAt) <= now;
304
+ const selected = grant.id === status.nextGrantId;
305
+ const usable =
306
+ status.eligible &&
307
+ selected &&
308
+ grant.usableNow &&
309
+ !grant.paused &&
310
+ !expired &&
311
+ !cooldownActive &&
312
+ grant.remainingCount > 0;
313
+ const creditStatus = expired
314
+ ? "expired"
315
+ : grant.remainingCount === 0
316
+ ? "redeemed"
317
+ : grant.paused
318
+ ? "paused"
319
+ : usable
320
+ ? "available"
321
+ : "unavailable";
322
+ if (!expired && grant.remainingCount > 0) availableCount += grant.remainingCount;
323
+ const credit: UsageResetCredit = {
324
+ id: grant.id,
325
+ title: grant.label || "Claude limit reset",
326
+ program: CEDAR_PROGRAM,
327
+ remainingCount: grant.remainingCount,
328
+ usable,
329
+ requiresLimit: grant.requiresLimit,
330
+ clears: grant.clears,
331
+ blocking: grant.blocking,
332
+ usedFractions: grant.usedFractions,
333
+ ...(grant.grantedAt ? { grantedAt: grant.grantedAt } : {}),
334
+ ...(grant.expiresAt ? { expiresAt: grant.expiresAt } : {}),
335
+ status: creditStatus,
336
+ };
337
+ if (selected) selectedCredit = credit;
338
+ return credit;
339
+ });
340
+ return {
341
+ availableCount,
342
+ redeemableCount: selectedCredit?.usable ? (selectedCredit.remainingCount ?? 0) : 0,
343
+ ...(status.nextGrantId && status.eligible ? { nextCreditId: status.nextGrantId } : {}),
344
+ eligible: status.eligible,
345
+ ...(status.reason ? { reason: status.reason } : cooldownActive ? { reason: "cooldown" } : {}),
346
+ ...(status.cooldownUntil ? { cooldownUntil: status.cooldownUntil } : {}),
347
+ credits,
348
+ ...(orgId ? { orgId } : {}),
349
+ ...(baseUrl ? { baseUrl } : {}),
350
+ };
351
+ }
352
+
353
+ function normalizeJuniperList(status: JuniperStatus, orgId?: string, baseUrl?: string): ClaudeResetCreditList {
354
+ const eligible = status.eligible && status.arm === "reset";
355
+ const usable = eligible && status.available;
356
+ const offered = status.arm === "reset";
357
+ const reason =
358
+ status.reason ??
359
+ (!status.eligible
360
+ ? "ineligible"
361
+ : status.arm !== "reset"
362
+ ? "control"
363
+ : !status.available
364
+ ? "unavailable"
365
+ : undefined);
366
+ const credits: UsageResetCredit[] = offered
367
+ ? [
368
+ {
369
+ id: JUNIPER_PROGRAM,
370
+ title: "Claude session limit reset",
371
+ program: JUNIPER_PROGRAM,
372
+ remainingCount: usable ? 1 : 0,
373
+ usable,
374
+ requiresLimit: true,
375
+ clears: ["anthropic:5h"],
376
+ blocking: [],
377
+ usedFractions: {},
378
+ ...(status.weeklyResetsAt ? { expiresAt: status.weeklyResetsAt } : {}),
379
+ status: usable ? "available" : "unavailable",
380
+ },
381
+ ]
382
+ : [];
383
+ return {
384
+ availableCount: usable ? 1 : 0,
385
+ redeemableCount: usable ? 1 : 0,
386
+ ...(eligible ? { nextCreditId: JUNIPER_PROGRAM } : {}),
387
+ eligible,
388
+ ...(reason ? { reason } : {}),
389
+ ...(!usable && status.nextAvailableAt ? { cooldownUntil: status.nextAvailableAt } : {}),
390
+ credits,
391
+ ...(orgId ? { orgId } : {}),
392
+ ...(baseUrl ? { baseUrl } : {}),
393
+ };
394
+ }
395
+
396
+ interface OrganizationResolution {
397
+ orgId?: string;
398
+ status: number;
399
+ code?: "auth_error" | "organization_unavailable";
400
+ raw?: unknown;
401
+ }
402
+
403
+ async function resolveOrganization(auth: ClaudeResetAuth): Promise<OrganizationResolution> {
404
+ const configured = auth.orgId?.trim();
405
+ if (configured && ORGANIZATION_ID.test(configured)) return { orgId: configured, status: 0 };
406
+ if (configured) return { status: 0, code: "organization_unavailable" };
407
+ let response: Response;
408
+ try {
409
+ response = await auth.fetch(`${claudeOAuthBaseUrl(auth.baseUrl)}/profile`, {
410
+ headers: buildClaudeOAuthHeaders(auth.accessToken),
411
+ signal: auth.signal ?? AbortSignal.timeout(5_000),
412
+ });
413
+ } catch {
414
+ return { status: 0, code: "organization_unavailable" };
415
+ }
416
+ let payload: unknown;
417
+ try {
418
+ payload = await response.json();
419
+ } catch {
420
+ payload = undefined;
421
+ }
422
+ if (!response.ok) {
423
+ return {
424
+ status: response.status,
425
+ code: response.status === 401 || response.status === 403 ? "auth_error" : "organization_unavailable",
426
+ ...(payload !== undefined ? { raw: payload } : {}),
427
+ };
428
+ }
429
+ if (!isRecord(payload)) return { status: response.status, code: "organization_unavailable", raw: payload };
430
+ const organization = isRecord(payload.organization) ? payload.organization : undefined;
431
+ const candidate =
432
+ (typeof organization?.uuid === "string" ? organization.uuid : undefined) ??
433
+ (typeof payload.organization_uuid === "string" ? payload.organization_uuid : undefined);
434
+ const orgId = candidate?.trim();
435
+ if (!orgId || !ORGANIZATION_ID.test(orgId)) {
436
+ return { status: response.status, code: "organization_unavailable", raw: payload };
437
+ }
438
+ return { orgId, status: response.status };
439
+ }
440
+
441
+ async function withResolvedOrganization(
442
+ list: ClaudeResetCreditList,
443
+ auth: ClaudeResetAuth,
444
+ ): Promise<ClaudeResetCreditList> {
445
+ if (list.orgId || list.credits.length === 0) return list;
446
+ const resolution = await resolveOrganization({ ...auth, baseUrl: list.baseUrl ?? auth.baseUrl });
447
+ return resolution.orgId ? { ...list, orgId: resolution.orgId } : list;
448
+ }
449
+
450
+ /**
451
+ * Reuse program blocks when a normal usage response actually includes them.
452
+ * A lone non-redeemable Cedar block is inconclusive because Juniper requires
453
+ * the separate at-wall read, so callers must still perform discovery.
454
+ *
455
+ * @internal
456
+ */
457
+ export function parseClaudeResetCreditsFromUsagePayload(
458
+ payload: unknown,
459
+ orgId?: string,
460
+ baseUrl?: string,
461
+ ): ClaudeResetCreditList | null {
462
+ if (!isRecord(payload) || !Object.hasOwn(payload, CEDAR_PROGRAM)) return null;
463
+ const parsedCedar = parseCedarStatus(payload[CEDAR_PROGRAM]);
464
+ if (!parsedCedar.ok) return null;
465
+ const normalizedOrgId = orgId?.trim();
466
+ const safeOrgId = normalizedOrgId && ORGANIZATION_ID.test(normalizedOrgId) ? normalizedOrgId : undefined;
467
+ const cedarList = parsedCedar.status ? normalizeCedarList(parsedCedar.status, safeOrgId, baseUrl) : undefined;
468
+ if (cedarList && (cedarList.availableCount > 0 || cedarList.nextCreditId)) return cedarList;
469
+ if (!Object.hasOwn(payload, JUNIPER_PROGRAM)) return null;
470
+ const parsedJuniper = parseJuniperStatus(payload[JUNIPER_PROGRAM]);
471
+ if (!parsedJuniper.ok) return null;
472
+ if (parsedJuniper.status) return normalizeJuniperList(parsedJuniper.status, safeOrgId, baseUrl);
473
+ if (cedarList) return cedarList;
474
+ return {
475
+ availableCount: 0,
476
+ redeemableCount: 0,
477
+ eligible: false,
478
+ credits: [],
479
+ ...(safeOrgId ? { orgId: safeOrgId } : {}),
480
+ ...(baseUrl ? { baseUrl } : {}),
481
+ };
482
+ }
483
+
484
+ /**
485
+ * Discover Claude's saved Cedar grant first, then its Juniper session reset.
486
+ * `null` means discovery failed; a zero-count object is an authoritative answer.
487
+ */
488
+ export async function listClaudeResetCredits(auth: ClaudeResetAuth): Promise<ClaudeResetCreditList | null> {
489
+ const explicitOrgId = auth.orgId?.trim();
490
+ const orgId = explicitOrgId && ORGANIZATION_ID.test(explicitOrgId) ? explicitOrgId : undefined;
491
+ const cedar = await discoverStatus(auth, "cedar_ember=1&skip_spend=1", CEDAR_PROGRAM, parseCedarStatus);
492
+ if (cedar.kind === "failed") return null;
493
+ let cedarList: ClaudeResetCreditList | undefined;
494
+ if (cedar.status) {
495
+ cedarList = normalizeCedarList(cedar.status, orgId, cedar.baseUrl);
496
+ if (cedarList.availableCount > 0 || cedarList.nextCreditId) {
497
+ return withResolvedOrganization(cedarList, auth);
498
+ }
499
+ }
500
+
501
+ const juniper = await discoverStatus(auth, "at_wall=1&skip_spend=1", JUNIPER_PROGRAM, parseJuniperStatus);
502
+ if (juniper.kind === "answered" && juniper.status) {
503
+ return withResolvedOrganization(normalizeJuniperList(juniper.status, orgId, juniper.baseUrl), auth);
504
+ }
505
+ if (cedarList && juniper.kind === "answered") return cedarList;
506
+ if (juniper.kind === "answered") {
507
+ return {
508
+ availableCount: 0,
509
+ redeemableCount: 0,
510
+ eligible: false,
511
+ credits: [],
512
+ ...(orgId ? { orgId } : {}),
513
+ baseUrl: juniper.baseUrl,
514
+ };
515
+ }
516
+ return null;
517
+ }
518
+
519
+ function normalizeConsumeCode(result: string): string {
520
+ if (result === "already_used") return "already_redeemed";
521
+ if (result === "not_limited") return "nothing_to_reset";
522
+ return result;
523
+ }
524
+
525
+ /** Spend exactly the server-selected Claude reset. Mutation requests are never retried. */
526
+ export async function consumeClaudeResetCredit(
527
+ auth: ClaudeResetAuth & { credit: UsageResetCredit; redeemRequestId?: string },
528
+ ): Promise<ClaudeResetConsumeResult> {
529
+ const program = auth.credit.program;
530
+ if (program !== CEDAR_PROGRAM && program !== JUNIPER_PROGRAM) {
531
+ return { ok: false, code: "unknown_program", status: 0 };
532
+ }
533
+ const expiresAtMs = auth.credit.expiresAt === undefined ? undefined : Date.parse(auth.credit.expiresAt);
534
+ if (
535
+ auth.credit.usable !== true ||
536
+ typeof auth.credit.remainingCount !== "number" ||
537
+ !Number.isInteger(auth.credit.remainingCount) ||
538
+ auth.credit.remainingCount <= 0 ||
539
+ (expiresAtMs !== undefined && (!Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now()))
540
+ ) {
541
+ return { ok: false, code: "unavailable", status: 0, reason: "credit_not_usable" };
542
+ }
543
+ if (
544
+ (program === CEDAR_PROGRAM && !CEDAR_GRANT_ID.test(auth.credit.id)) ||
545
+ (program === JUNIPER_PROGRAM && auth.credit.id !== JUNIPER_PROGRAM)
546
+ ) {
547
+ return { ok: false, code: "invalid_credit", status: 0 };
548
+ }
549
+ const requestId = program === CEDAR_PROGRAM ? (auth.redeemRequestId ?? crypto.randomUUID()) : undefined;
550
+ if (requestId !== undefined && !REDEEM_REQUEST_ID.test(requestId)) {
551
+ return { ok: false, code: "invalid_request", status: 0 };
552
+ }
553
+ if (auth.baseUrl?.trim()) {
554
+ try {
555
+ new URL(auth.baseUrl);
556
+ } catch {
557
+ return { ok: false, code: "invalid_base_url", status: 0 };
558
+ }
559
+ }
560
+ const organization = await resolveOrganization(auth);
561
+ if (!organization.orgId) {
562
+ return {
563
+ ok: false,
564
+ code: organization.code ?? "organization_unavailable",
565
+ status: organization.status,
566
+ ...(organization.raw !== undefined ? { raw: organization.raw } : {}),
567
+ };
568
+ }
569
+ const body =
570
+ program === CEDAR_PROGRAM
571
+ ? { program: CEDAR_PROGRAM, grant_id: auth.credit.id, request_id: requestId }
572
+ : { program: JUNIPER_PROGRAM };
573
+ let response: Response;
574
+ try {
575
+ response = await auth.fetch(
576
+ claudeApiUrl(auth.baseUrl, `/api/organizations/${encodeURIComponent(organization.orgId)}/reset_rate_limits`),
577
+ {
578
+ method: "POST",
579
+ headers: buildClaudeOAuthHeaders(auth.accessToken),
580
+ body: JSON.stringify(body),
581
+ signal: auth.signal ?? AbortSignal.timeout(25_000),
582
+ },
583
+ );
584
+ } catch (error) {
585
+ return { ok: false, code: "network_error", status: 0, raw: error };
586
+ }
587
+ let payload: unknown;
588
+ try {
589
+ payload = await response.json();
590
+ } catch {
591
+ payload = undefined;
592
+ }
593
+ if (!response.ok) {
594
+ const code =
595
+ response.status === 401 || response.status === 403
596
+ ? "auth_error"
597
+ : response.status === 429
598
+ ? "rate_limited"
599
+ : `http_${response.status}`;
600
+ return {
601
+ ok: false,
602
+ code,
603
+ status: response.status,
604
+ ...(payload !== undefined ? { raw: payload } : {}),
605
+ };
606
+ }
607
+ if (!isRecord(payload) || typeof payload.result !== "string" || !payload.result.trim()) {
608
+ return {
609
+ ok: false,
610
+ code: "malformed_response",
611
+ status: response.status,
612
+ ...(payload !== undefined ? { raw: payload } : {}),
613
+ };
614
+ }
615
+ const reason = optionalString(payload.reason);
616
+ if (reason === null) {
617
+ return { ok: false, code: "malformed_response", status: response.status, raw: payload };
618
+ }
619
+ let cleared: string[] | undefined;
620
+ if (payload.cleared !== undefined) {
621
+ const normalized = normalizeWindowList(payload.cleared);
622
+ if (!normalized) {
623
+ return { ok: false, code: "malformed_response", status: response.status, raw: payload };
624
+ }
625
+ cleared = normalized;
626
+ } else if (payload.result === "reset") {
627
+ cleared = program === JUNIPER_PROGRAM ? ["anthropic:5h"] : auth.credit.clears;
628
+ }
629
+ const code = normalizeConsumeCode(payload.result.trim());
630
+ return {
631
+ ok: response.ok && payload.result === "reset",
632
+ code,
633
+ status: response.status,
634
+ ...(cleared ? { cleared } : {}),
635
+ ...(reason ? { reason } : {}),
636
+ raw: payload,
637
+ };
638
+ }