@bitkyc08/opencodex 2.40.0 → 2.41.0

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 (72) hide show
  1. package/README.md +4 -0
  2. package/gui/dist/assets/index-B2YjLA-i.css +1 -0
  3. package/gui/dist/assets/{index-BHe2rl_C.js → index-aPup8CKb.js} +20 -20
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/meta.svg +1 -0
  6. package/package.json +4 -3
  7. package/src/adapters/cursor/catalog.ts +71 -29
  8. package/src/adapters/cursor/claude-id.ts +76 -0
  9. package/src/adapters/cursor/discovery.ts +16 -3
  10. package/src/adapters/cursor/effort-map.ts +27 -12
  11. package/src/adapters/google.ts +39 -2
  12. package/src/adapters/openai-responses.ts +14 -1
  13. package/src/cli/claude.ts +11 -2
  14. package/src/cli/connect.ts +7 -1
  15. package/src/cli/registry.ts +1 -1
  16. package/src/cli/status.ts +19 -4
  17. package/src/client/connect.ts +5 -1
  18. package/src/client/hub-client.ts +29 -5
  19. package/src/clients/config-export.ts +12 -2
  20. package/src/codex/catalog/aggregation.ts +8 -0
  21. package/src/codex/catalog/metadata.ts +5 -0
  22. package/src/codex/catalog/parsing.ts +2 -0
  23. package/src/codex/catalog/provider-fetch.ts +163 -26
  24. package/src/codex/catalog.ts +1 -1
  25. package/src/codex/convergence-types.ts +1 -0
  26. package/src/codex/desired-state.ts +18 -11
  27. package/src/combos/failover.ts +185 -6
  28. package/src/combos/index.ts +6 -0
  29. package/src/combos/resolve.ts +43 -6
  30. package/src/config.ts +5 -1
  31. package/src/generated/compatibility-version.json +85 -61
  32. package/src/generated/model-metadata.ts +1 -1
  33. package/src/grok/sync.ts +10 -2
  34. package/src/integrations/cursor-effort-table.ts +143 -0
  35. package/src/integrations/state.ts +1 -1
  36. package/src/integrations/writer.ts +2 -2
  37. package/src/lib/app-owned-memory-stores.ts +27 -8
  38. package/src/lib/bounded-body.ts +16 -1
  39. package/src/oauth/generic-account-failover.ts +2 -2
  40. package/src/oauth/index.ts +11 -0
  41. package/src/oauth/meta-muse.ts +235 -0
  42. package/src/providers/antigravity-models.ts +71 -13
  43. package/src/providers/command-code-efforts.ts +15 -0
  44. package/src/providers/free-directory.ts +4 -1
  45. package/src/providers/registry.ts +116 -8
  46. package/src/responses/code-mode-helper-compat.ts +4 -1
  47. package/src/responses/state.ts +5 -4
  48. package/src/server/auth-cors.ts +241 -56
  49. package/src/server/chat-completions.ts +11 -2
  50. package/src/server/chat-native.ts +30 -4
  51. package/src/server/claude-messages.ts +17 -3
  52. package/src/server/effort-row.ts +131 -0
  53. package/src/server/index.ts +67 -38
  54. package/src/server/management/api-key-rotation.ts +2 -1
  55. package/src/server/management/api-key-usage.ts +97 -43
  56. package/src/server/management/context.ts +3 -0
  57. package/src/server/management/cursor-integration-routes.ts +36 -7
  58. package/src/server/management/logs-usage-routes.ts +64 -87
  59. package/src/server/management/provider-routes.ts +218 -1
  60. package/src/server/management/route-registry.ts +1 -0
  61. package/src/server/management/usage-aggregate-cache.ts +464 -0
  62. package/src/server/management/usage-summary-cache.ts +4 -0
  63. package/src/server/models-capabilities.ts +60 -5
  64. package/src/server/responses/core.ts +61 -7
  65. package/src/types/config.ts +10 -1
  66. package/src/types/tools.ts +12 -9
  67. package/src/usage/expected-prices.ts +43 -7
  68. package/src/usage/ledger-scanner.ts +448 -0
  69. package/src/usage/log.ts +1 -1
  70. package/src/usage/summary.ts +915 -655
  71. package/src/web-search/index.ts +1 -1
  72. package/gui/dist/assets/index-CJSb3HPe.css +0 -1
