@bitkyc08/opencodex 2.5.2 → 2.5.3

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.
@@ -16,8 +16,8 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-BP9hgC9S.js"></script>
20
- <link rel="stylesheet" crossorigin href="/assets/index-xJdeQjzZ.css">
19
+ <script type="module" crossorigin src="/assets/index-DpmZZGXn.js"></script>
20
+ <link rel="stylesheet" crossorigin href="/assets/index-CJF4_jax.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.5.2",
3
+ "version": "2.5.3",
4
4
  "description": "Universal provider proxy for OpenAI Codex — use any LLM with Codex CLI/App/SDK",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -150,6 +150,7 @@ async function fetchPoolAccountQuota(accountId: string, forceRefresh = false): P
150
150
  quota.fiveHourResetAt,
151
151
  quota.monthlyPercent,
152
152
  quota.monthlyResetAt,
153
+ quota.resetCredits,
153
154
  );
154
155
  return { quota: getAccountQuota(accountId), needsReauth: false };
155
156
  } catch (e) {
@@ -296,6 +297,98 @@ export async function handleCodexAuthAPI(
296
297
  return jsonResponse({ quotas });
297
298
  }
298
299
 
300
+ if (url.pathname === "/api/codex-auth/reset-credits" && req.method === "GET") {
301
+ const accountId = url.searchParams.get("accountId");
302
+ if (!accountId) return jsonResponse({ error: "accountId required" }, 400);
303
+
304
+ const isMain = accountId === "__main__";
305
+ let accessToken: string;
306
+ let chatgptAccountId: string;
307
+
308
+ try {
309
+ if (isMain) {
310
+ const tokens = readCodexTokens();
311
+ if (!tokens) return jsonResponse({ error: "Main Codex account not logged in" }, 401);
312
+ accessToken = tokens.access_token;
313
+ chatgptAccountId = tokens.account_id;
314
+ } else {
315
+ const cred = await getValidCodexToken(accountId);
316
+ accessToken = cred.accessToken;
317
+ chatgptAccountId = cred.chatgptAccountId;
318
+ }
319
+
320
+ const resp = await fetch(
321
+ "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits",
322
+ {
323
+ headers: {
324
+ Authorization: `Bearer ${accessToken}`,
325
+ "ChatGPT-Account-Id": chatgptAccountId,
326
+ },
327
+ signal: AbortSignal.timeout(8000),
328
+ },
329
+ );
330
+ if (!resp.ok) {
331
+ const text = await resp.text().catch(() => "");
332
+ return jsonResponse({ error: `Upstream error ${resp.status}`, detail: text }, resp.status);
333
+ }
334
+ return jsonResponse(await resp.json());
335
+ } catch (e) {
336
+ return jsonResponse({ error: String(e) }, 500);
337
+ }
338
+ }
339
+
340
+ if (url.pathname === "/api/codex-auth/reset-credits/consume" && req.method === "POST") {
341
+ const body = (await req.json().catch(() => ({}))) as { accountId?: string };
342
+ if (!body.accountId) return jsonResponse({ error: "accountId required" }, 400);
343
+
344
+ const isMain = body.accountId === "__main__";
345
+ let accessToken: string;
346
+ let chatgptAccountId: string;
347
+
348
+ try {
349
+ if (isMain) {
350
+ const tokens = readCodexTokens();
351
+ if (!tokens) return jsonResponse({ error: "Main Codex account not logged in" }, 401);
352
+ accessToken = tokens.access_token;
353
+ chatgptAccountId = tokens.account_id;
354
+ } else {
355
+ const cred = await getValidCodexToken(body.accountId);
356
+ accessToken = cred.accessToken;
357
+ chatgptAccountId = cred.chatgptAccountId;
358
+ }
359
+
360
+ const idempotencyKey = crypto.randomUUID();
361
+ const resp = await fetch(
362
+ "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume",
363
+ {
364
+ method: "POST",
365
+ headers: {
366
+ Authorization: `Bearer ${accessToken}`,
367
+ "ChatGPT-Account-Id": chatgptAccountId,
368
+ "Content-Type": "application/json",
369
+ },
370
+ body: JSON.stringify({ redeem_request_id: idempotencyKey }),
371
+ signal: AbortSignal.timeout(10_000),
372
+ },
373
+ );
374
+ if (!resp.ok) {
375
+ const text = await resp.text().catch(() => "");
376
+ return jsonResponse({ error: `Upstream error ${resp.status}`, detail: text }, resp.status);
377
+ }
378
+ const result = (await resp.json()) as { code: string };
379
+ if (result.code === "reset") {
380
+ if (isMain) {
381
+ await fetchMainAccountInfo(true);
382
+ } else {
383
+ await fetchPoolAccountQuota(body.accountId, true);
384
+ }
385
+ }
386
+ return jsonResponse(result);
387
+ } catch (e) {
388
+ return jsonResponse({ error: String(e) }, 500);
389
+ }
390
+ }
391
+
299
392
  if (url.pathname === "/api/codex-auth/login" && req.method === "POST") {
300
393
  const body = (await req.json().catch(() => ({}))) as { id?: string };
301
394
  const requestedAccountId = body.id?.trim();
@@ -374,6 +467,7 @@ export async function handleCodexAuthAPI(
374
467
  quota.fiveHourResetAt,
375
468
  quota.monthlyPercent,
376
469
  quota.monthlyResetAt,
470
+ quota.resetCredits,
377
471
  );
378
472
  }
379
473
 
@@ -244,16 +244,16 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
244
244
 
245
245
  writeFileSync(CODEX_CONFIG_PATH, content, "utf-8");
246
246
  writeFileSync(CODEX_PROFILE_PATH, buildProfileFile(port, catalogPath), "utf-8");
247
- const history = config?.syncResumeHistory === true
247
+ const history = config?.syncResumeHistory !== false
248
248
  ? syncCodexHistoryProvider("opencodex")
249
249
  : { rows: 0, files: 0 };
250
250
 
251
251
  const catalogMessage = catalogPath
252
252
  ? ` Codex model catalog: ${catalogPath}\n`
253
253
  : ` Codex model catalog not injected because no opencodex catalog file exists yet.\n`;
254
- const historyMessage = config?.syncResumeHistory === true
254
+ const historyMessage = config?.syncResumeHistory !== false
255
255
  ? ` Codex resume history: ${history.rows} thread(s) made visible for opencodex; originals backed up for restore.\n`
256
- : ` Codex resume history: left unchanged. Existing OpenAI and opencodex exec project chats may be hidden while opencodex is active; set syncResumeHistory=true to enable the reversible compatibility remap.\n`;
256
+ : ` Codex resume history: left unchanged (syncResumeHistory=false).\n`;
257
257
  return {
258
258
  success: true,
259
259
  message: `Injected opencodex as default provider into Codex config.\n` +
@@ -5,6 +5,7 @@ export type StoredAccountQuota = {
5
5
  weeklyResetAt?: number;
6
6
  fiveHourResetAt?: number;
7
7
  monthlyResetAt?: number;
8
+ resetCredits?: number;
8
9
  updatedAt: number;
9
10
  };
10
11
 
@@ -16,6 +17,9 @@ export type WhamUsageResponse = {
16
17
  secondary_window?: { used_percent?: number; reset_at?: number };
17
18
  tertiary_window?: { used_percent?: number; reset_at?: number };
18
19
  };
20
+ rate_limit_reset_credits?: {
21
+ available_count: number;
22
+ } | null;
19
23
  };
20
24
 
21
25
  const accountQuota = new Map<string, StoredAccountQuota>();
@@ -55,12 +59,13 @@ export function updateAccountQuota(
55
59
  fiveHourResetAt?: unknown,
56
60
  monthly?: unknown,
57
61
  monthlyResetAt?: unknown,
62
+ resetCredits?: number,
58
63
  ): void {
59
64
  const existing = accountQuota.get(accountId);
60
65
  const nextWeekly = normalizeUsagePercent(weekly);
61
66
  const nextFiveHour = normalizeUsagePercent(fiveHour);
62
67
  const nextMonthly = normalizeUsagePercent(monthly);
63
- if (nextWeekly === undefined && nextFiveHour === undefined && nextMonthly === undefined) return;
68
+ if (nextWeekly === undefined && nextFiveHour === undefined && nextMonthly === undefined && resetCredits === undefined) return;
64
69
 
65
70
  const quota: StoredAccountQuota = {
66
71
  ...(existing?.weeklyPercent !== undefined ? { weeklyPercent: existing.weeklyPercent } : {}),
@@ -69,6 +74,7 @@ export function updateAccountQuota(
69
74
  ...(existing?.weeklyResetAt !== undefined ? { weeklyResetAt: existing.weeklyResetAt } : {}),
70
75
  ...(existing?.fiveHourResetAt !== undefined ? { fiveHourResetAt: existing.fiveHourResetAt } : {}),
71
76
  ...(existing?.monthlyResetAt !== undefined ? { monthlyResetAt: existing.monthlyResetAt } : {}),
77
+ ...(existing?.resetCredits !== undefined ? { resetCredits: existing.resetCredits } : {}),
72
78
  updatedAt: Date.now(),
73
79
  };
74
80
 
@@ -87,6 +93,7 @@ export function updateAccountQuota(
87
93
  quota.monthlyPercent = nextMonthly;
88
94
  if (nextMonthlyResetAt !== undefined) quota.monthlyResetAt = nextMonthlyResetAt;
89
95
  }
96
+ if (resetCredits !== undefined) quota.resetCredits = resetCredits;
90
97
 
91
98
  accountQuota.set(accountId, quota);
92
99
  }
@@ -105,7 +112,14 @@ export function clearAccountQuota(accountId?: string): void {
105
112
  }
106
113
 
107
114
  export function parseUsageQuota(data: WhamUsageResponse): Omit<StoredAccountQuota, "updatedAt"> | null {
108
- if (!data.rate_limit) return null;
115
+ const resetCredits = typeof data.rate_limit_reset_credits?.available_count === "number"
116
+ ? data.rate_limit_reset_credits.available_count
117
+ : undefined;
118
+
119
+ if (!data.rate_limit) {
120
+ return resetCredits !== undefined ? { resetCredits } : null;
121
+ }
122
+
109
123
  const quota: Omit<StoredAccountQuota, "updatedAt"> = {};
110
124
  const weeklyPercent = normalizeUsagePercent(data.rate_limit.secondary_window?.used_percent);
111
125
  const fiveHourPercent = normalizeUsagePercent(data.rate_limit.primary_window?.used_percent);
@@ -125,6 +139,7 @@ export function parseUsageQuota(data: WhamUsageResponse): Omit<StoredAccountQuot
125
139
  quota.monthlyPercent = monthlyPercent;
126
140
  if (monthlyResetAt !== undefined) quota.monthlyResetAt = monthlyResetAt;
127
141
  }
142
+ if (resetCredits !== undefined) quota.resetCredits = resetCredits;
128
143
 
129
- return hasKnownQuotaValue(quota) ? quota : null;
144
+ return hasKnownQuotaValue(quota) || resetCredits !== undefined ? quota : null;
130
145
  }
package/src/config.ts CHANGED
@@ -37,7 +37,7 @@ const providerConfigSchema = z.object({
37
37
  const configSchema = z.object({
38
38
  port: z.number().int().min(0).max(65535).default(10100),
39
39
  providers: z.record(z.string(), providerConfigSchema),
40
- defaultProvider: z.string().min(1),
40
+ defaultProvider: z.string().min(1).default("openai"),
41
41
  }).passthrough().superRefine((config, ctx) => {
42
42
  if (Object.keys(config.providers).length > 0 && !(config.defaultProvider in config.providers)) {
43
43
  ctx.addIssue({
@@ -93,7 +93,26 @@ export function loadConfig(): OcxConfig {
93
93
  }
94
94
  try {
95
95
  const raw = readFileSync(configPath, "utf-8");
96
- return configSchema.parse(JSON.parse(raw)) as OcxConfig;
96
+ const parsed = JSON.parse(raw);
97
+ const result = configSchema.safeParse(parsed);
98
+ if (result.success) return result.data as OcxConfig;
99
+ // Schema validation failed — merge defaults into the raw object instead of
100
+ // discarding it entirely, so pool accounts and providers survive a missing
101
+ // field like defaultProvider.
102
+ const defaults = getDefaultConfig();
103
+ const merged = { ...defaults, ...parsed };
104
+ // Ensure providers from both sides survive
105
+ if (parsed.providers && defaults.providers) {
106
+ merged.providers = { ...defaults.providers, ...parsed.providers };
107
+ }
108
+ const retryResult = configSchema.safeParse(merged);
109
+ if (retryResult.success) {
110
+ warnConfigRepaired(configPath, result.error);
111
+ return retryResult.data as OcxConfig;
112
+ }
113
+ // Merge couldn't fix it — truly broken config
114
+ warnAndBackupInvalidConfig(configPath, result.error);
115
+ return getDefaultConfig();
97
116
  } catch (error) {
98
117
  warnAndBackupInvalidConfig(configPath, error);
99
118
  return getDefaultConfig();
@@ -181,6 +200,13 @@ export function removePid(): void {
181
200
  } catch { /* ignore */ }
182
201
  }
183
202
 
203
+ function warnConfigRepaired(configPath: string, error: z.ZodError): void {
204
+ if (warnedConfigFallbacks.has(configPath)) return;
205
+ warnedConfigFallbacks.add(configPath);
206
+ const fields = error.issues.map(i => i.path.join(".") || "config").join(", ");
207
+ console.error(`opencodex config at ${configPath}: repaired missing field(s) [${fields}] with defaults. Your providers and accounts are preserved.`);
208
+ }
209
+
184
210
  function warnAndBackupInvalidConfig(configPath: string, error: unknown): void {
185
211
  if (warnedConfigFallbacks.has(configPath)) return;
186
212
  warnedConfigFallbacks.add(configPath);
@@ -45,6 +45,8 @@ export type ProviderConfigSeed = Pick<
45
45
  >;
46
46
 
47
47
 
48
+ const OLLAMA_REASONING_MAP: Record<string, string> = { xhigh: "max" };
49
+
48
50
  const ZAI_GLM_52_MODELS = ["glm-5.2", "glm-5.2[1m]"];
49
51
  const ZAI_GLM_52_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"];
50
52
  const ZAI_GLM_52_REASONING_MAP: Record<string, string> = {
@@ -244,7 +246,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
244
246
  { id: "groq", label: "Groq", adapter: "openai-chat", baseUrl: "https://api.groq.com/openai/v1", authKind: "key", featured: true, dashboardUrl: "https://console.groq.com/keys" },
245
247
  { id: "google", label: "Google Gemini", adapter: "google", baseUrl: "https://generativelanguage.googleapis.com", authKind: "key", featured: true, dashboardUrl: "https://aistudio.google.com/apikey", defaultModel: "gemini-3-pro", jawcodeBundle: "google", extraMetadataAliases: ["gemini"] },
246
248
  { id: "azure-openai", label: "Azure OpenAI", adapter: "azure-openai", baseUrl: "https://{resource}.openai.azure.com/openai", authKind: "key", featured: true, dashboardUrl: "https://portal.azure.com" },
247
- { id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", featured: true, note: "Local — key usually blank" },
249
+ { id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", featured: true, note: "Local — key usually blank", reasoningEffortMap: OLLAMA_REASONING_MAP },
248
250
  { id: "vllm", label: "vLLM (local)", adapter: "openai-chat", baseUrl: "http://localhost:8000/v1", authKind: "local", featured: true, note: "Local — key usually blank" },
249
251
  { id: "lm-studio", label: "LM Studio (local)", adapter: "openai-chat", baseUrl: "http://localhost:1234/v1", authKind: "local", featured: true, note: "Local — no key needed" },
250
252
  { id: "deepseek", label: "DeepSeek", baseUrl: "https://api.deepseek.com", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://platform.deepseek.com/api_keys", models: ["deepseek-chat", "deepseek-reasoner"], defaultModel: "deepseek-chat" },
@@ -292,6 +294,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
292
294
  adapter: "openai-chat",
293
295
  authKind: "key",
294
296
  dashboardUrl: "https://ollama.com/settings/keys",
297
+ reasoningEffortMap: OLLAMA_REASONING_MAP,
295
298
  models: ["glm-5.2", "deepseek-v4-pro", "qwen3-coder", "gpt-oss:120b", "kimi-k2.6", "minimax-m3", "qwen3.5", "gemma4"],
296
299
  defaultModel: "glm-5.2",
297
300
  noVisionModels: [
package/src/types.ts CHANGED
@@ -192,8 +192,8 @@ export interface OcxConfig {
192
192
  /**
193
193
  * Compatibility mode: temporarily rewrite Codex resume-history metadata while the proxy is active
194
194
  * so Codex App can show old OpenAI chats and opencodex-created exec chats under its default
195
- * interactive-source/provider filters. Disabled by default because it mutates Codex's local
196
- * thread index; originals are backed up and restored by `ocx stop` / `ocx restore`.
195
+ * interactive-source/provider filters. Default true; originals are backed up and restored by
196
+ * `ocx stop` / `ocx restore`. Set false to opt out of history remapping.
197
197
  */
198
198
  syncResumeHistory?: boolean;
199
199
  /** Freshness window (ms) for the per-provider live `/models` cache. Defaults to 5 min. */