@bitkyc08/opencodex 2.18.2 → 2.20.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-DF_UFrGS.css +1 -0
- package/gui/dist/assets/index-DSK3S5HY.js +76 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/client-fingerprint.ts +14 -10
- package/src/adapters/google-antigravity-wire.ts +4 -3
- package/src/adapters/google.ts +1 -1
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat.ts +367 -32
- package/src/adapters/registry.ts +144 -0
- package/src/chat/inbound.ts +37 -8
- package/src/cli/claude.ts +2 -1
- package/src/cli/combo.ts +3 -0
- package/src/cli/dispatch.ts +8 -0
- package/src/cli/export-command.ts +2 -2
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +3 -2
- package/src/cli/lab.ts +135 -1
- package/src/cli/minimax.ts +491 -0
- package/src/cli/models-runtime.ts +22 -1
- package/src/cli/models.ts +67 -2
- package/src/cli/opencode.ts +2 -1
- package/src/cli/registry.ts +22 -2
- package/src/clients/config-export.ts +125 -7
- package/src/codex/app-server-processes.ts +57 -2
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/catalog/aggregation.ts +10 -1
- package/src/codex/catalog/effort.ts +15 -3
- package/src/codex/catalog/parsing.ts +3 -1
- package/src/codex/catalog/provider-fetch.ts +45 -5
- package/src/codex/catalog/sync.ts +74 -4
- package/src/codex/convergence.ts +2 -0
- package/src/codex/shim.ts +100 -5
- package/src/combos/index.ts +1 -0
- package/src/combos/request.ts +30 -0
- package/src/combos/types.ts +6 -0
- package/src/config.ts +52 -0
- package/src/generated/compatibility-version.json +239 -87
- package/src/images/loop.ts +11 -1
- package/src/integrations/registry.ts +7 -0
- package/src/lab/conformance/jcs.ts +42 -2
- package/src/lab/conformance/negative-controls.ts +6 -2
- package/src/lab/conformance/runner.ts +16 -5
- package/src/lab/fabric/observe.ts +49 -14
- package/src/lab/index.ts +16 -0
- package/src/lab/ledger/purge.ts +152 -83
- package/src/lab/ledger/store.ts +168 -54
- package/src/lab/observe/from-conformance.ts +8 -6
- package/src/lab/observe/from-live.ts +8 -2
- package/src/lab/paths.ts +23 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +219 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/errors.ts +27 -0
- package/src/lib/lab-activation.ts +109 -47
- package/src/lib/lab-live-pinned-sender.ts +16 -5
- package/src/lib/pinned-http.ts +70 -16
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/upstream-reachability.ts +4 -0
- package/src/lib/windows-elevation.ts +10 -1
- package/src/providers/derive.ts +24 -4
- package/src/providers/registry.ts +9 -3
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +143 -0
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/reasoning-effort.ts +27 -1
- package/src/responses/spill-store.ts +20 -1
- package/src/responses/state.ts +159 -3
- package/src/router.ts +23 -6
- package/src/routing/capability.ts +4 -2
- package/src/routing/compatibility/behavior.ts +5 -1
- package/src/server/adapter-resolve.ts +2 -32
- package/src/server/auth-cors.ts +8 -0
- package/src/server/chat-completions.ts +74 -34
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +371 -0
- package/src/server/effort-policy.ts +18 -0
- package/src/server/management/combo-routes.ts +16 -2
- package/src/server/management/config-routes.ts +6 -4
- package/src/server/management/context.ts +17 -0
- package/src/server/management/lab-routes.ts +181 -19
- package/src/server/management/model-routes.ts +76 -2
- package/src/server/management/model-rows.ts +8 -0
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +76 -4
- package/src/server/management/system-restart.ts +4 -2
- package/src/server/management/system-routes.ts +38 -0
- package/src/server/relay.ts +17 -3
- package/src/server/responses/compact.ts +4 -1
- package/src/server/responses/core.ts +278 -48
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/fetch-helpers.ts +35 -4
- package/src/server/responses/input-admission.ts +169 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/policy-fallback.ts +16 -2
- package/src/server/responses/terminal-guard.ts +1 -1
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses.ts +17 -2
- package/src/service-manager-probe.ts +2 -3
- package/src/types.ts +76 -6
- package/src/update/index.ts +6 -5
- package/src/update/job.ts +5 -6
- package/src/update/notify.ts +5 -3
- package/src/usage/log.ts +11 -1
- package/src/vision/index.ts +21 -4
- package/src/web-search/index.ts +2 -1
- package/gui/dist/assets/index-CXI1262_.js +0 -76
- package/gui/dist/assets/index-DUCH59lJ.css +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Server } from "bun";
|
|
2
2
|
import { bridgeToResponsesSSE, buildResponseJSON, formatErrorResponse, type ResponsesTerminalStatus } from "../../bridge";
|
|
3
3
|
import { formatPassthroughUpstreamError } from "./passthrough-error";
|
|
4
|
+
import { checkInputAdmission } from "./input-admission";
|
|
4
5
|
import { describeUpstreamConnectFailure } from "./upstream-error";
|
|
5
6
|
import {
|
|
6
7
|
getConfigPath,
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
comboDefaultEffort,
|
|
40
41
|
comboFailureDecision,
|
|
41
42
|
comboIdFromRawBody,
|
|
43
|
+
comboRequestHasImageInput,
|
|
42
44
|
concreteComboRequestBody,
|
|
43
45
|
getCombo,
|
|
44
46
|
isComboTargetInCooldown,
|
|
@@ -76,8 +78,8 @@ import {
|
|
|
76
78
|
} from "../../oauth/anthropic-routing";
|
|
77
79
|
import { buildWebSearchTool, planWebSearch, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search";
|
|
78
80
|
import { buildImageTool, buildVideoTool, planImageBridge, planVideoBridge, runWithImageBridge, clampImageMaxRounds, IMAGE_GEN_TOOL_NAME, VIDEO_GEN_TOOL_NAME } from "../../images";
|
|
79
|
-
import { describeImagesInPlace, planVisionSidecar, resolveOpenAiVisionModel, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision";
|
|
80
|
-
import { createAdapterEventQueue, preflightAdapterEvents } from "../../adapters/run-turn-queue";
|
|
81
|
+
import { describeImagesInPlace, isModelTextOnly, planVisionSidecar, resolveOpenAiVisionModel, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision";
|
|
82
|
+
import { createAdapterEventQueue, preflightAdapterEvents, type AdapterEventQueue } from "../../adapters/run-turn-queue";
|
|
81
83
|
import {
|
|
82
84
|
applyCodexAuthContextToProvider,
|
|
83
85
|
CodexAccountCooldownError,
|
|
@@ -115,6 +117,11 @@ import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } f
|
|
|
115
117
|
import { createTranslatorBudget, isTranslatorBudgetExceededError, type TranslatorBudget } from "../../lib/translator-budget";
|
|
116
118
|
import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
|
|
117
119
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
120
|
+
import { SERVICE_TIER_ADAPTERS, serviceTierSupportForModel } from "../../providers/service-tier";
|
|
121
|
+
import {
|
|
122
|
+
RequestPacingQueueOverloadError,
|
|
123
|
+
waitForProviderRequestSlot,
|
|
124
|
+
} from "../../providers/request-pacing";
|
|
118
125
|
import { slugsEquivalent } from "../../providers/slug-codec";
|
|
119
126
|
import { applyOpenAiVirtualModel, resolveOpenAiCompactModel } from "../../providers/openai-virtual-models";
|
|
120
127
|
import { isUsageDebugEnabled } from "../../usage/debug";
|
|
@@ -149,7 +156,6 @@ import {
|
|
|
149
156
|
import { isNativeMainTrafficBlocked } from "../../codex/native-profile-startup";
|
|
150
157
|
import {
|
|
151
158
|
beginRequestAttempt,
|
|
152
|
-
catalogModelSupportsServiceTier,
|
|
153
159
|
finishRequestAttempt,
|
|
154
160
|
inspectResponseLogJson,
|
|
155
161
|
noteAttemptSend,
|
|
@@ -234,6 +240,10 @@ import { createRoutedCustomToolRestoreBlockRewrite } from "../responses-custom-t
|
|
|
234
240
|
import { createGithubCopilotResponsesBlockRewrite } from "../github-copilot-responses-repair";
|
|
235
241
|
import { responsesJsonToSseStream } from "../responses-json-events";
|
|
236
242
|
import { guardTerminalEventStream } from "./terminal-guard";
|
|
243
|
+
import {
|
|
244
|
+
emptyCompletionRetryEnabled,
|
|
245
|
+
guardEmptyCompletionEventStream,
|
|
246
|
+
} from "./empty-completion-guard";
|
|
237
247
|
|
|
238
248
|
/**
|
|
239
249
|
* Adapters whose continuation state must survive Codex's store:false requests.
|
|
@@ -592,7 +602,10 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
592
602
|
upstream.signal,
|
|
593
603
|
connectMs,
|
|
594
604
|
stream,
|
|
595
|
-
providerFetch(route.provider, options.codexWsRuntimeIdentity
|
|
605
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
606
|
+
providerName: route.providerName,
|
|
607
|
+
modelId: route.modelId,
|
|
608
|
+
}),
|
|
596
609
|
// Credential-bearing forward send: never follow a redirect into a
|
|
597
610
|
// dead-host rejection after the credential was seen (#914).
|
|
598
611
|
route.provider.authMode === "forward",
|
|
@@ -1077,9 +1090,17 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
1077
1090
|
logCtx.preserveResolvedModelFromRoute = true;
|
|
1078
1091
|
}
|
|
1079
1092
|
|
|
1080
|
-
// Fast mode override
|
|
1081
|
-
//
|
|
1082
|
-
|
|
1093
|
+
// Fast mode override only where the final provider/model route explicitly documents
|
|
1094
|
+
// service-tier support. The same model-scoped resolver is used by catalog generation.
|
|
1095
|
+
const modelServiceTierSupport = serviceTierSupportForModel(
|
|
1096
|
+
route.provider,
|
|
1097
|
+
route.modelId,
|
|
1098
|
+
route.providerName,
|
|
1099
|
+
inboundWire,
|
|
1100
|
+
);
|
|
1101
|
+
if (config.fastMode !== undefined
|
|
1102
|
+
&& SERVICE_TIER_ADAPTERS.has(route.provider.adapter)
|
|
1103
|
+
&& modelServiceTierSupport === true) {
|
|
1083
1104
|
const tier = config.fastMode ? "priority" : undefined;
|
|
1084
1105
|
if (parsed._rawBody && typeof parsed._rawBody === "object") {
|
|
1085
1106
|
if (tier) (parsed._rawBody as Record<string, unknown>).service_tier = tier;
|
|
@@ -1087,8 +1108,15 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
1087
1108
|
}
|
|
1088
1109
|
parsed.options.serviceTier = tier;
|
|
1089
1110
|
}
|
|
1090
|
-
applyServiceTierGate(
|
|
1091
|
-
|
|
1111
|
+
applyServiceTierGate(
|
|
1112
|
+
route.provider,
|
|
1113
|
+
parsed._rawBody,
|
|
1114
|
+
parsed.options,
|
|
1115
|
+
route.modelId,
|
|
1116
|
+
route.providerName,
|
|
1117
|
+
inboundWire,
|
|
1118
|
+
);
|
|
1119
|
+
if (modelServiceTierSupport === false) {
|
|
1092
1120
|
logCtx.requestedServiceTier = undefined;
|
|
1093
1121
|
logCtx.requestedSpeedLabel = undefined;
|
|
1094
1122
|
}
|
|
@@ -1142,10 +1170,9 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
1142
1170
|
}
|
|
1143
1171
|
}
|
|
1144
1172
|
recordAttemptRequestedEffort(logCtx);
|
|
1145
|
-
logCtx.modelSupportsServiceTier =
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
);
|
|
1173
|
+
logCtx.modelSupportsServiceTier = SERVICE_TIER_ADAPTERS.has(route.provider.adapter)
|
|
1174
|
+
? modelServiceTierSupport
|
|
1175
|
+
: undefined;
|
|
1149
1176
|
}
|
|
1150
1177
|
|
|
1151
1178
|
|
|
@@ -1171,6 +1198,41 @@ export async function handleComboResponses(
|
|
|
1171
1198
|
if (!combo) {
|
|
1172
1199
|
return formatErrorResponse(404, "invalid_request_error", `Unknown combo: ${comboId}`);
|
|
1173
1200
|
}
|
|
1201
|
+
// Expand previous_response_id before image policy and child dispatch so a
|
|
1202
|
+
// continuation that only references prior images still fails closed when
|
|
1203
|
+
// imageInput is disabled (and so targets see the full replayed input).
|
|
1204
|
+
const body = expandPreviousResponseInput(rawBody);
|
|
1205
|
+
if (previousResponseReplayFailure(body)) {
|
|
1206
|
+
return formatErrorResponse(
|
|
1207
|
+
400,
|
|
1208
|
+
"previous_response_not_found",
|
|
1209
|
+
"Continuation state is unavailable or corrupt; resend the full conversation without previous_response_id.",
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
// Missing state returns the original body without a failure marker. Reject
|
|
1213
|
+
// that unresolved continuation for image-disabled combos so a target cannot
|
|
1214
|
+
// resolve prior images out of band. A successful expansion yields a new
|
|
1215
|
+
// object (still carrying previous_response_id) and must not be treated as
|
|
1216
|
+
// unresolved — text-only stored continuations remain allowed.
|
|
1217
|
+
const requestedPreviousId = typeof (rawBody as { previous_response_id?: unknown } | null)?.previous_response_id === "string"
|
|
1218
|
+
? (rawBody as { previous_response_id: string }).previous_response_id.trim()
|
|
1219
|
+
: "";
|
|
1220
|
+
const unresolvedPrevious = requestedPreviousId.length > 0 && body === rawBody;
|
|
1221
|
+
if (combo.imageInput === "disabled" && unresolvedPrevious) {
|
|
1222
|
+
return formatErrorResponse(
|
|
1223
|
+
400,
|
|
1224
|
+
"previous_response_not_found",
|
|
1225
|
+
"Continuation state is unavailable or corrupt; resend the full conversation without previous_response_id.",
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
if (combo.imageInput === "disabled" && comboRequestHasImageInput(body)) {
|
|
1229
|
+
return formatErrorResponse(400, "invalid_request_error", `Combo "${comboId}" does not accept image input`);
|
|
1230
|
+
}
|
|
1231
|
+
// Expansion already materialised prior input. Drop the id so the child
|
|
1232
|
+
// handleResponses path does not expand again and double-prepend history.
|
|
1233
|
+
if (body !== rawBody && body && typeof body === "object" && !Array.isArray(body)) {
|
|
1234
|
+
delete (body as Record<string, unknown>).previous_response_id;
|
|
1235
|
+
}
|
|
1174
1236
|
const adoptFailedChildLog = (childLog: RequestLogContext): void => {
|
|
1175
1237
|
// Attempts remain the complete physical history; the logical row mirrors the most recent
|
|
1176
1238
|
// failed target so an exhausted combo still has useful top-level reasoning diagnostics.
|
|
@@ -1187,7 +1249,7 @@ export async function handleComboResponses(
|
|
|
1187
1249
|
};
|
|
1188
1250
|
|
|
1189
1251
|
const unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
|
|
1190
|
-
(
|
|
1252
|
+
(body as { input?: unknown } | undefined)?.input,
|
|
1191
1253
|
);
|
|
1192
1254
|
const canDecryptUnreadableAgentTask = (target: (typeof combo.targets)[number]): boolean => {
|
|
1193
1255
|
const provider = config.providers[target.provider];
|
|
@@ -1229,7 +1291,7 @@ export async function handleComboResponses(
|
|
|
1229
1291
|
};
|
|
1230
1292
|
const targetRoute = routeConcreteModel(config, `${pick.target.provider}/${pick.target.model}`);
|
|
1231
1293
|
const childBody = concreteComboRequestBody(
|
|
1232
|
-
|
|
1294
|
+
body,
|
|
1233
1295
|
pick.target,
|
|
1234
1296
|
comboDefaultEffort(config, comboId),
|
|
1235
1297
|
supportedLadderFor({ provider: targetRoute.provider, modelId: targetRoute.modelId }),
|
|
@@ -1450,8 +1512,19 @@ export function applyServiceTierGate(
|
|
|
1450
1512
|
provider: OcxProviderConfig,
|
|
1451
1513
|
rawBody: unknown,
|
|
1452
1514
|
options: { serviceTier?: string },
|
|
1515
|
+
modelId?: string,
|
|
1516
|
+
providerName?: string,
|
|
1517
|
+
inbound: InboundWire = "responses",
|
|
1453
1518
|
): void {
|
|
1454
|
-
|
|
1519
|
+
// A direct unit caller without a model id retains the historical tri-state behavior for
|
|
1520
|
+
// adapters outside the OpenAI service-tier family. Once a model is known, resolve the final
|
|
1521
|
+
// model adapter as well: an explicit override to Anthropic (or another non-OpenAI wire) must
|
|
1522
|
+
// not carry a caller-supplied `service_tier` through a route that cannot forward it.
|
|
1523
|
+
if (modelId === undefined && !SERVICE_TIER_ADAPTERS.has(provider.adapter)) return;
|
|
1524
|
+
const support = modelId === undefined
|
|
1525
|
+
? provider.supportsServiceTier
|
|
1526
|
+
: serviceTierSupportForModel(provider, modelId, providerName, inbound);
|
|
1527
|
+
if (support !== false) return;
|
|
1455
1528
|
if (rawBody && typeof rawBody === "object") {
|
|
1456
1529
|
delete (rawBody as Record<string, unknown>).service_tier;
|
|
1457
1530
|
}
|
|
@@ -1853,6 +1926,24 @@ async function handleResponsesInner(
|
|
|
1853
1926
|
}
|
|
1854
1927
|
|
|
1855
1928
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
1929
|
+
// Refuse an input that cannot plausibly fit the model context window before spending auth,
|
|
1930
|
+
// circuit budget, or upstream bandwidth on a turn the provider will reject anyway (#1412).
|
|
1931
|
+
//
|
|
1932
|
+
// Compaction turns are exempt: Codex sends compaction_trigger BECAUSE context is full, so
|
|
1933
|
+
// refusing the turn that shrinks the context would deadlock the client against the very
|
|
1934
|
+
// limit this gate reports — it would be told to compact and then denied the compaction.
|
|
1935
|
+
if (parsed._compactionRequest !== true) {
|
|
1936
|
+
const inputAdmission = checkInputAdmission(parsed, route.provider, route.providerName, parsed.modelId);
|
|
1937
|
+
if (!inputAdmission.admitted) {
|
|
1938
|
+
return formatErrorResponse(
|
|
1939
|
+
413,
|
|
1940
|
+
"request_too_large",
|
|
1941
|
+
`Estimated input (~${inputAdmission.estimatedTokens} tokens) is far past the context window `
|
|
1942
|
+
+ `of ${parsed.modelId} (${inputAdmission.ceiling} tokens). Start a new session or choose a `
|
|
1943
|
+
+ `model with a larger context window.`,
|
|
1944
|
+
);
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1856
1947
|
const preAuthHostKey = preAuthUpstreamHostCircuitKey(route, config);
|
|
1857
1948
|
if (preAuthHostKey) {
|
|
1858
1949
|
const admission = acquireUpstreamHostAdmission(
|
|
@@ -2076,7 +2167,7 @@ async function handleResponsesInner(
|
|
|
2076
2167
|
recordSidecarOutcome,
|
|
2077
2168
|
translatorBudget,
|
|
2078
2169
|
);
|
|
2079
|
-
} else if (
|
|
2170
|
+
} else if (isModelTextOnly(route.provider, route.modelId)) {
|
|
2080
2171
|
// Sidecar-covered model but NO plan (no forward provider / missing forwarded auth / sidecar
|
|
2081
2172
|
// disabled): fail closed — never forward raw images to a text-only upstream.
|
|
2082
2173
|
stripImagesInPlace(parsed, translatorBudget);
|
|
@@ -2282,7 +2373,10 @@ async function handleResponsesInner(
|
|
|
2282
2373
|
headers: request.headers,
|
|
2283
2374
|
body: request.body,
|
|
2284
2375
|
}, recovery), upstream.signal, connectMs, parsed.stream,
|
|
2285
|
-
providerFetch(route.provider, options.codexWsRuntimeIdentity
|
|
2376
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
2377
|
+
providerName: route.providerName,
|
|
2378
|
+
modelId: route.modelId,
|
|
2379
|
+
}),
|
|
2286
2380
|
route.provider.authMode === "forward")
|
|
2287
2381
|
// Every real attempt response — including an intermediate 5xx the
|
|
2288
2382
|
// retry wrapper replaces — proves the host was reached (#914 review).
|
|
@@ -2344,7 +2438,10 @@ async function handleResponsesInner(
|
|
|
2344
2438
|
headers: request.headers,
|
|
2345
2439
|
body: request.body,
|
|
2346
2440
|
}, recovery), upstream.signal, connectMs, parsed.stream,
|
|
2347
|
-
providerFetch(route.provider, options.codexWsRuntimeIdentity
|
|
2441
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
2442
|
+
providerName: route.providerName,
|
|
2443
|
+
modelId: route.modelId,
|
|
2444
|
+
}),
|
|
2348
2445
|
route.provider.authMode === "forward")
|
|
2349
2446
|
.then(res => {
|
|
2350
2447
|
settleObservedHostResponse();
|
|
@@ -2649,6 +2746,7 @@ async function handleResponsesInner(
|
|
|
2649
2746
|
} else {
|
|
2650
2747
|
logCtx.transportPhase = "mid_stream";
|
|
2651
2748
|
logCtx.terminalSource = "synthetic";
|
|
2749
|
+
if (logCtx.activeAttempt) logCtx.activeAttempt.streamAborted = true;
|
|
2652
2750
|
reportNativeTerminal("failed", 502);
|
|
2653
2751
|
}
|
|
2654
2752
|
},
|
|
@@ -2932,6 +3030,11 @@ async function handleResponsesInner(
|
|
|
2932
3030
|
&& (tc.name === "image_generation" || imgPlan.toolNames.has(tc.name))) {
|
|
2933
3031
|
parsed.options.toolChoice = { ...tc, name: IMAGE_GEN_TOOL_NAME };
|
|
2934
3032
|
}
|
|
3033
|
+
const imageProviderFetch = providerFetch(
|
|
3034
|
+
route.provider,
|
|
3035
|
+
options.codexWsRuntimeIdentity,
|
|
3036
|
+
{ providerName: route.providerName, modelId: route.modelId },
|
|
3037
|
+
);
|
|
2935
3038
|
const imgResponse = await runWithImageBridge({
|
|
2936
3039
|
parsed, adapter,
|
|
2937
3040
|
incomingMeta: { headers: selectedForwardHeaders, abortSignal: options.abortSignal, translatorBudget },
|
|
@@ -2948,7 +3051,8 @@ async function handleResponsesInner(
|
|
|
2948
3051
|
: clampImageMaxRounds(config.images?.videoMaxRounds ?? 2),
|
|
2949
3052
|
connectTimeoutMs: config.connectTimeoutMs ?? 200_000,
|
|
2950
3053
|
stallTimeoutSec: config.stallTimeoutSec,
|
|
2951
|
-
|
|
3054
|
+
waitForRequestSlot: imageProviderFetch.waitForPacing,
|
|
3055
|
+
fetchImpl: imageProviderFetch.unpacedFetch ?? imageProviderFetch,
|
|
2952
3056
|
onRequestBuilt: request => recordAdapterReasoning(logCtx, request),
|
|
2953
3057
|
...(vidPlan?.timeoutMs ? { videoTimeoutMs: vidPlan.timeoutMs } : {}),
|
|
2954
3058
|
onUsage: usage => {
|
|
@@ -3077,34 +3181,88 @@ async function handleResponsesInner(
|
|
|
3077
3181
|
return wsResponse;
|
|
3078
3182
|
}
|
|
3079
3183
|
|
|
3184
|
+
// Empty-completion guard (codex-router PR #145 port): a 200 that completes with no output
|
|
3185
|
+
// text and no tool call is a failure the client cannot see — it silently records the turn as
|
|
3186
|
+
// done. The guard holds pre-content adapter events, suppresses the terminal of an empty
|
|
3187
|
+
// turn, retries the IDENTICAL request once, and surfaces a stated error when the retry is
|
|
3188
|
+
// empty or fails. This is a top-level config opt-in; OCX_EMPTY_COMPLETION_RETRY=0 is a
|
|
3189
|
+
// disable-only emergency override. Compaction turns and combo attempts keep their own
|
|
3190
|
+
// machinery (the combo preflight already handles empty streams). Native Chat-to-Chat
|
|
3191
|
+
// requests return from handleChatCompletions before entering Responses core, so they are
|
|
3192
|
+
// intentionally outside this guard and retain their existing one-send wire behavior.
|
|
3193
|
+
const emptyCompletionGuardEnabled =
|
|
3194
|
+
emptyCompletionRetryEnabled(config)
|
|
3195
|
+
&& !options.comboAttempt
|
|
3196
|
+
&& !routedCompaction;
|
|
3197
|
+
|
|
3080
3198
|
if (adapter.runTurn) {
|
|
3081
3199
|
const runTurnAbort = new AbortController();
|
|
3082
|
-
linkAbortSignal(runTurnAbort, options.abortSignal);
|
|
3200
|
+
const cleanupRunTurnAbort = linkAbortSignal(runTurnAbort, options.abortSignal);
|
|
3083
3201
|
const queue = createAdapterEventQueue({
|
|
3084
3202
|
onBacklogExceeded: () => runTurnAbort.abort(),
|
|
3085
3203
|
});
|
|
3086
|
-
|
|
3204
|
+
// Initial admission must settle before the streaming Response commits HTTP 200.
|
|
3205
|
+
// Let the outer Responses facade preserve the local retryable-429 contract.
|
|
3206
|
+
try {
|
|
3207
|
+
await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
|
|
3208
|
+
} catch (error) {
|
|
3209
|
+
cleanupRunTurnAbort();
|
|
3210
|
+
queue.close();
|
|
3211
|
+
throw error;
|
|
3212
|
+
}
|
|
3213
|
+
// One attempt of the runTurn transport, against an explicit queue. The
|
|
3214
|
+
// empty-completion guard re-invokes the IDENTICAL turn (same parsed request,
|
|
3215
|
+
// same forwarded headers, same abort signal) through a fresh queue, so the
|
|
3216
|
+
// attempt body must not capture the first queue. Each attempt consumes its
|
|
3217
|
+
// own provider pacing slot (#1584): retries are paced like first attempts.
|
|
3218
|
+
const runTurnAttempt = async (
|
|
3219
|
+
targetQueue: AdapterEventQueue,
|
|
3220
|
+
recovery?: AttemptRecoveryKind,
|
|
3221
|
+
pacingSlotAcquired = false,
|
|
3222
|
+
): Promise<void> => {
|
|
3087
3223
|
try {
|
|
3088
|
-
|
|
3224
|
+
if (!pacingSlotAcquired) {
|
|
3225
|
+
await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
|
|
3226
|
+
}
|
|
3227
|
+
noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens, recovery);
|
|
3089
3228
|
await adapter.runTurn?.(
|
|
3090
3229
|
parsed,
|
|
3091
3230
|
{ headers: selectedForwardHeaders, abortSignal: runTurnAbort.signal, translatorBudget },
|
|
3092
|
-
|
|
3231
|
+
targetQueue.push,
|
|
3093
3232
|
);
|
|
3094
3233
|
} catch (err) {
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3234
|
+
targetQueue.push(err instanceof RequestPacingQueueOverloadError
|
|
3235
|
+
? {
|
|
3236
|
+
type: "error",
|
|
3237
|
+
status: 429,
|
|
3238
|
+
errorType: "rate_limit_error",
|
|
3239
|
+
retryable: true,
|
|
3240
|
+
message: err.message,
|
|
3241
|
+
}
|
|
3242
|
+
: {
|
|
3243
|
+
type: "error",
|
|
3244
|
+
message: err instanceof Error ? err.message : String(err),
|
|
3245
|
+
});
|
|
3099
3246
|
} finally {
|
|
3100
3247
|
// Cursor assigns a stable conversation id inside runTurn on the first headerless
|
|
3101
3248
|
// turn; backfill so Logs can filter/total that opening request (#330 / #522).
|
|
3102
3249
|
if (!logCtx.conversationId && parsed._cursorConversationId) {
|
|
3103
3250
|
logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId);
|
|
3104
3251
|
}
|
|
3105
|
-
|
|
3252
|
+
targetQueue.close();
|
|
3106
3253
|
}
|
|
3107
3254
|
};
|
|
3255
|
+
const runTurn = async (): Promise<void> => runTurnAttempt(queue, undefined, true);
|
|
3256
|
+
// The empty-completion retry re-runs the turn against a fresh queue: the
|
|
3257
|
+
// first queue is closed once its attempt settles, and pushing into it after
|
|
3258
|
+
// close is a silent no-op.
|
|
3259
|
+
const runTurnRetrySource = (): AsyncIterable<AdapterEvent> => {
|
|
3260
|
+
const retryQueue = createAdapterEventQueue({
|
|
3261
|
+
onBacklogExceeded: () => runTurnAbort.abort(),
|
|
3262
|
+
});
|
|
3263
|
+
void runTurnAttempt(retryQueue, "empty-completion");
|
|
3264
|
+
return retryQueue.stream();
|
|
3265
|
+
};
|
|
3108
3266
|
|
|
3109
3267
|
const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
|
|
3110
3268
|
if (parsed.stream) {
|
|
@@ -3120,8 +3278,16 @@ async function handleResponsesInner(
|
|
|
3120
3278
|
}
|
|
3121
3279
|
eventSource = preflight.stream;
|
|
3122
3280
|
}
|
|
3281
|
+
const guardedSource = emptyCompletionGuardEnabled
|
|
3282
|
+
? guardEmptyCompletionEventStream({
|
|
3283
|
+
firstEvents: eventSource,
|
|
3284
|
+
// Identical-turn retry: same parsed request, same headers, same
|
|
3285
|
+
// signal — run the adapter transport again against a fresh queue.
|
|
3286
|
+
continuation: runTurnRetrySource,
|
|
3287
|
+
})
|
|
3288
|
+
: eventSource;
|
|
3123
3289
|
const sseStream = bridgeToResponsesSSE(
|
|
3124
|
-
|
|
3290
|
+
guardedSource, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
|
|
3125
3291
|
() => {
|
|
3126
3292
|
runTurnAbort.abort();
|
|
3127
3293
|
queue.close();
|
|
@@ -3164,7 +3330,17 @@ async function handleResponsesInner(
|
|
|
3164
3330
|
}
|
|
3165
3331
|
|
|
3166
3332
|
await runTurn();
|
|
3167
|
-
const
|
|
3333
|
+
const firstAttemptEvents = await queue.collect();
|
|
3334
|
+
let events: AdapterEvent[];
|
|
3335
|
+
if (emptyCompletionGuardEnabled) {
|
|
3336
|
+
events = [];
|
|
3337
|
+
for await (const event of guardEmptyCompletionEventStream({
|
|
3338
|
+
firstEvents: (async function* () { yield* firstAttemptEvents; })(),
|
|
3339
|
+
continuation: runTurnRetrySource,
|
|
3340
|
+
})) events.push(event);
|
|
3341
|
+
} else {
|
|
3342
|
+
events = firstAttemptEvents;
|
|
3343
|
+
}
|
|
3168
3344
|
if (options.comboAttempt) {
|
|
3169
3345
|
const firstMeaningful = events.find(event => event.type !== "heartbeat");
|
|
3170
3346
|
if (!firstMeaningful || firstMeaningful.type === "error") {
|
|
@@ -3259,6 +3435,7 @@ async function handleResponsesInner(
|
|
|
3259
3435
|
try {
|
|
3260
3436
|
if (activeAdapter.fetchResponse) {
|
|
3261
3437
|
noteAttemptSend(logCtx.activeAttempt, inputTokenEstimate);
|
|
3438
|
+
await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, upstream.signal);
|
|
3262
3439
|
upstreamResponse = await activeAdapter.fetchResponse(builtInitialRequest, {
|
|
3263
3440
|
abortSignal: upstream.signal,
|
|
3264
3441
|
timeoutMs: connectMs,
|
|
@@ -3273,7 +3450,10 @@ async function handleResponsesInner(
|
|
|
3273
3450
|
headers: builtInitialRequest.headers,
|
|
3274
3451
|
body: builtInitialRequest.body,
|
|
3275
3452
|
}, recovery), upstream.signal, connectMs, parsed.stream,
|
|
3276
|
-
providerFetch(route.provider, options.codexWsRuntimeIdentity
|
|
3453
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
3454
|
+
providerName: route.providerName,
|
|
3455
|
+
modelId: route.modelId,
|
|
3456
|
+
}));
|
|
3277
3457
|
},
|
|
3278
3458
|
{ abortSignal: upstream.signal, label: safeHostLabel(builtInitialRequest.url) },
|
|
3279
3459
|
);
|
|
@@ -3348,12 +3528,17 @@ async function handleResponsesInner(
|
|
|
3348
3528
|
noteAttemptSend(logCtx.activeAttempt, retryEstimate, recovery);
|
|
3349
3529
|
try {
|
|
3350
3530
|
try {
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3531
|
+
if (activeAdapter.fetchResponse) {
|
|
3532
|
+
await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, upstream.signal);
|
|
3533
|
+
return await activeAdapter.fetchResponse(retryRequest, { abortSignal: upstream.signal, timeoutMs: connectMs, stream: parsed.stream });
|
|
3534
|
+
}
|
|
3535
|
+
return await fetchWithHeaderTimeout(retryRequest.url, {
|
|
3536
|
+
method: retryRequest.method, headers: retryRequest.headers, body: retryRequest.body,
|
|
3537
|
+
}, upstream.signal, connectMs, parsed.stream,
|
|
3538
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
3539
|
+
providerName: route.providerName,
|
|
3540
|
+
modelId: route.modelId,
|
|
3541
|
+
}));
|
|
3357
3542
|
} finally {
|
|
3358
3543
|
retryRequest.releaseBodyObservation?.();
|
|
3359
3544
|
}
|
|
@@ -3602,22 +3787,30 @@ async function handleResponsesInner(
|
|
|
3602
3787
|
|
|
3603
3788
|
cancelBodyOnAbort(upstreamResponse.body, upstream.signal);
|
|
3604
3789
|
|
|
3605
|
-
//
|
|
3606
|
-
//
|
|
3607
|
-
|
|
3790
|
+
// One bounded internal continuation re-ask for clean end_turn turns that announced an edit
|
|
3791
|
+
// without emitting a tool call. Anthropic gets this by default; openai-chat providers opt in
|
|
3792
|
+
// per-provider via `terminalContinuationGuard` (the heuristic was tuned on Anthropic turns,
|
|
3793
|
+
// so it stays off for the shared openai-chat adapter unless a provider enables it).
|
|
3794
|
+
const terminalGuardEnabled = (activeAdapter.name === "anthropic"
|
|
3795
|
+
|| (activeAdapter.name === "openai-chat" && route.provider.terminalContinuationGuard === true))
|
|
3796
|
+
&& !options.comboAttempt && !routedCompaction;
|
|
3608
3797
|
/**
|
|
3609
3798
|
* One bounded internal re-ask for Anthropic end_turn-without-tool-call turns. Replays the
|
|
3610
3799
|
* continuation on a 429 with the same-key retry budget (hoisted per request), then falls
|
|
3611
3800
|
* back to key/account failover; a failure becomes an in-stream adapter error so the client
|
|
3612
3801
|
* never sees a second hidden HTTP response or an unbounded retry loop.
|
|
3613
3802
|
*/
|
|
3614
|
-
const fetchTerminalGuardContinuation = async function* (
|
|
3803
|
+
const fetchTerminalGuardContinuation = async function* (
|
|
3804
|
+
nextParsed: OcxParsedRequest,
|
|
3805
|
+
initialRecoveryKind?: AttemptRecoveryKind,
|
|
3806
|
+
): AsyncGenerator<AdapterEvent> {
|
|
3615
3807
|
let response: Response | undefined;
|
|
3616
3808
|
// One-shot recovery label for the next top-of-loop continuation send after a failover rotation.
|
|
3617
|
-
let nextContinuationRecoveryKind: AttemptRecoveryKind | undefined;
|
|
3809
|
+
let nextContinuationRecoveryKind: AttemptRecoveryKind | undefined = initialRecoveryKind;
|
|
3618
3810
|
/**
|
|
3619
3811
|
* Build and fetch one terminal-guard continuation. `recoveryKind` tags same-target and
|
|
3620
|
-
* failover sends (`rate-limit-429`, `key-429`,
|
|
3812
|
+
* failover sends (`empty-completion`, `rate-limit-429`, `key-429`,
|
|
3813
|
+
* `anthropic-oauth-429`, `image-413`); the
|
|
3621
3814
|
* adapter rebuild is deterministic for the same parsed request (tests assert byte-identical
|
|
3622
3815
|
* replays).
|
|
3623
3816
|
*/
|
|
@@ -3658,6 +3851,7 @@ async function handleResponsesInner(
|
|
|
3658
3851
|
try {
|
|
3659
3852
|
if (activeAdapter.fetchResponse) {
|
|
3660
3853
|
noteAttemptSend(logCtx.activeAttempt, continuationEstimate, replayKind);
|
|
3854
|
+
await waitForProviderRequestSlot(route.providerName, route.provider, nextParsed.modelId, upstream.signal);
|
|
3661
3855
|
return await activeAdapter.fetchResponse(builtContinuationRequest, {
|
|
3662
3856
|
abortSignal: upstream.signal,
|
|
3663
3857
|
timeoutMs: connectMs,
|
|
@@ -3677,7 +3871,10 @@ async function handleResponsesInner(
|
|
|
3677
3871
|
upstream.signal,
|
|
3678
3872
|
connectMs,
|
|
3679
3873
|
nextParsed.stream,
|
|
3680
|
-
providerFetch(route.provider, options.codexWsRuntimeIdentity
|
|
3874
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
3875
|
+
providerName: route.providerName,
|
|
3876
|
+
modelId: nextParsed.modelId,
|
|
3877
|
+
}),
|
|
3681
3878
|
);
|
|
3682
3879
|
},
|
|
3683
3880
|
{ abortSignal: upstream.signal, label: safeHostLabel(builtContinuationRequest.url) },
|
|
@@ -3861,6 +4058,19 @@ async function handleResponsesInner(
|
|
|
3861
4058
|
}
|
|
3862
4059
|
};
|
|
3863
4060
|
|
|
4061
|
+
const fetchGuardedEmptyCompletionRetry = (): AsyncIterable<AdapterEvent> => {
|
|
4062
|
+
const retryEvents = fetchTerminalGuardContinuation(parsed, "empty-completion");
|
|
4063
|
+
return terminalGuardEnabled
|
|
4064
|
+
? guardTerminalEventStream({
|
|
4065
|
+
parsed,
|
|
4066
|
+
firstEvents: retryEvents,
|
|
4067
|
+
adapterName: activeAdapter.name,
|
|
4068
|
+
maxAutoContinuations: 1,
|
|
4069
|
+
continuation: fetchTerminalGuardContinuation,
|
|
4070
|
+
})
|
|
4071
|
+
: retryEvents;
|
|
4072
|
+
};
|
|
4073
|
+
|
|
3864
4074
|
if (parsed.stream) {
|
|
3865
4075
|
const initialEventStream = activeAdapter.parseStream(upstreamResponse, translatorBudget);
|
|
3866
4076
|
const eventStream = terminalGuardEnabled
|
|
@@ -3872,9 +4082,19 @@ async function handleResponsesInner(
|
|
|
3872
4082
|
continuation: fetchTerminalGuardContinuation,
|
|
3873
4083
|
})
|
|
3874
4084
|
: initialEventStream;
|
|
4085
|
+
// The empty-completion guard sits OUTSIDE the terminal guard: a completed
|
|
4086
|
+
// turn with no text and no tool call is retried with the IDENTICAL request
|
|
4087
|
+
// (fetchTerminalGuardContinuation(parsed) replays the cached byte-identical
|
|
4088
|
+
// request — same body, same headers, same signal).
|
|
4089
|
+
const guardedEventStream = emptyCompletionGuardEnabled
|
|
4090
|
+
? guardEmptyCompletionEventStream({
|
|
4091
|
+
firstEvents: eventStream,
|
|
4092
|
+
continuation: fetchGuardedEmptyCompletionRetry,
|
|
4093
|
+
})
|
|
4094
|
+
: eventStream;
|
|
3875
4095
|
const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
|
|
3876
4096
|
const sseStream = bridgeToResponsesSSE(
|
|
3877
|
-
|
|
4097
|
+
guardedEventStream, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
|
|
3878
4098
|
() => upstream.abort(), 2_000,
|
|
3879
4099
|
{
|
|
3880
4100
|
translatorBudget,
|
|
@@ -3919,17 +4139,27 @@ async function handleResponsesInner(
|
|
|
3919
4139
|
let events: AdapterEvent[];
|
|
3920
4140
|
try {
|
|
3921
4141
|
const initialEvents = await activeAdapter.parseResponse(upstreamResponse, translatorBudget);
|
|
4142
|
+
let guardedEvents: AdapterEvent[];
|
|
3922
4143
|
if (terminalGuardEnabled) {
|
|
3923
|
-
|
|
4144
|
+
guardedEvents = [];
|
|
3924
4145
|
for await (const event of guardTerminalEventStream({
|
|
3925
4146
|
parsed,
|
|
3926
4147
|
firstEvents: (async function* () { yield* initialEvents; })(),
|
|
3927
4148
|
adapterName: activeAdapter.name,
|
|
3928
4149
|
maxAutoContinuations: 1,
|
|
3929
4150
|
continuation: fetchTerminalGuardContinuation,
|
|
4151
|
+
})) guardedEvents.push(event);
|
|
4152
|
+
} else {
|
|
4153
|
+
guardedEvents = initialEvents;
|
|
4154
|
+
}
|
|
4155
|
+
if (emptyCompletionGuardEnabled) {
|
|
4156
|
+
events = [];
|
|
4157
|
+
for await (const event of guardEmptyCompletionEventStream({
|
|
4158
|
+
firstEvents: (async function* () { yield* guardedEvents; })(),
|
|
4159
|
+
continuation: fetchGuardedEmptyCompletionRetry,
|
|
3930
4160
|
})) events.push(event);
|
|
3931
4161
|
} else {
|
|
3932
|
-
events =
|
|
4162
|
+
events = guardedEvents;
|
|
3933
4163
|
}
|
|
3934
4164
|
} finally {
|
|
3935
4165
|
cleanupUpstreamAbort();
|