@bitkyc08/opencodex 2.6.22 → 2.6.23

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 (51) hide show
  1. package/gui/dist/assets/{index-DDcEW0Cm.css → index-BcHhxo1I.css} +1 -1
  2. package/gui/dist/assets/index-Yuh5eZiD.js +9 -0
  3. package/gui/dist/index.html +2 -2
  4. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  5. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  6. package/gui/dist/provider-icons/antigravity.svg +1 -0
  7. package/gui/dist/provider-icons/claude-color.svg +1 -0
  8. package/gui/dist/provider-icons/claude.svg +1 -0
  9. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  10. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  11. package/gui/dist/provider-icons/copilot.svg +1 -0
  12. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  13. package/gui/dist/provider-icons/cursor.svg +2 -0
  14. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  15. package/gui/dist/provider-icons/discord.svg +1 -0
  16. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  17. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  18. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  19. package/gui/dist/provider-icons/gemini.svg +1 -0
  20. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  21. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  22. package/gui/dist/provider-icons/grok-color.svg +1 -0
  23. package/gui/dist/provider-icons/grok.svg +1 -0
  24. package/gui/dist/provider-icons/groq-color.svg +1 -0
  25. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  26. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  27. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  28. package/gui/dist/provider-icons/kiro.svg +14 -0
  29. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  30. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  31. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  32. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  33. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  34. package/gui/dist/provider-icons/openai.svg +1 -0
  35. package/gui/dist/provider-icons/opencode.svg +1 -0
  36. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  37. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  38. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  39. package/gui/dist/provider-icons/telegram.svg +1 -0
  40. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  41. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  42. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  43. package/package.json +1 -1
  44. package/src/cli-help.ts +27 -0
  45. package/src/cli-models.ts +138 -0
  46. package/src/cli-provider.ts +414 -0
  47. package/src/cli.ts +27 -1
  48. package/src/codex-auth-api.ts +35 -20
  49. package/src/provider-quota.ts +330 -0
  50. package/src/server.ts +6 -0
  51. package/gui/dist/assets/index-C0YNrCNA.js +0 -9