@@ -13,6 +13,28 @@ interface TargetCooldown {
13
13
 
14
14
  const DEFAULT_COOLDOWN_MS = 60_000;
15
15
  const MAX_COOLDOWN_MS = 10 * 60_000;
16
+ /** Short cooldown for request-rate 429s (for example provider code 1302) that omit Retry-After. */
17
+ export const COMBO_REQUEST_RATE_COOLDOWN_MS = 5_000;
18
+
19
+ const QUOTA_LIMIT_CODES = new Set([
20
+ "1308",
21
+ "1310",
22
+ "1316",
23
+ "1317",
24
+ "1318",
25
+ "1319",
26
+ "1320",
27
+ "1321",
28
+ "insufficient_quota",
29
+ ]);
30
+ const TRANSIENT_REQUEST_RATE_CODES = new Set(["1302", "1305"]);
31
+ const IMF_FIXDATE_RE = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\d{2}) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4}) (\d{2}):(\d{2}):(\d{2}) GMT$/i;
32
+ const RFC850_DATE_RE = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\d{2})-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d{2}) (\d{2}):(\d{2}):(\d{2}) GMT$/i;
33
+ const ASCTIME_DATE_RE = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ( \d|\d{2}) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/i;
34
+ const HTTP_MONTH_INDEX: Record<string, number> = {
35
+ jan: 0, feb: 1, mar: 2, apr: 3, may: 4, jun: 5,
36
+ jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11,
37
+ };
16
38
 
17
39
  /** Map<`${comboId}\0${provider/model}`, TargetCooldown> */
18
40
  const targetCooldowns = new Map<string, TargetCooldown>();
@@ -26,22 +48,92 @@ function cooldownMapKey(
26
48
  return `${comboId}\0${targetKey(target)}`;
27
49
  }
28
50
 
