@f5-sales-demo/pi-ai 19.51.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.
Files changed (128) hide show
  1. package/CHANGELOG.md +1997 -0
  2. package/README.md +1160 -0
  3. package/package.json +135 -0
  4. package/src/api-registry.ts +95 -0
  5. package/src/auth-storage.ts +2694 -0
  6. package/src/cli.ts +493 -0
  7. package/src/index.ts +42 -0
  8. package/src/model-cache.ts +97 -0
  9. package/src/model-manager.ts +349 -0
  10. package/src/model-thinking.ts +561 -0
  11. package/src/models.json +49439 -0
  12. package/src/models.json.d.ts +9 -0
  13. package/src/models.ts +56 -0
  14. package/src/prompts/turn-aborted-guidance.md +4 -0
  15. package/src/provider-details.ts +81 -0
  16. package/src/provider-models/descriptors.ts +285 -0
  17. package/src/provider-models/google.ts +90 -0
  18. package/src/provider-models/index.ts +4 -0
  19. package/src/provider-models/openai-compat.ts +2074 -0
  20. package/src/provider-models/special.ts +106 -0
  21. package/src/providers/amazon-bedrock.ts +706 -0
  22. package/src/providers/anthropic.ts +1682 -0
  23. package/src/providers/azure-openai-responses.ts +391 -0
  24. package/src/providers/cursor/gen/agent_pb.ts +15274 -0
  25. package/src/providers/cursor/proto/agent.proto +3526 -0
  26. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  27. package/src/providers/cursor/proto/buf.yaml +17 -0
  28. package/src/providers/cursor.ts +2218 -0
  29. package/src/providers/github-copilot-headers.ts +140 -0
  30. package/src/providers/gitlab-duo.ts +381 -0
  31. package/src/providers/google-gemini-cli.ts +1133 -0
  32. package/src/providers/google-shared.ts +354 -0
  33. package/src/providers/google-vertex.ts +436 -0
  34. package/src/providers/google.ts +381 -0
  35. package/src/providers/kimi.ts +151 -0
  36. package/src/providers/openai-codex/constants.ts +43 -0
  37. package/src/providers/openai-codex/request-transformer.ts +158 -0
  38. package/src/providers/openai-codex/response-handler.ts +81 -0
  39. package/src/providers/openai-codex-responses.ts +2345 -0
  40. package/src/providers/openai-completions-compat.ts +159 -0
  41. package/src/providers/openai-completions.ts +1290 -0
  42. package/src/providers/openai-responses-shared.ts +452 -0
  43. package/src/providers/openai-responses.ts +519 -0
  44. package/src/providers/register-builtins.ts +329 -0
  45. package/src/providers/synthetic.ts +154 -0
  46. package/src/providers/transform-messages.ts +234 -0
  47. package/src/rate-limit-utils.ts +84 -0
  48. package/src/stream.ts +728 -0
  49. package/src/types.ts +546 -0
  50. package/src/usage/claude.ts +337 -0
  51. package/src/usage/gemini.ts +248 -0
  52. package/src/usage/github-copilot.ts +421 -0
  53. package/src/usage/google-antigravity.ts +200 -0
  54. package/src/usage/kimi.ts +286 -0
  55. package/src/usage/minimax-code.ts +31 -0
  56. package/src/usage/openai-codex.ts +387 -0
  57. package/src/usage/zai.ts +247 -0
  58. package/src/usage.ts +130 -0
  59. package/src/utils/abort.ts +36 -0
  60. package/src/utils/anthropic-auth.ts +293 -0
  61. package/src/utils/discovery/antigravity.ts +261 -0
  62. package/src/utils/discovery/codex.ts +371 -0
  63. package/src/utils/discovery/cursor.ts +306 -0
  64. package/src/utils/discovery/gemini.ts +248 -0
  65. package/src/utils/discovery/index.ts +5 -0
  66. package/src/utils/discovery/openai-compatible.ts +224 -0
  67. package/src/utils/event-stream.ts +209 -0
  68. package/src/utils/http-inspector.ts +165 -0
  69. package/src/utils/idle-iterator.ts +176 -0
  70. package/src/utils/json-parse.ts +28 -0
  71. package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
  72. package/src/utils/oauth/anthropic.ts +134 -0
  73. package/src/utils/oauth/api-key-validation.ts +92 -0
  74. package/src/utils/oauth/callback-server.ts +276 -0
  75. package/src/utils/oauth/cerebras.ts +59 -0
  76. package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
  77. package/src/utils/oauth/cursor.ts +157 -0
  78. package/src/utils/oauth/github-copilot.ts +358 -0
  79. package/src/utils/oauth/gitlab-duo.ts +123 -0
  80. package/src/utils/oauth/google-antigravity.ts +275 -0
  81. package/src/utils/oauth/google-gemini-cli.ts +334 -0
  82. package/src/utils/oauth/huggingface.ts +62 -0
  83. package/src/utils/oauth/index.ts +512 -0
  84. package/src/utils/oauth/kagi.ts +47 -0
  85. package/src/utils/oauth/kilo.ts +87 -0
  86. package/src/utils/oauth/kimi.ts +251 -0
  87. package/src/utils/oauth/litellm.ts +81 -0
  88. package/src/utils/oauth/lm-studio.ts +40 -0
  89. package/src/utils/oauth/minimax-code.ts +78 -0
  90. package/src/utils/oauth/moonshot.ts +59 -0
  91. package/src/utils/oauth/nanogpt.ts +51 -0
  92. package/src/utils/oauth/nvidia.ts +70 -0
  93. package/src/utils/oauth/oauth.html +199 -0
  94. package/src/utils/oauth/ollama.ts +47 -0
  95. package/src/utils/oauth/openai-codex.ts +190 -0
  96. package/src/utils/oauth/opencode.ts +49 -0
  97. package/src/utils/oauth/parallel.ts +46 -0
  98. package/src/utils/oauth/perplexity.ts +200 -0
  99. package/src/utils/oauth/pkce.ts +18 -0
  100. package/src/utils/oauth/qianfan.ts +58 -0
  101. package/src/utils/oauth/qwen-portal.ts +60 -0
  102. package/src/utils/oauth/synthetic.ts +60 -0
  103. package/src/utils/oauth/tavily.ts +46 -0
  104. package/src/utils/oauth/together.ts +59 -0
  105. package/src/utils/oauth/types.ts +89 -0
  106. package/src/utils/oauth/venice.ts +59 -0
  107. package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
  108. package/src/utils/oauth/vllm.ts +40 -0
  109. package/src/utils/oauth/xiaomi.ts +88 -0
  110. package/src/utils/oauth/zai.ts +60 -0
  111. package/src/utils/oauth/zenmux.ts +51 -0
  112. package/src/utils/overflow.ts +134 -0
  113. package/src/utils/retry-after.ts +110 -0
  114. package/src/utils/retry.ts +93 -0
  115. package/src/utils/schema/CONSTRAINTS.md +160 -0
  116. package/src/utils/schema/adapt.ts +20 -0
  117. package/src/utils/schema/compatibility.ts +397 -0
  118. package/src/utils/schema/dereference.ts +93 -0
  119. package/src/utils/schema/equality.ts +93 -0
  120. package/src/utils/schema/fields.ts +147 -0
  121. package/src/utils/schema/index.ts +9 -0
  122. package/src/utils/schema/normalize-cca.ts +479 -0
  123. package/src/utils/schema/sanitize-google.ts +212 -0
  124. package/src/utils/schema/strict-mode.ts +385 -0
  125. package/src/utils/schema/types.ts +5 -0
  126. package/src/utils/tool-choice.ts +81 -0
  127. package/src/utils/validation.ts +664 -0
  128. package/src/utils.ts +147 -0
