@coseung2/opencodex 2.8.0-cs.13 → 2.8.0-cs.14

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 (60) hide show
  1. package/gui/dist/assets/index-MUpaVatk.js +67 -0
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +3 -3
  4. package/packages/ocx-notch/README.md +2 -1
  5. package/src/adapters/cursor/discovery.ts +6 -2
  6. package/src/adapters/cursor/effort-map.ts +3 -0
  7. package/src/adapters/google-antigravity-replay.ts +24 -0
  8. package/src/adapters/google.ts +16 -11
  9. package/src/chat/inbound.ts +5 -11
  10. package/src/cli/account-api.ts +9 -1
  11. package/src/cli/account-extended.ts +4 -1
  12. package/src/codex/account-label.ts +14 -1
  13. package/src/codex/account-lifecycle.ts +12 -1
  14. package/src/codex/account-namespaces.ts +21 -0
  15. package/src/codex/account-priority.ts +49 -0
  16. package/src/codex/account-store.ts +2 -1
  17. package/src/codex/auth-api.ts +108 -17
  18. package/src/codex/auth-context.ts +61 -16
  19. package/src/codex/catalog/metadata.ts +34 -12
  20. package/src/codex/catalog/parsing.ts +8 -1
  21. package/src/codex/catalog/provider-fetch.ts +24 -6
  22. package/src/codex/catalog.ts +1 -1
  23. package/src/codex/pool-rotation.ts +51 -4
  24. package/src/codex/quota.ts +154 -35
  25. package/src/codex/routing.ts +133 -33
  26. package/src/codex/warmup.ts +193 -85
  27. package/src/config.ts +84 -1
  28. package/src/lib/bounded-body.ts +13 -6
  29. package/src/lib/bun-stream-caps.ts +5 -6
  30. package/src/lib/redact.ts +13 -0
  31. package/src/oauth/index.ts +79 -12
  32. package/src/oauth/log.ts +3 -1
  33. package/src/oauth/store.ts +31 -8
  34. package/src/providers/antigravity-models.ts +53 -24
  35. package/src/providers/codex-capacity.ts +303 -0
  36. package/src/providers/model-rename-migration.ts +147 -0
  37. package/src/providers/model-rename-startup.ts +29 -0
  38. package/src/providers/quota.ts +126 -16
  39. package/src/providers/registry.ts +258 -38
  40. package/src/responses/parser.ts +19 -12
  41. package/src/responses/spill-store.ts +14 -1
  42. package/src/responses/state.ts +108 -14
  43. package/src/server/index.ts +9 -1
  44. package/src/server/management/logs-usage-routes.ts +1 -0
  45. package/src/server/management/oauth-account-routes.ts +8 -1
  46. package/src/server/relay.ts +10 -42
  47. package/src/server/request-log.ts +42 -1
  48. package/src/server/responses/compact.ts +16 -4
  49. package/src/server/responses/core.ts +217 -59
  50. package/src/server/responses/empty-completion-guard.ts +275 -0
  51. package/src/server/responses/encrypted-payload.ts +54 -39
  52. package/src/server/responses/fetch-helpers.ts +24 -3
  53. package/src/server/responses/ws-upstream.ts +318 -0
  54. package/src/server/sse-frame-buffer.ts +292 -0
  55. package/src/server/ws-bridge.ts +17 -11
  56. package/src/types.ts +8 -0
  57. package/src/usage/log.ts +24 -0
  58. package/src/usage/summary.ts +152 -2
  59. package/vendor/ocx-notch/win32-x64/ocx-notch.exe +0 -0
  60. package/gui/dist/assets/index-BucjyD4I.js +0 -67
@@ -1,4 +1,4 @@
1
- import { fetchMainAccountInfo, listCodexAuthAccounts } from "../codex/auth-api";
1
+ import { effectiveCodexAuthAccountId, fetchMainAccountInfo, listCodexAuthAccounts } from "../codex/auth-api";
2
2
  import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
3
3
  import { resolveEnvValue } from "../config";
4
4
  import { getValidAccessToken, getValidAccessTokenForAccount } from "../oauth";
@@ -15,12 +15,21 @@ import {
15
15
  sweepExpiredOnWrite,
16
16
  type GenerationContext,
17
17
  } from "../lib/state-store-sweeper";