51
+ function parseUtcDateParts(
52
+ year: number,
53
+ monthName: string,
54
+ day: number,
55
+ hour: number,
56
+ minute: number,
57
+ second: number,
58
+ ): number | undefined {
59
+ const month = HTTP_MONTH_INDEX[monthName.toLowerCase()];
60
+ if (month === undefined) return undefined;
61
+ const timestamp = Date.UTC(year, month, day, hour, minute, second);
62
+ const parsed = new Date(timestamp);
63
+ return parsed.getUTCFullYear() === year
64
+ && parsed.getUTCMonth() === month
65
+ && parsed.getUTCDate() === day
66
+ && parsed.getUTCHours() === hour
67
+ && parsed.getUTCMinutes() === minute
68
+ && parsed.getUTCSeconds() === second
69
+ ? timestamp
70
+ : undefined;
71
+ }
72
+
73
+ function parseHttpDate(value: string, now: number): number | undefined {
74
+ const imf = IMF_FIXDATE_RE.exec(value);
75
+ if (imf) {
76
+ return parseUtcDateParts(
77
+ Number(imf[3]), imf[2]!, Number(imf[1]),
78
+ Number(imf[4]), Number(imf[5]), Number(imf[6]),
79
+ );
80
+ }
81
+ const rfc850 = RFC850_DATE_RE.exec(value);
82
+ if (rfc850) {
83
+ const current = new Date(now);
84
+ const currentYear = current.getUTCFullYear();
85
+ const month = HTTP_MONTH_INDEX[rfc850[2]!.toLowerCase()];
86
+ if (month === undefined) return undefined;
87
+ let year = Math.floor(currentYear / 100) * 100 + Number(rfc850[3]);
88
+ const yearDelta = year - currentYear;
89
+ const candidateTimeOfYear = Date.UTC(
90
+ 2000, month, Number(rfc850[1]),
91
+ Number(rfc850[4]), Number(rfc850[5]), Number(rfc850[6]),
92
+ );
93
+ const currentTimeOfYear = Date.UTC(
94
+ 2000, current.getUTCMonth(), current.getUTCDate(),
95
+ current.getUTCHours(), current.getUTCMinutes(), current.getUTCSeconds(),
96
+ current.getUTCMilliseconds(),
97
+ );
98
+ if (yearDelta < -50 || (yearDelta === -50 && candidateTimeOfYear < currentTimeOfYear)) {
99
+ year += 100;
100
+ } else if (yearDelta > 50 || (yearDelta === 50 && candidateTimeOfYear > currentTimeOfYear)) {
101
+ year -= 100;
102
+ }
103
+ return parseUtcDateParts(
104
+ year, rfc850[2]!, Number(rfc850[1]),
105
+ Number(rfc850[4]), Number(rfc850[5]), Number(rfc850[6]),
106
+ );
107
+ }
108
+ const asctime = ASCTIME_DATE_RE.exec(value);
109
+ if (!asctime) return undefined;
110
+ return parseUtcDateParts(
111
+ Number(asctime[6]), asctime[1]!, Number(asctime[2]),
112
+ Number(asctime[3]), Number(asctime[4]), Number(asctime[5]),
113
+ );
114
+ }
115
+
29
116
  export function parseRetryAfterMs(
30
117
  value: string | null | undefined,
31
118
  now = Date.now(),
119
+ options?: { preserveImmediate?: boolean },
32
120
  ): number | undefined {
33
121
  const text = value?.trim();
34
122
  if (!text) return undefined;
35
123
  if (/^\d+(?:\.\d+)?$/.test(text)) {
36
124
  const seconds = Number(text);
37
- if (Number.isFinite(seconds) && seconds > 0) {
125
+ if (
126
+ Number.isFinite(seconds)
127
+ && (seconds > 0 || (options?.preserveImmediate && seconds === 0))
128
+ ) {
38
129
  return Math.min(Math.max(Math.ceil(seconds * 1000), 1), MAX_COOLDOWN_MS);
39
130
  }
40
131
  }
41
- const timestamp = Date.parse(text);
42
- if (!Number.isFinite(timestamp)) return undefined;
132
+ const timestamp = parseHttpDate(text, now);
133
+ if (timestamp === undefined) return undefined;
43
134
  const delay = timestamp - now;
44
- return delay > 0 ? Math.min(delay, MAX_COOLDOWN_MS) : undefined;
135
+ if (delay > 0) return Math.min(delay, MAX_COOLDOWN_MS);
136
+ return options?.preserveImmediate ? 1 : undefined;
45
137
  }
46
138
 
47
139
  export function isComboTargetInCooldown(
@@ -59,10 +151,59 @@ export function isComboTargetInCooldown(
59
151
  return true;
60
152
  }
61
153
 
154
+ export function isTransientRequestRateLimit(input: {
155
+ status?: number;
156
+ code?: string | null;
157
+ message?: string;
158
+ }): boolean {
159
+ if (isProviderScopedQuotaCap(input.status, input.message ?? "", input.code)) return false;
160
+ const code = (input.code ?? "").trim().toLowerCase().replaceAll("-", "_");
161
+ if (QUOTA_LIMIT_CODES.has(code)) return false;
162
+ if (TRANSIENT_REQUEST_RATE_CODES.has(code)) return true;
163
+ const text = (input.message ?? "").toLowerCase();
164
+ if (
165
+ text.includes("usage limit reached")
166
+ || text.includes("insufficient_quota")
167
+ || text.includes("quota exhausted")
168
+ ) {
169
+ return false;
170
+ }
171
+ return text.includes("rate limit reached for requests");
172
+ }
173
+
174
+ export function remainingComboCooldownMs(comboId: string, now = Date.now()): number | undefined {
175
+ const prefix = `${comboId}\0`;
176
+ let soonest: number | undefined;
177
+ for (const [key, cooldown] of targetCooldowns) {
178
+ if (!key.startsWith(prefix)) continue;
179
+ const remaining = cooldown.cooldownUntil - now;
180
+ if (remaining <= 0) {
181
+ targetCooldowns.delete(key);
182
+ continue;
183
+ }
184
+ if (soonest === undefined || remaining < soonest) soonest = remaining;
185
+ }
186
+ return soonest;
187
+ }
188
+
189
+ export function comboCooldownRetryAfterSeconds(comboId: string, now = Date.now()): string | undefined {
190
+ const remainingMs = remainingComboCooldownMs(comboId, now);
191
+ if (remainingMs === undefined) return undefined;
192
+ return String(Math.max(1, Math.ceil(remainingMs / 1000)));
193
+ }
194
+
62
195
  export function coolComboTarget(
63
196
  comboId: string,
64
197
  target: Pick<OcxComboTarget, "provider" | "model">,
65
- options?: { retryAfter?: string | null; now?: number; cooldownMs?: number; writerGeneration?: number },
198
+ options?: {
199
+ retryAfter?: string | null;
200
+ now?: number;
201
+ cooldownMs?: number;
202
+ writerGeneration?: number;
203
+ status?: number;
204
+ code?: string | null;
205
+ message?: string;
206
+ },
66
207
  ): void {
67
208
  const now = options?.now ?? Date.now();
68
209
  const writerGeneration = options?.writerGeneration ?? captureConfigGeneration();
@@ -70,7 +211,11 @@ export function coolComboTarget(
70
211
  if (writerGeneration < lastReconciledGeneration && !liveComboTargets.has(ownerKey)) return;
71
212
  const cooldownMs = options?.cooldownMs
72
213
  ?? parseRetryAfterMs(options?.retryAfter, now)
73
- ?? DEFAULT_COOLDOWN_MS;
214
+ ?? (isTransientRequestRateLimit({
215
+ status: options?.status,
216
+ code: options?.code,
217
+ message: options?.message,
218
+ }) ? COMBO_REQUEST_RATE_COOLDOWN_MS : DEFAULT_COOLDOWN_MS);
74
219
  targetCooldowns.set(cooldownMapKey(comboId, target), {
75
220
  cooldownUntil: now + Math.min(Math.max(cooldownMs, 1), MAX_COOLDOWN_MS),
76
221
  });
@@ -108,6 +253,37 @@ export function clearComboTargetCooldowns(comboId?: string): void {
108
253
  }
109
254
 
110
255
  export type ComboFailureDecision = "hop" | "stop";
256
+ export type ComboFailureCooldownScope = "target" | "provider";
257
+
258
+ function normalizedFailureCode(code?: string | null): string {
259
+ return code?.trim().toLowerCase().replaceAll("-", "_") ?? "";
260
+ }
261
+
262
+ function isProviderScopedQuotaCap(
263
+ status: number | undefined,
264
+ message: string,
265
+ code?: string | null,
266
+ ): boolean {
267
+ const normalizedCode = normalizedFailureCode(code);
268
+ const text = message.toLowerCase();
269
+ if (
270
+ status === 429
271
+ && (normalizedCode === "gousagelimiterror" || text.includes("monthly usage limit reached"))
272
+ ) {
273
+ return true;
274
+ }
275
+ return normalizedCode === "free_rate_limited"
276
+ || text.includes("err_free_prompt_cap")
277
+ || (text.includes("free tier") && text.includes("single request"));
278
+ }
279
+
280
+ export function comboFailureCooldownScope(
281
+ status: number,
282
+ message: string,
283
+ options?: { code?: string | null },
284
+ ): ComboFailureCooldownScope {
285
+ return isProviderScopedQuotaCap(status, message, options?.code) ? "provider" : "target";
286
+ }
111
287
 
112
288
  function isModelLifecycleGone(
113
289
  status: number,
@@ -168,6 +344,9 @@ export function comboFailureDecision(
168
344
  if (options?.code === "input_admission_refused" || error.code === "input_admission_refused") {
169
345
  return "hop";
170
346
  }
347
+ if (isProviderScopedQuotaCap(status, message, options?.code || error.code)) {
348
+ return "hop";
349
+ }
171
350
  if (["origin_rejected", "context_length_exceeded", "invalid_request_error"].includes(error.code ?? "")) {
172
351
  return "stop";
173
352
  }
@@ -31,11 +31,17 @@ export {
31
31
  } from "./resolve";
32
32
  export {
33
33
  clearComboTargetCooldowns,
34
+ comboCooldownRetryAfterSeconds,
35
+ COMBO_REQUEST_RATE_COOLDOWN_MS,
34
36
  coolComboTarget,
35
37
  isComboTargetInCooldown,
38
+ isTransientRequestRateLimit,
36
39
  parseRetryAfterMs,
40
+ remainingComboCooldownMs,
37
41
  comboFailureDecision,
42
+ comboFailureCooldownScope,
38
43
  type ComboFailureDecision,
44
+ type ComboFailureCooldownScope,
39
45
  } from "./failover";
40
46
  export {
41
47
  comboIdFromRawBody,
@@ -1,6 +1,7 @@
1
1
  import type { OcxComboTarget, OcxConfig } from "../types";
2
2
  import { getCachedProviderQuota } from "../providers/quota-routing-cache";
3
- import { coolComboTarget, isComboTargetInCooldown } from "./failover";
3
+ import type { ProviderQuota } from "../providers/quota-types";
4
+ import { coolComboTarget, isComboTargetInCooldown, type ComboFailureCooldownScope } from "./failover";
4
5
  import { quotaResetRemainingMs } from "./reset-window";
5
6
  import { getCombo, resolveComboId, targetKey } from "./types";
6
7
  import type { NormalizedComboConfig } from "./types";
@@ -58,6 +59,28 @@ function targetProviderIsUsable(config: OcxConfig, target: OcxComboTarget): bool
58
59
  && config.providers[target.provider]?.disabled !== true;
59
60
  }
60
61
 
62
+ function quotaWindowExhausted(percent: number | undefined, resetAt: number | undefined, now: number): boolean {
63
+ if (typeof percent !== "number" || !Number.isFinite(percent) || percent < 100) return false;
64
+ return typeof resetAt !== "number" || !Number.isFinite(resetAt) || resetAt > now;
65
+ }
66
+
67
+ export function cachedProviderQuotaIsExhausted(
68
+ quota: ProviderQuota | null,
69
+ now = Date.now(),
70
+ ): boolean {
71
+ if (!quota) return false;
72
+ if (quotaWindowExhausted(quota.fiveHourPercent, quota.fiveHourResetAt, now)) return true;
73
+ if (quotaWindowExhausted(quota.weeklyPercent, quota.weeklyResetAt, now)) return true;
74
+ if (quotaWindowExhausted(quota.monthlyPercent, quota.monthlyResetAt, now)) return true;
75
+ if (quota.customWindows?.some(window => quotaWindowExhausted(window.percent, window.resetAt, now))) return true;
76
+ if (quota.creditsUsd?.unlimited !== true
77
+ && typeof quota.creditsUsd?.percent === "number"
78
+ && Number.isFinite(quota.creditsUsd.percent)
79
+ && quota.creditsUsd.percent >= 100
80
+ && quota.creditsUsd.remaining <= 0) return true;
81
+ return false;
82
+ }
83
+
61
84
  function smoothWeightedIndex(
62
85
  targets: Required<OcxComboTarget>[],
63
86
  state: SelectionState,
@@ -121,14 +144,17 @@ export function pickComboTarget(
121
144
  options: {
122
145
  exclude?: Iterable<string>;
123
146
  eligible?: (target: Required<OcxComboTarget>) => boolean;
147
+ now?: number;
124
148
  } = {},
125
149
  ): ComboPick | null {
126
150
  const writerGeneration = captureConfigGeneration();
127
151
  const combo = getCombo(config, comboId);
128
152
  if (!combo) throw new UnknownComboError(comboId);
129
153
  const excluded = new Set(options.exclude ?? []);
154
+ const now = options.now ?? Date.now();
130
155
  const eligible = (target: Required<OcxComboTarget>): boolean =>
131
156
  targetProviderIsUsable(config, target)
157
+ && !cachedProviderQuotaIsExhausted(getCachedProviderQuota(target.provider, now), now)
132
158
  && !excluded.has(targetKey(target))
133
159
  && (options.eligible?.(target) ?? true);
134
160
 
@@ -187,7 +213,7 @@ export function pickComboTarget(
187
213
  }
188
214
  }
189
215
  } else if (combo.strategy === "reset-window") {
190
- targetIndex = resetWindowIndex(combo.targets, eligible);
216
+ targetIndex = resetWindowIndex(combo.targets, eligible, now);
191
217
  } else {
192
218
  targetIndex = combo.targets.findIndex(eligible);
193
219
  }
@@ -250,15 +276,26 @@ export function advanceComboAfterFailure(
250
276
  retryAfter?: string | null;
251
277
  now?: number;
252
278
  eligible?: (target: Required<OcxComboTarget>) => boolean;
279
+ cooldownScope?: ComboFailureCooldownScope;
280
+ status?: number;
281
+ code?: string | null;
282
+ message?: string;
253
283
  } = {},
254
284
  ): ComboPick | null {
255
285
  noteComboFailure(pick.comboId, pick.target, pick.writerGeneration);
256
- coolComboTarget(pick.comboId, pick.target, {
257
- ...options,
258
- writerGeneration: pick.writerGeneration,
259
- });
286
+ const combo = getCombo(config, pick.comboId);
287
+ const cooldownTargets = options.cooldownScope === "provider" && combo
288
+ ? combo.targets.filter(target => target.provider === pick.target.provider)
289
+ : [pick.target];
290
+ for (const target of cooldownTargets) {
291
+ coolComboTarget(pick.comboId, target, {
292
+ ...options,
293
+ writerGeneration: pick.writerGeneration,
294
+ });
295
+ }
260
296
  return pickComboTarget(config, pick.comboId, {
261
297
  exclude: pick.attempted,
298
+ now: options.now,
262
299
  eligible: target => !isComboTargetInCooldown(pick.comboId, target, options.now)
263
300
  && (options.eligible?.(target) ?? true),
264
301
  });
package/src/config.ts CHANGED
@@ -1010,7 +1010,9 @@ const configSchema = z.object({
1010
1010
  // A malformed present client block must remain diagnosable from raw config and
1011
1011
  // fail closed through src/client/state.ts; unrelated provider state still loads.
1012
1012
  client: clientConnectionSchema.optional().catch(undefined),
1013
- managementUsageMaxReadBytes: z.number().int().positive().default(64 * 1024 * 1024),
1013
+ managementUsageMaxReadBytes: z.number().int().positive().default(64 * 1024 * 1024).describe(
1014
+ "Deprecated compatibility limit for bounded legacy usage readers; GET /api/usage always aggregates the complete ledger",
1015
+ ),
1014
1016
  // Invalid hand edits disable only this opt-in circuit. Live writes remain strict.
1015
1017
  upstreamHostCircuitThreshold: z.number().int()
1016
1018
  .min(0)
@@ -1046,6 +1048,8 @@ const configSchema = z.object({
1046
1048
  providers: z.record(z.string(), providerConfigSchema),
1047
1049
  defaultProvider: z.string().min(1).default("openai"),
1048
1050
  defaultModelAliases: z.boolean().optional(),
1051
+ // Malformed hand edits disable this opt-in projection without rejecting providers.
1052
+ cursorEffortRows: z.boolean().optional().catch(false),
1049
1053
  // Future versions remain opaque through passthrough-compatible whole-config saves.
1050
1054
  // Only version 1 grants deletion authority in the rebase path.
1051
1055
  configRebaseProvenance: z.unknown().optional(),