@bitkyc08/opencodex 2.64.0-preview.20260923 → 2.65.0-preview.20260925
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/README.md +28 -25
- package/bin/ocx.mjs +16 -2
- package/gui/dist/assets/App-8NMiZxT0.css +1 -0
- package/gui/dist/assets/App-CsYvvpr3.js +50 -0
- package/gui/dist/assets/Tray-DUvc_Wul.js +1 -0
- package/gui/dist/assets/{index-DiBRuK-d.css → index--EWgGQvZ.css} +1 -1
- package/gui/dist/assets/{index-BEq4OCOz.js → index-BB0iHG8-.js} +12 -12
- package/gui/dist/assets/tray-data-f0lTZ4sF.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -4
- package/src/adapters/anthropic.ts +30 -1
- package/src/adapters/claude-cli/adapter.ts +218 -0
- package/src/adapters/claude-cli/profiles.ts +38 -0
- package/src/adapters/codebuddy/profiles.ts +2 -0
- package/src/adapters/coding-agent/profile.ts +12 -4
- package/src/adapters/coding-agent/turn.ts +16 -6
- package/src/adapters/command-code-tool-text.ts +133 -23
- package/src/adapters/cursor/catalog.ts +4 -8
- package/src/adapters/cursor/discovery.ts +10 -5
- package/src/adapters/cursor/effort-map.ts +2 -6
- package/src/adapters/cursor/envelope-echo.ts +10 -5
- package/src/adapters/cursor/request-builder.ts +9 -4
- package/src/adapters/cursor/thread-continuity.ts +53 -0
- package/src/adapters/cursor.ts +29 -13
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/google-tool-schema.ts +26 -3
- package/src/adapters/identity.ts +214 -2
- package/src/adapters/openai-chat/passthrough.ts +1 -0
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +569 -0
- package/src/adapters/openai-chat.ts +43 -15
- package/src/adapters/openai-responses/canonical-forward.ts +17 -10
- package/src/adapters/openai-responses/passthrough.ts +19 -4
- package/src/adapters/openai-responses/request-strips.ts +25 -0
- package/src/adapters/openai-responses/web-search.ts +22 -4
- package/src/adapters/qoder/profiles.ts +2 -0
- package/src/adapters/registry.ts +8 -0
- package/src/adapters/run-turn-queue.ts +7 -0
- package/src/bridge/response-json.ts +6 -4
- package/src/bridge/sse.ts +7 -5
- package/src/claude/alias.ts +73 -26
- package/src/claude/context-windows.ts +15 -6
- package/src/claude/desktop-3p-library.ts +3 -2
- package/src/claude/desktop-3p.ts +1 -1
- package/src/claude/desktop-first-party.ts +63 -22
- package/src/claude/desktop-picker-profile.ts +302 -0
- package/src/claude/desktop-picker.ts +365 -0
- package/src/claude/desktop-risk.ts +20 -0
- package/src/claude/gateway-cache.ts +13 -4
- package/src/claude/inbound.ts +5 -2
- package/src/claude/intercept/connect-proxy.ts +106 -29
- package/src/claude/intercept/local-ca.ts +76 -18
- package/src/claude/intercept/model-bindings.ts +145 -0
- package/src/claude/intercept/picker-bootstrap.ts +141 -0
- package/src/claude/intercept/picker-ca.ts +126 -0
- package/src/claude/intercept/picker-listener.ts +213 -0
- package/src/claude/intercept/picker-models.ts +101 -0
- package/src/claude/intercept/picker-runtime.ts +330 -0
- package/src/claude/intercept/picker-trust.ts +125 -0
- package/src/claude/intercept/runtime.ts +149 -2
- package/src/claude/model-info.ts +18 -4
- package/src/cli/agent.ts +16 -2
- package/src/cli/capabilities.ts +71 -2
- package/src/cli/claude-desktop.ts +319 -33
- package/src/cli/claude.ts +15 -7
- package/src/cli/codex-shim-autorestore.ts +1 -0
- package/src/cli/dispatch.ts +29 -23
- package/src/cli/effort.ts +16 -6
- package/src/cli/ensure-desired-integrations.ts +29 -4
- package/src/cli/ready.ts +1 -1
- package/src/cli/registry.ts +8 -0
- package/src/cli/restart-scope.ts +9 -1
- package/src/cli/runtime-api.ts +17 -0
- package/src/cli/system-command.ts +11 -12
- package/src/client/connect.ts +12 -2
- package/src/client/state.ts +39 -1
- package/src/clients/config-export.ts +57 -9
- package/src/codex/auth-context.ts +51 -11
- package/src/codex/catalog/derive-entry.ts +5 -5
- package/src/codex/catalog/gather-capture.ts +25 -6
- package/src/codex/catalog/metadata.ts +3 -3
- package/src/codex/catalog/parsing.ts +39 -1
- package/src/codex/catalog/provider-models.ts +8 -6
- package/src/codex/catalog/retained-sync.ts +17 -7
- package/src/codex/catalog/sync.ts +2 -2
- package/src/codex/desktop-app-restart.ts +8 -0
- package/src/codex/desktop-switches.ts +9 -1
- package/src/codex/home.ts +43 -4
- package/src/codex/inject/config-toml.ts +137 -0
- package/src/codex/inject/plan.ts +23 -0
- package/src/codex/inject/remove.ts +21 -1
- package/src/codex/inject.ts +63 -8
- package/src/codex/journal.ts +36 -0
- package/src/codex/main-account-hard-lock.ts +14 -1
- package/src/codex/main-account-policy-wait.ts +109 -0
- package/src/codex/native-profile-startup.ts +24 -6
- package/src/codex/native-residue.ts +28 -10
- package/src/codex/quota-types.ts +8 -1
- package/src/codex/quota.ts +49 -7
- package/src/codex/runtime.ts +31 -10
- package/src/combos/failover.ts +7 -6
- package/src/combos/resolve.ts +91 -14
- package/src/combos/types.ts +18 -1
- package/src/config/load-degrade.ts +2 -0
- package/src/config/schema/config-schema.ts +21 -2
- package/src/config/schema/leaf-validators.ts +6 -3
- package/src/config/subagent-models.ts +3 -1
- package/src/generated/compatibility-version.json +275 -163
- package/src/images/artifacts.ts +14 -0
- package/src/integrations/owned-refresh.ts +1 -1
- package/src/integrations/ownership-policy.ts +32 -1
- package/src/integrations/state.ts +3 -1
- package/src/integrations/writer.ts +9 -0
- package/src/lib/config-ownership.ts +3 -0
- package/src/lib/errors.ts +87 -0
- package/src/lib/package-tree-integrity.ts +1 -1
- package/src/lib/request-execution-budget.ts +42 -14
- package/src/lib/request-resend-gate.ts +4 -3
- package/src/lib/retry-delay.ts +7 -1
- package/src/lib/tool-envelope-echo-filter.ts +236 -0
- package/src/lib/upstream-retry.ts +20 -12
- package/src/oauth/index.ts +1 -0
- package/src/oauth/kiro-credentials.ts +14 -1
- package/src/oauth/login-cli.ts +1 -0
- package/src/providers/derive.ts +4 -0
- package/src/providers/fast-opt-in.ts +31 -0
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/provider-id-rewrite.ts +2 -0
- package/src/providers/quota/vendor-probes-key.ts +8 -2
- package/src/providers/registry/entries-core.ts +45 -2
- package/src/providers/registry/entries-extended.ts +99 -12
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/types.ts +7 -1
- package/src/providers/resolved-model-policy.ts +11 -4
- package/src/providers/service-tier.ts +10 -3
- package/src/responses/code-mode-helper-compat.ts +3 -0
- package/src/responses/parser.ts +45 -3
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +8 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/claude-messages.ts +69 -14
- package/src/server/grok-upstream-envelope-echo.ts +120 -0
- package/src/server/index/claude-intercept-lifecycle.ts +24 -0
- package/src/server/index/serve-options.ts +2 -2
- package/src/server/index.ts +7 -0
- package/src/server/management/agent-settings-routes.ts +204 -112
- package/src/server/management/claude-desktop-picker-routes.ts +128 -0
- package/src/server/management/combo-routes.ts +59 -2
- package/src/server/management/config-routes.ts +81 -39
- package/src/server/management/context.ts +3 -0
- package/src/server/management/native-integration-routes.ts +169 -93
- package/src/server/management/provider-routes.ts +46 -4
- package/src/server/management/route-registry.ts +10 -0
- package/src/server/management/routing-profile-routes.ts +9 -0
- package/src/server/management/sidebar-routes.ts +52 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/proxy-liveness.ts +38 -3
- package/src/server/request-log-account-rotation.ts +53 -0
- package/src/server/request-log.ts +3 -16
- package/src/server/responses/adapter-dispatch.ts +17 -1
- package/src/server/responses/agent-task-recovery.ts +49 -26
- package/src/server/responses/codex-ws-exchange.ts +18 -7
- package/src/server/responses/codex-ws-wire.ts +27 -4
- package/src/server/responses/core-combo.ts +21 -3
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-opaque-recovery.ts +3 -2
- package/src/server/responses/encrypted-payload.ts +15 -14
- package/src/server/responses/fetch-helpers.ts +1 -1
- package/src/server/responses/input-admission.ts +10 -6
- package/src/server/responses/native-response-control.ts +2 -2
- package/src/server/responses/passthrough-delivery.ts +182 -12
- package/src/server/responses/passthrough-dispatch.ts +102 -44
- package/src/server/responses/policy-fallback.ts +3 -0
- package/src/server/responses/policy-refusal.ts +67 -0
- package/src/server/responses/request-send-budget.ts +4 -0
- package/src/server/responses/run-turn-execution.ts +42 -11
- package/src/server/responses/ws-upstream.ts +2 -1
- package/src/server/system-env-shell.ts +0 -1
- package/src/server/system-env.ts +25 -4
- package/src/service/cli.ts +34 -3
- package/src/tray/assets/opencodex-tray-offline-update.ico +0 -0
- package/src/tray/assets/opencodex-tray-online-update.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning-update.ico +0 -0
- package/src/tray/windows-tray.ps1 +188 -7
- package/src/tray/windows.ts +23 -2
- package/src/types/config.ts +52 -12
- package/src/types/provider.ts +22 -5
- package/src/types/tools.ts +11 -5
- package/src/types.ts +1 -0
- package/src/update/async-check.ts +109 -0
- package/src/update/badge.ts +11 -17
- package/src/update/check-types.ts +9 -0
- package/src/update/desktop-badge.ts +102 -0
- package/src/update/index.ts +48 -8
- package/src/update/install-detection.d.mts +29 -2
- package/src/update/install-detection.mjs +213 -7
- package/src/update/job.ts +17 -12
- package/src/update/notify.ts +16 -11
- package/src/update/pnpm-owner-worker.ts +12 -0
- package/src/update/refresh-scheduler.ts +141 -0
- package/src/usage/cost.ts +31 -4
- package/src/usage/expected-prices.ts +57 -0
- package/assets/download-linux.svg +0 -10
- package/assets/download-macos.svg +0 -10
- package/assets/download-windows.svg +0 -10
- package/gui/dist/assets/App-D5eN1TID.js +0 -50
- package/gui/dist/assets/App-I5AnaSLh.css +0 -1
- package/gui/dist/assets/Tray-Bh_ErQDh.js +0 -1
- package/gui/dist/assets/usage-companion-chart-DBG_37kQ.js +0 -1
|
@@ -82,6 +82,9 @@ const JOURNAL_FILE_NAME = "opencodex-journal.json";
|
|
|
82
82
|
const ROUTED_CATALOG_DESCRIPTION_PREFIX = "Routed via opencodex → ";
|
|
83
83
|
const MAX_ROLLOUT_INSPECTION_BYTES = 64 * 1024 * 1024;
|
|
84
84
|
const ROLLOUT_READ_CHUNK_BYTES = 64 * 1024;
|
|
85
|
+
// Bound one observation across every referenced rollout, not just each file.
|
|
86
|
+
const MAX_TOTAL_ROLLOUT_INSPECTION_BYTES = 64 * 1024 * 1024;
|
|
87
|
+
type RolloutInspectionBudget = { remainingBytes: number };
|
|
85
88
|
|
|
86
89
|
function errorCode(error: unknown): string | undefined {
|
|
87
90
|
return (error as NodeJS.ErrnoException | undefined)?.code;
|
|
@@ -424,6 +427,7 @@ function classifyPartialWrites(targetPaths: string[]): NativeRoutedResidueResult
|
|
|
424
427
|
function classifyReferencedRollout(
|
|
425
428
|
surface: "history" | "history-backup",
|
|
426
429
|
reference: RolloutReference,
|
|
430
|
+
budget: RolloutInspectionBudget,
|
|
427
431
|
): NativeRoutedResidueResult {
|
|
428
432
|
const resolved = resolveRegularFile(reference.path);
|
|
429
433
|
if (resolved.kind === "absent") {
|
|
@@ -451,6 +455,14 @@ function classifyReferencedRollout(
|
|
|
451
455
|
`referenced rollout exceeds the ${MAX_ROLLOUT_INSPECTION_BYTES} byte inspection limit`,
|
|
452
456
|
);
|
|
453
457
|
}
|
|
458
|
+
if (opened.size > budget.remainingBytes) {
|
|
459
|
+
return indeterminate(
|
|
460
|
+
surface,
|
|
461
|
+
resolved.path,
|
|
462
|
+
`referenced rollouts exceed the ${MAX_TOTAL_ROLLOUT_INSPECTION_BYTES} byte aggregate inspection limit`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
budget.remainingBytes -= opened.size;
|
|
454
466
|
const decoder = new TextDecoder("utf-8", { ignoreBOM: true });
|
|
455
467
|
const buffer = Buffer.allocUnsafe(ROLLOUT_READ_CHUNK_BYTES);
|
|
456
468
|
while (totalRead < opened.size) {
|
|
@@ -528,15 +540,16 @@ function classifyReferencedRollout(
|
|
|
528
540
|
function classifyReferencedRollouts(
|
|
529
541
|
surface: "history" | "history-backup",
|
|
530
542
|
references: RolloutReference[],
|
|
543
|
+
budget: RolloutInspectionBudget,
|
|
531
544
|
): NativeRoutedResidueResult {
|
|
532
545
|
for (const reference of references) {
|
|
533
|
-
const result = classifyReferencedRollout(surface, reference);
|
|
546
|
+
const result = classifyReferencedRollout(surface, reference, budget);
|
|
534
547
|
if (result.kind !== "clean") return result;
|
|
535
548
|
}
|
|
536
549
|
return { kind: "clean" };
|
|
537
550
|
}
|
|
538
551
|
|
|
539
|
-
function classifyHistoryDatabase(path: string): NativeRoutedResidueResult {
|
|
552
|
+
function classifyHistoryDatabase(path: string, budget: RolloutInspectionBudget): NativeRoutedResidueResult {
|
|
540
553
|
const resolved = resolveRegularFile(path);
|
|
541
554
|
if (resolved.kind === "absent") {
|
|
542
555
|
for (const suffix of ["-wal", "-shm"]) {
|
|
@@ -576,6 +589,7 @@ function classifyHistoryDatabase(path: string): NativeRoutedResidueResult {
|
|
|
576
589
|
rows
|
|
577
590
|
.filter(row => row.model_provider !== "opencodex")
|
|
578
591
|
.map(row => ({ id: row.id, path: row.rollout_path })),
|
|
592
|
+
budget,
|
|
579
593
|
);
|
|
580
594
|
if (rollouts.kind !== "clean") return rollouts;
|
|
581
595
|
const after = statSync(resolved.path);
|
|
@@ -600,7 +614,7 @@ function historyBackupPath(stateDatabasePath: string): string {
|
|
|
600
614
|
return legacy !== canonical && existsSync(legacy) ? legacy : canonical;
|
|
601
615
|
}
|
|
602
616
|
|
|
603
|
-
function classifyHistoryBackup(path: string, stateDatabasePath: string): NativeRoutedResidueResult {
|
|
617
|
+
function classifyHistoryBackup(path: string, stateDatabasePath: string, budget: RolloutInspectionBudget): NativeRoutedResidueResult {
|
|
604
618
|
const read = readRegularFile(path);
|
|
605
619
|
if (read.kind === "absent") return { kind: "clean" };
|
|
606
620
|
if (read.kind === "indeterminate") return indeterminate("history-backup", path, read.reason);
|
|
@@ -630,7 +644,7 @@ function classifyHistoryBackup(path: string, stateDatabasePath: string): NativeR
|
|
|
630
644
|
for (const entry of Object.values(validated.manifest.entries)) {
|
|
631
645
|
references.push({ id: entry.id, path: entry.rolloutPath });
|
|
632
646
|
}
|
|
633
|
-
const rollouts = classifyReferencedRollouts("history-backup", references);
|
|
647
|
+
const rollouts = classifyReferencedRollouts("history-backup", references, budget);
|
|
634
648
|
if (rollouts.kind !== "clean") return rollouts;
|
|
635
649
|
return entries.length > 0
|
|
636
650
|
? { kind: "residue", surface: "history-backup", path: read.path }
|
|
@@ -672,6 +686,7 @@ export function classifyNativeRoutedResidue(): NativeRoutedResidueResult {
|
|
|
672
686
|
journalPath,
|
|
673
687
|
...config.catalogTargets.map(target => target.path),
|
|
674
688
|
];
|
|
689
|
+
const budget = { remainingBytes: MAX_TOTAL_ROLLOUT_INSPECTION_BYTES };
|
|
675
690
|
const classifiers = [
|
|
676
691
|
() => classifyPartialWrites(atomicWriteTargets),
|
|
677
692
|
() => config.classification,
|
|
@@ -679,11 +694,14 @@ export function classifyNativeRoutedResidue(): NativeRoutedResidueResult {
|
|
|
679
694
|
...config.catalogTargets.map(target => () => classifyCatalogLike("catalog", target.path, target.configured)),
|
|
680
695
|
() => classifyCatalogLike("models-cache", modelsCachePath),
|
|
681
696
|
() => classifyJournal(journalPath),
|
|
682
|
-
() => classifyHistoryDatabase(stateDatabasePath),
|
|
683
|
-
() => classifyHistoryBackup(historyBackupPath(stateDatabasePath), stateDatabasePath),
|
|
697
|
+
() => classifyHistoryDatabase(stateDatabasePath, budget),
|
|
698
|
+
() => classifyHistoryBackup(historyBackupPath(stateDatabasePath), stateDatabasePath, budget),
|
|
684
699
|
];
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
700
|
+
let firstResidue: NativeRoutedResidueResult = { kind: "clean" };
|
|
701
|
+
for (const classify of classifiers) {
|
|
702
|
+
const result = classify();
|
|
703
|
+
if (result.kind === "indeterminate") return result;
|
|
704
|
+
if (result.kind === "residue" && firstResidue.kind === "clean") firstResidue = result;
|
|
705
|
+
}
|
|
706
|
+
return firstResidue;
|
|
689
707
|
}
|
package/src/codex/quota-types.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
/** Quota wire/storage shapes. This leaf must not import credential or config owners. */
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Observed usage at which new identity-matched main-account requests are refused (#5694).
|
|
4
|
+
*
|
|
5
|
+
* The policy is on by default, so this constant is what every installation without an explicit
|
|
6
|
+
* `codexMainAccountHardLock: false` admits against. 98 leaves one point of headroom under the
|
|
7
|
+
* exhausted reading Codex Desktop already treats as its own disabled send button.
|
|
8
|
+
*/
|
|
9
|
+
export const MAIN_ACCOUNT_HARD_LOCK_PERCENT = 98;
|
|
3
10
|
|
|
4
11
|
/**
|
|
5
12
|
* How recently a 100% burst reading must have been observed to exclude an account when it
|
package/src/codex/quota.ts
CHANGED
|
@@ -29,6 +29,8 @@ type QuotaDiskFile = {
|
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
type MainPolicyQuota = { identityKey: string; quota: StoredAccountQuota };
|
|
32
|
+
/** Fresh WHAM topology proof is consumed by the merge, never retained in a cache or DTO. */
|
|
33
|
+
type MainPolicyQuotaObservation = Omit<StoredAccountQuota, "updatedAt"> & { shortWindowAbsent?: true };
|
|
32
34
|
let mainPolicyQuota: MainPolicyQuota | null = null;
|
|
33
35
|
let diskHydrated = false;
|
|
34
36
|
let persistTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -199,6 +201,12 @@ function isExplicitMonthlyWindow(window: WhamUsageWindow | null | undefined): bo
|
|
|
199
201
|
&& seconds >= MONTHLY_WINDOW_MIN_SECONDS;
|
|
200
202
|
}
|
|
201
203
|
|
|
204
|
+
/** Same 24h short/long boundary as the parser; this includes a declared one-day window. */
|
|
205
|
+
function isExplicitLongWindow(window: WhamUsageWindow | null | undefined): boolean {
|
|
206
|
+
const seconds = window?.limit_window_seconds;
|
|
207
|
+
return typeof seconds === "number" && Number.isFinite(seconds) && seconds >= WEEKLY_WINDOW_MIN_SECONDS;
|
|
208
|
+
}
|
|
209
|
+
|
|
202
210
|
function isExplicitMonthlyWindowMinutes(windowMinutes: unknown): boolean {
|
|
203
211
|
const minutes = windowMinutes_(windowMinutes);
|
|
204
212
|
return minutes !== undefined && minutes >= MONTHLY_WINDOW_MIN_MINUTES;
|
|
@@ -267,12 +275,17 @@ function snapshotHasCustom(quota: Omit<StoredAccountQuota, "updatedAt">): boolea
|
|
|
267
275
|
function snapshotHasUsage(quota: Omit<StoredAccountQuota, "updatedAt">): boolean {
|
|
268
276
|
return snapshotHasWeekly(quota) || snapshotHasMonthly(quota) || snapshotHasShort(quota) || snapshotHasCustom(quota);
|
|
269
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Publish parsed display quota and separately validated main-policy evidence after writer checks.
|
|
280
|
+
* A null policy observation retains only the matching main identity's previous evidence;
|
|
281
|
+
* transient replacement markers are consumed during merging and never enter stored snapshots.
|
|
282
|
+
*/
|
|
270
283
|
export function setAccountQuotaFromParsed(
|
|
271
284
|
accountId: string,
|
|
272
285
|
quota: Omit<StoredAccountQuota, "updatedAt"> | null,
|
|
273
286
|
writerGeneration = captureConfigGeneration(),
|
|
274
287
|
mainWriter?: MainQuotaWriter,
|
|
275
|
-
policyQuota:
|
|
288
|
+
policyQuota: MainPolicyQuotaObservation | null = quota,
|
|
276
289
|
historyEvidence?: QuotaObservationEvidence,
|
|
277
290
|
): void {
|
|
278
291
|
quota = withoutRetiredCodexQuota(quota);
|
|
@@ -313,9 +326,13 @@ export function setAccountQuotaFromParsed(
|
|
|
313
326
|
}
|
|
314
327
|
}
|
|
315
328
|
|
|
316
|
-
/**
|
|
329
|
+
/**
|
|
330
|
+
* Merge a partial observation into the legacy or identity-bound policy snapshot.
|
|
331
|
+
* Policy mode retains omitted blocking short usage unless this observation authorizes replacement;
|
|
332
|
+
* the returned snapshot contains quota fields only, without the transient replacement marker.
|
|
333
|
+
*/
|
|
317
334
|
function mergeAccountQuota(
|
|
318
|
-
quota:
|
|
335
|
+
quota: MainPolicyQuotaObservation,
|
|
319
336
|
existing: StoredAccountQuota | undefined,
|
|
320
337
|
updatedAt: number,
|
|
321
338
|
policyEvidence = false,
|
|
@@ -377,7 +394,7 @@ function mergeAccountQuota(
|
|
|
377
394
|
}
|
|
378
395
|
if (quota.shortResetAt !== undefined) next.shortResetAt = quota.shortResetAt;
|
|
379
396
|
if (quota.shortWindowSeconds !== undefined) next.shortWindowSeconds = quota.shortWindowSeconds;
|
|
380
|
-
} else {
|
|
397
|
+
} else if (!policyEvidence || quota.shortWindowAbsent !== true) {
|
|
381
398
|
// Unknown usage is not a lower reading. Retain the entire known tuple: pairing
|
|
382
399
|
// its percentage with new metadata would silently extend or shorten its reset.
|
|
383
400
|
// An elapsed reset is the exception. It describes a window that has already rolled over,
|
|
@@ -793,13 +810,38 @@ function filterMainPolicyMonthlyQuota(
|
|
|
793
810
|
return hasKnownQuotaValue(filtered) || filtered.resetCredits !== undefined ? filtered : null;
|
|
794
811
|
}
|
|
795
812
|
|
|
796
|
-
/**
|
|
797
|
-
|
|
813
|
+
/**
|
|
814
|
+
* Parse ordinary main-policy usage, rejecting messages with invalid numeric window percentages.
|
|
815
|
+
* Mark a valid primary of at least 24h as replacement evidence only when both other windows
|
|
816
|
+
* are explicitly null or at least 24h. A null result supplies no usable policy observation.
|
|
817
|
+
*/
|
|
818
|
+
export function parseMainPolicyUsageQuota(data: WhamUsageResponse): MainPolicyQuotaObservation | null {
|
|
798
819
|
const windows = [data.rate_limit?.primary_window, data.rate_limit?.secondary_window, data.rate_limit?.tertiary_window];
|
|
799
820
|
if (windows.some(window => isInvalidPolicyUsagePercent(window?.used_percent))) return null;
|
|
800
|
-
|
|
821
|
+
const quota = filterMainPolicyMonthlyQuota(parseUsageQuota(data), isThirtyDayOnlyCodexPlan(data.plan_type));
|
|
822
|
+
const [primary, secondary, tertiary] = windows;
|
|
823
|
+
// WHAM explicitly reports absent windows as null; omissions cannot prove replacement.
|
|
824
|
+
// Policy trusts one complete snapshot only when every non-null window is >=24h AND
|
|
825
|
+
// carries a valid usage reading: a long window without used_percent leaves that
|
|
826
|
+
// window's usage unknown, and unknown usage must never release a block.
|
|
827
|
+
// Headers never supply this proof, and reset time alone still cannot release a block.
|
|
828
|
+
if (quota && normalizeUsagePercent(primary?.used_percent) !== undefined && isExplicitLongWindow(primary)
|
|
829
|
+
&& (secondary === null || isMeasuredLongWindow(secondary))
|
|
830
|
+
&& (tertiary === null || isMeasuredLongWindow(tertiary))) {
|
|
831
|
+
return { ...quota, shortWindowAbsent: true };
|
|
832
|
+
}
|
|
833
|
+
return quota;
|
|
801
834
|
}
|
|
802
835
|
|
|
836
|
+
function isMeasuredLongWindow(window: WhamUsageWindow | null | undefined): boolean {
|
|
837
|
+
return isExplicitLongWindow(window) && normalizeUsagePercent(window?.used_percent) !== undefined;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Normalize WHAM windows into the display snapshot, preserving declared short-window shape.
|
|
842
|
+
* Finite percentages are clamped for compatibility; policy callers must validate raw readings
|
|
843
|
+
* separately. Return null when neither a quota value/window nor reset credits are available.
|
|
844
|
+
*/
|
|
803
845
|
export function parseUsageQuota(data: WhamUsageResponse): Omit<StoredAccountQuota, "updatedAt"> | null {
|
|
804
846
|
const resetCredits = typeof data.rate_limit_reset_credits?.available_count === "number"
|
|
805
847
|
? data.rate_limit_reset_credits.available_count
|
package/src/codex/runtime.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { homedir, tmpdir } from "node:os";
|
|
|
4
4
|
import { delimiter, join } from "node:path";
|
|
5
5
|
import { atomicWriteFile, getConfigDir } from "../config";
|
|
6
6
|
import { codexExecInvocation, isSpawnableCodexCandidate } from "./exec-invocation";
|
|
7
|
+
import { resolveCodexHomeDir } from "./home";
|
|
7
8
|
import { redactSecretString, redactUserPath } from "../lib/redact";
|
|
8
9
|
|
|
9
10
|
export type CodexRuntimeSource =
|
|
@@ -508,16 +509,13 @@ function pathCandidates(deps: ResolveCodexRuntimeDeps): string[] {
|
|
|
508
509
|
function installedCodexCandidates(deps: ResolveCodexRuntimeDeps): string[] {
|
|
509
510
|
const platform = deps.platform ?? process.platform;
|
|
510
511
|
const env = deps.env ?? process.env;
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const readDir = deps.readdirSync ?? ((path: string) => readdirSync(path));
|
|
516
|
-
const stat = deps.statSync ?? ((path: string) => statSync(path));
|
|
512
|
+
const readDir = deps.readdirSync ?? ((path: string) => readdirSync(path));
|
|
513
|
+
const stat = deps.statSync ?? ((path: string) => statSync(path));
|
|
514
|
+
/** Version directories directly under root, newest first; unreadable roots yield nothing. */
|
|
515
|
+
const versionDirectories = (root: string): string[] => {
|
|
517
516
|
try {
|
|
518
|
-
const names = readDir(root);
|
|
519
517
|
const dirs: Array<{ name: string; directory: string; mtimeMs: number }> = [];
|
|
520
|
-
for (const name of
|
|
518
|
+
for (const name of readDir(root)) {
|
|
521
519
|
const directory = join(root, name);
|
|
522
520
|
try {
|
|
523
521
|
const st = stat(directory);
|
|
@@ -528,18 +526,39 @@ function installedCodexCandidates(deps: ResolveCodexRuntimeDeps): string[] {
|
|
|
528
526
|
}
|
|
529
527
|
}
|
|
530
528
|
dirs.sort((a, b) => b.mtimeMs - a.mtimeMs || a.name.localeCompare(b.name));
|
|
531
|
-
return dirs.map(entry =>
|
|
529
|
+
return dirs.map(entry => entry.directory);
|
|
532
530
|
} catch {
|
|
533
531
|
return [];
|
|
534
532
|
}
|
|
533
|
+
};
|
|
534
|
+
if (platform === "win32") {
|
|
535
|
+
const localAppData = env.LOCALAPPDATA?.trim();
|
|
536
|
+
if (!localAppData) return [];
|
|
537
|
+
return versionDirectories(join(localAppData, "OpenAI", "Codex", "bin"))
|
|
538
|
+
.map(directory => join(directory, "codex.exe"));
|
|
535
539
|
}
|
|
536
540
|
const home = env.HOME?.trim() || env.USERPROFILE?.trim() || homedir();
|
|
537
|
-
|
|
541
|
+
const posix = [
|
|
538
542
|
join(home, ".codex", "packages", "standalone", "current", "bin", "codex"),
|
|
539
543
|
join(home, ".local", "bin", "codex"),
|
|
540
544
|
"/usr/local/bin/codex",
|
|
541
545
|
"/opt/homebrew/bin/codex",
|
|
542
546
|
];
|
|
547
|
+
if (platform !== "linux") return posix;
|
|
548
|
+
// Windows Codex Desktop in WSL app-server mode ships its Linux binary under the
|
|
549
|
+
// effective Codex home as bin/wsl/<version-hash>/codex, and a Desktop update replaces
|
|
550
|
+
// that hash directory. The service PATH usually has no codex (issue 5635), so these
|
|
551
|
+
// rank after PATH and the ordinary locations and are re-enumerated on every resolve
|
|
552
|
+
// rather than trusted from a remembered hash.
|
|
553
|
+
let codexHome: string;
|
|
554
|
+
try {
|
|
555
|
+
codexHome = resolveCodexHomeDir({ env });
|
|
556
|
+
} catch {
|
|
557
|
+
return posix;
|
|
558
|
+
}
|
|
559
|
+
const desktopWsl = versionDirectories(join(codexHome, "bin", "wsl"))
|
|
560
|
+
.map(directory => join(directory, "codex"));
|
|
561
|
+
return [...posix, ...desktopWsl];
|
|
543
562
|
}
|
|
544
563
|
|
|
545
564
|
interface RankedCandidate {
|
|
@@ -735,6 +754,8 @@ function resolveCacheKey(deps: ResolveCodexRuntimeDeps): string | null {
|
|
|
735
754
|
localAppData: env.LOCALAPPDATA?.trim() ?? "",
|
|
736
755
|
homeDir: env.HOME?.trim() ?? "",
|
|
737
756
|
userProfile: env.USERPROFILE?.trim() ?? "",
|
|
757
|
+
// Linux discovery enumerates <CODEX_HOME>/bin/wsl, so the home is part of the key.
|
|
758
|
+
codexHome: env.CODEX_HOME?.trim() ?? "",
|
|
738
759
|
home: process.env.OPENCODEX_HOME ?? "",
|
|
739
760
|
persisted: persistedRuntimeCacheStamp(deps),
|
|
740
761
|
});
|
package/src/combos/failover.ts
CHANGED
|
@@ -15,6 +15,7 @@ interface TargetCooldown {
|
|
|
15
15
|
|
|
16
16
|
const DEFAULT_COOLDOWN_MS = 60_000;
|
|
17
17
|
const MAX_COOLDOWN_MS = 10 * 60_000;
|
|
18
|
+
const MAX_SERVER_DELAY_MS = 24 * 60 * 60_000;
|
|
18
19
|
/** Short cooldown for request-rate 429s (for example provider code 1302) that omit Retry-After. */
|
|
19
20
|
export const COMBO_REQUEST_RATE_COOLDOWN_MS = 5_000;
|
|
20
21
|
|
|
@@ -115,6 +116,7 @@ function parseHttpDate(value: string, now: number): number | undefined {
|
|
|
115
116
|
);
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
/** Parse a Retry-After delay, optionally retaining an upstream delay up to one day. */
|
|
118
120
|
export function parseRetryAfterMs(
|
|
119
121
|
value: string | null | undefined,
|
|
120
122
|
now = Date.now(),
|
|
@@ -122,11 +124,10 @@ export function parseRetryAfterMs(
|
|
|
122
124
|
): number | undefined {
|
|
123
125
|
const text = value?.trim();
|
|
124
126
|
if (!text) return undefined;
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
// the combo picker still independently limits how long a live request waits.
|
|
127
|
+
// Keep legacy bounded parsing for other callers. Combo cooldowns preserve
|
|
128
|
+
// multi-hour upstream delays, but never quarantine a target beyond one day.
|
|
128
129
|
const maximum = options?.preserveServerDelay === true
|
|
129
|
-
?
|
|
130
|
+
? MAX_SERVER_DELAY_MS
|
|
130
131
|
: MAX_COOLDOWN_MS;
|
|
131
132
|
if (/^\d+(?:\.\d+)?$/.test(text)) {
|
|
132
133
|
const seconds = Number(text);
|
|
@@ -200,6 +201,7 @@ export function comboCooldownRetryAfterSeconds(comboId: string, now = Date.now()
|
|
|
200
201
|
return String(Math.max(1, Math.ceil(remainingMs / 1000)));
|
|
201
202
|
}
|
|
202
203
|
|
|
204
|
+
/** Record a combo target cooldown, preferring bounded upstream retry evidence. */
|
|
203
205
|
export function coolComboTarget(
|
|
204
206
|
comboId: string,
|
|
205
207
|
target: Pick<OcxComboTarget, "provider" | "model">,
|
|
@@ -234,8 +236,7 @@ export function coolComboTarget(
|
|
|
234
236
|
message: options?.message,
|
|
235
237
|
}) ? COMBO_REQUEST_RATE_COOLDOWN_MS : DEFAULT_COOLDOWN_MS);
|
|
236
238
|
targetCooldowns.set(cooldownMapKey(comboId, target), {
|
|
237
|
-
//
|
|
238
|
-
// server lower bound (including one hour) remains authoritative.
|
|
239
|
+
// Local fallbacks are capped at ten minutes; explicit server delays at one day.
|
|
239
240
|
cooldownUntil: now + (serverDelayMs ?? Math.min(Math.max(cooldownMs, 1), MAX_COOLDOWN_MS)),
|
|
240
241
|
});
|
|
241
242
|
sweepExpiredOnWrite(now);
|
package/src/combos/resolve.ts
CHANGED
|
@@ -359,11 +359,19 @@ export function advanceComboAfterFailure(
|
|
|
359
359
|
});
|
|
360
360
|
}
|
|
361
361
|
}
|
|
362
|
+
// #5691: under `cooldownWaitPolicy: "before-last-resort"` this final synchronous pick
|
|
363
|
+
// must not dispatch an emergency target while a normal one is merely cooling. Returning
|
|
364
|
+
// null hands the decision to `pickComboTargetWithWait`, which waits a normal target out
|
|
365
|
+
// inside the combo's wait budget or dispatches the last resort when none is reachable.
|
|
366
|
+
// Without the policy the eligible set is unchanged and the pick is exactly as before.
|
|
367
|
+
const defersLastResort = combo?.cooldownWaitPolicy === "before-last-resort"
|
|
368
|
+
&& combo.targets.some(target => !target.lastResort);
|
|
362
369
|
return pickComboTarget(config, pick.comboId, {
|
|
363
370
|
exclude: pick.attempted,
|
|
364
371
|
now: options.now,
|
|
365
372
|
eligible: target => !isComboTargetInCooldown(pick.comboId, target, options.now)
|
|
366
|
-
&& (options.eligible?.(target) ?? true)
|
|
373
|
+
&& (options.eligible?.(target) ?? true)
|
|
374
|
+
&& (!defersLastResort || !target.lastResort),
|
|
367
375
|
});
|
|
368
376
|
}
|
|
369
377
|
|
|
@@ -382,23 +390,94 @@ export async function pickComboTargetWithWait(
|
|
|
382
390
|
const now = options.now ?? Date.now();
|
|
383
391
|
const excluded = new Set(options.exclude ?? []);
|
|
384
392
|
const customEligible = options.eligible;
|
|
385
|
-
const
|
|
386
|
-
!isComboTargetInCooldown(comboId, target,
|
|
393
|
+
const eligibleAt = (target: Required<OcxComboTarget>, at: number): boolean =>
|
|
394
|
+
!isComboTargetInCooldown(comboId, target, at)
|
|
387
395
|
&& (customEligible?.(target) ?? true);
|
|
388
|
-
const
|
|
389
|
-
|
|
396
|
+
const eligible = (target: Required<OcxComboTarget>): boolean => eligibleAt(target, now);
|
|
397
|
+
// Milliseconds already slept inside this call. `waitForCooldownMs` is documented as a cap
|
|
398
|
+
// per *selection attempt*, so a deferral wait and the ordinary wait below must share it —
|
|
399
|
+
// otherwise a 3s deferral followed by a 9s ordinary wait spends 12s against a 10s budget.
|
|
400
|
+
let spentWaitMs = 0;
|
|
401
|
+
|
|
402
|
+
// #5691: `cooldownWaitPolicy: "before-last-resort"` makes one extra attempt
|
|
403
|
+
// over the normal targets alone, so a *brief* cooldown on a preferred target
|
|
404
|
+
// waits rather than dispatching a target the operator marked emergency-only.
|
|
405
|
+
//
|
|
406
|
+
// It only ever defers. Every exit below falls through to the unchanged
|
|
407
|
+
// selection, which still sees the last-resort target — a policy that could
|
|
408
|
+
// withhold it when no normal target is reachable would turn a fallback into
|
|
409
|
+
// an outage, which is worse than the premature routing it prevents.
|
|
410
|
+
const policyCombo = getCombo(config, comboId);
|
|
411
|
+
const defersLastResort = policyCombo?.cooldownWaitPolicy === "before-last-resort"
|
|
412
|
+
&& policyCombo.targets.some(target => !target.lastResort);
|
|
413
|
+
if (defersLastResort && !options.abortSignal?.aborted) {
|
|
414
|
+
const normalOnly = (target: Required<OcxComboTarget>): boolean =>
|
|
415
|
+
!target.lastResort && eligible(target);
|
|
416
|
+
const normalPick = pickComboTarget(config, comboId, {
|
|
417
|
+
exclude: excluded,
|
|
418
|
+
eligible: normalOnly,
|
|
419
|
+
now,
|
|
420
|
+
});
|
|
421
|
+
if (normalPick) return normalPick;
|
|
422
|
+
|
|
423
|
+
const waitable = policyCombo.targets.filter(target =>
|
|
424
|
+
!target.lastResort
|
|
425
|
+
&& targetProviderIsUsable(config, target, now)
|
|
426
|
+
&& !excluded.has(targetKey(target))
|
|
427
|
+
&& isComboTargetInCooldown(comboId, target, now)
|
|
428
|
+
&& (customEligible?.(target) ?? true),
|
|
429
|
+
);
|
|
430
|
+
const soonest = earliestComboCooldown(comboId, waitable, now);
|
|
431
|
+
const normalDelay = soonest === undefined ? undefined : soonest.expiry - now;
|
|
432
|
+
if (normalDelay !== undefined && normalDelay <= options.waitForCooldownMs) {
|
|
433
|
+
console.warn(
|
|
434
|
+
`[combo] ${comboId}: deferring last resort, waiting ${normalDelay}ms for ${targetKey(soonest!.target)}`,
|
|
435
|
+
);
|
|
436
|
+
try {
|
|
437
|
+
await (options.sleep ?? sleepWithAbort)(normalDelay, options.abortSignal);
|
|
438
|
+
} catch (error) {
|
|
439
|
+
if (options.abortSignal?.aborted) return null;
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
442
|
+
spentWaitMs += normalDelay;
|
|
443
|
+
if (options.abortSignal?.aborted) return null;
|
|
444
|
+
// The combo can be deleted or renamed while this request sleeps.
|
|
445
|
+
if (!getCombo(config, comboId)) return null;
|
|
446
|
+
const waited = pickComboTarget(config, comboId, {
|
|
447
|
+
exclude: excluded,
|
|
448
|
+
now: now + normalDelay,
|
|
449
|
+
eligible: target => !target.lastResort
|
|
450
|
+
&& !isComboTargetInCooldown(comboId, target, now + normalDelay)
|
|
451
|
+
&& (customEligible?.(target) ?? true),
|
|
452
|
+
});
|
|
453
|
+
if (waited) return waited;
|
|
454
|
+
}
|
|
455
|
+
// No normal target is reachable. Fall through; the last resort is eligible.
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Both advance when the deferral above slept; they are identical to `now` and
|
|
459
|
+
// `options.waitForCooldownMs` when it did not, so the non-policy path is unchanged.
|
|
460
|
+
const clock = now + spentWaitMs;
|
|
461
|
+
const remainingWaitMs = options.waitForCooldownMs - spentWaitMs;
|
|
462
|
+
|
|
463
|
+
const pick = pickComboTarget(config, comboId, {
|
|
464
|
+
exclude: excluded,
|
|
465
|
+
eligible: target => eligibleAt(target, clock),
|
|
466
|
+
now: clock,
|
|
467
|
+
});
|
|
468
|
+
if (pick || remainingWaitMs <= 0 || options.abortSignal?.aborted) return pick;
|
|
390
469
|
const combo = getCombo(config, comboId);
|
|
391
470
|
if (!combo) throw new UnknownComboError(comboId);
|
|
392
471
|
const waitingTargets = combo.targets.filter(target =>
|
|
393
|
-
targetProviderIsUsable(config, target,
|
|
472
|
+
targetProviderIsUsable(config, target, clock)
|
|
394
473
|
&& !excluded.has(targetKey(target))
|
|
395
|
-
&& isComboTargetInCooldown(comboId, target,
|
|
474
|
+
&& isComboTargetInCooldown(comboId, target, clock)
|
|
396
475
|
&& (customEligible?.(target) ?? true),
|
|
397
476
|
);
|
|
398
|
-
const earliest = earliestComboCooldown(comboId, waitingTargets,
|
|
477
|
+
const earliest = earliestComboCooldown(comboId, waitingTargets, clock);
|
|
399
478
|
if (earliest === undefined) return null;
|
|
400
|
-
const delay = earliest.expiry -
|
|
401
|
-
if (delay >
|
|
479
|
+
const delay = earliest.expiry - clock;
|
|
480
|
+
if (delay > remainingWaitMs) return null;
|
|
402
481
|
// The expiry computation above is the single source of truth for the wait budget.
|
|
403
482
|
// Its target preserves configured order for ties.
|
|
404
483
|
const target = earliest.target;
|
|
@@ -416,10 +495,8 @@ export async function pickComboTargetWithWait(
|
|
|
416
495
|
if (!getCombo(config, comboId)) return null;
|
|
417
496
|
return pickComboTarget(config, comboId, {
|
|
418
497
|
exclude: excluded,
|
|
419
|
-
now:
|
|
420
|
-
eligible: targetCandidate =>
|
|
421
|
-
!isComboTargetInCooldown(comboId, targetCandidate, now + delay)
|
|
422
|
-
&& (customEligible?.(targetCandidate) ?? true),
|
|
498
|
+
now: clock + delay,
|
|
499
|
+
eligible: targetCandidate => eligibleAt(targetCandidate, clock + delay),
|
|
423
500
|
});
|
|
424
501
|
}
|
|
425
502
|
|
package/src/combos/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isCodexReasoningEffort } from "../reasoning-effort";
|
|
2
2
|
import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../codex/catalog/native-models";
|
|
3
|
-
import type { OcxComboConfig, OcxComboDefaultEffort, OcxComboDefaultEffortMode, OcxComboReasoningEffortMode, OcxComboStrategy, OcxComboTarget, OcxProviderConfig } from "../types";
|
|
3
|
+
import type { OcxComboConfig, OcxComboCooldownWaitPolicy, OcxComboDefaultEffort, OcxComboDefaultEffortMode, OcxComboReasoningEffortMode, OcxComboStrategy, OcxComboTarget, OcxProviderConfig } from "../types";
|
|
4
4
|
import { COMBO_NAMESPACE, isValidComboId, targetKey } from "./identifiers";
|
|
5
5
|
|
|
6
6
|
export const COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS = 0;
|
|
@@ -25,6 +25,8 @@ export interface NormalizedComboConfig {
|
|
|
25
25
|
stickyLimit: number;
|
|
26
26
|
cooldownMs?: number;
|
|
27
27
|
waitForCooldownMs: number;
|
|
28
|
+
/** `before-last-resort` defers lastResort targets while a normal one can be waited out (#5691). */
|
|
29
|
+
cooldownWaitPolicy: OcxComboCooldownWaitPolicy | null;
|
|
28
30
|
defaultEffort: OcxComboDefaultEffort | null;
|
|
29
31
|
/** Client-precedence policy; `fallback` preserves legacy behavior. */
|
|
30
32
|
defaultEffortMode: OcxComboDefaultEffortMode;
|
|
@@ -161,6 +163,13 @@ export function comboConfigIssues(
|
|
|
161
163
|
|| body.waitForCooldownMs > 600_000)) {
|
|
162
164
|
issues.push({ path: ["waitForCooldownMs"], message: "waitForCooldownMs must be an integer from 0 to 600000" });
|
|
163
165
|
}
|
|
166
|
+
if (body.cooldownWaitPolicy !== undefined && body.cooldownWaitPolicy !== null
|
|
167
|
+
&& body.cooldownWaitPolicy !== "before-last-resort") {
|
|
168
|
+
issues.push({
|
|
169
|
+
path: ["cooldownWaitPolicy"],
|
|
170
|
+
message: 'cooldownWaitPolicy must be "before-last-resort" when set',
|
|
171
|
+
});
|
|
172
|
+
}
|
|
164
173
|
if (body.defaultEffort !== undefined
|
|
165
174
|
&& body.defaultEffort !== null
|
|
166
175
|
&& (typeof body.defaultEffort !== "string" || !isCodexReasoningEffort(body.defaultEffort))) {
|
|
@@ -277,6 +286,12 @@ export function comboConfigIssues(
|
|
|
277
286
|
message: `targets[${i}].weight must be an integer from 1 to 10000`,
|
|
278
287
|
});
|
|
279
288
|
}
|
|
289
|
+
if (target.lastResort !== undefined && typeof target.lastResort !== "boolean") {
|
|
290
|
+
issues.push({
|
|
291
|
+
path: ["targets", i, "lastResort"],
|
|
292
|
+
message: `targets[${i}].lastResort must be a boolean`,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
280
295
|
|
|
281
296
|
if (provider && model) {
|
|
282
297
|
const key = targetKey({ provider, model });
|
|
@@ -318,6 +333,7 @@ export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig
|
|
|
318
333
|
stickyLimit: raw.stickyLimit ?? 1,
|
|
319
334
|
cooldownMs: raw.cooldownMs,
|
|
320
335
|
waitForCooldownMs: raw.waitForCooldownMs ?? COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS,
|
|
336
|
+
cooldownWaitPolicy: raw.cooldownWaitPolicy === "before-last-resort" ? "before-last-resort" : null,
|
|
321
337
|
defaultEffort,
|
|
322
338
|
defaultEffortMode: raw.defaultEffortMode === "force" && defaultEffort !== null ? "force" : "fallback",
|
|
323
339
|
reasoningEffortMode: raw.reasoningEffortMode === "adaptive" ? "adaptive" : "strict",
|
|
@@ -329,6 +345,7 @@ export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig
|
|
|
329
345
|
provider: target.provider.trim(),
|
|
330
346
|
model: target.model.trim(),
|
|
331
347
|
weight: target.weight ?? 1,
|
|
348
|
+
lastResort: target.lastResort === true,
|
|
332
349
|
})),
|
|
333
350
|
};
|
|
334
351
|
}
|
|
@@ -16,6 +16,7 @@ import { isValidProviderName } from "./provider-name";
|
|
|
16
16
|
import { MODEL_ALIAS_PATTERN } from "../providers/default-aliases";
|
|
17
17
|
import { MODEL_DISCOVERY_MAX_MODELS } from "../providers/model-discovery-limits";
|
|
18
18
|
import { getProviderRegistryEntry, providerMatchesRegistryTransport, registryModelServiceTierCapabilityApplies } from "../providers/registry";
|
|
19
|
+
import { providerFastSwitchOff } from "../providers/fast-opt-in";
|
|
19
20
|
import { isCodexReasoningEffort } from "../reasoning-effort";
|
|
20
21
|
import { refreshConfigDerivedRegistries } from "./derived-registries";
|
|
21
22
|
import { type OcxClaudeCodeConfig, type OcxConfig } from "../types";
|
|
@@ -835,6 +836,7 @@ export function inheritedFastWireConflictProviderNames(
|
|
|
835
836
|
const conflicts: string[] = [];
|
|
836
837
|
for (const [name, provider] of Object.entries(config.providers)) {
|
|
837
838
|
if (provider.fastWire !== null || provider.supportsServiceTier === false) continue;
|
|
839
|
+
if (providerFastSwitchOff(name, provider)) continue;
|
|
838
840
|
const registry = providerMatchesRegistryTransport(name, provider)
|
|
839
841
|
? getProviderRegistryEntry(name)
|
|
840
842
|
: undefined;
|
|
@@ -60,6 +60,7 @@ import { OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
|
60
60
|
import { modelAutoCompactTokenLimitsConfigError } from "../../providers/auto-compact-budget";
|
|
61
61
|
import { hasFastWireCapabilityConflict } from "../../providers/fastwire";
|
|
62
62
|
import { parseDesktopProfile } from "../../claude/desktop-profile";
|
|
63
|
+
import { isInterceptBindingId, isInterceptBindingRoute } from "../../claude/intercept/model-bindings";
|
|
63
64
|
import { DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES, MAX_APP_OWNED_MEMORY_BUDGET_MB, MIN_APP_OWNED_MEMORY_BUDGET_MB } from "../../lib/app-owned-memory";
|
|
64
65
|
|
|
65
66
|
export const configSchema = z.object({
|
|
@@ -148,7 +149,9 @@ export const configSchema = z.object({
|
|
|
148
149
|
// Ultra Fast is opt-in for the same reason and degrades the same way: a malformed hand
|
|
149
150
|
// edit turns the tier off rather than rejecting the config that carries it.
|
|
150
151
|
ultraFastTier: z.boolean().optional().catch(false),
|
|
151
|
-
|
|
152
|
+
// Default-on policy (#5694): absence and malformed hand edits both mean "on", and only an
|
|
153
|
+
// explicit `false` written by the settings PUT opts out.
|
|
154
|
+
codexMainAccountHardLock: z.boolean().optional().catch(undefined),
|
|
152
155
|
// Future versions remain opaque through passthrough-compatible whole-config saves.
|
|
153
156
|
// Only version 1 grants deletion authority in the rebase path.
|
|
154
157
|
configRebaseProvenance: z.unknown().optional(),
|
|
@@ -284,13 +287,29 @@ export const configSchema = z.object({
|
|
|
284
287
|
if (!intercept || typeof intercept !== "object" || Array.isArray(intercept)) {
|
|
285
288
|
ctx.addIssue({ code: "custom", path: ["claudeCode", "intercept"], message: "intercept must be an object" });
|
|
286
289
|
} else {
|
|
287
|
-
const { enabled, port } = intercept as { enabled?: unknown; port?: unknown };
|
|
290
|
+
const { enabled, port, picker, modelMap } = intercept as { enabled?: unknown; port?: unknown; picker?: unknown; modelMap?: unknown };
|
|
288
291
|
if (enabled !== undefined && typeof enabled !== "boolean") {
|
|
289
292
|
ctx.addIssue({ code: "custom", path: ["claudeCode", "intercept", "enabled"], message: "intercept.enabled must be a boolean" });
|
|
290
293
|
}
|
|
294
|
+
if (picker !== undefined && typeof picker !== "boolean") {
|
|
295
|
+
ctx.addIssue({ code: "custom", path: ["claudeCode", "intercept", "picker"], message: "intercept.picker must be a boolean" });
|
|
296
|
+
}
|
|
291
297
|
if (port !== undefined && (typeof port !== "number" || !Number.isInteger(port) || port < 1 || port > 65535)) {
|
|
292
298
|
ctx.addIssue({ code: "custom", path: ["claudeCode", "intercept", "port"], message: "intercept.port must be an integer between 1 and 65535" });
|
|
293
299
|
}
|
|
300
|
+
if (modelMap !== undefined) {
|
|
301
|
+
if (!modelMap || typeof modelMap !== "object" || Array.isArray(modelMap)) {
|
|
302
|
+
ctx.addIssue({ code: "custom", path: ["claudeCode", "intercept", "modelMap"], message: "intercept.modelMap must be an object of picker id to route" });
|
|
303
|
+
} else {
|
|
304
|
+
for (const [id, route] of Object.entries(modelMap as Record<string, unknown>)) {
|
|
305
|
+
if (!isInterceptBindingId(id)) {
|
|
306
|
+
ctx.addIssue({ code: "custom", path: ["claudeCode", "intercept", "modelMap", id], message: "intercept.modelMap keys must be claude- picker model ids" });
|
|
307
|
+
} else if (!isInterceptBindingRoute(route)) {
|
|
308
|
+
ctx.addIssue({ code: "custom", path: ["claudeCode", "intercept", "modelMap", id], message: "intercept.modelMap values must be non-empty routes without whitespace" });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
294
313
|
}
|
|
295
314
|
}
|
|
296
315
|
if (claude.desktopProfile !== undefined) {
|
|
@@ -292,6 +292,7 @@ export const providerConfigSchema = z.object({
|
|
|
292
292
|
annotateEmptyToolOutputs: z.boolean().optional(),
|
|
293
293
|
foldDeveloperRoleToSystem: z.boolean().optional(),
|
|
294
294
|
fastWire: fastWireSchema.nullable().optional(),
|
|
295
|
+
fastEnabled: z.boolean().optional(),
|
|
295
296
|
supportsServiceTier: z.boolean().optional(),
|
|
296
297
|
modelSupportsServiceTier: z.record(z.string().min(1), z.boolean()).optional(),
|
|
297
298
|
modelSuppressSyntheticMax: z.record(z.string().min(1), z.boolean()).optional(),
|
|
@@ -311,9 +312,11 @@ export const providerConfigSchema = z.object({
|
|
|
311
312
|
upstreamHttpVersion: z.enum(UPSTREAM_HTTP_VERSION_VALUES)
|
|
312
313
|
.nullish()
|
|
313
314
|
.transform(value => value ?? undefined),
|
|
314
|
-
// Opt-in upstream Responses WebSocket for OpenAI-compatible providers
|
|
315
|
-
//
|
|
316
|
-
// canonical ChatGPT
|
|
315
|
+
// Opt-in upstream Responses WebSocket for OpenAI-compatible providers, honored only
|
|
316
|
+
// for the first-party api.openai.com/v1 upstream; other custom endpoints stay on
|
|
317
|
+
// bounded HTTP/SSE. On the canonical ChatGPT `openai` provider the same field selects
|
|
318
|
+
// the transport: omitted keeps the upstream WebSocket on eligible turns, explicit
|
|
319
|
+
// `false` sends streaming turns over HTTP/SSE, and provider management rejects `true`.
|
|
317
320
|
upstreamWebsocket: z.boolean().optional(),
|
|
318
321
|
directGeminiWireRenames: z.boolean().optional(),
|
|
319
322
|
googleToolSchemaPolicy: z.enum(["compatible", "reject-lossy"]).optional(),
|