18
+ import { readBoundedResponseBody } from "../lib/bounded-body";
19
+ import {
20
+ aggregateCodexPoolCapacity,
21
+ type CodexCapacityAggregation,
22
+ type CodexCapacityQuota,
23
+ type CodexCapacityWindowAggregation,
24
+ } from "./codex-capacity";
18
25
 
19
26
  /** Match oauth/index REFRESH_SKEW_MS — use stored access without refresh when still fresh. */
20
27
  const ACCOUNT_TOKEN_SKEW_MS = 60_000;
21
28
 
22
29
  const CACHE_TTL_MS = 5 * 60_000;
23
30
  const REQUEST_TIMEOUT_MS = 8_000;
31
+ /** Successful provider quota payloads are small; reject oversized or stalled JSON. */
32
+ export const QUOTA_RESPONSE_MAX_BYTES = 512 * 1024;
24
33
  /** Kiro's getUsageLimits endpoint cold-starts slowly after idle periods (>8s seen); keep a
25
34
  * dedicated, more generous bound so a flaky first hit does not drop the provider into the
26
35
  * "no quota" bucket for the rest of the negative-cache window. */
@@ -87,6 +96,7 @@ export interface ProviderQuotaReport {
87
96
  quota: ProviderQuota;
88
97
  updatedAt: number;
89
98
  reverseEngineered?: boolean;
99
+ aggregation?: CodexCapacityAggregation;
90
100
  }
91
101
 
