@bitkyc08/opencodex 2.56.0 → 2.57.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-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-D4zuyIxQ.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +3 -3
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- 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/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 +32 -1
- package/src/bridge/errors.ts +26 -2
- package/src/bridge/response-json.ts +7 -1
- package/src/bridge/sse.ts +19 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- 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/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- 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 +157 -7
- 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/inject/restore.ts +29 -2
- package/src/codex/inject.ts +9 -9
- package/src/codex/model-entitlements.ts +152 -15
- 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/pending-teardown.ts +31 -0
- package/src/generated/compatibility-version.json +163 -135
- package/src/images/loop.ts +1 -1
- 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/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- package/src/providers/registry/entries-core.ts +7 -0
- package/src/providers/registry/entries-extended.ts +9 -0
- package/src/providers/registry/model-seeds.ts +4 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/routing/identity-domains.ts +21 -14
- package/src/routing/probe-lease.ts +103 -1
- 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 +6 -2
- 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/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-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/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 +103 -23
- package/src/server/responses/core-errors.ts +18 -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/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +19 -6
- package/src/server/responses/passthrough-dispatch.ts +28 -10
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/request-prepare.ts +132 -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-undeclared-tool-guard.ts +9 -5
- 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 +4 -1
- 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
|
@@ -138,6 +138,7 @@ export async function resolveResponsesCodexAuth(
|
|
|
138
138
|
route: RouteResult,
|
|
139
139
|
options: HandleResponsesOptions,
|
|
140
140
|
credentialDomainWasRewritten = false,
|
|
141
|
+
retainAccountForUploadedFiles = false,
|
|
141
142
|
): Promise<ResponsesAuthResolution> {
|
|
142
143
|
try {
|
|
143
144
|
let authInputHeaders = codexRouteCredentialDomainHeaders(
|
|
@@ -232,6 +233,7 @@ export async function resolveResponsesCodexAuth(
|
|
|
232
233
|
resolveCodexModelEntitlements: options.resolveCodexModelEntitlements,
|
|
233
234
|
signal: options.abortSignal,
|
|
234
235
|
nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
|
|
236
|
+
retainAccountForUploadedFiles,
|
|
235
237
|
});
|
|
236
238
|
options.onCodexAuthContextResolved?.(authCtx);
|
|
237
239
|
} else {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
resetUpstreamHostHealth,
|
|
18
18
|
} from "../../codex/upstream-host-health";
|
|
19
19
|
import { safeOriginLabel, fetchWithHeaderTimeout, providerFetch } from "./fetch-helpers";
|
|
20
|
+
import { classifyPoolRecoveryDispatch } from "../../routing/probe-lease";
|
|
20
21
|
import { formatErrorResponse } from "../../bridge";
|
|
21
22
|
import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
22
23
|
import { upstreamErrorMessageFromPayload, isRateLimitOrQuotaFailureMessage } from "../../lib/errors";
|
|
@@ -40,6 +41,7 @@ import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
|
|
|
40
41
|
import { slugsEquivalent } from "../../providers/slug-codec";
|
|
41
42
|
import {
|
|
42
43
|
codexProbeLeaseId,
|
|
44
|
+
codexTransientProbeGrant,
|
|
43
45
|
codexProbeQuotaScope,
|
|
44
46
|
releaseCodexAuthContextProbeLease,
|
|
45
47
|
resolveCodexAuthContext,
|
|
@@ -60,13 +62,14 @@ import { bindRouteReasoningReplayScope } from "./core-replay";
|
|
|
60
62
|
import {
|
|
61
63
|
conversationStateBindingFromAuth,
|
|
62
64
|
applyAccountChangeConversationStateScrub,
|
|
65
|
+
conversationCarriesUploadedFiles,
|
|
63
66
|
} from "./account-change-state";
|
|
64
67
|
import {
|
|
65
68
|
recordAdapterReasoning,
|
|
66
69
|
recordAdapterTier,
|
|
67
70
|
sealRequestAttemptIdentity,
|
|
68
71
|
recordAttemptCredentialSource,
|
|
69
|
-
|
|
72
|
+
noteProviderAttemptSend,
|
|
70
73
|
} from "../request-log";
|
|
71
74
|
import { codexAuthContextLogLabel } from "../../codex/account-label";
|
|
72
75
|
import { chargeWorkflowSends } from "../../lib/workflow-budget";
|
|
@@ -221,7 +224,17 @@ export async function shouldRetryCodexPoolAccountQuota(
|
|
|
221
224
|
// A post-send WebSocket gateway status must not become a second account's send; the
|
|
222
225
|
// body carries no quota evidence either, but the marker is the contract, not the prose.
|
|
223
226
|
if (isNonReplayableResponse(response)) return false;
|
|
224
|
-
if (response.status === 402 || response.status === 429)
|
|
227
|
+
if (response.status === 402 || response.status === 429) {
|
|
228
|
+
// Status alone used to authorize the move, which is right for a limit the ACCOUNT owns and
|
|
229
|
+
// wrong for one it merely belongs to. An organization- or project-scoped exhaustion refuses
|
|
230
|
+
// every credential inside that organization, so the second account meets the same counter
|
|
231
|
+
// and the only thing the rotation buys is a second cold prompt prefix (#4546). Positive
|
|
232
|
+
// evidence is required to withhold it: the helper fails closed, so an unreadable or
|
|
233
|
+
// ambiguous body keeps the broad #584 behaviour unchanged, and `rate_limit_exceeded`,
|
|
234
|
+
// `slow_down` and plan-level exhaustion still rotate exactly as before.
|
|
235
|
+
const { codexScopedExhaustionCode } = await import("../../codex/quota-rejection");
|
|
236
|
+
return await codexScopedExhaustionCode(response, { signal }) === undefined;
|
|
237
|
+
}
|
|
225
238
|
if (response.status < 500 || response.status >= 600) return false;
|
|
226
239
|
try {
|
|
227
240
|
// Reject malformed UTF-8 instead of matching quota words around replacement characters.
|
|
@@ -459,6 +472,7 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
459
472
|
modelId: route.modelId,
|
|
460
473
|
probeLeaseId: codexProbeLeaseId(firstAuthCtx),
|
|
461
474
|
probeQuotaScope: codexProbeQuotaScope(firstAuthCtx),
|
|
475
|
+
transientProbe: codexTransientProbeGrant(firstAuthCtx),
|
|
462
476
|
writerGeneration: firstAuthCtx.writerGeneration,
|
|
463
477
|
});
|
|
464
478
|
};
|
|
@@ -489,6 +503,17 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
489
503
|
recordUnmovedTransientOutcome();
|
|
490
504
|
return { kind: "no-alternate" };
|
|
491
505
|
}
|
|
506
|
+
// An uploaded file is readable only by the account it was sent to, so NO alternate can serve
|
|
507
|
+
// this body. Which account would be chosen does not change that, which is why this asks before
|
|
508
|
+
// the resolution rather than after it: refusing here reserves no send, cancels no response, and
|
|
509
|
+
// leaves the caller holding the first account's rejection to return unchanged (#4710). The
|
|
510
|
+
// initial-dispatch sites answer with a 400 instead, because there is no earlier response there
|
|
511
|
+
// to fall back to. A same-account replay -- the gated-model 400 ladder above -- is unaffected,
|
|
512
|
+
// since it never leaves the issuing account.
|
|
513
|
+
if (!retryAuthCtx && conversationCarriesUploadedFiles(parsed._rawBody)) {
|
|
514
|
+
recordUnmovedTransientOutcome();
|
|
515
|
+
return { kind: "no-alternate" };
|
|
516
|
+
}
|
|
492
517
|
// An account move is the guarded profile's fourth send and draws the single shared
|
|
493
518
|
// final-recovery reserve. Nothing bounded it per request before: `excludeAccountId` excludes
|
|
494
519
|
// only the account that just failed, and the caller's recovery loop can return here after the
|
|
@@ -557,6 +582,7 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
557
582
|
modelId: route.modelId,
|
|
558
583
|
probeLeaseId: codexProbeLeaseId(firstAuthCtx),
|
|
559
584
|
probeQuotaScope: codexProbeQuotaScope(firstAuthCtx),
|
|
585
|
+
transientProbe: codexTransientProbeGrant(firstAuthCtx),
|
|
560
586
|
writerGeneration: firstAuthCtx.writerGeneration,
|
|
561
587
|
});
|
|
562
588
|
}
|
|
@@ -588,6 +614,7 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
588
614
|
modelId: route.modelId,
|
|
589
615
|
probeLeaseId: codexProbeLeaseId(firstAuthCtx),
|
|
590
616
|
probeQuotaScope: codexProbeQuotaScope(firstAuthCtx),
|
|
617
|
+
transientProbe: codexTransientProbeGrant(firstAuthCtx),
|
|
591
618
|
writerGeneration: firstAuthCtx.writerGeneration,
|
|
592
619
|
// Retry already advanced the RR ring via excludeAccountId — reuse for promotion.
|
|
593
620
|
...(retryAuthCtx.accountId ? { promoteAccountId: retryAuthCtx.accountId } : {}),
|
|
@@ -693,16 +720,35 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
693
720
|
// The same-account gated-model 400 ladder below keeps its own `maxRetrySends` bound and
|
|
694
721
|
// does not take the reserve again; only the move itself does.
|
|
695
722
|
if (accountMovePermit) {
|
|
723
|
+
// The pool-wide recovery window is consulted BEFORE the request-local permit is used.
|
|
724
|
+
// `reserveDispatch` charges at reservation time and `release()` is the only way back, so
|
|
725
|
+
// using the permit first and refusing afterwards would spend a send the request never
|
|
726
|
+
// made. An account move is recovery traffic like any other: one request's own budget
|
|
727
|
+
// cannot see that a thousand other requests are moving at the same moment, which is
|
|
728
|
+
// precisely the amplification this window exists to bound (#4701).
|
|
729
|
+
//
|
|
730
|
+
// A refusal here is not a new failure mode: "no alternate was available" is already the
|
|
731
|
+
// outcome when the pool has nowhere to move this request to, and it is handled.
|
|
732
|
+
if (!classifyPoolRecoveryDispatch("retry").admitted) {
|
|
733
|
+
accountMovePermit.release();
|
|
734
|
+
accountMovePermit = undefined;
|
|
735
|
+
// The alternate context was resolved and will not send. Hand back whatever recovery
|
|
736
|
+
// lease it is holding rather than leaving that account unprobeable.
|
|
737
|
+
releaseCodexAuthContextProbeLease(retryAuthCtx);
|
|
738
|
+
recordUnmovedTransientOutcome();
|
|
739
|
+
return { kind: "no-alternate" };
|
|
740
|
+
}
|
|
696
741
|
const charged = accountMovePermit.use();
|
|
697
742
|
accountMovePermit = undefined;
|
|
698
743
|
if (!charged) {
|
|
744
|
+
releaseCodexAuthContextProbeLease(retryAuthCtx);
|
|
699
745
|
recordUnmovedTransientOutcome();
|
|
700
746
|
return { kind: "no-alternate" };
|
|
701
747
|
}
|
|
702
748
|
// The move is a physical send like any other, so the root workflow is charged too.
|
|
703
749
|
chargeWorkflowSends(args.options.workflowRootId, 1);
|
|
704
750
|
}
|
|
705
|
-
|
|
751
|
+
noteProviderAttemptSend(logCtx, route.providerName, route.provider, passthroughEstimate);
|
|
706
752
|
try {
|
|
707
753
|
upstreamResponse = await fetchWithHeaderTimeout(
|
|
708
754
|
request.url,
|
|
@@ -827,6 +873,7 @@ export function codexForwardTerminalOutcomeRecorder(
|
|
|
827
873
|
modelId,
|
|
828
874
|
probeLeaseId: codexProbeLeaseId(authCtx),
|
|
829
875
|
probeQuotaScope: codexProbeQuotaScope(authCtx),
|
|
876
|
+
transientProbe: codexTransientProbeGrant(authCtx),
|
|
830
877
|
writerGeneration: authCtx.writerGeneration,
|
|
831
878
|
});
|
|
832
879
|
return;
|
|
@@ -849,6 +896,7 @@ export function codexForwardTerminalOutcomeRecorder(
|
|
|
849
896
|
modelId,
|
|
850
897
|
probeLeaseId: codexProbeLeaseId(authCtx),
|
|
851
898
|
probeQuotaScope: codexProbeQuotaScope(authCtx),
|
|
899
|
+
transientProbe: codexTransientProbeGrant(authCtx),
|
|
852
900
|
writerGeneration: authCtx.writerGeneration,
|
|
853
901
|
// A mid-stream terminal can carry a semantic 401 long after the credential was
|
|
854
902
|
// replaced. It is never replayed — the client already saw output — but it must
|
|
@@ -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,7 +740,7 @@ 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,
|
|
@@ -674,12 +750,16 @@ export async function executeComboResponses(
|
|
|
674
750
|
} else {
|
|
675
751
|
pick = await pickWithWait({
|
|
676
752
|
exclude: pick.attempted,
|
|
677
|
-
eligible:
|
|
753
|
+
eligible: targetEligible,
|
|
678
754
|
now: failureNow,
|
|
679
755
|
});
|
|
680
756
|
}
|
|
681
757
|
if (!pick) {
|
|
682
758
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
759
|
+
if (onlyReplayIncompatibleTargetsRemain(attemptedTargets)) {
|
|
760
|
+
adoptFailedChildLog(childLog);
|
|
761
|
+
return targetIncompatibleResponse();
|
|
762
|
+
}
|
|
683
763
|
if (unreadableEncryptedAgentTask && !comboPayloadReadable) {
|
|
684
764
|
const recoveredTarget = await pickWithWait({
|
|
685
765
|
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
|
+
}
|
|
@@ -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) {
|