@@ -0,0 +1,286 @@
1
+ import { $env } from "@f5xc-salesdemos/pi-utils";
2
+ import type {
3
+ UsageAmount,
4
+ UsageFetchContext,
5
+ UsageFetchParams,
6
+ UsageLimit,
7
+ UsageProvider,
8
+ UsageReport,
9
+ UsageStatus,
10
+ UsageWindow,
11
+ } from "../usage";
12
+ import { isRecord } from "../utils";
13
+ import { getKimiCommonHeaders, refreshKimiToken } from "../utils/oauth/kimi";
14
+
15
+ const DEFAULT_BASE_URL = "https://api.kimi.com/coding/v1";
16
+ const USAGE_PATH = "usages";
17
+
18
+ interface KimiUsagePayload {
19
+ usage?: unknown;
20
+ limits?: unknown;
21
+ }
22
+
23
+ type KimiUsageRow = {
24
+ label: string;
25
+ used?: number;
26
+ limit?: number;
27
+ remaining?: number;
28
+ resetsAt?: number;
29
+ window?: UsageWindow;
30
+ };
31
+
32
+ const toNumber = (value: unknown): number | undefined => {
33
+ if (typeof value === "number" && Number.isFinite(value)) return value;
34
+ if (typeof value === "string") {
35
+ const trimmed = value.trim();
36
+ if (!trimmed) return undefined;
37
+ const parsed = Number(trimmed);
38
+ if (Number.isFinite(parsed)) return parsed;
39
+ }
40
+ return undefined;
41
+ };
42
+
43
+ function normalizeBaseUrl(baseUrl?: string): string {
44
+ const envBase = $env.KIMI_CODE_BASE_URL?.trim();
45
+ const candidate = baseUrl?.trim() || envBase || DEFAULT_BASE_URL;
46
+ return candidate.replace(/\/+$/, "");
47
+ }
48
+
49
+ function buildUsageUrl(baseUrl: string): string {
50
+ const normalized = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
51
+ return `${normalized}${USAGE_PATH}`;
52
+ }
53
+
54
+ function parseResetTime(data: Record<string, unknown>, nowMs: number): number | undefined {
55
+ const timeKeys = ["reset_at", "resetAt", "reset_time", "resetTime"] as const;
56
+ for (const key of timeKeys) {
57
+ const value = data[key];
58
+ if (typeof value === "string" && value.trim()) {
59
+ const parsed = Date.parse(value);
60
+ if (Number.isFinite(parsed)) return parsed;
61
+ }
62
+ if (typeof value === "number" && Number.isFinite(value)) {
63
+ return value > 1_000_000_000_000 ? value : value * 1000;
64
+ }
65
+ }
66
+
67
+ const secondsKeys = ["reset_in", "resetIn", "ttl", "window"] as const;
68
+ for (const key of secondsKeys) {
69
+ const seconds = toNumber(data[key]);
70
+ if (seconds !== undefined) return nowMs + seconds * 1000;
71
+ }
72
+
73
+ return undefined;
74
+ }
75
+
76
+ function formatDurationLabel(duration: number, timeUnit: string): string | undefined {
77
+ const upper = timeUnit.toUpperCase();
78
+ if (upper.includes("MINUTE")) {
79
+ if (duration >= 60 && duration % 60 === 0) return `${duration / 60}h limit`;
80
+ return `${duration}m limit`;
81
+ }
82
+ if (upper.includes("HOUR")) return `${duration}h limit`;
83
+ if (upper.includes("DAY")) return `${duration}d limit`;
84
+ if (upper.includes("SECOND")) return `${duration}s limit`;
85
+ return undefined;
86
+ }
87
+
88
+ function buildWindow(windowData: Record<string, unknown>, nowMs: number): UsageWindow | undefined {
89
+ const duration = toNumber(windowData.duration);
90
+ const timeUnit = typeof windowData.timeUnit === "string" ? windowData.timeUnit : "";
91
+ const label = duration !== undefined && timeUnit ? formatDurationLabel(duration, timeUnit) : undefined;
92
+ const resetsAt = parseResetTime(windowData, nowMs);
93
+
94
+ if (duration === undefined && !label && !resetsAt) return undefined;
95
+ let durationMs: number | undefined;
96
+ if (duration !== undefined) {
97
+ if (timeUnit.toUpperCase().includes("MINUTE")) durationMs = duration * 60_000;
98
+ else if (timeUnit.toUpperCase().includes("HOUR")) durationMs = duration * 3_600_000;
99
+ else if (timeUnit.toUpperCase().includes("DAY")) durationMs = duration * 86_400_000;
100
+ else if (timeUnit.toUpperCase().includes("SECOND")) durationMs = duration * 1000;
101
+ }
102
+
103
+ return {
104
+ id: duration !== undefined && timeUnit ? `${duration}${timeUnit.toLowerCase()}` : "default",
105
+ label: label ?? "Usage window",
106
+ durationMs,
107
+ resetsAt,
108
+ };
109
+ }
110
+
111
+ function buildUsageRow(data: Record<string, unknown>, defaultLabel: string, nowMs: number): KimiUsageRow | null {
112
+ const limit = toNumber(data.limit);
113
+ let used = toNumber(data.used);
114
+ const remaining = toNumber(data.remaining);
115
+ if (used === undefined && remaining !== undefined && limit !== undefined) {
116
+ used = limit - remaining;
117
+ }
118
+
119
+ if (used === undefined && limit === undefined) return null;
120
+ const resetsAt = parseResetTime(data, nowMs);
121
+ return {
122
+ label:
123
+ typeof data.name === "string" && data.name
124
+ ? data.name
125
+ : typeof data.title === "string" && data.title
126
+ ? data.title
127
+ : defaultLabel,
128
+ used,
129
+ limit,
130
+ remaining,
131
+ resetsAt,
132
+ };
133
+ }
134
+
135
+ function buildUsageAmount(row: KimiUsageRow): UsageAmount {
136
+ const amount: UsageAmount = { unit: "unknown" };
137
+ if (row.limit !== undefined) amount.limit = row.limit;
138
+ if (row.used !== undefined) amount.used = row.used;
139
+ if (row.remaining !== undefined) amount.remaining = row.remaining;
140
+ if (row.limit !== undefined && row.used !== undefined && row.limit > 0) {
141
+ amount.usedFraction = Math.min(Math.max(row.used / row.limit, 0), 1);
142
+ amount.remainingFraction = Math.min(Math.max((row.limit - row.used) / row.limit, 0), 1);
143
+ amount.remaining = amount.remaining ?? row.limit - row.used;
144
+ }
145
+ return amount;
146
+ }
147
+
148
+ function buildUsageStatus(amount: UsageAmount): UsageStatus {
149
+ if (amount.usedFraction === undefined) return "unknown";
150
+ if (amount.usedFraction >= 1) return "exhausted";
151
+ if (amount.usedFraction >= 0.9) return "warning";
152
+ return "ok";
153
+ }
154
+
155
+ function toUsageLimit(row: KimiUsageRow, provider: string, index: number, accountId?: string): UsageLimit {
156
+ const window: UsageWindow | undefined =
157
+ row.window ??
158
+ (row.resetsAt
159
+ ? {
160
+ id: "default",
161
+ label: "Usage window",
162
+ resetsAt: row.resetsAt,
163
+ }
164
+ : undefined);
165
+
166
+ const amount = buildUsageAmount(row);
167
+ return {
168
+ id: `${provider}:${index}`,
169
+ label: row.label,
170
+ scope: {
171
+ provider,
172
+ accountId,
173
+ windowId: window?.id,
174
+ shared: true,
175
+ },
176
+ window,
177
+ amount,
178
+ status: buildUsageStatus(amount),
179
+ };
180
+ }
181
+
182
+ function parseUsagePayload(payload: unknown, nowMs: number): { rows: KimiUsageRow[]; raw: KimiUsagePayload } | null {
183
+ if (!isRecord(payload)) return null;
184
+ const data = payload as KimiUsagePayload;
185
+ const rows: KimiUsageRow[] = [];
186
+
187
+ if (isRecord(data.usage)) {
188
+ const summary = buildUsageRow(data.usage, "Total quota", nowMs);
189
+ if (summary) rows.push(summary);
190
+ }
191
+
192
+ if (Array.isArray(data.limits)) {
193
+ data.limits.forEach((item, idx) => {
194
+ if (!isRecord(item)) return;
195
+ const detail = isRecord(item.detail) ? item.detail : item;
196
+ const windowData = isRecord(item.window) ? item.window : {};
197
+ const label =
198
+ (typeof item.name === "string" && item.name) ||
199
+ (typeof item.title === "string" && item.title) ||
200
+ (typeof item.scope === "string" && item.scope) ||
201
+ (typeof detail.name === "string" && detail.name) ||
202
+ (typeof detail.title === "string" && detail.title) ||
203
+ formatDurationLabel(toNumber(windowData.duration) ?? 0, String(windowData.timeUnit || "")) ||
204
+ `Limit #${idx + 1}`;
205
+ const row = buildUsageRow(detail, label, nowMs);
206
+ if (row) {
207
+ row.window = buildWindow(windowData, nowMs);
208
+ rows.push(row);
209
+ }
210
+ });
211
+ }
212
+
213
+ return { rows, raw: data };
214
+ }
215
+
216
+ export const kimiUsageProvider: UsageProvider = {
217
+ id: "kimi-code",
218
+ supports(params: UsageFetchParams): boolean {
219
+ return params.provider === "kimi-code" && params.credential.type === "oauth";
220
+ },
221
+ async fetchUsage(params: UsageFetchParams, ctx: UsageFetchContext): Promise<UsageReport | null> {
222
+ if (params.provider !== "kimi-code") return null;
223
+ const { credential } = params;
224
+ if (credential.type !== "oauth") return null;
225
+
226
+ let accessToken = credential.accessToken;
227
+ if (!accessToken) return null;
228
+
229
+ const nowMs = Date.now();
230
+ if (credential.expiresAt !== undefined && credential.expiresAt <= nowMs) {
231
+ if (!credential.refreshToken) {
232
+ ctx.logger?.warn("Kimi usage token expired, no refresh token", { provider: params.provider });
233
+ return null;
234
+ }
235
+ try {
236
+ ctx.logger?.debug("Kimi usage token expired, refreshing", { provider: params.provider });
237
+ const refreshed = await refreshKimiToken(credential.refreshToken);
238
+ accessToken = refreshed.access;
239
+ } catch (error) {
240
+ ctx.logger?.warn("Kimi usage token refresh failed", { provider: params.provider, error: String(error) });
241
+ return null;
242
+ }
243
+ }
244
+
245
+ const baseUrl = normalizeBaseUrl(params.baseUrl);
246
+ const url = buildUsageUrl(baseUrl);
247
+ let payload: unknown;
248
+ try {
249
+ const response = await ctx.fetch(url, {
250
+ headers: {
251
+ ...(await getKimiCommonHeaders()),
252
+ Authorization: `Bearer ${accessToken}`,
253
+ },
254
+ signal: params.signal,
255
+ });
256
+ if (!response.ok) {
257
+ ctx.logger?.warn("Kimi usage request failed", { status: response.status, provider: params.provider });
258
+ return null;
259
+ }
260
+ payload = await response.json();
261
+ } catch (error) {
262
+ ctx.logger?.warn("Kimi usage request error", { provider: params.provider, error: String(error) });
263
+ return null;
264
+ }
265
+
266
+ const parsed = parseUsagePayload(payload, nowMs);
267
+ if (!parsed || parsed.rows.length === 0) {
268
+ ctx.logger?.warn("Kimi usage response invalid", { provider: params.provider });
269
+ return null;
270
+ }
271
+
272
+ const limits = parsed.rows.map((row, index) => toUsageLimit(row, params.provider, index, credential.accountId));
273
+
274
+ const report: UsageReport = {
275
+ provider: params.provider,
276
+ fetchedAt: nowMs,
277
+ limits,
278
+ metadata: {
279
+ endpoint: url,
280
+ },
281
+ raw: parsed.raw,
282
+ };
283
+
284
+ return report;
285
+ },
286
+ };
@@ -0,0 +1,31 @@
1
+ import type { UsageFetchContext, UsageFetchParams, UsageProvider, UsageReport } from "../usage";
2
+
3
+ /**
4
+ * MiniMax Coding Plan usage provider.
5
+ *
6
+ * MiniMax Coding Plan is a subscription-based service with a 5-hour rolling window
7
+ * quota system. The quota resets automatically based on a rolling window.
8
+ *
9
+ * Currently, MiniMax does not expose a usage/quota API endpoint for the Coding Plan.
10
+ * Usage is tracked via the web dashboard at https://platform.minimax.io/user-center/payment/coding-plan
11
+ *
12
+ * This provider exists to register support for the minimax-code provider in the
13
+ * usage system. When MiniMax adds a usage API, this can be implemented.
14
+ */
15
+ async function fetchMiniMaxCodeUsage(params: UsageFetchParams, _ctx: UsageFetchContext): Promise<UsageReport | null> {
16
+ if (params.provider !== "minimax-code" && params.provider !== "minimax-code-cn") {
17
+ return null;
18
+ }
19
+
20
+ // MiniMax Coding Plan does not currently expose a usage API
21
+ // Users can check their usage via the web dashboard
22
+ return null;
23
+ }
24
+
25
+ export const minimaxCodeUsageProvider: UsageProvider = {
26
+ id: "minimax-code",
27
+ fetchUsage: fetchMiniMaxCodeUsage,
28
+ supports: (params: UsageFetchParams) =>
29
+ (params.provider === "minimax-code" || params.provider === "minimax-code-cn") &&
30
+ params.credential.type === "api_key",
31
+ };
@@ -0,0 +1,387 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { CODEX_BASE_URL } from "../providers/openai-codex/constants";
3
+ import type {
4
+ CredentialRankingStrategy,
5
+ UsageAmount,
6
+ UsageFetchContext,
7
+ UsageFetchParams,
8
+ UsageLimit,
9
+ UsageProvider,
10
+ UsageReport,
11
+ UsageWindow,
12
+ } from "../usage";
13
+ import { isRecord } from "../utils";
14
+
15
+ const CODEX_USAGE_PATH = "wham/usage";
16
+ const JWT_AUTH_CLAIM = "https://api.openai.com/auth";
17
+ const JWT_PROFILE_CLAIM = "https://api.openai.com/profile";
18
+
19
+ interface CodexUsageWindowPayload {
20
+ used_percent?: number;
21
+ limit_window_seconds?: number;
22
+ reset_after_seconds?: number;
23
+ reset_at?: number;
24
+ }
25
+
26
+ interface CodexUsageRateLimitPayload {
27
+ allowed?: boolean;
28
+ limit_reached?: boolean;
29
+ primary_window?: CodexUsageWindowPayload | null;
30
+ secondary_window?: CodexUsageWindowPayload | null;
31
+ }
32
+
33
+ interface CodexUsagePayload {
34
+ plan_type?: string;
35
+ rate_limit?: CodexUsageRateLimitPayload | null;
36
+ }
37
+
38
+ interface ParsedUsageWindow {
39
+ usedPercent?: number;
40
+ limitWindowSeconds?: number;
41
+ resetAfterSeconds?: number;
42
+ resetAt?: number;
43
+ }
44
+
45
+ interface ParsedUsage {
46
+ planType?: string;
47
+ allowed?: boolean;
48
+ limitReached?: boolean;
49
+ primary?: ParsedUsageWindow;
50
+ secondary?: ParsedUsageWindow;
51
+ raw: CodexUsagePayload;
52
+ }
53
+
54
+ interface JwtPayload {
55
+ [JWT_AUTH_CLAIM]?: {
56
+ chatgpt_account_id?: string;
57
+ };
58
+ [JWT_PROFILE_CLAIM]?: {
59
+ email?: string;
60
+ };
61
+ }
62
+
63
+ const toNumber = (value: unknown): number | undefined => {
64
+ if (typeof value === "number" && Number.isFinite(value)) return value;
65
+ if (typeof value === "string") {
66
+ const trimmed = value.trim();
67
+ if (!trimmed) return undefined;
68
+ const parsed = Number(trimmed);
69
+ if (Number.isFinite(parsed)) return parsed;
70
+ }
71
+ return undefined;
72
+ };
73
+
74
+ const toBoolean = (value: unknown): boolean | undefined => {
75
+ if (typeof value === "boolean") return value;
76
+ return undefined;
77
+ };
78
+
79
+ function base64UrlDecode(input: string): string {
80
+ const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
81
+ const padLen = (4 - (base64.length % 4)) % 4;
82
+ const padded = base64 + "=".repeat(padLen);
83
+ return Buffer.from(padded, "base64").toString("utf8");
84
+ }
85
+
86
+ function parseJwt(token: string): JwtPayload | null {
87
+ const parts = token.split(".");
88
+ if (parts.length !== 3) return null;
89
+ try {
90
+ const payloadJson = base64UrlDecode(parts[1]);
91
+ return JSON.parse(payloadJson) as JwtPayload;
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ function normalizeEmail(email: string | undefined): string | undefined {
98
+ if (!email) return undefined;
99
+ const normalized = email.trim().toLowerCase();
100
+ return normalized || undefined;
101
+ }
102
+
103
+ function extractAccountId(token: string | undefined): string | undefined {
104
+ if (!token) return undefined;
105
+ const payload = parseJwt(token);
106
+ return payload?.[JWT_AUTH_CLAIM]?.chatgpt_account_id ?? undefined;
107
+ }
108
+
109
+ function extractEmail(token: string | undefined): string | undefined {
110
+ if (!token) return undefined;
111
+ const payload = parseJwt(token);
112
+ return normalizeEmail(payload?.[JWT_PROFILE_CLAIM]?.email);
113
+ }
114
+
115
+ function parseUsageWindow(payload: unknown): ParsedUsageWindow | undefined {
116
+ if (!isRecord(payload)) return undefined;
117
+ const usedPercent = toNumber(payload.used_percent);
118
+ const limitWindowSeconds = toNumber(payload.limit_window_seconds);
119
+ const resetAfterSeconds = toNumber(payload.reset_after_seconds);
120
+ const resetAt = toNumber(payload.reset_at);
121
+ if (
122
+ usedPercent === undefined &&
123
+ limitWindowSeconds === undefined &&
124
+ resetAfterSeconds === undefined &&
125
+ resetAt === undefined
126
+ ) {
127
+ return undefined;
128
+ }
129
+ return {
130
+ usedPercent,
131
+ limitWindowSeconds,
132
+ resetAfterSeconds,
133
+ resetAt,
134
+ };
135
+ }
136
+
137
+ function parseUsagePayload(payload: unknown): ParsedUsage | null {
138
+ if (!isRecord(payload)) return null;
139
+ const planType = typeof payload.plan_type === "string" ? payload.plan_type : undefined;
140
+ const rateLimit = isRecord(payload.rate_limit) ? payload.rate_limit : undefined;
141
+ if (!rateLimit) return null;
142
+ const parsed: ParsedUsage = {
143
+ planType,
144
+ allowed: toBoolean(rateLimit.allowed),
145
+ limitReached: toBoolean(rateLimit.limit_reached),
146
+ primary: parseUsageWindow(rateLimit.primary_window),
147
+ secondary: parseUsageWindow(rateLimit.secondary_window),
148
+ raw: payload as CodexUsagePayload,
149
+ };
150
+ if (!parsed.primary && !parsed.secondary && parsed.allowed === undefined && parsed.limitReached === undefined) {
151
+ return null;
152
+ }
153
+ return parsed;
154
+ }
155
+
156
+ function normalizeCodexBaseUrl(baseUrl?: string): string {
157
+ const fallback = CODEX_BASE_URL;
158
+ const trimmed = baseUrl?.trim() ? baseUrl.trim() : fallback;
159
+ const base = trimmed.replace(/\/+$/, "");
160
+ const lower = base.toLowerCase();
161
+ if (
162
+ (lower.startsWith("https://chatgpt.com") || lower.startsWith("https://chat.openai.com")) &&
163
+ !lower.includes("/backend-api")
164
+ ) {
165
+ return `${base}/backend-api`;
166
+ }
167
+ return base;
168
+ }
169
+
170
+ function buildCodexUsageUrl(baseUrl: string): string {
171
+ const normalized = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
172
+ return `${normalized}${CODEX_USAGE_PATH}`;
173
+ }
174
+
175
+ function formatWindowLabel(value: number, unit: "hour" | "day"): string {
176
+ const rounded = Math.round(value);
177
+ const suffix = rounded === 1 ? unit : `${unit}s`;
178
+ return `${rounded} ${suffix}`;
179
+ }
180
+
181
+ function buildWindowLabel(seconds: number): { id: string; label: string } {
182
+ const daySeconds = 86_400;
183
+ if (seconds >= daySeconds) {
184
+ const days = Math.round(seconds / daySeconds);
185
+ return { id: `${days}d`, label: formatWindowLabel(days, "day") };
186
+ }
187
+ const hours = Math.max(1, Math.round(seconds / 3600));
188
+ return { id: `${hours}h`, label: formatWindowLabel(hours, "hour") };
189
+ }
190
+
191
+ function resolveResetTime(window: ParsedUsageWindow, nowMs: number): number | undefined {
192
+ const resetAt = window.resetAt;
193
+ if (resetAt !== undefined) {
194
+ const resetAtMs = resetAt > 1_000_000_000_000 ? resetAt : resetAt * 1000;
195
+ if (Number.isFinite(resetAtMs)) return resetAtMs;
196
+ }
197
+ if (window.resetAfterSeconds !== undefined) {
198
+ return nowMs + window.resetAfterSeconds * 1000;
199
+ }
200
+ return undefined;
201
+ }
202
+
203
+ function buildUsageWindow(window: ParsedUsageWindow, key: string, nowMs: number): UsageWindow {
204
+ const resetsAt = resolveResetTime(window, nowMs);
205
+ if (window.limitWindowSeconds !== undefined) {
206
+ const { id, label } = buildWindowLabel(window.limitWindowSeconds);
207
+ const durationMs = window.limitWindowSeconds * 1000;
208
+ return { id, label, durationMs, ...(resetsAt !== undefined ? { resetsAt } : {}) };
209
+ }
210
+ const fallbackLabel = key === "primary" ? "Primary window" : "Secondary window";
211
+ return { id: key, label: fallbackLabel, ...(resetsAt !== undefined ? { resetsAt } : {}) };
212
+ }
213
+
214
+ function buildUsageAmount(window: ParsedUsageWindow): UsageAmount {
215
+ const usedPercent = window.usedPercent;
216
+ if (usedPercent === undefined) {
217
+ return { unit: "percent" };
218
+ }
219
+ const clamped = Math.min(Math.max(usedPercent, 0), 100);
220
+ const usedFraction = clamped / 100;
221
+ return {
222
+ used: clamped,
223
+ limit: 100,
224
+ remaining: Math.max(0, 100 - clamped),
225
+ usedFraction,
226
+ remainingFraction: Math.max(0, 1 - usedFraction),
227
+ unit: "percent",
228
+ };
229
+ }
230
+
231
+ function buildUsageStatus(usedFraction?: number, limitReached?: boolean): UsageLimit["status"] {
232
+ if (limitReached) return "exhausted";
233
+ if (usedFraction === undefined) return "unknown";
234
+ if (usedFraction >= 1) return "exhausted";
235
+ if (usedFraction >= 0.9) return "warning";
236
+ return "ok";
237
+ }
238
+
239
+ function buildUsageLimit(args: {
240
+ key: "primary" | "secondary";
241
+ window: ParsedUsageWindow;
242
+ accountId?: string;
243
+ planType?: string;
244
+ limitReached?: boolean;
245
+ nowMs: number;
246
+ }): UsageLimit {
247
+ const usageWindow = buildUsageWindow(args.window, args.key, args.nowMs);
248
+ const amount = buildUsageAmount(args.window);
249
+ return {
250
+ id: `openai-codex:${args.key}`,
251
+ label: usageWindow.label,
252
+ scope: {
253
+ provider: "openai-codex",
254
+ accountId: args.accountId,
255
+ tier: args.planType,
256
+ windowId: usageWindow.id,
257
+ shared: true,
258
+ },
259
+ window: usageWindow,
260
+ amount,
261
+ status: buildUsageStatus(amount.usedFraction, args.limitReached),
262
+ };
263
+ }
264
+
265
+ export const openaiCodexUsageProvider: UsageProvider = {
266
+ id: "openai-codex",
267
+ supports(params: UsageFetchParams): boolean {
268
+ return params.provider === "openai-codex" && params.credential.type === "oauth";
269
+ },
270
+ async fetchUsage(params: UsageFetchParams, ctx: UsageFetchContext): Promise<UsageReport | null> {
271
+ if (params.provider !== "openai-codex") return null;
272
+ const { credential } = params;
273
+ if (credential.type !== "oauth") return null;
274
+
275
+ const accessToken = credential.accessToken;
276
+ if (!accessToken) return null;
277
+
278
+ const nowMs = Date.now();
279
+ if (credential.expiresAt !== undefined && credential.expiresAt <= nowMs) {
280
+ ctx.logger?.warn("Codex usage token expired", { provider: params.provider });
281
+ return null;
282
+ }
283
+
284
+ const baseUrl = normalizeCodexBaseUrl(params.baseUrl);
285
+ const accountId = credential.accountId ?? extractAccountId(accessToken);
286
+ const email = normalizeEmail(credential.email ?? extractEmail(accessToken));
287
+
288
+ const headers: Record<string, string> = {
289
+ Authorization: `Bearer ${accessToken}`,
290
+ "User-Agent": "OpenCode-Status-Plugin/1.0",
291
+ };
292
+ if (accountId) {
293
+ headers["ChatGPT-Account-Id"] = accountId;
294
+ }
295
+
296
+ const url = buildCodexUsageUrl(baseUrl);
297
+ let payload: unknown;
298
+ try {
299
+ const response = await ctx.fetch(url, { headers, signal: params.signal });
300
+ if (!response.ok) {
301
+ ctx.logger?.warn("Codex usage request failed", { status: response.status, provider: params.provider });
302
+ return null;
303
+ }
304
+ payload = await response.json();
305
+ } catch (error) {
306
+ ctx.logger?.warn("Codex usage request error", { provider: params.provider, error: String(error) });
307
+ return null;
308
+ }
309
+
310
+ const parsed = parseUsagePayload(payload);
311
+ const planType =
312
+ parsed?.planType ??
313
+ (isRecord(payload) && typeof payload.plan_type === "string" ? payload.plan_type : undefined);
314
+
315
+ const limits: UsageLimit[] = [];
316
+ if (parsed?.primary) {
317
+ limits.push(
318
+ buildUsageLimit({
319
+ key: "primary",
320
+ window: parsed.primary,
321
+ accountId,
322
+ planType,
323
+ limitReached: parsed.limitReached,
324
+ nowMs,
325
+ }),
326
+ );
327
+ }
328
+ if (parsed?.secondary) {
329
+ limits.push(
330
+ buildUsageLimit({
331
+ key: "secondary",
332
+ window: parsed.secondary,
333
+ accountId,
334
+ planType,
335
+ limitReached: parsed.limitReached,
336
+ nowMs,
337
+ }),
338
+ );
339
+ }
340
+
341
+ const report: UsageReport = {
342
+ provider: "openai-codex",
343
+ fetchedAt: nowMs,
344
+ limits,
345
+ metadata: {
346
+ planType,
347
+ allowed: parsed?.allowed,
348
+ limitReached: parsed?.limitReached,
349
+ email,
350
+ accountId,
351
+ },
352
+ raw: parsed?.raw ?? payload,
353
+ };
354
+
355
+ return report;
356
+ },
357
+ };
358
+
359
+ const FIVE_HOUR_MS = 5 * 60 * 60 * 1000;
360
+
361
+ export const codexRankingStrategy: CredentialRankingStrategy = {
362
+ findWindowLimits(report) {
363
+ const findLimit = (key: "primary" | "secondary"): UsageLimit | undefined => {
364
+ const direct = report.limits.find(l => l.id === `openai-codex:${key}`);
365
+ if (direct) return direct;
366
+ const byId = report.limits.find(l => l.id.toLowerCase().includes(key));
367
+ if (byId) return byId;
368
+ const windowId = key === "secondary" ? "7d" : "1h";
369
+ return report.limits.find(l => l.scope.windowId?.toLowerCase() === windowId);
370
+ };
371
+ return { primary: findLimit("primary"), secondary: findLimit("secondary") };
372
+ },
373
+ windowDefaults: { primaryMs: 60 * 60 * 1000, secondaryMs: 7 * 24 * 60 * 60 * 1000 },
374
+ hasPriorityBoost(primary) {
375
+ if (!primary) return false;
376
+ const windowId = primary.scope.windowId?.toLowerCase();
377
+ const durationMs = primary.window?.durationMs;
378
+ const isFiveHourWindow =
379
+ windowId === "5h" ||
380
+ (typeof durationMs === "number" &&
381
+ Number.isFinite(durationMs) &&
382
+ Math.abs(durationMs - FIVE_HOUR_MS) <= 60_000);
383
+ if (!isFiveHourWindow) return false;
384
+ const usedFraction = primary.amount.usedFraction;
385
+ return typeof usedFraction === "number" && Number.isFinite(usedFraction) && usedFraction === 0;
386
+ },
387
+ };