@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +64 -0
- package/README.md +28 -1
- package/bin/ocx.mjs +382 -209
- package/gui/dist/assets/App-E64Rzjap.js +50 -0
- package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
- package/gui/dist/assets/index-DpdfZWMK.js +86 -0
- package/gui/dist/assets/index-_bpvxJu0.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/stepfun-color.svg +1 -0
- package/package.json +8 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +20 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/protocol.ts +36 -6
- package/src/adapters/coding-agent/turn.ts +32 -4
- package/src/adapters/command-code.ts +52 -4
- package/src/adapters/cursor/catalog.ts +51 -7
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/protobuf-request.ts +6 -3
- package/src/adapters/cursor/request-builder.ts +20 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +14 -3
- package/src/adapters/declaration-carrier.ts +45 -0
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +125 -36
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +34 -7
- package/src/adapters/google-errors.ts +7 -1
- package/src/adapters/google-http.ts +49 -10
- 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 +66 -11
- package/src/adapters/image.ts +4 -1
- package/src/adapters/input-media-guard.ts +21 -9
- package/src/adapters/kiro/usage.ts +3 -2
- package/src/adapters/kiro-tool-fallback.ts +1 -1
- package/src/adapters/ollama-native.ts +6 -0
- package/src/adapters/openai-chat/developer-role.ts +61 -0
- package/src/adapters/openai-chat/messages.ts +46 -27
- package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
- package/src/adapters/openai-chat/passthrough.ts +33 -9
- package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +23 -58
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +17 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/registry.ts +3 -2
- package/src/adapters/run-turn-queue.ts +178 -29
- package/src/adapters/xai-web-search.ts +16 -1
- package/src/bridge/errors.ts +8 -2
- package/src/bridge/response-json.ts +9 -1
- package/src/bridge/sse.ts +13 -151
- package/src/chat/inbound.ts +141 -5
- package/src/claude/desktop-3p.ts +7 -1
- package/src/claude/desktop-first-party.ts +183 -0
- package/src/claude/desktop-gateway-state.ts +41 -0
- package/src/claude/inbound-content-options.ts +6 -0
- package/src/claude/inbound.ts +32 -6
- package/src/claude/intercept/connect-proxy.ts +179 -0
- package/src/claude/intercept/listener.ts +122 -0
- package/src/claude/intercept/local-ca.ts +298 -0
- package/src/claude/intercept/runtime.ts +98 -0
- package/src/claude/intercept/settings.ts +189 -0
- package/src/cli/access.ts +87 -0
- package/src/cli/account-auth.ts +19 -0
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/capabilities.ts +31 -0
- package/src/cli/claude-desktop.ts +206 -16
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/companion.ts +56 -0
- package/src/cli/dispatch.ts +46 -7
- package/src/cli/doctor.ts +28 -9
- package/src/cli/ensure-desired-integrations.ts +43 -5
- package/src/cli/help.ts +7 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +203 -59
- package/src/cli/init.ts +8 -0
- package/src/cli/integrations.ts +7 -1
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +41 -2
- package/src/cli/resolve.ts +230 -0
- package/src/cli/root.ts +24 -1
- package/src/cli/start-ownership-publication.ts +56 -0
- package/src/cli/status-probes.ts +2 -18
- package/src/cli/status.ts +62 -0
- package/src/cli/stop-report.ts +143 -0
- package/src/cli/uninstall-plan.ts +9 -0
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +4 -7
- package/src/client/runtime.ts +26 -2
- package/src/clients/aside-profiles.ts +4 -0
- package/src/clients/config-export/zcode-store.ts +157 -0
- package/src/clients/config-export.ts +36 -0
- package/src/codex/account-store.ts +65 -0
- package/src/codex/app-server-processes.ts +72 -40
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/login-flow.ts +6 -1
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/autostart-health.ts +28 -0
- package/src/codex/catalog/build-entries.ts +2 -2
- package/src/codex/catalog/effort.ts +3 -3
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +43 -17
- package/src/codex/catalog/retained-sync.ts +24 -28
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +2 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +31 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/inject/config-toml.ts +41 -6
- package/src/codex/inject/paginated-openai-compat.ts +90 -0
- package/src/codex/inject.ts +18 -15
- package/src/codex/injected-marker.ts +18 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/main-account.ts +6 -0
- package/src/codex/model-cache.ts +99 -6
- package/src/codex/model-entitlement-admission.ts +59 -0
- package/src/codex/model-entitlements.ts +116 -54
- package/src/codex/native-main-admission.ts +83 -0
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing/health-store.ts +39 -0
- package/src/codex/routing/selection.ts +37 -1
- package/src/codex/routing.ts +12 -42
- package/src/codex/shim-templates.ts +29 -3
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/companion/settings.ts +132 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/atomic-write.ts +117 -5
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +52 -7
- package/src/config/process-state.ts +1 -1
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +31 -1
- package/src/config/schema/leaf-validators.ts +59 -0
- package/src/config.ts +3 -3
- package/src/generated/compatibility-version.json +604 -280
- 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/config-io.ts +44 -10
- package/src/integrations/merge.ts +120 -13
- package/src/integrations/mutation-plan.ts +921 -0
- package/src/integrations/registry.ts +38 -0
- package/src/integrations/state.ts +78 -45
- package/src/integrations/target.ts +208 -0
- package/src/integrations/writer.ts +134 -110
- package/src/lab/conformance/fixture-provider.ts +5 -0
- 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/browser-launch-notice.ts +59 -0
- package/src/lib/bun-runtime.ts +6 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +63 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/open-url.ts +51 -7
- package/src/lib/package-tree-integrity.ts +2 -1
- package/src/lib/package-version.ts +8 -0
- 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-egress.ts +310 -0
- package/src/lib/provider-outbound.ts +109 -16
- package/src/lib/proxy-env.ts +82 -7
- package/src/lib/request-execution-budget.ts +72 -0
- package/src/lib/request-failure-attribution.ts +183 -0
- package/src/lib/request-failure-model.ts +236 -0
- package/src/lib/request-resend-gate.ts +138 -0
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/standalone.ts +16 -0
- package/src/lib/upstream-retry.ts +167 -16
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/winsw.ts +2 -2
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +27 -108
- package/src/oauth/login-cli.ts +80 -29
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/api-key-resolve.ts +133 -0
- package/src/providers/api-key-selection.ts +5 -1
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +97 -19
- package/src/providers/key-store.ts +34 -110
- package/src/providers/model-rename-fields.ts +147 -0
- package/src/providers/model-rename-migration.ts +179 -38
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +38 -23
- package/src/providers/reasoning-metadata.ts +43 -18
- package/src/providers/registry/entries-core.ts +47 -20
- package/src/providers/registry/entries-extended.ts +63 -4
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +56 -10
- 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/providers/xai-transport.ts +12 -1
- package/src/reasoning-effort.ts +8 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/function-call-compat.ts +38 -1
- package/src/responses/inline-document.ts +65 -0
- package/src/responses/input-media.ts +42 -8
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/muse-tool-name-alias.ts +19 -0
- package/src/responses/parser-content.ts +8 -2
- package/src/responses/parser-tools.ts +3 -0
- package/src/responses/parser.ts +3 -1
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +3 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +108 -117
- 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/admission-model-scope.ts +219 -0
- package/src/server/audio-live.ts +9 -3
- package/src/server/audio-upstream.ts +18 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +60 -4
- package/src/server/chat-native.ts +19 -4
- package/src/server/claude-messages.ts +61 -20
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/grok-responses-snapshot-repair.ts +113 -11
- package/src/server/gui-freshness.ts +103 -0
- package/src/server/gui-static.ts +7 -9
- package/src/server/images.ts +59 -6
- package/src/server/index/claude-intercept-lifecycle.ts +49 -0
- package/src/server/index/serve-options.ts +142 -39
- package/src/server/index/spend-ledger-lifecycle.ts +92 -0
- package/src/server/index/startup-warnings.ts +24 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +34 -38
- package/src/server/lifecycle.ts +4 -4
- package/src/server/live-call-bindings.ts +6 -0
- package/src/server/live.ts +88 -3
- package/src/server/management/agent-settings-routes.ts +121 -36
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/companion-routes.ts +77 -0
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/logs-usage-routes.ts +19 -0
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/native-integration-routes.ts +103 -6
- package/src/server/management/oauth-account-routes.ts +45 -7
- package/src/server/management/route-registry.ts +20 -0
- package/src/server/management/shared.ts +28 -4
- 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/usage-timeline-routes.ts +44 -0
- package/src/server/management-api.ts +10 -9
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +75 -0
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +138 -12
- package/src/server/request-log-failure-attribution.ts +99 -0
- package/src/server/request-log.ts +169 -2
- package/src/server/request-metrics.ts +298 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/codex-ws-wire.ts +34 -8
- package/src/server/responses/combo-stream-preflight.ts +168 -6
- package/src/server/responses/compact.ts +43 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +91 -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 +124 -8
- package/src/server/responses/input-admission.ts +10 -0
- package/src/server/responses/passthrough-delivery.ts +45 -15
- package/src/server/responses/passthrough-dispatch.ts +215 -44
- package/src/server/responses/passthrough-error.ts +27 -8
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +109 -18
- package/src/server/responses/request-send-budget.ts +17 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +26 -6
- package/src/server/responses/reset-replay.ts +108 -0
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-request-tool-scope.ts +214 -0
- package/src/server/responses-undeclared-tool-guard.ts +35 -2
- package/src/server/search.ts +25 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/server/usage-ledger-retention.ts +73 -0
- package/src/service/cli.ts +48 -2
- package/src/service/health.ts +3 -2
- package/src/service/install-state-contract.d.mts +27 -0
- package/src/service/install-state-contract.mjs +34 -0
- package/src/service/launchd.ts +1 -1
- package/src/service/orchestration.ts +2 -4
- package/src/service/ownership-compatibility.ts +164 -0
- package/src/service/ownership-mutation-lease.d.mts +32 -0
- package/src/service/ownership-mutation-lease.mjs +211 -0
- package/src/service/repair.ts +45 -1
- package/src/service/state-lock.ts +269 -0
- package/src/service/state-record.d.mts +36 -0
- package/src/service/state-record.mjs +138 -0
- package/src/service/state.ts +582 -68
- package/src/service/windows-taskxml.ts +11 -10
- package/src/service.ts +7 -3
- package/src/tray/windows-tray.ps1 +156 -4
- package/src/types/config.ts +48 -3
- package/src/types/provider.ts +91 -0
- package/src/types/request.ts +30 -2
- package/src/types/tools.ts +33 -0
- package/src/types.ts +4 -0
- package/src/update/index.ts +207 -63
- package/src/update/job.ts +9 -5
- package/src/update/ownership-transaction.ts +47 -0
- package/src/update/restart-ownership.ts +54 -0
- package/src/update/runtime-ownership.d.mts +40 -0
- package/src/update/runtime-ownership.mjs +122 -0
- package/src/usage/attempt-delivery.ts +198 -0
- package/src/usage/cache-diagnostic.ts +305 -0
- package/src/usage/failure-fingerprint.ts +118 -0
- package/src/usage/failure-projection-cache.ts +174 -0
- package/src/usage/failure-projection.ts +174 -0
- package/src/usage/ledger-retention.ts +165 -0
- package/src/usage/log.ts +126 -79
- package/src/usage/request-outcome.ts +150 -0
- package/src/usage/retention-contract.ts +28 -0
- package/src/usage/summary.ts +2 -2
- package/src/usage/telemetry-contract.ts +237 -0
- package/src/usage/timeline.ts +236 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/alpha-search.ts +21 -1
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
|
@@ -102,7 +102,7 @@ import {
|
|
|
102
102
|
clearCodexModelDenialEvidence,
|
|
103
103
|
recordCodexModelDenialEvidence,
|
|
104
104
|
} from "../../codex/model-entitlements";
|
|
105
|
-
import { readCodexWsStage } from "./codex-ws-wire";
|
|
105
|
+
import { isCodexWsUpstreamResponse, readCodexWsStage } from "./codex-ws-wire";
|
|
106
106
|
import { linkAbortSignal } from "./core-lifetime";
|
|
107
107
|
import type { CodexAuthContext } from "../../codex/auth-context";
|
|
108
108
|
import { checkOutboundBodySize, describeOutboundBodyRefusal } from "./outbound-body-guard";
|
|
@@ -112,7 +112,8 @@ import {
|
|
|
112
112
|
fetchWithTransientRetry,
|
|
113
113
|
applyUpstreamRecoveryInit,
|
|
114
114
|
isNonReplayableResponse,
|
|
115
|
-
|
|
115
|
+
refetchAfterProtocolSafeReset,
|
|
116
|
+
prepareSameTarget429Wait,
|
|
116
117
|
sleepWithAbort,
|
|
117
118
|
} from "../../lib/upstream-retry";
|
|
118
119
|
import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
|
|
@@ -121,6 +122,7 @@ import { recordCodexUpstreamOutcome } from "../../codex/routing";
|
|
|
121
122
|
import { describeUpstreamConnectFailure } from "./upstream-error";
|
|
122
123
|
import type { OpaqueBlobRecoveryGuard } from "./core-opaque-recovery";
|
|
123
124
|
import {
|
|
125
|
+
isOpenCodeGoDestination,
|
|
124
126
|
rateLimitRetryPolicyFor,
|
|
125
127
|
rateLimitRetryDelayMs,
|
|
126
128
|
transientRetryPolicyFor,
|
|
@@ -142,6 +144,7 @@ import { captureCodexAffinityDiagnostic } from "../../codex/affinity-debug";
|
|
|
142
144
|
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
|
|
143
145
|
import {
|
|
144
146
|
attemptOpaqueBlobRecovery,
|
|
147
|
+
isEncryptedFunctionOutputRejection,
|
|
145
148
|
outboundResponsesBodyCarriesEncryptedFunctionOutput,
|
|
146
149
|
resetStreamedOpaqueBlobLogContext,
|
|
147
150
|
consoleGoUploadRejectionBody,
|
|
@@ -149,7 +152,9 @@ import {
|
|
|
149
152
|
reasoningEffortRejectionText,
|
|
150
153
|
} from "./core-opaque-recovery";
|
|
151
154
|
import type { RequestLogContext } from "../request-log";
|
|
152
|
-
import { preflightComboStreamResponse } from "./combo-stream-preflight";
|
|
155
|
+
import { deferProtocolSafeResetRecovery, preflightComboStreamResponse } from "./combo-stream-preflight";
|
|
156
|
+
import { authorizeResendForRecovery } from "../../lib/request-resend-gate";
|
|
157
|
+
import { ambiguousResendAllowanceFor, selfContainedResponsesBody } from "./reset-replay";
|
|
153
158
|
import { upstreamErrorMessageFromPayload, ENCRYPTED_FUNCTION_OUTPUT_REJECTION } from "../../lib/errors";
|
|
154
159
|
import { isTransientConsoleGoUploadRejection } from "../../providers/opencode-zen-rate-limit";
|
|
155
160
|
import { planReasoningEffortDowngrade } from "../../providers/reasoning-metadata";
|
|
@@ -187,6 +192,8 @@ export async function preparePassthroughExchange(
|
|
|
187
192
|
| "genericFailovers"
|
|
188
193
|
| "applyFailoverSnapshot"
|
|
189
194
|
| "noteRoutedAttemptSend"
|
|
195
|
+
| "selectionIsCurrent"
|
|
196
|
+
| "requestBindings"
|
|
190
197
|
>,
|
|
191
198
|
responseEffects: Pick<
|
|
192
199
|
ResponsesEffects,
|
|
@@ -204,6 +211,7 @@ export async function preparePassthroughExchange(
|
|
|
204
211
|
| "recoverySendAllowance"
|
|
205
212
|
| "recoveryClassFor"
|
|
206
213
|
| "sendBudgetExhausted"
|
|
214
|
+
| "claimAmbiguousResend"
|
|
207
215
|
| "reserveCredentialHop"
|
|
208
216
|
| "pendingHopPermit"
|
|
209
217
|
| "workflowRootId"
|
|
@@ -237,6 +245,7 @@ export async function preparePassthroughExchange(
|
|
|
237
245
|
recoverySendAllowance,
|
|
238
246
|
recoveryClassFor,
|
|
239
247
|
sendBudgetExhausted,
|
|
248
|
+
claimAmbiguousResend,
|
|
240
249
|
reserveCredentialHop,
|
|
241
250
|
workflowRootId,
|
|
242
251
|
} = sendBudgetState;
|
|
@@ -702,6 +711,35 @@ export async function preparePassthroughExchange(
|
|
|
702
711
|
);
|
|
703
712
|
const configuredTransientSendBudgetExhausted = (): boolean =>
|
|
704
713
|
transientSendPolicy() !== null && transientSendAttempts() === 0;
|
|
714
|
+
/**
|
|
715
|
+
* Judged once. The inbound body does not change between legs, and every rebuild this lane
|
|
716
|
+
* performs only ever REMOVES a hazard -- `previous_response_id` is expanded, hosted tools
|
|
717
|
+
* are lowered into client execution -- so a body that was replaceable stays replaceable.
|
|
718
|
+
* Memoized rather than recomputed because it walks the input array, and a provider that
|
|
719
|
+
* never opted in must not pay for it at all.
|
|
720
|
+
*/
|
|
721
|
+
let selfContainedJudgment: boolean | undefined;
|
|
722
|
+
const requestIsSelfContained = (): boolean =>
|
|
723
|
+
selfContainedJudgment ??= selfContainedResponsesBody(parsed._rawBody);
|
|
724
|
+
/**
|
|
725
|
+
* The operator's replacement grant for THIS logical request.
|
|
726
|
+
*
|
|
727
|
+
* Read per leg because `route.provider` is reassigned by credential rotation and transport
|
|
728
|
+
* resolution inside the recovery loop, exactly like `transientSendPolicy`. The counter it
|
|
729
|
+
* claims from is not per leg: it lives on the request's execution budget, which a combo
|
|
730
|
+
* child shares, so every ambiguous stage of this request draws on the same grant.
|
|
731
|
+
*/
|
|
732
|
+
const ambiguousResend = () =>
|
|
733
|
+
ambiguousResendAllowanceFor(route.provider, requestIsSelfContained, claimAmbiguousResend);
|
|
734
|
+
/**
|
|
735
|
+
* The pre-header row of the stage table, asked through the one gate.
|
|
736
|
+
*
|
|
737
|
+
* `fetchWithResetRetry` takes a plain callback because it is a leaf that must not import
|
|
738
|
+
* the server tree; routing the answer through `authorizeResendForRecovery` here is what
|
|
739
|
+
* keeps the decision derived from the table rather than restated as a boolean.
|
|
740
|
+
*/
|
|
741
|
+
const claimPreHeaderResend = (): boolean =>
|
|
742
|
+
authorizeResendForRecovery("pre-header", "connection-reset", ambiguousResend()).allowed;
|
|
705
743
|
/**
|
|
706
744
|
* Refuse a built body that exceeds the operator's configured ceiling, before it is sent.
|
|
707
745
|
*
|
|
@@ -841,7 +879,16 @@ export async function preparePassthroughExchange(
|
|
|
841
879
|
// retry wrapper replaces — proves the host was reached (#914 review).
|
|
842
880
|
.then(adoptObservedResponse);
|
|
843
881
|
},
|
|
844
|
-
{ abortSignal: upstream.signal, label: safeHostLabel(request.url),
|
|
882
|
+
{ abortSignal: upstream.signal, label: safeHostLabel(request.url),
|
|
883
|
+
attempts: remainingTransientSendBudget(transientSendAttempts()), onSendsConsumed: noteTransientSends,
|
|
884
|
+
claimAmbiguousResend: claimPreHeaderResend,
|
|
885
|
+
// The OpenCode Go destination stalls-then-drops inference sends (ambiguous
|
|
886
|
+
// pre-header resets surfacing as refused 429s); its subscription traffic is
|
|
887
|
+
// inference-only, so a bounded reset replay here absorbs the blip instead of
|
|
888
|
+
// failing the turn. Recovery legs keep the fail-closed refusal; only this
|
|
889
|
+
// initial send is replay-eligible. Attempts stay budget-bounded via attempts.
|
|
890
|
+
replaySafe: isOpenCodeGoDestination(route.provider),
|
|
891
|
+
},
|
|
845
892
|
);
|
|
846
893
|
} catch (err) {
|
|
847
894
|
return transportFailureResponse(err);
|
|
@@ -863,7 +910,7 @@ export async function preparePassthroughExchange(
|
|
|
863
910
|
recovery: AttemptRecoveryKind,
|
|
864
911
|
): Promise<Response | { failed: Response }> => {
|
|
865
912
|
const retryAdapter = resolveSelectionAdapter(
|
|
866
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
913
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
867
914
|
config.cacheRetention,
|
|
868
915
|
);
|
|
869
916
|
if (!("passthrough" in retryAdapter) || !retryAdapter.passthrough) {
|
|
@@ -939,7 +986,8 @@ export async function preparePassthroughExchange(
|
|
|
939
986
|
route.provider.authMode === "forward")
|
|
940
987
|
.then(adoptObservedResponse);
|
|
941
988
|
},
|
|
942
|
-
{ abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: allowance.attempts,
|
|
989
|
+
{ abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: allowance.attempts,
|
|
990
|
+
onSendsConsumed: noteTransientSends, claimAmbiguousResend: claimPreHeaderResend },
|
|
943
991
|
);
|
|
944
992
|
} catch (err) {
|
|
945
993
|
return { failed: transportFailureResponse(err) };
|
|
@@ -989,7 +1037,7 @@ export async function preparePassthroughExchange(
|
|
|
989
1037
|
route.provider = replay.provider;
|
|
990
1038
|
requestState.selectedForwardHeaders = withClaudeNativeSession(replay.headers, replay.provider, options.claudeNativeSessionId);
|
|
991
1039
|
const replayAdapter = resolveSelectionAdapter(
|
|
992
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, replay.provider, inboundWire),
|
|
1040
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, replay.provider, inboundWire, route.staticPolicy),
|
|
993
1041
|
config.cacheRetention,
|
|
994
1042
|
);
|
|
995
1043
|
if (!("passthrough" in replayAdapter) || !replayAdapter.passthrough) {
|
|
@@ -1020,35 +1068,54 @@ export async function preparePassthroughExchange(
|
|
|
1020
1068
|
// every other build site; a replay is exactly when a grown payload reappears.
|
|
1021
1069
|
const replayBodyRefusal = refuseOversizedOutboundBody(request);
|
|
1022
1070
|
if (replayBodyRefusal) return replayBodyRefusal;
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1071
|
+
// The replay-dispatched signal is what bounds the rest of this logical request, so it
|
|
1072
|
+
// has to describe a send that actually happened. fetchWithHeaderTimeout awaits pacing
|
|
1073
|
+
// admission BEFORE calling the executor, so signalling at the call site would spend the
|
|
1074
|
+
// budget even when a rejected pacing wait means nothing reaches the network. Wrapping
|
|
1075
|
+
// the executor moves the signal to the last moment before the send, where a throw from
|
|
1076
|
+
// here on is a genuine transport attempt. The notifier is built once for the whole leg:
|
|
1077
|
+
// it fires on the first dispatch, and a replacement is another send of the same replay
|
|
1078
|
+
// rather than a second one to announce.
|
|
1079
|
+
const oauthReplayExecutor = storedPoolReplayDispatchNotifier(
|
|
1080
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
1081
|
+
nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
|
|
1082
|
+
&& responseEffects.plaintextV2AgentMessageToolNames.size === 0
|
|
1083
|
+
? options.nativeControl : undefined,
|
|
1084
|
+
dispatchOverride: oauthDispatch(request),
|
|
1085
|
+
providerName: route.providerName,
|
|
1086
|
+
modelId: route.modelId,
|
|
1087
|
+
onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
|
|
1088
|
+
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
1089
|
+
? createCodexReserveDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
1090
|
+
}),
|
|
1091
|
+
codex401ReplayKind === "stored" ? options.onStoredPool401ReplayDispatched : undefined,
|
|
1092
|
+
);
|
|
1093
|
+
// Routed through the shared helper so an ambiguous reset on THIS leg answers with the
|
|
1094
|
+
// same refusal every other leg gives. A bare fetch here rejected instead, and the
|
|
1095
|
+
// caller's transport-failure path turns a rejection into a client-retryable 502 --
|
|
1096
|
+
// which invites the whole turn to be sent again, on a leg whose first send may already
|
|
1097
|
+
// have run it.
|
|
1098
|
+
upstreamResponse = await fetchWithTransientRetry(
|
|
1099
|
+
recovery => {
|
|
1100
|
+
transportState.noteRoutedAttemptSend(passthroughEstimate, recovery ?? "oauth-401");
|
|
1101
|
+
return fetchWithHeaderTimeout(
|
|
1102
|
+
request.url,
|
|
1103
|
+
applyUpstreamRecoveryInit({
|
|
1104
|
+
method: request.method,
|
|
1105
|
+
headers: request.headers,
|
|
1106
|
+
body: request.body,
|
|
1107
|
+
}, recovery),
|
|
1108
|
+
upstream.signal,
|
|
1109
|
+
connectMs,
|
|
1110
|
+
parsed.stream,
|
|
1111
|
+
oauthReplayExecutor,
|
|
1112
|
+
route.provider.authMode === "forward",
|
|
1113
|
+
).then(adoptObservedResponse);
|
|
1114
|
+
},
|
|
1115
|
+
{ abortSignal: upstream.signal, label: safeHostLabel(request.url),
|
|
1116
|
+
attempts: remainingTransientSendBudget(transientSendAttempts()),
|
|
1117
|
+
onSendsConsumed: noteTransientSends, claimAmbiguousResend: claimPreHeaderResend },
|
|
1118
|
+
);
|
|
1052
1119
|
} catch (err) {
|
|
1053
1120
|
return transportFailureResponse(err);
|
|
1054
1121
|
} finally {
|
|
@@ -1110,7 +1177,7 @@ export async function preparePassthroughExchange(
|
|
|
1110
1177
|
);
|
|
1111
1178
|
route.provider = refreshedProvider;
|
|
1112
1179
|
const refreshedAdapter = resolveSelectionAdapter(
|
|
1113
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire),
|
|
1180
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire, route.staticPolicy),
|
|
1114
1181
|
config.cacheRetention,
|
|
1115
1182
|
);
|
|
1116
1183
|
if (!("passthrough" in refreshedAdapter) || !refreshedAdapter.passthrough) {
|
|
@@ -1172,7 +1239,8 @@ export async function preparePassthroughExchange(
|
|
|
1172
1239
|
route.provider.authMode === "forward")
|
|
1173
1240
|
.then(adoptObservedResponse);
|
|
1174
1241
|
},
|
|
1175
|
-
{ abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: remainingTransientSendBudget(transientSendAttempts()),
|
|
1242
|
+
{ abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: remainingTransientSendBudget(transientSendAttempts()),
|
|
1243
|
+
onSendsConsumed: noteTransientSends, claimAmbiguousResend: claimPreHeaderResend },
|
|
1176
1244
|
);
|
|
1177
1245
|
} catch (err) {
|
|
1178
1246
|
return transportFailureResponse(err);
|
|
@@ -1304,7 +1372,8 @@ export async function preparePassthroughExchange(
|
|
|
1304
1372
|
route.provider.authMode === "forward")
|
|
1305
1373
|
.then(adoptObservedResponse);
|
|
1306
1374
|
},
|
|
1307
|
-
{ abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: remainingTransientSendBudget(transientSendAttempts()),
|
|
1375
|
+
{ abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: remainingTransientSendBudget(transientSendAttempts()),
|
|
1376
|
+
onSendsConsumed: noteTransientSends, claimAmbiguousResend: claimPreHeaderResend },
|
|
1308
1377
|
);
|
|
1309
1378
|
} catch (err) {
|
|
1310
1379
|
return transportFailureResponse(err);
|
|
@@ -1340,8 +1409,16 @@ export async function preparePassthroughExchange(
|
|
|
1340
1409
|
// refusal: whatever the entitlement was when upstream declined, it is not that now. Both
|
|
1341
1410
|
// ids are cleared because the wire model can differ from the routed one.
|
|
1342
1411
|
if (upstreamResponse.ok) {
|
|
1343
|
-
clearCodexModelDenialEvidence(
|
|
1344
|
-
|
|
1412
|
+
clearCodexModelDenialEvidence(
|
|
1413
|
+
admissionState.authCtx.accountId,
|
|
1414
|
+
route.modelId,
|
|
1415
|
+
admissionState.authCtx.kind === "pool" ? admissionState.authCtx.generation : undefined,
|
|
1416
|
+
);
|
|
1417
|
+
clearCodexModelDenialEvidence(
|
|
1418
|
+
admissionState.authCtx.accountId,
|
|
1419
|
+
parsed.modelId,
|
|
1420
|
+
admissionState.authCtx.kind === "pool" ? admissionState.authCtx.generation : undefined,
|
|
1421
|
+
);
|
|
1345
1422
|
}
|
|
1346
1423
|
const model400Denial = await codexPoolAccountModel400Denial(
|
|
1347
1424
|
upstreamResponse,
|
|
@@ -1354,7 +1431,11 @@ export async function preparePassthroughExchange(
|
|
|
1354
1431
|
// answer about this model, and the roster cache that selection otherwise reads expires
|
|
1355
1432
|
// five minutes after a catalog sync fills it -- so without remembering this, the next
|
|
1356
1433
|
// request selects the same account on quota alone and takes the same 400 (#4906).
|
|
1357
|
-
recordCodexModelDenialEvidence(
|
|
1434
|
+
recordCodexModelDenialEvidence(
|
|
1435
|
+
admissionState.authCtx.accountId,
|
|
1436
|
+
model400Denial,
|
|
1437
|
+
admissionState.authCtx.kind === "pool" ? admissionState.authCtx.generation : undefined,
|
|
1438
|
+
);
|
|
1358
1439
|
poolRetryOutcome = 400;
|
|
1359
1440
|
} else if (!admissionState.authCtx.fixedAccount && await shouldRetryCodexPoolAccountQuota(
|
|
1360
1441
|
upstreamResponse,
|
|
@@ -1453,8 +1534,13 @@ export async function preparePassthroughExchange(
|
|
|
1453
1534
|
payload => {
|
|
1454
1535
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
1455
1536
|
const type = (payload as { type?: unknown }).type;
|
|
1456
|
-
|
|
1457
|
-
&&
|
|
1537
|
+
const decryptRejection = (type === "error" || type === "response.failed" || type === "response.incomplete")
|
|
1538
|
+
&& isEncryptedFunctionOutputRejection(JSON.stringify(payload));
|
|
1539
|
+
// `detail` is a real WebSocket error shape but the generic preflight failure projector
|
|
1540
|
+
// intentionally understands only Responses error/message fields. Preserve only this
|
|
1541
|
+
// exact identity so the projected 502 can enter the existing single-shot recovery.
|
|
1542
|
+
if (decryptRejection) preflightLog.upstreamError = ENCRYPTED_FUNCTION_OUTPUT_REJECTION;
|
|
1543
|
+
return decryptRejection;
|
|
1458
1544
|
}, {
|
|
1459
1545
|
allowMissingContentType: !recoveryContentType && parsed.stream,
|
|
1460
1546
|
replayReadErrors: true,
|
|
@@ -1544,6 +1630,91 @@ export async function preparePassthroughExchange(
|
|
|
1544
1630
|
continue passthroughRecovery;
|
|
1545
1631
|
}
|
|
1546
1632
|
}
|
|
1633
|
+
|
|
1634
|
+
// The post-header row of the same table. A native SSE body can die after the head with the
|
|
1635
|
+
// caller having observed nothing, which is the identical question the pre-header helper
|
|
1636
|
+
// answers -- and the identical grant, because both claim from this request's one allowance.
|
|
1637
|
+
const streamRecoveryContentType = upstreamResponse.headers.get("content-type")?.toLowerCase() ?? "";
|
|
1638
|
+
const protocolRecoveryCandidate = upstreamResponse.ok
|
|
1639
|
+
&& !!upstreamResponse.body
|
|
1640
|
+
&& !isNonReplayableResponse(upstreamResponse)
|
|
1641
|
+
// The WebSocket transport settles its own ambiguous failures and marks them
|
|
1642
|
+
// non-replayable; re-reading its body here would be a second owner of one exchange.
|
|
1643
|
+
&& !isCodexWsUpstreamResponse(upstreamResponse)
|
|
1644
|
+
// A downstream WebSocket turn that fell back to HTTP must relay response.created
|
|
1645
|
+
// immediately so the client can address the turn and receive explicit control refusal.
|
|
1646
|
+
// This preflight retains that event until output commits, so the two contracts cannot
|
|
1647
|
+
// share one body owner.
|
|
1648
|
+
&& !(options.nativeControl && options.inboundTransport === "websocket")
|
|
1649
|
+
&& ambiguousResend() !== undefined
|
|
1650
|
+
&& remainingTransientSendBudget(transientSendAttempts()) > 0
|
|
1651
|
+
&& (streamRecoveryContentType.includes("text/event-stream") || (!streamRecoveryContentType && parsed.stream));
|
|
1652
|
+
if (protocolRecoveryCandidate) {
|
|
1653
|
+
upstreamResponse = deferProtocolSafeResetRecovery(
|
|
1654
|
+
upstreamResponse,
|
|
1655
|
+
{ model: logCtx.model, provider: logCtx.provider },
|
|
1656
|
+
(error, stage) => refetchAfterProtocolSafeReset(
|
|
1657
|
+
(signal = upstream.signal) => fetchWithHeaderTimeout(
|
|
1658
|
+
request.url,
|
|
1659
|
+
applyUpstreamRecoveryInit({
|
|
1660
|
+
method: request.method,
|
|
1661
|
+
headers: request.headers,
|
|
1662
|
+
body: request.body,
|
|
1663
|
+
}, "connection-reset"),
|
|
1664
|
+
signal,
|
|
1665
|
+
connectMs,
|
|
1666
|
+
true,
|
|
1667
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
1668
|
+
// A replacement HTTP body must not open a fresh WebSocket exchange: the turn it
|
|
1669
|
+
// replaces was an HTTP stream, and a WS create frame is a different send.
|
|
1670
|
+
httpOnly: true,
|
|
1671
|
+
providerName: route.providerName,
|
|
1672
|
+
modelId: route.modelId,
|
|
1673
|
+
dispatchOverride: oauthDispatch(request),
|
|
1674
|
+
beforeDispatch: headers => {
|
|
1675
|
+
if (signal.aborted) throw signal.reason;
|
|
1676
|
+
if (!transportState.selectionIsCurrent(transportState.requestBindings.get(request))) {
|
|
1677
|
+
throw new Error("Credential selection changed before pre-output stream recovery");
|
|
1678
|
+
}
|
|
1679
|
+
if (isCanonicalOpenAiForwardProvider(route.provider)) {
|
|
1680
|
+
createCodexReserveDispatchGuard(
|
|
1681
|
+
admissionState.authCtx,
|
|
1682
|
+
options.codexAuthPolicy ?? config,
|
|
1683
|
+
route.modelId,
|
|
1684
|
+
options.admission,
|
|
1685
|
+
options.visionDescribeTerminal === true,
|
|
1686
|
+
)?.(headers);
|
|
1687
|
+
}
|
|
1688
|
+
// Recorded with the kind the gate derived its cause from, at the moment the
|
|
1689
|
+
// send actually leaves. One authorisation, one recorded reason, one send.
|
|
1690
|
+
transportState.noteRoutedAttemptSend(passthroughEstimate, "connection-reset");
|
|
1691
|
+
// Charged to the SAME request counter every other send goes through. The
|
|
1692
|
+
// replacement is bought here rather than by a nested retry helper, so there is
|
|
1693
|
+
// one charge for one send and no per-layer counter to reconcile.
|
|
1694
|
+
noteTransientSends(1);
|
|
1695
|
+
},
|
|
1696
|
+
}),
|
|
1697
|
+
route.provider.authMode === "forward",
|
|
1698
|
+
).then(adoptObservedResponse),
|
|
1699
|
+
error,
|
|
1700
|
+
{
|
|
1701
|
+
abortSignal: upstream.signal,
|
|
1702
|
+
label: safeHostLabel(request.url),
|
|
1703
|
+
attempts: remainingTransientSendBudget(transientSendAttempts()),
|
|
1704
|
+
// The whole decision, including the stage the preflight observed and the grant the
|
|
1705
|
+
// pre-header helper shares. A committed stage refuses here without touching the
|
|
1706
|
+
// allowance, which is what keeps a turn that already emitted output from draining
|
|
1707
|
+
// the replacement a later ambiguous reset would have been entitled to.
|
|
1708
|
+
authorize: () => authorizeResendForRecovery(stage, "connection-reset", ambiguousResend()).allowed,
|
|
1709
|
+
acceptResponse: candidate => {
|
|
1710
|
+
const type = candidate.headers.get("content-type")?.toLowerCase() ?? "";
|
|
1711
|
+
return type.includes("text/event-stream") || (!type && parsed.stream);
|
|
1712
|
+
},
|
|
1713
|
+
},
|
|
1714
|
+
),
|
|
1715
|
+
{ allowMissingContentType: !streamRecoveryContentType && parsed.stream },
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1547
1718
|
break;
|
|
1548
1719
|
}
|
|
1549
1720
|
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { formatErrorResponse } from "../../bridge";
|
|
2
2
|
import { isCyberPolicyCode, isCyberPolicyMessage } from "../../lib/errors";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
applyReplayRefusalClientHeaders,
|
|
5
|
+
isReplayRefusalCode,
|
|
6
|
+
retainReplayRefusal,
|
|
7
|
+
UPSTREAM_RESET_REPLAY_REFUSED_CODE,
|
|
8
|
+
} from "../../lib/upstream-retry";
|
|
4
9
|
import {
|
|
5
10
|
resolveClientRetryAfter,
|
|
6
11
|
validateClientRetryAfterHeader,
|
|
@@ -62,6 +67,10 @@ function isReplayRefusalBody(body: string): boolean {
|
|
|
62
67
|
* - a replay refusal this proxy wrote gets none and keeps none: the whole point of the
|
|
63
68
|
* refusal is that the turn may already be running, and the synthetic default for a
|
|
64
69
|
* retryable 429 is a direct instruction to the client to send it a second time
|
|
70
|
+
*
|
|
71
|
+
* A refusal also leaves with the shared no-retry header and the in-process marker, so the
|
|
72
|
+
* verdict survives this re-wrap as a property of the response rather than as a status a later
|
|
73
|
+
* reader would have to guess from.
|
|
65
74
|
*/
|
|
66
75
|
export function formatPassthroughUpstreamError(
|
|
67
76
|
status: number,
|
|
@@ -84,11 +93,10 @@ export function formatPassthroughUpstreamError(
|
|
|
84
93
|
const upstreamRetryAfter = options?.headers?.get("retry-after")?.trim() || undefined;
|
|
85
94
|
const originalValid = validateClientRetryAfterHeader(upstreamRetryAfter, now);
|
|
86
95
|
const cyberPolicyFailure = isCyberPolicyBody(trimmed);
|
|
96
|
+
const replayRefusal = options?.replayRefusal === true || isReplayRefusalBody(trimmed);
|
|
87
97
|
// Two different reasons to answer with no wait at all, handled the same way: a hard policy
|
|
88
98
|
// block will not become servable, and a refusal we made was never a rate limit.
|
|
89
|
-
const suppressRetryAfter = cyberPolicyFailure
|
|
90
|
-
|| options?.replayRefusal === true
|
|
91
|
-
|| isReplayRefusalBody(trimmed);
|
|
99
|
+
const suppressRetryAfter = cyberPolicyFailure || replayRefusal;
|
|
92
100
|
const resolved = suppressRetryAfter
|
|
93
101
|
? undefined
|
|
94
102
|
: resolveClientRetryAfter({
|
|
@@ -105,7 +113,9 @@ export function formatPassthroughUpstreamError(
|
|
|
105
113
|
&& upstreamRetryAfter !== undefined
|
|
106
114
|
&& originalValid === undefined);
|
|
107
115
|
|
|
108
|
-
|
|
116
|
+
// A refusal always takes the rewriting path: it has a header to add even when the
|
|
117
|
+
// upstream named no wait for it to remove.
|
|
118
|
+
if (!needsSet && !needsDelete && !replayRefusal) {
|
|
109
119
|
return new Response(bodyText, {
|
|
110
120
|
status,
|
|
111
121
|
...(options?.statusText ? { statusText: options.statusText } : {}),
|
|
@@ -118,21 +128,30 @@ export function formatPassthroughUpstreamError(
|
|
|
118
128
|
: new Headers({ "Content-Type": "application/json" });
|
|
119
129
|
if (needsSet) headers.set("Retry-After", resolved!);
|
|
120
130
|
else headers.delete("Retry-After");
|
|
121
|
-
|
|
131
|
+
if (replayRefusal) applyReplayRefusalClientHeaders(headers);
|
|
132
|
+
const rewritten = new Response(bodyText, {
|
|
122
133
|
status,
|
|
123
134
|
...(options?.statusText ? { statusText: options.statusText } : {}),
|
|
124
135
|
headers,
|
|
125
136
|
});
|
|
137
|
+
return replayRefusal ? retainReplayRefusal(rewritten) : rewritten;
|
|
126
138
|
}
|
|
127
139
|
|
|
128
140
|
const response = formatErrorResponse(
|
|
129
141
|
status,
|
|
130
142
|
"upstream_error",
|
|
131
143
|
`Provider error ${status}: (empty body)`,
|
|
132
|
-
|
|
144
|
+
// Provenance is all that is left when the bounded read returned nothing display-safe, and
|
|
145
|
+
// it is enough: the formatter allowlists this code, restates the refusal status and marks
|
|
146
|
+
// the response, so an unreadable refusal reaches the client as the same refusal.
|
|
147
|
+
replayRefusal
|
|
148
|
+
? { code: UPSTREAM_RESET_REPLAY_REFUSED_CODE }
|
|
149
|
+
: resolved !== undefined ? { retryAfter: resolved } : undefined,
|
|
133
150
|
);
|
|
134
151
|
const headers = new Headers(response.headers);
|
|
135
152
|
headers.set("Content-Type", "application/json");
|
|
136
153
|
if (resolved !== undefined) headers.set("Retry-After", resolved);
|
|
137
|
-
|
|
154
|
+
if (replayRefusal) applyReplayRefusalClientHeaders(headers);
|
|
155
|
+
const wrapped = new Response(response.body, { status: response.status, headers });
|
|
156
|
+
return replayRefusal ? retainReplayRefusal(wrapped) : wrapped;
|
|
138
157
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { comboFailureDecision } from "../../combos/failover";
|
|
2
2
|
import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
3
|
-
import { readJsonRequestBody, resolveInboundBodyLimitBytes } from "../request-decompress";
|
|
4
3
|
import { finishRequestAttempt, type RequestLogContext } from "../request-log";
|
|
5
4
|
import { linkRequestSessionLane } from "../request-log-conversation";
|
|
6
5
|
import type { OcxConfig } from "../../types";
|
|
@@ -129,6 +128,7 @@ export async function handleResponsesWithPolicyFallback(
|
|
|
129
128
|
const runCore = deps.runCore ?? handleResponsesCore;
|
|
130
129
|
let requestBodyReadNotified = false;
|
|
131
130
|
let storedPool401ReplayDispatched = false;
|
|
131
|
+
let rawBody: Record<string, unknown> | null = null;
|
|
132
132
|
const coreOptions: CoreOptions = {
|
|
133
133
|
...options,
|
|
134
134
|
openAiSidecarAuth: options.openAiSidecarAuth === undefined
|
|
@@ -144,23 +144,15 @@ export async function handleResponsesWithPolicyFallback(
|
|
|
144
144
|
options.onRequestBodyRead?.();
|
|
145
145
|
},
|
|
146
146
|
} : {}),
|
|
147
|
+
onRequestBodyParsed: body => {
|
|
148
|
+
options.onRequestBodyParsed?.(body);
|
|
149
|
+
if (body && typeof body === "object" && !Array.isArray(body)) rawBody = body as Record<string, unknown>;
|
|
150
|
+
},
|
|
147
151
|
onStoredPool401ReplayDispatched: () => {
|
|
148
152
|
storedPool401ReplayDispatched = true;
|
|
149
153
|
options.onStoredPool401ReplayDispatched?.();
|
|
150
154
|
},
|
|
151
155
|
};
|
|
152
|
-
let rawBody: Record<string, unknown> | null = null;
|
|
153
|
-
try {
|
|
154
|
-
const parsed = await readJsonRequestBody(
|
|
155
|
-
req.clone(),
|
|
156
|
-
undefined,
|
|
157
|
-
resolveInboundBodyLimitBytes(config.maxInboundBodyBytes),
|
|
158
|
-
);
|
|
159
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) rawBody = parsed as Record<string, unknown>;
|
|
160
|
-
} catch {
|
|
161
|
-
// Core owns the client-facing parse/decompression error.
|
|
162
|
-
}
|
|
163
|
-
|
|
164
156
|
let response: Response;
|
|
165
157
|
try {
|
|
166
158
|
response = await runCore(req, config, logCtx, coreOptions);
|