@bitkyc08/opencodex 2.81.0 → 2.83.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/bin/ocx.mjs +166 -73
- package/gui/dist/assets/App-Ba8uG696.js +54 -0
- package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-IImbNPRy.js} +1 -1
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/{index-RsoaAxXY.js → index-BHRZdJuv.js} +13 -13
- package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DiJO6sYY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/cursor/catalog.ts +2 -2
- package/src/adapters/cursor/effort-map.ts +1 -1
- package/src/adapters/devin/live-models.ts +2 -2
- package/src/adapters/google-truncation.ts +22 -0
- package/src/adapters/google.ts +7 -3
- package/src/adapters/ollama-native.ts +18 -14
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +11 -4
- package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
- package/src/adapters/openai-responses/reasoning.ts +11 -1
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/bridge/errors.ts +7 -1
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +22 -20
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-picker.ts +1 -1
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound.ts +77 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- package/src/cli/account-auth.ts +20 -12
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +2 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +5 -0
- package/src/cli/doctor.ts +16 -3
- package/src/cli/index.ts +14 -9
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integrations.ts +16 -3
- package/src/cli/resolve.ts +26 -3
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- package/src/clients/config-export/model-metadata.ts +16 -0
- package/src/clients/config-export/zcode-store.ts +46 -11
- package/src/clients/config-export/zcode.ts +3 -5
- package/src/clients/config-export.ts +1 -1
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/pool-mode-gate.ts +4 -5
- package/src/codex/auth-api/routes.ts +10 -49
- package/src/codex/auth-context.ts +28 -4
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/build-entries.ts +18 -0
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/effort.ts +26 -5
- package/src/codex/catalog/gather-capture.ts +5 -0
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/native-models.ts +1 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/provider-models.ts +68 -35
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/desktop-app/darwin.ts +1 -1
- package/src/codex/desktop-app/handoff.ts +1 -1
- package/src/codex/desktop-app/linux.ts +1 -1
- package/src/codex/desktop-app/lock.ts +1 -1
- package/src/codex/desktop-app/types.ts +1 -1
- package/src/codex/desktop-app/windows.ts +1 -1
- package/src/codex/desktop-app-restart.ts +1 -1
- package/src/codex/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/main-account.ts +6 -7
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -220
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +8 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/generated/compatibility-version.json +375 -227
- package/src/generated/model-metadata.ts +2 -2
- package/src/grok/inject.ts +276 -206
- package/src/grok/reset-coupon-ledger.ts +223 -57
- package/src/grok/reset-coupons.ts +13 -6
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +29 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +18 -2
- package/src/integrations/cursor-detect.ts +1 -1
- package/src/integrations/writer.ts +9 -3
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/errors.ts +25 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/request-execution-budget.ts +8 -0
- package/src/lib/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- package/src/oauth/anthropic-account-refusal.ts +39 -1
- package/src/oauth/anthropic-routing.ts +1 -1
- package/src/oauth/anthropic-send-ownership.ts +3 -3
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/index.ts +7 -3
- package/src/oauth/meta-muse-device.ts +1 -1
- package/src/oauth/store.ts +1 -1
- package/src/oauth/types.ts +1 -1
- package/src/providers/anthropic-fast.ts +1 -1
- package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +21 -0
- package/src/providers/devin-cli-authmode-migration.ts +1 -1
- package/src/providers/devin-provider-merge-migration.ts +1 -1
- package/src/providers/github-copilot-auto.ts +169 -0
- package/src/providers/model-presets.ts +1 -1
- package/src/providers/muse-subscription-usage.ts +1 -1
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota.ts +1 -1
- package/src/providers/registry/entries-core.ts +6 -6
- package/src/providers/registry/entries-extended.ts +4 -0
- package/src/providers/registry/model-seeds.ts +3 -3
- package/src/providers/xai-fast-model.ts +1 -1
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +15 -2
- package/src/responses/state/metrics.ts +8 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +12 -5
- package/src/responses/state.ts +18 -10
- package/src/responses/task-input.ts +17 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +1 -0
- package/src/server/chat-native.ts +28 -12
- package/src/server/claude-messages.ts +43 -6
- package/src/server/inbound-body-admission.ts +25 -7
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/link-listener.ts +30 -17
- package/src/server/index/serve-options.ts +18 -4
- package/src/server/index/spend-ledger-lifecycle.ts +2 -1
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +2 -1
- package/src/server/lifecycle.ts +31 -4
- package/src/server/management/agent-settings-routes.ts +93 -32
- package/src/server/management/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +59 -8
- package/src/server/management/context.ts +2 -2
- package/src/server/management/grok-coupon-routes.ts +277 -52
- package/src/server/management/link-routes.ts +42 -20
- package/src/server/management/route-registry.ts +2 -2
- package/src/server/management/shared.ts +3 -3
- package/src/server/messages-native-oauth.ts +9 -8
- package/src/server/messages-native.ts +49 -13
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +18 -7
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log-failure-attribution.ts +3 -0
- package/src/server/request-log-terminal-status.ts +186 -0
- package/src/server/request-log.ts +15 -89
- package/src/server/responses/adapter-continuation.ts +11 -3
- package/src/server/responses/adapter-dispatch.ts +99 -19
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/compact.ts +3 -2
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +20 -3
- package/src/server/responses/core-combo.ts +22 -14
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-opaque-recovery.ts +5 -5
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +73 -0
- package/src/server/responses/core.ts +9 -9
- package/src/server/responses/empty-completion-guard.ts +18 -2
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/jev-model-invoke.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +24 -2
- package/src/server/responses/passthrough-dispatch.ts +134 -62
- package/src/server/responses/passthrough-execution.ts +3 -3
- package/src/server/responses/request-prepare.ts +4 -0
- package/src/server/responses/request-send-budget.ts +112 -49
- package/src/server/responses/request-transport.ts +125 -5
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +15 -5
- package/src/server/responses/sidecar-execution.ts +30 -16
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/spend-storage-error.ts +33 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +3 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/config.ts +21 -0
- package/src/types/request.ts +10 -0
- package/src/types/wire.ts +1 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- package/src/usage/expected-prices.ts +1 -1
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-7yhoys8a.js +0 -54
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spendLedgerStorageErrorResponse } from "./spend-storage-error";
|
|
1
2
|
import { classifyAnthropic429 } from "../../oauth/anthropic-rate-limit-policy";
|
|
2
3
|
import { rotateAnthropicAccountOnResponseForInstance } from "../../oauth/anthropic-account-refusal";
|
|
3
4
|
import { authorizeResendForRecovery } from "../../lib/request-resend-gate";
|
|
@@ -134,12 +135,15 @@ export async function prepareAdapterExchange(
|
|
|
134
135
|
| "transportToken"
|
|
135
136
|
| "oauthDispatch"
|
|
136
137
|
| "imageTierBias"
|
|
138
|
+
| "oauth401ReplayAttempted"
|
|
137
139
|
| "isOAuth401ReplayProvider"
|
|
138
140
|
| "sentOAuthSnapshot"
|
|
139
141
|
| "refreshResolvedOAuthSelection"
|
|
140
142
|
| "replayOAuthCredentialSnapshot"
|
|
141
143
|
| "invalidateSameTargetRequest"
|
|
142
144
|
| "resolveSelectionAdapter"
|
|
145
|
+
| "resolveCopilotSelection"
|
|
146
|
+
| "copilotRefusalResponse"
|
|
143
147
|
| "anthropicInstance"
|
|
144
148
|
| "currentAnthropicRouteDecision"
|
|
145
149
|
| "anthropicRouteDecision"
|
|
@@ -156,6 +160,7 @@ export async function prepareAdapterExchange(
|
|
|
156
160
|
responseEffects: Pick<ResponsesEffects, "cancelResponseCompletion" | "notifyResponseComplete" | "refreshRequestToolAliases">,
|
|
157
161
|
sendBudgetState: Pick<
|
|
158
162
|
ResponsesSendBudget,
|
|
163
|
+
| "adapterSendBudget"
|
|
159
164
|
| "adapterDispatchBudget"
|
|
160
165
|
| "noteAdapterPhysicalSend"
|
|
161
166
|
| "noteAdapterRecoveryWithheld"
|
|
@@ -170,6 +175,9 @@ export async function prepareAdapterExchange(
|
|
|
170
175
|
| "workflowRootId"
|
|
171
176
|
| "claimAmbiguousResend"
|
|
172
177
|
| "sendsUsed"
|
|
178
|
+
| "targetSendsUsed"
|
|
179
|
+
| "initialSendAllowance"
|
|
180
|
+
| "noteInitialDispatch"
|
|
173
181
|
>,
|
|
174
182
|
) {
|
|
175
183
|
const { options, config, logCtx, req } = requestContext;
|
|
@@ -179,6 +187,8 @@ export async function prepareAdapterExchange(
|
|
|
179
187
|
refreshResolvedOAuthSelection,
|
|
180
188
|
invalidateSameTargetRequest,
|
|
181
189
|
resolveSelectionAdapter,
|
|
190
|
+
resolveCopilotSelection,
|
|
191
|
+
copilotRefusalResponse,
|
|
182
192
|
anthropicInstance,
|
|
183
193
|
currentAnthropicRouteDecision,
|
|
184
194
|
anthropicSessionKey,
|
|
@@ -196,6 +206,7 @@ export async function prepareAdapterExchange(
|
|
|
196
206
|
} = requestState;
|
|
197
207
|
const { cancelResponseCompletion, notifyResponseComplete, refreshRequestToolAliases } = responseEffects;
|
|
198
208
|
const {
|
|
209
|
+
adapterSendBudget,
|
|
199
210
|
adapterDispatchBudget,
|
|
200
211
|
noteAdapterPhysicalSend,
|
|
201
212
|
noteAdapterRecoveryWithheld,
|
|
@@ -219,6 +230,17 @@ export async function prepareAdapterExchange(
|
|
|
219
230
|
|
|
220
231
|
const upstream = new AbortController();
|
|
221
232
|
const cleanupUpstreamAbort = linkAbortSignal(upstream, options.abortSignal);
|
|
233
|
+
/** Contain replacement negotiation before adapter admission and release its upstream abort link. */
|
|
234
|
+
const resolveRotatedCopilotSelection = async (): Promise<Response | undefined> => {
|
|
235
|
+
try { await resolveCopilotSelection(parsed); }
|
|
236
|
+
catch (error) {
|
|
237
|
+
cleanupUpstreamAbort();
|
|
238
|
+
upstream.abort();
|
|
239
|
+
if ((options.abortSignal ?? req.signal).aborted) return clientCancelledResponse();
|
|
240
|
+
return copilotRefusalResponse(error)
|
|
241
|
+
?? formatErrorResponse(502, "upstream_error", "GitHub Copilot Auto negotiation failed.");
|
|
242
|
+
}
|
|
243
|
+
};
|
|
222
244
|
const connectMs = config.connectTimeoutMs ?? 200_000;
|
|
223
245
|
// Bridge stall budget (seconds of silence before upstream_stall_timeout); the retry backoff
|
|
224
246
|
// heartbeat interval is derived from it so the watchdog is always fed during deliberate waits.
|
|
@@ -335,10 +357,15 @@ export async function prepareAdapterExchange(
|
|
|
335
357
|
* is invisible to it and a missed bump would replay a request built with a stale key.
|
|
336
358
|
*/
|
|
337
359
|
|
|
360
|
+
const initialRecovery = route.providerName === "github-copilot"
|
|
361
|
+
? sendBudgetState.pendingHopPermit?.sendClass === "auth-recovery" ? "oauth-account-429"
|
|
362
|
+
: transportState.oauth401ReplayAttempted ? "oauth-401" : undefined
|
|
363
|
+
: undefined;
|
|
338
364
|
let upstreamResponse: Response;
|
|
365
|
+
let handoffPermit: ResponsesSendBudget["pendingHopPermit"];
|
|
339
366
|
try {
|
|
340
367
|
if (transportState.activeAdapter.fetchResponse) {
|
|
341
|
-
transportState.noteRoutedAttemptSend(inputTokenEstimate);
|
|
368
|
+
transportState.noteRoutedAttemptSend(inputTokenEstimate, initialRecovery);
|
|
342
369
|
const producer = adapterDispatchBudget?.beginSpendProducer?.();
|
|
343
370
|
try {
|
|
344
371
|
upstreamResponse = await withProviderRequestSlot(route.providerName, route.provider, route.modelId, upstream.signal, pacingSlot =>
|
|
@@ -371,31 +398,55 @@ export async function prepareAdapterExchange(
|
|
|
371
398
|
// reset-only semantics so combo failover hops on the first 5xx.
|
|
372
399
|
const transientPolicy = transientRetryPolicyFor(route.provider);
|
|
373
400
|
const resetPolicy = resetReplayPolicyFor(route.provider);
|
|
374
|
-
|
|
401
|
+
// Rebook a native Copilot hop against the negotiated destination before dispatch.
|
|
402
|
+
// Raw externally counted permits settle through the retry helper's send reporter.
|
|
403
|
+
if (route.providerName === "github-copilot" && adapterSendBudget && sendBudgetState.pendingHopPermit
|
|
404
|
+
&& sendBudgetState.pendingHopPermit.sendClass === "auth-recovery") {
|
|
405
|
+
const original = sendBudgetState.pendingHopPermit;
|
|
406
|
+
sendBudgetState.pendingHopPermit = undefined;
|
|
407
|
+
original.release();
|
|
408
|
+
const decision = adapterSendBudget.reserveDispatch({
|
|
409
|
+
sendClass: original.sendClass,
|
|
410
|
+
targetKey: `${route.providerName}|${builtInitialRequest.url}|${route.modelId}`,
|
|
411
|
+
countedExternally: true,
|
|
412
|
+
});
|
|
413
|
+
if (!decision.allowed) throw new SendBudgetExhaustedError(safeHostLabel(builtInitialRequest.url));
|
|
414
|
+
handoffPermit = decision.permit;
|
|
415
|
+
}
|
|
416
|
+
const compactPrepaid = handoffPermit ?? (options.compactionRecoveryAttempted ? sendBudgetState.pendingHopPermit : undefined);
|
|
375
417
|
if (compactPrepaid) sendBudgetState.pendingHopPermit = undefined;
|
|
376
418
|
let compactPrepaidUsed = false;
|
|
419
|
+
// A combo-owned or compaction-prepaid send is settled by the physical-dispatch receipt
|
|
420
|
+
// when spend enforcement is off. Enforced spend and direct requests keep the shared
|
|
421
|
+
// physical-send reporter, which already owns the permit lifecycle there.
|
|
422
|
+
// Stable per request: a Combo or compaction permit comes from `reserveDispatch`, which starts
|
|
423
|
+
// (and freezes) the spend policy first, so `spendEnforced` cannot change before dispatch.
|
|
424
|
+
const receiptMode = !sendBudgetState.adapterSendBudget?.spendEnforced
|
|
425
|
+
&& Boolean(options.comboInitialSend || compactPrepaid);
|
|
377
426
|
// Combo and emergency compaction admission already book this target's first send.
|
|
378
427
|
// Its configured initial ceiling is target-local; the shared remainder below still
|
|
379
428
|
// accounts for earlier targets without deducting their sends from this target twice.
|
|
380
429
|
const initialSendCap = transientPolicy || resetPolicy
|
|
430
|
+
|| (options.comboAttempt && route.provider.adapter === "google")
|
|
381
431
|
? transientSendCapFor(transientPolicy?.attempts,
|
|
382
432
|
(options.comboAttempt || compactPrepaid) ? 0 : sendBudgetState.sendsUsed)
|
|
383
433
|
: 1;
|
|
384
434
|
const fetchWithRetryPolicy = (route.provider.adapter === "google" || transientPolicy)
|
|
385
435
|
? fetchWithTransientRetry
|
|
386
436
|
: fetchWithResetRetry;
|
|
387
|
-
upstreamResponse = await fetchWithRetryPolicy(
|
|
437
|
+
try { upstreamResponse = await fetchWithRetryPolicy(
|
|
388
438
|
recovery => {
|
|
389
439
|
// Unconfigured generic sends may omit the counting reporter. Capture policy
|
|
390
440
|
// independently so a later recovery still belongs to this request's start.
|
|
391
441
|
sendBudgetState.adapterSendBudget?.startRequest?.({
|
|
392
442
|
poolId: logCtx.spendPoolId ?? route.providerName, identityId: logCtx.accountLogLabel,
|
|
393
443
|
});
|
|
394
|
-
|
|
444
|
+
// Receipt mode books the physical send at executor admission instead of here.
|
|
445
|
+
if (!receiptMode && compactPrepaid && !compactPrepaidUsed) {
|
|
395
446
|
if (!compactPrepaid.use()) throw new SendBudgetExhaustedError(safeHostLabel(builtInitialRequest.url));
|
|
396
447
|
compactPrepaidUsed = true;
|
|
397
448
|
}
|
|
398
|
-
transportState.noteRoutedAttemptSend(inputTokenEstimate, recovery);
|
|
449
|
+
transportState.noteRoutedAttemptSend(inputTokenEstimate, recovery ?? initialRecovery);
|
|
399
450
|
return fetchWithHeaderTimeout(builtInitialRequest.url, applyUpstreamRecoveryInit({
|
|
400
451
|
method: builtInitialRequest.method,
|
|
401
452
|
headers: builtInitialRequest.headers,
|
|
@@ -405,6 +456,14 @@ export async function prepareAdapterExchange(
|
|
|
405
456
|
dispatchOverride: oauthDispatch(builtInitialRequest),
|
|
406
457
|
providerName: route.providerName,
|
|
407
458
|
modelId: route.modelId,
|
|
459
|
+
onPhysicalDispatch: () => {
|
|
460
|
+
// Preserve the unconfigured direct-adapter accounting contract; Combo admission
|
|
461
|
+
// and explicit retry/compaction budgets require a receipt for their booked send.
|
|
462
|
+
if (receiptMode) {
|
|
463
|
+
sendBudgetState.noteInitialDispatch(compactPrepaid && !compactPrepaidUsed ? compactPrepaid : undefined);
|
|
464
|
+
compactPrepaidUsed = true;
|
|
465
|
+
}
|
|
466
|
+
},
|
|
408
467
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
409
468
|
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
410
469
|
}));
|
|
@@ -413,27 +472,34 @@ export async function prepareAdapterExchange(
|
|
|
413
472
|
abortSignal: upstream.signal,
|
|
414
473
|
label: safeHostLabel(builtInitialRequest.url),
|
|
415
474
|
claimAmbiguousResend: claimPreHeaderResend,
|
|
416
|
-
...(sendBudgetState.adapterSendBudget?.spendEnforced || options.comboDispatchPermit || transientPolicy || resetPolicy || compactPrepaid
|
|
475
|
+
...(sendBudgetState.adapterSendBudget?.spendEnforced || options.comboDispatchPermit || transientPolicy || resetPolicy || compactPrepaid || options.comboAttempt
|
|
417
476
|
? {
|
|
418
477
|
// A pending compaction permit already booked this leg's first physical send.
|
|
419
|
-
...(transientPolicy || resetPolicy || compactPrepaid
|
|
478
|
+
...(transientPolicy || resetPolicy || compactPrepaid || options.comboAttempt
|
|
420
479
|
? {
|
|
421
480
|
attempts: Math.min(initialSendCap,
|
|
422
|
-
|
|
481
|
+
sendBudgetState.initialSendAllowance(initialSendCap) + (compactPrepaid ? 1 : 0)),
|
|
423
482
|
}
|
|
424
483
|
: {}),
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
|
|
484
|
+
// Reporter path: direct requests and enforced spend. Receipt-mode sends are
|
|
485
|
+
// settled by `onPhysicalDispatch`, so a reporter here would count them twice.
|
|
486
|
+
...(receiptMode
|
|
487
|
+
? {}
|
|
488
|
+
: { onSendsConsumed: transientSendReporter(compactPrepaid ?? options.comboDispatchPermit) }),
|
|
428
489
|
}
|
|
429
490
|
: {}),
|
|
430
491
|
},
|
|
431
|
-
);
|
|
492
|
+
); } finally { compactPrepaid?.release(); }
|
|
432
493
|
}
|
|
433
494
|
} catch (err) {
|
|
434
495
|
cleanupUpstreamAbort();
|
|
435
496
|
upstream.abort();
|
|
436
497
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
498
|
+
const storageRefusal = spendLedgerStorageErrorResponse(err, logCtx);
|
|
499
|
+
if (storageRefusal) {
|
|
500
|
+
releaseCodexAuthContextProbeLease(admissionState.authCtx);
|
|
501
|
+
return storageRefusal;
|
|
502
|
+
}
|
|
437
503
|
const refusal = err instanceof UpstreamRetryEvidenceError ? err.cause : err;
|
|
438
504
|
const codexRefusal = mapCodexAuthContextErrorToResponse(unwrapUpstreamRetryEvidenceError(err), {
|
|
439
505
|
now: Date.now(), accountSelector: route.codexAccountNamespace,
|
|
@@ -464,6 +530,7 @@ export async function prepareAdapterExchange(
|
|
|
464
530
|
const msg = describeUpstreamConnectFailure(err, connectMs);
|
|
465
531
|
return formatErrorResponse(502, "upstream_error", msg);
|
|
466
532
|
} finally {
|
|
533
|
+
handoffPermit?.release();
|
|
467
534
|
builtInitialRequest.releaseBodyObservation?.();
|
|
468
535
|
}
|
|
469
536
|
|
|
@@ -487,7 +554,7 @@ export async function prepareAdapterExchange(
|
|
|
487
554
|
// Console Go answers a transient 400 "Invalid upload request." for bodies it accepts
|
|
488
555
|
// moments later; at most one byte-identical replay is allowed per request.
|
|
489
556
|
const consoleGoUploadRetryGuard: { attempted: boolean } = { attempted: false };
|
|
490
|
-
let oauth401ReplayAttempted =
|
|
557
|
+
let oauth401ReplayAttempted = transportState.oauth401ReplayAttempted;
|
|
491
558
|
let antigravityAuthRotationAttempted = false;
|
|
492
559
|
// At most one reasoning-effort downgrade per request. This sits outside the recovery loop
|
|
493
560
|
// below for the same reason the two guards above do: a guard declared inside it is reset by
|
|
@@ -609,8 +676,7 @@ export async function prepareAdapterExchange(
|
|
|
609
676
|
const helperCountsSends = sendBudgetState.adapterSendBudget?.spendEnforced === true || refetchTransientPolicy !== null || resetReplayPolicyFor(route.provider) !== null;
|
|
610
677
|
const prepaid = sendBudgetState.pendingHopPermit;
|
|
611
678
|
const configuredTotal = refetchTransientPolicy?.attempts;
|
|
612
|
-
const refetchCap = transientSendCapFor(configuredTotal,
|
|
613
|
-
sendBudgetState.sendsUsed - (prepaid ? 1 : 0));
|
|
679
|
+
const refetchCap = transientSendCapFor(configuredTotal, sendBudgetState.targetSendsUsed);
|
|
614
680
|
// An exact total includes a booked hop, even when it consumed the last base slot.
|
|
615
681
|
// Keep that funded send while forbidding the final reserve from widening the total.
|
|
616
682
|
const prepaidLastSlot = helperCountsSends && configuredTotal !== undefined && prepaid
|
|
@@ -671,6 +737,13 @@ export async function prepareAdapterExchange(
|
|
|
671
737
|
retryRequest.releaseBodyObservation?.();
|
|
672
738
|
}
|
|
673
739
|
} catch (err) {
|
|
740
|
+
const storageRefusal = !options.abortSignal?.aborted && spendLedgerStorageErrorResponse(err, logCtx);
|
|
741
|
+
if (storageRefusal) {
|
|
742
|
+
cleanupUpstreamAbort();
|
|
743
|
+
upstream.abort();
|
|
744
|
+
releaseCodexAuthContextProbeLease(admissionState.authCtx);
|
|
745
|
+
return { failed: storageRefusal };
|
|
746
|
+
}
|
|
674
747
|
const codexRefusal = !options.abortSignal?.aborted && mapCodexAuthContextErrorToResponse(unwrapUpstreamRetryEvidenceError(err), {
|
|
675
748
|
now: Date.now(), accountSelector: route.codexAccountNamespace,
|
|
676
749
|
});
|
|
@@ -772,7 +845,7 @@ export async function prepareAdapterExchange(
|
|
|
772
845
|
&& !oauth401ReplayAttempted
|
|
773
846
|
&& !sendBudgetExhausted()
|
|
774
847
|
) {
|
|
775
|
-
oauth401ReplayAttempted = true;
|
|
848
|
+
oauth401ReplayAttempted = transportState.oauth401ReplayAttempted = true;
|
|
776
849
|
let refreshed: OAuthAccessSnapshot;
|
|
777
850
|
try {
|
|
778
851
|
refreshed = await refreshResolvedOAuthSelection(transportState.sentOAuthSnapshot);
|
|
@@ -843,7 +916,7 @@ export async function prepareAdapterExchange(
|
|
|
843
916
|
if (route.providerName === "kiro") {
|
|
844
917
|
parsed._kiroAuthContext = { ...(refreshed.kiro ?? {}) };
|
|
845
918
|
}
|
|
846
|
-
|
|
919
|
+
let refreshedProvider = resolveProviderTransport(
|
|
847
920
|
route.providerName,
|
|
848
921
|
{
|
|
849
922
|
...route.provider,
|
|
@@ -856,6 +929,9 @@ export async function prepareAdapterExchange(
|
|
|
856
929
|
: undefined,
|
|
857
930
|
);
|
|
858
931
|
route.provider = refreshedProvider;
|
|
932
|
+
const copilotRefusal = await resolveRotatedCopilotSelection();
|
|
933
|
+
if (copilotRefusal) return copilotRefusal;
|
|
934
|
+
refreshedProvider = route.provider;
|
|
859
935
|
invalidateSameTargetRequest();
|
|
860
936
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
861
937
|
resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire, route.staticPolicy),
|
|
@@ -908,6 +984,8 @@ export async function prepareAdapterExchange(
|
|
|
908
984
|
// until runtime cleanup (one per rotated key).
|
|
909
985
|
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
910
986
|
route.provider = rotated;
|
|
987
|
+
const selectionFailure = await resolveRotatedCopilotSelection();
|
|
988
|
+
if (selectionFailure) return selectionFailure;
|
|
911
989
|
invalidateSameTargetRequest();
|
|
912
990
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
913
991
|
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
@@ -1054,6 +1132,8 @@ export async function prepareAdapterExchange(
|
|
|
1054
1132
|
// until runtime cleanup (one per rotated key under a rate-limit storm).
|
|
1055
1133
|
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
1056
1134
|
route.provider = rotated;
|
|
1135
|
+
const selectionFailure = await resolveRotatedCopilotSelection();
|
|
1136
|
+
if (selectionFailure) return selectionFailure;
|
|
1057
1137
|
invalidateSameTargetRequest();
|
|
1058
1138
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
1059
1139
|
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
@@ -1076,7 +1156,7 @@ export async function prepareAdapterExchange(
|
|
|
1076
1156
|
// Anthropic OAuth: recover a rate limit or proven account entitlement refusal
|
|
1077
1157
|
// before output, within the shared request and account rotation limits.
|
|
1078
1158
|
while (
|
|
1079
|
-
(upstreamResponse.status === 429 || upstreamResponse.status === 403)
|
|
1159
|
+
(upstreamResponse.status === 429 || upstreamResponse.status === 403 || upstreamResponse.status === 401)
|
|
1080
1160
|
&& anthropicInstance
|
|
1081
1161
|
&& transportState.anthropicPoolAccountId
|
|
1082
1162
|
) {
|
|
@@ -1101,7 +1181,7 @@ export async function prepareAdapterExchange(
|
|
|
1101
1181
|
);
|
|
1102
1182
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
|
|
1103
1183
|
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, transportState.activeAdapter.name);
|
|
1104
|
-
const result = await rebuildAndRefetch("anthropic-oauth-429");
|
|
1184
|
+
const result = await rebuildAndRefetch(upstreamResponse.status === 401 ? "oauth-401" : "anthropic-oauth-429");
|
|
1105
1185
|
if ("failed" in result) return result.failed;
|
|
1106
1186
|
upstreamResponse = result;
|
|
1107
1187
|
if (isNonReplayableResponse(upstreamResponse)) continue recovery;
|
|
@@ -9,7 +9,7 @@ import { CODEX_RESPONSES_HTTP_URL, type PreparedCodexWsRequest } from "./codex-w
|
|
|
9
9
|
import { CodexWsCorrelation } from "./codex-ws-correlation";
|
|
10
10
|
import type { CodexWsSession } from "./codex-ws-session";
|
|
11
11
|
import { UPGRADE_DEADLINE_MS, CODEX_WS_LIVENESS_PING_INTERVAL_MS, CODEX_WS_RESPONSE_PRELUDE_TIMEOUT_MS, MAX_CODEX_WS_FRAME_BYTES,
|
|
12
|
-
MAX_CODEX_WS_QUEUE_BYTES, markCodexWsResponse, normalizeResponsesWsRelayEvent, closedBeforeTerminalMessage,
|
|
12
|
+
MAX_CODEX_WS_QUEUE_BYTES, markCodexWsResponse, markCodexWsPreludeProjection, normalizeResponsesWsRelayEvent, closedBeforeTerminalMessage,
|
|
13
13
|
codexWsCreateFrameExceedsLimit, codexWsFailureDetail, codexWsPreResponseFailure, markCodexWsStage, codexWsOcxVersion,
|
|
14
14
|
markCodexWsSocketDeath, type CodexWsFailureStage, type CodexWsStageRecord } from "./codex-ws-wire";
|
|
15
15
|
|
|
@@ -23,6 +23,7 @@ interface ExchangeOptions {
|
|
|
23
23
|
sseFallback: typeof globalThis.fetch;
|
|
24
24
|
onQuota?: CodexWsQuotaObserver;
|
|
25
25
|
beforeDispatch?: (headers: Headers) => void;
|
|
26
|
+
onPhysicalDispatch?: () => void;
|
|
26
27
|
/** Bun version string the caller gated on; stamped onto the stage record. */
|
|
27
28
|
bunVersion?: string;
|
|
28
29
|
}
|
|
@@ -44,7 +45,8 @@ function rejectionHeaders(source: Record<string, unknown>, prelude: Headers): He
|
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
// Reuse the metadata owner's count/value/family budgets and window freshness
|
|
47
|
-
// rules, without publishing quota twice.
|
|
48
|
+
// rules, without publishing quota twice. Pool bookkeeping consumes the HTTP
|
|
49
|
+
// projection; plain-main publication belongs only to the WS observer.
|
|
48
50
|
const projected = new CodexWsMetadata();
|
|
49
51
|
try {
|
|
50
52
|
for (const values of [Object.fromEntries(prelude), source]) {
|
|
@@ -240,6 +242,7 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
240
242
|
// are left out: their channel may already have sent continuation frames on this socket, so
|
|
241
243
|
// the create frame alone no longer describes the turn.
|
|
242
244
|
if (socketDied && !nativeControl) markCodexWsSocketDeath(failureResponse, stage);
|
|
245
|
+
markCodexWsPreludeProjection(failureResponse);
|
|
243
246
|
resolve(failureResponse);
|
|
244
247
|
return;
|
|
245
248
|
}
|
|
@@ -411,6 +414,17 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
411
414
|
failStream(error);
|
|
412
415
|
return;
|
|
413
416
|
}
|
|
417
|
+
try { options.onPhysicalDispatch?.(); } catch (error) {
|
|
418
|
+
// Admission refused before the frame left. This is a pre-open rejection,
|
|
419
|
+
// not a transport failure and not an authorization to replay over SSE.
|
|
420
|
+
settledPreOpen = true;
|
|
421
|
+
sent = false;
|
|
422
|
+
terminal = true;
|
|
423
|
+
cleanup();
|
|
424
|
+
session.dispose();
|
|
425
|
+
reject(error);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
414
428
|
try {
|
|
415
429
|
ws.send(frameText);
|
|
416
430
|
sentAt = Date.now();
|
|
@@ -501,6 +515,7 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
501
515
|
cleanup();
|
|
502
516
|
try { controller.close(); } catch { /* unused stream already closed */ }
|
|
503
517
|
session.dispose();
|
|
518
|
+
markCodexWsPreludeProjection(rejection);
|
|
504
519
|
resolve(rejection);
|
|
505
520
|
return;
|
|
506
521
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHmac, randomBytes } from "node:crypto";
|
|
2
|
+
import { codexWsReuseAcrossTurnsEnabled } from "../../config/codex-ws-reuse-setting";
|
|
2
3
|
import { registerOptionalShutdownHook } from "../../lib/optional-shutdown-hooks";
|
|
3
4
|
import { CODEX_RESPONSES_HTTP_URL, CODEX_WS_FRAME_HEADERS } from "./codex-ws-request";
|
|
4
5
|
import { CODEX_WS_ID_MAX_BYTES } from "./codex-ws-correlation";
|
|
@@ -7,11 +8,15 @@ import { CodexWsSession } from "./codex-ws-session";
|
|
|
7
8
|
export const CODEX_WS_POOL_MAX_SESSIONS = 32;
|
|
8
9
|
export const CODEX_WS_POOL_IDLE_MS = 30_000;
|
|
9
10
|
export const CODEX_WS_POOL_MAX_AGE_MS = 5 * 60_000;
|
|
11
|
+
export const CODEX_WS_POOL_WAIT_MS = 750;
|
|
12
|
+
/** Retained sockets per account+thread while cross-turn reuse is on. */
|
|
13
|
+
export const CODEX_WS_POOL_SCOPE_LIMIT = 2;
|
|
14
|
+
const CODEX_WS_POOL_WAIT_DEPTH = 2;
|
|
10
15
|
const MUTABLE_HEADERS = new Set<string>(CODEX_WS_FRAME_HEADERS);
|
|
11
16
|
let processKey: Buffer | undefined;
|
|
12
17
|
let poolSequence = 0;
|
|
13
18
|
|
|
14
|
-
export interface CodexWsReuseIdentity { key: string; scope: string }
|
|
19
|
+
export interface CodexWsReuseIdentity { key: string; scope: string; routeTag: string }
|
|
15
20
|
function value(value: unknown): value is string {
|
|
16
21
|
return typeof value === "string" && value.trim().length > 0
|
|
17
22
|
&& !/[\u0000-\u001f\u007f]/.test(value) && Buffer.byteLength(value) <= CODEX_WS_ID_MAX_BYTES;
|
|
@@ -49,19 +54,23 @@ export function codexWsReuseIdentity(url: string, headers: Record<string, string
|
|
|
49
54
|
const immutable = Object.entries(headers).filter(([name]) => !MUTABLE_HEADERS.has(name)).sort(([a], [b]) => a.localeCompare(b));
|
|
50
55
|
if (immutable.length > 128 || immutable.some(([, field]) => !value(field))
|
|
51
56
|
|| immutable.reduce((bytes, [name, field]) => bytes + Buffer.byteLength(name) + Buffer.byteLength(field), 0) > 32 * 1024) return null;
|
|
52
|
-
const
|
|
57
|
+
const reuseAcrossTurns = codexWsReuseAcrossTurnsEnabled();
|
|
58
|
+
const scope = digest(reuseAcrossTurns ? [url, account, thread] : [url, account, thread, turn]);
|
|
53
59
|
const lite = metadata.ws_request_header_x_openai_internal_codex_responses_lite;
|
|
54
60
|
if (lite !== undefined && lite !== "true" && lite !== "false") return null;
|
|
55
61
|
// `dialUrl` is the socket destination after plugin rewrites; absent means the canonical one.
|
|
56
|
-
|
|
62
|
+
const routeTag = digest(["route", authorization, proxy ?? null, dialUrl ?? null]);
|
|
63
|
+
return { scope, routeTag, key: digest([scope, authorization, body.model, body.service_tier ?? null, lite ?? null, immutable, proxy ?? null, dialUrl ?? null]) };
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
interface Entry { identity: CodexWsReuseIdentity; session: CodexWsSession; createdAt: number; idleAt: number; retired: boolean }
|
|
60
|
-
interface
|
|
67
|
+
interface Waiter { key: string; resolve: (session: CodexWsSession | null) => void; timer: ReturnType<typeof setTimeout>; onAbort: () => void; signal?: AbortSignal }
|
|
68
|
+
interface PoolOptions { now?: () => number; maxSessions?: number; idleMs?: number; maxAgeMs?: number; waitMs?: number }
|
|
61
69
|
|
|
62
70
|
/** Bounded retained sockets only. Busy/capacity misses keep the existing one-shot path. */
|
|
63
71
|
export class CodexWsPool {
|
|
64
72
|
private readonly entries = new Map<string, Entry>();
|
|
73
|
+
private readonly waiters: Waiter[] = [];
|
|
65
74
|
private timer?: ReturnType<typeof setTimeout>;
|
|
66
75
|
private detachShutdown?: () => void;
|
|
67
76
|
private readonly hookKey = `codex-upstream-ws-pool-${++poolSequence}`;
|
|
@@ -69,29 +78,74 @@ export class CodexWsPool {
|
|
|
69
78
|
private readonly maxSessions: number;
|
|
70
79
|
private readonly idleMs: number;
|
|
71
80
|
private readonly maxAgeMs: number;
|
|
81
|
+
private readonly waitMs: number;
|
|
72
82
|
constructor(options: PoolOptions = {}) {
|
|
73
83
|
this.now = options.now ?? Date.now;
|
|
74
84
|
this.maxSessions = options.maxSessions ?? CODEX_WS_POOL_MAX_SESSIONS;
|
|
75
85
|
this.idleMs = options.idleMs ?? CODEX_WS_POOL_IDLE_MS;
|
|
76
86
|
this.maxAgeMs = options.maxAgeMs ?? CODEX_WS_POOL_MAX_AGE_MS;
|
|
87
|
+
this.waitMs = options.waitMs ?? CODEX_WS_POOL_WAIT_MS;
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
acquire(identity: CodexWsReuseIdentity, url: string, headers: Record<string, string>, proxy?: string): CodexWsSession | null {
|
|
80
|
-
this.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
91
|
+
return this.claim(identity, url, headers, proxy).session;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Before any frame is sent, wait for the matching busy socket instead of dialing a throwaway.
|
|
96
|
+
* The queue is FIFO and at most two deep. An abort or the deadline returns null.
|
|
97
|
+
*/
|
|
98
|
+
acquireWaiting(
|
|
99
|
+
identity: CodexWsReuseIdentity,
|
|
100
|
+
url: string,
|
|
101
|
+
headers: Record<string, string>,
|
|
102
|
+
proxy: string | undefined,
|
|
103
|
+
signal?: AbortSignal,
|
|
104
|
+
): Promise<CodexWsSession | null> {
|
|
105
|
+
const claimed = this.claim(identity, url, headers, proxy);
|
|
106
|
+
if (claimed.session || !claimed.busy) return Promise.resolve(claimed.session);
|
|
107
|
+
if (signal?.aborted) return Promise.resolve(null);
|
|
108
|
+
if (this.waiters.filter(waiter => waiter.key === identity.key).length >= CODEX_WS_POOL_WAIT_DEPTH) {
|
|
109
|
+
return Promise.resolve(null);
|
|
85
110
|
}
|
|
111
|
+
return new Promise(resolve => {
|
|
112
|
+
const waiter: Waiter = {
|
|
113
|
+
key: identity.key,
|
|
114
|
+
resolve,
|
|
115
|
+
signal,
|
|
116
|
+
timer: setTimeout(() => this.finishWaiter(waiter, null), this.waitMs),
|
|
117
|
+
onAbort: () => this.finishWaiter(waiter, null),
|
|
118
|
+
};
|
|
119
|
+
waiter.timer.unref?.();
|
|
120
|
+
signal?.addEventListener("abort", waiter.onAbort, { once: true });
|
|
121
|
+
this.waiters.push(waiter);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private claim(
|
|
126
|
+
identity: CodexWsReuseIdentity,
|
|
127
|
+
url: string,
|
|
128
|
+
headers: Record<string, string>,
|
|
129
|
+
proxy?: string,
|
|
130
|
+
): { session: CodexWsSession | null; busy: boolean } {
|
|
131
|
+
this.sweep();
|
|
132
|
+
this.retireSiblings(identity);
|
|
86
133
|
const existing = this.entries.get(identity.key);
|
|
87
134
|
if (existing) {
|
|
88
|
-
if (existing.retired
|
|
89
|
-
if (existing.session.
|
|
135
|
+
if (existing.retired) return { session: null, busy: false };
|
|
136
|
+
if (existing.session.busy) return { session: null, busy: true };
|
|
137
|
+
if (existing.session.reserve()) { this.arm(); return { session: existing.session, busy: false }; }
|
|
90
138
|
this.remove(existing);
|
|
139
|
+
} else if (codexWsReuseAcrossTurnsEnabled() && this.scopeCount(identity.scope) >= CODEX_WS_POOL_SCOPE_LIMIT) {
|
|
140
|
+
const oldest = [...this.entries.values()]
|
|
141
|
+
.filter(entry => entry.identity.scope === identity.scope && !entry.session.busy)
|
|
142
|
+
.sort((left, right) => left.idleAt - right.idleAt)[0];
|
|
143
|
+
if (!oldest) return { session: null, busy: false };
|
|
144
|
+
this.remove(oldest);
|
|
91
145
|
}
|
|
92
146
|
if (this.entries.size >= this.maxSessions) {
|
|
93
147
|
const oldest = [...this.entries.values()].filter(entry => !entry.session.busy).sort((a, b) => a.idleAt - b.idleAt)[0];
|
|
94
|
-
if (!oldest) return null;
|
|
148
|
+
if (!oldest) return { session: null, busy: false };
|
|
95
149
|
this.remove(oldest);
|
|
96
150
|
}
|
|
97
151
|
const createdAt = this.now();
|
|
@@ -100,13 +154,45 @@ export class CodexWsPool {
|
|
|
100
154
|
session.reserve();
|
|
101
155
|
this.entries.set(identity.key, entry);
|
|
102
156
|
this.detachShutdown ??= registerOptionalShutdownHook(this.hookKey, () => this.dispose());
|
|
103
|
-
return session;
|
|
157
|
+
return { session, busy: false };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private retireSiblings(identity: CodexWsReuseIdentity): void {
|
|
161
|
+
const reuseAcrossTurns = codexWsReuseAcrossTurnsEnabled();
|
|
162
|
+
for (const entry of this.entries.values()) {
|
|
163
|
+
if (entry.identity.scope !== identity.scope || entry.identity.key === identity.key) continue;
|
|
164
|
+
const routeChanged = entry.identity.routeTag !== identity.routeTag;
|
|
165
|
+
if (reuseAcrossTurns && !routeChanged) continue;
|
|
166
|
+
entry.retired = true;
|
|
167
|
+
if (!entry.session.busy) this.remove(entry);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private scopeCount(scope: string): number {
|
|
172
|
+
let count = 0;
|
|
173
|
+
for (const entry of this.entries.values()) if (entry.identity.scope === scope && !entry.retired) count += 1;
|
|
174
|
+
return count;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private finishWaiter(waiter: Waiter, session: CodexWsSession | null): void {
|
|
178
|
+
const index = this.waiters.indexOf(waiter);
|
|
179
|
+
if (index < 0) return;
|
|
180
|
+
this.waiters.splice(index, 1);
|
|
181
|
+
clearTimeout(waiter.timer);
|
|
182
|
+
waiter.signal?.removeEventListener("abort", waiter.onAbort);
|
|
183
|
+
waiter.resolve(session);
|
|
104
184
|
}
|
|
105
185
|
|
|
106
186
|
private changed(entry: Entry): void {
|
|
107
187
|
if (this.entries.get(entry.identity.key) !== entry) return;
|
|
108
188
|
if (entry.session.closed) this.entries.delete(entry.identity.key);
|
|
109
189
|
else if (!entry.session.busy) {
|
|
190
|
+
const waiter = this.waiters.find(candidate => candidate.key === entry.identity.key);
|
|
191
|
+
if (waiter && !entry.retired && entry.session.reserve()) {
|
|
192
|
+
this.finishWaiter(waiter, entry.session);
|
|
193
|
+
this.arm();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
110
196
|
entry.idleAt = this.now();
|
|
111
197
|
if (entry.retired || entry.idleAt - entry.createdAt >= this.maxAgeMs) this.remove(entry);
|
|
112
198
|
}
|
|
@@ -152,6 +238,7 @@ export class CodexWsPool {
|
|
|
152
238
|
this.detachShutdown = undefined;
|
|
153
239
|
const entries = [...this.entries.values()];
|
|
154
240
|
this.entries.clear();
|
|
241
|
+
for (const waiter of [...this.waiters]) this.finishWaiter(waiter, null);
|
|
155
242
|
for (const entry of entries) entry.session.dispose(new DOMException("codex websocket pool shutdown", "AbortError"));
|
|
156
243
|
}
|
|
157
244
|
|
|
@@ -49,6 +49,7 @@ const WS_CLOSE_MESSAGE_TOO_BIG = 1009;
|
|
|
49
49
|
|
|
50
50
|
const codexWsUpstreamResponses = new WeakSet<Response>();
|
|
51
51
|
const quotaObservedResponses = new WeakSet<Response>();
|
|
52
|
+
const codexWsPreludeProjections = new WeakSet<Response>();
|
|
52
53
|
|
|
53
54
|
/** Quota arrived directly at its captured account; do not replay old HTTP prelude headers. */
|
|
54
55
|
export function isCodexWsQuotaObservedResponse(response: Response): boolean {
|
|
@@ -60,6 +61,14 @@ export function isCodexWsUpstreamResponse(response: Response): boolean {
|
|
|
60
61
|
return codexWsUpstreamResponses.has(response);
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
/** Pre-response projection; its headers retain the exchange's prelude snapshot. */
|
|
65
|
+
export function markCodexWsPreludeProjection(response: Response): void {
|
|
66
|
+
codexWsPreludeProjections.add(response);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function isCodexWsPreludeProjection(response: Response): boolean {
|
|
70
|
+
return codexWsPreludeProjections.has(response);
|
|
71
|
+
}
|
|
63
72
|
|
|
64
73
|
export function markCodexWsResponse(response: Response, observed: boolean): void {
|
|
65
74
|
codexWsUpstreamResponses.add(response);
|
|
@@ -10,5 +10,7 @@ export function comboRequestedEffortLabel(
|
|
|
10
10
|
const forcedTransition = forcedEffort != null && forcedEffort !== originalRequestedEffort
|
|
11
11
|
? `->${forcedEffort}`
|
|
12
12
|
: "";
|
|
13
|
-
|
|
13
|
+
const segments = `${originalRequestedEffort}${forcedTransition}${laterTransitions}`.split("->");
|
|
14
|
+
// A pin that kept the effort it was given is not a transition; drop the repeated segment.
|
|
15
|
+
return segments.filter((segment, index) => index === 0 || segment !== segments[index - 1]).join("->");
|
|
14
16
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spendLedgerStorageErrorResponse } from "./spend-storage-error";
|
|
1
2
|
import { createPhysicalSendReporter } from "../../lib/request-execution-budget";
|
|
2
3
|
import { createInferenceSendBudget } from "../inference/context";
|
|
3
4
|
import { unboundPoolSpendRefusalResponse } from "../workflow-refusal";
|
|
@@ -1099,11 +1100,11 @@ export async function handleResponsesCompact(
|
|
|
1099
1100
|
let outcomeCtx = authCtx;
|
|
1100
1101
|
const localDispatchRefusal = (error: unknown): Response | undefined => {
|
|
1101
1102
|
const cause = unwrapUpstreamRetryEvidenceError(error);
|
|
1102
|
-
const response = cause instanceof SendBudgetExhaustedError
|
|
1103
|
+
const response = spendLedgerStorageErrorResponse(error, logCtx) ?? (cause instanceof SendBudgetExhaustedError
|
|
1103
1104
|
? unboundPoolSpendRefusalResponse(logCtx) ?? formatErrorResponse(429, "request_send_budget_exhausted", cause.message)
|
|
1104
1105
|
: mapCodexAuthContextErrorToResponse(cause, {
|
|
1105
1106
|
now: Date.now(), accountSelector: route.codexAccountNamespace,
|
|
1106
|
-
});
|
|
1107
|
+
}));
|
|
1107
1108
|
if (response) {
|
|
1108
1109
|
releaseUpstreamHostAdmission(compactHostAdmissionLease);
|
|
1109
1110
|
compactHostAdmissionLease = null;
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
computeQuotaCooldown,
|
|
7
7
|
formatCodexProviderForLog,
|
|
8
8
|
} from "../../codex/routing";
|
|
9
|
+
import { claimMainQuotaDispatchForWs, isMainQuotaDispatchLive, renewMainQuotaDispatchForAttempt, type MainQuotaDispatch } from "../../codex/main-account-cache";
|
|
9
10
|
import type { CodexWsQuotaObserver } from "./codex-ws-metadata";
|
|
10
11
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
11
12
|
import { isCodexAccountGenerationLive } from "../../codex/account-store";
|
|
@@ -121,8 +122,28 @@ export function usesCodexForwardPoolAuth(
|
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
|
|
125
|
+
/** Live proof that this plain-main response used the observed main credential. */
|
|
126
|
+
export function liveMainQuotaDispatch(
|
|
127
|
+
authCtx: CodexAuthContext, provider: OcxProviderConfig,
|
|
128
|
+
): MainQuotaDispatch | undefined {
|
|
129
|
+
if (authCtx.kind !== "main" || !authCtx.mainQuotaDispatch) return undefined;
|
|
130
|
+
if (!isCanonicalOpenAiForwardProvider(provider)
|
|
131
|
+
|| provider.authMode !== "forward" || provider.adapter !== "openai-responses") return undefined;
|
|
132
|
+
return isMainQuotaDispatchLive(authCtx.mainQuotaDispatch) ? authCtx.mainQuotaDispatch : undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
124
135
|
export function codexWsQuotaObserver(authCtx: CodexAuthContext, provider: OcxProviderConfig, modelId?: string): CodexWsQuotaObserver | undefined {
|
|
125
|
-
if (!isCanonicalOpenAiForwardProvider(provider)
|
|
136
|
+
if (!isCanonicalOpenAiForwardProvider(provider)) return undefined;
|
|
137
|
+
if (!usesCodexForwardPoolAuth(authCtx, provider)) {
|
|
138
|
+
const captured = liveMainQuotaDispatch(authCtx, provider);
|
|
139
|
+
if (!captured || authCtx.kind !== "main") return undefined;
|
|
140
|
+
const dispatch = authCtx.mainQuotaDispatch = renewMainQuotaDispatchForAttempt(captured);
|
|
141
|
+
return headers => {
|
|
142
|
+
claimMainQuotaDispatchForWs(dispatch);
|
|
143
|
+
if (!isMainQuotaDispatchLive(dispatch)) return;
|
|
144
|
+
applyCapturedCodexQuota(MAIN_CODEX_ACCOUNT_ID, headers, dispatch.configGeneration, dispatch.writer, { modelId });
|
|
145
|
+
};
|
|
146
|
+
}
|
|
126
147
|
const { accountId, writerGeneration } = authCtx;
|
|
127
148
|
const credentialGeneration = authCtx.kind === "pool" ? authCtx.generation : undefined;
|
|
128
149
|
const mainWriter = authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined;
|