@bitkyc08/opencodex 2.58.0 → 2.60.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/README.md +28 -10
- package/gui/dist/assets/index-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +34 -1
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-request.ts +8 -5
- package/src/adapters/cursor/request-builder.ts +21 -4
- package/src/adapters/cursor/thread-continuity.ts +105 -31
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +45 -2
- package/src/adapters/devin/cloud-direct/chat.ts +14 -3
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
- package/src/adapters/devin.ts +125 -26
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +55 -4
- package/src/adapters/google-http.ts +57 -11
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +60 -10
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +10 -11
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +25 -4
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +28 -35
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +35 -4
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +26 -6
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/dispatch.ts +57 -3
- package/src/cli/doctor.ts +218 -4
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +95 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +2 -0
- package/src/client/machine-api.ts +6 -5
- package/src/client/machine-listener.ts +16 -3
- package/src/client/runtime.ts +26 -2
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +146 -5
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +44 -10
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +16 -12
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +22 -2
- package/src/codex/catalog/model-hints.ts +66 -33
- package/src/codex/catalog/parsing.ts +90 -5
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +41 -26
- package/src/codex/catalog/routed-gather.ts +61 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +24 -167
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject.ts +3 -2
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +31 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +80 -2
- package/src/codex/observed-model-denials.ts +230 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +289 -16
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +20 -16
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +114 -2
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +125 -5
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +43 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +33 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +31 -7
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +25 -0
- package/src/config/schema/leaf-validators.ts +76 -17
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +410 -258
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +82 -15
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +53 -5
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +741 -0
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +332 -35
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +41 -27
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +327 -25
- package/src/providers/model-rename-migration.ts +56 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +131 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +87 -21
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +32 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +9 -2
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +13 -5
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +30 -5
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +24 -25
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +48 -7
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +136 -34
- package/src/server/request-log.ts +80 -3
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +74 -30
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +63 -30
- package/src/server/responses/compact.ts +89 -18
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +90 -24
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +16 -15
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +68 -2
- package/src/server/responses/passthrough-delivery.ts +41 -15
- package/src/server/responses/passthrough-dispatch.ts +145 -53
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +96 -17
- package/src/server/responses/request-send-budget.ts +89 -8
- package/src/server/responses/request-sidecar-auth.ts +17 -9
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +15 -12
- package/src/server/responses/run-turn-execution.ts +45 -8
- package/src/server/responses/sidecar-execution.ts +19 -2
- package/src/server/responses/ws-upstream.ts +16 -28
- package/src/server/responses-custom-tool-repair.ts +29 -56
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +37 -10
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/service/cli.ts +16 -6
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +111 -6
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +9 -1
- package/src/types/tools.ts +14 -0
- package/src/types/wire.ts +9 -1
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-BbrHOIY0.js +0 -128
- package/gui/dist/assets/index-C5-RdDmD.css +0 -1
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
hasKeyPoolFailover,
|
|
26
26
|
rotateProviderTransportOn401,
|
|
27
27
|
rateLimitRetryDelayMs,
|
|
28
|
+
readQuotaResetAt,
|
|
28
29
|
rotateProviderTransportOn429,
|
|
29
30
|
} from "../../providers/key-failover";
|
|
30
31
|
import {
|
|
@@ -127,6 +128,7 @@ export async function prepareAdapterExchange(
|
|
|
127
128
|
ResponsesSendBudget,
|
|
128
129
|
| "adapterDispatchBudget"
|
|
129
130
|
| "noteAdapterPhysicalSend"
|
|
131
|
+
| "noteAdapterRecoveryWithheld"
|
|
130
132
|
| "remainingTransientSendBudget"
|
|
131
133
|
| "noteTransientSends"
|
|
132
134
|
| "recoverySendAllowance"
|
|
@@ -160,6 +162,7 @@ export async function prepareAdapterExchange(
|
|
|
160
162
|
const {
|
|
161
163
|
adapterDispatchBudget,
|
|
162
164
|
noteAdapterPhysicalSend,
|
|
165
|
+
noteAdapterRecoveryWithheld,
|
|
163
166
|
remainingTransientSendBudget,
|
|
164
167
|
noteTransientSends,
|
|
165
168
|
recoverySendAllowance,
|
|
@@ -243,7 +246,11 @@ export async function prepareAdapterExchange(
|
|
|
243
246
|
return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
|
|
244
247
|
}
|
|
245
248
|
try {
|
|
246
|
-
initialRequest = await transportState.activeAdapter.buildRequest(parsed, {
|
|
249
|
+
initialRequest = await transportState.activeAdapter.buildRequest(parsed, {
|
|
250
|
+
headers: requestState.selectedForwardHeaders,
|
|
251
|
+
translatorBudget,
|
|
252
|
+
abortSignal: upstream.signal,
|
|
253
|
+
});
|
|
247
254
|
refreshRequestToolAliases(initialRequest);
|
|
248
255
|
recordAdapterReasoning(logCtx, initialRequest);
|
|
249
256
|
recordAdapterTier(logCtx, initialRequest);
|
|
@@ -286,6 +293,7 @@ export async function prepareAdapterExchange(
|
|
|
286
293
|
timeoutMs: connectMs,
|
|
287
294
|
sendBudget: adapterDispatchBudget,
|
|
288
295
|
onPhysicalSend: send => noteAdapterPhysicalSend(inputTokenEstimate, send),
|
|
296
|
+
onRecoveryWithheld: noteAdapterRecoveryWithheld,
|
|
289
297
|
stream: parsed.stream,
|
|
290
298
|
executor: providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
291
299
|
pacingSlotAcquired: true,
|
|
@@ -401,6 +409,7 @@ export async function prepareAdapterExchange(
|
|
|
401
409
|
retryRequest = await transportState.activeAdapter.buildRequest(parsed, {
|
|
402
410
|
headers: requestState.selectedForwardHeaders,
|
|
403
411
|
translatorBudget,
|
|
412
|
+
abortSignal: upstream.signal,
|
|
404
413
|
...(transportState.imageTierBias > 0 ? { imageTierBias: transportState.imageTierBias } : {}),
|
|
405
414
|
});
|
|
406
415
|
recordAdapterReasoning(logCtx, retryRequest);
|
|
@@ -442,6 +451,7 @@ export async function prepareAdapterExchange(
|
|
|
442
451
|
timeoutMs: connectMs,
|
|
443
452
|
sendBudget: adapterDispatchBudget,
|
|
444
453
|
onPhysicalSend: send => noteAdapterPhysicalSend(retryEstimate, send),
|
|
454
|
+
onRecoveryWithheld: noteAdapterRecoveryWithheld,
|
|
445
455
|
stream: parsed.stream,
|
|
446
456
|
executor: providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
447
457
|
pacingSlotAcquired: true,
|
|
@@ -525,17 +535,17 @@ export async function prepareAdapterExchange(
|
|
|
525
535
|
return { failed: formatErrorResponse(502, "upstream_error", msg) };
|
|
526
536
|
}
|
|
527
537
|
};
|
|
528
|
-
|
|
529
|
-
|
|
538
|
+
// Keep recovery kinds in sync with the native Responses `passthroughRecovery:` loop above.
|
|
539
|
+
recovery: for (;;) {
|
|
530
540
|
// Preserve the terminal verdict through adapter and combo error formatting.
|
|
531
541
|
// This also covers a reset reached by a 401/429/413 recovery refetch.
|
|
532
542
|
if (isNonReplayableResponse(upstreamResponse)) {
|
|
533
543
|
cleanupUpstreamAbort();
|
|
534
544
|
return upstreamResponse;
|
|
535
545
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
546
|
+
if (
|
|
547
|
+
upstreamResponse.status === 401
|
|
548
|
+
&& isOAuth401ReplayProvider
|
|
539
549
|
&& transportState.sentOAuthSnapshot
|
|
540
550
|
&& !oauth401ReplayAttempted
|
|
541
551
|
&& !sendBudgetExhausted()
|
|
@@ -576,7 +586,7 @@ export async function prepareAdapterExchange(
|
|
|
576
586
|
route.provider = refreshedProvider;
|
|
577
587
|
invalidateSameTargetRequest();
|
|
578
588
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
579
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire),
|
|
589
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire, route.staticPolicy),
|
|
580
590
|
config.cacheRetention,
|
|
581
591
|
);
|
|
582
592
|
bindRouteReasoningReplayScope({
|
|
@@ -609,7 +619,7 @@ export async function prepareAdapterExchange(
|
|
|
609
619
|
route.provider = rotated;
|
|
610
620
|
invalidateSameTargetRequest();
|
|
611
621
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
612
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
622
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
613
623
|
config.cacheRetention,
|
|
614
624
|
);
|
|
615
625
|
bindRouteReasoningReplayScope({
|
|
@@ -618,15 +628,15 @@ export async function prepareAdapterExchange(
|
|
|
618
628
|
provider: route.provider,
|
|
619
629
|
adapterName: transportState.activeAdapter.name,
|
|
620
630
|
});
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
631
|
+
const result = await rebuildAndRefetch("key-401");
|
|
632
|
+
if ("failed" in result) return result.failed;
|
|
633
|
+
upstreamResponse = result;
|
|
624
634
|
// A recovery refetch can itself die on an ambiguous pre-header reset, and the refusal
|
|
625
635
|
// that answers it is a 429. Every arm below keys on 429, so letting it fall through
|
|
626
636
|
// hands the marked refusal to the next waiting arm and replays the send it exists to
|
|
627
637
|
// stop. Re-enter the loop guard instead, which returns it unchanged.
|
|
628
638
|
if (isNonReplayableResponse(upstreamResponse)) continue recovery;
|
|
629
|
-
|
|
639
|
+
}
|
|
630
640
|
|
|
631
641
|
// Same-target 429 wait-and-retry (opt-in `retryOn429`, issue #487). Codex never retries
|
|
632
642
|
// 429 itself (it retries 5xx only), and single-key pools cannot use the failover below,
|
|
@@ -663,23 +673,44 @@ export async function prepareAdapterExchange(
|
|
|
663
673
|
upstream.abort();
|
|
664
674
|
return clientCancelledResponse();
|
|
665
675
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
676
|
+
const result = await rebuildAndRefetch("rate-limit-429");
|
|
677
|
+
if ("failed" in result) return result.failed;
|
|
678
|
+
upstreamResponse = result;
|
|
669
679
|
// The refusal is a 429 too: without this the while condition is still true and the
|
|
670
680
|
// next configured attempt replays it on the same target.
|
|
671
681
|
if (isNonReplayableResponse(upstreamResponse)) continue recovery;
|
|
672
|
-
|
|
682
|
+
}
|
|
673
683
|
|
|
674
684
|
// Multi-key 429 failover: rotate to the next pool key (cooldown-aware) and retry the
|
|
675
685
|
// SAME request once per remaining key. OAuth/forward providers and single-key pools
|
|
676
686
|
// return null immediately, so this stays a no-op for them (src/providers/key-failover.ts).
|
|
677
687
|
while (upstreamResponse.status === 429 && hasKeyPoolFailover(route.provider)) {
|
|
688
|
+
// A quota exhaustion is dated in the BODY, not in `Retry-After` — OpenRouter
|
|
689
|
+
// sends no header for it (#4024). Read a bounded prefix before the socket is
|
|
690
|
+
// released below; a failed or slow read just leaves the header path in charge.
|
|
691
|
+
// Peeks a bounded prefix and hands back a Response still carrying the whole
|
|
692
|
+
// body, so the cancel below still releases the socket.
|
|
693
|
+
let peeked: Awaited<ReturnType<typeof readQuotaResetAt>>;
|
|
694
|
+
try {
|
|
695
|
+
peeked = await readQuotaResetAt(upstreamResponse, { signal: options.abortSignal });
|
|
696
|
+
} catch {
|
|
697
|
+
cleanupUpstreamAbort();
|
|
698
|
+
upstream.abort();
|
|
699
|
+
return clientCancelledResponse();
|
|
700
|
+
}
|
|
701
|
+
if (options.abortSignal?.aborted) {
|
|
702
|
+
cleanupUpstreamAbort();
|
|
703
|
+
upstream.abort();
|
|
704
|
+
return clientCancelledResponse();
|
|
705
|
+
}
|
|
706
|
+
upstreamResponse = peeked.response;
|
|
707
|
+
const quotaResetAt = peeked.at;
|
|
678
708
|
const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
|
|
679
709
|
retryAfter: upstreamResponse.headers.get("retry-after"),
|
|
680
710
|
now: Date.now(),
|
|
681
711
|
attemptedKey: route.provider.apiKey,
|
|
682
712
|
promptCacheKey: parsed.options.promptCacheKey,
|
|
713
|
+
quotaResetAt,
|
|
683
714
|
});
|
|
684
715
|
if (!rotated) break;
|
|
685
716
|
// Release the failed response's socket before retrying; unread bodies otherwise linger
|
|
@@ -688,7 +719,7 @@ export async function prepareAdapterExchange(
|
|
|
688
719
|
route.provider = rotated;
|
|
689
720
|
invalidateSameTargetRequest();
|
|
690
721
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
691
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
722
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
692
723
|
config.cacheRetention,
|
|
693
724
|
);
|
|
694
725
|
bindRouteReasoningReplayScope({
|
|
@@ -697,13 +728,13 @@ export async function prepareAdapterExchange(
|
|
|
697
728
|
provider: route.provider,
|
|
698
729
|
adapterName: transportState.activeAdapter.name,
|
|
699
730
|
});
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
731
|
+
const result = await rebuildAndRefetch("key-429");
|
|
732
|
+
if ("failed" in result) return result.failed;
|
|
733
|
+
upstreamResponse = result;
|
|
703
734
|
// Rotating on the refusal would also write a cooldown against a key that rate-limited
|
|
704
735
|
// nothing, which outlives the request.
|
|
705
736
|
if (isNonReplayableResponse(upstreamResponse)) continue recovery;
|
|
706
|
-
|
|
737
|
+
}
|
|
707
738
|
|
|
708
739
|
// Opt-in Anthropic OAuth account pool (#294): cool the failed account and retry
|
|
709
740
|
// with another eligible OAuth account (bounded per request). Disabled by default.
|
|
@@ -731,16 +762,16 @@ export async function prepareAdapterExchange(
|
|
|
731
762
|
invalidateSameTargetRequest();
|
|
732
763
|
logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
|
|
733
764
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
734
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
765
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
735
766
|
config.cacheRetention,
|
|
736
767
|
);
|
|
737
768
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
|
|
738
769
|
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, transportState.activeAdapter.name);
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
770
|
+
const result = await rebuildAndRefetch("anthropic-oauth-429");
|
|
771
|
+
if ("failed" in result) return result.failed;
|
|
772
|
+
upstreamResponse = result;
|
|
742
773
|
if (isNonReplayableResponse(upstreamResponse)) continue recovery;
|
|
743
|
-
|
|
774
|
+
} catch {
|
|
744
775
|
break;
|
|
745
776
|
}
|
|
746
777
|
}
|
|
@@ -786,6 +817,8 @@ export async function prepareAdapterExchange(
|
|
|
786
817
|
route.providerName,
|
|
787
818
|
transportState.genericFailoverAccountId,
|
|
788
819
|
upstreamResponse.headers.get("retry-after"),
|
|
820
|
+
Date.now(),
|
|
821
|
+
route.modelId,
|
|
789
822
|
);
|
|
790
823
|
if (!nextAccountId) {
|
|
791
824
|
hop.permit?.release();
|
|
@@ -804,7 +837,7 @@ export async function prepareAdapterExchange(
|
|
|
804
837
|
}
|
|
805
838
|
invalidateSameTargetRequest();
|
|
806
839
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
807
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
840
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
808
841
|
config.cacheRetention,
|
|
809
842
|
);
|
|
810
843
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
|
|
@@ -832,12 +865,12 @@ export async function prepareAdapterExchange(
|
|
|
832
865
|
hop.permit?.release();
|
|
833
866
|
return result.failed;
|
|
834
867
|
}
|
|
835
|
-
|
|
868
|
+
upstreamResponse = result;
|
|
836
869
|
// The hop's permit is already settled by the dispatch boundary above; continuing
|
|
837
870
|
// only skips the remaining arms, it does not abandon a reservation.
|
|
838
871
|
if (isNonReplayableResponse(upstreamResponse)) continue recovery;
|
|
839
|
-
|
|
840
|
-
|
|
872
|
+
} catch {
|
|
873
|
+
// A throw before the send — snapshot fetch, credential application, adapter
|
|
841
874
|
// resolution — must hand the reservation back. Without this the ladder charges the
|
|
842
875
|
// request for a send it never made, and a later recovery in the same request is
|
|
843
876
|
// refused on an allowance nothing spent. release() is idempotent and a no-op once
|
|
@@ -10,6 +10,7 @@ import { parseRequest } from "../../responses/parser";
|
|
|
10
10
|
import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
|
|
11
11
|
import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../../adapters/openai-responses";
|
|
12
12
|
import { expandPreviousResponseInput, previousResponseProviderState, rememberResponseState } from "../../responses/state";
|
|
13
|
+
import { repairLegacyDottedToolCallNames } from "../../responses/legacy-dotted-tool-name-repair";
|
|
13
14
|
import { NoEligiblePolicyCandidateError, routeCompactionModel } from "../../router";
|
|
14
15
|
import { evidenceFromBody } from "../../routing/request-evidence";
|
|
15
16
|
import {
|
|
@@ -107,6 +108,7 @@ import {
|
|
|
107
108
|
} from "../../codex/upstream-host-health";
|
|
108
109
|
import {
|
|
109
110
|
ForwardAdmissionCredentialError,
|
|
111
|
+
contextPrincipalIdOf,
|
|
110
112
|
hasForwardableCodexBearer,
|
|
111
113
|
validateForwardAdmissionCredential,
|
|
112
114
|
} from "../auth-cors";
|
|
@@ -114,12 +116,21 @@ import type { DataPlaneAdmission } from "../auth-cors";
|
|
|
114
116
|
import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
|
|
115
117
|
import { CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint } from "../../providers/openai-tiers";
|
|
116
118
|
import { NATIVE_RESERVE_MODEL } from "../../codex/catalog/native-models";
|
|
117
|
-
import {
|
|
119
|
+
import {
|
|
120
|
+
isCodexReserveOptInMissing,
|
|
121
|
+
isCodexReserveRequestEligible,
|
|
122
|
+
CODEX_RESERVE_OPT_IN_REQUIRED_MESSAGE,
|
|
123
|
+
} from "../../codex/loopback-target";
|
|
118
124
|
import { slugsEquivalent } from "../../providers/slug-codec";
|
|
119
125
|
import { decideTier, tierValueAfterDecision } from "../../providers/fastwire";
|
|
120
126
|
import { fastPolicyForModel } from "../../providers/service-tier";
|
|
121
127
|
import { parseFastOnlyRowId } from "../fast-row";
|
|
122
|
-
import {
|
|
128
|
+
import { captureOpenAiVirtualWirePolicy, resolveOpenAiCompactModel } from "../../providers/openai-virtual-models";
|
|
129
|
+
import {
|
|
130
|
+
applyCompactionRoutingOverride,
|
|
131
|
+
compactionRoutingKeepsProviderIdentity,
|
|
132
|
+
type CompactionRoutingOverride,
|
|
133
|
+
} from "./compaction-routing";
|
|
123
134
|
import { isUsageDebugEnabled } from "../../usage/debug";
|
|
124
135
|
import {
|
|
125
136
|
readJsonRequestBody,
|
|
@@ -210,8 +221,33 @@ function pruneCompactHandoffRoutes(now: number): void {
|
|
|
210
221
|
}
|
|
211
222
|
}
|
|
212
223
|
|
|
213
|
-
|
|
214
|
-
|
|
224
|
+
/**
|
|
225
|
+
* The handoff map is process-global, so a caller-controlled lane header alone
|
|
226
|
+
* cannot be the key: two authenticated clients sending the same lane header
|
|
227
|
+
* would share one fallback route. Namespace the lane by the admitted principal.
|
|
228
|
+
* A loopback or missing admission has no authenticated identity to bind this
|
|
229
|
+
* cross-request state to, so it is ineligible rather than trusted.
|
|
230
|
+
*/
|
|
231
|
+
function compactHandoffRouteKey(req: Request, admission: DataPlaneAdmission | undefined): string | null {
|
|
232
|
+
const lane = sessionLaneIdFromRequest(req.headers);
|
|
233
|
+
if (!lane || !admission || admission.kind === "loopback") return null;
|
|
234
|
+
// Fail closed rather than substituting a weaker identity. `keyId` survives a
|
|
235
|
+
// rotation and every identity-less environment admission would collapse into
|
|
236
|
+
// one bucket, which is the collision this key exists to prevent. Production
|
|
237
|
+
// admission always mints `contextPrincipalId` for configured and environment
|
|
238
|
+
// holders, so no real authenticated caller loses the route.
|
|
239
|
+
const principal = contextPrincipalIdOf(admission);
|
|
240
|
+
if (!principal) return null;
|
|
241
|
+
return `${principal}\u0000${lane}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function rememberCompactHandoffRoute(
|
|
245
|
+
req: Request,
|
|
246
|
+
admission: DataPlaneAdmission | undefined,
|
|
247
|
+
model: string,
|
|
248
|
+
now = Date.now(),
|
|
249
|
+
): void {
|
|
250
|
+
const key = compactHandoffRouteKey(req, admission);
|
|
215
251
|
if (!key || model.length > COMPACT_HANDOFF_MODEL_MAX_LENGTH) return;
|
|
216
252
|
pruneCompactHandoffRoutes(now);
|
|
217
253
|
compactHandoffRoutes.delete(key);
|
|
@@ -219,13 +255,18 @@ function rememberCompactHandoffRoute(req: Request, model: string, now = Date.now
|
|
|
219
255
|
pruneCompactHandoffRoutes(now);
|
|
220
256
|
}
|
|
221
257
|
|
|
222
|
-
function forgetCompactHandoffRoute(req: Request): void {
|
|
223
|
-
const key =
|
|
258
|
+
function forgetCompactHandoffRoute(req: Request, admission?: DataPlaneAdmission): void {
|
|
259
|
+
const key = compactHandoffRouteKey(req, admission);
|
|
224
260
|
if (key) compactHandoffRoutes.delete(key);
|
|
225
261
|
}
|
|
226
262
|
|
|
227
|
-
function compactHandoffRoute(
|
|
228
|
-
|
|
263
|
+
function compactHandoffRoute(
|
|
264
|
+
req: Request,
|
|
265
|
+
admission: DataPlaneAdmission | undefined,
|
|
266
|
+
previousModel: string,
|
|
267
|
+
now = Date.now(),
|
|
268
|
+
): string | null {
|
|
269
|
+
const key = compactHandoffRouteKey(req, admission);
|
|
229
270
|
if (!key) return null;
|
|
230
271
|
pruneCompactHandoffRoutes(now);
|
|
231
272
|
const entry = compactHandoffRoutes.get(key);
|
|
@@ -236,6 +277,7 @@ function compactHandoffRoute(req: Request, previousModel: string, now = Date.now
|
|
|
236
277
|
}
|
|
237
278
|
|
|
238
279
|
export interface HandleResponsesCompactOptions {
|
|
280
|
+
compactionRoutingOverride?: CompactionRoutingOverride | null;
|
|
239
281
|
nativeMainRefreshDependencies?: NativeMainRefreshDependencies;
|
|
240
282
|
/** Release the listener's idle guard only after the complete request body is accepted. */
|
|
241
283
|
onRequestBodyRead?: () => void;
|
|
@@ -568,6 +610,9 @@ export async function handleResponsesCompact(
|
|
|
568
610
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
569
611
|
return formatErrorResponse(400, "invalid_request_error", "Invalid compaction request body");
|
|
570
612
|
}
|
|
613
|
+
if (!options.compactionRoutingOverride) {
|
|
614
|
+
options = { ...options, compactionRoutingOverride: applyCompactionRoutingOverride(body, req.headers, config, { endpoint: "compact" }) };
|
|
615
|
+
}
|
|
571
616
|
const raw = body as { model?: unknown; input?: unknown };
|
|
572
617
|
if (typeof raw.model !== "string" || raw.model.length === 0) {
|
|
573
618
|
return formatErrorResponse(400, "invalid_request_error", "compaction request requires a model");
|
|
@@ -582,11 +627,12 @@ export async function handleResponsesCompact(
|
|
|
582
627
|
// The client's own selector, kept for the request log: `raw.model` is rewritten to the
|
|
583
628
|
// base id above, and logCtx.requestedModel is assigned from it further down, so without
|
|
584
629
|
// this the log would lose which id the client actually asked for.
|
|
585
|
-
const compactRequestedModel =
|
|
630
|
+
const compactRequestedModel = options.compactionRoutingOverride?.sourceModel
|
|
631
|
+
?? (compactFastRow ? compactFastRow.baseId + "--fast" : raw.model);
|
|
586
632
|
|
|
587
633
|
// Recall the last completed client-visible bare model after a combo switch (#3891).
|
|
588
634
|
// Configured selectors take precedence over this implicit session hint.
|
|
589
|
-
if (typeof compactModel === "string" && !compactModel.includes("/") && !compactFastRow
|
|
635
|
+
if (!options.compactionRoutingOverride && typeof compactModel === "string" && !compactModel.includes("/") && !compactFastRow
|
|
590
636
|
&& !resolveComboId(config, compactModel)) {
|
|
591
637
|
const recalledComboId = recallComboForLane(config, sessionLaneIdFromRequest(req.headers), compactModel);
|
|
592
638
|
if (recalledComboId) {
|
|
@@ -632,9 +678,24 @@ export async function handleResponsesCompact(
|
|
|
632
678
|
? `${route.providerName}-${route.codexAccountNamespace}`
|
|
633
679
|
: route.providerName;
|
|
634
680
|
logCtx.providerAdapter = route.provider.adapter;
|
|
681
|
+
// #4940, and the same refusal the ordinary Responses path makes in request-prepare.ts. Compact has
|
|
682
|
+
// to repeat it rather than inherit it: the native branch below dispatches straight to
|
|
683
|
+
// `/responses/compact` and only the routed fallback replays through handleResponses, so relying on
|
|
684
|
+
// that one seam would leave the native compact turn forwarding a Reserve model the opt-in has made
|
|
685
|
+
// unservable. Composed from the same three facts, in the same order, as `customReserveForward`
|
|
686
|
+
// further down, and placed before the virtual-model rewrite so it reads the model the caller
|
|
687
|
+
// actually selected -- and before auth, host-circuit admission, or any upstream byte.
|
|
688
|
+
//
|
|
689
|
+
// No terminal-helper or inbound-wire qualifier is needed here the way it is on the ordinary path:
|
|
690
|
+
// this endpoint is the native Codex compaction wire, and a vision/search helper never reaches it.
|
|
691
|
+
if (isCodexReserveOptInMissing(config, selectedModelId, admission)
|
|
692
|
+
&& isCanonicalOpenAiForwardProvider(route.provider)) {
|
|
693
|
+
return formatErrorResponse(400, "invalid_request_error", CODEX_RESERVE_OPT_IN_REQUIRED_MESSAGE);
|
|
694
|
+
}
|
|
635
695
|
const virtual = resolveOpenAiCompactModel(route.providerName, selectedModelId);
|
|
636
696
|
if (virtual) {
|
|
637
697
|
route.modelId = virtual.wireModelId;
|
|
698
|
+
captureOpenAiVirtualWirePolicy(route, virtual);
|
|
638
699
|
logCtx.model = virtual.selectedModelId;
|
|
639
700
|
logCtx.resolvedModel = virtual.wireModelId;
|
|
640
701
|
} else {
|
|
@@ -698,7 +759,12 @@ export async function handleResponsesCompact(
|
|
|
698
759
|
// no budget at all, so `handleResponsesInner` minted a fresh four after the native attempt
|
|
699
760
|
// had already spent some of the first one.
|
|
700
761
|
const sendBudget: RequestExecutionBudget = options.sendBudget ?? createRequestExecutionBudget();
|
|
701
|
-
|
|
762
|
+
// A manual override onto another backend must not mint ciphertext the conversation model cannot replay.
|
|
763
|
+
const manualOverrideCrossesProvider = options.compactionRoutingOverride
|
|
764
|
+
? !compactionRoutingKeepsProviderIdentity(config, options.compactionRoutingOverride, route)
|
|
765
|
+
: false;
|
|
766
|
+
if (supportsNativeResponsesCompactEndpoint(route.providerName, route.provider) && !accountGatedCompactWireModel && !route.combo
|
|
767
|
+
&& !manualOverrideCrossesProvider) {
|
|
702
768
|
if (req.signal.aborted) {
|
|
703
769
|
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
704
770
|
}
|
|
@@ -796,7 +862,12 @@ export async function handleResponsesCompact(
|
|
|
796
862
|
// The regular /v1/responses path applies sanitizeReasoningInputContent via the adapter's
|
|
797
863
|
// buildRequest, but the compact endpoint forwards directly. Apply the same sanitizer here
|
|
798
864
|
// so routed-model reasoning items (reasoning_text content) don't 400 the ChatGPT backend.
|
|
799
|
-
|
|
865
|
+
// #5095: the compact endpoint forwards `raw` directly, so it needs the same legacy dotted
|
|
866
|
+
// call-name repair the adapter applies. A damaged item here refuses the compaction itself,
|
|
867
|
+
// which is the request a long task depends on to keep going.
|
|
868
|
+
const compactBody = repairLegacyDottedToolCallNames(
|
|
869
|
+
sanitizeReasoningInputContent(compactBodyRaw),
|
|
870
|
+
) as typeof compactBodyRaw;
|
|
800
871
|
{
|
|
801
872
|
const binding = conversationStateBindingFromAuth(authCtx, codexPoolAffinityKey(req.headers));
|
|
802
873
|
if (binding) {
|
|
@@ -1254,10 +1325,10 @@ export async function handleResponsesCompact(
|
|
|
1254
1325
|
// synthetic buffer errors are not upstream bodies and stay uninspected.
|
|
1255
1326
|
if (buffered.ok) {
|
|
1256
1327
|
inspectResponseLogJson(logCtx, await buffered.clone().text());
|
|
1257
|
-
forgetCompactHandoffRoute(req);
|
|
1328
|
+
if (!options.compactionRoutingOverride) forgetCompactHandoffRoute(req, admission);
|
|
1258
1329
|
rememberServingConversationStateIssuer(outcomeCtx, codexPoolAffinityKey(req.headers));
|
|
1259
|
-
} else if (quotaFailure && !storedPool401ReplayAttempted) {
|
|
1260
|
-
const fallbackModel = compactHandoffRoute(req, raw.model);
|
|
1330
|
+
} else if (!options.compactionRoutingOverride && quotaFailure && !storedPool401ReplayAttempted) {
|
|
1331
|
+
const fallbackModel = compactHandoffRoute(req, admission, raw.model);
|
|
1261
1332
|
if (fallbackModel && !req.signal.aborted) {
|
|
1262
1333
|
const fallbackReq = new Request(req.url, {
|
|
1263
1334
|
method: "POST",
|
|
@@ -1319,7 +1390,7 @@ export async function handleResponsesCompact(
|
|
|
1319
1390
|
// The routed compaction turn is a handoff inside the same logical request, so it draws the
|
|
1320
1391
|
// REMAINDER. Minting here is what let a native attempt spend three sends and the routed
|
|
1321
1392
|
// fallback spend four more.
|
|
1322
|
-
const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal, turnAdmissionLease, sendBudget, ...(admission ? { admission } : {}) });
|
|
1393
|
+
const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal, turnAdmissionLease, sendBudget, compactionRoutingOverride: options.compactionRoutingOverride, ...(admission ? { admission } : {}) });
|
|
1323
1394
|
if (!response.ok) return response;
|
|
1324
1395
|
let json: { output?: unknown[]; status?: unknown; error?: unknown };
|
|
1325
1396
|
if (response.headers.get("content-type")?.includes("text/event-stream")) {
|
|
@@ -1389,7 +1460,7 @@ export async function handleResponsesCompact(
|
|
|
1389
1460
|
const result = new Response(JSON.stringify({ output: compactionItems }), {
|
|
1390
1461
|
headers: { "Content-Type": "application/json" },
|
|
1391
1462
|
});
|
|
1392
|
-
rememberCompactHandoffRoute(req, raw.model);
|
|
1463
|
+
if (!options.compactionRoutingOverride) rememberCompactHandoffRoute(req, admission, raw.model);
|
|
1393
1464
|
return result;
|
|
1394
1465
|
}
|
|
1395
1466
|
const encrypted = compactionItems[0]!.encrypted_content;
|
|
@@ -1400,6 +1471,6 @@ export async function handleResponsesCompact(
|
|
|
1400
1471
|
}
|
|
1401
1472
|
const summary = decoded;
|
|
1402
1473
|
const output = buildCompactV1Output(extractCompactUserMessages(inputItems), summary);
|
|
1403
|
-
rememberCompactHandoffRoute(req, raw.model);
|
|
1474
|
+
if (!options.compactionRoutingOverride) rememberCompactHandoffRoute(req, admission, raw.model);
|
|
1404
1475
|
return new Response(JSON.stringify({ output }), { headers: { "Content-Type": "application/json" } });
|
|
1405
1476
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { OcxConfig } from "../../types";
|
|
2
|
+
import { isDeclaredReasoningEffort } from "../../reasoning-effort";
|
|
3
|
+
import { COMPACTION_TRIGGERS } from "../../config/schema/compaction-triggers";
|
|
4
|
+
import { routeConcreteModel, type RouteResult } from "../../router";
|
|
5
|
+
import { resolveComboId } from "../../combos/identifiers";
|
|
6
|
+
import { recallComboForLane } from "./combo-session-recall";
|
|
7
|
+
import { sessionLaneIdFromRequest } from "../request-log-conversation";
|
|
8
|
+
|
|
9
|
+
/** `sourceModel` is the conversation's own selector before the rewrite. */
|
|
10
|
+
export interface CompactionRoutingOverride {
|
|
11
|
+
sourceModel: string;
|
|
12
|
+
/** Combo the lane remembers for a bare `sourceModel` (#3891); the conversation resumes there, not on the bare route. */
|
|
13
|
+
sourceCombo?: string;
|
|
14
|
+
/** Combo the configured override resolves to; its children route concretely but stay portable. */
|
|
15
|
+
targetCombo?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
19
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
20
|
+
? value as Record<string, unknown>
|
|
21
|
+
: undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Triggers this override covers. Absent means manual only, which is the narrowest
|
|
26
|
+
* reading of the setting and leaves automatic compaction exactly as it routes today.
|
|
27
|
+
* A hand-edited value the schema would have rejected disables the override rather
|
|
28
|
+
* than widening it, so a malformed edit can never route more than it names.
|
|
29
|
+
*/
|
|
30
|
+
function configuredTriggers(value: unknown): readonly string[] | null {
|
|
31
|
+
if (value === undefined) return ["manual"];
|
|
32
|
+
if (!Array.isArray(value) || value.length === 0) return null;
|
|
33
|
+
if (!value.every(entry => typeof entry === "string" && (COMPACTION_TRIGGERS as readonly string[]).includes(entry))) return null;
|
|
34
|
+
if (new Set(value).size !== value.length) return null;
|
|
35
|
+
return value as readonly string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CompactionRoutingOverrideOptions {
|
|
39
|
+
/** `responses` requires a `compaction_trigger` input item; the native compact endpoint carries none. */
|
|
40
|
+
endpoint?: "responses" | "compact";
|
|
41
|
+
transport?: "websocket";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function applyCompactionRoutingOverride(
|
|
45
|
+
body: unknown,
|
|
46
|
+
headers: Headers,
|
|
47
|
+
config: OcxConfig,
|
|
48
|
+
options: CompactionRoutingOverrideOptions = {},
|
|
49
|
+
): CompactionRoutingOverride | null {
|
|
50
|
+
const override = config.compactionRouting;
|
|
51
|
+
const raw = record(body);
|
|
52
|
+
if (!raw || typeof raw.model !== "string" || !raw.model.trim()
|
|
53
|
+
|| typeof override?.model !== "string" || !override.model.trim()) return null;
|
|
54
|
+
if (override.reasoningEffort !== undefined
|
|
55
|
+
&& (typeof override.reasoningEffort !== "string" || !isDeclaredReasoningEffort(override.reasoningEffort))) return null;
|
|
56
|
+
const triggers = configuredTriggers(override.triggers);
|
|
57
|
+
if (!triggers) return null;
|
|
58
|
+
if (options.endpoint !== "compact"
|
|
59
|
+
&& !(Array.isArray(raw.input) && raw.input.some(item => record(item)?.type === "compaction_trigger"))) return null;
|
|
60
|
+
|
|
61
|
+
const metadata: unknown[] = [];
|
|
62
|
+
const header = headers.get("x-codex-turn-metadata");
|
|
63
|
+
if (options.transport !== "websocket" && header !== null) metadata.push(header);
|
|
64
|
+
const client = record(raw.client_metadata);
|
|
65
|
+
if (client && Object.hasOwn(client, "x-codex-turn-metadata")) metadata.push(client["x-codex-turn-metadata"]);
|
|
66
|
+
if (metadata.length === 0) return null;
|
|
67
|
+
let trigger: string | undefined;
|
|
68
|
+
for (const value of metadata) {
|
|
69
|
+
if (typeof value !== "string") return null;
|
|
70
|
+
try {
|
|
71
|
+
const parsed = record(JSON.parse(value));
|
|
72
|
+
if (parsed?.request_kind !== "compaction") return null;
|
|
73
|
+
const carried = record(parsed.compaction)?.trigger;
|
|
74
|
+
if (typeof carried !== "string" || !triggers.includes(carried)) return null;
|
|
75
|
+
// Copies that name different triggers are not agreement, and picking either one would
|
|
76
|
+
// let a caller widen an override by disagreeing with itself.
|
|
77
|
+
if (trigger !== undefined && trigger !== carried) return null;
|
|
78
|
+
trigger = carried;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (trigger === undefined) return null;
|
|
84
|
+
|
|
85
|
+
const sourceModel = raw.model;
|
|
86
|
+
const sourceCombo = recallComboForLane(config, sessionLaneIdFromRequest(headers), sourceModel);
|
|
87
|
+
const targetCombo = resolveComboId(config, override.model.trim()) ?? undefined;
|
|
88
|
+
raw.model = override.model.trim();
|
|
89
|
+
if (override.reasoningEffort !== undefined) {
|
|
90
|
+
raw.reasoning = { ...record(raw.reasoning), effort: override.reasoningEffort };
|
|
91
|
+
}
|
|
92
|
+
return { sourceModel, ...(sourceCombo ? { sourceCombo } : {}), ...(targetCombo ? { targetCombo } : {}) };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Same provider identity keeps caller auth and may use native compact; its ciphertext replays only there. */
|
|
96
|
+
export function compactionRoutingKeepsProviderIdentity(
|
|
97
|
+
config: OcxConfig,
|
|
98
|
+
override: CompactionRoutingOverride,
|
|
99
|
+
route: RouteResult,
|
|
100
|
+
): boolean {
|
|
101
|
+
if (route.combo || override.sourceCombo || override.targetCombo || resolveComboId(config, override.sourceModel)) return false;
|
|
102
|
+
let source: RouteResult;
|
|
103
|
+
try {
|
|
104
|
+
source = routeConcreteModel(config, override.sourceModel);
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return source.providerName === route.providerName
|
|
109
|
+
&& source.codexAccountMode === route.codexAccountMode
|
|
110
|
+
&& source.codexAccountNamespace === route.codexAccountNamespace;
|
|
111
|
+
}
|