@bitkyc08/opencodex 2.56.0 → 2.58.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/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-D4zuyIxQ.js → index-BbrHOIY0.js} +21 -21
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +249 -0
- package/src/adapters/command-code.ts +12 -3
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +19 -1
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +55 -5
- package/src/adapters/google-http.ts +38 -13
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +25 -7
- package/src/adapters/openai-chat.ts +8 -8
- package/src/adapters/openai-responses/passthrough.ts +62 -5
- package/src/adapters/openai-responses/request-strips.ts +43 -0
- package/src/adapters/physical-send.ts +50 -0
- package/src/bridge/errors.ts +26 -2
- package/src/bridge/response-json.ts +8 -2
- package/src/bridge/sse.ts +20 -2
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +32 -4
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/config-command.ts +35 -18
- package/src/cli/dispatch.ts +17 -4
- package/src/cli/index.ts +92 -7
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +74 -5
- package/src/cli/uninstall-client-state.ts +12 -0
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-store.ts +113 -26
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/login-flow.ts +14 -2
- package/src/codex/auth-api/reset-credit-service.ts +11 -2
- package/src/codex/auth-context.ts +199 -15
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/model-visibility.ts +1 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/retained-sync.ts +9 -1
- package/src/codex/catalog/routed-gather.ts +38 -1
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +33 -4
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +231 -32
- package/src/codex/inject.ts +12 -16
- package/src/codex/loopback-target.ts +9 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/native-profile-startup.ts +64 -20
- package/src/codex/pool-refresh-backoff.ts +12 -3
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +10 -0
- package/src/codex/routing/selection.ts +79 -2
- package/src/codex/routing/thread-affinity.ts +50 -2
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +29 -49
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/atomic-write.ts +83 -8
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/schema/config-schema.ts +2 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +272 -180
- package/src/images/loop.ts +1 -1
- package/src/lib/bounded-subprocess.ts +62 -10
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +147 -21
- package/src/lib/spend-reservation-ledger.ts +18 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +77 -10
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/derive.ts +6 -0
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +35 -7
- package/src/providers/registry/entries-core.ts +18 -0
- package/src/providers/registry/entries-extended.ts +59 -28
- package/src/providers/registry/model-seeds.ts +71 -17
- package/src/providers/registry/types.ts +9 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- 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/routing/identity-domains.ts +21 -14
- package/src/routing/probe-lease.ts +103 -1
- package/src/server/auth-cors.ts +1 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/live-sideband.ts +37 -1
- package/src/server/index/websocket-handler.ts +54 -3
- package/src/server/index.ts +5 -5
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/config-routes.ts +27 -5
- package/src/server/models-capabilities.ts +24 -3
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log.ts +127 -3
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +74 -0
- package/src/server/responses/adapter-continuation.ts +33 -7
- package/src/server/responses/adapter-delivery.ts +5 -11
- package/src/server/responses/adapter-dispatch.ts +84 -13
- package/src/server/responses/codex-ws-exchange.ts +65 -4
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/combo-stream-preflight.ts +68 -5
- package/src/server/responses/compact.ts +54 -13
- package/src/server/responses/core-auth.ts +2 -0
- package/src/server/responses/core-codex-account.ts +51 -3
- package/src/server/responses/core-combo.ts +129 -23
- package/src/server/responses/core-errors.ts +18 -0
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/core-replay.ts +105 -32
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/fetch-helpers.ts +4 -1
- package/src/server/responses/input-admission.ts +126 -6
- 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 +30 -6
- package/src/server/responses/passthrough-dispatch.ts +61 -11
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/request-prepare.ts +173 -22
- package/src/server/responses/request-send-budget.ts +97 -2
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +62 -3
- package/src/server/responses/run-turn-execution.ts +59 -31
- package/src/server/responses/sidecar-execution.ts +7 -13
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses/ws-upstream.ts +21 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/stop-teardown.ts +8 -1
- package/src/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +13 -1
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +8 -1
- package/src/types/provider.ts +13 -0
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +1 -1
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { isDeclaredReasoningEffort } from "../../reasoning-effort";
|
|
2
|
+
import { recordAttemptRequestedEffort } from "../request-log";
|
|
1
3
|
import {
|
|
2
4
|
CODEX_TEXT_GUARDED_BUDGET_POLICY,
|
|
3
|
-
|
|
5
|
+
deriveRequestExecutionBudget,
|
|
4
6
|
isRequestExecutionBudget,
|
|
5
7
|
} from "../../lib/request-execution-budget";
|
|
6
8
|
import type {
|
|
@@ -44,6 +46,7 @@ import { isThreadSpawnRequest, supportedLadderFor } from "../effort-policy";
|
|
|
44
46
|
import {
|
|
45
47
|
clientCancelledResponse,
|
|
46
48
|
comboUnavailable,
|
|
49
|
+
targetIncompatibleResponse,
|
|
47
50
|
unreadableEncryptedAgentTaskResponse,
|
|
48
51
|
} from "./core-errors";
|
|
49
52
|
import {
|
|
@@ -51,7 +54,12 @@ import {
|
|
|
51
54
|
createChildPassthroughCallbackGate,
|
|
52
55
|
consumeComboFailure,
|
|
53
56
|
} from "./core-combo-failure";
|
|
54
|
-
import {
|
|
57
|
+
import {
|
|
58
|
+
linkRequestSessionLane,
|
|
59
|
+
reasoningReplayConversationIdFromResponsesRequest,
|
|
60
|
+
sessionIdHeaderFromRequest,
|
|
61
|
+
sessionLaneIdFromRequest,
|
|
62
|
+
} from "../request-log-conversation";
|
|
55
63
|
import type { CodexAuthContext } from "../../codex/auth-context";
|
|
56
64
|
import type { ResponsesTerminalStatus } from "../../bridge";
|
|
57
65
|
import { beginRequestAttempt, sealRequestAttemptIdentity, finishRequestAttempt } from "../request-log";
|
|
@@ -65,6 +73,7 @@ import {
|
|
|
65
73
|
} from "../relay";
|
|
66
74
|
import { preflightComboStreamResponse } from "./combo-stream-preflight";
|
|
67
75
|
import { streamingContextOverflowResponse, jsonContextOverflowResponse } from "./context-overflow";
|
|
76
|
+
import { mandatoryResponsesReasoningReplayUnavailable } from "./core-replay";
|
|
68
77
|
|
|
69
78
|
/**
|
|
70
79
|
* Sends one combo target may run on its own before the ladder moves on. A target is a whole
|
|
@@ -105,25 +114,27 @@ export function comboExecutionBudgetPolicy(declaredTargets: number): RequestExec
|
|
|
105
114
|
/**
|
|
106
115
|
* A budget scope that keeps its own recovery ledgers but spends the SAME request-wide counter.
|
|
107
116
|
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
117
|
+
* The sharing has to happen inside the factory. Redefining `used` as an accessor onto the parent
|
|
118
|
+
* only shared what callers read from the outside: `remainingBaseSends`, the total check and the
|
|
119
|
+
* reserve test all consult the factory's own private counter, which an overridden property
|
|
120
|
+
* cannot reach. Each derived scope therefore admitted dispatches as though the request had spent
|
|
121
|
+
* nothing, and the per-target holdback below -- expressed against `maxTotalModelSends` -- had
|
|
122
|
+
* nothing to hold back from.
|
|
123
|
+
*
|
|
124
|
+
* `deriveRequestExecutionBudget` binds the scope to the parent's real ledger, including pending
|
|
125
|
+
* externally-counted bookings and the durable-spend observer, all of which must travel together.
|
|
126
|
+
* A pending booking is a send already counted in the total and waiting for its reporter, and the
|
|
127
|
+
* observer books by watching that same counter move (#4707) -- so a scope that spent the counter
|
|
128
|
+
* without carrying the observer would move it without booking, and this combo's child sends
|
|
129
|
+
* would go missing from the spend ledger. The reserve, alternate-target and transition ledgers
|
|
130
|
+
* stay per-scope on purpose: a combo target's account failover is its own recovery decision,
|
|
131
|
+
* while the request total still bounds every target together.
|
|
114
132
|
*/
|
|
115
133
|
export function deriveSendBudgetScope(
|
|
116
134
|
parent: RequestExecutionBudget,
|
|
117
135
|
policy: RequestExecutionBudgetPolicy,
|
|
118
136
|
): RequestExecutionBudget {
|
|
119
|
-
|
|
120
|
-
Object.defineProperty(scope, "used", {
|
|
121
|
-
get: () => parent.used,
|
|
122
|
-
set: (value: number) => { parent.used = value; },
|
|
123
|
-
enumerable: true,
|
|
124
|
-
configurable: true,
|
|
125
|
-
});
|
|
126
|
-
return scope;
|
|
137
|
+
return deriveRequestExecutionBudget(parent, policy);
|
|
127
138
|
}
|
|
128
139
|
|
|
129
140
|
|
|
@@ -229,6 +240,29 @@ export async function executeComboResponses(
|
|
|
229
240
|
: undefined,
|
|
230
241
|
recoveredPlaintext: false,
|
|
231
242
|
};
|
|
243
|
+
const reasoningReplayConversationId = reasoningReplayConversationIdFromResponsesRequest({
|
|
244
|
+
clientThreadId: inboundClientThreadId,
|
|
245
|
+
threadIdHeader: req.headers.get("thread-id"),
|
|
246
|
+
sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
|
|
247
|
+
});
|
|
248
|
+
const reasoningReplayEligible = (target: (typeof combo.targets)[number]): boolean => {
|
|
249
|
+
try {
|
|
250
|
+
const route = routeConcreteModel(config, `${target.provider}/${target.model}`);
|
|
251
|
+
const unavailable = mandatoryResponsesReasoningReplayUnavailable({
|
|
252
|
+
body,
|
|
253
|
+
clientThreadId: reasoningReplayConversationId,
|
|
254
|
+
providerName: route.providerName,
|
|
255
|
+
provider: route.provider,
|
|
256
|
+
adapterName: route.provider.adapter,
|
|
257
|
+
modelId: route.modelId,
|
|
258
|
+
});
|
|
259
|
+
return !unavailable;
|
|
260
|
+
} catch {
|
|
261
|
+
// Routing failures are not evidence of replay incompatibility. Keep the target eligible so
|
|
262
|
+
// the existing selection and dispatch path preserves its original routing failure surface.
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
232
266
|
const adoptFailedChildLog = (childLog: RequestLogContext): void => {
|
|
233
267
|
// Attempts remain the complete physical history; the logical row mirrors the most recent
|
|
234
268
|
// failed target so an exhausted combo still has useful top-level reasoning diagnostics.
|
|
@@ -260,6 +294,18 @@ export async function executeComboResponses(
|
|
|
260
294
|
let comboPayloadReadable = false;
|
|
261
295
|
const payloadEligible = (target: (typeof combo.targets)[number]): boolean =>
|
|
262
296
|
comboPayloadReadable || !unreadableEncryptedAgentTask || canDecryptUnreadableAgentTask(target);
|
|
297
|
+
const targetEligible = (target: (typeof combo.targets)[number]): boolean =>
|
|
298
|
+
payloadEligible(target) && reasoningReplayEligible(target);
|
|
299
|
+
const onlyReplayIncompatibleTargetsRemain = (excluded: Iterable<string> = []): boolean => {
|
|
300
|
+
const excludedKeys = new Set(excluded);
|
|
301
|
+
const remaining = combo.targets.filter(target => {
|
|
302
|
+
const provider = config.providers[target.provider];
|
|
303
|
+
return provider?.disabled !== true
|
|
304
|
+
&& !excludedKeys.has(targetKey(target))
|
|
305
|
+
&& payloadEligible(target);
|
|
306
|
+
});
|
|
307
|
+
return remaining.length > 0 && remaining.every(target => !reasoningReplayEligible(target));
|
|
308
|
+
};
|
|
263
309
|
let encryptedTaskRecoveryAttempted = false;
|
|
264
310
|
let recoveryFailureReason: AgentTaskRecoveryFailureReason | undefined;
|
|
265
311
|
let storedPool401ReplayDispatched = false;
|
|
@@ -324,7 +370,7 @@ export async function executeComboResponses(
|
|
|
324
370
|
abortSignal: options.abortSignal,
|
|
325
371
|
});
|
|
326
372
|
let pick = await pickWithWait({
|
|
327
|
-
eligible:
|
|
373
|
+
eligible: targetEligible,
|
|
328
374
|
now: initialNow,
|
|
329
375
|
});
|
|
330
376
|
|
|
@@ -349,6 +395,7 @@ export async function executeComboResponses(
|
|
|
349
395
|
}
|
|
350
396
|
|
|
351
397
|
if (!pick) {
|
|
398
|
+
if (onlyReplayIncompatibleTargetsRemain()) return targetIncompatibleResponse();
|
|
352
399
|
return options.abortSignal?.aborted
|
|
353
400
|
? clientCancelledResponse()
|
|
354
401
|
: comboUnavailable(comboId);
|
|
@@ -357,6 +404,26 @@ export async function executeComboResponses(
|
|
|
357
404
|
// adoption below must never replace it with a concrete child route trace.
|
|
358
405
|
logCtx.routeDecision = comboRouteDecisionTrace(config, comboId, pick, requestedModel);
|
|
359
406
|
|
|
407
|
+
const originalReasoning = body && typeof body === "object" && !Array.isArray(body)
|
|
408
|
+
? (body as { reasoning?: unknown }).reasoning
|
|
409
|
+
: undefined;
|
|
410
|
+
const originalRequestedEffortValue = originalReasoning && typeof originalReasoning === "object" && !Array.isArray(originalReasoning)
|
|
411
|
+
? (originalReasoning as { effort?: unknown }).effort
|
|
412
|
+
: undefined;
|
|
413
|
+
const originalRequestedEffort = typeof originalRequestedEffortValue === "string"
|
|
414
|
+
&& isDeclaredReasoningEffort(originalRequestedEffortValue)
|
|
415
|
+
? originalRequestedEffortValue
|
|
416
|
+
: undefined;
|
|
417
|
+
const restoreOriginalRequestedEffort = (childLog: RequestLogContext): void => {
|
|
418
|
+
if (originalRequestedEffort === undefined) return;
|
|
419
|
+
const normalizedRequestedEffort = childLog.requestedEffort;
|
|
420
|
+
const transitionIndex = normalizedRequestedEffort?.indexOf("->") ?? -1;
|
|
421
|
+
childLog.requestedEffort = transitionIndex >= 0
|
|
422
|
+
? `${originalRequestedEffort}${normalizedRequestedEffort!.slice(transitionIndex)}`
|
|
423
|
+
: originalRequestedEffort;
|
|
424
|
+
recordAttemptRequestedEffort(childLog);
|
|
425
|
+
};
|
|
426
|
+
|
|
360
427
|
let lastFailure: Response | null = null;
|
|
361
428
|
// Dispatched targets, not attempted picks: it indexes the declared target list so the clamp
|
|
362
429
|
// below can tell how many targets are still entitled to a send.
|
|
@@ -407,6 +474,7 @@ export async function executeComboResponses(
|
|
|
407
474
|
comboDefaultEffort(config, comboId),
|
|
408
475
|
supportedLadderFor({ provider: targetRoute.provider, modelId: targetRoute.modelId }),
|
|
409
476
|
combo.reasoningEffortMode,
|
|
477
|
+
combo.defaultEffortMode,
|
|
410
478
|
);
|
|
411
479
|
const childHeaders = buildComboChildHeaders(req.headers);
|
|
412
480
|
const childRequest = new Request(req.url, {
|
|
@@ -425,6 +493,13 @@ export async function executeComboResponses(
|
|
|
425
493
|
config.providers[pick.target.provider]!.adapter,
|
|
426
494
|
);
|
|
427
495
|
childLog.activeAttempt = attempt;
|
|
496
|
+
if (originalRequestedEffort !== undefined) {
|
|
497
|
+
childLog.requestedEffort = originalRequestedEffort;
|
|
498
|
+
recordAttemptRequestedEffort(childLog);
|
|
499
|
+
}
|
|
500
|
+
childLog.activeAttemptStartedAt = started;
|
|
501
|
+
childLog.attempts = logCtx.attempts ??= [];
|
|
502
|
+
childLog.attempts.push(attempt);
|
|
428
503
|
let attemptRetained = false;
|
|
429
504
|
const retainCancelledAttempt = (): void => {
|
|
430
505
|
if (attemptRetained) return;
|
|
@@ -435,7 +510,6 @@ export async function executeComboResponses(
|
|
|
435
510
|
childLog.accountLogLabel,
|
|
436
511
|
);
|
|
437
512
|
finishRequestAttempt(attempt, 499, Date.now() - started, childLog.usage);
|
|
438
|
-
(logCtx.attempts ??= []).push(attempt);
|
|
439
513
|
attemptRetained = true;
|
|
440
514
|
};
|
|
441
515
|
const completedTarget = { provider: pick.target.provider, model: pick.target.model };
|
|
@@ -472,7 +546,7 @@ export async function executeComboResponses(
|
|
|
472
546
|
const deferCodexResetDerivedCooldown = combo.strategy === "failover"
|
|
473
547
|
&& combo.targets.slice(pick.targetIndex + 1).some(target =>
|
|
474
548
|
target.provider === currentTargetProvider
|
|
475
|
-
&&
|
|
549
|
+
&& targetEligible(target)
|
|
476
550
|
&& !isComboTargetInCooldown(comboId, target),
|
|
477
551
|
);
|
|
478
552
|
response = await requestDispatchers.handleResponses(childRequest, config, childLog, {
|
|
@@ -499,12 +573,14 @@ export async function executeComboResponses(
|
|
|
499
573
|
onNativePassthroughCancel: callbackGate.onCancel,
|
|
500
574
|
onResponseComplete: callbackGate.onResponseComplete,
|
|
501
575
|
});
|
|
576
|
+
restoreOriginalRequestedEffort(childLog);
|
|
502
577
|
} catch (error) {
|
|
503
578
|
callbackGate.discard();
|
|
504
579
|
if (options.abortSignal?.aborted) {
|
|
505
580
|
retainCancelledAttempt();
|
|
506
581
|
return clientCancelledResponse();
|
|
507
582
|
}
|
|
583
|
+
finishRequestAttempt(attempt, 502, Date.now() - started, childLog.usage);
|
|
508
584
|
throw error;
|
|
509
585
|
}
|
|
510
586
|
|
|
@@ -526,6 +602,7 @@ export async function executeComboResponses(
|
|
|
526
602
|
retainCancelledAttempt();
|
|
527
603
|
return clientCancelledResponse();
|
|
528
604
|
}
|
|
605
|
+
finishRequestAttempt(attempt, 502, Date.now() - started, childLog.usage);
|
|
529
606
|
throw error;
|
|
530
607
|
}
|
|
531
608
|
if (preflight.kind === "failed") {
|
|
@@ -546,7 +623,6 @@ export async function executeComboResponses(
|
|
|
546
623
|
childLog.providerAdapter ?? attempt.adapter,
|
|
547
624
|
childLog.accountLogLabel,
|
|
548
625
|
);
|
|
549
|
-
(logCtx.attempts ??= []).push(attempt);
|
|
550
626
|
attemptRetained = true;
|
|
551
627
|
noteComboSuccess(comboId, combo, pick.target, pick.writerGeneration);
|
|
552
628
|
Object.assign(logCtx, childLog, {
|
|
@@ -580,6 +656,7 @@ export async function executeComboResponses(
|
|
|
580
656
|
retainCancelledAttempt();
|
|
581
657
|
return clientCancelledResponse();
|
|
582
658
|
}
|
|
659
|
+
finishRequestAttempt(attempt, 502, Date.now() - started, childLog.usage);
|
|
583
660
|
throw error;
|
|
584
661
|
}
|
|
585
662
|
if (options.abortSignal?.aborted) {
|
|
@@ -598,7 +675,6 @@ export async function executeComboResponses(
|
|
|
598
675
|
Date.now() - started,
|
|
599
676
|
failure.usage,
|
|
600
677
|
);
|
|
601
|
-
(logCtx.attempts ??= []).push(attempt);
|
|
602
678
|
attemptRetained = true;
|
|
603
679
|
lastFailure = failure.response;
|
|
604
680
|
lastFailedChildLog = childLog;
|
|
@@ -664,22 +740,52 @@ export async function executeComboResponses(
|
|
|
664
740
|
cooldownScope: comboFailureCooldownScope(failure.response.status, failure.classificationText, {
|
|
665
741
|
code: failure.upstreamCode,
|
|
666
742
|
}),
|
|
667
|
-
eligible:
|
|
743
|
+
eligible: targetEligible,
|
|
668
744
|
status: failure.response.status,
|
|
669
745
|
code: failure.upstreamCode,
|
|
670
746
|
message: failure.classificationText,
|
|
671
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
|
+
});
|
|
672
755
|
if (nextPick) {
|
|
673
756
|
pick = nextPick;
|
|
674
757
|
} else {
|
|
675
758
|
pick = await pickWithWait({
|
|
676
759
|
exclude: pick.attempted,
|
|
677
|
-
eligible:
|
|
760
|
+
eligible: targetEligible,
|
|
678
761
|
now: failureNow,
|
|
679
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
|
+
}
|
|
680
782
|
}
|
|
681
783
|
if (!pick) {
|
|
682
784
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
785
|
+
if (onlyReplayIncompatibleTargetsRemain(attemptedTargets)) {
|
|
786
|
+
adoptFailedChildLog(childLog);
|
|
787
|
+
return targetIncompatibleResponse();
|
|
788
|
+
}
|
|
683
789
|
if (unreadableEncryptedAgentTask && !comboPayloadReadable) {
|
|
684
790
|
const recoveredTarget = await pickWithWait({
|
|
685
791
|
exclude: attemptedTargets,
|
|
@@ -150,3 +150,21 @@ export function unreadableEncryptedAgentTaskResponse(reason?: AgentTaskRecoveryF
|
|
|
150
150
|
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
151
151
|
);
|
|
152
152
|
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
export const TARGET_INCOMPATIBLE_MESSAGE =
|
|
156
|
+
"No remaining combo target can continue this tool-bearing history because the reasoning required for replay is unavailable after the serving route changed. Start a new conversation or configure a combo target that can consume the available reasoning.";
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
export function targetIncompatibleResponse(): Response {
|
|
160
|
+
return new Response(
|
|
161
|
+
JSON.stringify({
|
|
162
|
+
error: {
|
|
163
|
+
message: TARGET_INCOMPATIBLE_MESSAGE,
|
|
164
|
+
type: "invalid_request_error",
|
|
165
|
+
code: "target_incompatible",
|
|
166
|
+
},
|
|
167
|
+
}),
|
|
168
|
+
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
169
|
+
);
|
|
170
|
+
}
|
|
@@ -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;
|
|
@@ -27,6 +27,7 @@ import type { OAuthAccessSnapshot } from "../../oauth";
|
|
|
27
27
|
import type { CodexAuthContext } from "../../codex/auth-context";
|
|
28
28
|
import { thoughtSignatureReplaySalt } from "../../responses/thought-signature-replay";
|
|
29
29
|
import { randomUUID } from "node:crypto";
|
|
30
|
+
import { requiresPlaintextReasoningReplay } from "../../adapters/openai-responses/passthrough";
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* Adapters whose continuation state must survive Codex's store:false requests.
|
|
@@ -107,6 +108,53 @@ export function bindRouteReasoningReplayScope(args: {
|
|
|
107
108
|
forwardHeaders?: Headers;
|
|
108
109
|
}): void {
|
|
109
110
|
const { parsed, providerName, provider, adapterName } = args;
|
|
111
|
+
const replayIdentity = routeReasoningReplayIdentity({
|
|
112
|
+
...args,
|
|
113
|
+
modelId: parsed.modelId,
|
|
114
|
+
});
|
|
115
|
+
const continuationDestinationIdentity = providerContinuationDestinationIdentity(parsed, provider);
|
|
116
|
+
const continuationOwner = providerContinuationOwnerFromReplayIdentity(
|
|
117
|
+
replayIdentity && continuationDestinationIdentity
|
|
118
|
+
? { ...replayIdentity, providerDestinationIdentity: continuationDestinationIdentity }
|
|
119
|
+
: undefined,
|
|
120
|
+
);
|
|
121
|
+
if (adapterName === "cursor") {
|
|
122
|
+
// The final route owner is authoritative for Cursor and supersedes the account-derived
|
|
123
|
+
// seed assigned before route binding. A Cursor conversation must be scoped to the exact
|
|
124
|
+
// provider/destination/adapter/model/credential that serves it.
|
|
125
|
+
if (continuationOwner) parsed._cursorIdentityScope = providerContinuationRouteScope(continuationOwner);
|
|
126
|
+
else if (!parsed._cursorIdentityScope?.startsWith("cursor-unowned:")) {
|
|
127
|
+
// Prevent the adapter's token-only fallback from recreating a provider-private id after the
|
|
128
|
+
// route owner failed closed. The sentinel is per parsed request and contains no credential.
|
|
129
|
+
parsed._cursorIdentityScope = `cursor-unowned:${randomUUID()}`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
bindReasoningReplayScope(
|
|
133
|
+
parsed._reasoningReplayScope,
|
|
134
|
+
replayIdentity,
|
|
135
|
+
);
|
|
136
|
+
// Keep this sticky for the whole outbound request: a later auth/key rebind may compare equal
|
|
137
|
+
// after the first mismatch, but it cannot make history minted by the prior route decodable.
|
|
138
|
+
if (reasoningReplayServingIdentityChanged(parsed._reasoningReplayScope)) {
|
|
139
|
+
parsed._stripReasoningEncryptedContent = true;
|
|
140
|
+
}
|
|
141
|
+
if (reasoningReplayOpaqueBlobRejectionMemoized(parsed._reasoningReplayScope)) {
|
|
142
|
+
parsed._stripReasoningEncryptedContent = true;
|
|
143
|
+
}
|
|
144
|
+
bindProviderContinuationForRoute(parsed, continuationOwner);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
function routeReasoningReplayIdentity(args: {
|
|
149
|
+
providerName: string;
|
|
150
|
+
provider: OcxProviderConfig;
|
|
151
|
+
adapterName: string;
|
|
152
|
+
modelId: string;
|
|
153
|
+
oauthCredentialSnapshot?: Pick<OAuthAccessSnapshot, "accountId" | "generation">;
|
|
154
|
+
codexAuthContext?: CodexAuthContext;
|
|
155
|
+
forwardHeaders?: Headers;
|
|
156
|
+
}): OcxReasoningReplayIdentity | undefined {
|
|
157
|
+
const { providerName, provider, adapterName, modelId } = args;
|
|
110
158
|
let credentialIdentity: string | undefined;
|
|
111
159
|
let credentialDurableIdentity: string | undefined;
|
|
112
160
|
const durableSalt = thoughtSignatureReplaySalt();
|
|
@@ -165,47 +213,72 @@ export function bindRouteReasoningReplayScope(args: {
|
|
|
165
213
|
);
|
|
166
214
|
}
|
|
167
215
|
const providerDestinationIdentity = reasoningReplayDestinationIdentity(provider.baseUrl);
|
|
168
|
-
|
|
216
|
+
return credentialIdentity && providerDestinationIdentity
|
|
169
217
|
? {
|
|
170
218
|
providerName,
|
|
171
219
|
providerDestinationIdentity,
|
|
172
220
|
providerDestinationDurableIdentity: durableReplayDestinationIdentity(provider.baseUrl),
|
|
173
221
|
adapterName,
|
|
174
|
-
modelId
|
|
222
|
+
modelId,
|
|
175
223
|
credentialIdentity,
|
|
176
224
|
...(credentialDurableIdentity ? { credentialDurableIdentity } : {}),
|
|
177
225
|
}
|
|
178
226
|
: undefined;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Whether this exact route cannot satisfy its documented plaintext reasoning replay contract.
|
|
232
|
+
*
|
|
233
|
+
* A generic plaintext-preserving gateway is not made ineligible. Unknown serving provenance also
|
|
234
|
+
* stays eligible; only a proven route mismatch may suppress a combo candidate.
|
|
235
|
+
*/
|
|
236
|
+
export function mandatoryResponsesReasoningReplayUnavailable(args: {
|
|
237
|
+
body: unknown;
|
|
238
|
+
clientThreadId: string | undefined;
|
|
239
|
+
providerName: string;
|
|
240
|
+
provider: OcxProviderConfig;
|
|
241
|
+
adapterName: string;
|
|
242
|
+
modelId: string;
|
|
243
|
+
}): boolean {
|
|
244
|
+
const { body, clientThreadId, provider, adapterName } = args;
|
|
245
|
+
if (
|
|
246
|
+
adapterName !== "openai-responses"
|
|
247
|
+
|| !requiresPlaintextReasoningReplay(provider)
|
|
248
|
+
|| !clientThreadId
|
|
249
|
+
|| !requestCarriesTools(body)
|
|
250
|
+
|| !hasOpaqueOnlyReasoningItem(body)
|
|
251
|
+
) return false;
|
|
252
|
+
|
|
253
|
+
const current = routeReasoningReplayIdentity(args);
|
|
254
|
+
return reasoningReplayServingIdentityChanged({ clientThreadId, current });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
function requestCarriesTools(body: unknown): boolean {
|
|
259
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return false;
|
|
260
|
+
return Array.isArray((body as { tools?: unknown }).tools)
|
|
261
|
+
&& (body as { tools: unknown[] }).tools.length > 0;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
function hasOpaqueOnlyReasoningItem(body: unknown): boolean {
|
|
266
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return false;
|
|
267
|
+
const input = (body as { input?: unknown }).input;
|
|
268
|
+
if (!Array.isArray(input)) return false;
|
|
269
|
+
return input.some(item => {
|
|
270
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return false;
|
|
271
|
+
const reasoning = item as Record<string, unknown>;
|
|
272
|
+
if (reasoning.type !== "reasoning" || typeof reasoning.encrypted_content !== "string") return false;
|
|
273
|
+
const content = reasoning.content;
|
|
274
|
+
const hasPlaintext = Array.isArray(content) && content.some(part =>
|
|
275
|
+
!!part && typeof part === "object" && !Array.isArray(part)
|
|
276
|
+
&& (part as Record<string, unknown>).type === "reasoning_text"
|
|
277
|
+
&& typeof (part as Record<string, unknown>).text === "string"
|
|
278
|
+
&& ((part as Record<string, unknown>).text as string).length > 0
|
|
279
|
+
);
|
|
280
|
+
return !hasPlaintext;
|
|
281
|
+
});
|
|
209
282
|
}
|
|
210
283
|
|
|
211
284
|
|
|
@@ -10,6 +10,7 @@ import { createTranslatorBudget } from "../../lib/translator-budget";
|
|
|
10
10
|
import { captureExplicitOpenAiCallerAuth } from "../../providers/openai-sidecar";
|
|
11
11
|
import { captureCallerDirectAuth } from "../../providers/caller-authorization";
|
|
12
12
|
import { createRequestExecutionBudget } from "../../lib/request-execution-budget";
|
|
13
|
+
import { attachRequestSpendTracker } from "./request-spend";
|
|
13
14
|
import { finalizeOwnedTranslatorBudget } from "./core-lifetime";
|
|
14
15
|
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
15
16
|
import { executeComboResponses } from "./core-combo";
|
|
@@ -56,9 +57,8 @@ export async function handleResponses(
|
|
|
56
57
|
visionDescribeTerminal: options.visionDescribeTerminal === true
|
|
57
58
|
|| req.headers.get("x-opencodex-vision-describe") === "1",
|
|
58
59
|
translatorBudget,
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
sendBudget: options.sendBudget ?? createRequestExecutionBudget(),
|
|
60
|
+
// Once at ingress, spend observer included: a combo child inherits the parent's holder.
|
|
61
|
+
sendBudget: options.sendBudget ?? createRequestExecutionBudget(undefined, undefined, attachRequestSpendTracker(req, logCtx)),
|
|
62
62
|
});
|
|
63
63
|
return ownsBudget ? finalizeOwnedTranslatorBudget(response, translatorBudget) : response;
|
|
64
64
|
} catch (error) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { NativeResponseControl } from "./native-response-control";
|
|
1
2
|
import type { Server } from "bun";
|
|
2
3
|
import {
|
|
3
4
|
codexWsUpstreamFetch,
|
|
@@ -53,6 +54,7 @@ export interface PaceAwareFetch {
|
|
|
53
54
|
export type ProviderFetch = typeof globalThis.fetch & PaceAwareFetch;
|
|
54
55
|
|
|
55
56
|
export interface ProviderFetchOptions {
|
|
57
|
+
nativeControl?: NativeResponseControl;
|
|
56
58
|
providerName?: string;
|
|
57
59
|
modelId?: string;
|
|
58
60
|
/** One pacing slot was acquired immediately before this fetch wrapper was created. */
|
|
@@ -101,7 +103,8 @@ export function providerFetch(
|
|
|
101
103
|
// used, protocol pin included: a WS turn that falls back is serving the
|
|
102
104
|
// request over HTTP, and dropping the provider's `upstreamHttpVersion`
|
|
103
105
|
// there would silently negotiate a transport the operator ruled out.
|
|
104
|
-
return codexWsUpstreamFetch(input, init, httpFetch, runtime, options.onCodexWsQuota, options.beforeDispatch
|
|
106
|
+
return codexWsUpstreamFetch(input, init, httpFetch, runtime, options.onCodexWsQuota, options.beforeDispatch, options.nativeControl,
|
|
107
|
+
() => waitForPacing(init.signal ?? undefined));
|
|
105
108
|
}
|
|
106
109
|
return httpFetch(input, init);
|
|
107
110
|
};
|