@bitkyc08/opencodex 2.31.0 → 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-DkcRs1fL.js → index-BJwu-ldX.js} +14 -14
- package/gui/dist/assets/index-DcBbHIAz.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.ts +2 -3
- package/src/adapters/cursor/tool-definitions.ts +1 -1
- package/src/adapters/google.ts +6 -7
- 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/parsing.ts +18 -0
- 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 +75 -812
- package/src/generated/compatibility-version.json +135 -79
- 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/lib/bounded-body.ts +14 -2
- package/src/lib/process-control.ts +2 -1
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/health.ts +1 -1
- 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/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 +225 -94
- 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/request.ts +6 -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/gui/dist/assets/index-CH7ncHCC.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";
|
|
@@ -113,16 +115,12 @@ import {
|
|
|
113
115
|
applyCodexAuthContextToProvider,
|
|
114
116
|
codexPoolAffinityKey,
|
|
115
117
|
CodexAccountCooldownError,
|
|
116
|
-
codexMainProfileDrainingResponse,
|
|
117
|
-
cooldownErrorResponse,
|
|
118
118
|
CodexAuthContextError,
|
|
119
|
-
CodexDirectAuthenticationError,
|
|
120
119
|
CodexMainProfileDrainingError,
|
|
121
120
|
CodexPoolAuthenticationError,
|
|
122
121
|
CodexThreadAffinityExpiredError,
|
|
123
122
|
headersForCodexAuthContext,
|
|
124
123
|
materializeCodexUpstreamAuth,
|
|
125
|
-
CodexMainSubstitutionUnavailableError,
|
|
126
124
|
isCodexAuthContextUsable,
|
|
127
125
|
resolveCodexAuthContext,
|
|
128
126
|
codexProbeLeaseId,
|
|
@@ -225,6 +223,7 @@ import {
|
|
|
225
223
|
import {
|
|
226
224
|
conversationIdFromResponsesRequest,
|
|
227
225
|
normalizeLogConversationId,
|
|
226
|
+
reasoningReplayConversationIdFromResponsesRequest,
|
|
228
227
|
sessionIdHeaderFromRequest,
|
|
229
228
|
} from "../request-log-conversation";
|
|
230
229
|
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
@@ -271,6 +270,7 @@ import { createResponsesModelPayloadRewrite, rewriteResponsesModelJson } from ".
|
|
|
271
270
|
import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
|
|
272
271
|
|
|
273
272
|
import { buildToolBridgeMaps, collabSurface, injectDeveloperMessage, multiAgentGuidanceText } from "./collaboration";
|
|
273
|
+
import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
|
|
274
274
|
import { hasUnreadableEncryptedAgentTask, looksLikeBackendCiphertext, sanitizeEncryptedContentInPlace } from "./encrypted-payload";
|
|
275
275
|
import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel } from "./fetch-helpers";
|
|
276
276
|
import { classifyTransportFailureKind, transportErrorCode } from "../../lib/upstream-reachability";
|
|
@@ -306,8 +306,11 @@ import {
|
|
|
306
306
|
type RoutedNamespaceToolAliases,
|
|
307
307
|
} from "../../responses/namespace-tool-compat";
|
|
308
308
|
import {
|
|
309
|
+
collectDeclaredNamelessClientCallTypes,
|
|
309
310
|
collectDeclaredWireToolNames,
|
|
310
311
|
createUndeclaredToolCallGuardBlockRewrite,
|
|
312
|
+
currentTurnWireToolCatalogBody,
|
|
313
|
+
hasExplicitWireToolCatalog,
|
|
311
314
|
undeclaredToolCallMessage,
|
|
312
315
|
undeclaredToolCallName,
|
|
313
316
|
undeclaredToolCallNameInResponse,
|
|
@@ -319,6 +322,12 @@ import {
|
|
|
319
322
|
emptyCompletionRetryEnabled,
|
|
320
323
|
guardEmptyCompletionEventStream,
|
|
321
324
|
} from "./empty-completion-guard";
|
|
325
|
+
import { preflightComboStreamResponse } from "./combo-stream-preflight";
|
|
326
|
+
|
|
327
|
+
// runTurn adapters own an event queue and perform their combo preflight before
|
|
328
|
+
// bridging. A second byte-stream reader would reinterpret that transport's
|
|
329
|
+
// already-committed event boundary and can replay custom adapter work.
|
|
330
|
+
const runTurnAdapterSseResponses = new WeakSet<Response>();
|
|
322
331
|
|
|
323
332
|
/**
|
|
324
333
|
* Adapters whose continuation state must survive Codex's store:false requests.
|
|
@@ -515,6 +524,9 @@ function bindRouteReasoningReplayScope(args: {
|
|
|
515
524
|
if (reasoningReplayServingIdentityChanged(parsed._reasoningReplayScope)) {
|
|
516
525
|
parsed._stripReasoningEncryptedContent = true;
|
|
517
526
|
}
|
|
527
|
+
if (reasoningReplayOpaqueBlobRejectionMemoized(parsed._reasoningReplayScope)) {
|
|
528
|
+
parsed._stripReasoningEncryptedContent = true;
|
|
529
|
+
}
|
|
518
530
|
bindProviderContinuationForRoute(parsed, continuationOwner);
|
|
519
531
|
}
|
|
520
532
|
|
|
@@ -628,6 +640,26 @@ async function opaqueBlobRejectionBodyForRecovery(
|
|
|
628
640
|
}
|
|
629
641
|
}
|
|
630
642
|
|
|
643
|
+
/**
|
|
644
|
+
* Materialize an upstream error body only when the bounded reader observed a complete,
|
|
645
|
+
* display-safe payload. Partial timeout and over-limit prefixes are attacker-controlled,
|
|
646
|
+
* so callers keep their existing status-only fallback instead.
|
|
647
|
+
*/
|
|
648
|
+
export async function readDisplaySafeErrorText(
|
|
649
|
+
response: Response,
|
|
650
|
+
signal: AbortSignal,
|
|
651
|
+
fallback: string,
|
|
652
|
+
): Promise<string> {
|
|
653
|
+
try {
|
|
654
|
+
const body = await readBoundedResponseBody(response, { signal });
|
|
655
|
+
return body.displaySafe ? body.text : fallback;
|
|
656
|
+
} catch {
|
|
657
|
+
// Preserve the former Response.text().catch(fallback) contract. Request-abort
|
|
658
|
+
// classification remains owned by the surrounding response pipeline.
|
|
659
|
+
return fallback;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
631
663
|
function prepareOpaqueBlobRecovery(parsed: OcxParsedRequest): void {
|
|
632
664
|
parsed._stripReasoningEncryptedContent = true;
|
|
633
665
|
}
|
|
@@ -668,9 +700,20 @@ async function attemptOpaqueBlobRecovery(
|
|
|
668
700
|
}
|
|
669
701
|
|
|
670
702
|
args.guard.attempted = true;
|
|
703
|
+
const rejectedScope = args.parsed._reasoningReplayScope
|
|
704
|
+
? {
|
|
705
|
+
clientThreadId: args.parsed._reasoningReplayScope.clientThreadId,
|
|
706
|
+
...(args.parsed._reasoningReplayScope.current
|
|
707
|
+
? { current: { ...args.parsed._reasoningReplayScope.current } }
|
|
708
|
+
: {}),
|
|
709
|
+
}
|
|
710
|
+
: undefined;
|
|
671
711
|
prepareOpaqueBlobRecovery(args.parsed);
|
|
672
712
|
try { void args.response.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
673
713
|
const result = await rebuild("opaque-blob-rejection");
|
|
714
|
+
if (!("failed" in result) && result.ok) {
|
|
715
|
+
rememberReasoningReplayOpaqueBlobRejection(rejectedScope);
|
|
716
|
+
}
|
|
674
717
|
return "failed" in result
|
|
675
718
|
? { kind: "failed", response: result.failed }
|
|
676
719
|
: { kind: "recovered", response: result };
|
|
@@ -1495,44 +1538,20 @@ async function resolveResponsesCodexAuth(
|
|
|
1495
1538
|
substituteMainCredential,
|
|
1496
1539
|
};
|
|
1497
1540
|
} 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
1541
|
if (err instanceof CodexAuthContextError) {
|
|
1511
1542
|
const safeAccountLabel = route.codexAccountNamespace
|
|
1512
1543
|
? `${route.providerName}-${route.codexAccountNamespace}`
|
|
1513
1544
|
: formatCodexProviderForLog(route.providerName, err.accountId, config);
|
|
1514
1545
|
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
1546
|
}
|
|
1526
1547
|
if (err instanceof ForwardAdmissionCredentialError) {
|
|
1527
1548
|
return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
|
|
1528
1549
|
}
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
};
|
|
1535
|
-
}
|
|
1550
|
+
const response = mapCodexAuthContextErrorToResponse(err, {
|
|
1551
|
+
accountSelector: route.codexAccountNamespace,
|
|
1552
|
+
now: Date.now(),
|
|
1553
|
+
});
|
|
1554
|
+
if (response) return { ok: false, response };
|
|
1536
1555
|
throw err;
|
|
1537
1556
|
}
|
|
1538
1557
|
}
|
|
@@ -1900,6 +1919,31 @@ export async function handleComboResponses(
|
|
|
1900
1919
|
return clientCancelledResponse();
|
|
1901
1920
|
}
|
|
1902
1921
|
|
|
1922
|
+
if (response.ok && !runTurnAdapterSseResponses.has(response)) {
|
|
1923
|
+
const nativePassthrough = isNativePassthroughSseResponse(response);
|
|
1924
|
+
const eagerRelay = isEagerRelaySseResponse(response);
|
|
1925
|
+
let preflight;
|
|
1926
|
+
try {
|
|
1927
|
+
preflight = await preflightComboStreamResponse(response, childLog);
|
|
1928
|
+
} catch (error) {
|
|
1929
|
+
callbackGate.discard();
|
|
1930
|
+
if (options.abortSignal?.aborted) {
|
|
1931
|
+
retainCancelledAttempt();
|
|
1932
|
+
return clientCancelledResponse();
|
|
1933
|
+
}
|
|
1934
|
+
throw error;
|
|
1935
|
+
}
|
|
1936
|
+
if (preflight.kind === "failed") {
|
|
1937
|
+
callbackGate.discard();
|
|
1938
|
+
terminalRecorder?.("failed", preflight.response.status);
|
|
1939
|
+
response = preflight.response;
|
|
1940
|
+
} else {
|
|
1941
|
+
response = preflight.response;
|
|
1942
|
+
if (nativePassthrough) markNativePassthroughSseResponse(response);
|
|
1943
|
+
if (eagerRelay) markEagerRelaySseResponse(response);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1903
1947
|
if (response.ok) {
|
|
1904
1948
|
sealRequestAttemptIdentity(
|
|
1905
1949
|
attempt,
|
|
@@ -1955,7 +1999,7 @@ export async function handleComboResponses(
|
|
|
1955
1999
|
);
|
|
1956
2000
|
finishRequestAttempt(
|
|
1957
2001
|
attempt,
|
|
1958
|
-
response.status,
|
|
2002
|
+
failure.response.status,
|
|
1959
2003
|
Date.now() - started,
|
|
1960
2004
|
failure.usage,
|
|
1961
2005
|
);
|
|
@@ -1969,7 +2013,7 @@ export async function handleComboResponses(
|
|
|
1969
2013
|
return lastFailure;
|
|
1970
2014
|
}
|
|
1971
2015
|
console.warn(
|
|
1972
|
-
`[combo] ${comboId}: ${targetKey(pick.target)} failed with ${response.status} after ${Date.now() - started}ms`,
|
|
2016
|
+
`[combo] ${comboId}: ${targetKey(pick.target)} failed with ${failure.response.status} after ${Date.now() - started}ms`,
|
|
1973
2017
|
);
|
|
1974
2018
|
const nextPick = advanceComboAfterFailure(config, pick, {
|
|
1975
2019
|
retryAfter: failure.retryAfter,
|
|
@@ -2185,7 +2229,6 @@ async function handleResponsesInner(
|
|
|
2185
2229
|
if (providerContinuationCandidate) parsed._providerContinuationCandidate = providerContinuationCandidate;
|
|
2186
2230
|
if (inboundClientThreadId) {
|
|
2187
2231
|
parsed._clientThreadId = inboundClientThreadId;
|
|
2188
|
-
parsed._reasoningReplayScope = { clientThreadId: inboundClientThreadId };
|
|
2189
2232
|
} else if (
|
|
2190
2233
|
options.inboundWire === "anthropic"
|
|
2191
2234
|
&& options.promptCacheKeyIsSharedCohort !== true
|
|
@@ -2221,15 +2264,31 @@ async function handleResponsesInner(
|
|
|
2221
2264
|
...(force ? { force: true } : {}),
|
|
2222
2265
|
...(parsed._clientThreadId ? { clientThreadId: parsed._clientThreadId } : {}),
|
|
2223
2266
|
});
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2267
|
+
const resolvedConversationId = conversationIdFromResponsesRequest({
|
|
2268
|
+
clientThreadId: parsed._clientThreadId,
|
|
2269
|
+
sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
|
|
2270
|
+
threadIdHeader: req.headers.get("thread-id"),
|
|
2271
|
+
cursorConversationId: parsed._cursorConversationId,
|
|
2272
|
+
});
|
|
2273
|
+
// _clientThreadId remains the routing/continuation identity supplied by Codex. Replay state uses
|
|
2274
|
+
// a dedicated raw conversation namespace so mixed headers that carry the same identity still
|
|
2275
|
+
// match, and a shared/synthetic session_id cannot coalesce distinct thread/Cursor conversations.
|
|
2276
|
+
// Keep an Anthropic prompt_cache_key scope already bound above (#1735/#1926).
|
|
2277
|
+
if (!parsed._reasoningReplayScope) {
|
|
2278
|
+
const reasoningReplayConversationId = reasoningReplayConversationIdFromResponsesRequest({
|
|
2228
2279
|
clientThreadId: parsed._clientThreadId,
|
|
2229
|
-
sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
|
|
2230
2280
|
threadIdHeader: req.headers.get("thread-id"),
|
|
2231
2281
|
cursorConversationId: parsed._cursorConversationId,
|
|
2282
|
+
sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
|
|
2232
2283
|
});
|
|
2284
|
+
if (reasoningReplayConversationId) {
|
|
2285
|
+
parsed._reasoningReplayScope = { clientThreadId: reasoningReplayConversationId };
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
// Prefer a pre-populated id (routed Claude) over Responses headers that may be
|
|
2289
|
+
// absent or synthetically injected (session_id from prompt_cache_key).
|
|
2290
|
+
if (!logCtx.conversationId) {
|
|
2291
|
+
logCtx.conversationId = resolvedConversationId;
|
|
2233
2292
|
}
|
|
2234
2293
|
logCtx.requestedModel = parsed.modelId;
|
|
2235
2294
|
logCtx.requestedEffort = parsed.options.reasoning;
|
|
@@ -2848,6 +2907,7 @@ async function handleResponsesInner(
|
|
|
2848
2907
|
? new Map<string, { namespace: string; name: string }>()
|
|
2849
2908
|
: imageGenToolCallAliases(toolBridgeMaps.toolNsMap, parsed._rawBody, translatorBudget);
|
|
2850
2909
|
const routedCustomToolNames = new Set<string>();
|
|
2910
|
+
const routedCustomToolRepairNames = new Set<string>();
|
|
2851
2911
|
const routedToolSearchNames = new Set<string>();
|
|
2852
2912
|
// Local continuation cache for the ChatGPT passthrough. Codex WS turns chain with
|
|
2853
2913
|
// previous_response_id, ocx converts them to internal HTTP requests, and the ChatGPT Codex
|
|
@@ -2870,6 +2930,18 @@ async function handleResponsesInner(
|
|
|
2870
2930
|
+ `(model ${parsed.modelId}); forwarding without it — earlier turns may be missing from this request`,
|
|
2871
2931
|
);
|
|
2872
2932
|
}
|
|
2933
|
+
// Preserve the caller's readable catalog boundary before provider-specific normalization can
|
|
2934
|
+
// remove an unsupported final entry (for example xAI cached-only web search).
|
|
2935
|
+
const replayedInputPrefixLength = parsed._replayPrefixLen ?? 0;
|
|
2936
|
+
const clientToolAuthorizationBody = currentTurnWireToolCatalogBody(
|
|
2937
|
+
parsed._rawBody,
|
|
2938
|
+
replayedInputPrefixLength,
|
|
2939
|
+
);
|
|
2940
|
+
const clientExplicitWireToolCatalog = hasExplicitWireToolCatalog(clientToolAuthorizationBody);
|
|
2941
|
+
const clientDeclaredWireToolNames = collectDeclaredWireToolNames(clientToolAuthorizationBody);
|
|
2942
|
+
const clientDeclaredNamelessCallTypes = collectDeclaredNamelessClientCallTypes(
|
|
2943
|
+
clientToolAuthorizationBody,
|
|
2944
|
+
);
|
|
2873
2945
|
let request: Awaited<ReturnType<typeof adapter.buildRequest>>;
|
|
2874
2946
|
try {
|
|
2875
2947
|
request = await adapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
|
|
@@ -2891,6 +2963,12 @@ async function handleResponsesInner(
|
|
|
2891
2963
|
|| toolBridgeMaps.toolNsMap.get(name)?.freeform === true
|
|
2892
2964
|
) routedCustomToolNames.add(name);
|
|
2893
2965
|
}
|
|
2966
|
+
for (const name of request.routedCustomToolRepairNames ?? []) {
|
|
2967
|
+
if (
|
|
2968
|
+
toolBridgeMaps.freeformToolNames.has(name)
|
|
2969
|
+
|| toolBridgeMaps.toolNsMap.get(name)?.freeform === true
|
|
2970
|
+
) routedCustomToolRepairNames.add(name);
|
|
2971
|
+
}
|
|
2894
2972
|
}
|
|
2895
2973
|
for (const name of request.convertedRoutedToolSearchNames ?? []) {
|
|
2896
2974
|
// The adapter already keeps this set empty when tool_choice forbids the private search.
|
|
@@ -2906,13 +2984,13 @@ async function handleResponsesInner(
|
|
|
2906
2984
|
// call it cannot execute, and the turn showed a bare `aborted` with the file untouched.
|
|
2907
2985
|
// Forward auth is the canonical ChatGPT backend speaking Codex's own protocol rather than a
|
|
2908
2986
|
// 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
|
-
//
|
|
2987
|
+
// The guard needs a catalog to compare against, so it stands down when the request omits one.
|
|
2988
|
+
// An explicit empty catalog is still authoritative: it declares that no client tools may be
|
|
2989
|
+
// called. A passthrough request can legitimately omit `tools` entirely and still receive a call
|
|
2990
|
+
// the client understands — `tests/github-copilot-stream-contract.test.ts` sends
|
|
2991
|
+
// `{model, input, stream}` with no tools and Copilot answers with a `custom_tool_call` for
|
|
2992
|
+
// `apply_patch`. Policing an absent catalog truncates that turn. An unreadable body lands there
|
|
2993
|
+
// too because the proxy cannot establish the caller's declared authorization boundary.
|
|
2916
2994
|
const parseOutboundRequestBody = (bodyText: string): Record<string, unknown> | undefined => {
|
|
2917
2995
|
try {
|
|
2918
2996
|
const body = JSON.parse(bodyText) as unknown;
|
|
@@ -2923,16 +3001,46 @@ async function handleResponsesInner(
|
|
|
2923
3001
|
return undefined;
|
|
2924
3002
|
}
|
|
2925
3003
|
};
|
|
2926
|
-
let outboundRequestBody
|
|
2927
|
-
const declaredWireToolNames =
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
3004
|
+
let outboundRequestBody: Record<string, unknown> | undefined;
|
|
3005
|
+
const declaredWireToolNames = new Set<string>();
|
|
3006
|
+
const declaredNamelessClientCallTypes = new Set<string>();
|
|
3007
|
+
let undeclaredToolGuardActive = false;
|
|
3008
|
+
const refreshUndeclaredToolGuard = (builtRequest: AdapterRequest): void => {
|
|
3009
|
+
outboundRequestBody = parseOutboundRequestBody(builtRequest.body);
|
|
3010
|
+
declaredWireToolNames.clear();
|
|
3011
|
+
// With no replay prefix the full outbound body belongs to this turn and its normalized
|
|
3012
|
+
// aliases are authoritative. A continuation's outbound body still contains historical
|
|
3013
|
+
// catalogs (and may promote historical tool-search definitions), so it can never widen the
|
|
3014
|
+
// current caller snapshot captured above.
|
|
3015
|
+
if (replayedInputPrefixLength === 0) {
|
|
3016
|
+
for (const name of collectDeclaredWireToolNames(outboundRequestBody)) {
|
|
3017
|
+
declaredWireToolNames.add(name);
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
for (const name of clientDeclaredWireToolNames) declaredWireToolNames.add(name);
|
|
3021
|
+
declaredNamelessClientCallTypes.clear();
|
|
3022
|
+
if (replayedInputPrefixLength === 0) {
|
|
3023
|
+
for (const callType of collectDeclaredNamelessClientCallTypes(outboundRequestBody)) {
|
|
3024
|
+
declaredNamelessClientCallTypes.add(callType);
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
for (const callType of clientDeclaredNamelessCallTypes) {
|
|
3028
|
+
declaredNamelessClientCallTypes.add(callType);
|
|
3029
|
+
}
|
|
3030
|
+
// On an ordinary request these maps capture caller-catalog identities that normalization may
|
|
3031
|
+
// replace on the outbound wire (for example a client image tool becoming hosted). On replay,
|
|
3032
|
+
// however, the parsed maps also contain historical catalog entries, so only the bounded
|
|
3033
|
+
// current-turn wire snapshot above may authorize a call.
|
|
3034
|
+
if (replayedInputPrefixLength === 0) {
|
|
3035
|
+
for (const name of toolBridgeMaps.declaredToolNames) declaredWireToolNames.add(name);
|
|
3036
|
+
}
|
|
3037
|
+
undeclaredToolGuardActive = (
|
|
3038
|
+
declaredWireToolNames.size > 0
|
|
3039
|
+
|| clientDeclaredNamelessCallTypes.size > 0
|
|
3040
|
+
|| clientExplicitWireToolCatalog
|
|
3041
|
+
) && route.provider.authMode !== "forward";
|
|
3042
|
+
};
|
|
3043
|
+
refreshUndeclaredToolGuard(request);
|
|
2936
3044
|
// A refused turn must not seed `previous_response_id` replay. The inspection branch reads the
|
|
2937
3045
|
// untouched upstream stream, so it can still observe a `response.completed` the client never
|
|
2938
3046
|
// received; checking the payload itself rather than a flag shared with the client relay keeps
|
|
@@ -2949,7 +3057,11 @@ async function handleResponsesInner(
|
|
|
2949
3057
|
// provider) every name looks undeclared, and flipping this would stop recording continuation
|
|
2950
3058
|
// state for exactly the passthrough traffic the guard deliberately stands down for.
|
|
2951
3059
|
if (!undeclaredToolGuardActive || inspectionSawUndeclaredTool) return;
|
|
2952
|
-
if (undeclaredToolCallName(
|
|
3060
|
+
if (undeclaredToolCallName(
|
|
3061
|
+
payload,
|
|
3062
|
+
declaredWireToolNames,
|
|
3063
|
+
declaredNamelessClientCallTypes,
|
|
3064
|
+
) !== undefined) {
|
|
2953
3065
|
inspectionSawUndeclaredTool = true;
|
|
2954
3066
|
}
|
|
2955
3067
|
};
|
|
@@ -2958,7 +3070,11 @@ async function handleResponsesInner(
|
|
|
2958
3070
|
if (inspectionSawUndeclaredTool) return;
|
|
2959
3071
|
if (
|
|
2960
3072
|
undeclaredToolGuardActive
|
|
2961
|
-
&& undeclaredToolCallNameInResponse(
|
|
3073
|
+
&& undeclaredToolCallNameInResponse(
|
|
3074
|
+
response,
|
|
3075
|
+
declaredWireToolNames,
|
|
3076
|
+
declaredNamelessClientCallTypes,
|
|
3077
|
+
) !== undefined
|
|
2962
3078
|
) {
|
|
2963
3079
|
return;
|
|
2964
3080
|
}
|
|
@@ -3125,7 +3241,7 @@ async function handleResponsesInner(
|
|
|
3125
3241
|
? request.usageLog.inputTokens
|
|
3126
3242
|
: undefined;
|
|
3127
3243
|
if (passthroughEstimate !== undefined) logCtx.usageLogInputTokens = passthroughEstimate;
|
|
3128
|
-
|
|
3244
|
+
refreshUndeclaredToolGuard(request);
|
|
3129
3245
|
logCtx.providerAdapter = retryAdapter.name;
|
|
3130
3246
|
sealRequestAttemptIdentity(
|
|
3131
3247
|
logCtx.activeAttempt,
|
|
@@ -3235,6 +3351,7 @@ async function handleResponsesInner(
|
|
|
3235
3351
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3236
3352
|
return formatErrorResponse(400, "invalid_request_error", redactSecretString(msg));
|
|
3237
3353
|
}
|
|
3354
|
+
refreshUndeclaredToolGuard(request);
|
|
3238
3355
|
try {
|
|
3239
3356
|
upstreamResponse = await fetchWithTransientRetry(
|
|
3240
3357
|
recovery => {
|
|
@@ -3386,6 +3503,7 @@ async function handleResponsesInner(
|
|
|
3386
3503
|
authCtx = retry.authCtx;
|
|
3387
3504
|
request = retry.request;
|
|
3388
3505
|
refreshRoutedNamespaceToolAliases(request);
|
|
3506
|
+
refreshUndeclaredToolGuard(request);
|
|
3389
3507
|
upstreamResponse = retry.upstreamResponse;
|
|
3390
3508
|
selectedForwardHeaders = retry.selectedForwardHeaders;
|
|
3391
3509
|
// Keep subagent quota-failure health keyed to the account that actually served.
|
|
@@ -3424,13 +3542,21 @@ async function handleResponsesInner(
|
|
|
3424
3542
|
const passthroughCt = headers.get("content-type")?.toLowerCase();
|
|
3425
3543
|
const isEventStream = passthroughCt?.includes("text/event-stream")
|
|
3426
3544
|
|| (upstreamResponse.ok && !!upstreamResponse.body && !passthroughCt && parsed.stream);
|
|
3427
|
-
const
|
|
3545
|
+
const recordTerminalOutcome = codexForwardTerminalOutcomeRecorder(
|
|
3428
3546
|
config,
|
|
3429
3547
|
authCtx,
|
|
3430
3548
|
route.provider,
|
|
3431
3549
|
route.modelId,
|
|
3432
3550
|
logCtx,
|
|
3433
3551
|
);
|
|
3552
|
+
let terminalOutcomeRecorded = false;
|
|
3553
|
+
const terminalRecorder = recordTerminalOutcome
|
|
3554
|
+
? (status: ResponsesTerminalStatus, httpStatusOverride?: number): void => {
|
|
3555
|
+
if (terminalOutcomeRecorded) return;
|
|
3556
|
+
terminalOutcomeRecorded = true;
|
|
3557
|
+
recordTerminalOutcome(status, httpStatusOverride);
|
|
3558
|
+
}
|
|
3559
|
+
: undefined;
|
|
3434
3560
|
const terminalBodyWillRecord = !!terminalRecorder && upstreamResponse.ok && isEventStream;
|
|
3435
3561
|
// Capture quota from upstream response for multi-account tracking
|
|
3436
3562
|
if (usesCodexForwardPoolAuth(authCtx, route.provider)) {
|
|
@@ -3507,19 +3633,13 @@ async function handleResponsesInner(
|
|
|
3507
3633
|
options.onConsumedComboFailure?.(failure);
|
|
3508
3634
|
return failure.response;
|
|
3509
3635
|
}
|
|
3510
|
-
// The
|
|
3511
|
-
//
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
statusText: upstreamResponse.statusText,
|
|
3518
|
-
headers,
|
|
3519
|
-
});
|
|
3520
|
-
} finally {
|
|
3521
|
-
detachPassthroughErrorGuard();
|
|
3522
|
-
}
|
|
3636
|
+
// The bounded reader owns the original body, deadline, abort settlement, and lock.
|
|
3637
|
+
// Unsafe partial data falls back to #452's non-empty status-only JSON.
|
|
3638
|
+
const errorText = await readDisplaySafeErrorText(upstreamResponse, upstream.signal, "");
|
|
3639
|
+
return formatPassthroughUpstreamError(upstreamResponse.status, errorText, {
|
|
3640
|
+
statusText: upstreamResponse.statusText,
|
|
3641
|
+
headers,
|
|
3642
|
+
});
|
|
3523
3643
|
}
|
|
3524
3644
|
|
|
3525
3645
|
// Bun#32111 workaround: passthrough SSE uses tee()+native relay to avoid the
|
|
@@ -3582,8 +3702,12 @@ async function handleResponsesInner(
|
|
|
3582
3702
|
payloadRewrites.length > 0
|
|
3583
3703
|
? payloadRewriteAsBlockRewrite(composeSsePayloadRewrites(...payloadRewrites))
|
|
3584
3704
|
: undefined,
|
|
3585
|
-
routedCustomToolNames.size > 0
|
|
3586
|
-
? createRoutedCustomToolRestoreBlockRewrite(
|
|
3705
|
+
routedCustomToolNames.size > 0 || routedCustomToolRepairNames.size > 0
|
|
3706
|
+
? createRoutedCustomToolRestoreBlockRewrite(
|
|
3707
|
+
routedCustomToolNames,
|
|
3708
|
+
translatorBudget,
|
|
3709
|
+
routedCustomToolRepairNames,
|
|
3710
|
+
)
|
|
3587
3711
|
: undefined,
|
|
3588
3712
|
routedToolSearchNames.size > 0
|
|
3589
3713
|
? createRoutedToolSearchRestoreBlockRewrite(routedToolSearchNames, translatorBudget)
|
|
@@ -3598,7 +3722,10 @@ async function handleResponsesInner(
|
|
|
3598
3722
|
// Last: every rewrite above can still rename or reshape a call item, so the guard must
|
|
3599
3723
|
// compare the names the client will actually receive against the declared catalog.
|
|
3600
3724
|
undeclaredToolGuardActive
|
|
3601
|
-
? createUndeclaredToolCallGuardBlockRewrite(
|
|
3725
|
+
? createUndeclaredToolCallGuardBlockRewrite(
|
|
3726
|
+
declaredWireToolNames,
|
|
3727
|
+
declaredNamelessClientCallTypes,
|
|
3728
|
+
)
|
|
3602
3729
|
: undefined,
|
|
3603
3730
|
].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
|
|
3604
3731
|
const clientBlockRewrite = blockRewrites.length > 0
|
|
@@ -3788,6 +3915,7 @@ async function handleResponsesInner(
|
|
|
3788
3915
|
const restored = restoreRoutedCustomCallsInJson(
|
|
3789
3916
|
restoredNamespace,
|
|
3790
3917
|
routedCustomToolNames,
|
|
3918
|
+
routedCustomToolRepairNames,
|
|
3791
3919
|
);
|
|
3792
3920
|
const restoredToolSearch = restoreRoutedToolSearchCallsInJson(
|
|
3793
3921
|
restored,
|
|
@@ -3814,7 +3942,11 @@ async function handleResponsesInner(
|
|
|
3814
3942
|
if (undeclaredToolGuardActive) {
|
|
3815
3943
|
const undeclared = (() => {
|
|
3816
3944
|
try {
|
|
3817
|
-
return undeclaredToolCallNameInResponse(
|
|
3945
|
+
return undeclaredToolCallNameInResponse(
|
|
3946
|
+
JSON.parse(clientJson),
|
|
3947
|
+
declaredWireToolNames,
|
|
3948
|
+
declaredNamelessClientCallTypes,
|
|
3949
|
+
);
|
|
3818
3950
|
} catch {
|
|
3819
3951
|
return undefined;
|
|
3820
3952
|
}
|
|
@@ -4318,9 +4450,11 @@ async function handleResponsesInner(
|
|
|
4318
4450
|
);
|
|
4319
4451
|
const bridgeTurnAc = new AbortController();
|
|
4320
4452
|
const trackedSse = trackStreamLifetime(sseStream, bridgeTurnAc, undefined, options.turnAdmissionLease);
|
|
4321
|
-
|
|
4453
|
+
const response = new Response(trackedSse, {
|
|
4322
4454
|
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no" },
|
|
4323
4455
|
});
|
|
4456
|
+
runTurnAdapterSseResponses.add(response);
|
|
4457
|
+
return response;
|
|
4324
4458
|
}
|
|
4325
4459
|
|
|
4326
4460
|
await runTurn();
|
|
@@ -4783,14 +4917,16 @@ async function handleResponsesInner(
|
|
|
4783
4917
|
options.onConsumedComboFailure?.(failure);
|
|
4784
4918
|
return failure.response;
|
|
4785
4919
|
}
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4920
|
+
let errorText: string;
|
|
4921
|
+
try {
|
|
4922
|
+
errorText = await readDisplaySafeErrorText(
|
|
4923
|
+
upstreamResponse,
|
|
4924
|
+
upstream.signal,
|
|
4925
|
+
"unknown error",
|
|
4926
|
+
);
|
|
4927
|
+
} finally {
|
|
4928
|
+
cleanupUpstreamAbort();
|
|
4929
|
+
}
|
|
4794
4930
|
if (!isFixedCodexAccount(authCtx)) {
|
|
4795
4931
|
recordSubagentQuotaFailureForThreadSpawn(
|
|
4796
4932
|
req.headers,
|
|
@@ -5081,12 +5217,7 @@ async function handleResponsesInner(
|
|
|
5081
5217
|
}
|
|
5082
5218
|
|
|
5083
5219
|
if (!response.ok) {
|
|
5084
|
-
|
|
5085
|
-
// is still a Bun fetch body, and an abort landing before `.text()` attaches its reader
|
|
5086
|
-
// orphans the internal rejection.
|
|
5087
|
-
const detachContinuationErrorGuard = cancelBodyOnAbort(response.body, upstream.signal);
|
|
5088
|
-
const errorText = await response.text().catch(() => "unknown error");
|
|
5089
|
-
detachContinuationErrorGuard();
|
|
5220
|
+
const errorText = await readDisplaySafeErrorText(response, upstream.signal, "unknown error");
|
|
5090
5221
|
yield {
|
|
5091
5222
|
type: "error",
|
|
5092
5223
|
status: response.status,
|