92
102
  export interface ProviderQuotaResponse {
@@ -167,18 +177,86 @@ function asRecord(value: unknown): Record<string, unknown> | null {
167
177
  return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
168
178
  }
169
179
 
180
+ const QUOTA_JSON_READ_FAILURE = Symbol("quota-json-read-failure");
181
+
182
+ async function readQuotaJson(
183
+ response: Response,
184
+ timeoutMs = REQUEST_TIMEOUT_MS,
185
+ ): Promise<unknown | typeof QUOTA_JSON_READ_FAILURE> {
186
+ const declaredLength = Number(response.headers.get("content-length"));
187
+ if (Number.isFinite(declaredLength) && declaredLength > QUOTA_RESPONSE_MAX_BYTES) {
188
+ try {
189
+ void response.body?.cancel(
190
+ new DOMException("Provider quota response is too large", "QuotaExceededError"),
191
+ ).catch(() => undefined);
192
+ } catch {
193
+ // Best-effort cancellation only.
194
+ }
195
+ return QUOTA_JSON_READ_FAILURE;
196
+ }
197
+
198
+ try {
199
+ const bounded = await readBoundedResponseBody(response, {
200
+ maxBytes: QUOTA_RESPONSE_MAX_BYTES,
201
+ totalTimeoutMs: timeoutMs,
202
+ inactivityTimeoutMs: timeoutMs,
203
+ firstByteTimeoutMs: timeoutMs,
204
+ });
205
+ if (bounded.oversized || bounded.truncated || !bounded.displaySafe) return QUOTA_JSON_READ_FAILURE;
206
+ return JSON.parse(bounded.text) as unknown;
207
+ } catch {
208
+ return QUOTA_JSON_READ_FAILURE;
209
+ }
210
+ }
211
+
212
+ /** Test-only access to the quota reader's deadline and cancellation contract. */
213
+ export async function readProviderQuotaJsonForTests(response: Response, timeoutMs: number): Promise<unknown> {
214
+ const result = await readQuotaJson(response, timeoutMs);
215
+ return result === QUOTA_JSON_READ_FAILURE ? null : result;
216
+ }
217
+
170
218
  function isBuiltInChatGptForwardProvider(name: string, provider: OcxProviderConfig): boolean {
171
219
  return name === OPENAI_CODEX_PROVIDER_ID && isCanonicalOpenAiForwardProvider(provider);
172
220
  }
173
221
 
174
- function report(provider: string, source: string, quota: ProviderQuota): ProviderQuotaReport | null {
175
- if (!hasQuotaRows(quota)) return null;
222
+ function report(
223
+ provider: string,
224
+ source: string,
225
+ quota: ProviderQuota,
226
+ aggregation?: CodexCapacityAggregation,
227
+ ): ProviderQuotaReport | null {
228
+ if (!hasQuotaRows(quota) && !aggregation) return null;
176
229
  return {
177
230
  provider,
178
231
  label: providerLabel(provider),
179
232
  source,
180
233
  quota,
181
234
  updatedAt: quota.updatedAt,
235
+ ...(aggregation ? { aggregation } : {}),
236
+ };
237
+ }
238
+
239
+ function publicCapacityWindow(window: CodexCapacityWindowAggregation): CodexCapacityWindowAggregation {
240
+ const { totalWeight: _totalWeight, consumedWeight: _consumedWeight, remainingWeight: _remainingWeight, ...safe } = window;
241
+ return safe as CodexCapacityWindowAggregation;
242
+ }
243
+
244
+ function publicCapacityAggregation(
245
+ aggregation: CodexCapacityAggregation,
246
+ presentation: NonNullable<CodexCapacityAggregation["presentation"]>,
247
+ ): CodexCapacityAggregation {
248
+ return {
249
+ ...aggregation,
250
+ presentation,
251
+ ...(aggregation.fiveHour ? { fiveHour: publicCapacityWindow(aggregation.fiveHour) } : {}),
252
+ ...(aggregation.weekly ? { weekly: publicCapacityWindow(aggregation.weekly) } : {}),
253
+ ...(aggregation.monthly ? { monthly: publicCapacityWindow(aggregation.monthly) } : {}),
254
+ ...(aggregation.customWindows ? {
255
+ customWindows: aggregation.customWindows.map(window => ({
256
+ label: window.label,
257
+ ...publicCapacityWindow(window),
258
+ })),
259
+ } : {}),
182
260
  };
183
261
  }
184
262
 
@@ -194,12 +272,44 @@ async function fetchChatGptForwardQuota(
194
272
  return quota ? report(provider, "chatgpt:wham", quota) : null;
195
273
  }
196
274
  const accounts = await listCodexAuthAccounts(config, forceRefresh);
197
- const activeId = config.activeCodexAccountId || MAIN_CODEX_ACCOUNT_ID;
198
- const active = accounts.find(account => account.id === activeId)
275
+ const activeId = effectiveCodexAuthAccountId(config);
276
+ const capacityAccounts = accounts.map(account => ({ ...account, active: account.id === activeId }));
277
+ const active = capacityAccounts.find(account => account.active)
199
278
  ?? accounts.find(account => account.id === MAIN_CODEX_ACCOUNT_ID)
200
279
  ?? accounts[0];
201
- const quota = active?.quota ? { ...active.quota, updatedAt: active.quota.updatedAt ?? Date.now() } as ProviderQuota : null;
202
- return quota ? report(provider, "chatgpt:wham", quota) : null;
280
+ const now = Date.now();
281
+ const capacity = aggregateCodexPoolCapacity(capacityAccounts, now);
282
+ if (capacity.aggregation && capacity.quota) {
283
+ return report(
284
+ provider,
285
+ "chatgpt:wham",
286
+ capacity.quota as ProviderQuota,
287
+ publicCapacityAggregation(capacity.aggregation, "aggregate"),
288
+ );
289
+ }
290
+ const activeUsable = !!active && !active.paused && active.needsReauth !== true;
291
+ const quota = activeUsable && active?.quota
292
+ ? { ...active.quota, updatedAt: active.quota.updatedAt ?? now } as CodexCapacityQuota
293
+ : null;
294
+ if (quota && now - quota.updatedAt <= 30 * 60_000) {
295
+ return report(
296
+ provider,
297
+ "chatgpt:wham",
298
+ quota as ProviderQuota,
299
+ capacity.aggregation
300
+ ? publicCapacityAggregation(capacity.aggregation, "effective-account-fallback")
301
+ : undefined,
302
+ );
303
+ }
304
+ if (capacity.aggregation) {
305
+ return report(
306
+ provider,
307
+ "chatgpt:wham",
308
+ { updatedAt: now },
309
+ publicCapacityAggregation(capacity.aggregation, "coverage-only"),
310
+ );
311
+ }
312
+ return null;
203
313
  }
204
314
 
205
315
  async function fetchXaiQuota(provider: string): Promise<ProviderQuotaReport | null> {
@@ -219,7 +329,7 @@ async function fetchXaiQuota(provider: string): Promise<ProviderQuotaReport | nu
219
329
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
220
330
  });
