@yansigit/opencodex 2.31.2 → 2.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-BG43zwVe.js +102 -0
- package/gui/dist/assets/index-CiSI-jrP.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +6 -8
- package/src/adapters/base.ts +2 -0
- package/src/adapters/command-code-project-context.ts +377 -0
- package/src/adapters/command-code.ts +7 -4
- package/src/adapters/cursor/live-models.ts +8 -0
- package/src/adapters/cursor/live-transport.ts +21 -0
- package/src/adapters/cursor/native-exec-bridge.ts +141 -0
- package/src/adapters/cursor/native-exec-desktop.ts +16 -0
- package/src/adapters/cursor/tool-definitions.ts +1 -1
- package/src/adapters/google-http.ts +12 -2
- package/src/adapters/google-wire-compiler.ts +83 -2
- package/src/adapters/google.ts +62 -21
- package/src/adapters/kiro.ts +0 -3
- package/src/adapters/openai-responses.ts +3 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-web-search.ts +7 -2
- package/src/bridge.ts +21 -15
- package/src/cli/dispatch.ts +50 -2
- package/src/cli/doctor.ts +24 -11
- package/src/cli/help.ts +4 -3
- package/src/cli/index.ts +11 -4
- package/src/cli/models.ts +13 -3
- package/src/cli/observe.ts +20 -5
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +7 -5
- package/src/cli/status.ts +2 -1
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +134 -0
- package/src/codex/app-server-processes.ts +3 -1
- package/src/codex/catalog/aggregation.ts +13 -1
- package/src/codex/catalog/effort.ts +24 -9
- package/src/codex/catalog/model-metadata.ts +566 -0
- package/src/codex/catalog/parsing.ts +33 -0
- package/src/codex/catalog/provider-fetch.ts +123 -32
- package/src/codex/catalog/sync.ts +5 -4
- package/src/codex/desktop-app-restart.ts +342 -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.ts +49 -21
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/combos/failover.ts +27 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +81 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +308 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config.ts +77 -812
- package/src/generated/compatibility-version.json +162 -94
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +141 -0
- package/src/integrations/ownership.ts +10 -0
- package/src/integrations/state.ts +44 -5
- package/src/integrations/writer.ts +6 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/process-control.ts +2 -1
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +3 -0
- package/src/providers/registry.ts +1 -1
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/custom-tool-compat.ts +132 -38
- package/src/responses/parser.ts +3 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/routing/compatibility/behavior.ts +3 -0
- package/src/server/auth-cors.ts +9 -7
- package/src/server/index.ts +102 -21
- 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 +1 -1
- package/src/server/management/config-routes.ts +4 -1
- package/src/server/management/context.ts +1 -1
- package/src/server/management/logs-usage-routes.ts +27 -6
- package/src/server/management/model-routes.ts +8 -4
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +2 -1
- package/src/server/request-log-conversation.ts +30 -0
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/combo-stream-preflight.ts +171 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +242 -101
- package/src/server/responses/fetch-helpers.ts +2 -97
- package/src/server/responses-custom-tool-repair.ts +41 -5
- package/src/server/responses-undeclared-tool-guard.ts +156 -15
- package/src/service.ts +8 -4
- package/src/types/provider.ts +7 -0
- package/src/types/request.ts +12 -1
- package/src/types/tools.ts +87 -11
- package/src/types.ts +1 -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/log.ts +16 -8
- package/src/usage/summary.ts +201 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/gemini-executor.ts +35 -13
- package/src/web-search/index.ts +85 -1
- package/gui/dist/assets/index-BAMgarF9.js +0 -102
- package/gui/dist/assets/index-CGoDO3uO.css +0 -1
|
@@ -22,8 +22,10 @@ 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";
|
|
@@ -125,7 +127,7 @@ import {
|
|
|
125
127
|
rotateCursorAccountOnAuth,
|
|
126
128
|
} from "../../oauth/cursor-routing";
|
|
127
129
|
import { getAccountCredential } from "../../oauth/store";
|
|
128
|
-
import { buildWebSearchTool, planWebSearch, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search";
|
|
130
|
+
import { buildWebSearchTool, mediaBridgeWillRun, planWebSearch, resolveCcaInTurnGrounding, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search";
|
|
129
131
|
import { buildImageTool, buildVideoTool, planImageBridge, planVideoBridge, runWithImageBridge, clampImageMaxRounds, IMAGE_GEN_TOOL_NAME, VIDEO_GEN_TOOL_NAME } from "../../images";
|
|
130
132
|
import { describeImagesInPlace, isModelTextOnly, planVisionSidecar, resolveOpenAiVisionModel, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision";
|
|
131
133
|
import { createAdapterEventQueue, preflightAdapterEvents, type AdapterEventQueue } from "../../adapters/run-turn-queue";
|
|
@@ -133,16 +135,12 @@ import {
|
|
|
133
135
|
applyCodexAuthContextToProvider,
|
|
134
136
|
codexPoolAffinityKey,
|
|
135
137
|
CodexAccountCooldownError,
|
|
136
|
-
codexMainProfileDrainingResponse,
|
|
137
|
-
cooldownErrorResponse,
|
|
138
138
|
CodexAuthContextError,
|
|
139
|
-
CodexDirectAuthenticationError,
|
|
140
139
|
CodexMainProfileDrainingError,
|
|
141
140
|
CodexPoolAuthenticationError,
|
|
142
141
|
CodexThreadAffinityExpiredError,
|
|
143
142
|
headersForCodexAuthContext,
|
|
144
143
|
materializeCodexUpstreamAuth,
|
|
145
|
-
CodexMainSubstitutionUnavailableError,
|
|
146
144
|
isCodexAuthContextUsable,
|
|
147
145
|
resolveCodexAuthContext,
|
|
148
146
|
codexProbeLeaseId,
|
|
@@ -245,6 +243,7 @@ import {
|
|
|
245
243
|
import {
|
|
246
244
|
conversationIdFromResponsesRequest,
|
|
247
245
|
normalizeLogConversationId,
|
|
246
|
+
reasoningReplayConversationIdFromResponsesRequest,
|
|
248
247
|
sessionIdHeaderFromRequest,
|
|
249
248
|
} from "../request-log-conversation";
|
|
250
249
|
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
@@ -291,6 +290,7 @@ import { createResponsesModelPayloadRewrite, rewriteResponsesModelJson } from ".
|
|
|
291
290
|
import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
|
|
292
291
|
|
|
293
292
|
import { buildToolBridgeMaps, collabSurface, injectDeveloperMessage, multiAgentGuidanceText } from "./collaboration";
|
|
293
|
+
import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
|
|
294
294
|
import { hasUnreadableEncryptedAgentTask, looksLikeBackendCiphertext, sanitizeEncryptedContentInPlace } from "./encrypted-payload";
|
|
295
295
|
import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel } from "./fetch-helpers";
|
|
296
296
|
import { classifyTransportFailureKind, transportErrorCode } from "../../lib/upstream-reachability";
|
|
@@ -326,8 +326,11 @@ import {
|
|
|
326
326
|
type RoutedNamespaceToolAliases,
|
|
327
327
|
} from "../../responses/namespace-tool-compat";
|
|
328
328
|
import {
|
|
329
|
+
collectDeclaredNamelessClientCallTypes,
|
|
329
330
|
collectDeclaredWireToolNames,
|
|
330
331
|
createUndeclaredToolCallGuardBlockRewrite,
|
|
332
|
+
currentTurnWireToolCatalogBody,
|
|
333
|
+
hasExplicitWireToolCatalog,
|
|
331
334
|
undeclaredToolCallMessage,
|
|
332
335
|
undeclaredToolCallName,
|
|
333
336
|
undeclaredToolCallNameInResponse,
|
|
@@ -339,6 +342,12 @@ import {
|
|
|
339
342
|
emptyCompletionRetryEnabled,
|
|
340
343
|
guardEmptyCompletionEventStream,
|
|
341
344
|
} from "./empty-completion-guard";
|
|
345
|
+
import { preflightComboStreamResponse } from "./combo-stream-preflight";
|
|
346
|
+
|
|
347
|
+
// runTurn adapters own an event queue and perform their combo preflight before
|
|
348
|
+
// bridging. A second byte-stream reader would reinterpret that transport's
|
|
349
|
+
// already-committed event boundary and can replay custom adapter work.
|
|
350
|
+
const runTurnAdapterSseResponses = new WeakSet<Response>();
|
|
342
351
|
|
|
343
352
|
/**
|
|
344
353
|
* Adapters whose continuation state must survive Codex's store:false requests.
|
|
@@ -535,6 +544,9 @@ function bindRouteReasoningReplayScope(args: {
|
|
|
535
544
|
if (reasoningReplayServingIdentityChanged(parsed._reasoningReplayScope)) {
|
|
536
545
|
parsed._stripReasoningEncryptedContent = true;
|
|
537
546
|
}
|
|
547
|
+
if (reasoningReplayOpaqueBlobRejectionMemoized(parsed._reasoningReplayScope)) {
|
|
548
|
+
parsed._stripReasoningEncryptedContent = true;
|
|
549
|
+
}
|
|
538
550
|
bindProviderContinuationForRoute(parsed, continuationOwner);
|
|
539
551
|
}
|
|
540
552
|
|
|
@@ -648,6 +660,26 @@ async function opaqueBlobRejectionBodyForRecovery(
|
|
|
648
660
|
}
|
|
649
661
|
}
|
|
650
662
|
|
|
663
|
+
/**
|
|
664
|
+
* Materialize an upstream error body only when the bounded reader observed a complete,
|
|
665
|
+
* display-safe payload. Partial timeout and over-limit prefixes are attacker-controlled,
|
|
666
|
+
* so callers keep their existing status-only fallback instead.
|
|
667
|
+
*/
|
|
668
|
+
export async function readDisplaySafeErrorText(
|
|
669
|
+
response: Response,
|
|
670
|
+
signal: AbortSignal,
|
|
671
|
+
fallback: string,
|
|
672
|
+
): Promise<string> {
|
|
673
|
+
try {
|
|
674
|
+
const body = await readBoundedResponseBody(response, { signal });
|
|
675
|
+
return body.displaySafe ? body.text : fallback;
|
|
676
|
+
} catch {
|
|
677
|
+
// Preserve the former Response.text().catch(fallback) contract. Request-abort
|
|
678
|
+
// classification remains owned by the surrounding response pipeline.
|
|
679
|
+
return fallback;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
651
683
|
function prepareOpaqueBlobRecovery(parsed: OcxParsedRequest): void {
|
|
652
684
|
parsed._stripReasoningEncryptedContent = true;
|
|
653
685
|
}
|
|
@@ -688,9 +720,20 @@ async function attemptOpaqueBlobRecovery(
|
|
|
688
720
|
}
|
|
689
721
|
|
|
690
722
|
args.guard.attempted = true;
|
|
723
|
+
const rejectedScope = args.parsed._reasoningReplayScope
|
|
724
|
+
? {
|
|
725
|
+
clientThreadId: args.parsed._reasoningReplayScope.clientThreadId,
|
|
726
|
+
...(args.parsed._reasoningReplayScope.current
|
|
727
|
+
? { current: { ...args.parsed._reasoningReplayScope.current } }
|
|
728
|
+
: {}),
|
|
729
|
+
}
|
|
730
|
+
: undefined;
|
|
691
731
|
prepareOpaqueBlobRecovery(args.parsed);
|
|
692
732
|
try { void args.response.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
693
733
|
const result = await rebuild("opaque-blob-rejection");
|
|
734
|
+
if (!("failed" in result) && result.ok) {
|
|
735
|
+
rememberReasoningReplayOpaqueBlobRejection(rejectedScope);
|
|
736
|
+
}
|
|
694
737
|
return "failed" in result
|
|
695
738
|
? { kind: "failed", response: result.failed }
|
|
696
739
|
: { kind: "recovered", response: result };
|
|
@@ -1517,44 +1560,20 @@ async function resolveResponsesCodexAuth(
|
|
|
1517
1560
|
substituteMainCredential,
|
|
1518
1561
|
};
|
|
1519
1562
|
} catch (err) {
|
|
1520
|
-
if (err instanceof CodexAccountCooldownError) {
|
|
1521
|
-
return { ok: false, response: cooldownErrorResponse(err, Date.now(), route.codexAccountNamespace) };
|
|
1522
|
-
}
|
|
1523
|
-
if (err instanceof CodexMainProfileDrainingError) {
|
|
1524
|
-
return { ok: false, response: codexMainProfileDrainingResponse() };
|
|
1525
|
-
}
|
|
1526
|
-
if (err instanceof CodexThreadAffinityExpiredError) {
|
|
1527
|
-
return {
|
|
1528
|
-
ok: false,
|
|
1529
|
-
response: formatErrorResponse(409, "invalid_request_error", "Codex thread account affinity expired; start a new session"),
|
|
1530
|
-
};
|
|
1531
|
-
}
|
|
1532
1563
|
if (err instanceof CodexAuthContextError) {
|
|
1533
1564
|
const safeAccountLabel = route.codexAccountNamespace
|
|
1534
1565
|
? `${route.providerName}-${route.codexAccountNamespace}`
|
|
1535
1566
|
: formatCodexProviderForLog(route.providerName, err.accountId, config);
|
|
1536
1567
|
console.error(`[codex-auth] Pool account ${safeAccountLabel} token failed; reauthentication required`);
|
|
1537
|
-
return {
|
|
1538
|
-
ok: false,
|
|
1539
|
-
response: formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"),
|
|
1540
|
-
};
|
|
1541
|
-
}
|
|
1542
|
-
if (err instanceof CodexPoolAuthenticationError) {
|
|
1543
|
-
return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
|
|
1544
|
-
}
|
|
1545
|
-
if (err instanceof CodexDirectAuthenticationError) {
|
|
1546
|
-
return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
|
|
1547
1568
|
}
|
|
1548
1569
|
if (err instanceof ForwardAdmissionCredentialError) {
|
|
1549
1570
|
return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
|
|
1550
1571
|
}
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
};
|
|
1557
|
-
}
|
|
1572
|
+
const response = mapCodexAuthContextErrorToResponse(err, {
|
|
1573
|
+
accountSelector: route.codexAccountNamespace,
|
|
1574
|
+
now: Date.now(),
|
|
1575
|
+
});
|
|
1576
|
+
if (response) return { ok: false, response };
|
|
1558
1577
|
throw err;
|
|
1559
1578
|
}
|
|
1560
1579
|
}
|
|
@@ -1932,6 +1951,31 @@ export async function handleComboResponses(
|
|
|
1932
1951
|
return clientCancelledResponse();
|
|
1933
1952
|
}
|
|
1934
1953
|
|
|
1954
|
+
if (response.ok && !runTurnAdapterSseResponses.has(response)) {
|
|
1955
|
+
const nativePassthrough = isNativePassthroughSseResponse(response);
|
|
1956
|
+
const eagerRelay = isEagerRelaySseResponse(response);
|
|
1957
|
+
let preflight;
|
|
1958
|
+
try {
|
|
1959
|
+
preflight = await preflightComboStreamResponse(response, childLog);
|
|
1960
|
+
} catch (error) {
|
|
1961
|
+
callbackGate.discard();
|
|
1962
|
+
if (options.abortSignal?.aborted) {
|
|
1963
|
+
retainCancelledAttempt();
|
|
1964
|
+
return clientCancelledResponse();
|
|
1965
|
+
}
|
|
1966
|
+
throw error;
|
|
1967
|
+
}
|
|
1968
|
+
if (preflight.kind === "failed") {
|
|
1969
|
+
callbackGate.discard();
|
|
1970
|
+
terminalRecorder?.("failed", preflight.response.status);
|
|
1971
|
+
response = preflight.response;
|
|
1972
|
+
} else {
|
|
1973
|
+
response = preflight.response;
|
|
1974
|
+
if (nativePassthrough) markNativePassthroughSseResponse(response);
|
|
1975
|
+
if (eagerRelay) markEagerRelaySseResponse(response);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1935
1979
|
if (response.ok) {
|
|
1936
1980
|
if (!attempt) return response;
|
|
1937
1981
|
sealRequestAttemptIdentity(
|
|
@@ -1989,7 +2033,7 @@ export async function handleComboResponses(
|
|
|
1989
2033
|
);
|
|
1990
2034
|
finishRequestAttempt(
|
|
1991
2035
|
attempt,
|
|
1992
|
-
response.status,
|
|
2036
|
+
failure.response.status,
|
|
1993
2037
|
Date.now() - (started ?? Date.now()),
|
|
1994
2038
|
failure.usage,
|
|
1995
2039
|
);
|
|
@@ -2004,7 +2048,7 @@ export async function handleComboResponses(
|
|
|
2004
2048
|
return lastFailure;
|
|
2005
2049
|
}
|
|
2006
2050
|
console.warn(
|
|
2007
|
-
`[combo] ${comboId}: ${targetKey(pick.target)} failed with ${response.status} after ${started === undefined ? 0 : Date.now() - started}ms`,
|
|
2051
|
+
`[combo] ${comboId}: ${targetKey(pick.target)} failed with ${failure.response.status} after ${started === undefined ? 0 : Date.now() - started}ms`,
|
|
2008
2052
|
);
|
|
2009
2053
|
const nextPick = advanceComboAfterFailure(config, pick, {
|
|
2010
2054
|
retryAfter: failure.retryAfter,
|
|
@@ -2220,7 +2264,6 @@ async function handleResponsesInner(
|
|
|
2220
2264
|
if (providerContinuationCandidate) parsed._providerContinuationCandidate = providerContinuationCandidate;
|
|
2221
2265
|
if (inboundClientThreadId) {
|
|
2222
2266
|
parsed._clientThreadId = inboundClientThreadId;
|
|
2223
|
-
parsed._reasoningReplayScope = { clientThreadId: inboundClientThreadId };
|
|
2224
2267
|
} else if (
|
|
2225
2268
|
options.inboundWire === "anthropic"
|
|
2226
2269
|
&& options.promptCacheKeyIsSharedCohort !== true
|
|
@@ -2256,15 +2299,31 @@ async function handleResponsesInner(
|
|
|
2256
2299
|
...(force ? { force: true } : {}),
|
|
2257
2300
|
...(parsed._clientThreadId ? { clientThreadId: parsed._clientThreadId } : {}),
|
|
2258
2301
|
});
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2302
|
+
const resolvedConversationId = conversationIdFromResponsesRequest({
|
|
2303
|
+
clientThreadId: parsed._clientThreadId,
|
|
2304
|
+
sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
|
|
2305
|
+
threadIdHeader: req.headers.get("thread-id"),
|
|
2306
|
+
cursorConversationId: parsed._cursorConversationId,
|
|
2307
|
+
});
|
|
2308
|
+
// _clientThreadId remains the routing/continuation identity supplied by Codex. Replay state uses
|
|
2309
|
+
// a dedicated raw conversation namespace so mixed headers that carry the same identity still
|
|
2310
|
+
// match, and a shared/synthetic session_id cannot coalesce distinct thread/Cursor conversations.
|
|
2311
|
+
// Keep an Anthropic prompt_cache_key scope already bound above (#1735/#1926).
|
|
2312
|
+
if (!parsed._reasoningReplayScope) {
|
|
2313
|
+
const reasoningReplayConversationId = reasoningReplayConversationIdFromResponsesRequest({
|
|
2263
2314
|
clientThreadId: parsed._clientThreadId,
|
|
2264
|
-
sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
|
|
2265
2315
|
threadIdHeader: req.headers.get("thread-id"),
|
|
2266
2316
|
cursorConversationId: parsed._cursorConversationId,
|
|
2317
|
+
sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
|
|
2267
2318
|
});
|
|
2319
|
+
if (reasoningReplayConversationId) {
|
|
2320
|
+
parsed._reasoningReplayScope = { clientThreadId: reasoningReplayConversationId };
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
// Prefer a pre-populated id (routed Claude) over Responses headers that may be
|
|
2324
|
+
// absent or synthetically injected (session_id from prompt_cache_key).
|
|
2325
|
+
if (!logCtx.conversationId) {
|
|
2326
|
+
logCtx.conversationId = resolvedConversationId;
|
|
2268
2327
|
}
|
|
2269
2328
|
logCtx.requestedModel = parsed.modelId;
|
|
2270
2329
|
logCtx.requestedEffort = parsed.options.reasoning;
|
|
@@ -2976,6 +3035,7 @@ async function handleResponsesInner(
|
|
|
2976
3035
|
? new Map<string, { namespace: string; name: string }>()
|
|
2977
3036
|
: imageGenToolCallAliases(toolBridgeMaps.toolNsMap, parsed._rawBody, translatorBudget);
|
|
2978
3037
|
const routedCustomToolNames = new Set<string>();
|
|
3038
|
+
const routedCustomToolRepairNames = new Set<string>();
|
|
2979
3039
|
const routedToolSearchNames = new Set<string>();
|
|
2980
3040
|
// Local continuation cache for the ChatGPT passthrough. Codex WS turns chain with
|
|
2981
3041
|
// previous_response_id, ocx converts them to internal HTTP requests, and the ChatGPT Codex
|
|
@@ -2998,6 +3058,18 @@ async function handleResponsesInner(
|
|
|
2998
3058
|
+ `(model ${parsed.modelId}); forwarding without it — earlier turns may be missing from this request`,
|
|
2999
3059
|
);
|
|
3000
3060
|
}
|
|
3061
|
+
// Preserve the caller's readable catalog boundary before provider-specific normalization can
|
|
3062
|
+
// remove an unsupported final entry (for example xAI cached-only web search).
|
|
3063
|
+
const replayedInputPrefixLength = parsed._replayPrefixLen ?? 0;
|
|
3064
|
+
const clientToolAuthorizationBody = currentTurnWireToolCatalogBody(
|
|
3065
|
+
parsed._rawBody,
|
|
3066
|
+
replayedInputPrefixLength,
|
|
3067
|
+
);
|
|
3068
|
+
const clientExplicitWireToolCatalog = hasExplicitWireToolCatalog(clientToolAuthorizationBody);
|
|
3069
|
+
const clientDeclaredWireToolNames = collectDeclaredWireToolNames(clientToolAuthorizationBody);
|
|
3070
|
+
const clientDeclaredNamelessCallTypes = collectDeclaredNamelessClientCallTypes(
|
|
3071
|
+
clientToolAuthorizationBody,
|
|
3072
|
+
);
|
|
3001
3073
|
let request: Awaited<ReturnType<typeof adapter.buildRequest>>;
|
|
3002
3074
|
try {
|
|
3003
3075
|
request = await adapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
|
|
@@ -3019,6 +3091,12 @@ async function handleResponsesInner(
|
|
|
3019
3091
|
|| toolBridgeMaps.toolNsMap.get(name)?.freeform === true
|
|
3020
3092
|
) routedCustomToolNames.add(name);
|
|
3021
3093
|
}
|
|
3094
|
+
for (const name of request.routedCustomToolRepairNames ?? []) {
|
|
3095
|
+
if (
|
|
3096
|
+
toolBridgeMaps.freeformToolNames.has(name)
|
|
3097
|
+
|| toolBridgeMaps.toolNsMap.get(name)?.freeform === true
|
|
3098
|
+
) routedCustomToolRepairNames.add(name);
|
|
3099
|
+
}
|
|
3022
3100
|
}
|
|
3023
3101
|
for (const name of request.convertedRoutedToolSearchNames ?? []) {
|
|
3024
3102
|
// The adapter already keeps this set empty when tool_choice forbids the private search.
|
|
@@ -3034,13 +3112,13 @@ async function handleResponsesInner(
|
|
|
3034
3112
|
// call it cannot execute, and the turn showed a bare `aborted` with the file untouched.
|
|
3035
3113
|
// Forward auth is the canonical ChatGPT backend speaking Codex's own protocol rather than a
|
|
3036
3114
|
// routed provider, so it keeps passing through unguarded, as it does for the rewrites above.
|
|
3037
|
-
// The guard needs a catalog to compare against, so it stands down when the request
|
|
3038
|
-
//
|
|
3039
|
-
// request can legitimately omit `tools` entirely and still receive a
|
|
3040
|
-
// understands — `tests/github-copilot-stream-contract.test.ts` sends
|
|
3041
|
-
// with no tools and Copilot answers with a `custom_tool_call` for
|
|
3042
|
-
//
|
|
3043
|
-
//
|
|
3115
|
+
// The guard needs a catalog to compare against, so it stands down when the request omits one.
|
|
3116
|
+
// An explicit empty catalog is still authoritative: it declares that no client tools may be
|
|
3117
|
+
// called. A passthrough request can legitimately omit `tools` entirely and still receive a call
|
|
3118
|
+
// the client understands — `tests/github-copilot-stream-contract.test.ts` sends
|
|
3119
|
+
// `{model, input, stream}` with no tools and Copilot answers with a `custom_tool_call` for
|
|
3120
|
+
// `apply_patch`. Policing an absent catalog truncates that turn. An unreadable body lands there
|
|
3121
|
+
// too because the proxy cannot establish the caller's declared authorization boundary.
|
|
3044
3122
|
const parseOutboundRequestBody = (bodyText: string): Record<string, unknown> | undefined => {
|
|
3045
3123
|
try {
|
|
3046
3124
|
const body = JSON.parse(bodyText) as unknown;
|
|
@@ -3051,16 +3129,46 @@ async function handleResponsesInner(
|
|
|
3051
3129
|
return undefined;
|
|
3052
3130
|
}
|
|
3053
3131
|
};
|
|
3054
|
-
let outboundRequestBody
|
|
3055
|
-
const declaredWireToolNames =
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3132
|
+
let outboundRequestBody: Record<string, unknown> | undefined;
|
|
3133
|
+
const declaredWireToolNames = new Set<string>();
|
|
3134
|
+
const declaredNamelessClientCallTypes = new Set<string>();
|
|
3135
|
+
let undeclaredToolGuardActive = false;
|
|
3136
|
+
const refreshUndeclaredToolGuard = (builtRequest: AdapterRequest): void => {
|
|
3137
|
+
outboundRequestBody = parseOutboundRequestBody(builtRequest.body);
|
|
3138
|
+
declaredWireToolNames.clear();
|
|
3139
|
+
// With no replay prefix the full outbound body belongs to this turn and its normalized
|
|
3140
|
+
// aliases are authoritative. A continuation's outbound body still contains historical
|
|
3141
|
+
// catalogs (and may promote historical tool-search definitions), so it can never widen the
|
|
3142
|
+
// current caller snapshot captured above.
|
|
3143
|
+
if (replayedInputPrefixLength === 0) {
|
|
3144
|
+
for (const name of collectDeclaredWireToolNames(outboundRequestBody)) {
|
|
3145
|
+
declaredWireToolNames.add(name);
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
for (const name of clientDeclaredWireToolNames) declaredWireToolNames.add(name);
|
|
3149
|
+
declaredNamelessClientCallTypes.clear();
|
|
3150
|
+
if (replayedInputPrefixLength === 0) {
|
|
3151
|
+
for (const callType of collectDeclaredNamelessClientCallTypes(outboundRequestBody)) {
|
|
3152
|
+
declaredNamelessClientCallTypes.add(callType);
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
for (const callType of clientDeclaredNamelessCallTypes) {
|
|
3156
|
+
declaredNamelessClientCallTypes.add(callType);
|
|
3157
|
+
}
|
|
3158
|
+
// On an ordinary request these maps capture caller-catalog identities that normalization may
|
|
3159
|
+
// replace on the outbound wire (for example a client image tool becoming hosted). On replay,
|
|
3160
|
+
// however, the parsed maps also contain historical catalog entries, so only the bounded
|
|
3161
|
+
// current-turn wire snapshot above may authorize a call.
|
|
3162
|
+
if (replayedInputPrefixLength === 0) {
|
|
3163
|
+
for (const name of toolBridgeMaps.declaredToolNames) declaredWireToolNames.add(name);
|
|
3164
|
+
}
|
|
3165
|
+
undeclaredToolGuardActive = (
|
|
3166
|
+
declaredWireToolNames.size > 0
|
|
3167
|
+
|| clientDeclaredNamelessCallTypes.size > 0
|
|
3168
|
+
|| clientExplicitWireToolCatalog
|
|
3169
|
+
) && route.provider.authMode !== "forward";
|
|
3170
|
+
};
|
|
3171
|
+
refreshUndeclaredToolGuard(request);
|
|
3064
3172
|
// A refused turn must not seed `previous_response_id` replay. The inspection branch reads the
|
|
3065
3173
|
// untouched upstream stream, so it can still observe a `response.completed` the client never
|
|
3066
3174
|
// received; checking the payload itself rather than a flag shared with the client relay keeps
|
|
@@ -3077,7 +3185,11 @@ async function handleResponsesInner(
|
|
|
3077
3185
|
// provider) every name looks undeclared, and flipping this would stop recording continuation
|
|
3078
3186
|
// state for exactly the passthrough traffic the guard deliberately stands down for.
|
|
3079
3187
|
if (!undeclaredToolGuardActive || inspectionSawUndeclaredTool) return;
|
|
3080
|
-
if (undeclaredToolCallName(
|
|
3188
|
+
if (undeclaredToolCallName(
|
|
3189
|
+
payload,
|
|
3190
|
+
declaredWireToolNames,
|
|
3191
|
+
declaredNamelessClientCallTypes,
|
|
3192
|
+
) !== undefined) {
|
|
3081
3193
|
inspectionSawUndeclaredTool = true;
|
|
3082
3194
|
}
|
|
3083
3195
|
};
|
|
@@ -3086,7 +3198,11 @@ async function handleResponsesInner(
|
|
|
3086
3198
|
if (inspectionSawUndeclaredTool) return;
|
|
3087
3199
|
if (
|
|
3088
3200
|
undeclaredToolGuardActive
|
|
3089
|
-
&& undeclaredToolCallNameInResponse(
|
|
3201
|
+
&& undeclaredToolCallNameInResponse(
|
|
3202
|
+
response,
|
|
3203
|
+
declaredWireToolNames,
|
|
3204
|
+
declaredNamelessClientCallTypes,
|
|
3205
|
+
) !== undefined
|
|
3090
3206
|
) {
|
|
3091
3207
|
return;
|
|
3092
3208
|
}
|
|
@@ -3253,7 +3369,7 @@ async function handleResponsesInner(
|
|
|
3253
3369
|
? request.usageLog.inputTokens
|
|
3254
3370
|
: undefined;
|
|
3255
3371
|
if (passthroughEstimate !== undefined) logCtx.usageLogInputTokens = passthroughEstimate;
|
|
3256
|
-
|
|
3372
|
+
refreshUndeclaredToolGuard(request);
|
|
3257
3373
|
logCtx.providerAdapter = retryAdapter.name;
|
|
3258
3374
|
sealRequestAttemptIdentity(
|
|
3259
3375
|
logCtx.activeAttempt,
|
|
@@ -3363,6 +3479,7 @@ async function handleResponsesInner(
|
|
|
3363
3479
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3364
3480
|
return formatErrorResponse(400, "invalid_request_error", redactSecretString(msg));
|
|
3365
3481
|
}
|
|
3482
|
+
refreshUndeclaredToolGuard(request);
|
|
3366
3483
|
try {
|
|
3367
3484
|
upstreamResponse = await fetchWithTransientRetry(
|
|
3368
3485
|
recovery => {
|
|
@@ -3514,6 +3631,7 @@ async function handleResponsesInner(
|
|
|
3514
3631
|
authCtx = retry.authCtx;
|
|
3515
3632
|
request = retry.request;
|
|
3516
3633
|
refreshRoutedNamespaceToolAliases(request);
|
|
3634
|
+
refreshUndeclaredToolGuard(request);
|
|
3517
3635
|
upstreamResponse = retry.upstreamResponse;
|
|
3518
3636
|
selectedForwardHeaders = retry.selectedForwardHeaders;
|
|
3519
3637
|
// Keep subagent quota-failure health keyed to the account that actually served.
|
|
@@ -3552,13 +3670,21 @@ async function handleResponsesInner(
|
|
|
3552
3670
|
const passthroughCt = headers.get("content-type")?.toLowerCase();
|
|
3553
3671
|
const isEventStream = passthroughCt?.includes("text/event-stream")
|
|
3554
3672
|
|| (upstreamResponse.ok && !!upstreamResponse.body && !passthroughCt && parsed.stream);
|
|
3555
|
-
const
|
|
3673
|
+
const recordTerminalOutcome = codexForwardTerminalOutcomeRecorder(
|
|
3556
3674
|
config,
|
|
3557
3675
|
authCtx,
|
|
3558
3676
|
route.provider,
|
|
3559
3677
|
route.modelId,
|
|
3560
3678
|
logCtx,
|
|
3561
3679
|
);
|
|
3680
|
+
let terminalOutcomeRecorded = false;
|
|
3681
|
+
const terminalRecorder = recordTerminalOutcome
|
|
3682
|
+
? (status: ResponsesTerminalStatus, httpStatusOverride?: number): void => {
|
|
3683
|
+
if (terminalOutcomeRecorded) return;
|
|
3684
|
+
terminalOutcomeRecorded = true;
|
|
3685
|
+
recordTerminalOutcome(status, httpStatusOverride);
|
|
3686
|
+
}
|
|
3687
|
+
: undefined;
|
|
3562
3688
|
const terminalBodyWillRecord = !!terminalRecorder && upstreamResponse.ok && isEventStream;
|
|
3563
3689
|
// Capture quota from upstream response for multi-account tracking
|
|
3564
3690
|
if (usesCodexForwardPoolAuth(authCtx, route.provider)) {
|
|
@@ -3635,19 +3761,13 @@ async function handleResponsesInner(
|
|
|
3635
3761
|
options.onConsumedComboFailure?.(failure);
|
|
3636
3762
|
return failure.response;
|
|
3637
3763
|
}
|
|
3638
|
-
// The
|
|
3639
|
-
//
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
statusText: upstreamResponse.statusText,
|
|
3646
|
-
headers,
|
|
3647
|
-
});
|
|
3648
|
-
} finally {
|
|
3649
|
-
detachPassthroughErrorGuard();
|
|
3650
|
-
}
|
|
3764
|
+
// The bounded reader owns the original body, deadline, abort settlement, and lock.
|
|
3765
|
+
// Unsafe partial data falls back to #452's non-empty status-only JSON.
|
|
3766
|
+
const errorText = await readDisplaySafeErrorText(upstreamResponse, upstream.signal, "");
|
|
3767
|
+
return formatPassthroughUpstreamError(upstreamResponse.status, errorText, {
|
|
3768
|
+
statusText: upstreamResponse.statusText,
|
|
3769
|
+
headers,
|
|
3770
|
+
});
|
|
3651
3771
|
}
|
|
3652
3772
|
|
|
3653
3773
|
// Bun#32111 workaround: passthrough SSE uses tee()+native relay to avoid the
|
|
@@ -3710,8 +3830,12 @@ async function handleResponsesInner(
|
|
|
3710
3830
|
payloadRewrites.length > 0
|
|
3711
3831
|
? payloadRewriteAsBlockRewrite(composeSsePayloadRewrites(...payloadRewrites))
|
|
3712
3832
|
: undefined,
|
|
3713
|
-
routedCustomToolNames.size > 0
|
|
3714
|
-
? createRoutedCustomToolRestoreBlockRewrite(
|
|
3833
|
+
routedCustomToolNames.size > 0 || routedCustomToolRepairNames.size > 0
|
|
3834
|
+
? createRoutedCustomToolRestoreBlockRewrite(
|
|
3835
|
+
routedCustomToolNames,
|
|
3836
|
+
translatorBudget,
|
|
3837
|
+
routedCustomToolRepairNames,
|
|
3838
|
+
)
|
|
3715
3839
|
: undefined,
|
|
3716
3840
|
routedToolSearchNames.size > 0
|
|
3717
3841
|
? createRoutedToolSearchRestoreBlockRewrite(routedToolSearchNames, translatorBudget)
|
|
@@ -3726,7 +3850,10 @@ async function handleResponsesInner(
|
|
|
3726
3850
|
// Last: every rewrite above can still rename or reshape a call item, so the guard must
|
|
3727
3851
|
// compare the names the client will actually receive against the declared catalog.
|
|
3728
3852
|
undeclaredToolGuardActive
|
|
3729
|
-
? createUndeclaredToolCallGuardBlockRewrite(
|
|
3853
|
+
? createUndeclaredToolCallGuardBlockRewrite(
|
|
3854
|
+
declaredWireToolNames,
|
|
3855
|
+
declaredNamelessClientCallTypes,
|
|
3856
|
+
)
|
|
3730
3857
|
: undefined,
|
|
3731
3858
|
].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
|
|
3732
3859
|
const clientBlockRewrite = blockRewrites.length > 0
|
|
@@ -3916,6 +4043,7 @@ async function handleResponsesInner(
|
|
|
3916
4043
|
const restored = restoreRoutedCustomCallsInJson(
|
|
3917
4044
|
restoredNamespace,
|
|
3918
4045
|
routedCustomToolNames,
|
|
4046
|
+
routedCustomToolRepairNames,
|
|
3919
4047
|
);
|
|
3920
4048
|
const restoredToolSearch = restoreRoutedToolSearchCallsInJson(
|
|
3921
4049
|
restored,
|
|
@@ -3942,7 +4070,11 @@ async function handleResponsesInner(
|
|
|
3942
4070
|
if (undeclaredToolGuardActive) {
|
|
3943
4071
|
const undeclared = (() => {
|
|
3944
4072
|
try {
|
|
3945
|
-
return undeclaredToolCallNameInResponse(
|
|
4073
|
+
return undeclaredToolCallNameInResponse(
|
|
4074
|
+
JSON.parse(clientJson),
|
|
4075
|
+
declaredWireToolNames,
|
|
4076
|
+
declaredNamelessClientCallTypes,
|
|
4077
|
+
);
|
|
3946
4078
|
} catch {
|
|
3947
4079
|
return undefined;
|
|
3948
4080
|
}
|
|
@@ -4061,13 +4193,23 @@ async function handleResponsesInner(
|
|
|
4061
4193
|
// - non-runTurn: web-search wins over image when both eligible (documented priority)
|
|
4062
4194
|
// - runTurn: image bridge may run (it supports runTurn); web-search is skipped so runTurn
|
|
4063
4195
|
// can proceed for web-search-only turns
|
|
4064
|
-
const wsPlan = !routedCompaction
|
|
4065
|
-
? planWebSearch(config, parsed, false, route.provider, route.modelId, openAiSidecar)
|
|
4066
|
-
: undefined;
|
|
4067
4196
|
const imgPlan = !routedCompaction ? await planImageBridge(config, parsed, route.provider) : undefined;
|
|
4068
4197
|
const vidPlan = !routedCompaction ? await planVideoBridge(config, parsed, route.provider) : undefined;
|
|
4069
|
-
const
|
|
4070
|
-
|
|
4198
|
+
const hasMediaPlan = !!(imgPlan || vidPlan);
|
|
4199
|
+
// A media plan is not enough to suppress Gemini 2.x grounding: both bridges inject tools only
|
|
4200
|
+
// on streaming turns. This is the potential-injection value used to resolve sidecar precedence.
|
|
4201
|
+
const mediaMayInject = mediaBridgeWillRun(hasMediaPlan, false, !!adapter.runTurn, parsed.stream);
|
|
4202
|
+
const wsPlan = !routedCompaction
|
|
4203
|
+
? planWebSearch(config, parsed, false, route.provider, route.modelId, openAiSidecar, mediaMayInject)
|
|
4204
|
+
: undefined;
|
|
4205
|
+
const webSearchWinsMedia = !!wsPlan && !adapter.runTurn;
|
|
4206
|
+
const mediaWillRun = mediaBridgeWillRun(hasMediaPlan, !!wsPlan, !!adapter.runTurn, parsed.stream);
|
|
4207
|
+
const ccaInTurnGrounding = !routedCompaction
|
|
4208
|
+
? resolveCcaInTurnGrounding(config, parsed, false, route.provider, route.modelId, mediaWillRun)
|
|
4209
|
+
: undefined;
|
|
4210
|
+
if (ccaInTurnGrounding) parsed._ccaInTurnGrounding = ccaInTurnGrounding;
|
|
4211
|
+
const canRunWebSearch = webSearchWinsMedia && !ccaInTurnGrounding;
|
|
4212
|
+
if (hasMediaPlan && webSearchWinsMedia) {
|
|
4071
4213
|
// Web search takes priority when both are active — the media bridge cannot run
|
|
4072
4214
|
// alongside runWithWebSearch. Surface a runtime signal so the user knows their
|
|
4073
4215
|
// configured video/image bridge was skipped for this turn, rather than silently
|
|
@@ -4075,7 +4217,7 @@ async function handleResponsesInner(
|
|
|
4075
4217
|
if (vidPlan) console.warn("[videos] video bridge skipped: web search is active for this turn");
|
|
4076
4218
|
if (imgPlan) console.warn("[images] image bridge skipped: web search is active for this turn");
|
|
4077
4219
|
}
|
|
4078
|
-
if (
|
|
4220
|
+
if (mediaWillRun || (imgPlan && !parsed.stream && !webSearchWinsMedia)) {
|
|
4079
4221
|
// The image bridge detects a hosted image_generation tool and requires streaming.
|
|
4080
4222
|
// The video bridge activates from config and injects a tool — it also needs streaming
|
|
4081
4223
|
// (the loop returns SSE). For video-only (no imgPlan) on a non-streaming request, skip
|
|
@@ -4448,9 +4590,11 @@ async function handleResponsesInner(
|
|
|
4448
4590
|
);
|
|
4449
4591
|
const bridgeTurnAc = new AbortController();
|
|
4450
4592
|
const trackedSse = trackStreamLifetime(sseStream, bridgeTurnAc, undefined, options.turnAdmissionLease);
|
|
4451
|
-
|
|
4593
|
+
const response = new Response(trackedSse, {
|
|
4452
4594
|
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no" },
|
|
4453
4595
|
});
|
|
4596
|
+
runTurnAdapterSseResponses.add(response);
|
|
4597
|
+
return response;
|
|
4454
4598
|
}
|
|
4455
4599
|
|
|
4456
4600
|
await runTurn();
|
|
@@ -5083,14 +5227,16 @@ async function handleResponsesInner(
|
|
|
5083
5227
|
options.onConsumedComboFailure?.(failure);
|
|
5084
5228
|
return failure.response;
|
|
5085
5229
|
}
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5230
|
+
let errorText: string;
|
|
5231
|
+
try {
|
|
5232
|
+
errorText = await readDisplaySafeErrorText(
|
|
5233
|
+
upstreamResponse,
|
|
5234
|
+
upstream.signal,
|
|
5235
|
+
"unknown error",
|
|
5236
|
+
);
|
|
5237
|
+
} finally {
|
|
5238
|
+
cleanupUpstreamAbort();
|
|
5239
|
+
}
|
|
5094
5240
|
if (!isFixedCodexAccount(authCtx)) {
|
|
5095
5241
|
recordSubagentQuotaFailureForThreadSpawn(
|
|
5096
5242
|
req.headers,
|
|
@@ -5489,12 +5635,7 @@ async function handleResponsesInner(
|
|
|
5489
5635
|
}
|
|
5490
5636
|
|
|
5491
5637
|
if (!response.ok) {
|
|
5492
|
-
|
|
5493
|
-
// is still a Bun fetch body, and an abort landing before `.text()` attaches its reader
|
|
5494
|
-
// orphans the internal rejection.
|
|
5495
|
-
const detachContinuationErrorGuard = cancelBodyOnAbort(response.body, upstream.signal);
|
|
5496
|
-
const errorText = await response.text().catch(() => "unknown error");
|
|
5497
|
-
detachContinuationErrorGuard();
|
|
5638
|
+
const errorText = await readDisplaySafeErrorText(response, upstream.signal, "unknown error");
|
|
5498
5639
|
yield {
|
|
5499
5640
|
type: "error",
|
|
5500
5641
|
status: response.status,
|