@bitkyc08/opencodex 2.57.0 → 2.59.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 +28 -10
- package/gui/dist/assets/index-C5IebErG.js +136 -0
- package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +2 -2
- package/src/adapters/base.ts +11 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +5 -4
- package/src/adapters/command-code.ts +13 -4
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +27 -6
- package/src/adapters/cursor/request-builder.ts +14 -3
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +141 -0
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +97 -6
- package/src/adapters/devin/cloud-direct/chat.ts +11 -2
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
- package/src/adapters/devin.ts +75 -13
- package/src/adapters/google-antigravity-wire.ts +29 -2
- package/src/adapters/google-http.ts +45 -13
- package/src/adapters/google.ts +23 -4
- package/src/adapters/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat.ts +5 -10
- package/src/adapters/openai-responses/passthrough.ts +40 -5
- package/src/adapters/openai-responses/request-strips.ts +43 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/physical-send.ts +50 -0
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +166 -25
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +46 -5
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +22 -2
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/config-command.ts +35 -18
- package/src/cli/dispatch.ts +71 -4
- package/src/cli/doctor.ts +197 -2
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +132 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +72 -1
- package/src/cli/uninstall-client-state.ts +12 -0
- package/src/client/machine-api.ts +4 -3
- package/src/client/machine-listener.ts +14 -1
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +81 -5
- package/src/codex/auth-api/pool-quota-probe.ts +14 -3
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +58 -20
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +1 -0
- package/src/codex/catalog/model-hints.ts +37 -5
- package/src/codex/catalog/parsing.ts +83 -5
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +19 -0
- package/src/codex/catalog/routed-gather.ts +42 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +37 -5
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +204 -32
- package/src/codex/inject.ts +6 -9
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +40 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-entitlements.ts +60 -1
- package/src/codex/native-profile-startup.ts +64 -20
- package/src/codex/observed-model-denials.ts +137 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +282 -12
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +13 -15
- package/src/codex/subagent-model-fallback.ts +94 -0
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +122 -5
- package/src/config/atomic-write.ts +83 -8
- package/src/config/diagnostics.ts +21 -0
- package/src/config/load-degrade.ts +15 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +23 -5
- package/src/config/schema/config-schema.ts +23 -0
- package/src/config/schema/leaf-validators.ts +65 -17
- package/src/generated/compatibility-version.json +337 -201
- package/src/generated/model-metadata.ts +1 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/bounded-subprocess.ts +62 -10
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +3 -15
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/provider-outbound.ts +3 -3
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +631 -0
- package/src/lib/spend-reservation-ledger.ts +115 -9
- package/src/lib/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/generic-account-failover.ts +40 -27
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +261 -7
- package/src/providers/model-discovery.ts +19 -7
- package/src/providers/model-rename-migration.ts +1 -0
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/registry/entries-core.ts +11 -0
- package/src/providers/registry/entries-extended.ts +146 -28
- package/src/providers/registry/model-seeds.ts +136 -29
- package/src/providers/registry/types.ts +9 -0
- package/src/responses/apply-patch-envelope.ts +44 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/schema.ts +9 -2
- package/src/responses/spill-store.ts +17 -0
- package/src/responses/state/body-policy.ts +25 -0
- package/src/responses/state/spill-queue.ts +8 -6
- package/src/responses/state.ts +3 -22
- package/src/router.ts +4 -0
- package/src/server/auth-cors.ts +27 -0
- package/src/server/chat-completions.ts +9 -4
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +24 -2
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/websocket-handler.ts +48 -1
- package/src/server/index.ts +15 -19
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/config-routes.ts +57 -10
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/models-capabilities.ts +24 -3
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/relay.ts +17 -24
- package/src/server/request-log.ts +25 -1
- package/src/server/responses/adapter-continuation.ts +71 -27
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +52 -24
- package/src/server/responses/codex-ws-exchange.ts +65 -4
- package/src/server/responses/combo-stream-preflight.ts +68 -5
- package/src/server/responses/compact.ts +60 -11
- package/src/server/responses/core-codex-account.ts +83 -22
- package/src/server/responses/core-combo.ts +26 -0
- package/src/server/responses/core-normalize.ts +12 -5
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/fetch-helpers.ts +72 -3
- package/src/server/responses/native-injection-protocol.ts +42 -0
- package/src/server/responses/native-injection-replay.ts +105 -0
- package/src/server/responses/native-injection.ts +242 -0
- package/src/server/responses/native-response-control.ts +56 -0
- package/src/server/responses/native-response-json.ts +14 -0
- package/src/server/responses/native-response-output.ts +37 -0
- package/src/server/responses/native-steering-log.ts +44 -0
- package/src/server/responses/native-steering-policy.ts +49 -0
- package/src/server/responses/native-steering-replay.ts +126 -0
- package/src/server/responses/native-steering-settings.ts +76 -0
- package/src/server/responses/native-steering.ts +400 -0
- package/src/server/responses/native-tool-results.ts +130 -0
- package/src/server/responses/passthrough-delivery.ts +21 -1
- package/src/server/responses/passthrough-dispatch.ts +146 -49
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/request-prepare.ts +70 -0
- package/src/server/responses/request-send-budget.ts +84 -7
- package/src/server/responses/request-sidecar-auth.ts +16 -8
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +13 -10
- package/src/server/responses/run-turn-execution.ts +20 -5
- package/src/server/responses/sidecar-execution.ts +2 -0
- package/src/server/responses/ws-upstream.ts +23 -2
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +36 -9
- package/src/server/stop-teardown.ts +8 -1
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +29 -7
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +104 -3
- package/src/types/provider.ts +32 -0
- package/src/types/request.ts +7 -1
- package/src/types/wire.ts +9 -1
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-Cz7CLdif.js +0 -128
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { mergeSteeringContinuation } from "./native-steering-settings";
|
|
2
|
+
import { markNativeControlResponse } from "./native-response-control";
|
|
3
|
+
import type { NativeResponseControl } from "./native-response-control";
|
|
1
4
|
import { MAX_CLIENT_SSE_FRAME_BYTES } from "../sse-frame-buffer";
|
|
2
5
|
import { isSafeResponseHeader } from "../safe-response-headers";
|
|
3
6
|
import { CodexWsMetadata, type CodexWsQuotaObserver } from "./codex-ws-metadata";
|
|
@@ -6,10 +9,12 @@ import { CodexWsCorrelation } from "./codex-ws-correlation";
|
|
|
6
9
|
import type { CodexWsSession } from "./codex-ws-session";
|
|
7
10
|
import { UPGRADE_DEADLINE_MS, CODEX_WS_LIVENESS_PING_INTERVAL_MS, CODEX_WS_RESPONSE_PRELUDE_TIMEOUT_MS, MAX_CODEX_WS_FRAME_BYTES,
|
|
8
11
|
MAX_CODEX_WS_QUEUE_BYTES, markCodexWsResponse, normalizeResponsesWsRelayEvent, closedBeforeTerminalMessage,
|
|
9
|
-
codexWsFailureDetail, codexWsPreResponseFailure, markCodexWsStage, codexWsOcxVersion,
|
|
12
|
+
codexWsCreateFrameExceedsLimit, codexWsFailureDetail, codexWsPreResponseFailure, markCodexWsStage, codexWsOcxVersion,
|
|
10
13
|
type CodexWsFailureStage, type CodexWsStageRecord } from "./codex-ws-wire";
|
|
11
14
|
|
|
12
15
|
interface ExchangeOptions {
|
|
16
|
+
nativeControl?: NativeResponseControl;
|
|
17
|
+
beforeContinuation?: () => Promise<void>;
|
|
13
18
|
session: CodexWsSession;
|
|
14
19
|
url: string;
|
|
15
20
|
init: RequestInit;
|
|
@@ -86,7 +91,7 @@ function wrappedRejectionResponse(payload: Record<string, unknown>, prelude: Hea
|
|
|
86
91
|
|
|
87
92
|
/** The sole SSE exchange state machine for both one-shot and retained sockets. */
|
|
88
93
|
export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
89
|
-
const { session, url, init, prepared, sseFallback, onQuota, beforeDispatch, bunVersion } = options;
|
|
94
|
+
const { session, url, init, prepared, sseFallback, onQuota, beforeDispatch, bunVersion, nativeControl, beforeContinuation } = options;
|
|
90
95
|
const { frameText, headers } = prepared;
|
|
91
96
|
const signal = init.signal ?? undefined;
|
|
92
97
|
return new Promise<Response>((resolve, reject) => {
|
|
@@ -116,6 +121,8 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
116
121
|
const metadata = url === CODEX_RESPONSES_HTTP_URL ? new CodexWsMetadata(onQuota) : null;
|
|
117
122
|
const correlation = session.retainable ? new CodexWsCorrelation(session.reused, id => session.hasCompleted(id)) : null;
|
|
118
123
|
let detachOwner = () => {};
|
|
124
|
+
let detachSteering = () => {};
|
|
125
|
+
let continuationBase: Record<string, unknown> | undefined;
|
|
119
126
|
// Liveness while waiting for the first response event (metadata path only): the
|
|
120
127
|
// silence timer is re-armed by every inbound frame or pong; the pinger runs on a fixed
|
|
121
128
|
// interval so a peer that answers pings can never trip the silence bound while alive.
|
|
@@ -142,6 +149,7 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
142
149
|
metadata?.finish();
|
|
143
150
|
correlation?.finish();
|
|
144
151
|
detachOwner();
|
|
152
|
+
detachSteering();
|
|
145
153
|
ws.removeEventListener("open", onOpen);
|
|
146
154
|
ws.removeEventListener("message", onMessage);
|
|
147
155
|
ws.removeEventListener("close", onClose);
|
|
@@ -198,6 +206,7 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
198
206
|
const response = new Response(stream, { status: 200, headers: responseHeaders });
|
|
199
207
|
metadata?.commit();
|
|
200
208
|
markCodexWsResponse(response, Boolean(metadata && onQuota));
|
|
209
|
+
if (nativeControl) markNativeControlResponse(response);
|
|
201
210
|
markCodexWsStage(response, stageRecord(null));
|
|
202
211
|
committedResponse = response;
|
|
203
212
|
resolve(response);
|
|
@@ -329,6 +338,54 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
329
338
|
}
|
|
330
339
|
if (terminal || settledPreOpen || signal?.aborted) return;
|
|
331
340
|
sent = true;
|
|
341
|
+
try {
|
|
342
|
+
if (nativeControl) {
|
|
343
|
+
// Parsed once: the base body is immutable for this exchange, and a
|
|
344
|
+
// full-replay frame runs to megabytes. It seeds continuationBase on
|
|
345
|
+
// the first create frame.
|
|
346
|
+
let base: Record<string, unknown> | undefined;
|
|
347
|
+
detachSteering = nativeControl.attach(frame => {
|
|
348
|
+
const sendControl = () => {
|
|
349
|
+
if (terminal || signal?.aborted || session.closed || ws.readyState !== WebSocket.OPEN) {
|
|
350
|
+
throw new Error("Native steering connection is no longer available");
|
|
351
|
+
}
|
|
352
|
+
beforeDispatch?.(new Headers(headers));
|
|
353
|
+
let outgoing = frame;
|
|
354
|
+
if (frame.type === "response.create") {
|
|
355
|
+
// Generation overrides have passed route policy; identity/tools remain pinned.
|
|
356
|
+
// Keep the last explicit wire settings for later explicit and automatic successors.
|
|
357
|
+
base ??= JSON.parse(frameText) as Record<string, unknown>;
|
|
358
|
+
continuationBase ??= base;
|
|
359
|
+
outgoing = nativeControl.kind === "steering"
|
|
360
|
+
? mergeSteeringContinuation(continuationBase, frame)
|
|
361
|
+
: { ...continuationBase, input: frame.input, previous_response_id: frame.previous_response_id };
|
|
362
|
+
}
|
|
363
|
+
const text = JSON.stringify(outgoing);
|
|
364
|
+
if (codexWsCreateFrameExceedsLimit(text)) {
|
|
365
|
+
throw new Error("Native steering frame exceeds the transport byte limit");
|
|
366
|
+
}
|
|
367
|
+
if (frame.type === "response.create") continuationBase = outgoing;
|
|
368
|
+
try { ws.send(text); } catch {
|
|
369
|
+
// A send failure has unknown delivery. Never replay or fall back.
|
|
370
|
+
failStream("Native steering send failed; delivery is unknown");
|
|
371
|
+
throw new Error("Native steering send failed; delivery is unknown");
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
if (frame.type === "response.create" && beforeContinuation) {
|
|
375
|
+
// Explicit tool-result continuations are physical request starts;
|
|
376
|
+
// they keep provider pacing and revalidate auth AFTER the wait.
|
|
377
|
+
void beforeContinuation().then(sendControl).catch(() => failStream("Native steering continuation could not be dispatched; do not automatically replay queued input"));
|
|
378
|
+
} else sendControl();
|
|
379
|
+
}, error => failStream(error));
|
|
380
|
+
}
|
|
381
|
+
} catch (error) {
|
|
382
|
+
// An attach failure is an ownership conflict, not a failed send: no frame
|
|
383
|
+
// left the process, but the channel can never bind, so resolving the HTTP
|
|
384
|
+
// fallback here would silently degrade a multi-agent turn into an ordinary
|
|
385
|
+
// one. Fail the turn visibly instead.
|
|
386
|
+
failStream(error);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
332
389
|
try {
|
|
333
390
|
ws.send(frameText);
|
|
334
391
|
sentAt = Date.now();
|
|
@@ -400,8 +457,12 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
400
457
|
return;
|
|
401
458
|
}
|
|
402
459
|
if (!controlFrame && !type.startsWith("response.") && type !== "error") return;
|
|
460
|
+
let steeringEnded = false;
|
|
403
461
|
if (!controlFrame) {
|
|
404
|
-
try {
|
|
462
|
+
try {
|
|
463
|
+
if (nativeControl) steeringEnded = nativeControl.observe(normalized.payload);
|
|
464
|
+
else correlation?.accept(normalized.payload);
|
|
465
|
+
} catch (error) { failStream(error); return; }
|
|
405
466
|
// Correlation must run first: a reused socket's foreign-stream error settles as a
|
|
406
467
|
// non-replayable 502 above, never as the refused-create 4xx projection below, which
|
|
407
468
|
// is the one status family that could authorize an account replay.
|
|
@@ -443,7 +504,7 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
443
504
|
return;
|
|
444
505
|
}
|
|
445
506
|
if (!controlFrame) relayedEvents += 1;
|
|
446
|
-
if (type === "response.completed" || type === "response.failed" || type === "response.incomplete" || type === "error") {
|
|
507
|
+
if (nativeControl ? steeringEnded : (type === "response.completed" || type === "response.failed" || type === "response.incomplete" || type === "error")) {
|
|
447
508
|
const completedId = correlation?.completed(normalized.payload) ?? null;
|
|
448
509
|
terminal = true;
|
|
449
510
|
cleanup();
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ResponsesTerminalStatus } from "../../bridge";
|
|
2
|
+
import { comboFailureDecision } from "../../combos";
|
|
3
|
+
import { httpStatusFromTerminalError } from "../../lib/errors";
|
|
2
4
|
import type { RequestLogContext } from "../request-log";
|
|
3
5
|
import { createSseInspector } from "../relay";
|
|
4
6
|
import { MAX_CLIENT_SSE_FRAME_BYTES } from "../sse-frame-buffer";
|
|
@@ -30,12 +32,67 @@ const RETRYABLE_ZERO_OUTPUT_INCOMPLETE_REASONS = new Set([
|
|
|
30
32
|
"upstream_stall_timeout",
|
|
31
33
|
]);
|
|
32
34
|
|
|
35
|
+
function bareErrorStatus(payload: unknown): number | undefined {
|
|
36
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
|
|
37
|
+
const event = payload as Record<string, unknown>;
|
|
38
|
+
if (event.type !== "error") return undefined;
|
|
39
|
+
const nested = event.error;
|
|
40
|
+
const error = nested && typeof nested === "object" && !Array.isArray(nested)
|
|
41
|
+
? nested as Record<string, unknown>
|
|
42
|
+
: event;
|
|
43
|
+
const explicitStatus = [
|
|
44
|
+
event.status,
|
|
45
|
+
event.status_code,
|
|
46
|
+
event.http_status,
|
|
47
|
+
error.status,
|
|
48
|
+
error.status_code,
|
|
49
|
+
error.http_status,
|
|
50
|
+
]
|
|
51
|
+
.map(value => typeof value === "number" && Number.isInteger(value)
|
|
52
|
+
? value
|
|
53
|
+
: typeof value === "string" && /^\d{3}$/.test(value.trim())
|
|
54
|
+
? Number(value)
|
|
55
|
+
: undefined)
|
|
56
|
+
.find(value => value !== undefined && value >= 400 && value <= 599);
|
|
57
|
+
const code = typeof error.code === "string"
|
|
58
|
+
? error.code
|
|
59
|
+
: typeof event.code === "string" ? event.code : null;
|
|
60
|
+
if (explicitStatus === undefined && code === "invalid_request_error") return 400;
|
|
61
|
+
return explicitStatus ?? httpStatusFromTerminalError({
|
|
62
|
+
type: typeof error.type === "string" && error.type !== "error" ? error.type : undefined,
|
|
63
|
+
code,
|
|
64
|
+
message: typeof error.message === "string"
|
|
65
|
+
? error.message
|
|
66
|
+
: typeof event.message === "string" ? event.message : undefined,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function bareErrorIsRetryable(payload: unknown): boolean {
|
|
71
|
+
const status = bareErrorStatus(payload);
|
|
72
|
+
if (status === undefined || !payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const event = payload as Record<string, unknown>;
|
|
76
|
+
const nested = event.error;
|
|
77
|
+
const error = nested && typeof nested === "object" && !Array.isArray(nested)
|
|
78
|
+
? nested as Record<string, unknown>
|
|
79
|
+
: event;
|
|
80
|
+
const code = typeof error.code === "string"
|
|
81
|
+
? error.code
|
|
82
|
+
: typeof event.code === "string" ? event.code : null;
|
|
83
|
+
const message = typeof error.message === "string"
|
|
84
|
+
? error.message
|
|
85
|
+
: typeof event.message === "string" ? event.message : "";
|
|
86
|
+
return comboFailureDecision(status, message, { code }) === "hop";
|
|
87
|
+
}
|
|
88
|
+
|
|
33
89
|
function retryableZeroOutputTerminal(payload: unknown): boolean {
|
|
34
90
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
35
91
|
const event = payload as {
|
|
36
92
|
type?: unknown;
|
|
37
93
|
response?: { incomplete_details?: { reason?: unknown } };
|
|
38
94
|
};
|
|
95
|
+
if (bareErrorIsRetryable(event)) return true;
|
|
39
96
|
if (event.type === "response.failed") return true;
|
|
40
97
|
if (event.type !== "response.incomplete") return false;
|
|
41
98
|
const reason = event.response?.incomplete_details?.reason;
|
|
@@ -95,12 +152,17 @@ function failedTerminalResponse(
|
|
|
95
152
|
? nested as Record<string, unknown>
|
|
96
153
|
: {};
|
|
97
154
|
const nestedError = terminalResponse.error;
|
|
155
|
+
const topLevelError = terminalPayload.error;
|
|
98
156
|
const error = nestedError && typeof nestedError === "object" && !Array.isArray(nestedError)
|
|
99
157
|
? nestedError as Record<string, unknown>
|
|
158
|
+
: topLevelError && typeof topLevelError === "object" && !Array.isArray(topLevelError)
|
|
159
|
+
? topLevelError as Record<string, unknown>
|
|
100
160
|
: {
|
|
101
161
|
type: "upstream_error",
|
|
102
162
|
code: "upstream_server_error",
|
|
103
|
-
message:
|
|
163
|
+
message: typeof terminalPayload.message === "string"
|
|
164
|
+
? terminalPayload.message
|
|
165
|
+
: logCtx.upstreamError ?? "Provider stream failed before producing output",
|
|
104
166
|
};
|
|
105
167
|
const headers = new Headers(response.headers);
|
|
106
168
|
headers.set("content-type", "application/json");
|
|
@@ -117,7 +179,7 @@ function failedTerminalResponse(
|
|
|
117
179
|
...(usage && typeof usage === "object" && !Array.isArray(usage) ? { usage } : {}),
|
|
118
180
|
},
|
|
119
181
|
}), {
|
|
120
|
-
status: logCtx.terminalHttpStatus ?? 502,
|
|
182
|
+
status: logCtx.terminalHttpStatus ?? bareErrorStatus(terminalPayload) ?? 502,
|
|
121
183
|
headers,
|
|
122
184
|
});
|
|
123
185
|
}
|
|
@@ -154,11 +216,12 @@ export async function preflightComboStreamResponse(
|
|
|
154
216
|
const inspector = createSseInspector({
|
|
155
217
|
logCtx,
|
|
156
218
|
onParsedPayload: payload => {
|
|
219
|
+
if (terminalStatus !== undefined || outputCommitted || retryableTerminalPayload) return;
|
|
157
220
|
const retryable = retryableTerminal(payload);
|
|
158
221
|
const matchedBareError = retryable && payload !== null && typeof payload === "object"
|
|
159
222
|
&& !Array.isArray(payload) && (payload as { type?: unknown }).type === "error";
|
|
160
|
-
//
|
|
161
|
-
//
|
|
223
|
+
// A zero-output bare error is terminal evidence. Explicit client errors stay
|
|
224
|
+
// committed; unknown and retryable upstream failures may advance the combo.
|
|
162
225
|
if (comboStreamPayloadCommitsOutput(payload) && !matchedBareError) outputCommitted = true;
|
|
163
226
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return;
|
|
164
227
|
if (retryable) retryableTerminalPayload = payload as Record<string, unknown>;
|
|
@@ -197,7 +260,7 @@ export async function preflightComboStreamResponse(
|
|
|
197
260
|
}
|
|
198
261
|
|
|
199
262
|
// A bare error event is not a protocol terminal (terminalStatus stays undefined),
|
|
200
|
-
// so its
|
|
263
|
+
// so its retryable classification doubles as the terminal evidence.
|
|
201
264
|
if ((terminalStatus === "failed" || terminalStatus === "incomplete"
|
|
202
265
|
|| retryableTerminalPayload?.type === "error")
|
|
203
266
|
&& !outputCommitted && retryableTerminalPayload) {
|
|
@@ -107,6 +107,7 @@ import {
|
|
|
107
107
|
} from "../../codex/upstream-host-health";
|
|
108
108
|
import {
|
|
109
109
|
ForwardAdmissionCredentialError,
|
|
110
|
+
contextPrincipalIdOf,
|
|
110
111
|
hasForwardableCodexBearer,
|
|
111
112
|
validateForwardAdmissionCredential,
|
|
112
113
|
} from "../auth-cors";
|
|
@@ -114,7 +115,11 @@ import type { DataPlaneAdmission } from "../auth-cors";
|
|
|
114
115
|
import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
|
|
115
116
|
import { CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint } from "../../providers/openai-tiers";
|
|
116
117
|
import { NATIVE_RESERVE_MODEL } from "../../codex/catalog/native-models";
|
|
117
|
-
import {
|
|
118
|
+
import {
|
|
119
|
+
isCodexReserveOptInMissing,
|
|
120
|
+
isCodexReserveRequestEligible,
|
|
121
|
+
CODEX_RESERVE_OPT_IN_REQUIRED_MESSAGE,
|
|
122
|
+
} from "../../codex/loopback-target";
|
|
118
123
|
import { slugsEquivalent } from "../../providers/slug-codec";
|
|
119
124
|
import { decideTier, tierValueAfterDecision } from "../../providers/fastwire";
|
|
120
125
|
import { fastPolicyForModel } from "../../providers/service-tier";
|
|
@@ -210,8 +215,33 @@ function pruneCompactHandoffRoutes(now: number): void {
|
|
|
210
215
|
}
|
|
211
216
|
}
|
|
212
217
|
|
|
213
|
-
|
|
214
|
-
|
|
218
|
+
/**
|
|
219
|
+
* The handoff map is process-global, so a caller-controlled lane header alone
|
|
220
|
+
* cannot be the key: two authenticated clients sending the same lane header
|
|
221
|
+
* would share one fallback route. Namespace the lane by the admitted principal.
|
|
222
|
+
* A loopback or missing admission has no authenticated identity to bind this
|
|
223
|
+
* cross-request state to, so it is ineligible rather than trusted.
|
|
224
|
+
*/
|
|
225
|
+
function compactHandoffRouteKey(req: Request, admission: DataPlaneAdmission | undefined): string | null {
|
|
226
|
+
const lane = sessionLaneIdFromRequest(req.headers);
|
|
227
|
+
if (!lane || !admission || admission.kind === "loopback") return null;
|
|
228
|
+
// Fail closed rather than substituting a weaker identity. `keyId` survives a
|
|
229
|
+
// rotation and every identity-less environment admission would collapse into
|
|
230
|
+
// one bucket, which is the collision this key exists to prevent. Production
|
|
231
|
+
// admission always mints `contextPrincipalId` for configured and environment
|
|
232
|
+
// holders, so no real authenticated caller loses the route.
|
|
233
|
+
const principal = contextPrincipalIdOf(admission);
|
|
234
|
+
if (!principal) return null;
|
|
235
|
+
return `${principal}\u0000${lane}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function rememberCompactHandoffRoute(
|
|
239
|
+
req: Request,
|
|
240
|
+
admission: DataPlaneAdmission | undefined,
|
|
241
|
+
model: string,
|
|
242
|
+
now = Date.now(),
|
|
243
|
+
): void {
|
|
244
|
+
const key = compactHandoffRouteKey(req, admission);
|
|
215
245
|
if (!key || model.length > COMPACT_HANDOFF_MODEL_MAX_LENGTH) return;
|
|
216
246
|
pruneCompactHandoffRoutes(now);
|
|
217
247
|
compactHandoffRoutes.delete(key);
|
|
@@ -219,13 +249,18 @@ function rememberCompactHandoffRoute(req: Request, model: string, now = Date.now
|
|
|
219
249
|
pruneCompactHandoffRoutes(now);
|
|
220
250
|
}
|
|
221
251
|
|
|
222
|
-
function forgetCompactHandoffRoute(req: Request): void {
|
|
223
|
-
const key =
|
|
252
|
+
function forgetCompactHandoffRoute(req: Request, admission?: DataPlaneAdmission): void {
|
|
253
|
+
const key = compactHandoffRouteKey(req, admission);
|
|
224
254
|
if (key) compactHandoffRoutes.delete(key);
|
|
225
255
|
}
|
|
226
256
|
|
|
227
|
-
function compactHandoffRoute(
|
|
228
|
-
|
|
257
|
+
function compactHandoffRoute(
|
|
258
|
+
req: Request,
|
|
259
|
+
admission: DataPlaneAdmission | undefined,
|
|
260
|
+
previousModel: string,
|
|
261
|
+
now = Date.now(),
|
|
262
|
+
): string | null {
|
|
263
|
+
const key = compactHandoffRouteKey(req, admission);
|
|
229
264
|
if (!key) return null;
|
|
230
265
|
pruneCompactHandoffRoutes(now);
|
|
231
266
|
const entry = compactHandoffRoutes.get(key);
|
|
@@ -632,6 +667,20 @@ export async function handleResponsesCompact(
|
|
|
632
667
|
? `${route.providerName}-${route.codexAccountNamespace}`
|
|
633
668
|
: route.providerName;
|
|
634
669
|
logCtx.providerAdapter = route.provider.adapter;
|
|
670
|
+
// #4940, and the same refusal the ordinary Responses path makes in request-prepare.ts. Compact has
|
|
671
|
+
// to repeat it rather than inherit it: the native branch below dispatches straight to
|
|
672
|
+
// `/responses/compact` and only the routed fallback replays through handleResponses, so relying on
|
|
673
|
+
// that one seam would leave the native compact turn forwarding a Reserve model the opt-in has made
|
|
674
|
+
// unservable. Composed from the same three facts, in the same order, as `customReserveForward`
|
|
675
|
+
// further down, and placed before the virtual-model rewrite so it reads the model the caller
|
|
676
|
+
// actually selected -- and before auth, host-circuit admission, or any upstream byte.
|
|
677
|
+
//
|
|
678
|
+
// No terminal-helper or inbound-wire qualifier is needed here the way it is on the ordinary path:
|
|
679
|
+
// this endpoint is the native Codex compaction wire, and a vision/search helper never reaches it.
|
|
680
|
+
if (isCodexReserveOptInMissing(config, selectedModelId, admission)
|
|
681
|
+
&& isCanonicalOpenAiForwardProvider(route.provider)) {
|
|
682
|
+
return formatErrorResponse(400, "invalid_request_error", CODEX_RESERVE_OPT_IN_REQUIRED_MESSAGE);
|
|
683
|
+
}
|
|
635
684
|
const virtual = resolveOpenAiCompactModel(route.providerName, selectedModelId);
|
|
636
685
|
if (virtual) {
|
|
637
686
|
route.modelId = virtual.wireModelId;
|
|
@@ -1254,10 +1303,10 @@ export async function handleResponsesCompact(
|
|
|
1254
1303
|
// synthetic buffer errors are not upstream bodies and stay uninspected.
|
|
1255
1304
|
if (buffered.ok) {
|
|
1256
1305
|
inspectResponseLogJson(logCtx, await buffered.clone().text());
|
|
1257
|
-
forgetCompactHandoffRoute(req);
|
|
1306
|
+
forgetCompactHandoffRoute(req, admission);
|
|
1258
1307
|
rememberServingConversationStateIssuer(outcomeCtx, codexPoolAffinityKey(req.headers));
|
|
1259
1308
|
} else if (quotaFailure && !storedPool401ReplayAttempted) {
|
|
1260
|
-
const fallbackModel = compactHandoffRoute(req, raw.model);
|
|
1309
|
+
const fallbackModel = compactHandoffRoute(req, admission, raw.model);
|
|
1261
1310
|
if (fallbackModel && !req.signal.aborted) {
|
|
1262
1311
|
const fallbackReq = new Request(req.url, {
|
|
1263
1312
|
method: "POST",
|
|
@@ -1389,7 +1438,7 @@ export async function handleResponsesCompact(
|
|
|
1389
1438
|
const result = new Response(JSON.stringify({ output: compactionItems }), {
|
|
1390
1439
|
headers: { "Content-Type": "application/json" },
|
|
1391
1440
|
});
|
|
1392
|
-
rememberCompactHandoffRoute(req, raw.model);
|
|
1441
|
+
rememberCompactHandoffRoute(req, admission, raw.model);
|
|
1393
1442
|
return result;
|
|
1394
1443
|
}
|
|
1395
1444
|
const encrypted = compactionItems[0]!.encrypted_content;
|
|
@@ -1400,6 +1449,6 @@ export async function handleResponsesCompact(
|
|
|
1400
1449
|
}
|
|
1401
1450
|
const summary = decoded;
|
|
1402
1451
|
const output = buildCompactV1Output(extractCompactUserMessages(inputItems), summary);
|
|
1403
|
-
rememberCompactHandoffRoute(req, raw.model);
|
|
1452
|
+
rememberCompactHandoffRoute(req, admission, raw.model);
|
|
1404
1453
|
return new Response(JSON.stringify({ output }), { headers: { "Content-Type": "application/json" } });
|
|
1405
1454
|
}
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
resolveCodexModelEntitlements,
|
|
33
33
|
invalidateCodexModelEntitlementsForAccount,
|
|
34
34
|
entitledCodexAccountIdsForModel,
|
|
35
|
+
recordCodexModelDenialEvidence,
|
|
35
36
|
} from "../../codex/model-entitlements";
|
|
36
37
|
import type { TransientSendBudget } from "../../lib/upstream-retry";
|
|
37
38
|
import { resolveAdapter, resolveWireProtocolOverride } from "../adapter-resolve";
|
|
@@ -161,43 +162,93 @@ export function normalizeCodexUnsupportedModelDetail(value: string): string {
|
|
|
161
162
|
}
|
|
162
163
|
|
|
163
164
|
|
|
164
|
-
|
|
165
|
+
/**
|
|
166
|
+
* The model id an authenticated Codex refusal names, or `undefined` when the body is not that
|
|
167
|
+
* refusal.
|
|
168
|
+
*
|
|
169
|
+
* Extracted rather than string-compared so the caller can learn WHICH model was refused. The
|
|
170
|
+
* route and the wire can legitimately disagree: `applyCodexAccountGatedWireNormalization`
|
|
171
|
+
* rewrites `gpt-daybreak-blue-latest` to `gpt-5.6-sol` before dispatch, so upstream names the
|
|
172
|
+
* model it was actually sent. Building the expected sentence from `route.modelId` alone made
|
|
173
|
+
* that comparison fail for the one model that is still account-gated, which silently disabled
|
|
174
|
+
* both the alternate-account retry and the same-account ladder built for exactly that case.
|
|
175
|
+
*
|
|
176
|
+
* The envelope is unchanged and stays exact: a top-level `detail` string, whitespace-collapsed
|
|
177
|
+
* and case-folded, matching the whole sentence with nothing before or after it. No prose is
|
|
178
|
+
* inferred and no other 400 shape is admitted, because a 400 is also what a malformed request
|
|
179
|
+
* earns and that must never read as an entitlement fact.
|
|
180
|
+
*/
|
|
181
|
+
export function codexUnsupportedModelFromDetail(
|
|
165
182
|
status: number,
|
|
166
183
|
bodyText: string,
|
|
167
|
-
|
|
168
|
-
)
|
|
169
|
-
if (status !== 400) return false;
|
|
184
|
+
): string | undefined {
|
|
185
|
+
if (status !== 400) return undefined;
|
|
170
186
|
try {
|
|
171
187
|
const payload = JSON.parse(bodyText) as unknown;
|
|
172
|
-
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return
|
|
188
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
|
|
173
189
|
const detail = (payload as { detail?: unknown }).detail;
|
|
174
|
-
if (typeof detail !== "string") return
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
190
|
+
if (typeof detail !== "string") return undefined;
|
|
191
|
+
const matched = /^the '([^']{1,256})' model is not supported when using codex with a chatgpt account\.$/u
|
|
192
|
+
.exec(normalizeCodexUnsupportedModelDetail(detail));
|
|
193
|
+
return matched?.[1];
|
|
178
194
|
} catch {
|
|
179
|
-
return
|
|
195
|
+
return undefined;
|
|
180
196
|
}
|
|
181
197
|
}
|
|
182
198
|
|
|
183
199
|
|
|
184
|
-
|
|
200
|
+
/**
|
|
201
|
+
* The refused model id when this response is the exact unsupported-model refusal for this
|
|
202
|
+
* request, read from a bounded clone.
|
|
203
|
+
*
|
|
204
|
+
* Same admission rules {@link shouldRetryCodexPoolAccountModel400} always applied, which is now
|
|
205
|
+
* a predicate over this: a truncated or non-display-safe body proves nothing and is refused.
|
|
206
|
+
* Returning the id lets the caller record the denial against the model upstream actually named.
|
|
207
|
+
*/
|
|
208
|
+
export async function codexPoolAccountModel400Denial(
|
|
185
209
|
response: Response,
|
|
186
210
|
modelId: string,
|
|
187
211
|
signal?: AbortSignal,
|
|
188
|
-
|
|
189
|
-
|
|
212
|
+
wireModelId?: string,
|
|
213
|
+
): Promise<string | undefined> {
|
|
214
|
+
if (response.status !== 400) return undefined;
|
|
190
215
|
try {
|
|
191
216
|
const body = await readBoundedResponseBody(response.clone(), { signal });
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
217
|
+
if (!body.displaySafe || body.truncated) return undefined;
|
|
218
|
+
return isAllowListedCodexAccountModel400(response.status, body.text, modelId, wireModelId)
|
|
219
|
+
? codexUnsupportedModelFromDetail(response.status, body.text)
|
|
220
|
+
: undefined;
|
|
195
221
|
} catch {
|
|
196
|
-
return
|
|
222
|
+
return undefined;
|
|
197
223
|
}
|
|
198
224
|
}
|
|
199
225
|
|
|
200
226
|
|
|
227
|
+
export function isAllowListedCodexAccountModel400(
|
|
228
|
+
status: number,
|
|
229
|
+
bodyText: string,
|
|
230
|
+
modelId: string,
|
|
231
|
+
wireModelId?: string,
|
|
232
|
+
): boolean {
|
|
233
|
+
const refused = codexUnsupportedModelFromDetail(status, bodyText);
|
|
234
|
+
if (refused === undefined) return false;
|
|
235
|
+
return [modelId, wireModelId].some(candidate => (
|
|
236
|
+
candidate !== undefined
|
|
237
|
+
&& refused === normalizeCodexUnsupportedModelDetail(candidate)
|
|
238
|
+
));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
export async function shouldRetryCodexPoolAccountModel400(
|
|
243
|
+
response: Response,
|
|
244
|
+
modelId: string,
|
|
245
|
+
signal?: AbortSignal,
|
|
246
|
+
wireModelId?: string,
|
|
247
|
+
): Promise<boolean> {
|
|
248
|
+
return await codexPoolAccountModel400Denial(response, modelId, signal, wireModelId) !== undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
|
|
201
252
|
/** Pre-stream quota/billing rejections that warrant one alternate-account attempt (#584). */
|
|
202
253
|
export function codexQuotaFailureMessage(body: string): string | undefined {
|
|
203
254
|
try {
|
|
@@ -778,15 +829,25 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
778
829
|
}
|
|
779
830
|
retrySendCount += 1;
|
|
780
831
|
args.onResponse?.(upstreamResponse, retryAuthCtx, request);
|
|
832
|
+
// The alternate account can refuse the same model, and that refusal is evidence about the
|
|
833
|
+
// account that produced it. Read BEFORE the ladder's own break, so the ordinary
|
|
834
|
+
// single-retry path -- every flagship model, which is the #4906 case -- records it too
|
|
835
|
+
// rather than only the gated ladder below. Without this the pool learns nothing from a
|
|
836
|
+
// refusal and the next request repeats the same selection.
|
|
837
|
+
const retryModelDenial = await codexPoolAccountModel400Denial(
|
|
838
|
+
upstreamResponse,
|
|
839
|
+
route.modelId,
|
|
840
|
+
options.abortSignal,
|
|
841
|
+
parsed.modelId,
|
|
842
|
+
);
|
|
843
|
+
if (retryModelDenial !== undefined) {
|
|
844
|
+
recordCodexModelDenialEvidence(retryAuthCtx.accountId, retryModelDenial);
|
|
845
|
+
}
|
|
781
846
|
if (!retrySameConfirmedAccount || retrySendCount >= maxRetrySends) break;
|
|
782
847
|
// Caller-owned main is an alternate-account replay and can never enter the bounded
|
|
783
848
|
// same-stored-account 400 loop above. Keep that invariant explicit for the account-id reads.
|
|
784
849
|
if (retryAuthCtx.kind === "main") break;
|
|
785
|
-
if (
|
|
786
|
-
upstreamResponse,
|
|
787
|
-
route.modelId,
|
|
788
|
-
options.abortSignal,
|
|
789
|
-
)) break;
|
|
850
|
+
if (retryModelDenial === undefined) break;
|
|
790
851
|
invalidateCodexModelEntitlementsForAccount(retryAuthCtx.accountId);
|
|
791
852
|
let refreshed: Awaited<ReturnType<typeof resolveCodexModelEntitlements>>;
|
|
792
853
|
try {
|
|
@@ -745,6 +745,13 @@ export async function executeComboResponses(
|
|
|
745
745
|
code: failure.upstreamCode,
|
|
746
746
|
message: failure.classificationText,
|
|
747
747
|
});
|
|
748
|
+
// Same target selector as the exclusionary pick below, minus `exclude`: the only
|
|
749
|
+
// difference is deliberate and is the whole point of the single-target retry.
|
|
750
|
+
const retryAfterCooldown = () =>
|
|
751
|
+
pickWithWait({
|
|
752
|
+
eligible: targetEligible,
|
|
753
|
+
now: failureNow,
|
|
754
|
+
});
|
|
748
755
|
if (nextPick) {
|
|
749
756
|
pick = nextPick;
|
|
750
757
|
} else {
|
|
@@ -753,6 +760,25 @@ export async function executeComboResponses(
|
|
|
753
760
|
eligible: targetEligible,
|
|
754
761
|
now: failureNow,
|
|
755
762
|
});
|
|
763
|
+
// A single-target combo with waitForCooldownMs has no alternate target to fail over to,
|
|
764
|
+
// but can recover if it waits for its brief cooldown. The initial attempt accumulated into
|
|
765
|
+
// pick.attempted, so the first pickWithWait above excluded it. retryAfterCooldown below is
|
|
766
|
+
// the same selector with `exclude` deliberately dropped, so the single cooled target
|
|
767
|
+
// becomes eligible again once its cooldown expires.
|
|
768
|
+
// Termination is double-guarded:
|
|
769
|
+
// 1) comboSendScope?.reserveDispatch refuses a second failover hop via comboExecutionBudgetPolicy
|
|
770
|
+
// (maxAlternateTargetSends: 1 for a single declared target).
|
|
771
|
+
// 2) comboTargetsDispatched <= 1 bounds it locally so the retry never loops or waits unnecessarily
|
|
772
|
+
// even if sendBudget scope is absent.
|
|
773
|
+
if (
|
|
774
|
+
!pick
|
|
775
|
+
&& combo.targets.length === 1
|
|
776
|
+
&& combo.waitForCooldownMs > 0
|
|
777
|
+
&& comboTargetsDispatched <= 1
|
|
778
|
+
&& !options.abortSignal?.aborted
|
|
779
|
+
) {
|
|
780
|
+
pick = await retryAfterCooldown();
|
|
781
|
+
}
|
|
756
782
|
}
|
|
757
783
|
if (!pick) {
|
|
758
784
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
@@ -142,11 +142,18 @@ export async function applyFinalRouteRequestNormalization(args: {
|
|
|
142
142
|
route.modelId,
|
|
143
143
|
);
|
|
144
144
|
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
// Preserve the routed destination for Go recognition, then settle the wire before
|
|
146
|
+
// deriving protocol-scoped affinity. Recognition must not inspect the flipped adapter.
|
|
147
|
+
const routedProvider = route.provider;
|
|
148
|
+
const wireProvider = resolveWireProtocolOverride(
|
|
149
|
+
route.providerName,
|
|
150
|
+
route.modelId,
|
|
151
|
+
routedProvider,
|
|
152
|
+
inboundWire,
|
|
153
|
+
);
|
|
154
|
+
route.provider = resolveOpenCodeGoTransport(wireProvider,
|
|
155
|
+
args.claudeGoAffinity ? args.claudeGoAffinity.sessionLane : getOrAllocateRequestSessionLane(req),
|
|
156
|
+
routedProvider);
|
|
150
157
|
parsed._plaintextV2AgentMessages = shouldPreparePlaintextV2AgentMessages({
|
|
151
158
|
enabled: config.plaintextV2AgentMessages === true,
|
|
152
159
|
inboundWire,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { NativeResponseControl } from "./native-response-control";
|
|
1
2
|
import type { OcxUsage, OcxProviderContinuationState, OcxConfig } from "../../types";
|
|
2
3
|
import type { CodexAuthPolicyConfig, CodexAuthContext } from "../../codex/auth-context";
|
|
3
4
|
import type { AdmissionLease } from "../../lib/admission";
|
|
@@ -51,6 +52,8 @@ export interface HandleResponsesOptions {
|
|
|
51
52
|
/** Called at most once after the complete client body is read and accepted for dispatch. */
|
|
52
53
|
onRequestBodyRead?: () => void;
|
|
53
54
|
forceEmptyResponseId?: boolean;
|
|
55
|
+
/** Internal, connection-owned control channel; never reconstructed from headers. */
|
|
56
|
+
nativeControl?: NativeResponseControl;
|
|
54
57
|
abortSignal?: AbortSignal;
|
|
55
58
|
/** One-shot TTFT callback: first non-empty model output observed (WP4). */
|
|
56
59
|
onFirstOutput?: () => void;
|