221
331
  if (!response.ok) return null;
222
- const body = asRecord(await response.json().catch(() => null));
332
+ const body = asRecord(await readQuotaJson(response));
223
333
  const config = asRecord(body?.config);
224
334
  const currentPeriod = asRecord(config?.currentPeriod);
225
335
  // A weekly window is the meter contract; other shapes (spend-cap etc.) fail closed.
@@ -293,7 +403,7 @@ async function fetchKiroUsageQuota(
293
403
  signal: AbortSignal.timeout(KIRO_QUOTA_TIMEOUT_MS),
294
404
  });
295
405
  if (!response.ok) return null;
296
- const body = asRecord(await response.json().catch(() => null));
406
+ const body = asRecord(await readQuotaJson(response));
297
407
  if (!body) return null;
298
408
  const credit = kiroCreditRow(body);
299
409
  if (!credit) return null;
@@ -333,7 +443,7 @@ async function fetchAnthropicUsageQuota(accessToken: string): Promise<ProviderQu
333
443
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
334
444
  });
335
445
  if (!response.ok) return null;
336
- const body = asRecord(await response.json().catch(() => null));
446
+ const body = asRecord(await readQuotaJson(response));
337
447
  if (!body) return null;
338
448
  const fiveHour = parseClaudeBucket(body.five_hour);
339
449
  const sevenDay = parseClaudeBucket(body.seven_day);
@@ -762,7 +872,7 @@ async function fetchKimiQuota(provider: string, config: OcxProviderConfig): Prom
762
872
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
763
873
  });
764
874
  if (!response.ok) return null;
765
- const quota = parseKimiQuotaPayload(await response.json().catch(() => null));
875
+ const quota = parseKimiQuotaPayload(await readQuotaJson(response));
766
876
  return quota ? report(provider, "kimi:usages", quota) : null;
767
877
  }
768
878
 
@@ -845,7 +955,7 @@ async function fetchOpencodeGoUsageApi(apiKey: string | undefined): Promise<{
845
955
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
846
956
  });
847
957
  if (!response.ok) return null;
848
- const body = asRecord(await response.json().catch(() => null));
958
+ const body = asRecord(await readQuotaJson(response));
849
959
  const usage = asRecord(body?.usage);
850
960
  if (!usage) return null;
851
961
  const parse = (raw: unknown): OpencodeGoUsageWindow => {
@@ -1023,7 +1133,7 @@ async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport |
1023
1133
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1024
1134
  });
1025
1135
  if (periodRes.ok) {
1026
- const body = asRecord(await periodRes.json().catch(() => null));
1136
+ const body = asRecord(await readQuotaJson(periodRes));
1027
1137
  const planUsage = asRecord(body?.planUsage);
1028
1138
  if (planUsage) {
1029
1139
  const resetAt = normalizeResetAt(body?.billingCycleEnd ?? planUsage.billingCycleEnd ?? body?.periodEnd);
@@ -1085,7 +1195,7 @@ async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport |
1085
1195
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1086
1196
  });
1087
1197
  if (summaryRes.ok) {
1088
- const body = asRecord(await summaryRes.json().catch(() => null));
1198
+ const body = asRecord(await readQuotaJson(summaryRes));
1089
1199
  const individual = asRecord(body?.individualUsage);
1090
1200
  const plan = asRecord(individual?.plan);
1091
1201
  if (plan) {
@@ -1114,7 +1224,7 @@ async function fetchCursorQuota(provider: string): Promise<ProviderQuotaReport |
1114
1224
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1115
1225
  });
1116
1226
  if (!response.ok) return null;
1117
- const body = asRecord(await response.json().catch(() => null));
1227
+ const body = asRecord(await readQuotaJson(response));
1118
1228
  if (!body) return null;
1119
1229
 
1120
1230
  // Prefer the gpt-4 bucket (historical "fast requests"); else first model with used+limit.
@@ -1228,7 +1338,7 @@ async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig
1228
1338
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
1229
1339
  });
1230
1340
  if (!response.ok) return null;
1231
- const body = asRecord(await response.json().catch(() => null));
1341
+ const body = asRecord(await readQuotaJson(response));
1232
1342
  const models = asRecord(body?.models);
1233
1343
  if (!models) return null;
1234
1344