@bitkyc08/opencodex 2.59.0 → 2.60.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/gui/dist/assets/index-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/request-builder.ts +7 -1
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +3 -1
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +50 -13
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +29 -5
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +37 -6
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +5 -1
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +15 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/bridge/sse.ts +3 -151
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/dispatch.ts +3 -3
- package/src/cli/doctor.ts +28 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +7 -2
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +2 -2
- package/src/client/runtime.ts +26 -2
- package/src/codex/account-store.ts +65 -0
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/retained-sync.ts +22 -26
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +20 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/model-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +29 -10
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing.ts +7 -1
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +18 -0
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/config/schema/leaf-validators.ts +12 -0
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +249 -169
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +50 -2
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +66 -18
- package/src/providers/model-rename-migration.ts +55 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +35 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +9 -0
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-completions.ts +5 -2
- package/src/server/claude-messages.ts +6 -3
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +16 -13
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +119 -10
- package/src/server/request-log.ts +55 -2
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/compact.ts +32 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/passthrough-delivery.ts +31 -14
- package/src/server/responses/passthrough-dispatch.ts +36 -9
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +67 -17
- package/src/server/responses/request-send-budget.ts +5 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +2 -2
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/config.ts +11 -3
- package/src/types/provider.ts +18 -0
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +14 -0
- package/src/types.ts +1 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
|
@@ -13,7 +13,6 @@ import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
|
|
|
13
13
|
import type { RequestLogContext } from "../request-log";
|
|
14
14
|
import type { HandleResponsesOptions } from "./core-options";
|
|
15
15
|
import { prepareEffortNormalization } from "../effort-policy";
|
|
16
|
-
import { providerModelResponsesUpstreamStreaming } from "../../providers/registry";
|
|
17
16
|
import { resolveOpenCodeGoTransport } from "../../providers/opencode-go-transport";
|
|
18
17
|
import { getOrAllocateRequestSessionLane } from "../request-log-conversation";
|
|
19
18
|
import { shouldPreparePlaintextV2AgentMessages } from "../../responses/plaintext-v2-agent-messages";
|
|
@@ -80,6 +79,7 @@ export function canPassThroughEncryptedV2AgentTask(
|
|
|
80
79
|
route.modelId,
|
|
81
80
|
provider,
|
|
82
81
|
inboundWire,
|
|
82
|
+
route.staticPolicy,
|
|
83
83
|
).adapter === "openai-responses";
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -126,6 +126,8 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
126
126
|
const responseModelId = parsed.modelId;
|
|
127
127
|
const preserveAnthropicResponseModel = route.providerName === "anthropic"
|
|
128
128
|
|| route.provider.adapter === "anthropic";
|
|
129
|
+
const finalSelectedModelId = route.modelId;
|
|
130
|
+
const virtualModel = applyOpenAiVirtualModel(parsed, route, logCtx, inboundWire);
|
|
129
131
|
|
|
130
132
|
// Apply the routed model id upstream: routing may strip a "<provider>/" namespace.
|
|
131
133
|
if (route.modelId !== parsed.modelId) {
|
|
@@ -136,11 +138,7 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
136
138
|
}
|
|
137
139
|
// Transport-neutral reliability policy (#875): applies to any Responses
|
|
138
140
|
// upstream whose final adapter is openai-responses, not only WS turns.
|
|
139
|
-
const responsesUpstreamStreaming =
|
|
140
|
-
route.providerName,
|
|
141
|
-
route.provider,
|
|
142
|
-
route.modelId,
|
|
143
|
-
);
|
|
141
|
+
const responsesUpstreamStreaming = route.staticPolicy.model.responsesUpstreamStreaming;
|
|
144
142
|
|
|
145
143
|
// Preserve the routed destination for Go recognition, then settle the wire before
|
|
146
144
|
// deriving protocol-scoped affinity. Recognition must not inspect the flipped adapter.
|
|
@@ -150,6 +148,7 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
150
148
|
route.modelId,
|
|
151
149
|
routedProvider,
|
|
152
150
|
inboundWire,
|
|
151
|
+
route.staticPolicy,
|
|
153
152
|
);
|
|
154
153
|
route.provider = resolveOpenCodeGoTransport(wireProvider,
|
|
155
154
|
args.claudeGoAffinity ? args.claudeGoAffinity.sessionLane : getOrAllocateRequestSessionLane(req),
|
|
@@ -168,7 +167,7 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
168
167
|
|| (!summary && route.provider.showThinkingSummary !== true);
|
|
169
168
|
}
|
|
170
169
|
if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
|
|
171
|
-
logCtx.model = route.modelId;
|
|
170
|
+
logCtx.model = virtualModel?.selectedModelId ?? route.modelId;
|
|
172
171
|
logCtx.provider = route.providerName;
|
|
173
172
|
logCtx.providerAdapter = route.provider.adapter;
|
|
174
173
|
logCtx.routeDecision = route.routeDecision;
|
|
@@ -194,11 +193,6 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
194
193
|
(parsed._rawBody as Record<string, unknown>).store = false;
|
|
195
194
|
}
|
|
196
195
|
|
|
197
|
-
// Final selected model before virtual wire-model rewriting (Pro aliases).
|
|
198
|
-
const finalSelectedModelId = route.modelId;
|
|
199
|
-
|
|
200
|
-
// Virtual model rewriting: Pro aliases → base model + reasoning.mode="pro".
|
|
201
|
-
applyOpenAiVirtualModel(parsed, route, logCtx);
|
|
202
196
|
if (parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId) {
|
|
203
197
|
logCtx.resolvedModel = route.modelId;
|
|
204
198
|
logCtx.preserveResolvedModelFromRoute = true;
|
|
@@ -88,6 +88,7 @@ export function isEncryptedFunctionOutputRejection(bodyText: string): boolean {
|
|
|
88
88
|
try {
|
|
89
89
|
const payload = JSON.parse(bodyText) as unknown;
|
|
90
90
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
91
|
+
if (upstreamErrorMessageFromPayload(payload) === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
|
|
91
92
|
const record = payload as { detail?: unknown; message?: unknown; error?: unknown };
|
|
92
93
|
if (record.detail === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
|
|
93
94
|
if (record.message === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
|
|
@@ -11,6 +11,7 @@ import type { NativeMainRefreshDependencies } from "../../codex/main-account";
|
|
|
11
11
|
import type { InboundWire } from "../../providers/registry";
|
|
12
12
|
import type { ExplicitOpenAiCallerAuth } from "../../providers/openai-sidecar";
|
|
13
13
|
import type { CallerDirectAuth } from "../../providers/caller-authorization";
|
|
14
|
+
import type { CompactionRoutingOverride } from "./compaction-routing";
|
|
14
15
|
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
15
16
|
import type { TransientSendBudget } from "../../lib/upstream-retry";
|
|
16
17
|
import type { RequestLogContext } from "../request-log";
|
|
@@ -51,6 +52,8 @@ export interface HandleResponsesOptions {
|
|
|
51
52
|
admission?: DataPlaneAdmission;
|
|
52
53
|
/** Called at most once after the complete client body is read and accepted for dispatch. */
|
|
53
54
|
onRequestBodyRead?: () => void;
|
|
55
|
+
/** Internal handoff for retry wrappers that must reuse the already-accounted request body. */
|
|
56
|
+
onRequestBodyParsed?: (body: unknown) => void;
|
|
54
57
|
forceEmptyResponseId?: boolean;
|
|
55
58
|
/** Internal, connection-owned control channel; never reconstructed from headers. */
|
|
56
59
|
nativeControl?: NativeResponseControl;
|
|
@@ -105,6 +108,7 @@ export interface HandleResponsesOptions {
|
|
|
105
108
|
callerDirectAuth?: CallerDirectAuth | null;
|
|
106
109
|
/** Internal recursion guard; callers outside this module must not set it. */
|
|
107
110
|
comboAttempt?: boolean;
|
|
111
|
+
compactionRoutingOverride?: CompactionRoutingOverride | null;
|
|
108
112
|
/** Internal combo handoff for one parent-validated continuation snapshot. */
|
|
109
113
|
comboReplaySnapshot?: {
|
|
110
114
|
sourceBody: unknown;
|
|
@@ -98,6 +98,19 @@ import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/cat
|
|
|
98
98
|
|
|
99
99
|
|
|
100
100
|
export function looksLikeBackendCiphertext(payload: string): boolean {
|
|
101
|
+
// Unknown replay history has no authenticity proof. Require the key-independent Fernet wire
|
|
102
|
+
// structure instead of granting ciphertext authority to any long base64-like model output.
|
|
103
|
+
// Proven backend bytes still remain opaque and byte-identical; malformed/plaintext slots are
|
|
104
|
+
// lowered by the compatibility path below rather than poisoning every later native replay.
|
|
105
|
+
return isStructurallyValidFernetToken(payload);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Pre-route compatibility keeps an encoded-looking unknown slot opaque until the destination is
|
|
110
|
+
* known. A routed destination strips that slot instead of exposing possible truncated ciphertext;
|
|
111
|
+
* the canonical backend later applies the stricter structural classifier.
|
|
112
|
+
*/
|
|
113
|
+
function looksLikeUnknownOpaqueSlot(payload: string): boolean {
|
|
101
114
|
return payload.length >= 64 && /^[A-Za-z0-9+/=_-]+$/.test(payload);
|
|
102
115
|
}
|
|
103
116
|
|
|
@@ -480,7 +493,10 @@ function contentWithoutCiphertext(content: unknown[]): unknown[] {
|
|
|
480
493
|
|
|
481
494
|
|
|
482
495
|
|
|
483
|
-
export function sanitizeEncryptedContentInPlace(
|
|
496
|
+
export function sanitizeEncryptedContentInPlace(
|
|
497
|
+
input: unknown,
|
|
498
|
+
options: { preserveUnknownOpaqueSlots?: boolean } = {},
|
|
499
|
+
): number {
|
|
484
500
|
if (!Array.isArray(input)) return 0;
|
|
485
501
|
let rewritten = 0;
|
|
486
502
|
const protectedFragments = new WeakSet<object>();
|
|
@@ -512,7 +528,9 @@ export function sanitizeEncryptedContentInPlace(input: unknown): number {
|
|
|
512
528
|
&& typeof (child as { encrypted_content?: unknown }).encrypted_content === "string"
|
|
513
529
|
) {
|
|
514
530
|
const payload = (child as { encrypted_content: string }).encrypted_content;
|
|
515
|
-
|
|
531
|
+
const preserveUnknown = options.preserveUnknownOpaqueSlots === true
|
|
532
|
+
&& looksLikeUnknownOpaqueSlot(payload);
|
|
533
|
+
if (!protectedFragments.has(child) && !looksLikeBackendCiphertext(payload) && !preserveUnknown) {
|
|
516
534
|
const parts = encryptedSlotParts(payload);
|
|
517
535
|
frame.node.splice(frame.index, 1, ...parts);
|
|
518
536
|
rewritten += 1;
|
|
@@ -35,10 +35,6 @@ import { consumeComboFailure } from "./core-combo-failure";
|
|
|
35
35
|
import { readDisplaySafeErrorText } from "./core-errors";
|
|
36
36
|
import { streamingContextOverflowResponse, jsonContextOverflowResponse } from "./context-overflow";
|
|
37
37
|
import { formatPassthroughUpstreamError } from "./passthrough-error";
|
|
38
|
-
import {
|
|
39
|
-
providerModelResponsesTerminalRepair,
|
|
40
|
-
providerModelResponsesUpstreamStreaming,
|
|
41
|
-
} from "../../providers/registry";
|
|
42
38
|
import {
|
|
43
39
|
resolvePassthroughWebSearchBridgeAuth,
|
|
44
40
|
planPassthroughWebSearchBridge,
|
|
@@ -83,7 +79,10 @@ import {
|
|
|
83
79
|
import { createRoutedCustomToolRestoreBlockRewrite } from "../responses-custom-tool-repair";
|
|
84
80
|
import { createRoutedToolSearchRestoreBlockRewrite } from "../responses-tool-search-repair";
|
|
85
81
|
import { createGithubCopilotResponsesBlockRewrite } from "../github-copilot-responses-repair";
|
|
86
|
-
import {
|
|
82
|
+
import {
|
|
83
|
+
createGrokResponsesControlFrameBlockRewrite,
|
|
84
|
+
createGrokResponsesTimestampBlockRewrite,
|
|
85
|
+
} from "../grok-responses-control-frame";
|
|
87
86
|
import { createGrokResponsesSparseTerminalBlockRewrite } from "../grok-responses-snapshot-repair";
|
|
88
87
|
import {
|
|
89
88
|
createPlaintextV2AgentMessageCallRestoreRewrite,
|
|
@@ -99,6 +98,24 @@ import {
|
|
|
99
98
|
normalizeDefaultNamespaceInJson,
|
|
100
99
|
} from "../responses-undeclared-tool-guard";
|
|
101
100
|
import { isWin32EagerRewrite, selectEagerPath } from "../../lib/bun-stream-caps";
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Platform override for the two relay-path policy calls below. Tests only.
|
|
104
|
+
*
|
|
105
|
+
* The eager relay is reachable only on win32 and darwin, so a Linux shard cannot exercise it
|
|
106
|
+
* without claiming to be one of them. Overwriting `process.platform` globally does that, and a
|
|
107
|
+
* great deal more: every filesystem, ACL and state-directory decision in the process follows it,
|
|
108
|
+
* and the spend-ledger owner lowercases its home on win32, which on a case-sensitive filesystem
|
|
109
|
+
* names a DIFFERENT directory. A row that did that stopped being able to reserve its send and
|
|
110
|
+
* delivered no terminal at all, reporting as a relay defect. This narrows the claim to the two
|
|
111
|
+
* calls that actually choose the relay path.
|
|
112
|
+
*/
|
|
113
|
+
let relayPlatformForTests: NodeJS.Platform | undefined;
|
|
114
|
+
|
|
115
|
+
/** Internal test contract, not operator configuration: no config key reaches this. */
|
|
116
|
+
export function setRelayPlatformForTests(platform: NodeJS.Platform | undefined): void {
|
|
117
|
+
relayPlatformForTests = platform;
|
|
118
|
+
}
|
|
102
119
|
import { linkAbortSignal, UPSTREAM_JSON_BODY_READ_OPTIONS } from "./core-lifetime";
|
|
103
120
|
import { registerTurn, unregisterTurn, trackStreamLifetime } from "../lifecycle";
|
|
104
121
|
import { relaySseEagerBounded } from "../relay-eager";
|
|
@@ -340,11 +357,7 @@ export async function deliverPassthroughResponse(
|
|
|
340
357
|
// before relay starts. Waiting for SSE completion would retain request state across the whole
|
|
341
358
|
// stream; a later body failure does not undo that this destination accepted and served the turn.
|
|
342
359
|
commitReasoningReplayServingRoute(nativeExchange.request.headers);
|
|
343
|
-
const terminalRepairPolicy =
|
|
344
|
-
route.providerName,
|
|
345
|
-
route.provider,
|
|
346
|
-
route.modelId,
|
|
347
|
-
);
|
|
360
|
+
const terminalRepairPolicy = route.staticPolicy.model.responsesTerminalRepair;
|
|
348
361
|
// #3761: opt-in hosted-web-search bridge. Codex always declares the hosted web_search tool,
|
|
349
362
|
// and this branch relays that declaration on the assumption the destination executes it.
|
|
350
363
|
// A KEY-auth gateway that does not (Ollama Cloud GLM) answers with a function_call named
|
|
@@ -498,6 +511,9 @@ export async function deliverPassthroughResponse(
|
|
|
498
511
|
grokClientCompatibilityEnabled
|
|
499
512
|
? createGrokResponsesControlFrameBlockRewrite()
|
|
500
513
|
: undefined,
|
|
514
|
+
grokClientCompatibilityEnabled
|
|
515
|
+
? createGrokResponsesTimestampBlockRewrite()
|
|
516
|
+
: undefined,
|
|
501
517
|
grokClientCompatibilityEnabled
|
|
502
518
|
? createGrokResponsesSparseTerminalBlockRewrite(translatorBudget)
|
|
503
519
|
: undefined,
|
|
@@ -529,12 +545,13 @@ export async function deliverPassthroughResponse(
|
|
|
529
545
|
? composeSseBlockRewrites(...blockRewrites)
|
|
530
546
|
: undefined;
|
|
531
547
|
const needsClientRewrite = clientBlockRewrite !== undefined;
|
|
548
|
+
const relayPlatform = relayPlatformForTests ?? process.platform;
|
|
532
549
|
// #864: win32 rewrite traffic must never enter the tee()+JS-pull chain
|
|
533
550
|
// (Bun#32111 JS-sink segfault — text frames pass, the terminal block is
|
|
534
551
|
// lost). The eager single reader applies the same rewrites inline.
|
|
535
|
-
const win32EagerRewrite = isWin32EagerRewrite(
|
|
552
|
+
const win32EagerRewrite = isWin32EagerRewrite(relayPlatform, needsClientRewrite);
|
|
536
553
|
const eagerPath = selectEagerPath(
|
|
537
|
-
|
|
554
|
+
relayPlatform,
|
|
538
555
|
needsClientRewrite,
|
|
539
556
|
config.streamMode ?? "auto",
|
|
540
557
|
);
|
|
@@ -805,7 +822,7 @@ export async function deliverPassthroughResponse(
|
|
|
805
822
|
// stream that never closes. Non-streaming clients keep the plain JSON.
|
|
806
823
|
if (clientRequestedStream === true
|
|
807
824
|
&& options.inboundTransport !== "websocket"
|
|
808
|
-
&&
|
|
825
|
+
&& route.staticPolicy.model.responsesUpstreamStreaming === false
|
|
809
826
|
&& route.provider.adapter === "openai-responses") {
|
|
810
827
|
let completed: Record<string, unknown> | undefined;
|
|
811
828
|
try {
|
|
@@ -852,7 +869,7 @@ export async function deliverPassthroughResponse(
|
|
|
852
869
|
// WS turns reframe this JSON into events in the bridge, which is the
|
|
853
870
|
// other relay-free path — normalize ids so both bounded-JSON paths agree.
|
|
854
871
|
const outboundJson = options.inboundTransport === "websocket"
|
|
855
|
-
&&
|
|
872
|
+
&& route.staticPolicy.model.responsesUpstreamStreaming === false
|
|
856
873
|
&& hasResponsesItemIdRepair(route.provider.responsesItemIdRepair)
|
|
857
874
|
? (() => {
|
|
858
875
|
try {
|
|
@@ -121,6 +121,7 @@ import { recordCodexUpstreamOutcome } from "../../codex/routing";
|
|
|
121
121
|
import { describeUpstreamConnectFailure } from "./upstream-error";
|
|
122
122
|
import type { OpaqueBlobRecoveryGuard } from "./core-opaque-recovery";
|
|
123
123
|
import {
|
|
124
|
+
isOpenCodeGoDestination,
|
|
124
125
|
rateLimitRetryPolicyFor,
|
|
125
126
|
rateLimitRetryDelayMs,
|
|
126
127
|
transientRetryPolicyFor,
|
|
@@ -142,6 +143,7 @@ import { captureCodexAffinityDiagnostic } from "../../codex/affinity-debug";
|
|
|
142
143
|
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
|
|
143
144
|
import {
|
|
144
145
|
attemptOpaqueBlobRecovery,
|
|
146
|
+
isEncryptedFunctionOutputRejection,
|
|
145
147
|
outboundResponsesBodyCarriesEncryptedFunctionOutput,
|
|
146
148
|
resetStreamedOpaqueBlobLogContext,
|
|
147
149
|
consoleGoUploadRejectionBody,
|
|
@@ -841,7 +843,15 @@ export async function preparePassthroughExchange(
|
|
|
841
843
|
// retry wrapper replaces — proves the host was reached (#914 review).
|
|
842
844
|
.then(adoptObservedResponse);
|
|
843
845
|
},
|
|
844
|
-
{ abortSignal: upstream.signal, label: safeHostLabel(request.url),
|
|
846
|
+
{ abortSignal: upstream.signal, label: safeHostLabel(request.url),
|
|
847
|
+
attempts: remainingTransientSendBudget(transientSendAttempts()), onSendsConsumed: noteTransientSends,
|
|
848
|
+
// The OpenCode Go destination stalls-then-drops inference sends (ambiguous
|
|
849
|
+
// pre-header resets surfacing as refused 429s); its subscription traffic is
|
|
850
|
+
// inference-only, so a bounded reset replay here absorbs the blip instead of
|
|
851
|
+
// failing the turn. Recovery legs keep the fail-closed refusal; only this
|
|
852
|
+
// initial send is replay-eligible. Attempts stay budget-bounded via attempts.
|
|
853
|
+
replaySafe: isOpenCodeGoDestination(route.provider),
|
|
854
|
+
},
|
|
845
855
|
);
|
|
846
856
|
} catch (err) {
|
|
847
857
|
return transportFailureResponse(err);
|
|
@@ -863,7 +873,7 @@ export async function preparePassthroughExchange(
|
|
|
863
873
|
recovery: AttemptRecoveryKind,
|
|
864
874
|
): Promise<Response | { failed: Response }> => {
|
|
865
875
|
const retryAdapter = resolveSelectionAdapter(
|
|
866
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
|
|
876
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
867
877
|
config.cacheRetention,
|
|
868
878
|
);
|
|
869
879
|
if (!("passthrough" in retryAdapter) || !retryAdapter.passthrough) {
|
|
@@ -989,7 +999,7 @@ export async function preparePassthroughExchange(
|
|
|
989
999
|
route.provider = replay.provider;
|
|
990
1000
|
requestState.selectedForwardHeaders = withClaudeNativeSession(replay.headers, replay.provider, options.claudeNativeSessionId);
|
|
991
1001
|
const replayAdapter = resolveSelectionAdapter(
|
|
992
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, replay.provider, inboundWire),
|
|
1002
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, replay.provider, inboundWire, route.staticPolicy),
|
|
993
1003
|
config.cacheRetention,
|
|
994
1004
|
);
|
|
995
1005
|
if (!("passthrough" in replayAdapter) || !replayAdapter.passthrough) {
|
|
@@ -1110,7 +1120,7 @@ export async function preparePassthroughExchange(
|
|
|
1110
1120
|
);
|
|
1111
1121
|
route.provider = refreshedProvider;
|
|
1112
1122
|
const refreshedAdapter = resolveSelectionAdapter(
|
|
1113
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire),
|
|
1123
|
+
resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire, route.staticPolicy),
|
|
1114
1124
|
config.cacheRetention,
|
|
1115
1125
|
);
|
|
1116
1126
|
if (!("passthrough" in refreshedAdapter) || !refreshedAdapter.passthrough) {
|
|
@@ -1340,8 +1350,16 @@ export async function preparePassthroughExchange(
|
|
|
1340
1350
|
// refusal: whatever the entitlement was when upstream declined, it is not that now. Both
|
|
1341
1351
|
// ids are cleared because the wire model can differ from the routed one.
|
|
1342
1352
|
if (upstreamResponse.ok) {
|
|
1343
|
-
clearCodexModelDenialEvidence(
|
|
1344
|
-
|
|
1353
|
+
clearCodexModelDenialEvidence(
|
|
1354
|
+
admissionState.authCtx.accountId,
|
|
1355
|
+
route.modelId,
|
|
1356
|
+
admissionState.authCtx.kind === "pool" ? admissionState.authCtx.generation : undefined,
|
|
1357
|
+
);
|
|
1358
|
+
clearCodexModelDenialEvidence(
|
|
1359
|
+
admissionState.authCtx.accountId,
|
|
1360
|
+
parsed.modelId,
|
|
1361
|
+
admissionState.authCtx.kind === "pool" ? admissionState.authCtx.generation : undefined,
|
|
1362
|
+
);
|
|
1345
1363
|
}
|
|
1346
1364
|
const model400Denial = await codexPoolAccountModel400Denial(
|
|
1347
1365
|
upstreamResponse,
|
|
@@ -1354,7 +1372,11 @@ export async function preparePassthroughExchange(
|
|
|
1354
1372
|
// answer about this model, and the roster cache that selection otherwise reads expires
|
|
1355
1373
|
// five minutes after a catalog sync fills it -- so without remembering this, the next
|
|
1356
1374
|
// request selects the same account on quota alone and takes the same 400 (#4906).
|
|
1357
|
-
recordCodexModelDenialEvidence(
|
|
1375
|
+
recordCodexModelDenialEvidence(
|
|
1376
|
+
admissionState.authCtx.accountId,
|
|
1377
|
+
model400Denial,
|
|
1378
|
+
admissionState.authCtx.kind === "pool" ? admissionState.authCtx.generation : undefined,
|
|
1379
|
+
);
|
|
1358
1380
|
poolRetryOutcome = 400;
|
|
1359
1381
|
} else if (!admissionState.authCtx.fixedAccount && await shouldRetryCodexPoolAccountQuota(
|
|
1360
1382
|
upstreamResponse,
|
|
@@ -1453,8 +1475,13 @@ export async function preparePassthroughExchange(
|
|
|
1453
1475
|
payload => {
|
|
1454
1476
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
1455
1477
|
const type = (payload as { type?: unknown }).type;
|
|
1456
|
-
|
|
1457
|
-
&&
|
|
1478
|
+
const decryptRejection = (type === "error" || type === "response.failed" || type === "response.incomplete")
|
|
1479
|
+
&& isEncryptedFunctionOutputRejection(JSON.stringify(payload));
|
|
1480
|
+
// `detail` is a real WebSocket error shape but the generic preflight failure projector
|
|
1481
|
+
// intentionally understands only Responses error/message fields. Preserve only this
|
|
1482
|
+
// exact identity so the projected 502 can enter the existing single-shot recovery.
|
|
1483
|
+
if (decryptRejection) preflightLog.upstreamError = ENCRYPTED_FUNCTION_OUTPUT_REJECTION;
|
|
1484
|
+
return decryptRejection;
|
|
1458
1485
|
}, {
|
|
1459
1486
|
allowMissingContentType: !recoveryContentType && parsed.stream,
|
|
1460
1487
|
replayReadErrors: true,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { comboFailureDecision } from "../../combos/failover";
|
|
2
2
|
import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
3
|
-
import { readJsonRequestBody, resolveInboundBodyLimitBytes } from "../request-decompress";
|
|
4
3
|
import { finishRequestAttempt, type RequestLogContext } from "../request-log";
|
|
5
4
|
import { linkRequestSessionLane } from "../request-log-conversation";
|
|
6
5
|
import type { OcxConfig } from "../../types";
|
|
@@ -129,6 +128,7 @@ export async function handleResponsesWithPolicyFallback(
|
|
|
129
128
|
const runCore = deps.runCore ?? handleResponsesCore;
|
|
130
129
|
let requestBodyReadNotified = false;
|
|
131
130
|
let storedPool401ReplayDispatched = false;
|
|
131
|
+
let rawBody: Record<string, unknown> | null = null;
|
|
132
132
|
const coreOptions: CoreOptions = {
|
|
133
133
|
...options,
|
|
134
134
|
openAiSidecarAuth: options.openAiSidecarAuth === undefined
|
|
@@ -144,23 +144,15 @@ export async function handleResponsesWithPolicyFallback(
|
|
|
144
144
|
options.onRequestBodyRead?.();
|
|
145
145
|
},
|
|
146
146
|
} : {}),
|
|
147
|
+
onRequestBodyParsed: body => {
|
|
148
|
+
options.onRequestBodyParsed?.(body);
|
|
149
|
+
if (body && typeof body === "object" && !Array.isArray(body)) rawBody = body as Record<string, unknown>;
|
|
150
|
+
},
|
|
147
151
|
onStoredPool401ReplayDispatched: () => {
|
|
148
152
|
storedPool401ReplayDispatched = true;
|
|
149
153
|
options.onStoredPool401ReplayDispatched?.();
|
|
150
154
|
},
|
|
151
155
|
};
|
|
152
|
-
let rawBody: Record<string, unknown> | null = null;
|
|
153
|
-
try {
|
|
154
|
-
const parsed = await readJsonRequestBody(
|
|
155
|
-
req.clone(),
|
|
156
|
-
undefined,
|
|
157
|
-
resolveInboundBodyLimitBytes(config.maxInboundBodyBytes),
|
|
158
|
-
);
|
|
159
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) rawBody = parsed as Record<string, unknown>;
|
|
160
|
-
} catch {
|
|
161
|
-
// Core owns the client-facing parse/decompression error.
|
|
162
|
-
}
|
|
163
|
-
|
|
164
156
|
let response: Response;
|
|
165
157
|
try {
|
|
166
158
|
response = await runCore(req, config, logCtx, coreOptions);
|
|
@@ -41,7 +41,6 @@ import {
|
|
|
41
41
|
import {
|
|
42
42
|
copyPreviousResponseReplayProvenance,
|
|
43
43
|
expandPreviousResponseInput,
|
|
44
|
-
previousResponseScopeMismatch,
|
|
45
44
|
previousResponseReplayFailure,
|
|
46
45
|
markBodyNonPersistable,
|
|
47
46
|
previousResponseProviderState,
|
|
@@ -56,6 +55,7 @@ import { bindTurnTerminationScope, rememberDeliveredFinalAnswer } from "../../re
|
|
|
56
55
|
import { requestLogSpeedLabel, readConfiguredCodexServiceTier } from "../request-log";
|
|
57
56
|
import type { RouteResult } from "../../router";
|
|
58
57
|
import {
|
|
58
|
+
captureRouteStaticPolicy,
|
|
59
59
|
routeConcreteModel,
|
|
60
60
|
routeCompactionModel,
|
|
61
61
|
routeModel,
|
|
@@ -119,6 +119,7 @@ import {
|
|
|
119
119
|
codexLogAccountId,
|
|
120
120
|
} from "./core-codex-account";
|
|
121
121
|
import { acquireUpstreamHostAdmission } from "../../codex/upstream-host-health";
|
|
122
|
+
import { applyCompactionRoutingOverride, compactionRoutingKeepsProviderIdentity } from "./compaction-routing";
|
|
122
123
|
import { codexAuthContextLogLabel } from "../../codex/account-label";
|
|
123
124
|
import {
|
|
124
125
|
conversationStateBindingFromAuth,
|
|
@@ -149,6 +150,13 @@ export async function prepareResponsesRequest(
|
|
|
149
150
|
}
|
|
150
151
|
return decodeRequestErrorResponse(err, "responses");
|
|
151
152
|
}
|
|
153
|
+
if (!options.comboAttempt && !options.compactionRoutingOverride && inboundWire === "responses") {
|
|
154
|
+
options.compactionRoutingOverride = applyCompactionRoutingOverride(body, req.headers, config, {
|
|
155
|
+
endpoint: "responses",
|
|
156
|
+
transport: options.inboundTransport,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
options.onRequestBodyParsed?.(body);
|
|
152
160
|
// An effort row naming a table-less combo (`combo/x--high`) must reach the combo dispatcher
|
|
153
161
|
// as its base id, so the selector is normalized here, before comboIdFromRawBody reads model.
|
|
154
162
|
const comboRows = !options.comboAttempt && body && typeof body === "object" && !Array.isArray(body)
|
|
@@ -180,7 +188,7 @@ export async function prepareResponsesRequest(
|
|
|
180
188
|
}
|
|
181
189
|
// Compaction may send the last client-visible bare model after a combo switch.
|
|
182
190
|
// Configured selectors take precedence; otherwise recall before combo dispatch (#3891).
|
|
183
|
-
if (!options.comboAttempt && body && typeof body === "object" && !Array.isArray(body)) {
|
|
191
|
+
if (!options.comboAttempt && !options.compactionRoutingOverride && body && typeof body === "object" && !Array.isArray(body)) {
|
|
184
192
|
const rawModel = (body as { model?: unknown }).model;
|
|
185
193
|
const rawInput = (body as { input?: unknown }).input;
|
|
186
194
|
const isCompactionTrigger = Array.isArray(rawInput)
|
|
@@ -202,7 +210,7 @@ export async function prepareResponsesRequest(
|
|
|
202
210
|
// hops — which only exist inside that loop — are unreachable (#4129). Rewrite the selector
|
|
203
211
|
// here instead, before comboIdFromRawBody reads `model`, and identify the combo by CONFIG
|
|
204
212
|
// LOOKUP so the check can never observe a one-candidate collapse.
|
|
205
|
-
if (!options.comboAttempt && body && typeof body === "object" && !Array.isArray(body)) {
|
|
213
|
+
if (!options.comboAttempt && !options.compactionRoutingOverride && body && typeof body === "object" && !Array.isArray(body)) {
|
|
206
214
|
const shadowIntercept = config.shadowCallIntercept;
|
|
207
215
|
const rawShadowModel = (body as { model?: unknown }).model;
|
|
208
216
|
if (shadowIntercept?.enabled && shadowIntercept.model && typeof rawShadowModel === "string"
|
|
@@ -243,10 +251,13 @@ export async function prepareResponsesRequest(
|
|
|
243
251
|
copyPreviousResponseReplayProvenance(options.comboReplaySnapshot.sourceBody, body);
|
|
244
252
|
} else {
|
|
245
253
|
body = expandPreviousResponseInput(body, inboundClientThreadId);
|
|
246
|
-
|
|
247
|
-
|
|
254
|
+
const replayFailure = previousResponseReplayFailure(body);
|
|
255
|
+
if (replayFailure?.reason === "scope_mismatch") {
|
|
256
|
+
// Bounded and content-free: no task scope and nothing about the retained entry.
|
|
257
|
+
console.warn("[opencodex] refusing continuation because the client task scope does not match replay state");
|
|
248
258
|
}
|
|
249
|
-
|
|
259
|
+
// Local replay failures require full client replay.
|
|
260
|
+
if (replayFailure) {
|
|
250
261
|
return formatErrorResponse(
|
|
251
262
|
400,
|
|
252
263
|
"previous_response_not_found",
|
|
@@ -262,10 +273,14 @@ export async function prepareResponsesRequest(
|
|
|
262
273
|
// encrypted_content slots as plaintext. Rewrite them to input_text on the RAW body BEFORE
|
|
263
274
|
// parsing so every consumer sees the payload: parseRequest (routed/translated providers read
|
|
264
275
|
// the parsed messages) and the native passthrough (_rawBody is this same object, serialized
|
|
265
|
-
// verbatim).
|
|
276
|
+
// verbatim). Structurally valid backend ciphertext stays byte-identical; encoded-looking unknown
|
|
277
|
+
// slots remain opaque only until final-route handling can preserve or strip them safely.
|
|
266
278
|
{
|
|
267
279
|
const rewritten = sanitizeEncryptedContentInPlace(
|
|
268
280
|
(body as { input?: unknown } | undefined)?.input,
|
|
281
|
+
// The final destination is not known yet. Keep ambiguous encoded slots opaque until route
|
|
282
|
+
// selection can either strip them for a third party or apply strict native classification.
|
|
283
|
+
{ preserveUnknownOpaqueSlots: true },
|
|
269
284
|
);
|
|
270
285
|
if (rewritten > 0)
|
|
271
286
|
console.warn(
|
|
@@ -378,7 +393,7 @@ export async function prepareResponsesRequest(
|
|
|
378
393
|
if (!logCtx.conversationId) {
|
|
379
394
|
logCtx.conversationId = resolvedConversationId;
|
|
380
395
|
}
|
|
381
|
-
logCtx.requestedModel = parsed.modelId;
|
|
396
|
+
logCtx.requestedModel = options.compactionRoutingOverride?.sourceModel ?? parsed.modelId;
|
|
382
397
|
logCtx.requestedEffort = parsed.options.reasoning;
|
|
383
398
|
// What this request may spend beyond its input, for the durable spend reservation (#4707).
|
|
384
399
|
// Read from the caller rather than from the adapter's serialized body, because the
|
|
@@ -396,19 +411,29 @@ export async function prepareResponsesRequest(
|
|
|
396
411
|
|
|
397
412
|
let route: RouteResult;
|
|
398
413
|
let credentialDomainWasRewritten = false;
|
|
414
|
+
const captureInboundRoutePolicy = (candidate: RouteResult): RouteResult => {
|
|
415
|
+
candidate.staticPolicy = captureRouteStaticPolicy(
|
|
416
|
+
candidate.providerName,
|
|
417
|
+
candidate.modelId,
|
|
418
|
+
candidate.provider,
|
|
419
|
+
candidate.staticPolicy.effectiveAlias,
|
|
420
|
+
inboundWire,
|
|
421
|
+
);
|
|
422
|
+
return candidate;
|
|
423
|
+
};
|
|
399
424
|
try {
|
|
400
425
|
// A `compaction_trigger` turn may name a bare native model the operator has
|
|
401
426
|
// no canonical OpenAI route for (#2901). Only the initial compaction route
|
|
402
427
|
// may fall back to the configured default provider; combo attempts and the
|
|
403
428
|
// later fallback/recovery re-routes keep the ordinary reservation.
|
|
404
|
-
const resolveRoute = (modelId: string) => options.comboAttempt
|
|
429
|
+
const resolveRoute = (modelId: string) => captureInboundRoutePolicy(options.comboAttempt
|
|
405
430
|
? routeConcreteModel(config, modelId)
|
|
406
431
|
: parsed._compactionRequest === true
|
|
407
432
|
? routeCompactionModel(config, modelId, evidenceFromBody(parsed._rawBody))
|
|
408
|
-
: routeModel(config, modelId, evidenceFromBody(parsed._rawBody));
|
|
433
|
+
: routeModel(config, modelId, evidenceFromBody(parsed._rawBody)));
|
|
409
434
|
const _sci = config.shadowCallIntercept;
|
|
410
435
|
let shadowRoute: RouteResult | undefined;
|
|
411
|
-
if (_sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
|
|
436
|
+
if (!options.compactionRoutingOverride && _sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
|
|
412
437
|
const sourcePrefix = shadowSourceModelPrefix(parsed.modelId, _sci.sourceModels)!;
|
|
413
438
|
let sourceIdentity = { providerName: OPENAI_CODEX_PROVIDER_ID, modelId: sourcePrefix };
|
|
414
439
|
try {
|
|
@@ -436,8 +461,17 @@ export async function prepareResponsesRequest(
|
|
|
436
461
|
shadowRoute = targetRoute;
|
|
437
462
|
}
|
|
438
463
|
}
|
|
439
|
-
if (parsed._compactionRequest === true) parsed._cursorIsolateConversation = true;
|
|
464
|
+
if (parsed._compactionRequest === true || options.compactionRoutingOverride) parsed._cursorIsolateConversation = true;
|
|
440
465
|
route = shadowRoute ?? resolveRoute(parsed.modelId);
|
|
466
|
+
if (options.compactionRoutingOverride && !compactionRoutingKeepsProviderIdentity(config, options.compactionRoutingOverride, route)) {
|
|
467
|
+
credentialDomainWasRewritten = true;
|
|
468
|
+
// The destination does not share the conversation's credential domain, so it can neither
|
|
469
|
+
// verify the source backend's reasoning ciphertext nor decode its native compaction blob.
|
|
470
|
+
// This is the same condition an account change already reports (account-change-state.ts),
|
|
471
|
+
// and the serializer turns a stored summary into readable text instead of dropping it.
|
|
472
|
+
parsed._stripReasoningEncryptedContent = true;
|
|
473
|
+
if (parsed._compactionRequest === true) parsed._portableCompaction = true;
|
|
474
|
+
}
|
|
441
475
|
logCtx.routeDecision = route.routeDecision;
|
|
442
476
|
} catch (err) {
|
|
443
477
|
if (err instanceof NoAvailableComboTargetsError) {
|
|
@@ -633,7 +667,9 @@ export async function prepareResponsesRequest(
|
|
|
633
667
|
|
|
634
668
|
if (fallback?.to && !slugsEquivalent(fallback.to, route.modelId)) {
|
|
635
669
|
try {
|
|
636
|
-
route =
|
|
670
|
+
route = captureInboundRoutePolicy(
|
|
671
|
+
routeModel(config, fallback.to, evidenceFromBody(parsed._rawBody)),
|
|
672
|
+
);
|
|
637
673
|
credentialDomainWasRewritten = true;
|
|
638
674
|
logCtx.routeDecision = route.routeDecision;
|
|
639
675
|
} catch (err) {
|
|
@@ -831,7 +867,9 @@ export async function prepareResponsesRequest(
|
|
|
831
867
|
|
|
832
868
|
if (fallback?.to && !slugsEquivalent(fallback.to, route.modelId)) {
|
|
833
869
|
try {
|
|
834
|
-
route =
|
|
870
|
+
route = captureInboundRoutePolicy(
|
|
871
|
+
routeModel(config, fallback.to, evidenceFromBody(parsed._rawBody)),
|
|
872
|
+
);
|
|
835
873
|
credentialDomainWasRewritten = true;
|
|
836
874
|
logCtx.routeDecision = route.routeDecision;
|
|
837
875
|
} catch (err) {
|
|
@@ -857,6 +895,17 @@ export async function prepareResponsesRequest(
|
|
|
857
895
|
|
|
858
896
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
859
897
|
|
|
898
|
+
if (inboundWire === "responses" && isCanonicalOpenAiForwardProvider(route.provider)) {
|
|
899
|
+
const rewritten = sanitizeEncryptedContentInPlace(
|
|
900
|
+
(body as { input?: unknown } | undefined)?.input,
|
|
901
|
+
);
|
|
902
|
+
if (rewritten > 0) {
|
|
903
|
+
console.warn(
|
|
904
|
+
`[opencodex] rewrote ${rewritten} non-Fernet encrypted_content part(s) before canonical native replay`,
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
860
909
|
// Encrypted child tasks may reach the canonical native backend or an explicitly trusted
|
|
861
910
|
// direct Responses route. This runs against the FINAL route so native-only fallback can
|
|
862
911
|
// rescue an incompatible primary without weakening combo behavior.
|
|
@@ -902,6 +951,7 @@ export async function prepareResponsesRequest(
|
|
|
902
951
|
route.modelId,
|
|
903
952
|
route.provider,
|
|
904
953
|
inboundWire,
|
|
954
|
+
route.staticPolicy,
|
|
905
955
|
);
|
|
906
956
|
if (wireProvider.adapter === "openai-responses" && !isCanonicalOpenAiForwardProvider(wireProvider)) {
|
|
907
957
|
const repaired = stripAgentMessageCiphertextInPlace((body as { input?: unknown } | undefined)?.input);
|
|
@@ -925,12 +975,12 @@ export async function prepareResponsesRequest(
|
|
|
925
975
|
return formatErrorResponse(
|
|
926
976
|
400,
|
|
927
977
|
"previous_response_not_found",
|
|
928
|
-
"
|
|
978
|
+
"Continuation state is unavailable or corrupt; resend the full conversation without previous_response_id.",
|
|
929
979
|
);
|
|
930
980
|
}
|
|
931
981
|
|
|
932
982
|
if (hasUnexpandedPreviousResponse) {
|
|
933
|
-
const continuationProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
|
|
983
|
+
const continuationProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy);
|
|
934
984
|
// Can the DESTINATION see the history this process failed to restore? Only the native
|
|
935
985
|
// Responses passthrough can: it forwards previous_response_id to a backend that stored the
|
|
936
986
|
// chain. Every translated wire rebuilds the conversation from this request's input alone —
|
|
@@ -951,7 +1001,7 @@ export async function prepareResponsesRequest(
|
|
|
951
1001
|
return formatErrorResponse(
|
|
952
1002
|
400,
|
|
953
1003
|
"previous_response_not_found",
|
|
954
|
-
"
|
|
1004
|
+
"Continuation state is unavailable or corrupt; resend the full conversation without previous_response_id.",
|
|
955
1005
|
);
|
|
956
1006
|
}
|
|
957
1007
|
}
|
|
@@ -109,8 +109,12 @@ export function createResponsesSendBudget(
|
|
|
109
109
|
const noteAdapterPhysicalSend = (
|
|
110
110
|
inputTokens: number | undefined,
|
|
111
111
|
send: { ordinal: number; recovery?: AttemptRecoveryKind },
|
|
112
|
+
options: { readonly includeFirst?: boolean } = {},
|
|
112
113
|
): void => {
|
|
113
|
-
|
|
114
|
+
// Ordinal 1 is skipped because the caller normally records it before dispatch. An adapter
|
|
115
|
+
// that reports every send asks for it to be counted here instead, so that the first send is
|
|
116
|
+
// logged where it actually happens rather than before admission could still refuse it.
|
|
117
|
+
if (send.ordinal <= 1 && options.includeFirst !== true) return;
|
|
114
118
|
noteAttemptSend(logCtx.activeAttempt, inputTokens, send.recovery);
|
|
115
119
|
};
|
|
116
120
|
/**
|
|
@@ -50,7 +50,7 @@ export async function prepareResponsesSidecarAuth(
|
|
|
50
50
|
let openAiSidecar: ResolvedOpenAiForwardSidecar | undefined;
|
|
51
51
|
const visionDescribeTerminal = options.visionDescribeTerminal === true;
|
|
52
52
|
const routedCompaction = parsed._compactionRequest === true
|
|
53
|
-
&& !isCanonicalOpenAiForwardProvider(route.provider);
|
|
53
|
+
&& (!isCanonicalOpenAiForwardProvider(route.provider) || parsed._portableCompaction === true);
|
|
54
54
|
const needsOpenAiVision = !visionDescribeTerminal
|
|
55
55
|
&& shouldResolveOpenAiVisionSidecar(config, route.provider, route.modelId, parsed, route.providerName);
|
|
56
56
|
const needsOpenAiSearch = !routedCompaction && !transportState.adapter.runTurn
|