@bitkyc08/opencodex 2.28.0 → 2.29.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 +9 -1
- package/gui/dist/assets/index-BNESwCzn.js +102 -0
- package/gui/dist/assets/index-CH7ncHCC.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/base.ts +3 -1
- package/src/adapters/cursor/checkpoint-store.ts +303 -0
- package/src/adapters/cursor/discovery.ts +25 -0
- package/src/adapters/cursor/live-transport.ts +19 -2
- package/src/adapters/cursor/native-exec.ts +47 -2
- package/src/adapters/cursor/protobuf-request.ts +80 -23
- package/src/adapters/cursor/request-builder.ts +119 -5
- package/src/adapters/cursor/transport.ts +5 -0
- package/src/adapters/cursor/types.ts +13 -0
- package/src/adapters/cursor.ts +109 -3
- package/src/adapters/google-antigravity-replay.ts +31 -4
- package/src/adapters/google.ts +323 -38
- package/src/adapters/openai-chat.ts +19 -0
- package/src/adapters/openai-responses.ts +218 -37
- package/src/bridge.ts +38 -20
- package/src/claude/desktop-3p.ts +15 -6
- package/src/cli/agent.ts +44 -1
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/dispatch.ts +24 -1
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +40 -38
- package/src/cli/integrations.ts +1 -1
- package/src/cli/registry.ts +2 -2
- package/src/clients/config-export.ts +119 -20
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/inject.ts +46 -14
- package/src/codex/journal.ts +22 -8
- package/src/config.ts +1 -0
- package/src/generated/compatibility-version.json +134 -66
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/registry.ts +25 -2
- package/src/integrations/writer.ts +32 -1
- package/src/lab/public/signature.ts +25 -1
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/redact.ts +2 -2
- package/src/oauth/log.ts +3 -1
- package/src/providers/derive.ts +9 -0
- package/src/providers/fastwire.ts +24 -18
- package/src/providers/openai-tiers.ts +60 -1
- package/src/providers/registry.ts +14 -7
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/responses/compaction.ts +18 -0
- package/src/responses/custom-tool-compat.ts +70 -5
- package/src/responses/namespace-tool-compat.ts +356 -0
- package/src/responses/parser.ts +2 -2
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/reasoning-replay-cache.ts +125 -7
- package/src/responses/spill-store.ts +6 -1
- package/src/responses/state.ts +11 -0
- package/src/router.ts +14 -0
- package/src/routing/compatibility/behavior.ts +1 -0
- package/src/server/auth-cors.ts +4 -0
- package/src/server/management/agent-settings-routes.ts +57 -9
- package/src/server/management/config-routes.ts +134 -15
- package/src/server/management/integration-routes.ts +8 -55
- package/src/server/management/model-routes.ts +23 -1
- package/src/server/management/provider-routes.ts +22 -0
- package/src/server/management/vision-sidecar-options.ts +18 -11
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/responses/core.ts +588 -78
- package/src/server/responses/responses-field-backfill.ts +7 -11
- package/src/server/responses/terminal-guard.ts +22 -11
- package/src/server/responses-custom-tool-repair.ts +3 -1
- package/src/server/responses-reasoning-summary-rewrite.ts +7 -0
- package/src/server/responses-tool-search-repair.ts +64 -14
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/types/config.ts +26 -5
- package/src/types/provider.ts +18 -0
- package/src/types/request.ts +26 -0
- package/src/types.ts +1 -0
- package/src/usage/log.ts +2 -0
- package/src/vision/index.ts +8 -9
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +88 -0
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +139 -12
- package/src/web-search/loop.ts +45 -5
- package/src/web-search/parse.ts +34 -23
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/xai-executor.ts +219 -0
- package/gui/dist/assets/index-D2sP-biU.js +0 -102
- package/gui/dist/assets/index-DQsMZzI5.css +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Server } from "bun";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { bridgeToResponsesSSE, buildResponseJSON, formatErrorResponse, type ResponsesTerminalStatus } from "../../bridge";
|
|
3
4
|
import { formatPassthroughUpstreamError } from "./passthrough-error";
|
|
4
5
|
import {
|
|
@@ -15,17 +16,20 @@ import {
|
|
|
15
16
|
import { parseRequest } from "../../responses/parser";
|
|
16
17
|
import {
|
|
17
18
|
bindReasoningReplayScope,
|
|
19
|
+
commitReasoningReplayServingIdentity,
|
|
18
20
|
reasoningReplayCodexCredentialIdentity,
|
|
19
21
|
reasoningReplayDestinationIdentity,
|
|
20
22
|
durableReplayDestinationIdentity,
|
|
21
23
|
durableReplayCredentialIdentity,
|
|
22
24
|
reasoningReplayKeyCredentialIdentity,
|
|
23
25
|
reasoningReplayOAuthCredentialIdentity,
|
|
26
|
+
reasoningReplayServingIdentityChanged,
|
|
24
27
|
} from "../../responses/reasoning-replay-cache";
|
|
25
28
|
import { awaitThoughtSignatureDurability, thoughtSignatureReplaySalt } from "../../responses/thought-signature-replay";
|
|
26
29
|
import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
|
|
27
30
|
import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../../adapters/openai-responses";
|
|
28
31
|
import {
|
|
32
|
+
copyPreviousResponseReplayProvenance,
|
|
29
33
|
expandPreviousResponseInput,
|
|
30
34
|
markBodyNonPersistable,
|
|
31
35
|
previousResponseProviderState,
|
|
@@ -33,6 +37,13 @@ import {
|
|
|
33
37
|
previousResponseScopeMismatch,
|
|
34
38
|
rememberResponseState,
|
|
35
39
|
} from "../../responses/state";
|
|
40
|
+
import {
|
|
41
|
+
isValidProviderContinuationOwner,
|
|
42
|
+
mergeProviderContinuationPayload,
|
|
43
|
+
providerContinuationOwnerFromReplayIdentity,
|
|
44
|
+
providerContinuationRouteScope,
|
|
45
|
+
sameProviderContinuationOwner,
|
|
46
|
+
} from "../../responses/provider-continuation";
|
|
36
47
|
import {
|
|
37
48
|
comboRouteDecisionTrace,
|
|
38
49
|
NoEligiblePolicyCandidateError,
|
|
@@ -62,7 +73,17 @@ import { injectionDebugLog } from "../../lib/injection-debug-log";
|
|
|
62
73
|
import { resolveClientRetryAfter } from "../../lib/retry-after";
|
|
63
74
|
import { enrichOpenCodeZenRateLimitMessage } from "../../providers/opencode-zen-rate-limit";
|
|
64
75
|
import { modelInList, namespacedToolName } from "../../types";
|
|
65
|
-
import type {
|
|
76
|
+
import type {
|
|
77
|
+
AdapterEvent,
|
|
78
|
+
OcxConfig,
|
|
79
|
+
OcxParsedRequest,
|
|
80
|
+
OcxProviderConfig,
|
|
81
|
+
OcxProviderContinuationOwner,
|
|
82
|
+
OcxProviderContinuationState,
|
|
83
|
+
OcxReasoningReplayIdentity,
|
|
84
|
+
OcxUsage,
|
|
85
|
+
TierDecision,
|
|
86
|
+
} from "../../types";
|
|
66
87
|
import {
|
|
67
88
|
forceRefreshOAuthAccessSnapshot,
|
|
68
89
|
getOAuthCredentialApiBaseUrl,
|
|
@@ -115,6 +136,7 @@ import {
|
|
|
115
136
|
resolveCodexModelEntitlements,
|
|
116
137
|
} from "../../codex/model-entitlements";
|
|
117
138
|
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
|
|
139
|
+
import { captureCodexAffinityDiagnostic } from "../../codex/affinity-debug";
|
|
118
140
|
import {
|
|
119
141
|
computeQuotaCooldown,
|
|
120
142
|
formatCodexProviderForLog,
|
|
@@ -275,6 +297,12 @@ import { restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-comp
|
|
|
275
297
|
import { createRoutedCustomToolRestoreBlockRewrite } from "../responses-custom-tool-repair";
|
|
276
298
|
import { restoreRoutedToolSearchCallsInJson } from "../../responses/tool-search-compat";
|
|
277
299
|
import { createRoutedToolSearchRestoreBlockRewrite } from "../responses-tool-search-repair";
|
|
300
|
+
import {
|
|
301
|
+
createRoutedNamespaceCallRestoreRewrite,
|
|
302
|
+
NamespaceToolCollisionError,
|
|
303
|
+
restoreRoutedNamespaceCallsInJson,
|
|
304
|
+
type RoutedNamespaceToolAliases,
|
|
305
|
+
} from "../../responses/namespace-tool-compat";
|
|
278
306
|
import {
|
|
279
307
|
collectDeclaredWireToolNames,
|
|
280
308
|
createUndeclaredToolCallGuardBlockRewrite,
|
|
@@ -325,6 +353,62 @@ export function codexLogAccountId(authCtx: CodexAuthContext): string | null {
|
|
|
325
353
|
return authCtx.kind === "pool" || authCtx.kind === "main-pool" ? authCtx.accountId : null;
|
|
326
354
|
}
|
|
327
355
|
|
|
356
|
+
type ContinuationOwnerRead =
|
|
357
|
+
| { kind: "missing" }
|
|
358
|
+
| { kind: "invalid" }
|
|
359
|
+
| { kind: "valid"; owner: OcxProviderContinuationOwner };
|
|
360
|
+
|
|
361
|
+
function readProviderContinuationOwner(
|
|
362
|
+
state: OcxProviderContinuationState | undefined,
|
|
363
|
+
): ContinuationOwnerRead {
|
|
364
|
+
if (!state || state.__ocxOwner === undefined) return { kind: "missing" };
|
|
365
|
+
const owner = state.__ocxOwner;
|
|
366
|
+
if (!isValidProviderContinuationOwner(owner)) return { kind: "invalid" };
|
|
367
|
+
return { kind: "valid", owner: { ...owner } };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function providerContinuationPayload(
|
|
371
|
+
state: OcxProviderContinuationState | undefined,
|
|
372
|
+
): OcxProviderContinuationState | undefined {
|
|
373
|
+
if (!state) return undefined;
|
|
374
|
+
const cloned = structuredClone(state);
|
|
375
|
+
delete cloned.__ocxOwner;
|
|
376
|
+
return Object.keys(cloned).length > 0 ? cloned : undefined;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function bindProviderContinuationForRoute(
|
|
380
|
+
parsed: OcxParsedRequest,
|
|
381
|
+
currentOwner: OcxProviderContinuationOwner | undefined,
|
|
382
|
+
): void {
|
|
383
|
+
const candidate = parsed._providerContinuationCandidate;
|
|
384
|
+
const storedOwner = readProviderContinuationOwner(candidate);
|
|
385
|
+
const mayRestore = storedOwner.kind === "valid"
|
|
386
|
+
&& !!currentOwner
|
|
387
|
+
&& sameProviderContinuationOwner(storedOwner.owner, currentOwner);
|
|
388
|
+
const restored = mayRestore ? providerContinuationPayload(candidate) : undefined;
|
|
389
|
+
if (restored) parsed._providerContinuation = restored;
|
|
390
|
+
else delete parsed._providerContinuation;
|
|
391
|
+
const cursorConversationId = restored?.cursor?.conversationId;
|
|
392
|
+
if (cursorConversationId) parsed._cursorConversationId = cursorConversationId;
|
|
393
|
+
else delete parsed._cursorConversationId;
|
|
394
|
+
if (currentOwner) parsed._providerContinuationOwner = { ...currentOwner };
|
|
395
|
+
else delete parsed._providerContinuationOwner;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function providerContinuationDestinationIdentity(
|
|
399
|
+
parsed: OcxParsedRequest,
|
|
400
|
+
provider: OcxProviderConfig,
|
|
401
|
+
): string | undefined {
|
|
402
|
+
const kiroContext = parsed._kiroAuthContext;
|
|
403
|
+
return reasoningReplayDestinationIdentity(JSON.stringify([
|
|
404
|
+
provider.baseUrl.trim().replace(/\/+$/, ""),
|
|
405
|
+
provider.responsesPath ?? "",
|
|
406
|
+
kiroContext?.profileArn ?? "",
|
|
407
|
+
kiroContext?.apiRegion ?? "",
|
|
408
|
+
kiroContext?.ssoRegion ?? "",
|
|
409
|
+
]));
|
|
410
|
+
}
|
|
411
|
+
|
|
328
412
|
function bindRouteReasoningReplayScope(args: {
|
|
329
413
|
parsed: OcxParsedRequest;
|
|
330
414
|
providerName: string;
|
|
@@ -393,20 +477,202 @@ function bindRouteReasoningReplayScope(args: {
|
|
|
393
477
|
);
|
|
394
478
|
}
|
|
395
479
|
const providerDestinationIdentity = reasoningReplayDestinationIdentity(provider.baseUrl);
|
|
480
|
+
const replayIdentity: OcxReasoningReplayIdentity | undefined = credentialIdentity && providerDestinationIdentity
|
|
481
|
+
? {
|
|
482
|
+
providerName,
|
|
483
|
+
providerDestinationIdentity,
|
|
484
|
+
providerDestinationDurableIdentity: durableReplayDestinationIdentity(provider.baseUrl),
|
|
485
|
+
adapterName,
|
|
486
|
+
modelId: parsed.modelId,
|
|
487
|
+
credentialIdentity,
|
|
488
|
+
...(credentialDurableIdentity ? { credentialDurableIdentity } : {}),
|
|
489
|
+
}
|
|
490
|
+
: undefined;
|
|
491
|
+
const continuationDestinationIdentity = providerContinuationDestinationIdentity(parsed, provider);
|
|
492
|
+
const continuationOwner = providerContinuationOwnerFromReplayIdentity(
|
|
493
|
+
replayIdentity && continuationDestinationIdentity
|
|
494
|
+
? { ...replayIdentity, providerDestinationIdentity: continuationDestinationIdentity }
|
|
495
|
+
: undefined,
|
|
496
|
+
);
|
|
497
|
+
if (adapterName === "cursor") {
|
|
498
|
+
// The final route owner is authoritative for Cursor and supersedes the account-derived
|
|
499
|
+
// seed assigned before route binding. A Cursor conversation must be scoped to the exact
|
|
500
|
+
// provider/destination/adapter/model/credential that serves it.
|
|
501
|
+
if (continuationOwner) parsed._cursorIdentityScope = providerContinuationRouteScope(continuationOwner);
|
|
502
|
+
else if (!parsed._cursorIdentityScope?.startsWith("cursor-unowned:")) {
|
|
503
|
+
// Prevent the adapter's token-only fallback from recreating a provider-private id after the
|
|
504
|
+
// route owner failed closed. The sentinel is per parsed request and contains no credential.
|
|
505
|
+
parsed._cursorIdentityScope = `cursor-unowned:${randomUUID()}`;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
396
508
|
bindReasoningReplayScope(
|
|
397
509
|
parsed._reasoningReplayScope,
|
|
398
|
-
|
|
399
|
-
? {
|
|
400
|
-
providerName,
|
|
401
|
-
providerDestinationIdentity,
|
|
402
|
-
providerDestinationDurableIdentity: durableReplayDestinationIdentity(provider.baseUrl),
|
|
403
|
-
adapterName,
|
|
404
|
-
modelId: parsed.modelId,
|
|
405
|
-
credentialIdentity,
|
|
406
|
-
...(credentialDurableIdentity ? { credentialDurableIdentity } : {}),
|
|
407
|
-
}
|
|
408
|
-
: undefined,
|
|
510
|
+
replayIdentity,
|
|
409
511
|
);
|
|
512
|
+
// Keep this sticky for the whole outbound request: a later auth/key rebind may compare equal
|
|
513
|
+
// after the first mismatch, but it cannot make history minted by the prior route decodable.
|
|
514
|
+
if (reasoningReplayServingIdentityChanged(parsed._reasoningReplayScope)) {
|
|
515
|
+
parsed._stripReasoningEncryptedContent = true;
|
|
516
|
+
}
|
|
517
|
+
bindProviderContinuationForRoute(parsed, continuationOwner);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function adapterResponseReachedServingTerminal(
|
|
521
|
+
events: readonly AdapterEvent[],
|
|
522
|
+
response: Readonly<Record<string, unknown>>,
|
|
523
|
+
): boolean {
|
|
524
|
+
return (response.status === "completed" || response.status === "incomplete")
|
|
525
|
+
&& events.some(event => event.type === "done" || event.type === "incomplete");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const OPAQUE_RESPONSES_INPUT_TYPES = new Set([
|
|
529
|
+
"reasoning",
|
|
530
|
+
"compaction",
|
|
531
|
+
"compaction_summary",
|
|
532
|
+
"context_compaction",
|
|
533
|
+
]);
|
|
534
|
+
|
|
535
|
+
function outboundResponsesBodyCarriesOpaqueBlob(bodyText: string | undefined): boolean {
|
|
536
|
+
if (!bodyText) return false;
|
|
537
|
+
try {
|
|
538
|
+
const body = JSON.parse(bodyText) as unknown;
|
|
539
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return false;
|
|
540
|
+
const input = (body as { input?: unknown }).input;
|
|
541
|
+
if (!Array.isArray(input)) return false;
|
|
542
|
+
return input.some(item => {
|
|
543
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return false;
|
|
544
|
+
const candidate = item as { type?: unknown; encrypted_content?: unknown };
|
|
545
|
+
return typeof candidate.type === "string"
|
|
546
|
+
&& OPAQUE_RESPONSES_INPUT_TYPES.has(candidate.type)
|
|
547
|
+
&& typeof candidate.encrypted_content === "string"
|
|
548
|
+
&& candidate.encrypted_content.length > 0;
|
|
549
|
+
});
|
|
550
|
+
} catch {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function isSelfIdentifiedOpaqueBlobRejection(bodyText: string): boolean {
|
|
556
|
+
try {
|
|
557
|
+
const payload = JSON.parse(bodyText) as unknown;
|
|
558
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
559
|
+
const record = payload as { code?: unknown; error?: unknown };
|
|
560
|
+
|
|
561
|
+
if (record.error && typeof record.error === "object" && !Array.isArray(record.error)) {
|
|
562
|
+
const error = record.error as { type?: unknown; code?: unknown; message?: unknown };
|
|
563
|
+
if (error.type === "invalid_request_error") {
|
|
564
|
+
if (error.code === "invalid_encrypted_content") return true;
|
|
565
|
+
if (
|
|
566
|
+
(error.code === null || error.code === undefined)
|
|
567
|
+
&& typeof error.message === "string"
|
|
568
|
+
&& error.message.startsWith("The encrypted content ")
|
|
569
|
+
&& error.message.endsWith(
|
|
570
|
+
" could not be verified. Reason: Encrypted content could not be decrypted or parsed.",
|
|
571
|
+
)
|
|
572
|
+
) return true;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (record.code !== "invalid-argument" || typeof record.error !== "string") return false;
|
|
577
|
+
return record.error.startsWith("Could not decode the compaction blob")
|
|
578
|
+
|| record.error.startsWith("Could not decrypt the provided encrypted_content");
|
|
579
|
+
} catch {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Whether an upstream Responses 4xx authoritatively rejected opaque replay state.
|
|
586
|
+
*
|
|
587
|
+
* The outbound-body check is intentional: the inbound transcript may contain a proxy envelope or
|
|
588
|
+
* compaction blob that the adapter already lowered, in which case a replay would be byte-identical.
|
|
589
|
+
* OpenAI usually exposes a dedicated nested code; ChatGPT also emits one exact code-less
|
|
590
|
+
* unverifiable-ciphertext message. xAI's code is generic, so its two concrete decoder error
|
|
591
|
+
* identities are also required. Unrelated error prose must never gain a hidden resend.
|
|
592
|
+
*/
|
|
593
|
+
export function shouldAttemptOpaqueBlobRecovery(args: {
|
|
594
|
+
status: number;
|
|
595
|
+
adapterName: string;
|
|
596
|
+
outboundBody?: string;
|
|
597
|
+
errorBody: string;
|
|
598
|
+
alreadyAttempted: boolean;
|
|
599
|
+
}): boolean {
|
|
600
|
+
return args.status >= 400
|
|
601
|
+
&& args.status < 500
|
|
602
|
+
&& args.adapterName === "openai-responses"
|
|
603
|
+
&& !args.alreadyAttempted
|
|
604
|
+
&& outboundResponsesBodyCarriesOpaqueBlob(args.outboundBody)
|
|
605
|
+
&& isSelfIdentifiedOpaqueBlobRejection(args.errorBody);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async function opaqueBlobRejectionBodyForRecovery(
|
|
609
|
+
response: Response,
|
|
610
|
+
outboundBody: string | undefined,
|
|
611
|
+
adapterName: string,
|
|
612
|
+
alreadyAttempted: boolean,
|
|
613
|
+
signal: AbortSignal,
|
|
614
|
+
): Promise<string | undefined> {
|
|
615
|
+
if (
|
|
616
|
+
response.status < 400
|
|
617
|
+
|| response.status >= 500
|
|
618
|
+
|| adapterName !== "openai-responses"
|
|
619
|
+
|| alreadyAttempted
|
|
620
|
+
|| !outboundResponsesBodyCarriesOpaqueBlob(outboundBody)
|
|
621
|
+
) return undefined;
|
|
622
|
+
try {
|
|
623
|
+
const body = await readBoundedResponseBody(response.clone(), { signal });
|
|
624
|
+
return body.displaySafe && !body.truncated ? body.text : undefined;
|
|
625
|
+
} catch {
|
|
626
|
+
return undefined;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function prepareOpaqueBlobRecovery(parsed: OcxParsedRequest): void {
|
|
631
|
+
parsed._stripReasoningEncryptedContent = true;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
type OpaqueBlobRecoveryGuard = { attempted: boolean };
|
|
635
|
+
|
|
636
|
+
type OpaqueBlobRecoveryResult =
|
|
637
|
+
| { kind: "skipped" }
|
|
638
|
+
| { kind: "recovered"; response: Response }
|
|
639
|
+
| { kind: "failed"; response: Response };
|
|
640
|
+
|
|
641
|
+
async function attemptOpaqueBlobRecovery(
|
|
642
|
+
args: {
|
|
643
|
+
response: Response;
|
|
644
|
+
outboundBody?: string;
|
|
645
|
+
adapterName: string;
|
|
646
|
+
parsed: OcxParsedRequest;
|
|
647
|
+
guard: OpaqueBlobRecoveryGuard;
|
|
648
|
+
signal: AbortSignal;
|
|
649
|
+
},
|
|
650
|
+
rebuild: (kind: AttemptRecoveryKind) => Promise<Response | { failed: Response }>,
|
|
651
|
+
): Promise<OpaqueBlobRecoveryResult> {
|
|
652
|
+
const errorBody = await opaqueBlobRejectionBodyForRecovery(
|
|
653
|
+
args.response,
|
|
654
|
+
args.outboundBody,
|
|
655
|
+
args.adapterName,
|
|
656
|
+
args.guard.attempted,
|
|
657
|
+
args.signal,
|
|
658
|
+
);
|
|
659
|
+
if (errorBody === undefined || !shouldAttemptOpaqueBlobRecovery({
|
|
660
|
+
status: args.response.status,
|
|
661
|
+
adapterName: args.adapterName,
|
|
662
|
+
outboundBody: args.outboundBody,
|
|
663
|
+
errorBody,
|
|
664
|
+
alreadyAttempted: args.guard.attempted,
|
|
665
|
+
})) {
|
|
666
|
+
return { kind: "skipped" };
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
args.guard.attempted = true;
|
|
670
|
+
prepareOpaqueBlobRecovery(args.parsed);
|
|
671
|
+
try { void args.response.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
672
|
+
const result = await rebuild("opaque-blob-rejection");
|
|
673
|
+
return "failed" in result
|
|
674
|
+
? { kind: "failed", response: result.failed }
|
|
675
|
+
: { kind: "recovered", response: result };
|
|
410
676
|
}
|
|
411
677
|
|
|
412
678
|
function nonEmptyProviderApiKey(provider: OcxProviderConfig): string | undefined {
|
|
@@ -521,6 +787,11 @@ interface CodexPoolAccountRetryArgs {
|
|
|
521
787
|
connectMs: number;
|
|
522
788
|
passthroughEstimate?: number;
|
|
523
789
|
stream: boolean;
|
|
790
|
+
onResponse?: (
|
|
791
|
+
response: Response,
|
|
792
|
+
authCtx: Extract<CodexAuthContext, { kind: "pool" | "main-pool" }>,
|
|
793
|
+
request: Awaited<ReturnType<ReturnType<typeof resolveAdapter>["buildRequest"]>>,
|
|
794
|
+
) => void;
|
|
524
795
|
}
|
|
525
796
|
|
|
526
797
|
type CodexPoolAccountRetryResult =
|
|
@@ -761,6 +1032,7 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
761
1032
|
route.provider.authMode === "forward",
|
|
762
1033
|
);
|
|
763
1034
|
retrySendCount += 1;
|
|
1035
|
+
args.onResponse?.(upstreamResponse, retryAuthCtx, request);
|
|
764
1036
|
if (!retrySameConfirmedAccount || retrySendCount >= maxRetrySends) break;
|
|
765
1037
|
if (!await shouldRetryCodexPoolAccountModel400(
|
|
766
1038
|
upstreamResponse,
|
|
@@ -943,6 +1215,12 @@ export interface HandleResponsesOptions {
|
|
|
943
1215
|
stripClaudeMainAuthForNoncanonicalForward?: boolean;
|
|
944
1216
|
/** Internal recursion guard; callers outside this module must not set it. */
|
|
945
1217
|
comboAttempt?: boolean;
|
|
1218
|
+
/** Internal combo handoff for one parent-validated continuation snapshot. */
|
|
1219
|
+
comboReplaySnapshot?: {
|
|
1220
|
+
sourceBody: unknown;
|
|
1221
|
+
previousResponseInputExpanded: boolean;
|
|
1222
|
+
providerContinuation: OcxProviderContinuationState | undefined;
|
|
1223
|
+
};
|
|
946
1224
|
/** Internal combo handoff: allow a later same-provider model after a reset-derived 429/402. */
|
|
947
1225
|
deferCodexResetDerivedCooldown?: boolean;
|
|
948
1226
|
/** 030-owned handoff when a child consumed the original failure under bounds. */
|
|
@@ -1141,7 +1419,7 @@ function unreadableEncryptedAgentTaskResponse(): Response {
|
|
|
1141
1419
|
}
|
|
1142
1420
|
|
|
1143
1421
|
type ResponsesAuthResolution =
|
|
1144
|
-
| { ok: true; authCtx: CodexAuthContext; headers: Headers }
|
|
1422
|
+
| { ok: true; authCtx: CodexAuthContext; headers: Headers; substituteMainCredential: boolean }
|
|
1145
1423
|
| { ok: false; response: Response };
|
|
1146
1424
|
|
|
1147
1425
|
/**
|
|
@@ -1205,6 +1483,7 @@ async function resolveResponsesCodexAuth(
|
|
|
1205
1483
|
ok: true,
|
|
1206
1484
|
authCtx,
|
|
1207
1485
|
headers: materializeCodexUpstreamAuth(req.headers, authCtx, { substituteMainCredential }),
|
|
1486
|
+
substituteMainCredential,
|
|
1208
1487
|
};
|
|
1209
1488
|
} catch (err) {
|
|
1210
1489
|
if (err instanceof CodexAccountCooldownError) {
|
|
@@ -1435,7 +1714,12 @@ export async function handleComboResponses(
|
|
|
1435
1714
|
// Expand previous_response_id before image policy and child dispatch so a
|
|
1436
1715
|
// continuation that only references prior images still fails closed when
|
|
1437
1716
|
// imageInput is disabled (and so targets see the full replayed input).
|
|
1438
|
-
const
|
|
1717
|
+
const inboundClientThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
|
|
1718
|
+
const body = expandPreviousResponseInput(rawBody, inboundClientThreadId);
|
|
1719
|
+
const scopeMismatch = previousResponseScopeMismatch(body);
|
|
1720
|
+
if (scopeMismatch) {
|
|
1721
|
+
console.warn("[opencodex] dropped a previous_response_id with a mismatched client task scope; continuing fresh");
|
|
1722
|
+
}
|
|
1439
1723
|
if (previousResponseReplayFailure(body)) {
|
|
1440
1724
|
return formatErrorResponse(
|
|
1441
1725
|
400,
|
|
@@ -1462,11 +1746,14 @@ export async function handleComboResponses(
|
|
|
1462
1746
|
if (combo.imageInput === "disabled" && comboRequestHasImageInput(body)) {
|
|
1463
1747
|
return formatErrorResponse(400, "invalid_request_error", `Combo "${comboId}" does not accept image input`);
|
|
1464
1748
|
}
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1749
|
+
const comboReplaySnapshot = {
|
|
1750
|
+
sourceBody: body,
|
|
1751
|
+
previousResponseInputExpanded: body !== rawBody
|
|
1752
|
+
&& typeof (body as { previous_response_id?: unknown }).previous_response_id === "string",
|
|
1753
|
+
providerContinuation: !scopeMismatch && body !== rawBody && requestedPreviousId
|
|
1754
|
+
? previousResponseProviderState(requestedPreviousId)
|
|
1755
|
+
: undefined,
|
|
1756
|
+
};
|
|
1470
1757
|
const adoptFailedChildLog = (childLog: RequestLogContext): void => {
|
|
1471
1758
|
// Attempts remain the complete physical history; the logical row mirrors the most recent
|
|
1472
1759
|
// failed target so an exhausted combo still has useful top-level reasoning diagnostics.
|
|
@@ -1573,6 +1860,7 @@ export async function handleComboResponses(
|
|
|
1573
1860
|
response = await handleResponses(childRequest, config, childLog, {
|
|
1574
1861
|
...options,
|
|
1575
1862
|
comboAttempt: true,
|
|
1863
|
+
comboReplaySnapshot,
|
|
1576
1864
|
deferCodexResetDerivedCooldown,
|
|
1577
1865
|
// Attempt-relative TTFT is recorded HERE (not via childLog.firstOutputMs — a later
|
|
1578
1866
|
// Object.assign(logCtx, childLog) would overwrite the request-relative value).
|
|
@@ -1842,19 +2130,24 @@ async function handleResponsesInner(
|
|
|
1842
2130
|
);
|
|
1843
2131
|
const inboundClientThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
|
|
1844
2132
|
const originalBody = body;
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
2133
|
+
if (options.comboReplaySnapshot) {
|
|
2134
|
+
copyPreviousResponseReplayProvenance(options.comboReplaySnapshot.sourceBody, body);
|
|
2135
|
+
} else {
|
|
2136
|
+
body = expandPreviousResponseInput(body, inboundClientThreadId);
|
|
2137
|
+
if (previousResponseScopeMismatch(body)) {
|
|
2138
|
+
console.warn("[opencodex] dropped a previous_response_id with a mismatched client task scope; continuing fresh");
|
|
2139
|
+
}
|
|
2140
|
+
if (previousResponseReplayFailure(body)) {
|
|
2141
|
+
return formatErrorResponse(
|
|
2142
|
+
400,
|
|
2143
|
+
"previous_response_not_found",
|
|
2144
|
+
"Continuation state is unavailable or corrupt; resend the full conversation without previous_response_id.",
|
|
2145
|
+
);
|
|
2146
|
+
}
|
|
1855
2147
|
}
|
|
1856
|
-
const previousResponseInputExpanded =
|
|
1857
|
-
|
|
2148
|
+
const previousResponseInputExpanded = options.comboReplaySnapshot?.previousResponseInputExpanded
|
|
2149
|
+
?? (body !== originalBody
|
|
2150
|
+
&& typeof (body as { previous_response_id?: unknown }).previous_response_id === "string");
|
|
1858
2151
|
|
|
1859
2152
|
// Spawn-message compatibility (both directions): agent_message task payloads ride in
|
|
1860
2153
|
// encrypted_content slots as plaintext. Rewrite them to input_text on the RAW body BEFORE
|
|
@@ -1877,8 +2170,10 @@ async function handleResponsesInner(
|
|
|
1877
2170
|
parsed = parseRequest(body);
|
|
1878
2171
|
toolBridgeMaps = buildToolBridgeMaps(parsed, translatorBudget);
|
|
1879
2172
|
if (previousResponseInputExpanded) parsed._previousResponseInputExpanded = true;
|
|
1880
|
-
|
|
1881
|
-
|
|
2173
|
+
const providerContinuationCandidate = options.comboReplaySnapshot
|
|
2174
|
+
? options.comboReplaySnapshot.providerContinuation
|
|
2175
|
+
: previousResponseProviderState(parsed.previousResponseId);
|
|
2176
|
+
if (providerContinuationCandidate) parsed._providerContinuationCandidate = providerContinuationCandidate;
|
|
1882
2177
|
if (inboundClientThreadId) {
|
|
1883
2178
|
parsed._clientThreadId = inboundClientThreadId;
|
|
1884
2179
|
parsed._reasoningReplayScope = { clientThreadId: inboundClientThreadId };
|
|
@@ -2077,6 +2372,8 @@ async function handleResponsesInner(
|
|
|
2077
2372
|
const kept: Array<keyof OcxParsedRequest> = [
|
|
2078
2373
|
"_previousResponseInputExpanded",
|
|
2079
2374
|
"_providerContinuation",
|
|
2375
|
+
"_providerContinuationCandidate",
|
|
2376
|
+
"_providerContinuationOwner",
|
|
2080
2377
|
"_cursorConversationId",
|
|
2081
2378
|
"_clientThreadId",
|
|
2082
2379
|
"_reasoningReplayScope",
|
|
@@ -2217,11 +2514,13 @@ async function handleResponsesInner(
|
|
|
2217
2514
|
pendingHostAdmissionLease = admission.lease;
|
|
2218
2515
|
}
|
|
2219
2516
|
|
|
2517
|
+
let substituteMainCredential = false;
|
|
2220
2518
|
{
|
|
2221
2519
|
const finalAuth = await resolveResponsesCodexAuth(req, config, route, options);
|
|
2222
2520
|
if (!finalAuth.ok) return finalAuth.response;
|
|
2223
2521
|
authCtx = finalAuth.authCtx;
|
|
2224
2522
|
selectedForwardHeaders = finalAuth.headers;
|
|
2523
|
+
substituteMainCredential = finalAuth.substituteMainCredential;
|
|
2225
2524
|
}
|
|
2226
2525
|
|
|
2227
2526
|
route.provider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
|
|
@@ -2230,8 +2529,9 @@ async function handleResponsesInner(
|
|
|
2230
2529
|
? `${route.providerName}-${route.codexAccountNamespace}`
|
|
2231
2530
|
: formatCodexProviderForLog(route.providerName, codexLogAccountId(authCtx), config);
|
|
2232
2531
|
logCtx.accountLogLabel = codexAuthContextLogLabel(authCtx, config);
|
|
2233
|
-
//
|
|
2234
|
-
//
|
|
2532
|
+
// Seed an account-derived scope before final adapter binding. Cursor never treats it as
|
|
2533
|
+
// authoritative: bindRouteReasoningReplayScope replaces it with the exact route owner or a
|
|
2534
|
+
// per-request fail-closed sentinel after the final provider and credential are known.
|
|
2235
2535
|
const identityScope = codexLogAccountId(authCtx);
|
|
2236
2536
|
if (identityScope) parsed._cursorIdentityScope = identityScope;
|
|
2237
2537
|
subagentFallbackAccountId = authCtx.kind === "pool" || authCtx.kind === "main-pool"
|
|
@@ -2345,6 +2645,9 @@ async function handleResponsesInner(
|
|
|
2345
2645
|
codexAuthContext: authCtx,
|
|
2346
2646
|
forwardHeaders: selectedForwardHeaders,
|
|
2347
2647
|
});
|
|
2648
|
+
if (!logCtx.conversationId && parsed._cursorConversationId) {
|
|
2649
|
+
logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId);
|
|
2650
|
+
}
|
|
2348
2651
|
logCtx.providerAdapter = adapter.name;
|
|
2349
2652
|
// Ordinary requests receive one durable attempt only after their final initial
|
|
2350
2653
|
// adapter is resolved. Combo children own their attempt and retries keep it.
|
|
@@ -2445,24 +2748,19 @@ async function handleResponsesInner(
|
|
|
2445
2748
|
emitted?: OcxProviderContinuationState,
|
|
2446
2749
|
): OcxProviderContinuationState | undefined => {
|
|
2447
2750
|
const cursorConversationId = parsed._cursorConversationId;
|
|
2448
|
-
const inherited = parsed._providerContinuation;
|
|
2449
|
-
|
|
2450
|
-
return
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
...(cursorConversationId
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
conversationId: cursorConversationId,
|
|
2462
|
-
},
|
|
2463
|
-
}
|
|
2464
|
-
: {}),
|
|
2465
|
-
};
|
|
2751
|
+
const inherited = providerContinuationPayload(parsed._providerContinuation);
|
|
2752
|
+
const emittedPayload = providerContinuationPayload(emitted);
|
|
2753
|
+
if (!emittedPayload && !inherited && !cursorConversationId) return undefined;
|
|
2754
|
+
const merged = mergeProviderContinuationPayload(
|
|
2755
|
+
inherited ?? {},
|
|
2756
|
+
emittedPayload ?? {},
|
|
2757
|
+
) as OcxProviderContinuationState;
|
|
2758
|
+
if (cursorConversationId) {
|
|
2759
|
+
merged.cursor = { ...(merged.cursor ?? {}), conversationId: cursorConversationId };
|
|
2760
|
+
}
|
|
2761
|
+
return parsed._providerContinuationOwner
|
|
2762
|
+
? { ...merged, __ocxOwner: { ...parsed._providerContinuationOwner } }
|
|
2763
|
+
: merged;
|
|
2466
2764
|
};
|
|
2467
2765
|
|
|
2468
2766
|
// Remote compaction v2 on a ROUTED model: Codex sent `compaction_trigger` and requires exactly
|
|
@@ -2475,6 +2773,9 @@ async function handleResponsesInner(
|
|
|
2475
2773
|
// message, and leave Codex fataling on a missing compaction item (#422).
|
|
2476
2774
|
const routedCompaction = parsed._compactionRequest === true
|
|
2477
2775
|
&& !isCanonicalOpenAiForwardProvider(route.provider);
|
|
2776
|
+
const commitReasoningReplayServingRoute = (): void => {
|
|
2777
|
+
commitReasoningReplayServingIdentity(parsed._reasoningReplayScope);
|
|
2778
|
+
};
|
|
2478
2779
|
if (routedCompaction) {
|
|
2479
2780
|
delete parsed.context.tools;
|
|
2480
2781
|
delete parsed._webSearch;
|
|
@@ -2492,6 +2793,11 @@ async function handleResponsesInner(
|
|
|
2492
2793
|
parsed.context.messages.push({ role: "user", content: COMPACT_PROMPT, timestamp: Date.now() });
|
|
2493
2794
|
}
|
|
2494
2795
|
|
|
2796
|
+
let routedNamespaceToolAliases: RoutedNamespaceToolAliases = new Map();
|
|
2797
|
+
const refreshRoutedNamespaceToolAliases = (builtRequest: AdapterRequest): void => {
|
|
2798
|
+
routedNamespaceToolAliases = builtRequest.convertedRoutedNamespaceToolAliases ?? new Map();
|
|
2799
|
+
};
|
|
2800
|
+
|
|
2495
2801
|
if ("passthrough" in adapter && adapter.passthrough && !routedCompaction) {
|
|
2496
2802
|
let hostAdmissionLease = pendingHostAdmissionLease;
|
|
2497
2803
|
pendingHostAdmissionLease = null;
|
|
@@ -2527,11 +2833,21 @@ async function handleResponsesInner(
|
|
|
2527
2833
|
request = await adapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
|
|
2528
2834
|
} catch (error) {
|
|
2529
2835
|
releaseCodexAuthContextProbeLease(authCtx);
|
|
2836
|
+
// A tool catalog this proxy cannot lower onto one wire namespace is a client input error, and
|
|
2837
|
+
// the rotation-rebuild and bridged paths already answer 400 for the identical throw. Rethrowing
|
|
2838
|
+
// it here escaped every catch up to the Bun handler, so the same request produced an
|
|
2839
|
+
// unstructured 500 — and no request log — depending only on whether a rotation ran first.
|
|
2840
|
+
if (error instanceof NamespaceToolCollisionError) {
|
|
2841
|
+
return formatErrorResponse(400, "invalid_request_error", redactSecretString(error.message));
|
|
2842
|
+
}
|
|
2530
2843
|
throw error;
|
|
2531
2844
|
}
|
|
2532
|
-
if (route.provider
|
|
2845
|
+
if (!isCanonicalOpenAiForwardProvider(route.provider)) {
|
|
2533
2846
|
for (const name of request.convertedRoutedCustomToolNames ?? []) {
|
|
2534
|
-
if (
|
|
2847
|
+
if (
|
|
2848
|
+
toolBridgeMaps.freeformToolNames.has(name)
|
|
2849
|
+
|| toolBridgeMaps.toolNsMap.get(name)?.freeform === true
|
|
2850
|
+
) routedCustomToolNames.add(name);
|
|
2535
2851
|
}
|
|
2536
2852
|
}
|
|
2537
2853
|
for (const name of request.convertedRoutedToolSearchNames ?? []) {
|
|
@@ -2540,6 +2856,7 @@ async function handleResponsesInner(
|
|
|
2540
2856
|
// would incorrectly disable restoration for the exact ambiguous-name case the alias fixes.
|
|
2541
2857
|
routedToolSearchNames.add(name);
|
|
2542
2858
|
}
|
|
2859
|
+
refreshRoutedNamespaceToolAliases(request);
|
|
2543
2860
|
// #1700: the bridged paths refuse a call to a tool the request never declared
|
|
2544
2861
|
// (`declaredToolNames`, src/bridge.ts). The passthrough had no equivalent, so a routed
|
|
2545
2862
|
// provider's top-level `apply_patch` — which under Codex code mode exists only as a nested
|
|
@@ -2554,14 +2871,17 @@ async function handleResponsesInner(
|
|
|
2554
2871
|
// with no tools and Copilot answers with a `custom_tool_call` for `apply_patch`. Policing an
|
|
2555
2872
|
// empty catalog truncates that turn. An unreadable body lands here too, since it yields no
|
|
2556
2873
|
// names either.
|
|
2557
|
-
const
|
|
2874
|
+
const parseOutboundRequestBody = (bodyText: string): Record<string, unknown> | undefined => {
|
|
2558
2875
|
try {
|
|
2559
|
-
const body = JSON.parse(
|
|
2560
|
-
return body && typeof body === "object" && !Array.isArray(body)
|
|
2876
|
+
const body = JSON.parse(bodyText) as unknown;
|
|
2877
|
+
return body && typeof body === "object" && !Array.isArray(body)
|
|
2878
|
+
? body as Record<string, unknown>
|
|
2879
|
+
: undefined;
|
|
2561
2880
|
} catch {
|
|
2562
2881
|
return undefined;
|
|
2563
2882
|
}
|
|
2564
|
-
}
|
|
2883
|
+
};
|
|
2884
|
+
let outboundRequestBody = parseOutboundRequestBody(request.body);
|
|
2565
2885
|
const declaredWireToolNames = collectDeclaredWireToolNames(outboundRequestBody);
|
|
2566
2886
|
// Union with the caller's own catalog, because the outbound body is not always a complete
|
|
2567
2887
|
// record of it: hosted-tool preference REPLACES a client tool with its hosted form, so a
|
|
@@ -2643,7 +2963,7 @@ async function handleResponsesInner(
|
|
|
2643
2963
|
}
|
|
2644
2964
|
hostAdmissionLease = null;
|
|
2645
2965
|
};
|
|
2646
|
-
|
|
2966
|
+
let passthroughEstimate = typeof request.usageLog?.inputTokens === "number"
|
|
2647
2967
|
? request.usageLog.inputTokens
|
|
2648
2968
|
: undefined;
|
|
2649
2969
|
if (passthroughEstimate !== undefined) {
|
|
@@ -2730,11 +3050,89 @@ async function handleResponsesInner(
|
|
|
2730
3050
|
request.releaseBodyObservation?.();
|
|
2731
3051
|
}
|
|
2732
3052
|
|
|
3053
|
+
const opaqueBlobRecoveryGuard: OpaqueBlobRecoveryGuard = { attempted: false };
|
|
3054
|
+
let oauth401ReplayAttempted = false;
|
|
3055
|
+
const rateLimitPolicy = rateLimitRetryPolicyFor(route.provider);
|
|
3056
|
+
let rateLimitRetries = 0;
|
|
3057
|
+
const rebuildAndRefetch = async (
|
|
3058
|
+
recovery: AttemptRecoveryKind,
|
|
3059
|
+
): Promise<Response | { failed: Response }> => {
|
|
3060
|
+
const retryAdapter = resolveAdapter(
|
|
3061
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
3062
|
+
config.cacheRetention,
|
|
3063
|
+
);
|
|
3064
|
+
if (!("passthrough" in retryAdapter) || !retryAdapter.passthrough) {
|
|
3065
|
+
upstream.abort();
|
|
3066
|
+
return { failed: formatErrorResponse(502, "upstream_error", "Recovery changed the provider wire unexpectedly") };
|
|
3067
|
+
}
|
|
3068
|
+
try {
|
|
3069
|
+
request = await retryAdapter.buildRequest(parsed, {
|
|
3070
|
+
headers: selectedForwardHeaders,
|
|
3071
|
+
translatorBudget,
|
|
3072
|
+
});
|
|
3073
|
+
refreshRoutedNamespaceToolAliases(request);
|
|
3074
|
+
recordAdapterReasoning(logCtx, request);
|
|
3075
|
+
recordAdapterTier(logCtx, request);
|
|
3076
|
+
} catch (err) {
|
|
3077
|
+
upstream.abort();
|
|
3078
|
+
if (options.abortSignal?.aborted) return { failed: clientCancelledResponse() };
|
|
3079
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3080
|
+
return { failed: formatErrorResponse(400, "invalid_request_error", redactSecretString(msg)) };
|
|
3081
|
+
}
|
|
3082
|
+
passthroughEstimate = typeof request.usageLog?.inputTokens === "number"
|
|
3083
|
+
? request.usageLog.inputTokens
|
|
3084
|
+
: undefined;
|
|
3085
|
+
if (passthroughEstimate !== undefined) logCtx.usageLogInputTokens = passthroughEstimate;
|
|
3086
|
+
outboundRequestBody = parseOutboundRequestBody(request.body);
|
|
3087
|
+
logCtx.providerAdapter = retryAdapter.name;
|
|
3088
|
+
sealRequestAttemptIdentity(
|
|
3089
|
+
logCtx.activeAttempt,
|
|
3090
|
+
logCtx.provider,
|
|
3091
|
+
retryAdapter.name,
|
|
3092
|
+
logCtx.accountLogLabel,
|
|
3093
|
+
);
|
|
3094
|
+
try {
|
|
3095
|
+
return await fetchWithTransientRetry(
|
|
3096
|
+
innerRecovery => {
|
|
3097
|
+
noteAttemptSend(logCtx.activeAttempt, passthroughEstimate, innerRecovery ?? recovery);
|
|
3098
|
+
return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({
|
|
3099
|
+
method: request.method,
|
|
3100
|
+
headers: request.headers,
|
|
3101
|
+
body: request.body,
|
|
3102
|
+
}, innerRecovery), upstream.signal, connectMs, parsed.stream,
|
|
3103
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
3104
|
+
providerName: route.providerName,
|
|
3105
|
+
modelId: route.modelId,
|
|
3106
|
+
}),
|
|
3107
|
+
route.provider.authMode === "forward")
|
|
3108
|
+
.then(response => {
|
|
3109
|
+
settleObservedHostResponse();
|
|
3110
|
+
return response;
|
|
3111
|
+
});
|
|
3112
|
+
},
|
|
3113
|
+
{ abortSignal: upstream.signal, label: safeHostLabel(request.url) },
|
|
3114
|
+
);
|
|
3115
|
+
} catch (err) {
|
|
3116
|
+
return { failed: transportFailureResponse(err) };
|
|
3117
|
+
} finally {
|
|
3118
|
+
request.releaseBodyObservation?.();
|
|
3119
|
+
}
|
|
3120
|
+
};
|
|
3121
|
+
|
|
3122
|
+
// Keep recovery kinds in sync with the generic `recovery:` loop below.
|
|
3123
|
+
passthroughRecovery: for (;;) {
|
|
3124
|
+
|
|
2733
3125
|
// Native Responses providers return before the generic adapter recovery loop below. Keep
|
|
2734
3126
|
// their OAuth contract identical: one pre-stream 401 forces a credential refresh and one
|
|
2735
3127
|
// rebuilt replay. xAI's current subscription models use this branch now that their official
|
|
2736
3128
|
// Grok CLI catalog declares the Responses backend.
|
|
2737
|
-
if (
|
|
3129
|
+
if (
|
|
3130
|
+
upstreamResponse.status === 401
|
|
3131
|
+
&& isOAuth401ReplayProvider
|
|
3132
|
+
&& sentOAuthSnapshot
|
|
3133
|
+
&& !oauth401ReplayAttempted
|
|
3134
|
+
) {
|
|
3135
|
+
oauth401ReplayAttempted = true;
|
|
2738
3136
|
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
2739
3137
|
let refreshed: OAuthAccessSnapshot;
|
|
2740
3138
|
try {
|
|
@@ -2786,6 +3184,7 @@ async function handleResponsesInner(
|
|
|
2786
3184
|
headers: selectedForwardHeaders,
|
|
2787
3185
|
translatorBudget,
|
|
2788
3186
|
});
|
|
3187
|
+
refreshRoutedNamespaceToolAliases(request);
|
|
2789
3188
|
recordAdapterReasoning(logCtx, request);
|
|
2790
3189
|
recordAdapterTier(logCtx, request);
|
|
2791
3190
|
} catch (err) {
|
|
@@ -2828,8 +3227,6 @@ async function handleResponsesInner(
|
|
|
2828
3227
|
// immediately with no same-key replay. Pre-stream only — nothing has been relayed yet, so
|
|
2829
3228
|
// the replay is lossless (same invariant as the recovery loop). Forward/OAuth providers
|
|
2830
3229
|
// keep their pool logic below (rateLimitRetryPolicyFor returns null for them).
|
|
2831
|
-
const rateLimitPolicy = rateLimitRetryPolicyFor(route.provider);
|
|
2832
|
-
let rateLimitRetries = 0;
|
|
2833
3230
|
while (
|
|
2834
3231
|
upstreamResponse.status === 429
|
|
2835
3232
|
&& rateLimitPolicy !== null
|
|
@@ -2884,6 +3281,29 @@ async function handleResponsesInner(
|
|
|
2884
3281
|
}
|
|
2885
3282
|
}
|
|
2886
3283
|
|
|
3284
|
+
const captureAffinityResponse = (
|
|
3285
|
+
response: Response,
|
|
3286
|
+
captureAuthCtx: CodexAuthContext = authCtx,
|
|
3287
|
+
captureRequest: Awaited<ReturnType<typeof adapter.buildRequest>> = request,
|
|
3288
|
+
credentialSubstituted = substituteMainCredential
|
|
3289
|
+
|| captureAuthCtx.kind === "pool"
|
|
3290
|
+
|| captureAuthCtx.kind === "main-pool",
|
|
3291
|
+
): void => {
|
|
3292
|
+
if (!isCanonicalOpenAiForwardProvider(route.provider)) return;
|
|
3293
|
+
captureCodexAffinityDiagnostic({
|
|
3294
|
+
inboundHeaders: req.headers,
|
|
3295
|
+
outboundHeaders: captureRequest.headers,
|
|
3296
|
+
authKind: captureAuthCtx.kind,
|
|
3297
|
+
accountMode: route.codexAccountMode,
|
|
3298
|
+
fixedAccount: isFixedCodexAccount(captureAuthCtx),
|
|
3299
|
+
credentialSubstituted,
|
|
3300
|
+
accountGatedModel: ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(route.modelId),
|
|
3301
|
+
wireModelNormalized: parsed.modelId !== route.modelId,
|
|
3302
|
+
status: response.status,
|
|
3303
|
+
});
|
|
3304
|
+
};
|
|
3305
|
+
captureAffinityResponse(upstreamResponse);
|
|
3306
|
+
|
|
2887
3307
|
if (usesCodexForwardPoolAuth(authCtx, route.provider)) {
|
|
2888
3308
|
let poolRetryOutcome: number | undefined;
|
|
2889
3309
|
if (await shouldRetryCodexPoolAccountModel400(
|
|
@@ -2912,6 +3332,9 @@ async function handleResponsesInner(
|
|
|
2912
3332
|
connectMs,
|
|
2913
3333
|
passthroughEstimate,
|
|
2914
3334
|
stream: parsed.stream,
|
|
3335
|
+
onResponse: (response, retryAuthCtx, retryRequest) => {
|
|
3336
|
+
captureAffinityResponse(response, retryAuthCtx, retryRequest, true);
|
|
3337
|
+
},
|
|
2915
3338
|
});
|
|
2916
3339
|
if (retry.kind === "transport") {
|
|
2917
3340
|
authCtx = retry.authCtx;
|
|
@@ -2920,6 +3343,7 @@ async function handleResponsesInner(
|
|
|
2920
3343
|
if (retry.kind === "retried") {
|
|
2921
3344
|
authCtx = retry.authCtx;
|
|
2922
3345
|
request = retry.request;
|
|
3346
|
+
refreshRoutedNamespaceToolAliases(request);
|
|
2923
3347
|
upstreamResponse = retry.upstreamResponse;
|
|
2924
3348
|
selectedForwardHeaders = retry.selectedForwardHeaders;
|
|
2925
3349
|
// Keep subagent quota-failure health keyed to the account that actually served.
|
|
@@ -2927,6 +3351,25 @@ async function handleResponsesInner(
|
|
|
2927
3351
|
}
|
|
2928
3352
|
}
|
|
2929
3353
|
}
|
|
3354
|
+
// The deterministic route record cannot classify history it never observed (restart, expiry,
|
|
3355
|
+
// eviction, or an older transcript). Inspect only a bounded clone of a 4xx whose exact outbound
|
|
3356
|
+
// Responses body still carries opaque state, then rebuild once through the ordinary adapter
|
|
3357
|
+
// sanitation path. A second rejection falls through unchanged because the guard stays armed.
|
|
3358
|
+
const opaqueBlobRecovery = await attemptOpaqueBlobRecovery({
|
|
3359
|
+
response: upstreamResponse,
|
|
3360
|
+
outboundBody: request.body,
|
|
3361
|
+
adapterName: adapter.name,
|
|
3362
|
+
parsed,
|
|
3363
|
+
guard: opaqueBlobRecoveryGuard,
|
|
3364
|
+
signal: upstream.signal,
|
|
3365
|
+
}, rebuildAndRefetch);
|
|
3366
|
+
if (opaqueBlobRecovery.kind === "failed") return opaqueBlobRecovery.response;
|
|
3367
|
+
if (opaqueBlobRecovery.kind === "recovered") {
|
|
3368
|
+
upstreamResponse = opaqueBlobRecovery.response;
|
|
3369
|
+
continue passthroughRecovery;
|
|
3370
|
+
}
|
|
3371
|
+
break;
|
|
3372
|
+
}
|
|
2930
3373
|
const headers = sanitizePassthroughHeaders(upstreamResponse.headers);
|
|
2931
3374
|
const resolvedModel = headers.get("openai-model")?.trim();
|
|
2932
3375
|
if (resolvedModel) logCtx.resolvedModel = resolvedModel;
|
|
@@ -3050,6 +3493,10 @@ async function handleResponsesInner(
|
|
|
3050
3493
|
// devlog/_fin/260731_macos_rss_retention/100_darwin_eager_optin.md).
|
|
3051
3494
|
// The bundled known-bad runtime remains on tee by default on both platforms.
|
|
3052
3495
|
if (isEventStream && upstreamResponse.body) {
|
|
3496
|
+
// For streamed passthrough, a successful terminal response means non-error upstream status
|
|
3497
|
+
// before relay starts. Waiting for SSE completion would retain request state across the whole
|
|
3498
|
+
// stream; a later body failure does not undo that this destination accepted and served the turn.
|
|
3499
|
+
commitReasoningReplayServingRoute();
|
|
3053
3500
|
const terminalRepairPolicy = providerModelResponsesTerminalRepair(
|
|
3054
3501
|
route.providerName,
|
|
3055
3502
|
route.provider,
|
|
@@ -3074,6 +3521,9 @@ async function handleResponsesInner(
|
|
|
3074
3521
|
// Compose opt-in payload rewrites into one parse/stringify pass (image-gen restore first).
|
|
3075
3522
|
const payloadRewrites = [
|
|
3076
3523
|
createImageGenCallRestoreRewrite(imageGenCallAliases),
|
|
3524
|
+
routedNamespaceToolAliases.size > 0
|
|
3525
|
+
? createRoutedNamespaceCallRestoreRewrite(routedNamespaceToolAliases)
|
|
3526
|
+
: undefined,
|
|
3077
3527
|
hasResponsesItemIdRepair(repairConfig)
|
|
3078
3528
|
? createResponsesItemIdPayloadRewrite(repairConfig!, translatorBudget)
|
|
3079
3529
|
: undefined,
|
|
@@ -3290,8 +3740,12 @@ async function handleResponsesInner(
|
|
|
3290
3740
|
const text = bounded.text;
|
|
3291
3741
|
inspectResponseLogJson(logCtx, text);
|
|
3292
3742
|
const clientJson = (() => {
|
|
3293
|
-
const
|
|
3743
|
+
const restoredNamespace = restoreRoutedNamespaceCallsInJson(
|
|
3294
3744
|
restoreImageGenCallsInJson(text, imageGenCallAliases),
|
|
3745
|
+
routedNamespaceToolAliases,
|
|
3746
|
+
);
|
|
3747
|
+
const restored = restoreRoutedCustomCallsInJson(
|
|
3748
|
+
restoredNamespace,
|
|
3295
3749
|
routedCustomToolNames,
|
|
3296
3750
|
);
|
|
3297
3751
|
const restoredToolSearch = restoreRoutedToolSearchCallsInJson(
|
|
@@ -3328,6 +3782,7 @@ async function handleResponsesInner(
|
|
|
3328
3782
|
return formatErrorResponse(502, "upstream_error", undeclaredToolCallMessage(undeclared));
|
|
3329
3783
|
}
|
|
3330
3784
|
}
|
|
3785
|
+
commitReasoningReplayServingRoute();
|
|
3331
3786
|
if (rememberPassthroughResponseChecked) {
|
|
3332
3787
|
try {
|
|
3333
3788
|
rememberPassthroughResponseChecked(
|
|
@@ -3409,6 +3864,9 @@ async function handleResponsesInner(
|
|
|
3409
3864
|
headers,
|
|
3410
3865
|
});
|
|
3411
3866
|
}
|
|
3867
|
+
// An unclassified passthrough body is relayed directly and has no bounded completion observer;
|
|
3868
|
+
// use the same non-error-status success boundary as SSE instead of retaining per-stream state.
|
|
3869
|
+
commitReasoningReplayServingRoute();
|
|
3412
3870
|
const body = relayWithAbort(upstreamResponse.body, upstream);
|
|
3413
3871
|
const turnAc = new AbortController();
|
|
3414
3872
|
const tracked = body ? trackStreamLifetime(body, turnAc, undefined, options.turnAdmissionLease) : null;
|
|
@@ -3554,13 +4012,15 @@ async function handleResponsesInner(
|
|
|
3554
4012
|
retryOn429Policy: rateLimitRetryPolicyFor(route.provider),
|
|
3555
4013
|
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
3556
4014
|
...(options.forceEmptyResponseId ? { forceEmptyResponseId: true } : {}),
|
|
3557
|
-
onCompletedResponse: (response, providerState) =>
|
|
4015
|
+
onCompletedResponse: (response, providerState) => {
|
|
4016
|
+
commitReasoningReplayServingRoute();
|
|
3558
4017
|
rememberResponseState(
|
|
3559
4018
|
parsed._rawBody,
|
|
3560
4019
|
response,
|
|
3561
4020
|
continuationStateForResponse(providerState),
|
|
3562
4021
|
responseStateOptions(adapterNeedsForcedContinuation(adapter.name)),
|
|
3563
|
-
)
|
|
4022
|
+
);
|
|
4023
|
+
},
|
|
3564
4024
|
});
|
|
3565
4025
|
if (imgResponse.body) {
|
|
3566
4026
|
const imgTurnAc = new AbortController();
|
|
@@ -3586,6 +4046,11 @@ async function handleResponsesInner(
|
|
|
3586
4046
|
backend: wsPlan.backend,
|
|
3587
4047
|
forwardProvider: wsPlan.forwardSidecar?.provider,
|
|
3588
4048
|
anthropicSidecar: wsPlan.anthropicSidecar,
|
|
4049
|
+
xaiSidecar: wsPlan.xaiSidecar,
|
|
4050
|
+
geminiSidecar: wsPlan.geminiSidecar,
|
|
4051
|
+
xaiSearchOptions: wsPlan.xaiSearchOptions,
|
|
4052
|
+
// The exa key never rides the plan: read it from config at unpack time (L9).
|
|
4053
|
+
...(wsPlan.exaConfigured ? { exaApiKey: config.webSearchSidecar?.exaApiKey } : {}),
|
|
3589
4054
|
hostedTool: wsPlan.hostedTool,
|
|
3590
4055
|
selectedForwardHeaders: wsPlan.forwardSidecar?.headers ?? selectedForwardHeaders,
|
|
3591
4056
|
settings: wsPlan.settings,
|
|
@@ -3633,6 +4098,7 @@ async function handleResponsesInner(
|
|
|
3633
4098
|
return rotatedAdapter;
|
|
3634
4099
|
},
|
|
3635
4100
|
retryOn429Policy: rateLimitRetryPolicyFor(route.provider),
|
|
4101
|
+
onCompletedResponse: commitReasoningReplayServingRoute,
|
|
3636
4102
|
});
|
|
3637
4103
|
// Register the sidecar stream as an active turn so drainAndShutdown waits for (or aborts)
|
|
3638
4104
|
// in-flight web-search turns instead of skipping them during graceful shutdown.
|
|
@@ -3796,15 +4262,17 @@ async function handleResponsesInner(
|
|
|
3796
4262
|
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
3797
4263
|
}
|
|
3798
4264
|
},
|
|
3799
|
-
|
|
3800
|
-
|
|
4265
|
+
onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => {
|
|
4266
|
+
commitReasoningReplayServingRoute();
|
|
4267
|
+
if (!routedCompaction) {
|
|
3801
4268
|
rememberResponseState(
|
|
3802
4269
|
parsed._rawBody,
|
|
3803
4270
|
response,
|
|
3804
4271
|
continuationStateForResponse(providerState),
|
|
3805
4272
|
responseStateOptions(adapterNeedsForcedContinuation(adapter.name)),
|
|
3806
|
-
)
|
|
3807
|
-
|
|
4273
|
+
);
|
|
4274
|
+
}
|
|
4275
|
+
},
|
|
3808
4276
|
},
|
|
3809
4277
|
);
|
|
3810
4278
|
const bridgeTurnAc = new AbortController();
|
|
@@ -3867,6 +4335,9 @@ async function handleResponsesInner(
|
|
|
3867
4335
|
// buildResponseJSON; bound the durability window before the JSON becomes
|
|
3868
4336
|
// externally visible.
|
|
3869
4337
|
await awaitThoughtSignatureDurability();
|
|
4338
|
+
if (adapterResponseReachedServingTerminal(events, json)) {
|
|
4339
|
+
commitReasoningReplayServingRoute();
|
|
4340
|
+
}
|
|
3870
4341
|
return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
|
|
3871
4342
|
}
|
|
3872
4343
|
|
|
@@ -3889,6 +4360,7 @@ async function handleResponsesInner(
|
|
|
3889
4360
|
let inputTokenEstimate: number | undefined;
|
|
3890
4361
|
try {
|
|
3891
4362
|
initialRequest = await activeAdapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
|
|
4363
|
+
refreshRoutedNamespaceToolAliases(initialRequest);
|
|
3892
4364
|
recordAdapterReasoning(logCtx, initialRequest);
|
|
3893
4365
|
recordAdapterTier(logCtx, initialRequest);
|
|
3894
4366
|
inputTokenEstimate = typeof initialRequest.usageLog?.inputTokens === "number"
|
|
@@ -3977,12 +4449,14 @@ async function handleResponsesInner(
|
|
|
3977
4449
|
// main request clear a 413 must not be forgotten on the very next continuation build.
|
|
3978
4450
|
let imageTierBias = 0;
|
|
3979
4451
|
if (!upstreamResponse.ok) {
|
|
3980
|
-
// Recovery loop: multi-key 429 failover + at most ONE
|
|
4452
|
+
// Recovery loop: multi-key 429 failover + at most ONE opaque-state rebuild and ONE
|
|
4453
|
+
// anthropic 413 tightened retry
|
|
3981
4454
|
// (devlog/260714_image_normalization_pipeline/030). One mutable activeAdapter serves
|
|
3982
4455
|
// both paths so a 429→413 sequence never rebuilds against a stale pre-rotation
|
|
3983
4456
|
// adapter, and imageTierBias — once armed — rides EVERY subsequent rebuild so a
|
|
3984
4457
|
// 413→429 rotation cannot silently undo the tightening.
|
|
3985
4458
|
let imageRetryAttempted = false;
|
|
4459
|
+
const opaqueBlobRecoveryGuard: OpaqueBlobRecoveryGuard = { attempted: false };
|
|
3986
4460
|
let oauth401ReplayAttempted = false;
|
|
3987
4461
|
/**
|
|
3988
4462
|
* Rebuild the request from the current parsed input (and any image-tier bias) and refetch
|
|
@@ -4019,6 +4493,7 @@ async function handleResponsesInner(
|
|
|
4019
4493
|
sameTargetParsed = parsed;
|
|
4020
4494
|
sameTargetToken = transportToken;
|
|
4021
4495
|
}
|
|
4496
|
+
refreshRoutedNamespaceToolAliases(retryRequest);
|
|
4022
4497
|
const retryEstimate = typeof retryRequest.usageLog?.inputTokens === "number"
|
|
4023
4498
|
? retryRequest.usageLog.inputTokens
|
|
4024
4499
|
: undefined;
|
|
@@ -4060,6 +4535,7 @@ async function handleResponsesInner(
|
|
|
4060
4535
|
return { failed: formatErrorResponse(502, "upstream_error", msg) };
|
|
4061
4536
|
}
|
|
4062
4537
|
};
|
|
4538
|
+
// Keep recovery kinds in sync with the native Responses `passthroughRecovery:` loop above.
|
|
4063
4539
|
recovery: for (;;) {
|
|
4064
4540
|
if (
|
|
4065
4541
|
upstreamResponse.status === 401
|
|
@@ -4215,6 +4691,27 @@ async function handleResponsesInner(
|
|
|
4215
4691
|
break;
|
|
4216
4692
|
}
|
|
4217
4693
|
}
|
|
4694
|
+
// Unknown provenance is deliberately fail-soft in pre-flight: after a restart, TTL expiry,
|
|
4695
|
+
// or LRU eviction, a valid same-backend blob must survive. A decoder's own 4xx identity is
|
|
4696
|
+
// the missing authoritative signal. Rebuild once through the same sanitation path used by a
|
|
4697
|
+
// known route switch; invalidating is mandatory because `parsed` mutates in place and the
|
|
4698
|
+
// same-target cache would otherwise replay the rejected bytes verbatim.
|
|
4699
|
+
const opaqueBlobRecovery = await attemptOpaqueBlobRecovery({
|
|
4700
|
+
response: upstreamResponse,
|
|
4701
|
+
outboundBody: sameTargetRequest?.body,
|
|
4702
|
+
adapterName: activeAdapter.name,
|
|
4703
|
+
parsed,
|
|
4704
|
+
guard: opaqueBlobRecoveryGuard,
|
|
4705
|
+
signal: upstream.signal,
|
|
4706
|
+
}, recovery => {
|
|
4707
|
+
invalidateSameTargetRequest();
|
|
4708
|
+
return rebuildAndRefetch(recovery);
|
|
4709
|
+
});
|
|
4710
|
+
if (opaqueBlobRecovery.kind === "failed") return opaqueBlobRecovery.response;
|
|
4711
|
+
if (opaqueBlobRecovery.kind === "recovered") {
|
|
4712
|
+
upstreamResponse = opaqueBlobRecovery.response;
|
|
4713
|
+
continue recovery;
|
|
4714
|
+
}
|
|
4218
4715
|
// Anthropic 413 request_too_large: rebuild once with every image one tier lower
|
|
4219
4716
|
// (spiral guard: single attempt). The biased response re-enters the 429 check above.
|
|
4220
4717
|
if (shouldAttemptImageTierRetry({
|
|
@@ -4475,6 +4972,14 @@ async function handleResponsesInner(
|
|
|
4475
4972
|
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
4476
4973
|
config.cacheRetention,
|
|
4477
4974
|
);
|
|
4975
|
+
bindRouteReasoningReplayScope({
|
|
4976
|
+
parsed: nextParsed,
|
|
4977
|
+
providerName: route.providerName,
|
|
4978
|
+
provider: route.provider,
|
|
4979
|
+
adapterName: activeAdapter.name,
|
|
4980
|
+
});
|
|
4981
|
+
// Response persistence closes over the outer parsed request; keep its owner binding in
|
|
4982
|
+
// sync with the terminal-guard clone that builds the rotated continuation request.
|
|
4478
4983
|
bindRouteReasoningReplayScope({
|
|
4479
4984
|
parsed,
|
|
4480
4985
|
providerName: route.providerName,
|
|
@@ -4636,18 +5141,20 @@ async function handleResponsesInner(
|
|
|
4636
5141
|
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
4637
5142
|
}
|
|
4638
5143
|
},
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
5144
|
+
onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => {
|
|
5145
|
+
commitReasoningReplayServingRoute();
|
|
5146
|
+
// Compaction turns must NOT enter the continuation cache: _rawBody still holds the full
|
|
5147
|
+
// PRE-compaction history, and a later previous_response_id expansion would rehydrate the
|
|
5148
|
+
// giant stale chain Codex just replaced.
|
|
5149
|
+
if (!routedCompaction) {
|
|
4644
5150
|
rememberResponseState(
|
|
4645
5151
|
parsed._rawBody,
|
|
4646
5152
|
response,
|
|
4647
5153
|
continuationStateForResponse(providerState),
|
|
4648
5154
|
responseStateOptions(activeAdapter.name === "kiro"),
|
|
4649
|
-
)
|
|
4650
|
-
|
|
5155
|
+
);
|
|
5156
|
+
}
|
|
5157
|
+
},
|
|
4651
5158
|
},
|
|
4652
5159
|
);
|
|
4653
5160
|
const bridgeTurnAc = new AbortController();
|
|
@@ -4722,6 +5229,9 @@ async function handleResponsesInner(
|
|
|
4722
5229
|
}
|
|
4723
5230
|
// #1926 gap 2: same buffered-path durability bound as the primary branch.
|
|
4724
5231
|
await awaitThoughtSignatureDurability();
|
|
5232
|
+
if (adapterResponseReachedServingTerminal(events, json)) {
|
|
5233
|
+
commitReasoningReplayServingRoute();
|
|
5234
|
+
}
|
|
4725
5235
|
return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
|
|
4726
5236
|
}
|
|
4727
5237
|
|