fix(codex): isolate Spark quota scope - #4293
diegosouzapw merged 13 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request isolates the standard 'codex' and premium 'spark' rate-limiting and quota pools for Codex, ensuring that exhausting one does not block requests to the other. It introduces model-scoping helpers, updates connection selection and rate-limiting logic to handle these scopes independently, and updates the usage service and dashboard to display Spark quotas. Feedback was provided to prevent potential data loss in chatCore.ts when updating connection data on transient database errors, to avoid modifying the cache map during iteration in codexQuotaFetcher.ts, and to use bracket notation instead of dot notation in usage.ts to prevent TypeScript compilation errors.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const connection = await getProviderConnectionById( | ||
| String(failedConnectionId) | ||
| ).catch(() => null); | ||
| const existingProviderData = | ||
| connection?.providerSpecificData && | ||
| typeof connection.providerSpecificData === "object" | ||
| ? connection.providerSpecificData | ||
| : credentials?.providerSpecificData && | ||
| typeof credentials.providerSpecificData === "object" | ||
| ? credentials.providerSpecificData | ||
| : {}; | ||
| const existingScopeMap = | ||
| existingProviderData.codexScopeRateLimitedUntil && | ||
| typeof existingProviderData.codexScopeRateLimitedUntil === "object" | ||
| ? (existingProviderData.codexScopeRateLimitedUntil as Record<string, unknown>) | ||
| : {}; | ||
| const nextProviderData = { | ||
| ...existingProviderData, | ||
| codexScopeRateLimitedUntil: { | ||
| ...existingScopeMap, | ||
| [failedScope]: rateLimitedUntil, | ||
| }, | ||
| }; | ||
| updateProviderConnection(String(failedConnectionId), { | ||
| rateLimitedUntil, | ||
| testStatus: "unavailable", | ||
| providerSpecificData: nextProviderData, | ||
| lastError: "429 rate limited — codex account rotation", | ||
| errorCode: 429, | ||
| }).catch(() => {}); |
There was a problem hiding this comment.
If getProviderConnectionById fails or returns null (due to a transient database error or connection issue), existingProviderData will fall back to credentials?.providerSpecificData or {}. Calling updateProviderConnection with providerSpecificData: nextProviderData in this case would overwrite the entire providerSpecificData column in the database with a nearly empty object, potentially wiping out critical credentials, OAuth tokens, or settings. To prevent accidental data loss, only include providerSpecificData in the database update if the connection was successfully retrieved.
const connection = await getProviderConnectionById(
String(failedConnectionId)
).catch(() => null);
const existingProviderData =
connection?.providerSpecificData &&
typeof connection.providerSpecificData === "object"
? connection.providerSpecificData
: credentials?.providerSpecificData &&
typeof credentials.providerSpecificData === "object"
? credentials.providerSpecificData
: {};
const existingScopeMap =
existingProviderData.codexScopeRateLimitedUntil &&
typeof existingProviderData.codexScopeRateLimitedUntil === "object"
? (existingProviderData.codexScopeRateLimitedUntil as Record<string, unknown>)
: {};
const nextProviderData = {
...existingProviderData,
codexScopeRateLimitedUntil: {
...existingScopeMap,
[failedScope]: rateLimitedUntil,
},
};
updateProviderConnection(String(failedConnectionId), {
...(connection ? { providerSpecificData: nextProviderData } : {}),
lastError: "429 rate limited — codex account rotation",
errorCode: 429,
}).catch(() => {});| function deleteQuotaCacheForConnection(connectionId: string): void { | ||
| quotaCache.delete(connectionId); | ||
| for (const key of quotaCache.keys()) { | ||
| if (key === connectionId || key.startsWith(`${connectionId}:`)) quotaCache.delete(key); | ||
| } | ||
| } |
There was a problem hiding this comment.
Modifying a Map or Cache during iteration over its keys can lead to unpredictable behavior or runtime errors depending on the underlying implementation (especially if quotaCache is a custom cache or an LRU cache). It is safer and cleaner to collect the keys to delete first, and then perform the deletion. Additionally, key === connectionId is redundant since quotaCache.delete(connectionId) is already called at the start of the function.
| function deleteQuotaCacheForConnection(connectionId: string): void { | |
| quotaCache.delete(connectionId); | |
| for (const key of quotaCache.keys()) { | |
| if (key === connectionId || key.startsWith(`${connectionId}:`)) quotaCache.delete(key); | |
| } | |
| } | |
| function deleteQuotaCacheForConnection(connectionId: string): void { | |
| quotaCache.delete(connectionId); | |
| const keysToDelete: string[] = []; | |
| for (const key of quotaCache.keys()) { | |
| if (key.startsWith(connectionId + ":")) { | |
| keysToDelete.push(key); | |
| } | |
| } | |
| for (const key of keysToDelete) { | |
| quotaCache.delete(key); | |
| } | |
| } |
| if ( | ||
| !isCodexSparkLimitDescriptor( | ||
| getFieldValue(entry, "limit_name", "limitName"), | ||
| getFieldValue(entry, "metered_feature", "meteredFeature"), | ||
| getFieldValue(entry, "limit_id", "limitId"), | ||
| entry.id, | ||
| entry.name, | ||
| entry.title, | ||
| entry.model, | ||
| getFieldValue(entry, "model_id", "modelId") | ||
| ) | ||
| ) { |
There was a problem hiding this comment.
Using dot notation like entry.id, entry.name, etc., on a record of type Record<string, unknown> will cause TypeScript compilation errors under strict mode. For consistency with open-sse/services/codexQuotaFetcher.ts and other fields in this file, use bracket notation to access these properties safely.
if (
!isCodexSparkLimitDescriptor(
getFieldValue(entry, "limit_name", "limitName"),
getFieldValue(entry, "metered_feature", "meteredFeature"),
getFieldValue(entry, "limit_id", "limitId"),
entry["id"],
entry["name"],
entry["title"],
entry["model"],
getFieldValue(entry, "model_id", "modelId")
)
)# Conflicts: # config/quality/complexity-baseline.json # config/quality/file-size-baseline.json # open-sse/handlers/chatCore.ts # tests/integration/integration-wiring.test.ts # tests/integration/search-providers-catalog.test.ts # tests/unit/tproxy-transparent-socket.test.ts
…ase/v3.8.30 merge (diegosouzapw#4293) Measured on the actual merged tree (not the PR's main-based estimate): complexity 1885->1887 (+2); file-size auth.ts 2219->2279, chatCore.ts 5116->5125, accountFallback.ts 1727->1731, + the 4 Codex test files. Drift test-file conflicts (search-providers-catalog, tproxy-transparent-socket, integration-wiring) resolved to the already-merged release versions (diegosouzapw#4276). Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
|
Thanks @xz-dev! 🙏 Merged into What I reconciled on merge (the branch was based on |
* fix(codex): isolate Spark quota scope * fix(codex): address Spark quota review feedback * fix(ci): update Electron undici override * fix(ci): update root undici overrides * test(integration): sync stale expectations * test(tproxy): tolerate available native addon * test(tproxy): avoid environment-specific skips * test(tproxy): keep assertion count stable * fix(ci): stabilize quality and tproxy checks * chore(ci): rebaseline auth file size * fix(ci): extend node compatibility budget * chore(quality): reconcile complexity + file-size baselines after release/v3.8.30 merge (diegosouzapw#4293) Measured on the actual merged tree (not the PR's main-based estimate): complexity 1885->1887 (+2); file-size auth.ts 2219->2279, chatCore.ts 5116->5125, accountFallback.ts 1727->1731, + the 4 Codex test files. Drift test-file conflicts (search-providers-catalog, tproxy-transparent-socket, integration-wiring) resolved to the already-merged release versions (diegosouzapw#4276). Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> --------- Co-authored-by: ci <ci@local> Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
Summary
additional_rate_limitsfor Spark-specific quota windows while preserving normal primary/secondary window handlingrateLimitedUntiland block normal Codex models likegpt-5.5open-sse/executors/codex.tsstable via re-exportsDetails
This adds a shared Codex quota scope helper module and uses it from the executor, quota fetcher, auth selection, account fallback/model lockout, usage parsing, and UI label formatting.
Spark detection covers:
gpt-5.3-codex-sparkcodex-sparksparkbengalfoxmetered_feature = gpt_5_3_codex_sparkNormal Codex requests continue to use the regular WHAM
rate_limit.primary_window/secondary_windowvalues. Spark requests use Spark-specific windows fromadditional_rate_limitsand do not fall back to normal Codex windows when Spark data is absent.Validation
node node_modules/prettier/bin/prettier.cjs --check ...changed files...DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx --import ./open-sse/utils/setupPolyfill.ts --test tests/unit/executor-codex.test.ts tests/unit/codex-quota-fetcher.test.ts tests/unit/account-fallback-service.test.ts tests/unit/sse-auth.test.ts tests/unit/usage-service-hardening.test.tstests 209pass 209fail 0node node_modules/typescript/bin/tsc --pretty false -p tsconfig.typecheck-core.jsongit diff --cached --check