@@ -0,0 +1,330 @@
1
+ import { listCodexAuthAccounts } from "./codex-auth-api";
2
+ import { MAIN_CODEX_ACCOUNT_ID } from "./codex-main-account";
3
+ import { getValidAccessToken } from "./oauth";
4
+ import { getCredential } from "./oauth/store";
5
+ import { antigravityUserAgent } from "./adapters/client-fingerprint";
6
+ import { getProviderRegistryEntry } from "./providers/registry";
7
+ import type { OcxConfig, OcxProviderConfig } from "./types";
8
+
9
+ const CACHE_TTL_MS = 5 * 60_000;
10
+ const REQUEST_TIMEOUT_MS = 8_000;
11
+ const REFRESH_SKEW_MS = 60_000;
12
+
13
+ export interface ProviderQuotaWindow {
14
+ label: string;
15
+ percent: number;
16
+ resetAt?: number;
17
+ }
18
+
19
+ export interface ProviderQuota {
20
+ fiveHourPercent?: number;
21
+ fiveHourResetAt?: number;
22
+ weeklyPercent?: number;
23
+ weeklyResetAt?: number;
24
+ monthlyPercent?: number;
25
+ monthlyResetAt?: number;
26
+ customWindows?: ProviderQuotaWindow[];
27
+ updatedAt: number;
28
+ }
29
+
30
+ export interface ProviderQuotaReport {
31
+ provider: string;
32
+ label: string;
33
+ source: string;
34
+ quota: ProviderQuota;
35
+ updatedAt: number;
36
+ reverseEngineered?: boolean;
37
+ }
38
+
39
+ export interface ProviderQuotaResponse {
40
+ generatedAt: number;
41
+ reports: ProviderQuotaReport[];
42
+ }
43
+
44
+ let cache: { key: string; ts: number; response: ProviderQuotaResponse } | null = null;
45
+
46
+ function cacheKey(config: OcxConfig): string {
47
+ const providers = Object.entries(config.providers)
48
+ .map(([name, provider]) => `${name}:${provider.authMode ?? "key"}:${provider.disabled === true ? "off" : "on"}:${provider.baseUrl}`)
49
+ .sort()
50
+ .join("|");
51
+ return `${config.defaultProvider}|${config.activeCodexAccountId ?? ""}|${providers}`;
52
+ }
53
+
54
+ function hasQuotaRows(quota: ProviderQuota | null | undefined): quota is ProviderQuota {
55
+ if (!quota) return false;
56
+ return typeof quota.fiveHourPercent === "number"
57
+ || typeof quota.weeklyPercent === "number"
58
+ || typeof quota.monthlyPercent === "number"
59
+ || !!quota.customWindows?.some(window => typeof window.percent === "number");
60
+ }
61
+
62
+ function providerLabel(providerId: string): string {
63
+ return getProviderRegistryEntry(providerId)?.label ?? providerId;
64
+ }
65
+
66
+ function normalizeResetAt(value: unknown): number | undefined {
67
+ if (typeof value === "number" && Number.isFinite(value)) return value > 10_000_000_000 ? value : value * 1000;
68
+ if (typeof value === "string" && value.trim()) {
69
+ const parsed = Date.parse(value);
70
+ return Number.isFinite(parsed) ? parsed : undefined;
71
+ }
72
+ return undefined;
73
+ }
74
+
75
+ function toFiniteNumber(value: unknown): number | undefined {
76
+ if (typeof value === "number" && Number.isFinite(value)) return value;
77
+ if (typeof value === "string" && value.trim()) {
78
+ const parsed = Number(value);
79
+ return Number.isFinite(parsed) ? parsed : undefined;
80
+ }
81
+ return undefined;
82
+ }
83
+
84
+ function normalizePercent(value: unknown): number | undefined {
85
+ const numeric = toFiniteNumber(value);
86
+ return numeric === undefined ? undefined : Math.max(0, Math.min(100, numeric));
87
+ }
88
+
89
+ function asRecord(value: unknown): Record<string, unknown> | null {
90
+ return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
91
+ }
92
+
93
+ function isBuiltInChatGptForwardProvider(name: string, provider: OcxProviderConfig): boolean {
94
+ const base = provider.baseUrl.replace(/\/+$/, "");
95
+ const normalizedName = name.toLowerCase();
96
+ return (normalizedName === "openai" || normalizedName === "chatgpt")
97
+ && provider.adapter === "openai-responses"
98
+ && provider.authMode === "forward"
99
+ && base === "https://chatgpt.com/backend-api/codex";
100
+ }
101
+
102
+ function report(provider: string, source: string, quota: ProviderQuota): ProviderQuotaReport | null {
103
+ if (!hasQuotaRows(quota)) return null;
104
+ return {
105
+ provider,
106
+ label: providerLabel(provider),
107
+ source,
108
+ quota,
109
+ updatedAt: quota.updatedAt,
110
+ };
111
+ }
112
+
113
+ async function fetchChatGptForwardQuota(config: OcxConfig, provider: string, forceRefresh: boolean): Promise<ProviderQuotaReport | null> {
114
+ const accounts = await listCodexAuthAccounts(config, forceRefresh);
115
+ const activeId = config.activeCodexAccountId || MAIN_CODEX_ACCOUNT_ID;
116
+ const active = accounts.find(account => account.id === activeId)
117
+ ?? accounts.find(account => account.id === MAIN_CODEX_ACCOUNT_ID)
118
+ ?? accounts[0];
119
+ const quota = active?.quota ? { ...active.quota, updatedAt: active.quota.updatedAt ?? Date.now() } as ProviderQuota : null;
120
+ return quota ? report(provider, "chatgpt:wham", quota) : null;
121
+ }
122
+
123
+ function centsValue(value: unknown): number | undefined {
124
+ const rec = asRecord(value);
125
+ return rec ? toFiniteNumber(rec.val) : undefined;
126
+ }
127
+
128
+ async function fetchXaiQuota(provider: string): Promise<ProviderQuotaReport | null> {
129
+ let accessToken: string;
130
+ try {
131
+ accessToken = await getValidAccessToken("xai");
132
+ } catch {
133
+ return null;
134
+ }
135
+ const response = await fetch("https://cli-chat-proxy.grok.com/v1/billing", {
136
+ headers: { Accept: "application/json", Authorization: `Bearer ${accessToken}` },
137
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
138
+ });
139
+ if (!response.ok) return null;
140
+ const body = asRecord(await response.json().catch(() => null));
141
+ const config = asRecord(body?.config);
142
+ if (!config) return null;
143
+ const limitCents = centsValue(config.monthlyLimit);
144
+ const usedCents = centsValue(config.used);
145
+ if (limitCents === undefined || usedCents === undefined || limitCents <= 0) return null;
146
+ const percent = normalizePercent((usedCents / limitCents) * 100);
147
+ if (percent === undefined) return null;
148
+ const quota: ProviderQuota = {
149
+ monthlyPercent: percent,
150
+ monthlyResetAt: normalizeResetAt(config.billingPeriodEnd),
151
+ updatedAt: Date.now(),
152
+ };
153
+ return report(provider, "xai:grok-billing", quota);
154
+ }
155
+
156
+ function parseClaudeBucket(value: unknown): { percent?: number; resetAt?: number } | null {
157
+ const rec = asRecord(value);
158
+ if (!rec) return null;
159
+ const percent = normalizePercent(rec.utilization);
160
+ const resetAt = normalizeResetAt(rec.resets_at);
161
+ if (percent === undefined && resetAt === undefined) return null;
162
+ return { percent, resetAt };
163
+ }
164
+
165
+ async function fetchAnthropicQuota(provider: string): Promise<ProviderQuotaReport | null> {
166
+ const credential = getCredential("anthropic");
167
+ if (!credential || credential.expires <= Date.now() + REFRESH_SKEW_MS) return null;
168
+ const response = await fetch("https://api.anthropic.com/api/oauth/usage", {
169
+ headers: {
170
+ Accept: "application/json, text/plain, */*",
171
+ "Content-Type": "application/json",
172
+ "User-Agent": "claude-cli/2.1.63 (external, cli)",
173
+ "anthropic-beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05",
174
+ Authorization: `Bearer ${credential.access}`,
175
+ },
176
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
177
+ });
178
+ if (!response.ok) return null;
179
+ const body = asRecord(await response.json().catch(() => null));
180
+ if (!body) return null;
181
+ const fiveHour = parseClaudeBucket(body.five_hour);
182
+ const sevenDay = parseClaudeBucket(body.seven_day);
183
+ const opus = parseClaudeBucket(body.seven_day_opus);
184
+ const sonnet = parseClaudeBucket(body.seven_day_sonnet);
185
+ const customWindows: ProviderQuotaWindow[] = [];
186
+ if (opus?.percent !== undefined) customWindows.push({ label: "Opus", percent: opus.percent, ...(opus.resetAt !== undefined ? { resetAt: opus.resetAt } : {}) });
187
+ if (sonnet?.percent !== undefined) customWindows.push({ label: "Sonnet", percent: sonnet.percent, ...(sonnet.resetAt !== undefined ? { resetAt: sonnet.resetAt } : {}) });
188
+ const quota: ProviderQuota = {
189
+ ...(fiveHour?.percent !== undefined ? { fiveHourPercent: fiveHour.percent } : {}),
190
+ ...(fiveHour?.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}),
191
+ ...(sevenDay?.percent !== undefined ? { weeklyPercent: sevenDay.percent } : {}),
192
+ ...(sevenDay?.resetAt !== undefined ? { weeklyResetAt: sevenDay.resetAt } : {}),
193
+ ...(customWindows.length > 0 ? { customWindows } : {}),
194
+ updatedAt: Date.now(),
195
+ };
196
+ return report(provider, "anthropic:oauth-usage", quota);
197
+ }
198
+
199
+ function quotaInfoEntries(modelInfo: Record<string, unknown>): Record<string, unknown>[] {
200
+ const entries: Record<string, unknown>[] = [];
201
+ const add = (value: unknown, tier?: string) => {
202
+ const rec = asRecord(value);
203
+ if (!rec) return;
204
+ entries.push(tier ? { ...rec, tier } : rec);
205
+ };
206
+ const addArray = (value: unknown) => {
207
+ if (!Array.isArray(value)) return;
208
+ for (const entry of value) add(entry);
209
+ };
210
+
211
+ if (Array.isArray(modelInfo.quotaInfo)) addArray(modelInfo.quotaInfo);
212
+ else add(modelInfo.quotaInfo);
213
+ addArray(modelInfo.quotaInfos);
214
+
215
+ const byTier = asRecord(modelInfo.quotaInfoByTier);
216
+ if (byTier) {
217
+ for (const [tier, value] of Object.entries(byTier)) {
218
+ if (Array.isArray(value)) {
219
+ for (const entry of value) add(entry, tier);
220
+ } else {
221
+ add(value, tier);
222
+ }
223
+ }
224
+ }
225
+ return entries;
226
+ }
227
+
228
+ function classifyAntigravityFamily(modelId: string, modelInfo: Record<string, unknown>, quotaInfo: Record<string, unknown>): "Gem" | "Cla" | null {
229
+ const displayName = typeof modelInfo.displayName === "string" ? modelInfo.displayName : "";
230
+ const tier = typeof quotaInfo.tier === "string" ? quotaInfo.tier : "";
231
+ const haystack = `${modelId} ${displayName} ${tier}`.toLowerCase();
232
+ if (haystack.includes("gemini")) return "Gem";
233
+ if (haystack.includes("claude") || haystack.includes("opus") || haystack.includes("sonnet") || haystack.includes("gpt-oss") || haystack.includes("gpt_oss")) return "Cla";
234
+ return null;
235
+ }
236
+
237
+ function antigravityUsedPercent(quotaInfo: Record<string, unknown>): number | undefined {
238
+ const remaining = normalizePercent(toFiniteNumber(quotaInfo.remainingFraction) !== undefined
239
+ ? toFiniteNumber(quotaInfo.remainingFraction)! * 100
240
+ : toFiniteNumber(quotaInfo.remainingPercentage) !== undefined
241
+ ? toFiniteNumber(quotaInfo.remainingPercentage)! * 100
242
+ : undefined);
243
+ if (remaining === undefined) return undefined;
244
+ return normalizePercent(100 - remaining);
245
+ }
246
+
247
+ async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaReport | null> {
248
+ const credential = getCredential("google-antigravity");
249
+ if (!credential?.projectId) return null;
250
+ let accessToken: string;
251
+ try {
252
+ accessToken = await getValidAccessToken("google-antigravity");
253
+ } catch {
254
+ return null;
255
+ }
256
+ const baseUrl = (config.baseUrl || "https://daily-cloudcode-pa.googleapis.com").replace(/\/+$/, "");
257
+ const response = await fetch(`${baseUrl}/v1internal:fetchAvailableModels`, {
258
+ method: "POST",
259
+ headers: {
260
+ Accept: "application/json",
261
+ "Content-Type": "application/json",
262
+ "User-Agent": antigravityUserAgent(),
263
+ Authorization: `Bearer ${accessToken}`,
264
+ },
265
+ body: JSON.stringify({ project: credential.projectId }),
266
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
267
+ });
268
+ if (!response.ok) return null;
269
+ const body = asRecord(await response.json().catch(() => null));
270
+ const models = asRecord(body?.models);
271
+ if (!models) return null;
272
+
273
+ const windows = new Map<string, ProviderQuotaWindow>();
274
+ for (const [modelId, rawModelInfo] of Object.entries(models)) {
275
+ const modelInfo = asRecord(rawModelInfo);
276
+ if (!modelInfo) continue;
277
+ for (const quotaInfo of quotaInfoEntries(modelInfo)) {
278
+ const label = classifyAntigravityFamily(modelId, modelInfo, quotaInfo);
279
+ if (!label || windows.has(label)) continue;
280
+ const percent = antigravityUsedPercent(quotaInfo);
281
+ if (percent === undefined) continue;
282
+ windows.set(label, {
283
+ label,
284
+ percent,
285
+ ...(normalizeResetAt(quotaInfo.resetTime) !== undefined ? { resetAt: normalizeResetAt(quotaInfo.resetTime) } : {}),
286
+ });
287
+ }
288
+ }
289
+
290
+ const customWindows = ["Gem", "Cla"].flatMap(label => {
291
+ const window = windows.get(label);
292
+ return window ? [window] : [];
293
+ });
294
+ if (customWindows.length === 0) return null;
295
+ return report(provider, "google-antigravity:fetchAvailableModels", {
296
+ customWindows,
297
+ updatedAt: Date.now(),
298
+ });
299
+ }
300
+
301
+ async function maybeFetchProviderQuota(
302
+ name: string,
303
+ provider: OcxProviderConfig,
304
+ config: OcxConfig,
305
+ forceRefresh: boolean,
306
+ ): Promise<ProviderQuotaReport | null> {
307
+ if (provider.disabled === true) return null;
308
+ try {
309
+ if (isBuiltInChatGptForwardProvider(name, provider)) return fetchChatGptForwardQuota(config, name, forceRefresh);
310
+ if (provider.authMode === "oauth" && name === "xai") return fetchXaiQuota(name);
311
+ if (provider.authMode === "oauth" && name === "anthropic") return fetchAnthropicQuota(name);
312
+ if (provider.authMode === "oauth" && name === "google-antigravity") return fetchAntigravityQuota(name, provider);
313
+ return null;
314
+ } catch {
315
+ return null;
316
+ }
317
+ }
318
+
319
+ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh = false): Promise<ProviderQuotaResponse> {
320
+ const key = cacheKey(config);
321
+ const now = Date.now();
322
+ if (!forceRefresh && cache && cache.key === key && now - cache.ts < CACHE_TTL_MS) return cache.response;
323
+
324
+ const reports = (await Promise.all(
325
+ Object.entries(config.providers).map(([name, provider]) => maybeFetchProviderQuota(name, provider, config, forceRefresh)),
326
+ )).filter((item): item is ProviderQuotaReport => item !== null);
327
+ const response = { generatedAt: Date.now(), reports };
328
+ cache = { key, ts: now, response };
329
+ return response;
330
+ }
package/src/server.ts CHANGED
@@ -54,6 +54,7 @@ import {
54
54
  type UsageStatus,
55
55
  } from "./usage-log";
56
56
  import { parseRange, summarizeUsage } from "./usage-summary";
57
+ import { fetchProviderQuotaReports } from "./provider-quota";
57
58
  import {
58
59
  appendUsageDebug,
59
60
  isUsageDebugEnabled,
@@ -1805,6 +1806,11 @@ async function handleManagementAPI(req: Request, url: URL, config: OcxConfig): P
1805
1806
  }
1806
1807
  }
1807
1808
 
1809
+ if (url.pathname === "/api/provider-quotas" && req.method === "GET") {
1810
+ const forceRefresh = url.searchParams.get("refresh") === "1" || url.searchParams.get("refresh") === "true";
1811
+ return jsonResponse(await fetchProviderQuotaReports(config, forceRefresh));
1812
+ }
1813
+
1808
1814
  if (url.pathname === "/api/providers" && req.method === "GET") {
1809
1815
  return jsonResponse(Object.entries(config.providers).map(([name, p]) => ({
1810
1816
  name, adapter: p.adapter, baseUrl: publicProviderBaseUrl(p.baseUrl), defaultModel: p.defaultModel,