@bitkyc08/opencodex 2.19.0 → 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/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 +13 -7
- 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/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 +224 -76
- 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/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 +7 -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/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 -36
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +371 -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 +257 -46
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/fetch-helpers.ts +35 -4
- 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/types.ts +66 -4
- 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/gui/dist/assets/index-CQ7bIKee.css +0 -1
- package/gui/dist/assets/index-D_JUZLEC.js +0 -76
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
comboDefaultEffort,
|
|
41
41
|
comboFailureDecision,
|
|
42
42
|
comboIdFromRawBody,
|
|
43
|
+
comboRequestHasImageInput,
|
|
43
44
|
concreteComboRequestBody,
|
|
44
45
|
getCombo,
|
|
45
46
|
isComboTargetInCooldown,
|
|
@@ -78,7 +79,7 @@ import {
|
|
|
78
79
|
import { buildWebSearchTool, planWebSearch, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search";
|
|
79
80
|
import { buildImageTool, buildVideoTool, planImageBridge, planVideoBridge, runWithImageBridge, clampImageMaxRounds, IMAGE_GEN_TOOL_NAME, VIDEO_GEN_TOOL_NAME } from "../../images";
|
|
80
81
|
import { describeImagesInPlace, isModelTextOnly, planVisionSidecar, resolveOpenAiVisionModel, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision";
|
|
81
|
-
import { createAdapterEventQueue, preflightAdapterEvents } from "../../adapters/run-turn-queue";
|
|
82
|
+
import { createAdapterEventQueue, preflightAdapterEvents, type AdapterEventQueue } from "../../adapters/run-turn-queue";
|
|
82
83
|
import {
|
|
83
84
|
applyCodexAuthContextToProvider,
|
|
84
85
|
CodexAccountCooldownError,
|
|
@@ -116,6 +117,11 @@ import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } f
|
|
|
116
117
|
import { createTranslatorBudget, isTranslatorBudgetExceededError, type TranslatorBudget } from "../../lib/translator-budget";
|
|
117
118
|
import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
|
|
118
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";
|
|
119
125
|
import { slugsEquivalent } from "../../providers/slug-codec";
|
|
120
126
|
import { applyOpenAiVirtualModel, resolveOpenAiCompactModel } from "../../providers/openai-virtual-models";
|
|
121
127
|
import { isUsageDebugEnabled } from "../../usage/debug";
|
|
@@ -150,7 +156,6 @@ import {
|
|
|
150
156
|
import { isNativeMainTrafficBlocked } from "../../codex/native-profile-startup";
|
|
151
157
|
import {
|
|
152
158
|
beginRequestAttempt,
|
|
153
|
-
catalogModelSupportsServiceTier,
|
|
154
159
|
finishRequestAttempt,
|
|
155
160
|
inspectResponseLogJson,
|
|
156
161
|
noteAttemptSend,
|
|
@@ -235,6 +240,10 @@ import { createRoutedCustomToolRestoreBlockRewrite } from "../responses-custom-t
|
|
|
235
240
|
import { createGithubCopilotResponsesBlockRewrite } from "../github-copilot-responses-repair";
|
|
236
241
|
import { responsesJsonToSseStream } from "../responses-json-events";
|
|
237
242
|
import { guardTerminalEventStream } from "./terminal-guard";
|
|
243
|
+
import {
|
|
244
|
+
emptyCompletionRetryEnabled,
|
|
245
|
+
guardEmptyCompletionEventStream,
|
|
246
|
+
} from "./empty-completion-guard";
|
|
238
247
|
|
|
239
248
|
/**
|
|
240
249
|
* Adapters whose continuation state must survive Codex's store:false requests.
|
|
@@ -593,7 +602,10 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
593
602
|
upstream.signal,
|
|
594
603
|
connectMs,
|
|
595
604
|
stream,
|
|
596
|
-
providerFetch(route.provider, options.codexWsRuntimeIdentity
|
|
605
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
606
|
+
providerName: route.providerName,
|
|
607
|
+
modelId: route.modelId,
|
|
608
|
+
}),
|
|
597
609
|
// Credential-bearing forward send: never follow a redirect into a
|
|
598
610
|
// dead-host rejection after the credential was seen (#914).
|
|
599
611
|
route.provider.authMode === "forward",
|
|
@@ -1078,9 +1090,17 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
1078
1090
|
logCtx.preserveResolvedModelFromRoute = true;
|
|
1079
1091
|
}
|
|
1080
1092
|
|
|
1081
|
-
// Fast mode override
|
|
1082
|
-
//
|
|
1083
|
-
|
|
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) {
|
|
1084
1104
|
const tier = config.fastMode ? "priority" : undefined;
|
|
1085
1105
|
if (parsed._rawBody && typeof parsed._rawBody === "object") {
|
|
1086
1106
|
if (tier) (parsed._rawBody as Record<string, unknown>).service_tier = tier;
|
|
@@ -1088,8 +1108,15 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
1088
1108
|
}
|
|
1089
1109
|
parsed.options.serviceTier = tier;
|
|
1090
1110
|
}
|
|
1091
|
-
applyServiceTierGate(
|
|
1092
|
-
|
|
1111
|
+
applyServiceTierGate(
|
|
1112
|
+
route.provider,
|
|
1113
|
+
parsed._rawBody,
|
|
1114
|
+
parsed.options,
|
|
1115
|
+
route.modelId,
|
|
1116
|
+
route.providerName,
|
|
1117
|
+
inboundWire,
|
|
1118
|
+
);
|
|
1119
|
+
if (modelServiceTierSupport === false) {
|
|
1093
1120
|
logCtx.requestedServiceTier = undefined;
|
|
1094
1121
|
logCtx.requestedSpeedLabel = undefined;
|
|
1095
1122
|
}
|
|
@@ -1143,10 +1170,9 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
1143
1170
|
}
|
|
1144
1171
|
}
|
|
1145
1172
|
recordAttemptRequestedEffort(logCtx);
|
|
1146
|
-
logCtx.modelSupportsServiceTier =
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
);
|
|
1173
|
+
logCtx.modelSupportsServiceTier = SERVICE_TIER_ADAPTERS.has(route.provider.adapter)
|
|
1174
|
+
? modelServiceTierSupport
|
|
1175
|
+
: undefined;
|
|
1150
1176
|
}
|
|
1151
1177
|
|
|
1152
1178
|
|
|
@@ -1172,6 +1198,41 @@ export async function handleComboResponses(
|
|
|
1172
1198
|
if (!combo) {
|
|
1173
1199
|
return formatErrorResponse(404, "invalid_request_error", `Unknown combo: ${comboId}`);
|
|
1174
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
|
+
}
|
|
1175
1236
|
const adoptFailedChildLog = (childLog: RequestLogContext): void => {
|
|
1176
1237
|
// Attempts remain the complete physical history; the logical row mirrors the most recent
|
|
1177
1238
|
// failed target so an exhausted combo still has useful top-level reasoning diagnostics.
|
|
@@ -1188,7 +1249,7 @@ export async function handleComboResponses(
|
|
|
1188
1249
|
};
|
|
1189
1250
|
|
|
1190
1251
|
const unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
|
|
1191
|
-
(
|
|
1252
|
+
(body as { input?: unknown } | undefined)?.input,
|
|
1192
1253
|
);
|
|
1193
1254
|
const canDecryptUnreadableAgentTask = (target: (typeof combo.targets)[number]): boolean => {
|
|
1194
1255
|
const provider = config.providers[target.provider];
|
|
@@ -1230,7 +1291,7 @@ export async function handleComboResponses(
|
|
|
1230
1291
|
};
|
|
1231
1292
|
const targetRoute = routeConcreteModel(config, `${pick.target.provider}/${pick.target.model}`);
|
|
1232
1293
|
const childBody = concreteComboRequestBody(
|
|
1233
|
-
|
|
1294
|
+
body,
|
|
1234
1295
|
pick.target,
|
|
1235
1296
|
comboDefaultEffort(config, comboId),
|
|
1236
1297
|
supportedLadderFor({ provider: targetRoute.provider, modelId: targetRoute.modelId }),
|
|
@@ -1451,8 +1512,19 @@ export function applyServiceTierGate(
|
|
|
1451
1512
|
provider: OcxProviderConfig,
|
|
1452
1513
|
rawBody: unknown,
|
|
1453
1514
|
options: { serviceTier?: string },
|
|
1515
|
+
modelId?: string,
|
|
1516
|
+
providerName?: string,
|
|
1517
|
+
inbound: InboundWire = "responses",
|
|
1454
1518
|
): void {
|
|
1455
|
-
|
|
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;
|
|
1456
1528
|
if (rawBody && typeof rawBody === "object") {
|
|
1457
1529
|
delete (rawBody as Record<string, unknown>).service_tier;
|
|
1458
1530
|
}
|
|
@@ -2301,7 +2373,10 @@ async function handleResponsesInner(
|
|
|
2301
2373
|
headers: request.headers,
|
|
2302
2374
|
body: request.body,
|
|
2303
2375
|
}, recovery), upstream.signal, connectMs, parsed.stream,
|
|
2304
|
-
providerFetch(route.provider, options.codexWsRuntimeIdentity
|
|
2376
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
2377
|
+
providerName: route.providerName,
|
|
2378
|
+
modelId: route.modelId,
|
|
2379
|
+
}),
|
|
2305
2380
|
route.provider.authMode === "forward")
|
|
2306
2381
|
// Every real attempt response — including an intermediate 5xx the
|
|
2307
2382
|
// retry wrapper replaces — proves the host was reached (#914 review).
|
|
@@ -2363,7 +2438,10 @@ async function handleResponsesInner(
|
|
|
2363
2438
|
headers: request.headers,
|
|
2364
2439
|
body: request.body,
|
|
2365
2440
|
}, recovery), upstream.signal, connectMs, parsed.stream,
|
|
2366
|
-
providerFetch(route.provider, options.codexWsRuntimeIdentity
|
|
2441
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
2442
|
+
providerName: route.providerName,
|
|
2443
|
+
modelId: route.modelId,
|
|
2444
|
+
}),
|
|
2367
2445
|
route.provider.authMode === "forward")
|
|
2368
2446
|
.then(res => {
|
|
2369
2447
|
settleObservedHostResponse();
|
|
@@ -2668,6 +2746,7 @@ async function handleResponsesInner(
|
|
|
2668
2746
|
} else {
|
|
2669
2747
|
logCtx.transportPhase = "mid_stream";
|
|
2670
2748
|
logCtx.terminalSource = "synthetic";
|
|
2749
|
+
if (logCtx.activeAttempt) logCtx.activeAttempt.streamAborted = true;
|
|
2671
2750
|
reportNativeTerminal("failed", 502);
|
|
2672
2751
|
}
|
|
2673
2752
|
},
|
|
@@ -2951,6 +3030,11 @@ async function handleResponsesInner(
|
|
|
2951
3030
|
&& (tc.name === "image_generation" || imgPlan.toolNames.has(tc.name))) {
|
|
2952
3031
|
parsed.options.toolChoice = { ...tc, name: IMAGE_GEN_TOOL_NAME };
|
|
2953
3032
|
}
|
|
3033
|
+
const imageProviderFetch = providerFetch(
|
|
3034
|
+
route.provider,
|
|
3035
|
+
options.codexWsRuntimeIdentity,
|
|
3036
|
+
{ providerName: route.providerName, modelId: route.modelId },
|
|
3037
|
+
);
|
|
2954
3038
|
const imgResponse = await runWithImageBridge({
|
|
2955
3039
|
parsed, adapter,
|
|
2956
3040
|
incomingMeta: { headers: selectedForwardHeaders, abortSignal: options.abortSignal, translatorBudget },
|
|
@@ -2967,7 +3051,8 @@ async function handleResponsesInner(
|
|
|
2967
3051
|
: clampImageMaxRounds(config.images?.videoMaxRounds ?? 2),
|
|
2968
3052
|
connectTimeoutMs: config.connectTimeoutMs ?? 200_000,
|
|
2969
3053
|
stallTimeoutSec: config.stallTimeoutSec,
|
|
2970
|
-
|
|
3054
|
+
waitForRequestSlot: imageProviderFetch.waitForPacing,
|
|
3055
|
+
fetchImpl: imageProviderFetch.unpacedFetch ?? imageProviderFetch,
|
|
2971
3056
|
onRequestBuilt: request => recordAdapterReasoning(logCtx, request),
|
|
2972
3057
|
...(vidPlan?.timeoutMs ? { videoTimeoutMs: vidPlan.timeoutMs } : {}),
|
|
2973
3058
|
onUsage: usage => {
|
|
@@ -3096,34 +3181,88 @@ async function handleResponsesInner(
|
|
|
3096
3181
|
return wsResponse;
|
|
3097
3182
|
}
|
|
3098
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
|
+
|
|
3099
3198
|
if (adapter.runTurn) {
|
|
3100
3199
|
const runTurnAbort = new AbortController();
|
|
3101
|
-
linkAbortSignal(runTurnAbort, options.abortSignal);
|
|
3200
|
+
const cleanupRunTurnAbort = linkAbortSignal(runTurnAbort, options.abortSignal);
|
|
3102
3201
|
const queue = createAdapterEventQueue({
|
|
3103
3202
|
onBacklogExceeded: () => runTurnAbort.abort(),
|
|
3104
3203
|
});
|
|
3105
|
-
|
|
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> => {
|
|
3106
3223
|
try {
|
|
3107
|
-
|
|
3224
|
+
if (!pacingSlotAcquired) {
|
|
3225
|
+
await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
|
|
3226
|
+
}
|
|
3227
|
+
noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens, recovery);
|
|
3108
3228
|
await adapter.runTurn?.(
|
|
3109
3229
|
parsed,
|
|
3110
3230
|
{ headers: selectedForwardHeaders, abortSignal: runTurnAbort.signal, translatorBudget },
|
|
3111
|
-
|
|
3231
|
+
targetQueue.push,
|
|
3112
3232
|
);
|
|
3113
3233
|
} catch (err) {
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
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
|
+
});
|
|
3118
3246
|
} finally {
|
|
3119
3247
|
// Cursor assigns a stable conversation id inside runTurn on the first headerless
|
|
3120
3248
|
// turn; backfill so Logs can filter/total that opening request (#330 / #522).
|
|
3121
3249
|
if (!logCtx.conversationId && parsed._cursorConversationId) {
|
|
3122
3250
|
logCtx.conversationId = normalizeLogConversationId(parsed._cursorConversationId);
|
|
3123
3251
|
}
|
|
3124
|
-
|
|
3252
|
+
targetQueue.close();
|
|
3125
3253
|
}
|
|
3126
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
|
+
};
|
|
3127
3266
|
|
|
3128
3267
|
const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
|
|
3129
3268
|
if (parsed.stream) {
|
|
@@ -3139,8 +3278,16 @@ async function handleResponsesInner(
|
|
|
3139
3278
|
}
|
|
3140
3279
|
eventSource = preflight.stream;
|
|
3141
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;
|
|
3142
3289
|
const sseStream = bridgeToResponsesSSE(
|
|
3143
|
-
|
|
3290
|
+
guardedSource, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
|
|
3144
3291
|
() => {
|
|
3145
3292
|
runTurnAbort.abort();
|
|
3146
3293
|
queue.close();
|
|
@@ -3183,7 +3330,17 @@ async function handleResponsesInner(
|
|
|
3183
3330
|
}
|
|
3184
3331
|
|
|
3185
3332
|
await runTurn();
|
|
3186
|
-
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
|
+
}
|
|
3187
3344
|
if (options.comboAttempt) {
|
|
3188
3345
|
const firstMeaningful = events.find(event => event.type !== "heartbeat");
|
|
3189
3346
|
if (!firstMeaningful || firstMeaningful.type === "error") {
|
|
@@ -3278,6 +3435,7 @@ async function handleResponsesInner(
|
|
|
3278
3435
|
try {
|
|
3279
3436
|
if (activeAdapter.fetchResponse) {
|
|
3280
3437
|
noteAttemptSend(logCtx.activeAttempt, inputTokenEstimate);
|
|
3438
|
+
await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, upstream.signal);
|
|
3281
3439
|
upstreamResponse = await activeAdapter.fetchResponse(builtInitialRequest, {
|
|
3282
3440
|
abortSignal: upstream.signal,
|
|
3283
3441
|
timeoutMs: connectMs,
|
|
@@ -3292,7 +3450,10 @@ async function handleResponsesInner(
|
|
|
3292
3450
|
headers: builtInitialRequest.headers,
|
|
3293
3451
|
body: builtInitialRequest.body,
|
|
3294
3452
|
}, recovery), upstream.signal, connectMs, parsed.stream,
|
|
3295
|
-
providerFetch(route.provider, options.codexWsRuntimeIdentity
|
|
3453
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
3454
|
+
providerName: route.providerName,
|
|
3455
|
+
modelId: route.modelId,
|
|
3456
|
+
}));
|
|
3296
3457
|
},
|
|
3297
3458
|
{ abortSignal: upstream.signal, label: safeHostLabel(builtInitialRequest.url) },
|
|
3298
3459
|
);
|
|
@@ -3367,12 +3528,17 @@ async function handleResponsesInner(
|
|
|
3367
3528
|
noteAttemptSend(logCtx.activeAttempt, retryEstimate, recovery);
|
|
3368
3529
|
try {
|
|
3369
3530
|
try {
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
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
|
+
}));
|
|
3376
3542
|
} finally {
|
|
3377
3543
|
retryRequest.releaseBodyObservation?.();
|
|
3378
3544
|
}
|
|
@@ -3621,22 +3787,30 @@ async function handleResponsesInner(
|
|
|
3621
3787
|
|
|
3622
3788
|
cancelBodyOnAbort(upstreamResponse.body, upstream.signal);
|
|
3623
3789
|
|
|
3624
|
-
//
|
|
3625
|
-
//
|
|
3626
|
-
|
|
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;
|
|
3627
3797
|
/**
|
|
3628
3798
|
* One bounded internal re-ask for Anthropic end_turn-without-tool-call turns. Replays the
|
|
3629
3799
|
* continuation on a 429 with the same-key retry budget (hoisted per request), then falls
|
|
3630
3800
|
* back to key/account failover; a failure becomes an in-stream adapter error so the client
|
|
3631
3801
|
* never sees a second hidden HTTP response or an unbounded retry loop.
|
|
3632
3802
|
*/
|
|
3633
|
-
const fetchTerminalGuardContinuation = async function* (
|
|
3803
|
+
const fetchTerminalGuardContinuation = async function* (
|
|
3804
|
+
nextParsed: OcxParsedRequest,
|
|
3805
|
+
initialRecoveryKind?: AttemptRecoveryKind,
|
|
3806
|
+
): AsyncGenerator<AdapterEvent> {
|
|
3634
3807
|
let response: Response | undefined;
|
|
3635
3808
|
// One-shot recovery label for the next top-of-loop continuation send after a failover rotation.
|
|
3636
|
-
let nextContinuationRecoveryKind: AttemptRecoveryKind | undefined;
|
|
3809
|
+
let nextContinuationRecoveryKind: AttemptRecoveryKind | undefined = initialRecoveryKind;
|
|
3637
3810
|
/**
|
|
3638
3811
|
* Build and fetch one terminal-guard continuation. `recoveryKind` tags same-target and
|
|
3639
|
-
* failover sends (`rate-limit-429`, `key-429`,
|
|
3812
|
+
* failover sends (`empty-completion`, `rate-limit-429`, `key-429`,
|
|
3813
|
+
* `anthropic-oauth-429`, `image-413`); the
|
|
3640
3814
|
* adapter rebuild is deterministic for the same parsed request (tests assert byte-identical
|
|
3641
3815
|
* replays).
|
|
3642
3816
|
*/
|
|
@@ -3677,6 +3851,7 @@ async function handleResponsesInner(
|
|
|
3677
3851
|
try {
|
|
3678
3852
|
if (activeAdapter.fetchResponse) {
|
|
3679
3853
|
noteAttemptSend(logCtx.activeAttempt, continuationEstimate, replayKind);
|
|
3854
|
+
await waitForProviderRequestSlot(route.providerName, route.provider, nextParsed.modelId, upstream.signal);
|
|
3680
3855
|
return await activeAdapter.fetchResponse(builtContinuationRequest, {
|
|
3681
3856
|
abortSignal: upstream.signal,
|
|
3682
3857
|
timeoutMs: connectMs,
|
|
@@ -3696,7 +3871,10 @@ async function handleResponsesInner(
|
|
|
3696
3871
|
upstream.signal,
|
|
3697
3872
|
connectMs,
|
|
3698
3873
|
nextParsed.stream,
|
|
3699
|
-
providerFetch(route.provider, options.codexWsRuntimeIdentity
|
|
3874
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
3875
|
+
providerName: route.providerName,
|
|
3876
|
+
modelId: nextParsed.modelId,
|
|
3877
|
+
}),
|
|
3700
3878
|
);
|
|
3701
3879
|
},
|
|
3702
3880
|
{ abortSignal: upstream.signal, label: safeHostLabel(builtContinuationRequest.url) },
|
|
@@ -3880,6 +4058,19 @@ async function handleResponsesInner(
|
|
|
3880
4058
|
}
|
|
3881
4059
|
};
|
|
3882
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
|
+
|
|
3883
4074
|
if (parsed.stream) {
|
|
3884
4075
|
const initialEventStream = activeAdapter.parseStream(upstreamResponse, translatorBudget);
|
|
3885
4076
|
const eventStream = terminalGuardEnabled
|
|
@@ -3891,9 +4082,19 @@ async function handleResponsesInner(
|
|
|
3891
4082
|
continuation: fetchTerminalGuardContinuation,
|
|
3892
4083
|
})
|
|
3893
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;
|
|
3894
4095
|
const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
|
|
3895
4096
|
const sseStream = bridgeToResponsesSSE(
|
|
3896
|
-
|
|
4097
|
+
guardedEventStream, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
|
|
3897
4098
|
() => upstream.abort(), 2_000,
|
|
3898
4099
|
{
|
|
3899
4100
|
translatorBudget,
|
|
@@ -3938,17 +4139,27 @@ async function handleResponsesInner(
|
|
|
3938
4139
|
let events: AdapterEvent[];
|
|
3939
4140
|
try {
|
|
3940
4141
|
const initialEvents = await activeAdapter.parseResponse(upstreamResponse, translatorBudget);
|
|
4142
|
+
let guardedEvents: AdapterEvent[];
|
|
3941
4143
|
if (terminalGuardEnabled) {
|
|
3942
|
-
|
|
4144
|
+
guardedEvents = [];
|
|
3943
4145
|
for await (const event of guardTerminalEventStream({
|
|
3944
4146
|
parsed,
|
|
3945
4147
|
firstEvents: (async function* () { yield* initialEvents; })(),
|
|
3946
4148
|
adapterName: activeAdapter.name,
|
|
3947
4149
|
maxAutoContinuations: 1,
|
|
3948
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,
|
|
3949
4160
|
})) events.push(event);
|
|
3950
4161
|
} else {
|
|
3951
|
-
events =
|
|
4162
|
+
events = guardedEvents;
|
|
3952
4163
|
}
|
|
3953
4164
|
} finally {
|
|
3954
4165
|
cleanupUpstreamAbort();
|