@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.
- package/gui/dist/assets/index-MUpaVatk.js +67 -0
- package/gui/dist/index.html +1 -1
- package/package.json +3 -3
- package/packages/ocx-notch/README.md +2 -1
- package/src/adapters/cursor/discovery.ts +6 -2
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/google-antigravity-replay.ts +24 -0
- package/src/adapters/google.ts +16 -11
- package/src/chat/inbound.ts +5 -11
- package/src/cli/account-api.ts +9 -1
- package/src/cli/account-extended.ts +4 -1
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-lifecycle.ts +12 -1
- package/src/codex/account-namespaces.ts +21 -0
- package/src/codex/account-priority.ts +49 -0
- package/src/codex/account-store.ts +2 -1
- package/src/codex/auth-api.ts +108 -17
- package/src/codex/auth-context.ts +61 -16
- package/src/codex/catalog/metadata.ts +34 -12
- package/src/codex/catalog/parsing.ts +8 -1
- package/src/codex/catalog/provider-fetch.ts +24 -6
- package/src/codex/catalog.ts +1 -1
- package/src/codex/pool-rotation.ts +51 -4
- package/src/codex/quota.ts +154 -35
- package/src/codex/routing.ts +133 -33
- package/src/codex/warmup.ts +193 -85
- package/src/config.ts +84 -1
- package/src/lib/bounded-body.ts +13 -6
- package/src/lib/bun-stream-caps.ts +5 -6
- package/src/lib/redact.ts +13 -0
- package/src/oauth/index.ts +79 -12
- package/src/oauth/log.ts +3 -1
- package/src/oauth/store.ts +31 -8
- package/src/providers/antigravity-models.ts +53 -24
- package/src/providers/codex-capacity.ts +303 -0
- package/src/providers/model-rename-migration.ts +147 -0
- package/src/providers/model-rename-startup.ts +29 -0
- package/src/providers/quota.ts +126 -16
- package/src/providers/registry.ts +258 -38
- package/src/responses/parser.ts +19 -12
- package/src/responses/spill-store.ts +14 -1
- package/src/responses/state.ts +108 -14
- package/src/server/index.ts +9 -1
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +8 -1
- package/src/server/relay.ts +10 -42
- package/src/server/request-log.ts +42 -1
- package/src/server/responses/compact.ts +16 -4
- package/src/server/responses/core.ts +217 -59
- package/src/server/responses/empty-completion-guard.ts +275 -0
- package/src/server/responses/encrypted-payload.ts +54 -39
- package/src/server/responses/fetch-helpers.ts +24 -3
- package/src/server/responses/ws-upstream.ts +318 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/ws-bridge.ts +17 -11
- package/src/types.ts +8 -0
- package/src/usage/log.ts +24 -0
- package/src/usage/summary.ts +152 -2
- package/vendor/ocx-notch/win32-x64/ocx-notch.exe +0 -0
- package/gui/dist/assets/index-BucjyD4I.js +0 -67
package/src/providers/quota.ts
CHANGED
|
@@ -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(
|
|
175
|
-
|
|
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
|
|
198
|
-
const
|
|
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
|
|
202
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1341
|
+
const body = asRecord(await readQuotaJson(response));
|
|
1232
1342
|
const models = asRecord(body?.models);
|
|
1233
1343
|
if (!models) return null;
|
|
1234
1344
|
|