@iislee/opencodex 2.31.0 → 2.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/anthropic.ts +104 -15
- package/src/adapters/base.ts +29 -1
- package/src/adapters/command-code.ts +42 -6
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +69 -18
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +290 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +2 -2
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -2
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +68 -14
- package/src/adapters/cursor/request-builder.ts +40 -10
- package/src/adapters/cursor/tool-definitions.ts +14 -4
- package/src/adapters/cursor/tool-result-normalize.ts +28 -5
- package/src/adapters/cursor/types.ts +20 -0
- package/src/adapters/cursor.ts +209 -38
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +173 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google.ts +136 -24
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- package/src/adapters/kiro-tools.ts +55 -16
- package/src/adapters/kiro.ts +182 -23
- package/src/adapters/openai-chat.ts +300 -242
- package/src/adapters/openai-responses.ts +353 -27
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +4 -3
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +17 -16
- package/src/bridge.ts +90 -37
- package/src/chat/inbound.ts +14 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/context-windows.ts +16 -9
- package/src/claude/outbound.ts +24 -11
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +116 -19
- package/src/cli/account-extended.ts +262 -29
- package/src/cli/account-main.ts +12 -12
- package/src/cli/account.ts +69 -15
- package/src/cli/agent.ts +8 -1
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-desktop.ts +31 -11
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +256 -28
- package/src/cli/doctor.ts +126 -14
- package/src/cli/help.ts +17 -6
- package/src/cli/index.ts +39 -9
- package/src/cli/init.ts +1 -0
- package/src/cli/inspect.ts +230 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +26 -10
- package/src/cli/observe.ts +31 -8
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +49 -10
- package/src/cli/runtime-api.ts +51 -7
- package/src/cli/status.ts +41 -1
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +34 -10
- package/src/cli/version-skew.ts +46 -0
- package/src/clients/config-export.ts +85 -3
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +21 -0
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +83 -7
- package/src/codex/auth-api.ts +50 -10
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +18 -0
- package/src/codex/catalog/effort.ts +18 -3
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +72 -27
- package/src/codex/catalog/provider-fetch.ts +187 -31
- package/src/codex/catalog/sync.ts +131 -5
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +27 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +355 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +63 -22
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +91 -10
- package/src/codex/routing.ts +9 -0
- package/src/codex/shim.ts +56 -3
- package/src/codex/subagent-model-fallback.ts +104 -25
- package/src/codex/transition-state.ts +116 -8
- package/src/combos/failover.ts +27 -0
- package/src/combos/types.ts +10 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +206 -824
- package/src/generated/compatibility-version.json +373 -213
- package/src/images/loop.ts +5 -4
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +160 -0
- package/src/integrations/ownership.ts +46 -2
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/state.ts +79 -7
- package/src/integrations/writer.ts +26 -2
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +36 -3
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/lib/process-control.ts +2 -1
- package/src/lib/service-secrets.ts +15 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/callback-server.ts +22 -2
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +65 -6
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/local-token-detect.ts +2 -0
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +14 -5
- package/src/oauth/types.ts +15 -0
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +28 -0
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/label.ts +34 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +65 -7
- package/src/providers/registry.ts +184 -69
- package/src/providers/slug-codec.ts +52 -0
- package/src/providers/xai-transport.ts +21 -0
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +157 -39
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +7 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/responses/schema.ts +5 -1
- package/src/responses/state.ts +94 -4
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/responses/turn-termination.ts +107 -0
- package/src/router.ts +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +27 -7
- package/src/server/chat-completions.ts +24 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/index.ts +179 -32
- package/src/server/lifecycle.ts +52 -1
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +45 -23
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +59 -12
- package/src/server/management/context.ts +10 -1
- package/src/server/management/logs-usage-routes.ts +30 -24
- package/src/server/management/model-routes.ts +274 -4
- package/src/server/management/model-rows.ts +4 -0
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/oauth-account-routes.ts +10 -4
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/provider-routes.ts +81 -11
- package/src/server/management/route-registry.ts +332 -0
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/management-api.ts +18 -16
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +29 -5
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +70 -16
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +37 -3
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +810 -208
- package/src/server/responses/empty-completion-guard.ts +63 -6
- package/src/server/responses/fetch-helpers.ts +17 -102
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +75 -2
- package/src/server/responses-custom-tool-repair.ts +87 -6
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/responses-undeclared-tool-guard.ts +241 -18
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/service.ts +42 -4
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- package/src/types/config.ts +66 -0
- package/src/types/provider.ts +69 -0
- package/src/types/request.ts +17 -4
- package/src/types/tools.ts +116 -11
- package/src/types.ts +3 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +63 -12
- package/src/usage/summary.ts +211 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-BF38heuV.js +0 -104
- package/gui/dist/assets/index-DMiI18Kv.css +0 -1
|
@@ -22,12 +22,15 @@ import {
|
|
|
22
22
|
durableReplayDestinationIdentity,
|
|
23
23
|
durableReplayCredentialIdentity,
|
|
24
24
|
reasoningReplayKeyCredentialIdentity,
|
|
25
|
+
reasoningReplayOpaqueBlobRejectionMemoized,
|
|
25
26
|
reasoningReplayOAuthCredentialIdentity,
|
|
26
27
|
reasoningReplayServingIdentityChanged,
|
|
28
|
+
rememberReasoningReplayOpaqueBlobRejection,
|
|
27
29
|
} from "../../responses/reasoning-replay-cache";
|
|
28
30
|
import { awaitThoughtSignatureDurability, thoughtSignatureReplaySalt } from "../../responses/thought-signature-replay";
|
|
29
31
|
import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
|
|
30
32
|
import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../../adapters/openai-responses";
|
|
33
|
+
import { XaiToolSchemaCompatibilityError } from "../../adapters/xai-tool-schema";
|
|
31
34
|
import {
|
|
32
35
|
copyPreviousResponseReplayProvenance,
|
|
33
36
|
expandPreviousResponseInput,
|
|
@@ -37,6 +40,10 @@ import {
|
|
|
37
40
|
previousResponseScopeMismatch,
|
|
38
41
|
rememberResponseState,
|
|
39
42
|
} from "../../responses/state";
|
|
43
|
+
import {
|
|
44
|
+
bindTurnTerminationScope,
|
|
45
|
+
rememberDeliveredFinalAnswer,
|
|
46
|
+
} from "../../responses/turn-termination";
|
|
40
47
|
import {
|
|
41
48
|
isValidProviderContinuationOwner,
|
|
42
49
|
mergeProviderContinuationPayload,
|
|
@@ -69,6 +76,13 @@ import {
|
|
|
69
76
|
targetKey,
|
|
70
77
|
} from "../../combos";
|
|
71
78
|
import { isInjectionDebugEnabled } from "../../lib/debug-settings";
|
|
79
|
+
import {
|
|
80
|
+
CYBER_POLICY_ERROR_CODE,
|
|
81
|
+
CYBER_POLICY_FALLBACK_MESSAGE,
|
|
82
|
+
adapterFailureFromMessage,
|
|
83
|
+
isCyberPolicyCode,
|
|
84
|
+
isCyberPolicyMessage,
|
|
85
|
+
} from "../../lib/errors";
|
|
72
86
|
import { injectionDebugLog } from "../../lib/injection-debug-log";
|
|
73
87
|
import { resolveClientRetryAfter } from "../../lib/retry-after";
|
|
74
88
|
import { enrichOpenCodeZenRateLimitMessage } from "../../providers/opencode-zen-rate-limit";
|
|
@@ -105,6 +119,14 @@ import {
|
|
|
105
119
|
resolveAnthropicAccountForSession,
|
|
106
120
|
rotateAnthropicAccountOn429,
|
|
107
121
|
} from "../../oauth/anthropic-routing";
|
|
122
|
+
import { stampOAuthAccountLabel } from "../../providers/label";
|
|
123
|
+
import {
|
|
124
|
+
failoverAccountSnapshot,
|
|
125
|
+
GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
|
|
126
|
+
isGenericFailoverProvider,
|
|
127
|
+
isGenericOAuthFailoverEnabled,
|
|
128
|
+
rotateGenericOAuthAccountOn429,
|
|
129
|
+
} from "../../oauth/generic-account-failover";
|
|
108
130
|
import { buildWebSearchTool, planWebSearch, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search";
|
|
109
131
|
import { buildImageTool, buildVideoTool, planImageBridge, planVideoBridge, runWithImageBridge, clampImageMaxRounds, IMAGE_GEN_TOOL_NAME, VIDEO_GEN_TOOL_NAME } from "../../images";
|
|
110
132
|
import { describeImagesInPlace, isModelTextOnly, planVisionSidecar, resolveOpenAiVisionModel, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision";
|
|
@@ -113,16 +135,12 @@ import {
|
|
|
113
135
|
applyCodexAuthContextToProvider,
|
|
114
136
|
codexPoolAffinityKey,
|
|
115
137
|
CodexAccountCooldownError,
|
|
116
|
-
codexMainProfileDrainingResponse,
|
|
117
|
-
cooldownErrorResponse,
|
|
118
138
|
CodexAuthContextError,
|
|
119
|
-
CodexDirectAuthenticationError,
|
|
120
139
|
CodexMainProfileDrainingError,
|
|
121
140
|
CodexPoolAuthenticationError,
|
|
122
141
|
CodexThreadAffinityExpiredError,
|
|
123
142
|
headersForCodexAuthContext,
|
|
124
143
|
materializeCodexUpstreamAuth,
|
|
125
|
-
CodexMainSubstitutionUnavailableError,
|
|
126
144
|
isCodexAuthContextUsable,
|
|
127
145
|
resolveCodexAuthContext,
|
|
128
146
|
codexProbeLeaseId,
|
|
@@ -137,6 +155,7 @@ import {
|
|
|
137
155
|
resolveCodexModelEntitlements,
|
|
138
156
|
} from "../../codex/model-entitlements";
|
|
139
157
|
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
|
|
158
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
|
|
140
159
|
import { captureCodexAffinityDiagnostic } from "../../codex/affinity-debug";
|
|
141
160
|
import {
|
|
142
161
|
computeQuotaCooldown,
|
|
@@ -185,7 +204,7 @@ import {
|
|
|
185
204
|
providerModelResponsesUpstreamStreaming,
|
|
186
205
|
type InboundWire,
|
|
187
206
|
} from "../../providers/registry";
|
|
188
|
-
import type { AdapterRequest } from "../../adapters/base";
|
|
207
|
+
import type { AdapterRequest, ProviderAdapter } from "../../adapters/base";
|
|
189
208
|
import {
|
|
190
209
|
hasKeyPoolFailover,
|
|
191
210
|
rateLimitRetryDelayMs,
|
|
@@ -193,7 +212,7 @@ import {
|
|
|
193
212
|
rotateProviderTransportOn429,
|
|
194
213
|
} from "../../providers/key-failover";
|
|
195
214
|
import { shouldAttemptImageTierRetry } from "../image-retry";
|
|
196
|
-
import { resolveProviderTransport } from "../../providers/xai-transport";
|
|
215
|
+
import { isXaiResponsesDestination, resolveProviderTransport } from "../../providers/xai-transport";
|
|
197
216
|
import type { WsData } from "../ws-bridge";
|
|
198
217
|
import { codexAccountSelectionForTurn, registerTurn, trackStreamLifetime, unregisterTurn } from "../lifecycle";
|
|
199
218
|
import { redactSecretString, sanitizeLogMetadataString } from "../../lib/redact";
|
|
@@ -205,6 +224,10 @@ import {
|
|
|
205
224
|
applySubagentModelFallback,
|
|
206
225
|
maybePrimeSubagentQuota,
|
|
207
226
|
recordSubagentQuotaFailureForThreadSpawn,
|
|
227
|
+
resolveSubagentFallbackChain,
|
|
228
|
+
subagentFallbackNeedsModelEntitlements,
|
|
229
|
+
type SubagentModelEligibleAccountIds,
|
|
230
|
+
type SubagentPoolAccountPreview,
|
|
208
231
|
} from "../../codex/subagent-model-fallback";
|
|
209
232
|
import { isNativeMainTrafficBlocked } from "../../codex/native-profile-startup";
|
|
210
233
|
import {
|
|
@@ -225,6 +248,7 @@ import {
|
|
|
225
248
|
import {
|
|
226
249
|
conversationIdFromResponsesRequest,
|
|
227
250
|
normalizeLogConversationId,
|
|
251
|
+
reasoningReplayConversationIdFromResponsesRequest,
|
|
228
252
|
sessionIdHeaderFromRequest,
|
|
229
253
|
} from "../request-log-conversation";
|
|
230
254
|
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
@@ -271,6 +295,7 @@ import { createResponsesModelPayloadRewrite, rewriteResponsesModelJson } from ".
|
|
|
271
295
|
import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
|
|
272
296
|
|
|
273
297
|
import { buildToolBridgeMaps, collabSurface, injectDeveloperMessage, multiAgentGuidanceText } from "./collaboration";
|
|
298
|
+
import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
|
|
274
299
|
import { hasUnreadableEncryptedAgentTask, looksLikeBackendCiphertext, sanitizeEncryptedContentInPlace } from "./encrypted-payload";
|
|
275
300
|
import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel } from "./fetch-helpers";
|
|
276
301
|
import { classifyTransportFailureKind, transportErrorCode } from "../../lib/upstream-reachability";
|
|
@@ -295,7 +320,7 @@ import {
|
|
|
295
320
|
payloadRewriteAsBlockRewrite,
|
|
296
321
|
relaySseWithBlockRewrite,
|
|
297
322
|
} from "../sse-payload-rewrite";
|
|
298
|
-
import { restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
|
|
323
|
+
import { restoreRoutedCustomCalls, restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
|
|
299
324
|
import { createRoutedCustomToolRestoreBlockRewrite } from "../responses-custom-tool-repair";
|
|
300
325
|
import { restoreRoutedToolSearchCallsInJson } from "../../responses/tool-search-compat";
|
|
301
326
|
import { createRoutedToolSearchRestoreBlockRewrite } from "../responses-tool-search-repair";
|
|
@@ -306,19 +331,31 @@ import {
|
|
|
306
331
|
type RoutedNamespaceToolAliases,
|
|
307
332
|
} from "../../responses/namespace-tool-compat";
|
|
308
333
|
import {
|
|
334
|
+
collectDeclaredNamelessClientCallTypes,
|
|
309
335
|
collectDeclaredWireToolNames,
|
|
336
|
+
collectProviderExecutedCallTypes,
|
|
310
337
|
createUndeclaredToolCallGuardBlockRewrite,
|
|
338
|
+
currentTurnWireToolCatalogBody,
|
|
339
|
+
hasExplicitWireToolCatalog,
|
|
311
340
|
undeclaredToolCallMessage,
|
|
312
341
|
undeclaredToolCallName,
|
|
313
342
|
undeclaredToolCallNameInResponse,
|
|
343
|
+
type ProviderExecutedCallType,
|
|
314
344
|
} from "../responses-undeclared-tool-guard";
|
|
315
345
|
import { createGithubCopilotResponsesBlockRewrite } from "../github-copilot-responses-repair";
|
|
316
346
|
import { responsesJsonToSseStream } from "../responses-json-events";
|
|
317
347
|
import { guardTerminalEventStream } from "./terminal-guard";
|
|
318
348
|
import {
|
|
319
349
|
emptyCompletionRetryEnabled,
|
|
350
|
+
observeEmptyCompletion,
|
|
320
351
|
guardEmptyCompletionEventStream,
|
|
321
352
|
} from "./empty-completion-guard";
|
|
353
|
+
import { preflightComboStreamResponse } from "./combo-stream-preflight";
|
|
354
|
+
|
|
355
|
+
// runTurn adapters own an event queue and perform their combo preflight before
|
|
356
|
+
// bridging. A second byte-stream reader would reinterpret that transport's
|
|
357
|
+
// already-committed event boundary and can replay custom adapter work.
|
|
358
|
+
const runTurnAdapterSseResponses = new WeakSet<Response>();
|
|
322
359
|
|
|
323
360
|
/**
|
|
324
361
|
* Adapters whose continuation state must survive Codex's store:false requests.
|
|
@@ -515,6 +552,9 @@ function bindRouteReasoningReplayScope(args: {
|
|
|
515
552
|
if (reasoningReplayServingIdentityChanged(parsed._reasoningReplayScope)) {
|
|
516
553
|
parsed._stripReasoningEncryptedContent = true;
|
|
517
554
|
}
|
|
555
|
+
if (reasoningReplayOpaqueBlobRejectionMemoized(parsed._reasoningReplayScope)) {
|
|
556
|
+
parsed._stripReasoningEncryptedContent = true;
|
|
557
|
+
}
|
|
518
558
|
bindProviderContinuationForRoute(parsed, continuationOwner);
|
|
519
559
|
}
|
|
520
560
|
|
|
@@ -628,6 +668,68 @@ async function opaqueBlobRejectionBodyForRecovery(
|
|
|
628
668
|
}
|
|
629
669
|
}
|
|
630
670
|
|
|
671
|
+
/**
|
|
672
|
+
* Materialize an upstream error body only when the bounded reader observed a complete,
|
|
673
|
+
* display-safe payload. Partial timeout and over-limit prefixes are attacker-controlled,
|
|
674
|
+
* so callers keep their existing status-only fallback instead.
|
|
675
|
+
*/
|
|
676
|
+
export async function readDisplaySafeErrorText(
|
|
677
|
+
response: Response,
|
|
678
|
+
signal: AbortSignal,
|
|
679
|
+
fallback: string,
|
|
680
|
+
): Promise<string> {
|
|
681
|
+
try {
|
|
682
|
+
const body = await readBoundedResponseBody(response, { signal });
|
|
683
|
+
return body.displaySafe ? body.text : fallback;
|
|
684
|
+
} catch {
|
|
685
|
+
// Preserve the former Response.text().catch(fallback) contract. Request-abort
|
|
686
|
+
// classification remains owned by the surrounding response pipeline.
|
|
687
|
+
return fallback;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
interface NormalizedUpstreamErrorText {
|
|
692
|
+
safeText: string;
|
|
693
|
+
message?: string;
|
|
694
|
+
type?: string;
|
|
695
|
+
code?: string;
|
|
696
|
+
cyberPolicy: boolean;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Extract the structured provider error envelope without making `error.type` authoritative.
|
|
701
|
+
* Policy identity comes from the dedicated code (or the legacy message fallback); a credible
|
|
702
|
+
* upstream type is only carried through so callers do not erase provider diagnostics.
|
|
703
|
+
*/
|
|
704
|
+
function normalizeUpstreamErrorText(text: string, fallback: string): NormalizedUpstreamErrorText {
|
|
705
|
+
const safeText = redactSecretString(text).slice(0, 500).trim() || fallback;
|
|
706
|
+
let message: string | undefined;
|
|
707
|
+
let type: string | undefined;
|
|
708
|
+
let code: string | undefined;
|
|
709
|
+
try {
|
|
710
|
+
const parsed = JSON.parse(text) as Record<string, unknown>;
|
|
711
|
+
const response = parsed.response && typeof parsed.response === "object" && !Array.isArray(parsed.response)
|
|
712
|
+
? parsed.response as Record<string, unknown>
|
|
713
|
+
: undefined;
|
|
714
|
+
const candidates = [parsed.error, response?.error, response?.last_error, parsed.last_error, parsed];
|
|
715
|
+
const source = candidates.find((candidate): candidate is Record<string, unknown> => {
|
|
716
|
+
if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate)) return false;
|
|
717
|
+
const record = candidate as Record<string, unknown>;
|
|
718
|
+
return [record.message, record.type, record.code].some(value => typeof value === "string");
|
|
719
|
+
});
|
|
720
|
+
if (!source) return { safeText, cyberPolicy: isCyberPolicyMessage(safeText) };
|
|
721
|
+
if (typeof source.message === "string" && source.message.trim()) {
|
|
722
|
+
message = redactSecretString(source.message.trim()).slice(0, 500);
|
|
723
|
+
}
|
|
724
|
+
if (typeof source.type === "string" && source.type.trim()) type = source.type.trim();
|
|
725
|
+
if (typeof source.code === "string" && source.code.trim()) code = source.code.trim();
|
|
726
|
+
} catch {
|
|
727
|
+
/* non-JSON upstream body — retain the bounded display-safe text */
|
|
728
|
+
}
|
|
729
|
+
const cyberPolicy = isCyberPolicyCode(code) || isCyberPolicyMessage(message ?? safeText);
|
|
730
|
+
return { safeText, message, type, code, cyberPolicy };
|
|
731
|
+
}
|
|
732
|
+
|
|
631
733
|
function prepareOpaqueBlobRecovery(parsed: OcxParsedRequest): void {
|
|
632
734
|
parsed._stripReasoningEncryptedContent = true;
|
|
633
735
|
}
|
|
@@ -668,9 +770,20 @@ async function attemptOpaqueBlobRecovery(
|
|
|
668
770
|
}
|
|
669
771
|
|
|
670
772
|
args.guard.attempted = true;
|
|
773
|
+
const rejectedScope = args.parsed._reasoningReplayScope
|
|
774
|
+
? {
|
|
775
|
+
clientThreadId: args.parsed._reasoningReplayScope.clientThreadId,
|
|
776
|
+
...(args.parsed._reasoningReplayScope.current
|
|
777
|
+
? { current: { ...args.parsed._reasoningReplayScope.current } }
|
|
778
|
+
: {}),
|
|
779
|
+
}
|
|
780
|
+
: undefined;
|
|
671
781
|
prepareOpaqueBlobRecovery(args.parsed);
|
|
672
782
|
try { void args.response.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
673
783
|
const result = await rebuild("opaque-blob-rejection");
|
|
784
|
+
if (!("failed" in result) && result.ok) {
|
|
785
|
+
rememberReasoningReplayOpaqueBlobRejection(rejectedScope);
|
|
786
|
+
}
|
|
674
787
|
return "failed" in result
|
|
675
788
|
? { kind: "failed", response: result.failed }
|
|
676
789
|
: { kind: "recovered", response: result };
|
|
@@ -828,11 +941,15 @@ export function codexAccountGatedCanonicalWireModel(modelId: string): string | u
|
|
|
828
941
|
return undefined;
|
|
829
942
|
}
|
|
830
943
|
|
|
831
|
-
function applyCodexAccountGatedWireNormalization(parsed: OcxParsedRequest, route: RouteResult): void {
|
|
944
|
+
function applyCodexAccountGatedWireNormalization(parsed: OcxParsedRequest, route: RouteResult, logCtx?: RequestLogContext): void {
|
|
832
945
|
if (!isCanonicalOpenAiForwardProvider(route.provider)) return;
|
|
833
946
|
const wireModel = codexAccountGatedCanonicalWireModel(route.modelId);
|
|
834
947
|
if (!wireModel) return;
|
|
835
948
|
|
|
949
|
+
if (logCtx) {
|
|
950
|
+
logCtx.preserveResolvedModelFromRoute = true;
|
|
951
|
+
delete logCtx.resolvedModel;
|
|
952
|
+
}
|
|
836
953
|
parsed.modelId = wireModel;
|
|
837
954
|
if (!parsed._rawBody || typeof parsed._rawBody !== "object") return;
|
|
838
955
|
const raw = parsed._rawBody as Record<string, unknown>;
|
|
@@ -1184,6 +1301,8 @@ export interface HandleResponsesOptions {
|
|
|
1184
1301
|
/** One-shot TTFT callback: first non-empty model output observed (WP4). */
|
|
1185
1302
|
onFirstOutput?: () => void;
|
|
1186
1303
|
onCodexAuthContextResolved?: (context: CodexAuthContext | undefined) => void;
|
|
1304
|
+
/** Internal deterministic seam for account-gated native fallback tests. */
|
|
1305
|
+
resolveCodexModelEntitlements?: typeof resolveCodexModelEntitlements;
|
|
1187
1306
|
recordTerminalOutcomes?: boolean;
|
|
1188
1307
|
setTerminalOutcomeRecorder?: (recorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined) => void;
|
|
1189
1308
|
onNativePassthroughTerminal?: (status: ResponsesTerminalStatus) => void;
|
|
@@ -1267,27 +1386,30 @@ export async function consumeComboFailure(
|
|
|
1267
1386
|
let classificationText = fallback;
|
|
1268
1387
|
let usage: OcxUsage | undefined;
|
|
1269
1388
|
let upstreamCode: string | undefined;
|
|
1389
|
+
let upstreamMessage: string | undefined;
|
|
1390
|
+
let upstreamType: string | undefined;
|
|
1270
1391
|
try {
|
|
1271
1392
|
const body = await readBoundedResponseBody(response, { signal });
|
|
1272
1393
|
usage = usageFromComboFailureText(body.text);
|
|
1273
1394
|
if (body.displaySafe) {
|
|
1274
|
-
const
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
if (typeof nested === "string" && nested.length > 0) upstreamCode = nested;
|
|
1280
|
-
} catch {
|
|
1281
|
-
/* non-JSON upstream body — message-only classification */
|
|
1282
|
-
}
|
|
1395
|
+
const normalized = normalizeUpstreamErrorText(body.text, fallback);
|
|
1396
|
+
classificationText = normalized.safeText;
|
|
1397
|
+
upstreamCode = normalized.code;
|
|
1398
|
+
upstreamMessage = normalized.message;
|
|
1399
|
+
upstreamType = normalized.type;
|
|
1283
1400
|
}
|
|
1284
1401
|
} catch (error) {
|
|
1285
1402
|
if (signal?.aborted) throw error;
|
|
1286
1403
|
classificationText = fallback;
|
|
1287
1404
|
}
|
|
1288
|
-
const
|
|
1289
|
-
|
|
1290
|
-
|
|
1405
|
+
const cyberFailure = isCyberPolicyCode(upstreamCode) || isCyberPolicyMessage(classificationText);
|
|
1406
|
+
const normalizedUpstreamCode = cyberFailure ? CYBER_POLICY_ERROR_CODE : upstreamCode;
|
|
1407
|
+
const message = cyberFailure
|
|
1408
|
+
? upstreamMessage
|
|
1409
|
+
?? (isCyberPolicyCode(upstreamCode) ? CYBER_POLICY_FALLBACK_MESSAGE : classificationText)
|
|
1410
|
+
: classificationText === fallback
|
|
1411
|
+
? fallback
|
|
1412
|
+
: `${fallback}: ${classificationText}`;
|
|
1291
1413
|
const upstreamRetryAfter = response.headers.get("retry-after");
|
|
1292
1414
|
// Client response may get the synthetic "2" fallback; cooldown metadata must not —
|
|
1293
1415
|
// otherwise coolComboTarget treats it as a 2s cooldown instead of the 60s default.
|
|
@@ -1305,13 +1427,18 @@ export async function consumeComboFailure(
|
|
|
1305
1427
|
includeDefault: false,
|
|
1306
1428
|
});
|
|
1307
1429
|
return {
|
|
1308
|
-
response: formatErrorResponse(
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1430
|
+
response: formatErrorResponse(
|
|
1431
|
+
response.status,
|
|
1432
|
+
cyberFailure ? (upstreamType ?? CYBER_POLICY_ERROR_CODE) : "upstream_error",
|
|
1433
|
+
message,
|
|
1434
|
+
{
|
|
1435
|
+
...(normalizedUpstreamCode !== undefined ? { code: normalizedUpstreamCode } : {}),
|
|
1436
|
+
...(clientRetryAfter !== undefined ? { retryAfter: clientRetryAfter } : {}),
|
|
1437
|
+
},
|
|
1438
|
+
),
|
|
1312
1439
|
classificationText,
|
|
1313
|
-
...(
|
|
1314
|
-
...(cooldownRetryAfter !== undefined ? { retryAfter: cooldownRetryAfter } : {}),
|
|
1440
|
+
...(normalizedUpstreamCode !== undefined ? { upstreamCode: normalizedUpstreamCode } : {}),
|
|
1441
|
+
...(!cyberFailure && cooldownRetryAfter !== undefined ? { retryAfter: cooldownRetryAfter } : {}),
|
|
1315
1442
|
...(usage ? { usage } : {}),
|
|
1316
1443
|
};
|
|
1317
1444
|
}
|
|
@@ -1475,6 +1602,7 @@ async function resolveResponsesCodexAuth(
|
|
|
1475
1602
|
modelId: route.modelId,
|
|
1476
1603
|
substituteMainCredentialForDirect: substituteMainCredential,
|
|
1477
1604
|
beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
|
|
1605
|
+
resolveCodexModelEntitlements: options.resolveCodexModelEntitlements,
|
|
1478
1606
|
});
|
|
1479
1607
|
options.onCodexAuthContextResolved?.(authCtx);
|
|
1480
1608
|
} else {
|
|
@@ -1495,48 +1623,43 @@ async function resolveResponsesCodexAuth(
|
|
|
1495
1623
|
substituteMainCredential,
|
|
1496
1624
|
};
|
|
1497
1625
|
} catch (err) {
|
|
1498
|
-
if (err instanceof CodexAccountCooldownError) {
|
|
1499
|
-
return { ok: false, response: cooldownErrorResponse(err, Date.now(), route.codexAccountNamespace) };
|
|
1500
|
-
}
|
|
1501
|
-
if (err instanceof CodexMainProfileDrainingError) {
|
|
1502
|
-
return { ok: false, response: codexMainProfileDrainingResponse() };
|
|
1503
|
-
}
|
|
1504
|
-
if (err instanceof CodexThreadAffinityExpiredError) {
|
|
1505
|
-
return {
|
|
1506
|
-
ok: false,
|
|
1507
|
-
response: formatErrorResponse(409, "invalid_request_error", "Codex thread account affinity expired; start a new session"),
|
|
1508
|
-
};
|
|
1509
|
-
}
|
|
1510
1626
|
if (err instanceof CodexAuthContextError) {
|
|
1511
1627
|
const safeAccountLabel = route.codexAccountNamespace
|
|
1512
1628
|
? `${route.providerName}-${route.codexAccountNamespace}`
|
|
1513
1629
|
: formatCodexProviderForLog(route.providerName, err.accountId, config);
|
|
1514
1630
|
console.error(`[codex-auth] Pool account ${safeAccountLabel} token failed; reauthentication required`);
|
|
1515
|
-
return {
|
|
1516
|
-
ok: false,
|
|
1517
|
-
response: formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"),
|
|
1518
|
-
};
|
|
1519
|
-
}
|
|
1520
|
-
if (err instanceof CodexPoolAuthenticationError) {
|
|
1521
|
-
return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
|
|
1522
|
-
}
|
|
1523
|
-
if (err instanceof CodexDirectAuthenticationError) {
|
|
1524
|
-
return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
|
|
1525
1631
|
}
|
|
1526
1632
|
if (err instanceof ForwardAdmissionCredentialError) {
|
|
1527
1633
|
return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
|
|
1528
1634
|
}
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
};
|
|
1535
|
-
}
|
|
1635
|
+
const response = mapCodexAuthContextErrorToResponse(err, {
|
|
1636
|
+
accountSelector: route.codexAccountNamespace,
|
|
1637
|
+
now: Date.now(),
|
|
1638
|
+
});
|
|
1639
|
+
if (response) return { ok: false, response };
|
|
1536
1640
|
throw err;
|
|
1537
1641
|
}
|
|
1538
1642
|
}
|
|
1539
1643
|
|
|
1644
|
+
async function resolveSubagentFallbackModelEligibility(args: {
|
|
1645
|
+
config: OcxConfig;
|
|
1646
|
+
fallbackChain: readonly string[] | null;
|
|
1647
|
+
nativeMainReadsForbidden: boolean;
|
|
1648
|
+
resolver: typeof resolveCodexModelEntitlements;
|
|
1649
|
+
}): Promise<SubagentModelEligibleAccountIds | undefined> {
|
|
1650
|
+
if (!subagentFallbackNeedsModelEntitlements(args.fallbackChain, args.config)) return undefined;
|
|
1651
|
+
const excludeAccountIds = args.nativeMainReadsForbidden
|
|
1652
|
+
? new Set([MAIN_CODEX_ACCOUNT_ID])
|
|
1653
|
+
: undefined;
|
|
1654
|
+
const snapshot = await args.resolver(args.config, { excludeAccountIds });
|
|
1655
|
+
return (modelId) => {
|
|
1656
|
+
const entitledAccountIds = entitledCodexAccountIdsForModel(snapshot, modelId);
|
|
1657
|
+
return entitledAccountIds
|
|
1658
|
+
? new Set([...entitledAccountIds].filter(accountId => !excludeAccountIds?.has(accountId)))
|
|
1659
|
+
: undefined;
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1540
1663
|
/**
|
|
1541
1664
|
* Apply every route-dependent request mutation against the final selected route.
|
|
1542
1665
|
* Must run only after subagent fallback has settled the model/provider.
|
|
@@ -1581,6 +1704,7 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
1581
1704
|
logCtx.provider = route.providerName;
|
|
1582
1705
|
logCtx.providerAdapter = route.provider.adapter;
|
|
1583
1706
|
logCtx.routeDecision = route.routeDecision;
|
|
1707
|
+
if (route.routeReason === "model-alias" || route.modelId !== responseModelId && responseModelId.includes("/")) logCtx.requestedAlias = responseModelId;
|
|
1584
1708
|
|
|
1585
1709
|
if (responsesUpstreamStreaming === false && route.provider.adapter === "openai-responses") {
|
|
1586
1710
|
parsed.stream = false;
|
|
@@ -1623,7 +1747,15 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
1623
1747
|
);
|
|
1624
1748
|
const modelServiceTierSupport = serviceTierSupportFromPolicy(fastPolicy);
|
|
1625
1749
|
const callerTier = parsed.options.serviceTier;
|
|
1626
|
-
|
|
1750
|
+
// The ChatGPT-internal Codex backend echoes `service_tier: "default"` even on turns it
|
|
1751
|
+
// scheduled as priority, so its echo cannot confirm OR deny Fast. Believing it reported every
|
|
1752
|
+
// Fast request as `response-declined` (#2558). The public API's echo stays authoritative.
|
|
1753
|
+
parsed.options.tierObservation = tierObservationContext(
|
|
1754
|
+
fastPolicy,
|
|
1755
|
+
config.fastMode,
|
|
1756
|
+
callerTier,
|
|
1757
|
+
isCanonicalOpenAiForwardProvider(route.provider) ? false : undefined,
|
|
1758
|
+
);
|
|
1627
1759
|
parsed.options.tierDecision = decideTier(fastPolicy, config.fastMode, callerTier);
|
|
1628
1760
|
parsed.options.serviceTier = tierValueAfterDecision(parsed.options.tierDecision, callerTier);
|
|
1629
1761
|
if (fastPolicy.capability === true && fastPolicy.fastWire === null) {
|
|
@@ -1900,6 +2032,31 @@ export async function handleComboResponses(
|
|
|
1900
2032
|
return clientCancelledResponse();
|
|
1901
2033
|
}
|
|
1902
2034
|
|
|
2035
|
+
if (response.ok && !runTurnAdapterSseResponses.has(response)) {
|
|
2036
|
+
const nativePassthrough = isNativePassthroughSseResponse(response);
|
|
2037
|
+
const eagerRelay = isEagerRelaySseResponse(response);
|
|
2038
|
+
let preflight;
|
|
2039
|
+
try {
|
|
2040
|
+
preflight = await preflightComboStreamResponse(response, childLog);
|
|
2041
|
+
} catch (error) {
|
|
2042
|
+
callbackGate.discard();
|
|
2043
|
+
if (options.abortSignal?.aborted) {
|
|
2044
|
+
retainCancelledAttempt();
|
|
2045
|
+
return clientCancelledResponse();
|
|
2046
|
+
}
|
|
2047
|
+
throw error;
|
|
2048
|
+
}
|
|
2049
|
+
if (preflight.kind === "failed") {
|
|
2050
|
+
callbackGate.discard();
|
|
2051
|
+
terminalRecorder?.("failed", preflight.response.status);
|
|
2052
|
+
response = preflight.response;
|
|
2053
|
+
} else {
|
|
2054
|
+
response = preflight.response;
|
|
2055
|
+
if (nativePassthrough) markNativePassthroughSseResponse(response);
|
|
2056
|
+
if (eagerRelay) markEagerRelaySseResponse(response);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
|
|
1903
2060
|
if (response.ok) {
|
|
1904
2061
|
sealRequestAttemptIdentity(
|
|
1905
2062
|
attempt,
|
|
@@ -1955,7 +2112,7 @@ export async function handleComboResponses(
|
|
|
1955
2112
|
);
|
|
1956
2113
|
finishRequestAttempt(
|
|
1957
2114
|
attempt,
|
|
1958
|
-
response.status,
|
|
2115
|
+
failure.response.status,
|
|
1959
2116
|
Date.now() - started,
|
|
1960
2117
|
failure.usage,
|
|
1961
2118
|
);
|
|
@@ -1969,7 +2126,7 @@ export async function handleComboResponses(
|
|
|
1969
2126
|
return lastFailure;
|
|
1970
2127
|
}
|
|
1971
2128
|
console.warn(
|
|
1972
|
-
`[combo] ${comboId}: ${targetKey(pick.target)} failed with ${response.status} after ${Date.now() - started}ms`,
|
|
2129
|
+
`[combo] ${comboId}: ${targetKey(pick.target)} failed with ${failure.response.status} after ${Date.now() - started}ms`,
|
|
1973
2130
|
);
|
|
1974
2131
|
const nextPick = advanceComboAfterFailure(config, pick, {
|
|
1975
2132
|
retryAfter: failure.retryAfter,
|
|
@@ -2138,6 +2295,7 @@ async function handleResponsesInner(
|
|
|
2138
2295
|
(body as { input?: unknown } | undefined)?.input,
|
|
2139
2296
|
);
|
|
2140
2297
|
const inboundClientThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
|
|
2298
|
+
const cursorClientThreadId = codexPoolAffinityKey(req.headers);
|
|
2141
2299
|
const originalBody = body;
|
|
2142
2300
|
if (options.comboReplaySnapshot) {
|
|
2143
2301
|
copyPreviousResponseReplayProvenance(options.comboReplaySnapshot.sourceBody, body);
|
|
@@ -2185,7 +2343,6 @@ async function handleResponsesInner(
|
|
|
2185
2343
|
if (providerContinuationCandidate) parsed._providerContinuationCandidate = providerContinuationCandidate;
|
|
2186
2344
|
if (inboundClientThreadId) {
|
|
2187
2345
|
parsed._clientThreadId = inboundClientThreadId;
|
|
2188
|
-
parsed._reasoningReplayScope = { clientThreadId: inboundClientThreadId };
|
|
2189
2346
|
} else if (
|
|
2190
2347
|
options.inboundWire === "anthropic"
|
|
2191
2348
|
&& options.promptCacheKeyIsSharedCohort !== true
|
|
@@ -2208,6 +2365,7 @@ async function handleResponsesInner(
|
|
|
2208
2365
|
parsed._reasoningReplayScope = { clientThreadId: normalizedCacheKey };
|
|
2209
2366
|
}
|
|
2210
2367
|
}
|
|
2368
|
+
if (cursorClientThreadId) parsed._cursorClientThreadId = cursorClientThreadId;
|
|
2211
2369
|
} catch (err) {
|
|
2212
2370
|
if (isTranslatorBudgetExceededError(err)) {
|
|
2213
2371
|
return formatErrorResponse(413, "request_too_large", "request translation buffer exceeded the safe limit", {
|
|
@@ -2221,15 +2379,35 @@ async function handleResponsesInner(
|
|
|
2221
2379
|
...(force ? { force: true } : {}),
|
|
2222
2380
|
...(parsed._clientThreadId ? { clientThreadId: parsed._clientThreadId } : {}),
|
|
2223
2381
|
});
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2382
|
+
const resolvedConversationId = conversationIdFromResponsesRequest({
|
|
2383
|
+
clientThreadId: parsed._clientThreadId,
|
|
2384
|
+
sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
|
|
2385
|
+
threadIdHeader: req.headers.get("thread-id"),
|
|
2386
|
+
cursorConversationId: parsed._cursorConversationId,
|
|
2387
|
+
});
|
|
2388
|
+
bindTurnTerminationScope(parsed, resolvedConversationId);
|
|
2389
|
+
const rememberKiroDeliveredFinalAnswer = (adapterName: string, response: unknown): void => {
|
|
2390
|
+
if (adapterName === "kiro") rememberDeliveredFinalAnswer(parsed, response);
|
|
2391
|
+
};
|
|
2392
|
+
// _clientThreadId remains the routing/continuation identity supplied by Codex. Replay state uses
|
|
2393
|
+
// a dedicated raw conversation namespace so mixed headers that carry the same identity still
|
|
2394
|
+
// match, and a shared/synthetic session_id cannot coalesce distinct thread/Cursor conversations.
|
|
2395
|
+
// Keep an Anthropic prompt_cache_key scope already bound above (#1735/#1926).
|
|
2396
|
+
if (!parsed._reasoningReplayScope) {
|
|
2397
|
+
const reasoningReplayConversationId = reasoningReplayConversationIdFromResponsesRequest({
|
|
2228
2398
|
clientThreadId: parsed._clientThreadId,
|
|
2229
|
-
sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
|
|
2230
2399
|
threadIdHeader: req.headers.get("thread-id"),
|
|
2231
2400
|
cursorConversationId: parsed._cursorConversationId,
|
|
2401
|
+
sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
|
|
2232
2402
|
});
|
|
2403
|
+
if (reasoningReplayConversationId) {
|
|
2404
|
+
parsed._reasoningReplayScope = { clientThreadId: reasoningReplayConversationId };
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
// Prefer a pre-populated id (routed Claude) over Responses headers that may be
|
|
2408
|
+
// absent or synthetically injected (session_id from prompt_cache_key).
|
|
2409
|
+
if (!logCtx.conversationId) {
|
|
2410
|
+
logCtx.conversationId = resolvedConversationId;
|
|
2233
2411
|
}
|
|
2234
2412
|
logCtx.requestedModel = parsed.modelId;
|
|
2235
2413
|
logCtx.requestedEffort = parsed.options.reasoning;
|
|
@@ -2293,7 +2471,12 @@ async function handleResponsesInner(
|
|
|
2293
2471
|
// also fail closed without polling quota upstream. Cached fallback state can still select a
|
|
2294
2472
|
// provider with native continuation support below.
|
|
2295
2473
|
const threadSpawn = isThreadSpawnRequest(req.headers);
|
|
2296
|
-
const
|
|
2474
|
+
const initialSubagentFallbackChain = threadSpawn && !options.comboAttempt
|
|
2475
|
+
? resolveSubagentFallbackChain(parsed, config)
|
|
2476
|
+
: null;
|
|
2477
|
+
const previewSelectionAdmission = threadSpawn
|
|
2478
|
+
&& !options.comboAttempt
|
|
2479
|
+
&& (route.codexAccountId === undefined || initialSubagentFallbackChain !== null)
|
|
2297
2480
|
? codexAccountSelectionForTurn(options.turnAdmissionLease)?.()
|
|
2298
2481
|
: undefined;
|
|
2299
2482
|
const nativeMainRecoveryBlocked = isNativeMainTrafficBlocked();
|
|
@@ -2305,7 +2488,8 @@ async function handleResponsesInner(
|
|
|
2305
2488
|
};
|
|
2306
2489
|
let selectedForwardHeaders = req.headers;
|
|
2307
2490
|
let subagentFallbackAccountId = config.activeCodexAccountId ?? null;
|
|
2308
|
-
let
|
|
2491
|
+
let subagentFallbackAccountPreview: SubagentPoolAccountPreview | undefined;
|
|
2492
|
+
let subagentFallbackModelEligibleAccountIdsForModel: SubagentModelEligibleAccountIds | undefined;
|
|
2309
2493
|
let subagentQuotaFailureModel = parsed.modelId;
|
|
2310
2494
|
const parentThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() ?? null;
|
|
2311
2495
|
const poolAffinityKey = codexPoolAffinityKey(req.headers) ?? null;
|
|
@@ -2323,28 +2507,47 @@ async function handleResponsesInner(
|
|
|
2323
2507
|
// normalization (virtual models, effort caps, service tier, wire protocol).
|
|
2324
2508
|
// Preview the preferred Codex account without acquiring a probe lease or refreshing
|
|
2325
2509
|
// tokens — auth is resolved only after the final route is selected.
|
|
2326
|
-
if (
|
|
2510
|
+
if (
|
|
2511
|
+
threadSpawn
|
|
2512
|
+
&& !options.comboAttempt
|
|
2513
|
+
&& (route.codexAccountId === undefined || initialSubagentFallbackChain !== null)
|
|
2514
|
+
) {
|
|
2327
2515
|
// The final resolveCodexAuthContext binds under codexQuotaScopeForModel(route.modelId),
|
|
2328
2516
|
// so the preview must read the same scope slot — an undefined scope would map to the
|
|
2329
2517
|
// "legacy" affinity bucket and never find a binding made under "shared" or a native
|
|
2330
2518
|
// model scope, making the preview diverge from the account that actually authenticates.
|
|
2331
|
-
const
|
|
2519
|
+
const fallbackChain = initialSubagentFallbackChain;
|
|
2520
|
+
subagentFallbackModelEligibleAccountIdsForModel = await resolveSubagentFallbackModelEligibility({
|
|
2521
|
+
config,
|
|
2522
|
+
fallbackChain,
|
|
2523
|
+
nativeMainReadsForbidden,
|
|
2524
|
+
resolver: options.resolveCodexModelEntitlements ?? resolveCodexModelEntitlements,
|
|
2525
|
+
});
|
|
2526
|
+
const fallbackNow = Date.now();
|
|
2527
|
+
subagentFallbackAccountPreview = (modelId, previewNow, modelEligibleAccountIds) => previewCodexAccountForRequest(
|
|
2332
2528
|
poolAffinityKey,
|
|
2333
2529
|
config,
|
|
2334
|
-
|
|
2335
|
-
codexQuotaScopeForModel(
|
|
2336
|
-
previewSelectionOptions,
|
|
2530
|
+
previewNow,
|
|
2531
|
+
codexQuotaScopeForModel(modelId),
|
|
2532
|
+
{ ...previewSelectionOptions, modelEligibleAccountIds },
|
|
2533
|
+
);
|
|
2534
|
+
const previewAccountId = route.codexAccountId ?? subagentFallbackAccountPreview(
|
|
2535
|
+
route.modelId,
|
|
2536
|
+
fallbackNow,
|
|
2537
|
+
subagentFallbackModelEligibleAccountIdsForModel?.(route.modelId),
|
|
2337
2538
|
);
|
|
2338
|
-
subagentFallbackPreviewAccountId = previewAccountId;
|
|
2339
2539
|
subagentFallbackAccountId = previewAccountId ?? config.activeCodexAccountId ?? null;
|
|
2340
2540
|
const fallback = applySubagentModelFallback(
|
|
2341
2541
|
parsed,
|
|
2342
2542
|
req.headers,
|
|
2343
2543
|
config,
|
|
2344
2544
|
previewAccountId,
|
|
2345
|
-
|
|
2545
|
+
fallbackNow,
|
|
2346
2546
|
unreadableEncryptedAgentTask,
|
|
2347
2547
|
previewSelectionOptions,
|
|
2548
|
+
subagentFallbackAccountPreview,
|
|
2549
|
+
subagentFallbackModelEligibleAccountIdsForModel,
|
|
2550
|
+
fallbackChain,
|
|
2348
2551
|
);
|
|
2349
2552
|
if (fallback) {
|
|
2350
2553
|
(logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
|
|
@@ -2410,6 +2613,7 @@ async function handleResponsesInner(
|
|
|
2410
2613
|
"_providerContinuationOwner",
|
|
2411
2614
|
"_cursorConversationId",
|
|
2412
2615
|
"_clientThreadId",
|
|
2616
|
+
"_cursorClientThreadId",
|
|
2413
2617
|
"_reasoningReplayScope",
|
|
2414
2618
|
"_cursorIsolateConversation",
|
|
2415
2619
|
];
|
|
@@ -2427,15 +2631,47 @@ async function handleResponsesInner(
|
|
|
2427
2631
|
// The ciphertext-only pass intentionally excludes routed candidates. Once recovery
|
|
2428
2632
|
// makes the assignment readable, run selection again with the full configured chain
|
|
2429
2633
|
// and keep the route in sync with any newly selected fallback.
|
|
2430
|
-
const
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2634
|
+
const recoverySelectionAdmission = codexAccountSelectionForTurn(options.turnAdmissionLease)?.();
|
|
2635
|
+
const fallback = (() => {
|
|
2636
|
+
try {
|
|
2637
|
+
const recoveryNativeMainBlocked = isNativeMainTrafficBlocked();
|
|
2638
|
+
const recoverySelectionOptions = {
|
|
2639
|
+
nativeMainSelectionOnly: !recoveryNativeMainBlocked
|
|
2640
|
+
&& recoverySelectionAdmission?.mainProfileDraining === true,
|
|
2641
|
+
};
|
|
2642
|
+
const recoveryNow = Date.now();
|
|
2643
|
+
// Carry the entitlement filter through recovery too (#2509/#2623). The scope was
|
|
2644
|
+
// already re-previewed per candidate here; the ELIGIBLE-ACCOUNT set was not, so a
|
|
2645
|
+
// recovered assignment could select an account that is not entitled to the model
|
|
2646
|
+
// and then fail closed at final auth — the same class of stale-selection bug as
|
|
2647
|
+
// the quota scope, one layer over.
|
|
2648
|
+
subagentFallbackAccountPreview = (modelId, previewNow, modelEligibleAccountIds) => previewCodexAccountForRequest(
|
|
2649
|
+
poolAffinityKey,
|
|
2650
|
+
config,
|
|
2651
|
+
previewNow,
|
|
2652
|
+
codexQuotaScopeForModel(modelId),
|
|
2653
|
+
{ ...recoverySelectionOptions, modelEligibleAccountIds },
|
|
2654
|
+
);
|
|
2655
|
+
const recoveryPreviewAccountId = subagentFallbackAccountPreview(
|
|
2656
|
+
parsed.modelId,
|
|
2657
|
+
recoveryNow,
|
|
2658
|
+
subagentFallbackModelEligibleAccountIdsForModel?.(parsed.modelId),
|
|
2659
|
+
);
|
|
2660
|
+
return applySubagentModelFallback(
|
|
2661
|
+
parsed,
|
|
2662
|
+
req.headers,
|
|
2663
|
+
config,
|
|
2664
|
+
recoveryPreviewAccountId,
|
|
2665
|
+
recoveryNow,
|
|
2666
|
+
false,
|
|
2667
|
+
recoverySelectionOptions,
|
|
2668
|
+
subagentFallbackAccountPreview,
|
|
2669
|
+
subagentFallbackModelEligibleAccountIdsForModel,
|
|
2670
|
+
);
|
|
2671
|
+
} finally {
|
|
2672
|
+
recoverySelectionAdmission?.release();
|
|
2673
|
+
}
|
|
2674
|
+
})();
|
|
2439
2675
|
if (fallback) {
|
|
2440
2676
|
(logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
|
|
2441
2677
|
(logCtx as unknown as Record<string, unknown>).subagentModelFallbackTo = fallback.to;
|
|
@@ -2558,7 +2794,7 @@ async function handleResponsesInner(
|
|
|
2558
2794
|
}
|
|
2559
2795
|
|
|
2560
2796
|
route.provider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
|
|
2561
|
-
applyCodexAccountGatedWireNormalization(parsed, route);
|
|
2797
|
+
applyCodexAccountGatedWireNormalization(parsed, route, logCtx);
|
|
2562
2798
|
logCtx.provider = route.codexAccountNamespace
|
|
2563
2799
|
? `${route.providerName}-${route.codexAccountNamespace}`
|
|
2564
2800
|
: formatCodexProviderForLog(route.providerName, codexLogAccountId(authCtx), config);
|
|
@@ -2580,6 +2816,46 @@ async function handleResponsesInner(
|
|
|
2580
2816
|
let replayOAuthCredentialSnapshot: Pick<OAuthAccessSnapshot, "accountId" | "generation"> | undefined;
|
|
2581
2817
|
let anthropicPoolAccountId: string | null = null;
|
|
2582
2818
|
let anthropicPoolFailovers = 0;
|
|
2819
|
+
// Generic OAuth rotation (#2568) for providers with no pool of their own. Bound to the account
|
|
2820
|
+
// the request actually used, so a concurrent rotation cannot cool an innocent replacement.
|
|
2821
|
+
let genericFailoverAccountId: string | null = null;
|
|
2822
|
+
let genericFailovers = 0;
|
|
2823
|
+
/**
|
|
2824
|
+
* Apply a rotated account's FULL credential snapshot to the live route (#2568d).
|
|
2825
|
+
*
|
|
2826
|
+
* One helper for all three rotation sites on purpose. Each site used to inline the same four
|
|
2827
|
+
* lines, and the divergence that produced was the bug: `apiKey` was swapped while the routing
|
|
2828
|
+
* metadata paired with it stayed behind.
|
|
2829
|
+
*
|
|
2830
|
+
* Returns false when the snapshot cannot be used safely, and the caller must then abandon the
|
|
2831
|
+
* rotation rather than send a half-applied identity:
|
|
2832
|
+
*
|
|
2833
|
+
* - Copilot pins its bearer to an account-scoped regional origin, so transport is re-resolved
|
|
2834
|
+
* with the new account's `apiBaseUrl` instead of inheriting the previous account's host.
|
|
2835
|
+
* - A Cloud Code Assist provider needs an account-matched project. Antigravity's refresh path
|
|
2836
|
+
* tolerates project discovery failing, so a stored account can legitimately have no project;
|
|
2837
|
+
* sending that account's bearer with the FAILED account's project is worse than not rotating.
|
|
2838
|
+
*/
|
|
2839
|
+
const applyFailoverSnapshot = (snapshot: OAuthAccessSnapshot): boolean => {
|
|
2840
|
+
if (route.provider.googleMode === "cloud-code-assist" && !snapshot.projectId) return false;
|
|
2841
|
+
let rotatedProvider: OcxProviderConfig = { ...route.provider, apiKey: snapshot.accessToken };
|
|
2842
|
+
if (route.providerName === "github-copilot") {
|
|
2843
|
+
rotatedProvider = resolveProviderTransport(
|
|
2844
|
+
route.providerName,
|
|
2845
|
+
rotatedProvider,
|
|
2846
|
+
parsed.options.promptCacheKey,
|
|
2847
|
+
snapshot.apiBaseUrl,
|
|
2848
|
+
) as OcxProviderConfig;
|
|
2849
|
+
}
|
|
2850
|
+
if (snapshot.projectId) rotatedProvider = { ...rotatedProvider, project: snapshot.projectId };
|
|
2851
|
+
route.provider = rotatedProvider;
|
|
2852
|
+
if (route.providerName === "kiro") parsed._kiroAuthContext = { ...(snapshot.kiro ?? {}) };
|
|
2853
|
+
// Re-stamp: a request that rotated accounts must be attributed to the account that actually
|
|
2854
|
+
// served it. All three rotation sites funnel through here, so this is the only re-stamp
|
|
2855
|
+
// needed -- and putting it anywhere else would let one of the three drift.
|
|
2856
|
+
stampOAuthAccountLabel(logCtx, route.providerName, route.provider, snapshot.accountId);
|
|
2857
|
+
return true;
|
|
2858
|
+
};
|
|
2583
2859
|
const anthropicSessionKey = route.providerName === "anthropic" && route.provider.authMode === "oauth"
|
|
2584
2860
|
? anthropicSessionKeyFromParts({
|
|
2585
2861
|
sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
|
|
@@ -2619,6 +2895,15 @@ async function handleResponsesInner(
|
|
|
2619
2895
|
};
|
|
2620
2896
|
if (isOAuth401ReplayProvider) sentOAuthSnapshot = resolved;
|
|
2621
2897
|
route.provider = { ...route.provider, apiKey: resolved.accessToken };
|
|
2898
|
+
// Attribution is independent of failover (#2699): stamped from the resolved snapshot
|
|
2899
|
+
// itself, not from inside the `isGenericFailoverProvider` branch below, so a future
|
|
2900
|
+
// narrowing of that predicate cannot silently switch attribution off.
|
|
2901
|
+
stampOAuthAccountLabel(logCtx, route.providerName, route.provider, resolved.accountId);
|
|
2902
|
+
// Remember which account actually served this request so a 429 cools THAT one, not
|
|
2903
|
+
// whichever account is active by the time the response comes back (#2568).
|
|
2904
|
+
if (isGenericFailoverProvider(route.providerName, route.provider)) {
|
|
2905
|
+
genericFailoverAccountId = resolved.accountId;
|
|
2906
|
+
}
|
|
2622
2907
|
if (route.providerName === "kiro") {
|
|
2623
2908
|
// `{}` is intentional: this is an account-scoped request with no stored routing metadata.
|
|
2624
2909
|
// Only genuinely accountless adapter calls leave the context undefined and use local/env fallback.
|
|
@@ -2697,6 +2982,7 @@ async function handleResponsesInner(
|
|
|
2697
2982
|
(logCtx.attempts ??= []).push(attempt);
|
|
2698
2983
|
}
|
|
2699
2984
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name, logCtx.accountLogLabel);
|
|
2985
|
+
let runTurnAdapter = adapter;
|
|
2700
2986
|
if (adapter.runTurn) {
|
|
2701
2987
|
recordAdapterTierMetadata(logCtx, adapter.tierLogForRunTurn?.(parsed));
|
|
2702
2988
|
}
|
|
@@ -2825,8 +3111,9 @@ async function handleResponsesInner(
|
|
|
2825
3111
|
delete parsed.options.parallelToolCalls;
|
|
2826
3112
|
// The compaction turn is a plain prose summary; a surviving structured-output format
|
|
2827
3113
|
// would force schema-constrained JSON into the synthetic compaction item. The flag and
|
|
2828
|
-
// the raw `text`
|
|
2829
|
-
//
|
|
3114
|
+
// the raw `text` control go too: the key-mode openai-responses adapter builds from
|
|
3115
|
+
// _rawBody, so a surviving format there would still reach the upstream. (The Kiro
|
|
3116
|
+
// guard no longer reads _rawBody.text; it refuses structured output only.)
|
|
2830
3117
|
delete parsed.options.textFormat;
|
|
2831
3118
|
delete parsed._structuredOutput;
|
|
2832
3119
|
if (parsed._rawBody && typeof parsed._rawBody === "object") {
|
|
@@ -2848,6 +3135,7 @@ async function handleResponsesInner(
|
|
|
2848
3135
|
? new Map<string, { namespace: string; name: string }>()
|
|
2849
3136
|
: imageGenToolCallAliases(toolBridgeMaps.toolNsMap, parsed._rawBody, translatorBudget);
|
|
2850
3137
|
const routedCustomToolNames = new Set<string>();
|
|
3138
|
+
const routedCustomToolRepairNames = new Set<string>();
|
|
2851
3139
|
const routedToolSearchNames = new Set<string>();
|
|
2852
3140
|
// Local continuation cache for the ChatGPT passthrough. Codex WS turns chain with
|
|
2853
3141
|
// previous_response_id, ocx converts them to internal HTTP requests, and the ChatGPT Codex
|
|
@@ -2870,6 +3158,23 @@ async function handleResponsesInner(
|
|
|
2870
3158
|
+ `(model ${parsed.modelId}); forwarding without it — earlier turns may be missing from this request`,
|
|
2871
3159
|
);
|
|
2872
3160
|
}
|
|
3161
|
+
// Preserve the caller's readable catalog boundary before provider-specific normalization can
|
|
3162
|
+
// remove an unsupported final entry (for example xAI cached-only web search).
|
|
3163
|
+
const replayedInputPrefixLength = parsed._replayPrefixLen ?? 0;
|
|
3164
|
+
const clientToolAuthorizationBody = currentTurnWireToolCatalogBody(
|
|
3165
|
+
parsed._rawBody,
|
|
3166
|
+
replayedInputPrefixLength,
|
|
3167
|
+
);
|
|
3168
|
+
const clientExplicitWireToolCatalog = hasExplicitWireToolCatalog(clientToolAuthorizationBody);
|
|
3169
|
+
const clientDeclaredWireToolNames = collectDeclaredWireToolNames(clientToolAuthorizationBody);
|
|
3170
|
+
const clientDeclaredNamelessCallTypes = collectDeclaredNamelessClientCallTypes(
|
|
3171
|
+
clientToolAuthorizationBody,
|
|
3172
|
+
);
|
|
3173
|
+
// Hosted calls the PROVIDER runs itself. Gated on the destination actually being xAI, so a
|
|
3174
|
+
// declaration alone cannot buy the exemption on some other upstream that never serves it.
|
|
3175
|
+
const providerExecutedCallTypes = isXaiResponsesDestination(route.provider)
|
|
3176
|
+
? collectProviderExecutedCallTypes(clientToolAuthorizationBody)
|
|
3177
|
+
: new Set<ProviderExecutedCallType>();
|
|
2873
3178
|
let request: Awaited<ReturnType<typeof adapter.buildRequest>>;
|
|
2874
3179
|
try {
|
|
2875
3180
|
request = await adapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
|
|
@@ -2879,7 +3184,9 @@ async function handleResponsesInner(
|
|
|
2879
3184
|
// the rotation-rebuild and bridged paths already answer 400 for the identical throw. Rethrowing
|
|
2880
3185
|
// it here escaped every catch up to the Bun handler, so the same request produced an
|
|
2881
3186
|
// unstructured 500 — and no request log — depending only on whether a rotation ran first.
|
|
2882
|
-
|
|
3187
|
+
// Same shape for a tool_choice this proxy cannot honor: the destination rejects a schema the
|
|
3188
|
+
// catalog had to drop, so the selector naming it is a client input error, not a 500.
|
|
3189
|
+
if (error instanceof NamespaceToolCollisionError || error instanceof XaiToolSchemaCompatibilityError) {
|
|
2883
3190
|
return formatErrorResponse(400, "invalid_request_error", redactSecretString(error.message));
|
|
2884
3191
|
}
|
|
2885
3192
|
throw error;
|
|
@@ -2891,6 +3198,12 @@ async function handleResponsesInner(
|
|
|
2891
3198
|
|| toolBridgeMaps.toolNsMap.get(name)?.freeform === true
|
|
2892
3199
|
) routedCustomToolNames.add(name);
|
|
2893
3200
|
}
|
|
3201
|
+
for (const name of request.routedCustomToolRepairNames ?? []) {
|
|
3202
|
+
if (
|
|
3203
|
+
toolBridgeMaps.freeformToolNames.has(name)
|
|
3204
|
+
|| toolBridgeMaps.toolNsMap.get(name)?.freeform === true
|
|
3205
|
+
) routedCustomToolRepairNames.add(name);
|
|
3206
|
+
}
|
|
2894
3207
|
}
|
|
2895
3208
|
for (const name of request.convertedRoutedToolSearchNames ?? []) {
|
|
2896
3209
|
// The adapter already keeps this set empty when tool_choice forbids the private search.
|
|
@@ -2906,13 +3219,13 @@ async function handleResponsesInner(
|
|
|
2906
3219
|
// call it cannot execute, and the turn showed a bare `aborted` with the file untouched.
|
|
2907
3220
|
// Forward auth is the canonical ChatGPT backend speaking Codex's own protocol rather than a
|
|
2908
3221
|
// routed provider, so it keeps passing through unguarded, as it does for the rewrites above.
|
|
2909
|
-
// The guard needs a catalog to compare against, so it stands down when the request
|
|
2910
|
-
//
|
|
2911
|
-
// request can legitimately omit `tools` entirely and still receive a
|
|
2912
|
-
// understands — `tests/github-copilot-stream-contract.test.ts` sends
|
|
2913
|
-
// with no tools and Copilot answers with a `custom_tool_call` for
|
|
2914
|
-
//
|
|
2915
|
-
//
|
|
3222
|
+
// The guard needs a catalog to compare against, so it stands down when the request omits one.
|
|
3223
|
+
// An explicit empty catalog is still authoritative: it declares that no client tools may be
|
|
3224
|
+
// called. A passthrough request can legitimately omit `tools` entirely and still receive a call
|
|
3225
|
+
// the client understands — `tests/github-copilot-stream-contract.test.ts` sends
|
|
3226
|
+
// `{model, input, stream}` with no tools and Copilot answers with a `custom_tool_call` for
|
|
3227
|
+
// `apply_patch`. Policing an absent catalog truncates that turn. An unreadable body lands there
|
|
3228
|
+
// too because the proxy cannot establish the caller's declared authorization boundary.
|
|
2916
3229
|
const parseOutboundRequestBody = (bodyText: string): Record<string, unknown> | undefined => {
|
|
2917
3230
|
try {
|
|
2918
3231
|
const body = JSON.parse(bodyText) as unknown;
|
|
@@ -2923,16 +3236,46 @@ async function handleResponsesInner(
|
|
|
2923
3236
|
return undefined;
|
|
2924
3237
|
}
|
|
2925
3238
|
};
|
|
2926
|
-
let outboundRequestBody
|
|
2927
|
-
const declaredWireToolNames =
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
3239
|
+
let outboundRequestBody: Record<string, unknown> | undefined;
|
|
3240
|
+
const declaredWireToolNames = new Set<string>();
|
|
3241
|
+
const declaredNamelessClientCallTypes = new Set<string>();
|
|
3242
|
+
let undeclaredToolGuardActive = false;
|
|
3243
|
+
const refreshUndeclaredToolGuard = (builtRequest: AdapterRequest): void => {
|
|
3244
|
+
outboundRequestBody = parseOutboundRequestBody(builtRequest.body);
|
|
3245
|
+
declaredWireToolNames.clear();
|
|
3246
|
+
// With no replay prefix the full outbound body belongs to this turn and its normalized
|
|
3247
|
+
// aliases are authoritative. A continuation's outbound body still contains historical
|
|
3248
|
+
// catalogs (and may promote historical tool-search definitions), so it can never widen the
|
|
3249
|
+
// current caller snapshot captured above.
|
|
3250
|
+
if (replayedInputPrefixLength === 0) {
|
|
3251
|
+
for (const name of collectDeclaredWireToolNames(outboundRequestBody)) {
|
|
3252
|
+
declaredWireToolNames.add(name);
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
for (const name of clientDeclaredWireToolNames) declaredWireToolNames.add(name);
|
|
3256
|
+
declaredNamelessClientCallTypes.clear();
|
|
3257
|
+
if (replayedInputPrefixLength === 0) {
|
|
3258
|
+
for (const callType of collectDeclaredNamelessClientCallTypes(outboundRequestBody)) {
|
|
3259
|
+
declaredNamelessClientCallTypes.add(callType);
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
for (const callType of clientDeclaredNamelessCallTypes) {
|
|
3263
|
+
declaredNamelessClientCallTypes.add(callType);
|
|
3264
|
+
}
|
|
3265
|
+
// On an ordinary request these maps capture caller-catalog identities that normalization may
|
|
3266
|
+
// replace on the outbound wire (for example a client image tool becoming hosted). On replay,
|
|
3267
|
+
// however, the parsed maps also contain historical catalog entries, so only the bounded
|
|
3268
|
+
// current-turn wire snapshot above may authorize a call.
|
|
3269
|
+
if (replayedInputPrefixLength === 0) {
|
|
3270
|
+
for (const name of toolBridgeMaps.declaredToolNames) declaredWireToolNames.add(name);
|
|
3271
|
+
}
|
|
3272
|
+
undeclaredToolGuardActive = (
|
|
3273
|
+
declaredWireToolNames.size > 0
|
|
3274
|
+
|| clientDeclaredNamelessCallTypes.size > 0
|
|
3275
|
+
|| clientExplicitWireToolCatalog
|
|
3276
|
+
) && route.provider.authMode !== "forward";
|
|
3277
|
+
};
|
|
3278
|
+
refreshUndeclaredToolGuard(request);
|
|
2936
3279
|
// A refused turn must not seed `previous_response_id` replay. The inspection branch reads the
|
|
2937
3280
|
// untouched upstream stream, so it can still observe a `response.completed` the client never
|
|
2938
3281
|
// received; checking the payload itself rather than a flag shared with the client relay keeps
|
|
@@ -2949,20 +3292,36 @@ async function handleResponsesInner(
|
|
|
2949
3292
|
// provider) every name looks undeclared, and flipping this would stop recording continuation
|
|
2950
3293
|
// state for exactly the passthrough traffic the guard deliberately stands down for.
|
|
2951
3294
|
if (!undeclaredToolGuardActive || inspectionSawUndeclaredTool) return;
|
|
2952
|
-
if (undeclaredToolCallName(
|
|
3295
|
+
if (undeclaredToolCallName(
|
|
3296
|
+
payload,
|
|
3297
|
+
declaredWireToolNames,
|
|
3298
|
+
declaredNamelessClientCallTypes,
|
|
3299
|
+
providerExecutedCallTypes,
|
|
3300
|
+
) !== undefined) {
|
|
2953
3301
|
inspectionSawUndeclaredTool = true;
|
|
2954
3302
|
}
|
|
2955
3303
|
};
|
|
2956
3304
|
const rememberPassthroughResponseChecked = rememberPassthroughResponse
|
|
2957
3305
|
? (response: { id?: unknown; output?: unknown; status?: unknown }) => {
|
|
2958
3306
|
if (inspectionSawUndeclaredTool) return;
|
|
3307
|
+
const restoredResponse = restoreRoutedCustomCalls(
|
|
3308
|
+
response,
|
|
3309
|
+
routedCustomToolNames,
|
|
3310
|
+
routedCustomToolRepairNames,
|
|
3311
|
+
declaredWireToolNames,
|
|
3312
|
+
).value as { id?: unknown; output?: unknown; status?: unknown };
|
|
2959
3313
|
if (
|
|
2960
3314
|
undeclaredToolGuardActive
|
|
2961
|
-
&& undeclaredToolCallNameInResponse(
|
|
3315
|
+
&& undeclaredToolCallNameInResponse(
|
|
3316
|
+
restoredResponse,
|
|
3317
|
+
declaredWireToolNames,
|
|
3318
|
+
declaredNamelessClientCallTypes,
|
|
3319
|
+
providerExecutedCallTypes,
|
|
3320
|
+
) !== undefined
|
|
2962
3321
|
) {
|
|
2963
3322
|
return;
|
|
2964
3323
|
}
|
|
2965
|
-
rememberPassthroughResponse(
|
|
3324
|
+
rememberPassthroughResponse(restoredResponse);
|
|
2966
3325
|
}
|
|
2967
3326
|
: undefined;
|
|
2968
3327
|
recordAdapterReasoning(logCtx, request);
|
|
@@ -3125,7 +3484,7 @@ async function handleResponsesInner(
|
|
|
3125
3484
|
? request.usageLog.inputTokens
|
|
3126
3485
|
: undefined;
|
|
3127
3486
|
if (passthroughEstimate !== undefined) logCtx.usageLogInputTokens = passthroughEstimate;
|
|
3128
|
-
|
|
3487
|
+
refreshUndeclaredToolGuard(request);
|
|
3129
3488
|
logCtx.providerAdapter = retryAdapter.name;
|
|
3130
3489
|
sealRequestAttemptIdentity(
|
|
3131
3490
|
logCtx.activeAttempt,
|
|
@@ -3235,6 +3594,7 @@ async function handleResponsesInner(
|
|
|
3235
3594
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3236
3595
|
return formatErrorResponse(400, "invalid_request_error", redactSecretString(msg));
|
|
3237
3596
|
}
|
|
3597
|
+
refreshUndeclaredToolGuard(request);
|
|
3238
3598
|
try {
|
|
3239
3599
|
upstreamResponse = await fetchWithTransientRetry(
|
|
3240
3600
|
recovery => {
|
|
@@ -3386,6 +3746,7 @@ async function handleResponsesInner(
|
|
|
3386
3746
|
authCtx = retry.authCtx;
|
|
3387
3747
|
request = retry.request;
|
|
3388
3748
|
refreshRoutedNamespaceToolAliases(request);
|
|
3749
|
+
refreshUndeclaredToolGuard(request);
|
|
3389
3750
|
upstreamResponse = retry.upstreamResponse;
|
|
3390
3751
|
selectedForwardHeaders = retry.selectedForwardHeaders;
|
|
3391
3752
|
// Keep subagent quota-failure health keyed to the account that actually served.
|
|
@@ -3414,7 +3775,7 @@ async function handleResponsesInner(
|
|
|
3414
3775
|
}
|
|
3415
3776
|
const headers = sanitizePassthroughHeaders(upstreamResponse.headers);
|
|
3416
3777
|
const resolvedModel = headers.get("openai-model")?.trim();
|
|
3417
|
-
if (resolvedModel) logCtx.resolvedModel = resolvedModel;
|
|
3778
|
+
if (resolvedModel && !logCtx.preserveResolvedModelFromRoute) logCtx.resolvedModel = resolvedModel;
|
|
3418
3779
|
if (isUsageDebugEnabled()) {
|
|
3419
3780
|
const upstreamContentType = upstreamResponse.headers.get("content-type");
|
|
3420
3781
|
if (upstreamContentType) logCtx.usageDebugContentType = upstreamContentType;
|
|
@@ -3424,13 +3785,21 @@ async function handleResponsesInner(
|
|
|
3424
3785
|
const passthroughCt = headers.get("content-type")?.toLowerCase();
|
|
3425
3786
|
const isEventStream = passthroughCt?.includes("text/event-stream")
|
|
3426
3787
|
|| (upstreamResponse.ok && !!upstreamResponse.body && !passthroughCt && parsed.stream);
|
|
3427
|
-
const
|
|
3788
|
+
const recordTerminalOutcome = codexForwardTerminalOutcomeRecorder(
|
|
3428
3789
|
config,
|
|
3429
3790
|
authCtx,
|
|
3430
3791
|
route.provider,
|
|
3431
3792
|
route.modelId,
|
|
3432
3793
|
logCtx,
|
|
3433
3794
|
);
|
|
3795
|
+
let terminalOutcomeRecorded = false;
|
|
3796
|
+
const terminalRecorder = recordTerminalOutcome
|
|
3797
|
+
? (status: ResponsesTerminalStatus, httpStatusOverride?: number): void => {
|
|
3798
|
+
if (terminalOutcomeRecorded) return;
|
|
3799
|
+
terminalOutcomeRecorded = true;
|
|
3800
|
+
recordTerminalOutcome(status, httpStatusOverride);
|
|
3801
|
+
}
|
|
3802
|
+
: undefined;
|
|
3434
3803
|
const terminalBodyWillRecord = !!terminalRecorder && upstreamResponse.ok && isEventStream;
|
|
3435
3804
|
// Capture quota from upstream response for multi-account tracking
|
|
3436
3805
|
if (usesCodexForwardPoolAuth(authCtx, route.provider)) {
|
|
@@ -3507,19 +3876,13 @@ async function handleResponsesInner(
|
|
|
3507
3876
|
options.onConsumedComboFailure?.(failure);
|
|
3508
3877
|
return failure.response;
|
|
3509
3878
|
}
|
|
3510
|
-
// The
|
|
3511
|
-
//
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
statusText: upstreamResponse.statusText,
|
|
3518
|
-
headers,
|
|
3519
|
-
});
|
|
3520
|
-
} finally {
|
|
3521
|
-
detachPassthroughErrorGuard();
|
|
3522
|
-
}
|
|
3879
|
+
// The bounded reader owns the original body, deadline, abort settlement, and lock.
|
|
3880
|
+
// Unsafe partial data falls back to #452's non-empty status-only JSON.
|
|
3881
|
+
const errorText = await readDisplaySafeErrorText(upstreamResponse, upstream.signal, "");
|
|
3882
|
+
return formatPassthroughUpstreamError(upstreamResponse.status, errorText, {
|
|
3883
|
+
statusText: upstreamResponse.statusText,
|
|
3884
|
+
headers,
|
|
3885
|
+
});
|
|
3523
3886
|
}
|
|
3524
3887
|
|
|
3525
3888
|
// Bun#32111 workaround: passthrough SSE uses tee()+native relay to avoid the
|
|
@@ -3582,8 +3945,13 @@ async function handleResponsesInner(
|
|
|
3582
3945
|
payloadRewrites.length > 0
|
|
3583
3946
|
? payloadRewriteAsBlockRewrite(composeSsePayloadRewrites(...payloadRewrites))
|
|
3584
3947
|
: undefined,
|
|
3585
|
-
routedCustomToolNames.size > 0
|
|
3586
|
-
? createRoutedCustomToolRestoreBlockRewrite(
|
|
3948
|
+
routedCustomToolNames.size > 0 || routedCustomToolRepairNames.size > 0
|
|
3949
|
+
? createRoutedCustomToolRestoreBlockRewrite(
|
|
3950
|
+
routedCustomToolNames,
|
|
3951
|
+
translatorBudget,
|
|
3952
|
+
routedCustomToolRepairNames,
|
|
3953
|
+
declaredWireToolNames,
|
|
3954
|
+
)
|
|
3587
3955
|
: undefined,
|
|
3588
3956
|
routedToolSearchNames.size > 0
|
|
3589
3957
|
? createRoutedToolSearchRestoreBlockRewrite(routedToolSearchNames, translatorBudget)
|
|
@@ -3598,7 +3966,11 @@ async function handleResponsesInner(
|
|
|
3598
3966
|
// Last: every rewrite above can still rename or reshape a call item, so the guard must
|
|
3599
3967
|
// compare the names the client will actually receive against the declared catalog.
|
|
3600
3968
|
undeclaredToolGuardActive
|
|
3601
|
-
? createUndeclaredToolCallGuardBlockRewrite(
|
|
3969
|
+
? createUndeclaredToolCallGuardBlockRewrite(
|
|
3970
|
+
declaredWireToolNames,
|
|
3971
|
+
declaredNamelessClientCallTypes,
|
|
3972
|
+
providerExecutedCallTypes,
|
|
3973
|
+
)
|
|
3602
3974
|
: undefined,
|
|
3603
3975
|
].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
|
|
3604
3976
|
const clientBlockRewrite = blockRewrites.length > 0
|
|
@@ -3788,6 +4160,8 @@ async function handleResponsesInner(
|
|
|
3788
4160
|
const restored = restoreRoutedCustomCallsInJson(
|
|
3789
4161
|
restoredNamespace,
|
|
3790
4162
|
routedCustomToolNames,
|
|
4163
|
+
routedCustomToolRepairNames,
|
|
4164
|
+
declaredWireToolNames,
|
|
3791
4165
|
);
|
|
3792
4166
|
const restoredToolSearch = restoreRoutedToolSearchCallsInJson(
|
|
3793
4167
|
restored,
|
|
@@ -3814,7 +4188,12 @@ async function handleResponsesInner(
|
|
|
3814
4188
|
if (undeclaredToolGuardActive) {
|
|
3815
4189
|
const undeclared = (() => {
|
|
3816
4190
|
try {
|
|
3817
|
-
return undeclaredToolCallNameInResponse(
|
|
4191
|
+
return undeclaredToolCallNameInResponse(
|
|
4192
|
+
JSON.parse(clientJson),
|
|
4193
|
+
declaredWireToolNames,
|
|
4194
|
+
declaredNamelessClientCallTypes,
|
|
4195
|
+
providerExecutedCallTypes,
|
|
4196
|
+
);
|
|
3818
4197
|
} catch {
|
|
3819
4198
|
return undefined;
|
|
3820
4199
|
}
|
|
@@ -3939,6 +4318,49 @@ async function handleResponsesInner(
|
|
|
3939
4318
|
const imgPlan = !routedCompaction ? await planImageBridge(config, parsed, route.provider) : undefined;
|
|
3940
4319
|
const vidPlan = !routedCompaction ? await planVideoBridge(config, parsed, route.provider) : undefined;
|
|
3941
4320
|
const canRunWebSearch = !!wsPlan && !adapter.runTurn;
|
|
4321
|
+
const rotateSidecarProviderOn429 = async (retryAfter: string | null): Promise<ProviderAdapter | null> => {
|
|
4322
|
+
const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
|
|
4323
|
+
retryAfter,
|
|
4324
|
+
now: Date.now(),
|
|
4325
|
+
attemptedKey: route.provider.apiKey,
|
|
4326
|
+
promptCacheKey: parsed.options.promptCacheKey,
|
|
4327
|
+
});
|
|
4328
|
+
if (rotated) {
|
|
4329
|
+
route.provider = rotated;
|
|
4330
|
+
} else {
|
|
4331
|
+
if (
|
|
4332
|
+
!genericFailoverAccountId
|
|
4333
|
+
|| genericFailovers >= GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
|
|
4334
|
+
|| !isGenericOAuthFailoverEnabled(config, route.providerName)
|
|
4335
|
+
) return null;
|
|
4336
|
+
const nextAccountId = rotateGenericOAuthAccountOn429(
|
|
4337
|
+
config,
|
|
4338
|
+
route.providerName,
|
|
4339
|
+
genericFailoverAccountId,
|
|
4340
|
+
retryAfter,
|
|
4341
|
+
);
|
|
4342
|
+
if (!nextAccountId) return null;
|
|
4343
|
+
try {
|
|
4344
|
+
const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
|
|
4345
|
+
genericFailoverAccountId = nextAccountId;
|
|
4346
|
+
genericFailovers += 1;
|
|
4347
|
+
if (!applyFailoverSnapshot(snapshot)) return null;
|
|
4348
|
+
} catch {
|
|
4349
|
+
return null;
|
|
4350
|
+
}
|
|
4351
|
+
}
|
|
4352
|
+
const rotatedAdapter = resolveAdapter(
|
|
4353
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
4354
|
+
config.cacheRetention,
|
|
4355
|
+
);
|
|
4356
|
+
bindRouteReasoningReplayScope({
|
|
4357
|
+
parsed,
|
|
4358
|
+
providerName: route.providerName,
|
|
4359
|
+
provider: route.provider,
|
|
4360
|
+
adapterName: rotatedAdapter.name,
|
|
4361
|
+
});
|
|
4362
|
+
return rotatedAdapter;
|
|
4363
|
+
};
|
|
3942
4364
|
if ((imgPlan || vidPlan) && canRunWebSearch) {
|
|
3943
4365
|
// Web search takes priority when both are active — the media bridge cannot run
|
|
3944
4366
|
// alongside runWithWebSearch. Surface a runtime signal so the user knows their
|
|
@@ -4029,32 +4451,13 @@ async function handleResponsesInner(
|
|
|
4029
4451
|
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
4030
4452
|
}
|
|
4031
4453
|
},
|
|
4032
|
-
on429:
|
|
4033
|
-
const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
|
|
4034
|
-
retryAfter,
|
|
4035
|
-
now: Date.now(),
|
|
4036
|
-
attemptedKey: route.provider.apiKey,
|
|
4037
|
-
promptCacheKey: parsed.options.promptCacheKey,
|
|
4038
|
-
});
|
|
4039
|
-
if (!rotated) return null;
|
|
4040
|
-
route.provider = rotated;
|
|
4041
|
-
const rotatedAdapter = resolveAdapter(
|
|
4042
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
4043
|
-
config.cacheRetention,
|
|
4044
|
-
);
|
|
4045
|
-
bindRouteReasoningReplayScope({
|
|
4046
|
-
parsed,
|
|
4047
|
-
providerName: route.providerName,
|
|
4048
|
-
provider: route.provider,
|
|
4049
|
-
adapterName: rotatedAdapter.name,
|
|
4050
|
-
});
|
|
4051
|
-
return rotatedAdapter;
|
|
4052
|
-
},
|
|
4454
|
+
on429: rotateSidecarProviderOn429,
|
|
4053
4455
|
retryOn429Policy: rateLimitRetryPolicyFor(route.provider),
|
|
4054
4456
|
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
4055
4457
|
...(options.forceEmptyResponseId ? { forceEmptyResponseId: true } : {}),
|
|
4056
4458
|
onCompletedResponse: (response, providerState) => {
|
|
4057
4459
|
commitReasoningReplayServingRoute();
|
|
4460
|
+
rememberKiroDeliveredFinalAnswer(adapter.name, response);
|
|
4058
4461
|
rememberResponseState(
|
|
4059
4462
|
parsed._rawBody,
|
|
4060
4463
|
response,
|
|
@@ -4117,27 +4520,7 @@ async function handleResponsesInner(
|
|
|
4117
4520
|
routedModelStallTimeoutMs: wsPlan.routedModelStallTimeoutMs,
|
|
4118
4521
|
stallTimeoutSec: wsPlan.stallTimeoutSec,
|
|
4119
4522
|
streamRoutedModelOutput: wsPlan.streamRoutedModelOutput,
|
|
4120
|
-
on429:
|
|
4121
|
-
const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
|
|
4122
|
-
retryAfter,
|
|
4123
|
-
now: Date.now(),
|
|
4124
|
-
attemptedKey: route.provider.apiKey,
|
|
4125
|
-
promptCacheKey: parsed.options.promptCacheKey,
|
|
4126
|
-
});
|
|
4127
|
-
if (!rotated) return null;
|
|
4128
|
-
route.provider = rotated;
|
|
4129
|
-
const rotatedAdapter = resolveAdapter(
|
|
4130
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
4131
|
-
config.cacheRetention,
|
|
4132
|
-
);
|
|
4133
|
-
bindRouteReasoningReplayScope({
|
|
4134
|
-
parsed,
|
|
4135
|
-
providerName: route.providerName,
|
|
4136
|
-
provider: route.provider,
|
|
4137
|
-
adapterName: rotatedAdapter.name,
|
|
4138
|
-
});
|
|
4139
|
-
return rotatedAdapter;
|
|
4140
|
-
},
|
|
4523
|
+
on429: rotateSidecarProviderOn429,
|
|
4141
4524
|
retryOn429Policy: rateLimitRetryPolicyFor(route.provider),
|
|
4142
4525
|
onCompletedResponse: commitReasoningReplayServingRoute,
|
|
4143
4526
|
});
|
|
@@ -4209,7 +4592,7 @@ async function handleResponsesInner(
|
|
|
4209
4592
|
pacingSlotAcquired: true,
|
|
4210
4593
|
},
|
|
4211
4594
|
);
|
|
4212
|
-
await
|
|
4595
|
+
await runTurnAdapter.runTurn?.(
|
|
4213
4596
|
parsed,
|
|
4214
4597
|
{
|
|
4215
4598
|
headers: selectedForwardHeaders,
|
|
@@ -4242,6 +4625,77 @@ async function handleResponsesInner(
|
|
|
4242
4625
|
}
|
|
4243
4626
|
};
|
|
4244
4627
|
const runTurn = async (): Promise<void> => runTurnAttempt(queue, undefined, true);
|
|
4628
|
+
const rotateRunTurnAdapterOnPreflight429 = async (
|
|
4629
|
+
error: Extract<AdapterEvent, { type: "error" }>,
|
|
4630
|
+
): Promise<boolean> => {
|
|
4631
|
+
const status = error.status ?? adapterFailureFromMessage(error.message).httpStatus;
|
|
4632
|
+
if (
|
|
4633
|
+
status !== 429
|
|
4634
|
+
|| !genericFailoverAccountId
|
|
4635
|
+
|| genericFailovers >= GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
|
|
4636
|
+
|| !isGenericOAuthFailoverEnabled(config, route.providerName)
|
|
4637
|
+
) return false;
|
|
4638
|
+
const nextAccountId = rotateGenericOAuthAccountOn429(
|
|
4639
|
+
config,
|
|
4640
|
+
route.providerName,
|
|
4641
|
+
genericFailoverAccountId,
|
|
4642
|
+
null,
|
|
4643
|
+
);
|
|
4644
|
+
if (!nextAccountId) return false;
|
|
4645
|
+
try {
|
|
4646
|
+
const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
|
|
4647
|
+
genericFailoverAccountId = nextAccountId;
|
|
4648
|
+
genericFailovers += 1;
|
|
4649
|
+
if (!applyFailoverSnapshot(snapshot)) return false;
|
|
4650
|
+
// A Cursor conversation/checkpoint is credential-scoped. The failed attempt emitted no
|
|
4651
|
+
// client-visible bytes, so replay is safe, but carrying its account identity into the next
|
|
4652
|
+
// account would not be. Let the rotated adapter derive a fresh identity and conversation.
|
|
4653
|
+
parsed._cursorIdentityScope = undefined;
|
|
4654
|
+
parsed._cursorConversationId = undefined;
|
|
4655
|
+
if (parsed._providerContinuation?.cursor) {
|
|
4656
|
+
const { cursor: _discardedCursor, ...otherProviderState } = parsed._providerContinuation;
|
|
4657
|
+
parsed._providerContinuation = otherProviderState;
|
|
4658
|
+
}
|
|
4659
|
+
const rotatedProvider = resolveWireProtocolOverride(
|
|
4660
|
+
route.providerName,
|
|
4661
|
+
route.modelId,
|
|
4662
|
+
route.provider,
|
|
4663
|
+
inboundWire,
|
|
4664
|
+
);
|
|
4665
|
+
const rotatedAdapter = resolveAdapter(rotatedProvider, config.cacheRetention);
|
|
4666
|
+
if (!rotatedAdapter.runTurn) return false;
|
|
4667
|
+
runTurnAdapter = rotatedAdapter;
|
|
4668
|
+
bindRouteReasoningReplayScope({
|
|
4669
|
+
parsed,
|
|
4670
|
+
providerName: route.providerName,
|
|
4671
|
+
provider: rotatedProvider,
|
|
4672
|
+
adapterName: rotatedAdapter.name,
|
|
4673
|
+
oauthCredentialSnapshot: { accountId: snapshot.accountId, generation: snapshot.generation },
|
|
4674
|
+
codexAuthContext: authCtx,
|
|
4675
|
+
forwardHeaders: selectedForwardHeaders,
|
|
4676
|
+
});
|
|
4677
|
+
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, rotatedAdapter.name, logCtx.accountLogLabel);
|
|
4678
|
+
return true;
|
|
4679
|
+
} catch {
|
|
4680
|
+
return false;
|
|
4681
|
+
}
|
|
4682
|
+
};
|
|
4683
|
+
const preflightRunTurnFailover = async (
|
|
4684
|
+
firstSource: AsyncIterable<AdapterEvent>,
|
|
4685
|
+
): Promise<AsyncIterable<AdapterEvent>> => {
|
|
4686
|
+
let source = firstSource;
|
|
4687
|
+
while (true) {
|
|
4688
|
+
const preflight = await preflightAdapterEvents(source);
|
|
4689
|
+
if (!preflight.error || !(await rotateRunTurnAdapterOnPreflight429(preflight.error))) {
|
|
4690
|
+
return preflight.stream;
|
|
4691
|
+
}
|
|
4692
|
+
const retryQueue = createAdapterEventQueue({
|
|
4693
|
+
onBacklogExceeded: () => runTurnAbort.abort(),
|
|
4694
|
+
});
|
|
4695
|
+
void runTurnAttempt(retryQueue, "oauth-account-429");
|
|
4696
|
+
source = retryQueue.stream();
|
|
4697
|
+
}
|
|
4698
|
+
};
|
|
4245
4699
|
// The empty-completion retry re-runs the turn against a fresh queue: the
|
|
4246
4700
|
// first queue is closed once its attempt settles, and pushing into it after
|
|
4247
4701
|
// close is a silent no-op.
|
|
@@ -4257,6 +4711,11 @@ async function handleResponsesInner(
|
|
|
4257
4711
|
if (parsed.stream) {
|
|
4258
4712
|
void runTurn();
|
|
4259
4713
|
let eventSource: AsyncIterable<AdapterEvent> = queue.stream();
|
|
4714
|
+
if (genericFailoverAccountId && isGenericOAuthFailoverEnabled(config, route.providerName)) {
|
|
4715
|
+
// Preflight holds only heartbeats and the first meaningful event. A first-event 429 can be
|
|
4716
|
+
// replayed transparently; after any output reaches the bridge, a later error stays terminal.
|
|
4717
|
+
eventSource = await preflightRunTurnFailover(eventSource);
|
|
4718
|
+
}
|
|
4260
4719
|
if (options.comboAttempt) {
|
|
4261
4720
|
const preflight = await preflightAdapterEvents(eventSource);
|
|
4262
4721
|
if (preflight.error || preflight.empty) {
|
|
@@ -4274,7 +4733,16 @@ async function handleResponsesInner(
|
|
|
4274
4733
|
// signal — run the adapter transport again against a fresh queue.
|
|
4275
4734
|
continuation: runTurnRetrySource,
|
|
4276
4735
|
})
|
|
4277
|
-
:
|
|
4736
|
+
// Guard off (the default): leave the stream alone, but record that the turn ended
|
|
4737
|
+
// empty so the user has something to correlate instead of an unexplained blank
|
|
4738
|
+
// result (#2472). Retrying by default would re-send a turn that may already have had
|
|
4739
|
+
// billable side effects, so the honest default is observability, not recovery.
|
|
4740
|
+
: observeEmptyCompletion(eventSource, () => {
|
|
4741
|
+
console.warn(
|
|
4742
|
+
`[opencodex] ${route.providerName}/${route.modelId} completed with no output text `
|
|
4743
|
+
+ "and no tool call. Set \"emptyCompletionRetry\": true to retry such turns once.",
|
|
4744
|
+
);
|
|
4745
|
+
});
|
|
4278
4746
|
const sseStream = bridgeToResponsesSSE(
|
|
4279
4747
|
guardedSource, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
|
|
4280
4748
|
() => {
|
|
@@ -4305,6 +4773,7 @@ async function handleResponsesInner(
|
|
|
4305
4773
|
},
|
|
4306
4774
|
onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => {
|
|
4307
4775
|
commitReasoningReplayServingRoute();
|
|
4776
|
+
rememberKiroDeliveredFinalAnswer(adapter.name, response);
|
|
4308
4777
|
if (!routedCompaction) {
|
|
4309
4778
|
rememberResponseState(
|
|
4310
4779
|
parsed._rawBody,
|
|
@@ -4318,22 +4787,31 @@ async function handleResponsesInner(
|
|
|
4318
4787
|
);
|
|
4319
4788
|
const bridgeTurnAc = new AbortController();
|
|
4320
4789
|
const trackedSse = trackStreamLifetime(sseStream, bridgeTurnAc, undefined, options.turnAdmissionLease);
|
|
4321
|
-
|
|
4790
|
+
const response = new Response(trackedSse, {
|
|
4322
4791
|
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no" },
|
|
4323
4792
|
});
|
|
4793
|
+
runTurnAdapterSseResponses.add(response);
|
|
4794
|
+
return response;
|
|
4324
4795
|
}
|
|
4325
4796
|
|
|
4326
4797
|
await runTurn();
|
|
4327
4798
|
const firstAttemptEvents = await queue.collect();
|
|
4799
|
+
let runTurnEvents: AdapterEvent[] = firstAttemptEvents;
|
|
4800
|
+
if (genericFailoverAccountId && isGenericOAuthFailoverEnabled(config, route.providerName)) {
|
|
4801
|
+
runTurnEvents = [];
|
|
4802
|
+
for await (const event of await preflightRunTurnFailover(
|
|
4803
|
+
(async function* () { yield* firstAttemptEvents; })(),
|
|
4804
|
+
)) runTurnEvents.push(event);
|
|
4805
|
+
}
|
|
4328
4806
|
let events: AdapterEvent[];
|
|
4329
4807
|
if (emptyCompletionGuardEnabled) {
|
|
4330
4808
|
events = [];
|
|
4331
4809
|
for await (const event of guardEmptyCompletionEventStream({
|
|
4332
|
-
firstEvents: (async function* () { yield*
|
|
4810
|
+
firstEvents: (async function* () { yield* runTurnEvents; })(),
|
|
4333
4811
|
continuation: runTurnRetrySource,
|
|
4334
4812
|
})) events.push(event);
|
|
4335
4813
|
} else {
|
|
4336
|
-
events =
|
|
4814
|
+
events = runTurnEvents;
|
|
4337
4815
|
}
|
|
4338
4816
|
if (options.comboAttempt) {
|
|
4339
4817
|
const firstMeaningful = events.find(event => event.type !== "heartbeat");
|
|
@@ -4365,6 +4843,7 @@ async function handleResponsesInner(
|
|
|
4365
4843
|
},
|
|
4366
4844
|
});
|
|
4367
4845
|
if (!routedCompaction) {
|
|
4846
|
+
rememberKiroDeliveredFinalAnswer(adapter.name, json);
|
|
4368
4847
|
rememberResponseState(
|
|
4369
4848
|
parsed._rawBody,
|
|
4370
4849
|
json,
|
|
@@ -4399,6 +4878,64 @@ async function handleResponsesInner(
|
|
|
4399
4878
|
// reuse is safe, and releaseBodyObservation is idempotent per build.
|
|
4400
4879
|
let initialRequest: AdapterRequest | undefined;
|
|
4401
4880
|
let inputTokenEstimate: number | undefined;
|
|
4881
|
+
// An adapter may know the turn needs no inference at all — Kiro's replayed history ending in a
|
|
4882
|
+
// delivered final answer. Answer it locally: no build (so no token estimate), no send (so
|
|
4883
|
+
// sendCount stays 0), and crucially no empty-completion guard, which treats an outputless
|
|
4884
|
+
// terminal as a failed turn and re-invokes the identical request. Routing this through the
|
|
4885
|
+
// ordinary event path would therefore reinstate the loop it exists to end.
|
|
4886
|
+
const localTerminal = activeAdapter.localTerminal?.(parsed);
|
|
4887
|
+
if (localTerminal) {
|
|
4888
|
+
logCtx.localTerminalReason = localTerminal.reason;
|
|
4889
|
+
// Mark the physical attempt too, not just the parent row. `finishRequestAttempt` finalizes the
|
|
4890
|
+
// attempt through the same estimated-provider path, so without this the row reads exact while
|
|
4891
|
+
// its own attempt still claims an estimate — the detailed accounting a maintainer actually
|
|
4892
|
+
// reads for a zero-send turn.
|
|
4893
|
+
if (logCtx.activeAttempt) logCtx.activeAttempt.locallyAnswered = true;
|
|
4894
|
+
cleanupUpstreamAbort();
|
|
4895
|
+
upstream.abort();
|
|
4896
|
+
const terminalEvents: AdapterEvent[] = [{
|
|
4897
|
+
type: "done",
|
|
4898
|
+
endTurn: true,
|
|
4899
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
4900
|
+
}];
|
|
4901
|
+
if (parsed.stream) {
|
|
4902
|
+
const localSse = bridgeToResponsesSSE(
|
|
4903
|
+
(async function* () { yield* terminalEvents; })(),
|
|
4904
|
+
parsed._responseModelId ?? parsed.modelId,
|
|
4905
|
+
toolBridgeMaps.toolNsMap,
|
|
4906
|
+
toolBridgeMaps.freeformToolNames,
|
|
4907
|
+
toolBridgeMaps.toolSearchToolNames,
|
|
4908
|
+
undefined,
|
|
4909
|
+
2_000,
|
|
4910
|
+
{
|
|
4911
|
+
translatorBudget,
|
|
4912
|
+
...(options.forceEmptyResponseId ? { responseId: "" } : {}),
|
|
4913
|
+
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
4914
|
+
},
|
|
4915
|
+
);
|
|
4916
|
+
// Same lifetime tracking as every other streaming return in this function: the turn
|
|
4917
|
+
// admission lease is released when the body finishes or the client disconnects. Returning
|
|
4918
|
+
// the raw stream would hold a lease for a turn that already has all of its output.
|
|
4919
|
+
const localTurnAc = new AbortController();
|
|
4920
|
+
return new Response(
|
|
4921
|
+
trackStreamLifetime(localSse, localTurnAc, undefined, options.turnAdmissionLease),
|
|
4922
|
+
{
|
|
4923
|
+
headers: {
|
|
4924
|
+
"Content-Type": "text/event-stream",
|
|
4925
|
+
"Cache-Control": "no-cache",
|
|
4926
|
+
"Connection": "keep-alive",
|
|
4927
|
+
"X-Accel-Buffering": "no",
|
|
4928
|
+
},
|
|
4929
|
+
},
|
|
4930
|
+
);
|
|
4931
|
+
}
|
|
4932
|
+
return new Response(
|
|
4933
|
+
JSON.stringify(buildResponseJSON(terminalEvents, parsed._responseModelId ?? parsed.modelId, {
|
|
4934
|
+
translatorBudget,
|
|
4935
|
+
})),
|
|
4936
|
+
{ headers: { "Content-Type": "application/json" } },
|
|
4937
|
+
);
|
|
4938
|
+
}
|
|
4402
4939
|
try {
|
|
4403
4940
|
initialRequest = await activeAdapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
|
|
4404
4941
|
refreshRoutedNamespaceToolAliases(initialRequest);
|
|
@@ -4732,6 +5269,49 @@ async function handleResponsesInner(
|
|
|
4732
5269
|
break;
|
|
4733
5270
|
}
|
|
4734
5271
|
}
|
|
5272
|
+
// Generic OAuth account failover (#2568) for providers with no pool of their own.
|
|
5273
|
+
// Presence is consent since #2568d: rotation is ON by default once two or more eligible
|
|
5274
|
+
// accounts are stored for the provider, because a second deliberate login is read as the
|
|
5275
|
+
// operator asking for it. A single-account install is still a strict no-op, and an
|
|
5276
|
+
// explicit `oauthAccountFailover.enabled: false` (global or per provider) still wins --
|
|
5277
|
+
// see isGenericOAuthFailoverEnabled in src/oauth/generic-account-failover.ts. Codex and
|
|
5278
|
+
// Anthropic are excluded by isGenericFailoverProvider: their pools own quota scopes,
|
|
5279
|
+
// probe leases and affinity that this must not reimplement.
|
|
5280
|
+
while (
|
|
5281
|
+
upstreamResponse.status === 429
|
|
5282
|
+
&& genericFailoverAccountId
|
|
5283
|
+
&& genericFailovers < GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
|
|
5284
|
+
&& isGenericOAuthFailoverEnabled(config, route.providerName)
|
|
5285
|
+
) {
|
|
5286
|
+
const nextAccountId = rotateGenericOAuthAccountOn429(
|
|
5287
|
+
config,
|
|
5288
|
+
route.providerName,
|
|
5289
|
+
genericFailoverAccountId,
|
|
5290
|
+
upstreamResponse.headers.get("retry-after"),
|
|
5291
|
+
);
|
|
5292
|
+
if (!nextAccountId) break;
|
|
5293
|
+
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
5294
|
+
try {
|
|
5295
|
+
// The FULL snapshot, not just the bearer: Antigravity pairs an account-matched
|
|
5296
|
+
// projectId with its token and Kiro carries routing metadata, so a token-only swap
|
|
5297
|
+
// would mix one account's credential with another's routing data.
|
|
5298
|
+
const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
|
|
5299
|
+
genericFailoverAccountId = nextAccountId;
|
|
5300
|
+
genericFailovers += 1;
|
|
5301
|
+
if (!applyFailoverSnapshot(snapshot)) break;
|
|
5302
|
+
invalidateSameTargetRequest();
|
|
5303
|
+
activeAdapter = resolveAdapter(
|
|
5304
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
5305
|
+
config.cacheRetention,
|
|
5306
|
+
);
|
|
5307
|
+
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name, logCtx.accountLogLabel);
|
|
5308
|
+
const result = await rebuildAndRefetch("oauth-account-429");
|
|
5309
|
+
if ("failed" in result) return result.failed;
|
|
5310
|
+
upstreamResponse = result;
|
|
5311
|
+
} catch {
|
|
5312
|
+
break;
|
|
5313
|
+
}
|
|
5314
|
+
}
|
|
4735
5315
|
// Unknown provenance is deliberately fail-soft in pre-flight: after a restart, TTL expiry,
|
|
4736
5316
|
// or LRU eviction, a valid same-backend blob must survive. A decoder's own 4xx identity is
|
|
4737
5317
|
// the missing authoritative signal. Rebuild once through the same sanitation path used by a
|
|
@@ -4783,14 +5363,16 @@ async function handleResponsesInner(
|
|
|
4783
5363
|
options.onConsumedComboFailure?.(failure);
|
|
4784
5364
|
return failure.response;
|
|
4785
5365
|
}
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
5366
|
+
let errorText: string;
|
|
5367
|
+
try {
|
|
5368
|
+
errorText = await readDisplaySafeErrorText(
|
|
5369
|
+
upstreamResponse,
|
|
5370
|
+
upstream.signal,
|
|
5371
|
+
"unknown error",
|
|
5372
|
+
);
|
|
5373
|
+
} finally {
|
|
5374
|
+
cleanupUpstreamAbort();
|
|
5375
|
+
}
|
|
4794
5376
|
if (!isFixedCodexAccount(authCtx)) {
|
|
4795
5377
|
recordSubagentQuotaFailureForThreadSpawn(
|
|
4796
5378
|
req.headers,
|
|
@@ -4805,29 +5387,41 @@ async function handleResponsesInner(
|
|
|
4805
5387
|
// Upstreams occasionally echo request details in error bodies — scrub token-shaped
|
|
4806
5388
|
// material before it reaches the client-facing error surface.
|
|
4807
5389
|
const upstreamRetryAfter = upstreamResponse.headers.get("retry-after");
|
|
4808
|
-
const
|
|
4809
|
-
|
|
4810
|
-
|
|
5390
|
+
const normalized = normalizeUpstreamErrorText(errorText, "unknown error");
|
|
5391
|
+
const message = normalized.cyberPolicy
|
|
5392
|
+
? normalized.message
|
|
5393
|
+
?? (isCyberPolicyCode(normalized.code) ? CYBER_POLICY_FALLBACK_MESSAGE : normalized.safeText)
|
|
5394
|
+
: enrichOpenCodeZenRateLimitMessage(
|
|
5395
|
+
`Provider error ${upstreamResponse.status}: ${normalized.safeText}`,
|
|
5396
|
+
{
|
|
5397
|
+
status: upstreamResponse.status,
|
|
5398
|
+
providerName: route.providerName,
|
|
5399
|
+
baseUrl: route.provider.baseUrl,
|
|
5400
|
+
adapter: route.provider.adapter,
|
|
5401
|
+
authMode: route.provider.authMode,
|
|
5402
|
+
hasApiKey: Boolean(route.provider.apiKey?.trim()),
|
|
5403
|
+
upstreamRetryAfter,
|
|
5404
|
+
// This recovery path is the HTTP Responses wire; custom runTurn transports
|
|
5405
|
+
// never reach enrichOpenCodeZenRateLimitMessage here.
|
|
5406
|
+
supportsHttpSameKeyRetry: true,
|
|
5407
|
+
},
|
|
5408
|
+
);
|
|
5409
|
+
const retryAfter = normalized.cyberPolicy
|
|
5410
|
+
? undefined
|
|
5411
|
+
: resolveClientRetryAfter({
|
|
4811
5412
|
status: upstreamResponse.status,
|
|
4812
|
-
|
|
4813
|
-
baseUrl: route.provider.baseUrl,
|
|
4814
|
-
adapter: route.provider.adapter,
|
|
4815
|
-
authMode: route.provider.authMode,
|
|
4816
|
-
hasApiKey: Boolean(route.provider.apiKey?.trim()),
|
|
5413
|
+
message,
|
|
4817
5414
|
upstreamRetryAfter,
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
5415
|
+
});
|
|
5416
|
+
return formatErrorResponse(
|
|
5417
|
+
upstreamResponse.status,
|
|
5418
|
+
normalized.cyberPolicy ? (normalized.type ?? CYBER_POLICY_ERROR_CODE) : "upstream_error",
|
|
5419
|
+
message,
|
|
5420
|
+
{
|
|
5421
|
+
...(normalized.cyberPolicy ? { code: CYBER_POLICY_ERROR_CODE } : {}),
|
|
5422
|
+
...(retryAfter !== undefined ? { retryAfter } : {}),
|
|
4821
5423
|
},
|
|
4822
5424
|
);
|
|
4823
|
-
const retryAfter = resolveClientRetryAfter({
|
|
4824
|
-
status: upstreamResponse.status,
|
|
4825
|
-
message,
|
|
4826
|
-
upstreamRetryAfter,
|
|
4827
|
-
});
|
|
4828
|
-
return formatErrorResponse(upstreamResponse.status, "upstream_error", message, {
|
|
4829
|
-
...(retryAfter !== undefined ? { retryAfter } : {}),
|
|
4830
|
-
});
|
|
4831
5425
|
}
|
|
4832
5426
|
}
|
|
4833
5427
|
|
|
@@ -5081,16 +5675,22 @@ async function handleResponsesInner(
|
|
|
5081
5675
|
}
|
|
5082
5676
|
|
|
5083
5677
|
if (!response.ok) {
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
// orphans the internal rejection.
|
|
5087
|
-
const detachContinuationErrorGuard = cancelBodyOnAbort(response.body, upstream.signal);
|
|
5088
|
-
const errorText = await response.text().catch(() => "unknown error");
|
|
5089
|
-
detachContinuationErrorGuard();
|
|
5678
|
+
const errorText = await readDisplaySafeErrorText(response, upstream.signal, "unknown error");
|
|
5679
|
+
const normalized = normalizeUpstreamErrorText(errorText, "unknown error");
|
|
5090
5680
|
yield {
|
|
5091
5681
|
type: "error",
|
|
5092
|
-
status: response.status,
|
|
5093
|
-
message:
|
|
5682
|
+
status: normalized.cyberPolicy ? 400 : response.status,
|
|
5683
|
+
message: normalized.cyberPolicy
|
|
5684
|
+
? normalized.message
|
|
5685
|
+
?? (isCyberPolicyCode(normalized.code) ? CYBER_POLICY_FALLBACK_MESSAGE : normalized.safeText)
|
|
5686
|
+
: `Provider continuation error ${response.status}: ${normalized.safeText}`,
|
|
5687
|
+
...(normalized.cyberPolicy
|
|
5688
|
+
? {
|
|
5689
|
+
errorType: normalized.type ?? CYBER_POLICY_ERROR_CODE,
|
|
5690
|
+
code: CYBER_POLICY_ERROR_CODE,
|
|
5691
|
+
retryable: false,
|
|
5692
|
+
}
|
|
5693
|
+
: {}),
|
|
5094
5694
|
};
|
|
5095
5695
|
return;
|
|
5096
5696
|
}
|
|
@@ -5184,6 +5784,7 @@ async function handleResponsesInner(
|
|
|
5184
5784
|
},
|
|
5185
5785
|
onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => {
|
|
5186
5786
|
commitReasoningReplayServingRoute();
|
|
5787
|
+
rememberKiroDeliveredFinalAnswer(activeAdapter.name, response);
|
|
5187
5788
|
// Compaction turns must NOT enter the continuation cache: _rawBody still holds the full
|
|
5188
5789
|
// PRE-compaction history, and a later previous_response_id expansion would rehydrate the
|
|
5189
5790
|
// giant stale chain Codex just replaced.
|
|
@@ -5261,6 +5862,7 @@ async function handleResponsesInner(
|
|
|
5261
5862
|
});
|
|
5262
5863
|
// See the streaming branch: compaction turns skip the continuation cache.
|
|
5263
5864
|
if (!routedCompaction) {
|
|
5865
|
+
rememberKiroDeliveredFinalAnswer(activeAdapter.name, json);
|
|
5264
5866
|
rememberResponseState(
|
|
5265
5867
|
parsed._rawBody,
|
|
5266
5868
|
json,
|