@bitkyc08/opencodex 2.55.0 → 2.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +642 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +566 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +431 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +339 -0
- package/src/server/index.ts +45 -2552
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +298 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +34 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +147 -1
- package/src/usage/summary.ts +171 -21
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -0,0 +1,869 @@
|
|
|
1
|
+
import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
|
|
2
|
+
import type { PreparedResponsesRequest } from "./request-prepare";
|
|
3
|
+
import type { ResponsesTransport } from "./request-transport";
|
|
4
|
+
import type { ResponsesSidecarAuth } from "./request-sidecar-auth";
|
|
5
|
+
import type { ResponsesEffects } from "./response-effects";
|
|
6
|
+
import type { PassthroughExchange } from "./passthrough-dispatch";
|
|
7
|
+
import {
|
|
8
|
+
sanitizePassthroughHeaders,
|
|
9
|
+
createSseInspector,
|
|
10
|
+
markEagerRelaySseResponse,
|
|
11
|
+
markNativePassthroughSseResponse,
|
|
12
|
+
consumeForInspection,
|
|
13
|
+
consumeForResponseLogMetadata,
|
|
14
|
+
relaySseWithFailedTail,
|
|
15
|
+
relayWithAbort,
|
|
16
|
+
} from "../relay";
|
|
17
|
+
import { isUsageDebugEnabled } from "../../usage/debug";
|
|
18
|
+
import { isReplayRefusalResponse } from "../../lib/upstream-retry";
|
|
19
|
+
import { teeWithBoundedInspection } from "../inspection-tee";
|
|
20
|
+
import {
|
|
21
|
+
codexForwardTerminalOutcomeRecorder,
|
|
22
|
+
usesCodexForwardPoolAuth,
|
|
23
|
+
codexQuotaOutcomeMeta,
|
|
24
|
+
codexDenialOutcomeMeta,
|
|
25
|
+
isFixedCodexAccount,
|
|
26
|
+
shouldDeferCodexResetDerivedCooldown,
|
|
27
|
+
} from "./core-codex-account";
|
|
28
|
+
import type { ResponsesTerminalStatus } from "../../bridge";
|
|
29
|
+
import { isCodexWsQuotaObservedResponse, isCodexWsUpstreamResponse } from "./ws-upstream";
|
|
30
|
+
import { recordSubagentQuotaFailureForThreadSpawn } from "../../codex/subagent-model-fallback";
|
|
31
|
+
import { recordCodexUpstreamOutcome } from "../../codex/routing";
|
|
32
|
+
import { codexProbeLeaseId, codexProbeQuotaScope, codexTransientProbeGrant } from "../../codex/auth-context";
|
|
33
|
+
import { consumeComboFailure } from "./core-combo-failure";
|
|
34
|
+
import { readDisplaySafeErrorText } from "./core-errors";
|
|
35
|
+
import { streamingContextOverflowResponse, jsonContextOverflowResponse } from "./context-overflow";
|
|
36
|
+
import { formatPassthroughUpstreamError } from "./passthrough-error";
|
|
37
|
+
import {
|
|
38
|
+
providerModelResponsesTerminalRepair,
|
|
39
|
+
providerModelResponsesUpstreamStreaming,
|
|
40
|
+
} from "../../providers/registry";
|
|
41
|
+
import {
|
|
42
|
+
resolvePassthroughWebSearchBridgeAuth,
|
|
43
|
+
planPassthroughWebSearchBridge,
|
|
44
|
+
createPassthroughWebSearchBridgeStream,
|
|
45
|
+
createPassthroughWebSearchBridgeExecutor,
|
|
46
|
+
} from "../../web-search/passthrough-bridge";
|
|
47
|
+
import { fetchWithHeaderTimeout, providerFetch } from "./fetch-helpers";
|
|
48
|
+
import { providerApiKeySelectionIsCurrent } from "../../providers/api-key-selection";
|
|
49
|
+
import { requiresVisionPreprocessing } from "../../vision";
|
|
50
|
+
import { checkOutboundBodySize, describeOutboundBodyRefusal } from "./outbound-body-guard";
|
|
51
|
+
import { relayResponsesSseWithTerminalRepair } from "../responses-terminal-repair";
|
|
52
|
+
import {
|
|
53
|
+
hasResponsesSnapshotRepair,
|
|
54
|
+
createResponsesSnapshotBlockRewrite,
|
|
55
|
+
} from "../responses-snapshot-repair";
|
|
56
|
+
import { createResponsesModelPayloadRewrite, rewriteResponsesModelJson } from "../responses-model-rewrite";
|
|
57
|
+
import { createImageGenCallRestoreRewrite, restoreImageGenCallsInJson } from "../responses-image-gen-repair";
|
|
58
|
+
import {
|
|
59
|
+
createSelfNamedToolCallNamespaceScrubRewrite,
|
|
60
|
+
scrubSelfNamedToolCallNamespaceInJson,
|
|
61
|
+
} from "../responses-self-named-namespace-scrub";
|
|
62
|
+
import {
|
|
63
|
+
createMuseToolNameRestoreRewrite,
|
|
64
|
+
restoreMuseToolNamesInJson,
|
|
65
|
+
} from "../../responses/muse-tool-name-alias";
|
|
66
|
+
import {
|
|
67
|
+
createRoutedNamespaceCallRestoreRewrite,
|
|
68
|
+
restoreRoutedNamespaceCallsInJson,
|
|
69
|
+
} from "../../responses/namespace-tool-compat";
|
|
70
|
+
import {
|
|
71
|
+
hasResponsesItemIdRepair,
|
|
72
|
+
createResponsesItemIdPayloadRewrite,
|
|
73
|
+
repairResponsesJsonItemIds,
|
|
74
|
+
} from "../responses-item-id-repair";
|
|
75
|
+
import {
|
|
76
|
+
payloadRewriteAsBlockRewrite,
|
|
77
|
+
composeSsePayloadRewrites,
|
|
78
|
+
composeSseBlockRewrites,
|
|
79
|
+
relaySseWithBlockRewrite,
|
|
80
|
+
} from "../sse-payload-rewrite";
|
|
81
|
+
import { createRoutedCustomToolRestoreBlockRewrite } from "../responses-custom-tool-repair";
|
|
82
|
+
import { createRoutedToolSearchRestoreBlockRewrite } from "../responses-tool-search-repair";
|
|
83
|
+
import { createGithubCopilotResponsesBlockRewrite } from "../github-copilot-responses-repair";
|
|
84
|
+
import { createGrokResponsesControlFrameBlockRewrite } from "../grok-responses-control-frame";
|
|
85
|
+
import { createGrokResponsesSparseTerminalBlockRewrite } from "../grok-responses-snapshot-repair";
|
|
86
|
+
import {
|
|
87
|
+
createPlaintextV2AgentMessageCallRestoreRewrite,
|
|
88
|
+
restorePlaintextV2AgentMessageCallsInJsonResult,
|
|
89
|
+
PLAINTEXT_V2_AGENT_MESSAGE_RESTORE_OVERFLOW_MESSAGE,
|
|
90
|
+
} from "../../responses/plaintext-v2-agent-messages";
|
|
91
|
+
import { createResponsesFieldBackfillBlockRewrite } from "./responses-field-backfill";
|
|
92
|
+
import { createResponsesFunctionToolRepairBlockRewrite } from "../responses-function-tool-repair";
|
|
93
|
+
import {
|
|
94
|
+
createUndeclaredToolCallGuardBlockRewrite,
|
|
95
|
+
undeclaredToolCallNameInResponse,
|
|
96
|
+
undeclaredToolCallMessage,
|
|
97
|
+
normalizeDefaultNamespaceInJson,
|
|
98
|
+
} from "../responses-undeclared-tool-guard";
|
|
99
|
+
import { isWin32EagerRewrite, selectEagerPath } from "../../lib/bun-stream-caps";
|
|
100
|
+
import { linkAbortSignal, UPSTREAM_JSON_BODY_READ_OPTIONS } from "./core-lifetime";
|
|
101
|
+
import { registerTurn, unregisterTurn, trackStreamLifetime } from "../lifecycle";
|
|
102
|
+
import { relaySseEagerBounded } from "../relay-eager";
|
|
103
|
+
import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
104
|
+
import { formatErrorResponse } from "../../bridge";
|
|
105
|
+
import { inspectResponseLogJson } from "../request-log";
|
|
106
|
+
import { restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
|
|
107
|
+
import { restoreRoutedToolSearchCallsInJson } from "../../responses/tool-search-compat";
|
|
108
|
+
import { responsesJsonToSseStream } from "../responses-json-events";
|
|
109
|
+
|
|
110
|
+
/** One responsibility of the Responses request pipeline; state owners are explicit. */
|
|
111
|
+
export async function deliverPassthroughResponse(
|
|
112
|
+
requestContext: Pick<ResponsesRequestContext, "logCtx" | "config" | "options" | "req">,
|
|
113
|
+
admissionState: ResponsesAdmissionState,
|
|
114
|
+
requestState: Pick<
|
|
115
|
+
PreparedResponsesRequest,
|
|
116
|
+
| "parsed"
|
|
117
|
+
| "route"
|
|
118
|
+
| "subagentQuotaFailureModel"
|
|
119
|
+
| "subagentFallbackAccountId"
|
|
120
|
+
| "clientRequestedStream"
|
|
121
|
+
| "translatorBudget"
|
|
122
|
+
>,
|
|
123
|
+
transportState: Pick<ResponsesTransport, "requestBindings">,
|
|
124
|
+
sidecarState: Pick<ResponsesSidecarAuth, "openAiSidecar">,
|
|
125
|
+
responseEffects: Pick<
|
|
126
|
+
ResponsesEffects,
|
|
127
|
+
| "plaintextV2AgentMessageToolNames"
|
|
128
|
+
| "commitReasoningReplayServingRoute"
|
|
129
|
+
| "routedMuseToolNameAliases"
|
|
130
|
+
| "routedNamespaceToolAliases"
|
|
131
|
+
| "plaintextV2AgentMessageAliasedToolNames"
|
|
132
|
+
| "recordTerminalOutcomes"
|
|
133
|
+
| "responseCompletionCancelled"
|
|
134
|
+
>,
|
|
135
|
+
nativeExchange: Pick<
|
|
136
|
+
PassthroughExchange,
|
|
137
|
+
| "upstreamResponse"
|
|
138
|
+
| "codexSafetyBufferingOptions"
|
|
139
|
+
| "upstream"
|
|
140
|
+
| "request"
|
|
141
|
+
| "connectMs"
|
|
142
|
+
| "imageGenCallAliases"
|
|
143
|
+
| "selfNamedNamespaceScrubAuthorization"
|
|
144
|
+
| "authorizedBareNamespaceToolAliases"
|
|
145
|
+
| "rememberPassthroughResponseChecked"
|
|
146
|
+
| "routedCustomToolNames"
|
|
147
|
+
| "routedCustomToolRepairNames"
|
|
148
|
+
| "declaredWireToolNames"
|
|
149
|
+
| "routedToolSearchNames"
|
|
150
|
+
| "outboundRequestBody"
|
|
151
|
+
| "functionRepairSchemas"
|
|
152
|
+
| "undeclaredToolGuardActive"
|
|
153
|
+
| "declaredNamelessClientCallTypes"
|
|
154
|
+
| "providerExecutedCallTypes"
|
|
155
|
+
| "declaredBareWireToolNames"
|
|
156
|
+
| "rememberPassthroughResponse"
|
|
157
|
+
| "noteInspectedPayload"
|
|
158
|
+
| "normalizeFunctionCompletionJson"
|
|
159
|
+
>,
|
|
160
|
+
): Promise<Response> {
|
|
161
|
+
const { logCtx, config, options, req } = requestContext;
|
|
162
|
+
const {
|
|
163
|
+
upstreamResponse,
|
|
164
|
+
codexSafetyBufferingOptions,
|
|
165
|
+
upstream,
|
|
166
|
+
connectMs,
|
|
167
|
+
imageGenCallAliases,
|
|
168
|
+
selfNamedNamespaceScrubAuthorization,
|
|
169
|
+
authorizedBareNamespaceToolAliases,
|
|
170
|
+
rememberPassthroughResponseChecked,
|
|
171
|
+
routedCustomToolNames,
|
|
172
|
+
routedCustomToolRepairNames,
|
|
173
|
+
declaredWireToolNames,
|
|
174
|
+
routedToolSearchNames,
|
|
175
|
+
functionRepairSchemas,
|
|
176
|
+
declaredNamelessClientCallTypes,
|
|
177
|
+
providerExecutedCallTypes,
|
|
178
|
+
declaredBareWireToolNames,
|
|
179
|
+
rememberPassthroughResponse,
|
|
180
|
+
noteInspectedPayload,
|
|
181
|
+
normalizeFunctionCompletionJson,
|
|
182
|
+
} = nativeExchange;
|
|
183
|
+
const { commitReasoningReplayServingRoute, recordTerminalOutcomes } = responseEffects;
|
|
184
|
+
const { parsed, route, subagentQuotaFailureModel, clientRequestedStream, translatorBudget } = requestState;
|
|
185
|
+
const { openAiSidecar } = sidecarState;
|
|
186
|
+
const { requestBindings } = transportState;
|
|
187
|
+
|
|
188
|
+
const headers = sanitizePassthroughHeaders(upstreamResponse.headers, codexSafetyBufferingOptions);
|
|
189
|
+
const resolvedModel = headers.get("openai-model")?.trim();
|
|
190
|
+
if (resolvedModel && !logCtx.preserveResolvedModelFromRoute) logCtx.resolvedModel = resolvedModel;
|
|
191
|
+
if (isUsageDebugEnabled()) {
|
|
192
|
+
const upstreamContentType = upstreamResponse.headers.get("content-type");
|
|
193
|
+
if (upstreamContentType) logCtx.usageDebugContentType = upstreamContentType;
|
|
194
|
+
}
|
|
195
|
+
// The chatgpt backend may omit Content-Type on SSE responses. Fall back to
|
|
196
|
+
// treating a successful body as SSE when the caller requested streaming.
|
|
197
|
+
const passthroughCt = headers.get("content-type")?.toLowerCase();
|
|
198
|
+
const isEventStream = passthroughCt?.includes("text/event-stream")
|
|
199
|
+
|| (responseEffects.plaintextV2AgentMessageToolNames.size === 0 && upstreamResponse.ok && !!upstreamResponse.body && !passthroughCt && parsed.stream);
|
|
200
|
+
const recordTerminalOutcome = codexForwardTerminalOutcomeRecorder(
|
|
201
|
+
config,
|
|
202
|
+
admissionState.authCtx,
|
|
203
|
+
route.provider,
|
|
204
|
+
route.modelId,
|
|
205
|
+
logCtx,
|
|
206
|
+
);
|
|
207
|
+
let terminalOutcomeRecorded = false;
|
|
208
|
+
const terminalRecorder = recordTerminalOutcome
|
|
209
|
+
? (status: ResponsesTerminalStatus, httpStatusOverride?: number): void => {
|
|
210
|
+
if (terminalOutcomeRecorded) return;
|
|
211
|
+
terminalOutcomeRecorded = true;
|
|
212
|
+
recordTerminalOutcome(status, httpStatusOverride);
|
|
213
|
+
}
|
|
214
|
+
: undefined;
|
|
215
|
+
const terminalBodyWillRecord = !!terminalRecorder && upstreamResponse.ok && isEventStream;
|
|
216
|
+
// Capture quota from upstream response for multi-account tracking
|
|
217
|
+
if (usesCodexForwardPoolAuth(admissionState.authCtx, route.provider)) {
|
|
218
|
+
// primary was the 5h window; it now carries weekly data for GPT plans.
|
|
219
|
+
// Prefer primary when present, fall back to secondary for compatibility.
|
|
220
|
+
const quotaMeta = { ...codexQuotaOutcomeMeta(upstreamResponse), ...(await codexDenialOutcomeMeta(upstreamResponse)) };
|
|
221
|
+
const { applyAccountQuotaFromUpstreamHeaders } = await import("../../codex/auth-api");
|
|
222
|
+
if (!isCodexWsQuotaObservedResponse(upstreamResponse)) {
|
|
223
|
+
applyAccountQuotaFromUpstreamHeaders(admissionState.authCtx.accountId, upstreamResponse.headers,
|
|
224
|
+
admissionState.authCtx.writerGeneration, admissionState.authCtx.kind === "main-pool" ? admissionState.authCtx.mainQuotaWriter : undefined,
|
|
225
|
+
{ modelId: route.modelId, poolWriter: admissionState.authCtx.kind === "pool" ? admissionState.authCtx.poolQuotaWriter : undefined });
|
|
226
|
+
}
|
|
227
|
+
if (terminalBodyWillRecord) {
|
|
228
|
+
options.setTerminalOutcomeRecorder?.((status, httpStatusOverride) => {
|
|
229
|
+
terminalRecorder(status, httpStatusOverride);
|
|
230
|
+
if (status === "failed" || status === "incomplete") {
|
|
231
|
+
const quotaFailureMessage = [httpStatusOverride, logCtx.terminalHttpStatus]
|
|
232
|
+
.find(value => value === 429 || value === 402);
|
|
233
|
+
if (!isFixedCodexAccount(admissionState.authCtx) && quotaFailureMessage !== undefined) {
|
|
234
|
+
recordSubagentQuotaFailureForThreadSpawn(
|
|
235
|
+
req.headers,
|
|
236
|
+
subagentQuotaFailureModel,
|
|
237
|
+
quotaFailureMessage,
|
|
238
|
+
config,
|
|
239
|
+
requestState.subagentFallbackAccountId,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
options.onNativePassthroughTerminal?.(status);
|
|
244
|
+
});
|
|
245
|
+
} else if (!shouldDeferCodexResetDerivedCooldown(
|
|
246
|
+
upstreamResponse,
|
|
247
|
+
options.deferCodexResetDerivedCooldown,
|
|
248
|
+
) && !isReplayRefusalResponse(upstreamResponse)) {
|
|
249
|
+
// A refusal this proxy made is not evidence about the account. Recording it would
|
|
250
|
+
// classify the synthetic 429 as quota exhaustion and write a default cooldown against
|
|
251
|
+
// a credential the request may never have reached, and that false signal outlives the
|
|
252
|
+
// request. The sibling recorders on this path already decline: the terminal recorder
|
|
253
|
+
// needs an ok streaming body, and the quota-header snapshot finds no quota headers.
|
|
254
|
+
recordCodexUpstreamOutcome(config, admissionState.authCtx.accountId, upstreamResponse.status, {
|
|
255
|
+
...quotaMeta,
|
|
256
|
+
threadId: admissionState.authCtx.affinityKey,
|
|
257
|
+
fixedAccount: admissionState.authCtx.fixedAccount,
|
|
258
|
+
modelId: route.modelId,
|
|
259
|
+
probeLeaseId: codexProbeLeaseId(admissionState.authCtx),
|
|
260
|
+
probeQuotaScope: codexProbeQuotaScope(admissionState.authCtx),
|
|
261
|
+
transientProbe: codexTransientProbeGrant(admissionState.authCtx),
|
|
262
|
+
writerGeneration: admissionState.authCtx.writerGeneration,
|
|
263
|
+
// Includes a replay's second 401, which is the case that actually retires the
|
|
264
|
+
// account — fence it on the credential the request was holding.
|
|
265
|
+
...(admissionState.authCtx.kind === "pool" ? { credentialGeneration: admissionState.authCtx.generation } : {}),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Non-2xx passthrough failures must never reach Codex as an empty body —
|
|
271
|
+
// Codex renders that as the opaque "Unknown error" (#452). Combo attempts
|
|
272
|
+
// keep their typed failure envelope. Except for the classified 413 below,
|
|
273
|
+
// non-empty bodies are relayed verbatim
|
|
274
|
+
// (headers included) so pool-retry Activation B/D and client diagnostics stay intact.
|
|
275
|
+
// Manual-redirect policy (#914): a 3xx is relayed as-is (Location preserved
|
|
276
|
+
// through sanitizePassthroughHeaders) so a redirect to a dead host can never
|
|
277
|
+
// masquerade as a pre-connection failure after the credential was seen.
|
|
278
|
+
// The numeric outcome above already classified it neutral — no streak.
|
|
279
|
+
if (upstreamResponse.status >= 300 && upstreamResponse.status < 400) {
|
|
280
|
+
return new Response(upstreamResponse.body, {
|
|
281
|
+
status: upstreamResponse.status,
|
|
282
|
+
statusText: upstreamResponse.statusText,
|
|
283
|
+
headers: sanitizePassthroughHeaders(upstreamResponse.headers, codexSafetyBufferingOptions),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
if (!upstreamResponse.ok) {
|
|
287
|
+
if (options.comboAttempt) {
|
|
288
|
+
// No pre-read guard here: `consumeComboFailure` -> `readBoundedResponseBody` reads
|
|
289
|
+
// `response.body` itself and already threads the abort signal through its own read,
|
|
290
|
+
// and the combo contract is that this body's getter is touched exactly once (pinned by
|
|
291
|
+
// "captures passthrough failed usage from its original bounded body exactly once").
|
|
292
|
+
// Attaching a guard would be a second `.body` access and break that contract for no
|
|
293
|
+
// gain, since the bounded reader owns settlement on this path.
|
|
294
|
+
const failure = await consumeComboFailure(upstreamResponse, options.abortSignal);
|
|
295
|
+
options.onConsumedComboFailure?.(failure);
|
|
296
|
+
return failure.response;
|
|
297
|
+
}
|
|
298
|
+
// The bounded reader owns the original body, deadline, abort settlement, and lock.
|
|
299
|
+
// Unsafe partial data falls back to #452's non-empty status-only JSON.
|
|
300
|
+
const errorText = await readDisplaySafeErrorText(upstreamResponse, upstream.signal, "");
|
|
301
|
+
if (upstreamResponse.status === 413) {
|
|
302
|
+
return clientRequestedStream
|
|
303
|
+
? streamingContextOverflowResponse(parsed._responseModelId ?? parsed.modelId, translatorBudget)
|
|
304
|
+
: jsonContextOverflowResponse();
|
|
305
|
+
}
|
|
306
|
+
return formatPassthroughUpstreamError(upstreamResponse.status, errorText, {
|
|
307
|
+
statusText: upstreamResponse.statusText,
|
|
308
|
+
headers,
|
|
309
|
+
// Provenance, not inference: `errorText` is empty when the bounded read finds nothing
|
|
310
|
+
// display-safe, and an empty body is exactly what the retryable-429 default fires on.
|
|
311
|
+
replayRefusal: isReplayRefusalResponse(upstreamResponse),
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Bun#32111 workaround: passthrough SSE uses tee()+native relay to avoid the
|
|
316
|
+
// async-pull segfault on Windows. Branch[0] goes directly to the Response (Bun
|
|
317
|
+
// native relay, never enters JS Sink.write); branch[1] is consumed in the
|
|
318
|
+
// background for terminal-outcome/quota inspection only.
|
|
319
|
+
// #314 alternative shape: win32 no-rewrite traffic follows the runtime/config
|
|
320
|
+
// gate; darwin no-rewrite traffic joins it only for explicit
|
|
321
|
+
// `streamMode: "eager-relay"` opt-in. Darwin `auto` always stays tee. The
|
|
322
|
+
// eager shape skips tee and uses one bounded reader with inline inspection
|
|
323
|
+
// (src/server/relay-eager.ts; policy:
|
|
324
|
+
// devlog/_fin/260731_macos_rss_retention/100_darwin_eager_optin.md).
|
|
325
|
+
// The bundled known-bad runtime remains on tee by default on both platforms.
|
|
326
|
+
if (isEventStream && upstreamResponse.body) {
|
|
327
|
+
// For streamed passthrough, a successful terminal response means non-error upstream status
|
|
328
|
+
// before relay starts. Waiting for SSE completion would retain request state across the whole
|
|
329
|
+
// stream; a later body failure does not undo that this destination accepted and served the turn.
|
|
330
|
+
commitReasoningReplayServingRoute(nativeExchange.request.headers);
|
|
331
|
+
const terminalRepairPolicy = providerModelResponsesTerminalRepair(
|
|
332
|
+
route.providerName,
|
|
333
|
+
route.provider,
|
|
334
|
+
route.modelId,
|
|
335
|
+
);
|
|
336
|
+
// #3761: opt-in hosted-web-search bridge. Codex always declares the hosted web_search tool,
|
|
337
|
+
// and this branch relays that declaration on the assumption the destination executes it.
|
|
338
|
+
// A KEY-auth gateway that does not (Ollama Cloud GLM) answers with a function_call named
|
|
339
|
+
// web_search that nothing runs, and the undeclared-tool guard below ends the turn. When the
|
|
340
|
+
// provider opts in, the bridge intercepts that one call, runs the search, continues the
|
|
341
|
+
// conversation upstream, and hands back ordinary Responses SSE — so every rewrite below,
|
|
342
|
+
// including the guard itself, still inspects the client-facing stream. Default OFF: without
|
|
343
|
+
// the opt-in this is one planner call and the relay is byte-identical to before.
|
|
344
|
+
const webSearchBridgeAuth = resolvePassthroughWebSearchBridgeAuth(
|
|
345
|
+
route.provider.webSearchBridge?.backend,
|
|
346
|
+
config,
|
|
347
|
+
openAiSidecar,
|
|
348
|
+
);
|
|
349
|
+
const webSearchBridgePlan = planPassthroughWebSearchBridge(parsed, route.provider, {
|
|
350
|
+
providerName: route.providerName,
|
|
351
|
+
isPassthrough: true,
|
|
352
|
+
stream: parsed.stream === true,
|
|
353
|
+
auth: webSearchBridgeAuth,
|
|
354
|
+
});
|
|
355
|
+
// Capture the binding that actually served the first leg, after its permitted reselection.
|
|
356
|
+
const webSearchBridgeBinding = requestBindings.get(nativeExchange.request);
|
|
357
|
+
// The bridge wraps the RAW upstream body, so terminal repair below still owns the single
|
|
358
|
+
// client-facing terminal — the bridge drops the terminal of every intercepted leg.
|
|
359
|
+
const upstreamSseBody = webSearchBridgePlan
|
|
360
|
+
? createPassthroughWebSearchBridgeStream({
|
|
361
|
+
plan: webSearchBridgePlan,
|
|
362
|
+
firstLeg: upstreamResponse.body,
|
|
363
|
+
requestBody: nativeExchange.request.body,
|
|
364
|
+
// Continuation legs replay the same built request with the executed search appended.
|
|
365
|
+
// The first leg already passed the recovery ladder, the outbound size ceiling, and the
|
|
366
|
+
// host circuit; a KEY-auth destination has no OAuth refresh to replay on a later leg.
|
|
367
|
+
send: (continuationBody: string) => fetchWithHeaderTimeout(
|
|
368
|
+
nativeExchange.request.url,
|
|
369
|
+
{ method: nativeExchange.request.method, headers: nativeExchange.request.headers, body: continuationBody },
|
|
370
|
+
upstream.signal,
|
|
371
|
+
connectMs,
|
|
372
|
+
true,
|
|
373
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
374
|
+
// Pacing can outlive a manual selection change. A continuation must retain the
|
|
375
|
+
// first leg's key and appended search result, never rebuild from the original turn.
|
|
376
|
+
beforeDispatch: () => {
|
|
377
|
+
if (webSearchBridgeBinding?.kind !== "api-key"
|
|
378
|
+
|| !providerApiKeySelectionIsCurrent(config, route.providerName, webSearchBridgeBinding.provider)) {
|
|
379
|
+
throw new Error("API key selection changed during a web-search continuation");
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
providerName: route.providerName,
|
|
383
|
+
modelId: route.modelId,
|
|
384
|
+
}),
|
|
385
|
+
false,
|
|
386
|
+
),
|
|
387
|
+
execute: createPassthroughWebSearchBridgeExecutor(webSearchBridgePlan, {
|
|
388
|
+
providerApiKey: route.provider.apiKey ?? "",
|
|
389
|
+
auth: webSearchBridgeAuth,
|
|
390
|
+
hostedTool: parsed._webSearch,
|
|
391
|
+
describeImages: requiresVisionPreprocessing(config, route.provider, route.modelId, route.providerName),
|
|
392
|
+
sidecar: config.webSearchSidecar,
|
|
393
|
+
}),
|
|
394
|
+
// Appending a search result can push the continuation past the ceiling the first leg
|
|
395
|
+
// was admitted under, so the same limit is re-applied before every later send.
|
|
396
|
+
checkOutboundBody: (continuationBody: string) => {
|
|
397
|
+
const result = checkOutboundBodySize(continuationBody, config.maxUpstreamBodyBytes);
|
|
398
|
+
return result.admitted ? undefined : describeOutboundBodyRefusal(result);
|
|
399
|
+
},
|
|
400
|
+
signal: upstream.signal,
|
|
401
|
+
})
|
|
402
|
+
: upstreamResponse.body;
|
|
403
|
+
const passthroughSseBody = terminalRepairPolicy
|
|
404
|
+
? relayResponsesSseWithTerminalRepair(
|
|
405
|
+
upstreamSseBody,
|
|
406
|
+
upstream,
|
|
407
|
+
terminalRepairPolicy,
|
|
408
|
+
translatorBudget,
|
|
409
|
+
options.responsesTerminalRepairScheduler,
|
|
410
|
+
)
|
|
411
|
+
: upstreamSseBody;
|
|
412
|
+
const repairConfig = route.provider.responsesItemIdRepair;
|
|
413
|
+
// Grok Build renders deltas live but reconstructs its durable assistant
|
|
414
|
+
// turn from the completed response snapshot. Native Responses streams
|
|
415
|
+
// may instead carry the complete items in output_item.done, so the
|
|
416
|
+
// explicit Grok compatibility marker enables strict client compatibility rewrites.
|
|
417
|
+
// The provider's broader snapshot/lifecycle repair remains opt-in.
|
|
418
|
+
const grokClientCompatibilityEnabled = logCtx.surface === "grok";
|
|
419
|
+
const snapshotRepairEnabled = hasResponsesSnapshotRepair(route.provider.responsesSnapshotRepair);
|
|
420
|
+
const githubCopilotRepairEnabled = route.providerName === "github-copilot";
|
|
421
|
+
const responseModelRewrite = parsed._responseModelId !== undefined
|
|
422
|
+
&& parsed._responseModelId !== parsed.modelId
|
|
423
|
+
? createResponsesModelPayloadRewrite(parsed._responseModelId)
|
|
424
|
+
: undefined;
|
|
425
|
+
// Compose opt-in payload rewrites into one parse/stringify pass (image-gen restore first).
|
|
426
|
+
const payloadRewrites = [
|
|
427
|
+
createImageGenCallRestoreRewrite(imageGenCallAliases),
|
|
428
|
+
// #3217: a call whose namespace repeats its own name is unroutable in codex-rs.
|
|
429
|
+
createSelfNamedToolCallNamespaceScrubRewrite(selfNamedNamespaceScrubAuthorization),
|
|
430
|
+
responseEffects.routedMuseToolNameAliases.size > 0
|
|
431
|
+
? createMuseToolNameRestoreRewrite(responseEffects.routedMuseToolNameAliases)
|
|
432
|
+
: undefined,
|
|
433
|
+
responseEffects.routedNamespaceToolAliases.size > 0
|
|
434
|
+
? createRoutedNamespaceCallRestoreRewrite(responseEffects.routedNamespaceToolAliases)
|
|
435
|
+
: undefined,
|
|
436
|
+
authorizedBareNamespaceToolAliases.size > 0
|
|
437
|
+
? createRoutedNamespaceCallRestoreRewrite(authorizedBareNamespaceToolAliases)
|
|
438
|
+
: undefined,
|
|
439
|
+
hasResponsesItemIdRepair(repairConfig)
|
|
440
|
+
? createResponsesItemIdPayloadRewrite(repairConfig!, translatorBudget)
|
|
441
|
+
: undefined,
|
|
442
|
+
responseModelRewrite,
|
|
443
|
+
].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
|
|
444
|
+
// #893: sparse-snapshot gateways get field backfills AND lifecycle event
|
|
445
|
+
// injection at the block level, after payload rewrites. Defaults come
|
|
446
|
+
// from the finalized OUTBOUND body — the normalized internal tool shapes
|
|
447
|
+
// are not the Responses wire shapes the snapshot must mirror.
|
|
448
|
+
// Only validated client blocks may publish plaintext continuation state.
|
|
449
|
+
// Raw inspection precedes rewriting on eager relays, so it cannot own this write.
|
|
450
|
+
const plaintextInspector = responseEffects.plaintextV2AgentMessageToolNames.size > 0
|
|
451
|
+
? createSseInspector({ onCompletedResponse: rememberPassthroughResponseChecked })
|
|
452
|
+
: undefined;
|
|
453
|
+
const plaintextEncoder = plaintextInspector ? new TextEncoder() : undefined;
|
|
454
|
+
const rememberPlaintextBlock = plaintextInspector
|
|
455
|
+
? Object.assign((block: string): readonly string[] => {
|
|
456
|
+
plaintextInspector.feed(plaintextEncoder!.encode(`${block}\n\n`));
|
|
457
|
+
return [block];
|
|
458
|
+
}, { dispose: () => plaintextInspector.dispose() })
|
|
459
|
+
: undefined;
|
|
460
|
+
const blockRewrites = [
|
|
461
|
+
payloadRewrites.length > 0
|
|
462
|
+
? payloadRewriteAsBlockRewrite(composeSsePayloadRewrites(...payloadRewrites))
|
|
463
|
+
: undefined,
|
|
464
|
+
routedCustomToolNames.size > 0 || routedCustomToolRepairNames.size > 0
|
|
465
|
+
? createRoutedCustomToolRestoreBlockRewrite(
|
|
466
|
+
routedCustomToolNames,
|
|
467
|
+
translatorBudget,
|
|
468
|
+
routedCustomToolRepairNames,
|
|
469
|
+
declaredWireToolNames,
|
|
470
|
+
)
|
|
471
|
+
: undefined,
|
|
472
|
+
routedToolSearchNames.size > 0
|
|
473
|
+
? createRoutedToolSearchRestoreBlockRewrite(routedToolSearchNames, translatorBudget)
|
|
474
|
+
: undefined,
|
|
475
|
+
githubCopilotRepairEnabled
|
|
476
|
+
? createGithubCopilotResponsesBlockRewrite(translatorBudget)
|
|
477
|
+
: undefined,
|
|
478
|
+
grokClientCompatibilityEnabled
|
|
479
|
+
? createGrokResponsesControlFrameBlockRewrite()
|
|
480
|
+
: undefined,
|
|
481
|
+
grokClientCompatibilityEnabled
|
|
482
|
+
? createGrokResponsesSparseTerminalBlockRewrite(translatorBudget)
|
|
483
|
+
: undefined,
|
|
484
|
+
snapshotRepairEnabled
|
|
485
|
+
? createResponsesSnapshotBlockRewrite(nativeExchange.outboundRequestBody, translatorBudget)
|
|
486
|
+
: undefined,
|
|
487
|
+
responseEffects.plaintextV2AgentMessageToolNames.size > 0
|
|
488
|
+
? payloadRewriteAsBlockRewrite(createPlaintextV2AgentMessageCallRestoreRewrite(
|
|
489
|
+
responseEffects.plaintextV2AgentMessageToolNames, responseEffects.plaintextV2AgentMessageAliasedToolNames,
|
|
490
|
+
))
|
|
491
|
+
: undefined,
|
|
492
|
+
createResponsesFieldBackfillBlockRewrite(),
|
|
493
|
+
functionRepairSchemas.size > 0
|
|
494
|
+
? createResponsesFunctionToolRepairBlockRewrite(functionRepairSchemas, translatorBudget)
|
|
495
|
+
: undefined,
|
|
496
|
+
// Last: every rewrite above can still rename or reshape a call item, so the guard must
|
|
497
|
+
// compare the names the client will actually receive against the declared catalog.
|
|
498
|
+
nativeExchange.undeclaredToolGuardActive
|
|
499
|
+
? createUndeclaredToolCallGuardBlockRewrite(
|
|
500
|
+
declaredWireToolNames,
|
|
501
|
+
declaredNamelessClientCallTypes,
|
|
502
|
+
providerExecutedCallTypes,
|
|
503
|
+
declaredBareWireToolNames,
|
|
504
|
+
)
|
|
505
|
+
: undefined,
|
|
506
|
+
rememberPlaintextBlock,
|
|
507
|
+
].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
|
|
508
|
+
const clientBlockRewrite = blockRewrites.length > 0
|
|
509
|
+
? composeSseBlockRewrites(...blockRewrites)
|
|
510
|
+
: undefined;
|
|
511
|
+
const needsClientRewrite = clientBlockRewrite !== undefined;
|
|
512
|
+
// #864: win32 rewrite traffic must never enter the tee()+JS-pull chain
|
|
513
|
+
// (Bun#32111 JS-sink segfault — text frames pass, the terminal block is
|
|
514
|
+
// lost). The eager single reader applies the same rewrites inline.
|
|
515
|
+
const win32EagerRewrite = isWin32EagerRewrite(process.platform, needsClientRewrite);
|
|
516
|
+
const eagerPath = selectEagerPath(
|
|
517
|
+
process.platform,
|
|
518
|
+
needsClientRewrite,
|
|
519
|
+
config.streamMode ?? "auto",
|
|
520
|
+
);
|
|
521
|
+
// A successful Codex WS upgrade is a push source. If it entered tee(),
|
|
522
|
+
// the inspection branch could drain continuously while the slow client
|
|
523
|
+
// branch retained bytes without a bound. Force the existing bounded,
|
|
524
|
+
// single-reader relay before tee; HTTP fallback responses stay unmarked.
|
|
525
|
+
const forceCodexWsEagerRelay = isCodexWsUpstreamResponse(upstreamResponse);
|
|
526
|
+
const inlineEagerRewrite = needsClientRewrite
|
|
527
|
+
&& (forceCodexWsEagerRelay || win32EagerRewrite || eagerPath?.useEagerRelay === true);
|
|
528
|
+
if (forceCodexWsEagerRelay || eagerPath?.useEagerRelay || win32EagerRewrite) {
|
|
529
|
+
const turnAc = new AbortController();
|
|
530
|
+
linkAbortSignal(upstream, turnAc.signal);
|
|
531
|
+
registerTurn(turnAc, options.turnAdmissionLease);
|
|
532
|
+
const reportNativeTerminal = recordTerminalOutcomes
|
|
533
|
+
? (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
|
|
534
|
+
terminalRecorder?.(status, httpStatusOverride);
|
|
535
|
+
if (status === "failed" || status === "incomplete") {
|
|
536
|
+
const quotaFailureMessage = [httpStatusOverride, logCtx.terminalHttpStatus]
|
|
537
|
+
.find(value => value === 429 || value === 402);
|
|
538
|
+
if (!isFixedCodexAccount(admissionState.authCtx) && quotaFailureMessage !== undefined) {
|
|
539
|
+
recordSubagentQuotaFailureForThreadSpawn(
|
|
540
|
+
req.headers,
|
|
541
|
+
subagentQuotaFailureModel,
|
|
542
|
+
quotaFailureMessage,
|
|
543
|
+
config,
|
|
544
|
+
requestState.subagentFallbackAccountId,
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
options.onNativePassthroughTerminal?.(status);
|
|
549
|
+
}
|
|
550
|
+
: undefined;
|
|
551
|
+
const inspector = createSseInspector({
|
|
552
|
+
onTerminal: reportNativeTerminal,
|
|
553
|
+
logCtx,
|
|
554
|
+
onCompletedResponse: rememberPassthroughResponse && responseEffects.plaintextV2AgentMessageToolNames.size === 0 ? rememberPassthroughResponseChecked : undefined,
|
|
555
|
+
onParsedPayload: noteInspectedPayload,
|
|
556
|
+
onFirstOutput: options.onFirstOutput,
|
|
557
|
+
pinCompletedResponseIdToFirstSeen: githubCopilotRepairEnabled,
|
|
558
|
+
});
|
|
559
|
+
const eagerBody = relaySseEagerBounded(passthroughSseBody, turnAc, {
|
|
560
|
+
inspectChunk: chunk => inspector.feed(chunk),
|
|
561
|
+
finishInspection: () => inspector.finish(),
|
|
562
|
+
disposeInspection: () => inspector.dispose(),
|
|
563
|
+
// Stream lifetime follows the protocol terminal even when this request
|
|
564
|
+
// has no outcome callback configured (reported() would stay false).
|
|
565
|
+
sawTerminal: () => inspector.terminalSeen(),
|
|
566
|
+
...(clientBlockRewrite
|
|
567
|
+
? { rewriteBlocks: clientBlockRewrite }
|
|
568
|
+
: {}),
|
|
569
|
+
onSynthetic: (kind, reason) => {
|
|
570
|
+
if (!reportNativeTerminal) return;
|
|
571
|
+
if (kind === "incomplete") {
|
|
572
|
+
logCtx.terminalSource = "synthetic";
|
|
573
|
+
reportNativeTerminal("incomplete");
|
|
574
|
+
} else if (reason === "upstream_error") {
|
|
575
|
+
logCtx.terminalSource = "synthetic";
|
|
576
|
+
reportNativeTerminal("failed", logCtx.terminalHttpStatus ?? 502);
|
|
577
|
+
} else {
|
|
578
|
+
logCtx.transportPhase = "mid_stream";
|
|
579
|
+
logCtx.terminalSource = "synthetic";
|
|
580
|
+
if (logCtx.activeAttempt) logCtx.activeAttempt.streamAborted = true;
|
|
581
|
+
reportNativeTerminal("failed", 502);
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
onClientCancel: () => {
|
|
585
|
+
responseEffects.responseCompletionCancelled = true;
|
|
586
|
+
options.onNativePassthroughCancel?.();
|
|
587
|
+
},
|
|
588
|
+
onDone: () => unregisterTurn(turnAc),
|
|
589
|
+
}, {
|
|
590
|
+
clientGoneSignal: options.abortSignal,
|
|
591
|
+
terminalBoundary: codexSafetyBufferingOptions,
|
|
592
|
+
...(inlineEagerRewrite ? { rewriteBudget: translatorBudget } : {}),
|
|
593
|
+
...(logCtx.upstreamError === undefined ? {} : { upstreamError: logCtx.upstreamError }),
|
|
594
|
+
});
|
|
595
|
+
// When selected, this relay closes response.completed even if upstream
|
|
596
|
+
// keeps the connection alive. Marked Codex WS traffic, Windows
|
|
597
|
+
// forced-rewrite traffic, and Darwin explicit eager traffic apply
|
|
598
|
+
// client rewrites inline rather than via the tee()+JS-pull chain.
|
|
599
|
+
if (!headers.has("content-type")) headers.set("content-type", "text/event-stream");
|
|
600
|
+
return markEagerRelaySseResponse(
|
|
601
|
+
markNativePassthroughSseResponse(new Response(eagerBody, {
|
|
602
|
+
status: upstreamResponse.status,
|
|
603
|
+
headers,
|
|
604
|
+
})),
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
const turnAc = new AbortController();
|
|
608
|
+
const clientGone = new AbortController();
|
|
609
|
+
const clientGoneSignal = options.abortSignal
|
|
610
|
+
? AbortSignal.any([clientGone.signal, options.abortSignal])
|
|
611
|
+
: clientGone.signal;
|
|
612
|
+
// Pace against raw bytes before rewrites, without detaching terminal ownership.
|
|
613
|
+
const [nativeBody, inspectBody] = teeWithBoundedInspection(passthroughSseBody, { clientGoneSignal });
|
|
614
|
+
linkAbortSignal(upstream, turnAc.signal);
|
|
615
|
+
registerTurn(turnAc, options.turnAdmissionLease);
|
|
616
|
+
const inspectionConsumerOptions = {
|
|
617
|
+
// Request abort can reject the fetch body before the response cancel hook runs.
|
|
618
|
+
clientGoneSignal,
|
|
619
|
+
drainBounds: { ms: 15_000, bytes: 32 * 1024 * 1024 },
|
|
620
|
+
upstream,
|
|
621
|
+
pinCompletedResponseIdToFirstSeen: githubCopilotRepairEnabled,
|
|
622
|
+
onParsedPayload: noteInspectedPayload,
|
|
623
|
+
};
|
|
624
|
+
if (recordTerminalOutcomes) {
|
|
625
|
+
// A real terminal was parsed from the (teed) inspection stream — record it as the outcome
|
|
626
|
+
// even if the client has already disconnected: the turn genuinely reached that terminal, so
|
|
627
|
+
// it must log as completed/failed, not be dropped or downgraded to a cancel (#44). A pure
|
|
628
|
+
// client-cancel (no terminal seen) is finalized separately via consumeForInspection's onCancel.
|
|
629
|
+
const reportNativeTerminal = (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
|
|
630
|
+
terminalRecorder?.(status, httpStatusOverride);
|
|
631
|
+
if (status === "failed" || status === "incomplete") {
|
|
632
|
+
const quotaFailureMessage = [httpStatusOverride, logCtx.terminalHttpStatus]
|
|
633
|
+
.find(value => value === 429 || value === 402);
|
|
634
|
+
if (!isFixedCodexAccount(admissionState.authCtx) && quotaFailureMessage !== undefined) {
|
|
635
|
+
recordSubagentQuotaFailureForThreadSpawn(
|
|
636
|
+
req.headers,
|
|
637
|
+
subagentQuotaFailureModel,
|
|
638
|
+
quotaFailureMessage,
|
|
639
|
+
config,
|
|
640
|
+
requestState.subagentFallbackAccountId,
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
options.onNativePassthroughTerminal?.(status);
|
|
645
|
+
};
|
|
646
|
+
consumeForInspection(
|
|
647
|
+
inspectBody,
|
|
648
|
+
reportNativeTerminal,
|
|
649
|
+
turnAc.signal,
|
|
650
|
+
() => unregisterTurn(turnAc),
|
|
651
|
+
logCtx,
|
|
652
|
+
() => {
|
|
653
|
+
responseEffects.responseCompletionCancelled = true;
|
|
654
|
+
options.onNativePassthroughCancel?.();
|
|
655
|
+
},
|
|
656
|
+
rememberPassthroughResponse && responseEffects.plaintextV2AgentMessageToolNames.size === 0 ? rememberPassthroughResponseChecked : undefined,
|
|
657
|
+
options.onFirstOutput,
|
|
658
|
+
inspectionConsumerOptions,
|
|
659
|
+
);
|
|
660
|
+
} else {
|
|
661
|
+
consumeForResponseLogMetadata(
|
|
662
|
+
inspectBody,
|
|
663
|
+
logCtx,
|
|
664
|
+
turnAc.signal,
|
|
665
|
+
() => unregisterTurn(turnAc),
|
|
666
|
+
rememberPassthroughResponse && responseEffects.plaintextV2AgentMessageToolNames.size === 0 ? rememberPassthroughResponseChecked : undefined,
|
|
667
|
+
options.onFirstOutput,
|
|
668
|
+
inspectionConsumerOptions,
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
if (!headers.has("content-type")) headers.set("content-type", "text/event-stream");
|
|
672
|
+
// Windows was handled by the eager terminal-aware branch above. Remaining
|
|
673
|
+
// tee traffic can use the JS relay to close on a protocol terminal and to
|
|
674
|
+
// convert a mid-stream reset into a clean response.failed event.
|
|
675
|
+
const rewrittenBody = clientBlockRewrite !== undefined
|
|
676
|
+
? relaySseWithBlockRewrite(nativeBody, clientBlockRewrite, translatorBudget)
|
|
677
|
+
: nativeBody;
|
|
678
|
+
const clientBody = relaySseWithFailedTail(
|
|
679
|
+
rewrittenBody,
|
|
680
|
+
upstream,
|
|
681
|
+
reason => {
|
|
682
|
+
responseEffects.responseCompletionCancelled = true;
|
|
683
|
+
clientGone.abort(reason);
|
|
684
|
+
},
|
|
685
|
+
{ upstreamError: logCtx.upstreamError, terminalBoundary: codexSafetyBufferingOptions },
|
|
686
|
+
);
|
|
687
|
+
return markNativePassthroughSseResponse(new Response(clientBody, {
|
|
688
|
+
status: upstreamResponse.status,
|
|
689
|
+
headers,
|
|
690
|
+
}));
|
|
691
|
+
}
|
|
692
|
+
if (headers.get("content-type")?.toLowerCase().includes("application/json")) {
|
|
693
|
+
// Bounded whole-body read: a non-streaming upstream JSON body is fully materialized
|
|
694
|
+
// here (and again by the request-log finalizer and the WebSocket bridge's reframing),
|
|
695
|
+
// so an unbounded .text() would let a hostile or stuck upstream grow proxy memory
|
|
696
|
+
// without limit. This path is no longer rare — WebSocket turns for models whose
|
|
697
|
+
// streaming terminal event is unreliable are deliberately answered with bounded JSON.
|
|
698
|
+
// Oversize and stall deadlines both fail closed; a partial body is never parsed.
|
|
699
|
+
const bounded = await readBoundedResponseBody(upstreamResponse, UPSTREAM_JSON_BODY_READ_OPTIONS);
|
|
700
|
+
if (bounded.oversized) {
|
|
701
|
+
return formatErrorResponse(502, "upstream_error", "upstream JSON response exceeded the safe body limit");
|
|
702
|
+
}
|
|
703
|
+
if (bounded.truncated) {
|
|
704
|
+
return formatErrorResponse(502, "upstream_error", "upstream JSON response stalled before completing");
|
|
705
|
+
}
|
|
706
|
+
const text = bounded.text;
|
|
707
|
+
inspectResponseLogJson(logCtx, text);
|
|
708
|
+
let plaintextV2RestoreFailed = false;
|
|
709
|
+
let clientJson = (() => {
|
|
710
|
+
const restoredNamespace = restoreRoutedNamespaceCallsInJson(
|
|
711
|
+
scrubSelfNamedToolCallNamespaceInJson(
|
|
712
|
+
restoreMuseToolNamesInJson(
|
|
713
|
+
restoreImageGenCallsInJson(text, imageGenCallAliases),
|
|
714
|
+
responseEffects.routedMuseToolNameAliases,
|
|
715
|
+
),
|
|
716
|
+
selfNamedNamespaceScrubAuthorization,
|
|
717
|
+
),
|
|
718
|
+
responseEffects.routedNamespaceToolAliases,
|
|
719
|
+
);
|
|
720
|
+
const restoredAuthorizedBareNamespace = restoreRoutedNamespaceCallsInJson(
|
|
721
|
+
restoredNamespace,
|
|
722
|
+
authorizedBareNamespaceToolAliases,
|
|
723
|
+
);
|
|
724
|
+
const restored = restoreRoutedCustomCallsInJson(
|
|
725
|
+
restoredAuthorizedBareNamespace,
|
|
726
|
+
routedCustomToolNames,
|
|
727
|
+
routedCustomToolRepairNames,
|
|
728
|
+
declaredWireToolNames,
|
|
729
|
+
);
|
|
730
|
+
const restoredToolSearch = restoreRoutedToolSearchCallsInJson(
|
|
731
|
+
restored,
|
|
732
|
+
routedToolSearchNames,
|
|
733
|
+
);
|
|
734
|
+
const normalizedJson = normalizeFunctionCompletionJson(restoredToolSearch);
|
|
735
|
+
const plaintextRestore = restorePlaintextV2AgentMessageCallsInJsonResult(
|
|
736
|
+
normalizedJson, responseEffects.plaintextV2AgentMessageToolNames, responseEffects.plaintextV2AgentMessageAliasedToolNames,
|
|
737
|
+
);
|
|
738
|
+
plaintextV2RestoreFailed = plaintextRestore.overflowed;
|
|
739
|
+
const repaired = plaintextRestore.value;
|
|
740
|
+
const modelRewritten = parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId
|
|
741
|
+
? rewriteResponsesModelJson(repaired, parsed._responseModelId)
|
|
742
|
+
: repaired;
|
|
743
|
+
return modelRewritten;
|
|
744
|
+
})();
|
|
745
|
+
if (plaintextV2RestoreFailed) {
|
|
746
|
+
return formatErrorResponse(502, "upstream_error", PLAINTEXT_V2_AGENT_MESSAGE_RESTORE_OVERFLOW_MESSAGE);
|
|
747
|
+
}
|
|
748
|
+
// #1700: same fail-closed policy as the SSE relay above. Both the plain JSON answer and
|
|
749
|
+
// the reframed-SSE branch below are built from this body, so one check covers them. This
|
|
750
|
+
// runs BEFORE the continuation cache write below: a refused turn must not become state a
|
|
751
|
+
// later `previous_response_id` replay can expand from.
|
|
752
|
+
if (nativeExchange.undeclaredToolGuardActive) {
|
|
753
|
+
const undeclared = (() => {
|
|
754
|
+
try {
|
|
755
|
+
return undeclaredToolCallNameInResponse(
|
|
756
|
+
JSON.parse(clientJson),
|
|
757
|
+
declaredWireToolNames,
|
|
758
|
+
declaredNamelessClientCallTypes,
|
|
759
|
+
providerExecutedCallTypes,
|
|
760
|
+
declaredBareWireToolNames,
|
|
761
|
+
);
|
|
762
|
+
} catch {
|
|
763
|
+
return undefined;
|
|
764
|
+
}
|
|
765
|
+
})();
|
|
766
|
+
if (undeclared !== undefined) {
|
|
767
|
+
return formatErrorResponse(502, "upstream_error", undeclaredToolCallMessage(undeclared));
|
|
768
|
+
}
|
|
769
|
+
clientJson = normalizeDefaultNamespaceInJson(
|
|
770
|
+
clientJson,
|
|
771
|
+
declaredWireToolNames,
|
|
772
|
+
declaredBareWireToolNames,
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
commitReasoningReplayServingRoute(nativeExchange.request.headers);
|
|
776
|
+
try {
|
|
777
|
+
rememberPassthroughResponseChecked(
|
|
778
|
+
JSON.parse(text) as { id?: unknown; output?: unknown; status?: unknown; model?: unknown },
|
|
779
|
+
);
|
|
780
|
+
} catch { /* non-JSON despite content-type; recording is best-effort */ }
|
|
781
|
+
// #875: the transport-neutral reliability policy forced a bounded JSON
|
|
782
|
+
// upstream for a client that asked for SSE. Reframe the completed JSON
|
|
783
|
+
// as the canonical terminal SSE sequence (created → output_item.done →
|
|
784
|
+
// terminal → [DONE]) so Codex commits the turn instead of hanging on a
|
|
785
|
+
// stream that never closes. Non-streaming clients keep the plain JSON.
|
|
786
|
+
if (clientRequestedStream === true
|
|
787
|
+
&& options.inboundTransport !== "websocket"
|
|
788
|
+
&& providerModelResponsesUpstreamStreaming(route.providerName, route.provider, route.modelId) === false
|
|
789
|
+
&& route.provider.adapter === "openai-responses") {
|
|
790
|
+
let completed: Record<string, unknown> | undefined;
|
|
791
|
+
try {
|
|
792
|
+
const parsedCompleted = JSON.parse(clientJson) as unknown;
|
|
793
|
+
if (!parsedCompleted || typeof parsedCompleted !== "object" || Array.isArray(parsedCompleted)) {
|
|
794
|
+
throw new TypeError("bounded Responses JSON is not an object");
|
|
795
|
+
}
|
|
796
|
+
let candidate = parsedCompleted as Record<string, unknown>;
|
|
797
|
+
// The bounded-JSON answer bypasses the SSE relay, so it also bypasses
|
|
798
|
+
// the SSE item-id rewrite. Apply the same client-facing normalization
|
|
799
|
+
// here or this policy would silently disable id repair for the very
|
|
800
|
+
// providers that need it (raw record already happened above).
|
|
801
|
+
if (hasResponsesItemIdRepair(route.provider.responsesItemIdRepair)) {
|
|
802
|
+
candidate = repairResponsesJsonItemIds(candidate, route.provider.responsesItemIdRepair!, translatorBudget);
|
|
803
|
+
}
|
|
804
|
+
completed = candidate;
|
|
805
|
+
} catch {
|
|
806
|
+
// Non-JSON despite content-type: fall through to the plain relay.
|
|
807
|
+
}
|
|
808
|
+
if (completed) {
|
|
809
|
+
let stream: ReadableStream<Uint8Array>;
|
|
810
|
+
try {
|
|
811
|
+
stream = responsesJsonToSseStream(completed);
|
|
812
|
+
} catch (error) {
|
|
813
|
+
if (error instanceof RangeError) {
|
|
814
|
+
return formatErrorResponse(
|
|
815
|
+
502,
|
|
816
|
+
"upstream_error",
|
|
817
|
+
"upstream JSON response exceeded the synthesized SSE item limit",
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
throw error;
|
|
821
|
+
}
|
|
822
|
+
const sseHeaders = sanitizePassthroughHeaders(headers, codexSafetyBufferingOptions);
|
|
823
|
+
sseHeaders.set("content-type", "text/event-stream");
|
|
824
|
+
sseHeaders.set("cache-control", "no-store");
|
|
825
|
+
return new Response(stream, {
|
|
826
|
+
status: upstreamResponse.status,
|
|
827
|
+
statusText: upstreamResponse.statusText,
|
|
828
|
+
headers: sseHeaders,
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
// WS turns reframe this JSON into events in the bridge, which is the
|
|
833
|
+
// other relay-free path — normalize ids so both bounded-JSON paths agree.
|
|
834
|
+
const outboundJson = options.inboundTransport === "websocket"
|
|
835
|
+
&& providerModelResponsesUpstreamStreaming(route.providerName, route.provider, route.modelId) === false
|
|
836
|
+
&& hasResponsesItemIdRepair(route.provider.responsesItemIdRepair)
|
|
837
|
+
? (() => {
|
|
838
|
+
try {
|
|
839
|
+
return JSON.stringify(repairResponsesJsonItemIds(
|
|
840
|
+
JSON.parse(clientJson) as Record<string, unknown>,
|
|
841
|
+
route.provider.responsesItemIdRepair!,
|
|
842
|
+
translatorBudget,
|
|
843
|
+
));
|
|
844
|
+
} catch {
|
|
845
|
+
return clientJson;
|
|
846
|
+
}
|
|
847
|
+
})()
|
|
848
|
+
: clientJson;
|
|
849
|
+
return new Response(outboundJson, {
|
|
850
|
+
status: upstreamResponse.status,
|
|
851
|
+
statusText: upstreamResponse.statusText,
|
|
852
|
+
headers,
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
if (responseEffects.plaintextV2AgentMessageToolNames.size > 0) {
|
|
856
|
+
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already closed */ }
|
|
857
|
+
return formatErrorResponse(502, "upstream_error", "plaintext V2 agent-message response used an unsupported content type");
|
|
858
|
+
}
|
|
859
|
+
// An unclassified passthrough body is relayed directly and has no bounded completion observer;
|
|
860
|
+
// use the same non-error-status success boundary as SSE instead of retaining per-stream state.
|
|
861
|
+
commitReasoningReplayServingRoute(nativeExchange.request.headers);
|
|
862
|
+
const body = relayWithAbort(upstreamResponse.body, upstream);
|
|
863
|
+
const turnAc = new AbortController();
|
|
864
|
+
const tracked = body ? trackStreamLifetime(body, turnAc, undefined, options.turnAdmissionLease) : null;
|
|
865
|
+
return new Response(tracked, {
|
|
866
|
+
status: upstreamResponse.status,
|
|
867
|
+
headers,
|
|
868
|
+
});
|
|
869
|
+
}
|