@bitkyc08/opencodex 2.49.0 → 2.50.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.
- package/AGENTS_INSTALL.md +9 -1
- package/README.md +3 -0
- package/gui/dist/assets/index-C39tnjXO.js +115 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/claude/inbound.ts +17 -5
- package/src/cli/account-api.ts +18 -3
- package/src/cli/account-auth.ts +8 -1
- package/src/cli/account-extended.ts +2 -1
- package/src/cli/account.ts +1 -0
- package/src/cli/capabilities.ts +15 -1
- package/src/cli/index.ts +5 -1
- package/src/cli/models-runtime.ts +8 -3
- package/src/cli/observe.ts +13 -3
- package/src/clients/config-export/zcode.ts +24 -0
- package/src/codex/account-runtime-state.ts +6 -1
- package/src/codex/account-store.ts +72 -9
- package/src/codex/account-usability.ts +3 -2
- package/src/codex/auth-api.ts +107 -23
- package/src/codex/auth-context.ts +21 -0
- package/src/codex/catalog/parsing.ts +23 -0
- package/src/codex/catalog/provider-fetch.ts +71 -2
- package/src/codex/catalog/sync.ts +14 -0
- package/src/codex/inject.ts +3 -2
- package/src/codex/quota-auto-refresh.ts +6 -1
- package/src/codex/quota.ts +54 -8
- package/src/combos/index.ts +2 -0
- package/src/combos/resolve.ts +52 -0
- package/src/config.ts +58 -0
- package/src/generated/compatibility-version.json +72 -60
- package/src/lib/errors.ts +8 -0
- package/src/lib/privacy.ts +25 -0
- package/src/oauth/health.ts +47 -12
- package/src/oauth/index.ts +46 -8
- package/src/oauth/token-guardian.ts +32 -6
- package/src/providers/google-ai-studio-model-discovery.ts +74 -0
- package/src/providers/opencode-zen-rate-limit.ts +75 -0
- package/src/providers/quota.ts +15 -0
- package/src/providers/registry.ts +1 -1
- package/src/server/auth-cors.ts +6 -0
- package/src/server/chat-completions.ts +4 -4
- package/src/server/chat-native.ts +10 -1
- package/src/server/claude-messages.ts +5 -5
- package/src/server/images.ts +2 -2
- package/src/server/index.ts +25 -2
- package/src/server/management/logs-usage-routes.ts +4 -1
- package/src/server/management/model-rows.ts +16 -1
- package/src/server/management/oauth-account-routes.ts +6 -2
- package/src/server/management/provider-routes.ts +9 -2
- package/src/server/management/request-history-routes.ts +4 -2
- package/src/server/management/route-registry.ts +5 -4
- package/src/server/management/shared.ts +66 -3
- package/src/server/management-api.ts +1 -1
- package/src/server/request-decompress.ts +91 -3
- package/src/server/request-log.ts +10 -0
- package/src/server/responses/codex-ws-wire.ts +1 -1
- package/src/server/responses/compact.ts +8 -2
- package/src/server/responses/context-overflow.ts +11 -0
- package/src/server/responses/core.ts +144 -38
- package/src/server/responses/policy-fallback.ts +6 -2
- package/src/server/search.ts +2 -2
- package/src/service.ts +92 -7
- package/src/types/accounts.ts +18 -0
- package/src/types/config.ts +36 -0
- package/src/types/provider.ts +56 -0
- package/src/types.ts +4 -0
- package/src/web-search/ollama-executor.ts +127 -0
- package/src/web-search/passthrough-bridge.ts +761 -0
- package/gui/dist/assets/index-BtyONQrZ.js +0 -115
package/src/codex/quota.ts
CHANGED
|
@@ -407,7 +407,20 @@ export function flushQuotaObservationsForTests(): Promise<void> {
|
|
|
407
407
|
return pendingObservation;
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
-
|
|
410
|
+
/** Wire marker shared by Spark-family models, whose upstream limit family is model-specific. */
|
|
411
|
+
const SPARK_MODEL_MARKER = "codex-spark";
|
|
412
|
+
/**
|
|
413
|
+
* Custom-window label for the Spark 5h window. The WHAM parser and the response-header path
|
|
414
|
+
* must write the SAME label so a header refresh replaces the WHAM reading instead of doubling it.
|
|
415
|
+
*/
|
|
416
|
+
const SPARK_SHORT_WINDOW_LABEL = "GPT-5.3-Codex-Spark 5h";
|
|
417
|
+
|
|
418
|
+
/** True when the routed model belongs to the Spark family, which carries its own rate limit. */
|
|
419
|
+
function isCodexSparkModel(modelId: string | undefined): boolean {
|
|
420
|
+
return typeof modelId === "string" && modelId.includes(SPARK_MODEL_MARKER);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function parseUpstreamQuotaHeaders(headers: Headers, options?: { modelId?: string }): Omit<StoredAccountQuota, "updatedAt"> | null {
|
|
411
424
|
const primaryRaw = headers.get("x-codex-primary-used-percent");
|
|
412
425
|
const secondaryRaw = headers.get("x-codex-secondary-used-percent");
|
|
413
426
|
const tertiaryRaw = headers.get("x-codex-tertiary-used-percent");
|
|
@@ -430,6 +443,10 @@ export function parseUpstreamQuotaHeaders(headers: Headers): Omit<StoredAccountQ
|
|
|
430
443
|
// it into weeklyPercent both discards the real weekly reading and leaves the account looking
|
|
431
444
|
// exhausted long after the burst window resets. Duration decides, exactly as parseUsageQuota
|
|
432
445
|
// already does for the WHAM payload — the two parsers must not disagree about the same data.
|
|
446
|
+
// One more attribution layer (#4122): on a Spark-family model response the sub-day primary is
|
|
447
|
+
// the MODEL-SPECIFIC limit, not an account window. Filing it as the account short tuple made
|
|
448
|
+
// one pool account display a 5h bar its identically-limited peers did not have, and fed a
|
|
449
|
+
// model limit to the account-policy readers (main-account hard lock, five-hour auto-refresh).
|
|
433
450
|
const primaryIsShort = isExplicitShortWindowMinutes(primaryWindowMinutes);
|
|
434
451
|
|
|
435
452
|
if (primaryIsMonthly) {
|
|
@@ -446,10 +463,21 @@ export function parseUpstreamQuotaHeaders(headers: Headers): Omit<StoredAccountQ
|
|
|
446
463
|
if (secondaryResetAt !== undefined) quota.weeklyResetAt = secondaryResetAt;
|
|
447
464
|
}
|
|
448
465
|
} else if (primaryIsShort) {
|
|
449
|
-
if (
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
466
|
+
if (isCodexSparkModel(options?.modelId)) {
|
|
467
|
+
if (primaryPercent !== undefined) {
|
|
468
|
+
const sparkWindow: { label: string; percent: number; resetAt?: number } = {
|
|
469
|
+
label: SPARK_SHORT_WINDOW_LABEL,
|
|
470
|
+
percent: primaryPercent,
|
|
471
|
+
};
|
|
472
|
+
if (primaryResetAt !== undefined) sparkWindow.resetAt = primaryResetAt;
|
|
473
|
+
quota.customWindows = [sparkWindow];
|
|
474
|
+
}
|
|
475
|
+
} else {
|
|
476
|
+
if (primaryPercent !== undefined) quota.shortPercent = primaryPercent;
|
|
477
|
+
if (primaryResetAt !== undefined) quota.shortResetAt = primaryResetAt;
|
|
478
|
+
const minutes = windowMinutes_(primaryWindowMinutes);
|
|
479
|
+
if (minutes !== undefined) quota.shortWindowSeconds = Math.round(minutes * 60);
|
|
480
|
+
}
|
|
453
481
|
// The burst window vacates the primary slot, so the weekly reading is the secondary — which
|
|
454
482
|
// is where it was all along. Without this the true weekly value is silently dropped.
|
|
455
483
|
if (secondaryPercent !== undefined) {
|
|
@@ -480,13 +508,31 @@ export function applyAccountQuotaFromUpstreamHeaders(
|
|
|
480
508
|
headers: Headers,
|
|
481
509
|
writerGeneration = captureConfigGeneration(),
|
|
482
510
|
mainWriter?: MainQuotaWriter,
|
|
511
|
+
options?: { modelId?: string },
|
|
483
512
|
): void {
|
|
484
|
-
const quota = parseUpstreamQuotaHeaders(headers);
|
|
513
|
+
const quota = parseUpstreamQuotaHeaders(headers, options);
|
|
485
514
|
if (!quota) return;
|
|
486
515
|
const policyQuota = [
|
|
487
516
|
"x-codex-primary-used-percent", "x-codex-secondary-used-percent", "x-codex-tertiary-used-percent",
|
|
488
517
|
].some(name => isInvalidPolicyUsagePercent(headers.get(name))) ? null : filterMainPolicyMonthlyQuota(quota);
|
|
489
|
-
|
|
518
|
+
// A header-observed Spark window is a partial update against the WHAM-recorded custom windows:
|
|
519
|
+
// merge by label so the weekly Spark entry survives, and hydrate first so the first call in a
|
|
520
|
+
// process does not merge against an empty map. The merged list goes only to the legacy
|
|
521
|
+
// snapshot — the identity-bound policy evidence keeps exactly what this response said.
|
|
522
|
+
let legacyQuota = quota;
|
|
523
|
+
if (quota.customWindows !== undefined) {
|
|
524
|
+
hydrateAccountQuotasFromDisk();
|
|
525
|
+
const existing = accountQuota.get(accountId)?.customWindows;
|
|
526
|
+
if (existing !== undefined) {
|
|
527
|
+
const incoming = new Map(quota.customWindows.map(window => [window.label, window]));
|
|
528
|
+
const merged = existing.map(window => incoming.get(window.label) ?? window);
|
|
529
|
+
for (const window of quota.customWindows) {
|
|
530
|
+
if (!existing.some(entry => entry.label === window.label)) merged.push(window);
|
|
531
|
+
}
|
|
532
|
+
legacyQuota = { ...quota, customWindows: merged };
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
setAccountQuotaFromParsed(accountId, legacyQuota, writerGeneration, mainWriter, policyQuota);
|
|
490
536
|
}
|
|
491
537
|
|
|
492
538
|
export function updateAccountQuota(
|
|
@@ -812,7 +858,7 @@ export function parseUsageQuota(data: WhamUsageResponse): Omit<StoredAccountQuot
|
|
|
812
858
|
});
|
|
813
859
|
const sparkCustomWindows: Array<{ label: string; percent: number; resetAt?: number }> = [];
|
|
814
860
|
for (const [label, window] of [
|
|
815
|
-
[
|
|
861
|
+
[SPARK_SHORT_WINDOW_LABEL, sparkShort],
|
|
816
862
|
["GPT-5.3-Codex-Spark Weekly", sparkWeekly],
|
|
817
863
|
] as const) {
|
|
818
864
|
const percent = normalizeUsagePercent(window?.used_percent);
|
package/src/combos/index.ts
CHANGED
|
@@ -27,9 +27,11 @@ export {
|
|
|
27
27
|
noteComboSuccess,
|
|
28
28
|
pickComboTarget,
|
|
29
29
|
pickComboTargetWithWait,
|
|
30
|
+
quotaInactiveReason,
|
|
30
31
|
tryPickComboModel,
|
|
31
32
|
UnknownComboError,
|
|
32
33
|
type ComboPick,
|
|
34
|
+
type QuotaInactiveReason,
|
|
33
35
|
} from "./resolve";
|
|
34
36
|
export {
|
|
35
37
|
clearComboTargetCooldowns,
|
package/src/combos/resolve.ts
CHANGED
|
@@ -92,6 +92,58 @@ export function cachedProviderQuotaIsExhausted(
|
|
|
92
92
|
return false;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Why a catalog row is offered but cannot currently serve a request (#1711).
|
|
97
|
+
*
|
|
98
|
+
* Only one reason exists today. It is a string rather than a boolean so a later cause — a
|
|
99
|
+
* cooldown, a revoked key — can be told apart by a consumer that already reads the field.
|
|
100
|
+
*/
|
|
101
|
+
export type QuotaInactiveReason = "no_credit";
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* `"no_credit"` when every USABLE target of a catalog row has positive exhaustion evidence
|
|
105
|
+
* (#1711), otherwise undefined.
|
|
106
|
+
*
|
|
107
|
+
* This deliberately reuses the runtime rules in `targetProviderIsUsable` above rather than the
|
|
108
|
+
* Dashboard's `quotaStateFromReport`, which is harsher: it treats `remaining <= 0` as exhausted
|
|
109
|
+
* without requiring `percent >= 100` and ignores an elapsed `resetAt`. A catalog row marked
|
|
110
|
+
* inactive on the harsher rule would contradict the router, which would still happily send the
|
|
111
|
+
* request.
|
|
112
|
+
*
|
|
113
|
+
* Three rules carry the correctness, all inherited rather than restated:
|
|
114
|
+
*
|
|
115
|
+
* - A target the operator has removed or disabled is not usable and is not evidence either way;
|
|
116
|
+
* it drops out before the vote. If nothing is left, the row is unavailable for an operator
|
|
117
|
+
* reason rather than a quota one, so this returns undefined.
|
|
118
|
+
* - The canonical ChatGPT forward provider is exempt. Native account selection owns model-scoped
|
|
119
|
+
* quota, and a provider-level summary cannot veto it.
|
|
120
|
+
* - A stale cache is NOT exhaustion. `getCachedProviderQuota` returns null past its 30-minute
|
|
121
|
+
* window, and a null reading ends the vote rather than counting as evidence, so an unprobed
|
|
122
|
+
* provider is never marked inactive.
|
|
123
|
+
*
|
|
124
|
+
* "Every" is the bar on purpose: one target that can still serve makes the row serviceable, which
|
|
125
|
+
* is exactly what the combo loop concludes at request time.
|
|
126
|
+
*/
|
|
127
|
+
export function quotaInactiveReason(
|
|
128
|
+
config: OcxConfig,
|
|
129
|
+
targets: readonly { provider: string }[],
|
|
130
|
+
now = Date.now(),
|
|
131
|
+
): QuotaInactiveReason | undefined {
|
|
132
|
+
const usable = targets.filter(target => {
|
|
133
|
+
if (!Object.hasOwn(config.providers, target.provider)) return false;
|
|
134
|
+
const provider = config.providers[target.provider];
|
|
135
|
+
return !!provider && provider.disabled !== true;
|
|
136
|
+
});
|
|
137
|
+
if (usable.length === 0) return undefined;
|
|
138
|
+
for (const target of usable) {
|
|
139
|
+
const provider = config.providers[target.provider]!;
|
|
140
|
+
if (isCanonicalOpenAiForwardProvider(provider)) return undefined;
|
|
141
|
+
const quota = getCachedProviderQuota(target.provider, now);
|
|
142
|
+
if (!quota || !cachedProviderQuotaIsExhausted(quota, now)) return undefined;
|
|
143
|
+
}
|
|
144
|
+
return "no_credit";
|
|
145
|
+
}
|
|
146
|
+
|
|
95
147
|
function smoothWeightedIndex(
|
|
96
148
|
targets: Required<OcxComboTarget>[],
|
|
97
149
|
state: SelectionState,
|
package/src/config.ts
CHANGED
|
@@ -73,6 +73,7 @@ import {
|
|
|
73
73
|
MODEL_ADAPTER_OVERRIDE_ALLOWED,
|
|
74
74
|
OPENAI_PROVIDER_TIER_VERSION,
|
|
75
75
|
pinnedWireAdapter,
|
|
76
|
+
PROVIDER_WEB_SEARCH_BRIDGE_BACKENDS,
|
|
76
77
|
UPSTREAM_HTTP_VERSION_VALUES,
|
|
77
78
|
type OcxClaudeCodeConfig,
|
|
78
79
|
type OcxConfig,
|
|
@@ -497,6 +498,47 @@ export function requestPacingConfigError(value: unknown): string | null {
|
|
|
497
498
|
return "requestPacing must contain enabled and a valid requestsPerMinute/minIntervalMs provider rule or model overrides";
|
|
498
499
|
}
|
|
499
500
|
|
|
501
|
+
/**
|
|
502
|
+
* Bounds for the opt-in passthrough web-search bridge (`providers.<name>.webSearchBridge`,
|
|
503
|
+
* #3761). Strict for the same reason `retryOn429` is: a misspelled key here would silently
|
|
504
|
+
* leave the bridge disarmed while the operator believes they enabled it. `endpoint` is only
|
|
505
|
+
* shape-checked here; `planPassthroughWebSearchBridge` re-validates the origin before any key
|
|
506
|
+
* is sent to it, because config validation is not an authorization boundary.
|
|
507
|
+
*/
|
|
508
|
+
const providerWebSearchBridgeSchema = z.object({
|
|
509
|
+
enabled: z.boolean().optional(),
|
|
510
|
+
backend: z.enum(PROVIDER_WEB_SEARCH_BRIDGE_BACKENDS).optional(),
|
|
511
|
+
maxSearches: z.number().int().min(1).max(10).optional(),
|
|
512
|
+
timeoutMs: z.number().int().min(1_000).max(600_000).optional(),
|
|
513
|
+
endpoint: z.string().min(1).optional(),
|
|
514
|
+
}).strict();
|
|
515
|
+
|
|
516
|
+
export function providerWebSearchBridgeConfigError(value: unknown): string | null {
|
|
517
|
+
if (value === undefined) return null;
|
|
518
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
519
|
+
return "webSearchBridge must be a plain object";
|
|
520
|
+
}
|
|
521
|
+
const parsed = providerWebSearchBridgeSchema.safeParse(value);
|
|
522
|
+
if (!parsed.success) {
|
|
523
|
+
return "webSearchBridge accepts only enabled (boolean), backend "
|
|
524
|
+
+ `(${PROVIDER_WEB_SEARCH_BRIDGE_BACKENDS.join("|")}), maxSearches (1..10), `
|
|
525
|
+
+ "timeoutMs (1000..600000), and endpoint (absolute http(s) URL)";
|
|
526
|
+
}
|
|
527
|
+
const endpoint = parsed.data.endpoint;
|
|
528
|
+
if (endpoint !== undefined) {
|
|
529
|
+
let url: URL;
|
|
530
|
+
try {
|
|
531
|
+
url = new URL(endpoint);
|
|
532
|
+
} catch {
|
|
533
|
+
return "webSearchBridge.endpoint must be an absolute http(s) URL";
|
|
534
|
+
}
|
|
535
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
536
|
+
return "webSearchBridge.endpoint must be an absolute http(s) URL";
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
|
|
500
542
|
const fastWireSchema = z.object({
|
|
501
543
|
kind: z.string(),
|
|
502
544
|
canonicalToWire: z.record(z.string().trim(), z.string().trim()),
|
|
@@ -600,6 +642,10 @@ const providerConfigSchema = z.object({
|
|
|
600
642
|
repairInvalidIds: z.boolean().optional(),
|
|
601
643
|
}).strict().optional(),
|
|
602
644
|
responsesSnapshotRepair: z.boolean().optional(),
|
|
645
|
+
// Invalid blocks degrade to "absent" rather than failing the whole config load: an unusable
|
|
646
|
+
// bridge block must never send an operator through invalid-config recovery for an opt-in
|
|
647
|
+
// feature that is off by default. The management write boundary still rejects it loudly.
|
|
648
|
+
webSearchBridge: providerWebSearchBridgeSchema.optional().catch(undefined),
|
|
603
649
|
xaiResponsesXSearch: z.boolean().optional(),
|
|
604
650
|
xaiResponsesDefaultVersion: z.number().int().positive().optional().catch(undefined),
|
|
605
651
|
}).passthrough();
|
|
@@ -1100,6 +1146,9 @@ const configSchema = z.object({
|
|
|
1100
1146
|
// candidates are rejected explicitly by remoteGuiConfigError below.
|
|
1101
1147
|
hub: hubConfigSchema.optional().catch(undefined),
|
|
1102
1148
|
remoteGui: remoteGuiConfigSchema.optional().catch(undefined),
|
|
1149
|
+
// A malformed privacy block must never be read as "unmask": .catch(undefined) drops it and
|
|
1150
|
+
// emailMaskingEnabled then falls back to masked, which is also what an absent block means.
|
|
1151
|
+
privacy: z.object({ maskEmails: z.boolean().optional() }).strict().optional().catch(undefined),
|
|
1103
1152
|
// A malformed present client block must remain diagnosable from raw config and
|
|
1104
1153
|
// fail closed through src/client/state.ts; unrelated provider state still loads.
|
|
1105
1154
|
client: clientConnectionSchema.optional().catch(undefined),
|
|
@@ -1118,6 +1167,15 @@ const configSchema = z.object({
|
|
|
1118
1167
|
.min(0)
|
|
1119
1168
|
.optional()
|
|
1120
1169
|
.catch(undefined),
|
|
1170
|
+
// Opt-in inbound body ceiling (#3573). An invalid hand edit degrades to the 256 MiB default
|
|
1171
|
+
// rather than failing the parse, matching the outbound guard above: a malformed number must
|
|
1172
|
+
// not change what the proxy admits. The hard ceiling is NOT enforced here — because of that
|
|
1173
|
+
// `.catch`, and because a config object can be built without this schema at all — but in
|
|
1174
|
+
// `resolveInboundBodyLimitBytes()`, which every reader goes through.
|
|
1175
|
+
maxInboundBodyBytes: z.number().int()
|
|
1176
|
+
.min(0)
|
|
1177
|
+
.optional()
|
|
1178
|
+
.catch(undefined),
|
|
1121
1179
|
appOwnedMemoryBudgetMb: z.number().int()
|
|
1122
1180
|
.min(MIN_APP_OWNED_MEMORY_BUDGET_MB)
|
|
1123
1181
|
.max(MAX_APP_OWNED_MEMORY_BUDGET_MB)
|