@bitkyc08/opencodex 2.55.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-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- 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/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- 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/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +642 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -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-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +566 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +431 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- 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/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- 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/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -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 +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- 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/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +339 -0
- package/src/server/index.ts +45 -2552
- 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/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- 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 +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +298 -0
- package/src/server/responses/core.ts +192 -8893
- 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 +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- 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 +34 -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 +147 -1
- package/src/usage/summary.ts +171 -21
- 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
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
import { isDeclaredReasoningEffort } from "../../reasoning-effort";
|
|
2
|
+
import { recordAttemptRequestedEffort } from "../request-log";
|
|
3
|
+
import {
|
|
4
|
+
CODEX_TEXT_GUARDED_BUDGET_POLICY,
|
|
5
|
+
deriveRequestExecutionBudget,
|
|
6
|
+
isRequestExecutionBudget,
|
|
7
|
+
} from "../../lib/request-execution-budget";
|
|
8
|
+
import type {
|
|
9
|
+
RequestExecutionBudgetPolicy,
|
|
10
|
+
RequestExecutionBudget,
|
|
11
|
+
} from "../../lib/request-execution-budget";
|
|
12
|
+
import type { OcxConfig } from "../../types";
|
|
13
|
+
import type { RequestLogContext } from "../request-log";
|
|
14
|
+
import type { HandleResponsesOptions, ResponsesDispatchers, ConsumedComboFailure } from "./core-options";
|
|
15
|
+
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
16
|
+
import {
|
|
17
|
+
getCombo,
|
|
18
|
+
comboRequestHasImageInput,
|
|
19
|
+
pickComboTargetWithWait,
|
|
20
|
+
targetKey,
|
|
21
|
+
concreteComboRequestBody,
|
|
22
|
+
comboDefaultEffort,
|
|
23
|
+
isComboTargetInCooldown,
|
|
24
|
+
noteComboSuccess,
|
|
25
|
+
comboFailureDecision,
|
|
26
|
+
advanceComboAfterFailure,
|
|
27
|
+
comboFailureCooldownScope,
|
|
28
|
+
} from "../../combos";
|
|
29
|
+
import { formatErrorResponse } from "../../bridge";
|
|
30
|
+
import {
|
|
31
|
+
expandPreviousResponseInput,
|
|
32
|
+
previousResponseScopeMismatch,
|
|
33
|
+
previousResponseReplayFailure,
|
|
34
|
+
previousResponseProviderState,
|
|
35
|
+
} from "../../responses/state";
|
|
36
|
+
import { hasUnreadableEncryptedAgentTask } from "./encrypted-payload";
|
|
37
|
+
import { routeConcreteModel, comboRouteDecisionTrace } from "../../router";
|
|
38
|
+
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
39
|
+
import type { AgentTaskRecoveryFailureReason } from "./agent-task-recovery";
|
|
40
|
+
import {
|
|
41
|
+
agentTaskRecoveryConfig,
|
|
42
|
+
discardEncryptedAgentTaskRecovery,
|
|
43
|
+
recoverEncryptedAgentTaskWithResult,
|
|
44
|
+
} from "./agent-task-recovery";
|
|
45
|
+
import { isThreadSpawnRequest, supportedLadderFor } from "../effort-policy";
|
|
46
|
+
import {
|
|
47
|
+
clientCancelledResponse,
|
|
48
|
+
comboUnavailable,
|
|
49
|
+
targetIncompatibleResponse,
|
|
50
|
+
unreadableEncryptedAgentTaskResponse,
|
|
51
|
+
} from "./core-errors";
|
|
52
|
+
import {
|
|
53
|
+
buildComboChildHeaders,
|
|
54
|
+
createChildPassthroughCallbackGate,
|
|
55
|
+
consumeComboFailure,
|
|
56
|
+
} from "./core-combo-failure";
|
|
57
|
+
import {
|
|
58
|
+
linkRequestSessionLane,
|
|
59
|
+
reasoningReplayConversationIdFromResponsesRequest,
|
|
60
|
+
sessionIdHeaderFromRequest,
|
|
61
|
+
sessionLaneIdFromRequest,
|
|
62
|
+
} from "../request-log-conversation";
|
|
63
|
+
import type { CodexAuthContext } from "../../codex/auth-context";
|
|
64
|
+
import type { ResponsesTerminalStatus } from "../../bridge";
|
|
65
|
+
import { beginRequestAttempt, sealRequestAttemptIdentity, finishRequestAttempt } from "../request-log";
|
|
66
|
+
import { rememberComboForLane } from "./combo-session-recall";
|
|
67
|
+
import { runTurnAdapterSseResponses } from "./core-lifetime";
|
|
68
|
+
import {
|
|
69
|
+
isNativePassthroughSseResponse,
|
|
70
|
+
isEagerRelaySseResponse,
|
|
71
|
+
markNativePassthroughSseResponse,
|
|
72
|
+
markEagerRelaySseResponse,
|
|
73
|
+
} from "../relay";
|
|
74
|
+
import { preflightComboStreamResponse } from "./combo-stream-preflight";
|
|
75
|
+
import { streamingContextOverflowResponse, jsonContextOverflowResponse } from "./context-overflow";
|
|
76
|
+
import { mandatoryResponsesReasoningReplayUnavailable } from "./core-replay";
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Sends one combo target may run on its own before the ladder moves on. A target is a whole
|
|
80
|
+
* request as far as its own provider is concerned, so this is the guarded profile's base
|
|
81
|
+
* allowance rather than a separate number to keep in sync.
|
|
82
|
+
*/
|
|
83
|
+
export const COMBO_TARGET_BASE_SENDS = CODEX_TEXT_GUARDED_BUDGET_POLICY.baseSendAllowance;
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* A combo's execution policy is DECLARED by the combo, not inherited from the single-target
|
|
88
|
+
* profile.
|
|
89
|
+
*
|
|
90
|
+
* `maxTargetTransitions: 1` and `maxAlternateTargetSends: 1` describe an account move, and
|
|
91
|
+
* applying them to a combo would refuse the second hop of a three-target combo -- which is why
|
|
92
|
+
* combo was left off `reserveDispatch` when the per-request split landed. The transitions a
|
|
93
|
+
* combo may make are exactly the targets it declares minus the one it starts on. What stays
|
|
94
|
+
* capped is the TOTAL: the first target's full ladder, one send for every further declared
|
|
95
|
+
* target, and the one shared final-recovery reserve. A one-target combo reduces to the guarded
|
|
96
|
+
* profile exactly, and a three-target combo whose every target fails hard reaches upstream six
|
|
97
|
+
* times instead of the twelve #4546 measured.
|
|
98
|
+
*/
|
|
99
|
+
export function comboExecutionBudgetPolicy(declaredTargets: number): RequestExecutionBudgetPolicy {
|
|
100
|
+
const targets = Math.max(1, Math.trunc(declaredTargets));
|
|
101
|
+
const hops = targets - 1;
|
|
102
|
+
const reserve = CODEX_TEXT_GUARDED_BUDGET_POLICY.finalRecoveryAllowance;
|
|
103
|
+
const total = COMBO_TARGET_BASE_SENDS + hops + reserve;
|
|
104
|
+
return {
|
|
105
|
+
maxTotalModelSends: total,
|
|
106
|
+
baseSendAllowance: total - reserve,
|
|
107
|
+
finalRecoveryAllowance: reserve,
|
|
108
|
+
maxAlternateTargetSends: Math.max(1, hops),
|
|
109
|
+
maxTargetTransitions: Math.max(1, hops),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* A budget scope that keeps its own recovery ledgers but spends the SAME request-wide counter.
|
|
116
|
+
*
|
|
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.
|
|
132
|
+
*/
|
|
133
|
+
export function deriveSendBudgetScope(
|
|
134
|
+
parent: RequestExecutionBudget,
|
|
135
|
+
policy: RequestExecutionBudgetPolicy,
|
|
136
|
+
): RequestExecutionBudget {
|
|
137
|
+
return deriveRequestExecutionBudget(parent, policy);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The ladder one combo target may run, expressed as an allowance on the request-wide counter.
|
|
143
|
+
*
|
|
144
|
+
* `used + COMBO_TARGET_BASE_SENDS` gives this target its own ladder from wherever the request
|
|
145
|
+
* already stands, and the clamp holds back one send for each target still declared after it: a
|
|
146
|
+
* first target that 5xx-streaks must not eat the send the last declared target is entitled to.
|
|
147
|
+
* That guarantee is the difference between a per-target policy and a shared pool the first
|
|
148
|
+
* target drains.
|
|
149
|
+
*/
|
|
150
|
+
export function comboTargetSendBudget(
|
|
151
|
+
comboScope: RequestExecutionBudget,
|
|
152
|
+
targetsDeclaredAfterThisOne: number,
|
|
153
|
+
): RequestExecutionBudget {
|
|
154
|
+
const policy = comboScope.policy;
|
|
155
|
+
const heldForLaterTargets = Math.max(0, targetsDeclaredAfterThisOne);
|
|
156
|
+
const ceiling = Math.max(1, policy.maxTotalModelSends - heldForLaterTargets);
|
|
157
|
+
return deriveSendBudgetScope(comboScope, {
|
|
158
|
+
maxTotalModelSends: policy.maxTotalModelSends,
|
|
159
|
+
baseSendAllowance: Math.min(ceiling, comboScope.used + COMBO_TARGET_BASE_SENDS),
|
|
160
|
+
finalRecoveryAllowance: policy.finalRecoveryAllowance,
|
|
161
|
+
// Within one target the account-move shape is unchanged: three same-account sends plus one
|
|
162
|
+
// alternate is the recovery live traffic depends on, and a combo does not widen it.
|
|
163
|
+
maxAlternateTargetSends: CODEX_TEXT_GUARDED_BUDGET_POLICY.maxAlternateTargetSends,
|
|
164
|
+
maxTargetTransitions: CODEX_TEXT_GUARDED_BUDGET_POLICY.maxTargetTransitions,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
export async function executeComboResponses(
|
|
170
|
+
req: Request,
|
|
171
|
+
rawBody: unknown,
|
|
172
|
+
comboId: string,
|
|
173
|
+
config: OcxConfig,
|
|
174
|
+
logCtx: RequestLogContext,
|
|
175
|
+
options: HandleResponsesOptions & { translatorBudget: TranslatorBudget },
|
|
176
|
+
requestDispatchers: ResponsesDispatchers,
|
|
177
|
+
): Promise<Response> {
|
|
178
|
+
const requestedModel = typeof (rawBody as { model?: unknown } | null)?.model === "string"
|
|
179
|
+
? (rawBody as { model: string }).model
|
|
180
|
+
: `combo/${comboId}`;
|
|
181
|
+
Object.assign(logCtx, {
|
|
182
|
+
requestedModel,
|
|
183
|
+
model: requestedModel,
|
|
184
|
+
provider: "combo",
|
|
185
|
+
comboId,
|
|
186
|
+
});
|
|
187
|
+
const combo = getCombo(config, comboId);
|
|
188
|
+
if (!combo) {
|
|
189
|
+
return formatErrorResponse(404, "invalid_request_error", `Unknown combo: ${comboId}`);
|
|
190
|
+
}
|
|
191
|
+
// The ladder's own scope, derived from what this combo DECLARES. It shares the request-wide
|
|
192
|
+
// counter with the holder that arrived on options -- a combo child already inherited that
|
|
193
|
+
// counter, but nothing read it as a limit across targets -- while its transition and
|
|
194
|
+
// alternate-target ledgers come from the target list rather than from the single-target
|
|
195
|
+
// account-move profile (#4546).
|
|
196
|
+
const comboSendScope = isRequestExecutionBudget(options.sendBudget)
|
|
197
|
+
? deriveSendBudgetScope(options.sendBudget, comboExecutionBudgetPolicy(combo.targets.length))
|
|
198
|
+
: undefined;
|
|
199
|
+
// Expand previous_response_id before image policy and child dispatch so a
|
|
200
|
+
// continuation that only references prior images still fails closed when
|
|
201
|
+
// imageInput is disabled (and so targets see the full replayed input).
|
|
202
|
+
const inboundClientThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
|
|
203
|
+
const body = expandPreviousResponseInput(rawBody, inboundClientThreadId);
|
|
204
|
+
const scopeMismatch = previousResponseScopeMismatch(body);
|
|
205
|
+
if (scopeMismatch) {
|
|
206
|
+
console.warn("[opencodex] dropped a previous_response_id with a mismatched client task scope; continuing fresh");
|
|
207
|
+
}
|
|
208
|
+
if (previousResponseReplayFailure(body)) {
|
|
209
|
+
return formatErrorResponse(
|
|
210
|
+
400,
|
|
211
|
+
"previous_response_not_found",
|
|
212
|
+
"Continuation state is unavailable or corrupt; resend the full conversation without previous_response_id.",
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
// Missing state returns the original body without a failure marker. Reject
|
|
216
|
+
// that unresolved continuation for image-disabled combos so a target cannot
|
|
217
|
+
// resolve prior images out of band. A successful expansion yields a new
|
|
218
|
+
// object (still carrying previous_response_id) and must not be treated as
|
|
219
|
+
// unresolved — text-only stored continuations remain allowed.
|
|
220
|
+
const requestedPreviousId = typeof (rawBody as { previous_response_id?: unknown } | null)?.previous_response_id === "string"
|
|
221
|
+
? (rawBody as { previous_response_id: string }).previous_response_id.trim()
|
|
222
|
+
: "";
|
|
223
|
+
const unresolvedPrevious = requestedPreviousId.length > 0 && body === rawBody;
|
|
224
|
+
if (combo.imageInput === "disabled" && unresolvedPrevious) {
|
|
225
|
+
return formatErrorResponse(
|
|
226
|
+
400,
|
|
227
|
+
"previous_response_not_found",
|
|
228
|
+
"Continuation state is unavailable or corrupt; resend the full conversation without previous_response_id.",
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
if (combo.imageInput === "disabled" && comboRequestHasImageInput(body)) {
|
|
232
|
+
return formatErrorResponse(400, "invalid_request_error", `Combo "${comboId}" does not accept image input`);
|
|
233
|
+
}
|
|
234
|
+
const comboReplaySnapshot = {
|
|
235
|
+
sourceBody: body,
|
|
236
|
+
previousResponseInputExpanded: body !== rawBody
|
|
237
|
+
&& typeof (body as { previous_response_id?: unknown }).previous_response_id === "string",
|
|
238
|
+
providerContinuation: !scopeMismatch && body !== rawBody && requestedPreviousId
|
|
239
|
+
? previousResponseProviderState(requestedPreviousId)
|
|
240
|
+
: undefined,
|
|
241
|
+
recoveredPlaintext: false,
|
|
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
|
+
};
|
|
266
|
+
const adoptFailedChildLog = (childLog: RequestLogContext): void => {
|
|
267
|
+
// Attempts remain the complete physical history; the logical row mirrors the most recent
|
|
268
|
+
// failed target so an exhausted combo still has useful top-level reasoning diagnostics.
|
|
269
|
+
Object.assign(logCtx, childLog, {
|
|
270
|
+
requestedModel,
|
|
271
|
+
model: requestedModel,
|
|
272
|
+
provider: "combo",
|
|
273
|
+
comboId,
|
|
274
|
+
routeDecision: logCtx.routeDecision,
|
|
275
|
+
attempts: logCtx.attempts,
|
|
276
|
+
activeAttempt: undefined,
|
|
277
|
+
activeAttemptStartedAt: undefined,
|
|
278
|
+
});
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
|
|
282
|
+
(body as { input?: unknown } | undefined)?.input,
|
|
283
|
+
);
|
|
284
|
+
const canDecryptUnreadableAgentTask = (target: (typeof combo.targets)[number]): boolean => {
|
|
285
|
+
const provider = config.providers[target.provider];
|
|
286
|
+
if (!provider || provider.disabled === true) return false;
|
|
287
|
+
try {
|
|
288
|
+
const route = routeConcreteModel(config, `${target.provider}/${target.model}`);
|
|
289
|
+
return isCanonicalOpenAiForwardProvider(route.provider);
|
|
290
|
+
} catch {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
let comboPayloadReadable = false;
|
|
295
|
+
const payloadEligible = (target: (typeof combo.targets)[number]): boolean =>
|
|
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
|
+
};
|
|
309
|
+
let encryptedTaskRecoveryAttempted = false;
|
|
310
|
+
let recoveryFailureReason: AgentTaskRecoveryFailureReason | undefined;
|
|
311
|
+
let storedPool401ReplayDispatched = false;
|
|
312
|
+
const recoverUnreadableEncryptedTask = async (): Promise<boolean> => {
|
|
313
|
+
if (encryptedTaskRecoveryAttempted) return false;
|
|
314
|
+
encryptedTaskRecoveryAttempted = true;
|
|
315
|
+
const recovery = agentTaskRecoveryConfig(config);
|
|
316
|
+
if (
|
|
317
|
+
(options.inboundWire ?? "responses") !== "responses"
|
|
318
|
+
|| !isThreadSpawnRequest(req.headers)
|
|
319
|
+
|| !recovery
|
|
320
|
+
|| options.comboAttempt
|
|
321
|
+
) {
|
|
322
|
+
discardEncryptedAgentTaskRecovery(
|
|
323
|
+
req,
|
|
324
|
+
(body as { input?: unknown } | undefined)?.input,
|
|
325
|
+
config,
|
|
326
|
+
{ parentThreadId: inboundClientThreadId },
|
|
327
|
+
);
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
let recovered = false;
|
|
331
|
+
try {
|
|
332
|
+
const result = await recoverEncryptedAgentTaskWithResult(
|
|
333
|
+
req,
|
|
334
|
+
(body as { input?: unknown } | undefined)?.input,
|
|
335
|
+
recovery,
|
|
336
|
+
config,
|
|
337
|
+
{ parentThreadId: inboundClientThreadId, abortSignal: options.abortSignal },
|
|
338
|
+
);
|
|
339
|
+
recovered = result.recovered;
|
|
340
|
+
recoveryFailureReason = result.recovered ? undefined : result.reason;
|
|
341
|
+
} catch {
|
|
342
|
+
recovered = false;
|
|
343
|
+
recoveryFailureReason = undefined;
|
|
344
|
+
}
|
|
345
|
+
// Recovery has the same in-place input mutation contract as the direct routed path.
|
|
346
|
+
if (
|
|
347
|
+
!recovered
|
|
348
|
+
|| hasUnreadableEncryptedAgentTask((body as { input?: unknown } | undefined)?.input)
|
|
349
|
+
) {
|
|
350
|
+
discardEncryptedAgentTaskRecovery(
|
|
351
|
+
req,
|
|
352
|
+
(body as { input?: unknown } | undefined)?.input,
|
|
353
|
+
config,
|
|
354
|
+
{ parentThreadId: inboundClientThreadId },
|
|
355
|
+
);
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
comboPayloadReadable = true;
|
|
359
|
+
comboReplaySnapshot.recoveredPlaintext = true;
|
|
360
|
+
return true;
|
|
361
|
+
};
|
|
362
|
+
const initialNow = Date.now();
|
|
363
|
+
const pickWithWait = (pickOptions: {
|
|
364
|
+
exclude?: Iterable<string>;
|
|
365
|
+
eligible?: (target: NonNullable<typeof combo>["targets"][number]) => boolean;
|
|
366
|
+
now?: number;
|
|
367
|
+
}) => pickComboTargetWithWait(config, comboId, {
|
|
368
|
+
...pickOptions,
|
|
369
|
+
waitForCooldownMs: combo.waitForCooldownMs,
|
|
370
|
+
abortSignal: options.abortSignal,
|
|
371
|
+
});
|
|
372
|
+
let pick = await pickWithWait({
|
|
373
|
+
eligible: targetEligible,
|
|
374
|
+
now: initialNow,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
if (unreadableEncryptedAgentTask && !pick) {
|
|
378
|
+
pick = await pickWithWait({ now: initialNow });
|
|
379
|
+
if (!pick) {
|
|
380
|
+
discardEncryptedAgentTaskRecovery(
|
|
381
|
+
req,
|
|
382
|
+
(body as { input?: unknown } | undefined)?.input,
|
|
383
|
+
config,
|
|
384
|
+
{ parentThreadId: inboundClientThreadId },
|
|
385
|
+
);
|
|
386
|
+
return options.abortSignal?.aborted
|
|
387
|
+
? clientCancelledResponse()
|
|
388
|
+
: comboUnavailable(comboId);
|
|
389
|
+
}
|
|
390
|
+
if (!(await recoverUnreadableEncryptedTask())) {
|
|
391
|
+
return options.abortSignal?.aborted
|
|
392
|
+
? clientCancelledResponse()
|
|
393
|
+
: unreadableEncryptedAgentTaskResponse(recoveryFailureReason);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!pick) {
|
|
398
|
+
if (onlyReplayIncompatibleTargetsRemain()) return targetIncompatibleResponse();
|
|
399
|
+
return options.abortSignal?.aborted
|
|
400
|
+
? clientCancelledResponse()
|
|
401
|
+
: comboUnavailable(comboId);
|
|
402
|
+
}
|
|
403
|
+
// One immutable combo selection trace, before any child dispatch; child
|
|
404
|
+
// adoption below must never replace it with a concrete child route trace.
|
|
405
|
+
logCtx.routeDecision = comboRouteDecisionTrace(config, comboId, pick, requestedModel);
|
|
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
|
+
|
|
427
|
+
let lastFailure: Response | null = null;
|
|
428
|
+
// Dispatched targets, not attempted picks: it indexes the declared target list so the clamp
|
|
429
|
+
// below can tell how many targets are still entitled to a send.
|
|
430
|
+
let comboTargetsDispatched = 0;
|
|
431
|
+
// The child log behind `lastFailure`. The natural end of the ladder adopts it inside the
|
|
432
|
+
// no-more-targets branch; a budget refusal ends the ladder one iteration later, where that
|
|
433
|
+
// iteration's own `childLog` is already out of scope.
|
|
434
|
+
let lastFailedChildLog: RequestLogContext | undefined;
|
|
435
|
+
// The exhausted-combo mapping below runs outside the loop, where `failure.upstreamCode`
|
|
436
|
+
// is gone, so carry the loop's own classification decision instead of re-deriving a
|
|
437
|
+
// weaker one from the status alone (#4149).
|
|
438
|
+
let lastFailureClassifiesOverflow = false;
|
|
439
|
+
while (pick) {
|
|
440
|
+
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
441
|
+
const firstComboTarget = comboTargetsDispatched === 0;
|
|
442
|
+
// The first target seeds the ledger's target identity and charges nothing; every later one
|
|
443
|
+
// is a real transition, refused once the declared hops, the alternate-target ledger or the
|
|
444
|
+
// request total are spent. `countedExternally` is required: the child charges its own
|
|
445
|
+
// physical sends, and charging here as well would halve the cap without saying so.
|
|
446
|
+
const hopDecision = comboSendScope?.reserveDispatch({
|
|
447
|
+
sendClass: firstComboTarget ? "initial" : "combo-failover",
|
|
448
|
+
targetKey: `${pick.target.provider}/${pick.target.model}`,
|
|
449
|
+
countedExternally: true,
|
|
450
|
+
});
|
|
451
|
+
if (hopDecision && hopDecision.allowed) hopDecision.permit.use();
|
|
452
|
+
else if (hopDecision && !firstComboTarget) {
|
|
453
|
+
// Out of budget is not this target's failure. The established exhaustion contract is to
|
|
454
|
+
// return the last real upstream answer with its status, headers and any quota body
|
|
455
|
+
// intact rather than to mint a synthetic error, and a later target only exists because
|
|
456
|
+
// an earlier one already recorded one.
|
|
457
|
+
if (lastFailedChildLog) adoptFailedChildLog(lastFailedChildLog);
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
const targetSendBudget = comboSendScope
|
|
461
|
+
? comboTargetSendBudget(comboSendScope, combo.targets.length - 1 - comboTargetsDispatched)
|
|
462
|
+
: options.sendBudget;
|
|
463
|
+
comboTargetsDispatched += 1;
|
|
464
|
+
const childLog: RequestLogContext = {
|
|
465
|
+
model: pick.target.model,
|
|
466
|
+
provider: pick.target.provider,
|
|
467
|
+
...(logCtx.conversationId ? { conversationId: logCtx.conversationId } : {}),
|
|
468
|
+
...(logCtx.surface ? { surface: logCtx.surface } : {}),
|
|
469
|
+
};
|
|
470
|
+
const targetRoute = routeConcreteModel(config, `${pick.target.provider}/${pick.target.model}`);
|
|
471
|
+
const childBody = concreteComboRequestBody(
|
|
472
|
+
body,
|
|
473
|
+
pick.target,
|
|
474
|
+
comboDefaultEffort(config, comboId),
|
|
475
|
+
supportedLadderFor({ provider: targetRoute.provider, modelId: targetRoute.modelId }),
|
|
476
|
+
combo.reasoningEffortMode,
|
|
477
|
+
combo.defaultEffortMode,
|
|
478
|
+
);
|
|
479
|
+
const childHeaders = buildComboChildHeaders(req.headers);
|
|
480
|
+
const childRequest = new Request(req.url, {
|
|
481
|
+
method: req.method,
|
|
482
|
+
headers: childHeaders,
|
|
483
|
+
body: JSON.stringify(childBody),
|
|
484
|
+
});
|
|
485
|
+
linkRequestSessionLane(req, childRequest);
|
|
486
|
+
let resolvedAuth: CodexAuthContext | undefined;
|
|
487
|
+
let terminalRecorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined;
|
|
488
|
+
const started = Date.now();
|
|
489
|
+
const attempt = beginRequestAttempt(
|
|
490
|
+
(logCtx.attempts?.length ?? 0) + 1,
|
|
491
|
+
pick.target.provider,
|
|
492
|
+
pick.target.model,
|
|
493
|
+
config.providers[pick.target.provider]!.adapter,
|
|
494
|
+
);
|
|
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);
|
|
503
|
+
let attemptRetained = false;
|
|
504
|
+
const retainCancelledAttempt = (): void => {
|
|
505
|
+
if (attemptRetained) return;
|
|
506
|
+
sealRequestAttemptIdentity(
|
|
507
|
+
attempt,
|
|
508
|
+
childLog.provider,
|
|
509
|
+
childLog.providerAdapter ?? attempt.adapter,
|
|
510
|
+
childLog.accountLogLabel,
|
|
511
|
+
);
|
|
512
|
+
finishRequestAttempt(attempt, 499, Date.now() - started, childLog.usage);
|
|
513
|
+
attemptRetained = true;
|
|
514
|
+
};
|
|
515
|
+
const completedTarget = { provider: pick.target.provider, model: pick.target.model };
|
|
516
|
+
const writerGeneration = pick.writerGeneration;
|
|
517
|
+
let consumedChildFailure: ConsumedComboFailure | undefined;
|
|
518
|
+
const callbackGate = createChildPassthroughCallbackGate({
|
|
519
|
+
...options,
|
|
520
|
+
onResponseComplete: model => {
|
|
521
|
+
// The live config can change while the child is streaming. Never retain credentials.
|
|
522
|
+
const currentCombo = getCombo(config, comboId);
|
|
523
|
+
const provider = config.providers[completedTarget.provider];
|
|
524
|
+
if (Object.hasOwn(config.providers, completedTarget.provider)
|
|
525
|
+
&& provider && provider.disabled !== true
|
|
526
|
+
&& currentCombo?.targets.some(target => targetKey(target) === targetKey(completedTarget))) {
|
|
527
|
+
rememberComboForLane(sessionLaneIdFromRequest(req.headers), comboId, completedTarget, model, writerGeneration);
|
|
528
|
+
}
|
|
529
|
+
options.onResponseComplete?.(model);
|
|
530
|
+
},
|
|
531
|
+
onNativePassthroughTerminal: status => {
|
|
532
|
+
// A committed stream can acquire terminal metadata after preflight copied
|
|
533
|
+
// the child log. Publish it before the outer logger finalizes, but only
|
|
534
|
+
// through the gate: discarded attempts must never affect the parent.
|
|
535
|
+
// Undefined child fields must preserve metadata already inspected by WS.
|
|
536
|
+
if (childLog.terminalHttpStatus !== undefined) logCtx.terminalHttpStatus = childLog.terminalHttpStatus;
|
|
537
|
+
if (childLog.terminalIncompleteReason !== undefined) logCtx.terminalIncompleteReason = childLog.terminalIncompleteReason;
|
|
538
|
+
if (childLog.terminalErrorCode !== undefined) logCtx.terminalErrorCode = childLog.terminalErrorCode;
|
|
539
|
+
if (childLog.upstreamError !== undefined) logCtx.upstreamError = childLog.upstreamError;
|
|
540
|
+
options.onNativePassthroughTerminal?.(status);
|
|
541
|
+
},
|
|
542
|
+
});
|
|
543
|
+
let response: Response;
|
|
544
|
+
try {
|
|
545
|
+
const currentTargetProvider = pick.target.provider;
|
|
546
|
+
const deferCodexResetDerivedCooldown = combo.strategy === "failover"
|
|
547
|
+
&& combo.targets.slice(pick.targetIndex + 1).some(target =>
|
|
548
|
+
target.provider === currentTargetProvider
|
|
549
|
+
&& targetEligible(target)
|
|
550
|
+
&& !isComboTargetInCooldown(comboId, target),
|
|
551
|
+
);
|
|
552
|
+
response = await requestDispatchers.handleResponses(childRequest, config, childLog, {
|
|
553
|
+
...options,
|
|
554
|
+
// After the spread: the child must run on THIS target's ladder, not on the holder the
|
|
555
|
+
// parent arrived with.
|
|
556
|
+
sendBudget: targetSendBudget,
|
|
557
|
+
comboAttempt: true,
|
|
558
|
+
comboReplaySnapshot,
|
|
559
|
+
deferCodexResetDerivedCooldown,
|
|
560
|
+
// Attempt-relative TTFT is recorded HERE (not via childLog.firstOutputMs — a later
|
|
561
|
+
// Object.assign(logCtx, childLog) would overwrite the request-relative value).
|
|
562
|
+
onFirstOutput: () => {
|
|
563
|
+
if (attempt.firstOutputMs === undefined) {
|
|
564
|
+
attempt.firstOutputMs = Math.max(0, Date.now() - started);
|
|
565
|
+
}
|
|
566
|
+
options.onFirstOutput?.();
|
|
567
|
+
},
|
|
568
|
+
onCodexAuthContextResolved: value => { resolvedAuth = value; },
|
|
569
|
+
setTerminalOutcomeRecorder: value => { terminalRecorder = value; },
|
|
570
|
+
onConsumedComboFailure: value => { consumedChildFailure = value; },
|
|
571
|
+
onStoredPool401ReplayDispatched: () => { storedPool401ReplayDispatched = true; },
|
|
572
|
+
onNativePassthroughTerminal: callbackGate.onTerminal,
|
|
573
|
+
onNativePassthroughCancel: callbackGate.onCancel,
|
|
574
|
+
onResponseComplete: callbackGate.onResponseComplete,
|
|
575
|
+
});
|
|
576
|
+
restoreOriginalRequestedEffort(childLog);
|
|
577
|
+
} catch (error) {
|
|
578
|
+
callbackGate.discard();
|
|
579
|
+
if (options.abortSignal?.aborted) {
|
|
580
|
+
retainCancelledAttempt();
|
|
581
|
+
return clientCancelledResponse();
|
|
582
|
+
}
|
|
583
|
+
finishRequestAttempt(attempt, 502, Date.now() - started, childLog.usage);
|
|
584
|
+
throw error;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (options.abortSignal?.aborted) {
|
|
588
|
+
callbackGate.discard();
|
|
589
|
+
retainCancelledAttempt();
|
|
590
|
+
return clientCancelledResponse();
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (response.ok && !runTurnAdapterSseResponses.has(response)) {
|
|
594
|
+
const nativePassthrough = isNativePassthroughSseResponse(response);
|
|
595
|
+
const eagerRelay = isEagerRelaySseResponse(response);
|
|
596
|
+
let preflight;
|
|
597
|
+
try {
|
|
598
|
+
preflight = await preflightComboStreamResponse(response, childLog);
|
|
599
|
+
} catch (error) {
|
|
600
|
+
callbackGate.discard();
|
|
601
|
+
if (options.abortSignal?.aborted) {
|
|
602
|
+
retainCancelledAttempt();
|
|
603
|
+
return clientCancelledResponse();
|
|
604
|
+
}
|
|
605
|
+
finishRequestAttempt(attempt, 502, Date.now() - started, childLog.usage);
|
|
606
|
+
throw error;
|
|
607
|
+
}
|
|
608
|
+
if (preflight.kind === "failed") {
|
|
609
|
+
callbackGate.discard();
|
|
610
|
+
terminalRecorder?.("failed", preflight.response.status);
|
|
611
|
+
response = preflight.response;
|
|
612
|
+
} else {
|
|
613
|
+
response = preflight.response;
|
|
614
|
+
if (nativePassthrough) markNativePassthroughSseResponse(response);
|
|
615
|
+
if (eagerRelay) markEagerRelaySseResponse(response);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (response.ok) {
|
|
620
|
+
sealRequestAttemptIdentity(
|
|
621
|
+
attempt,
|
|
622
|
+
childLog.provider,
|
|
623
|
+
childLog.providerAdapter ?? attempt.adapter,
|
|
624
|
+
childLog.accountLogLabel,
|
|
625
|
+
);
|
|
626
|
+
attemptRetained = true;
|
|
627
|
+
noteComboSuccess(comboId, combo, pick.target, pick.writerGeneration);
|
|
628
|
+
Object.assign(logCtx, childLog, {
|
|
629
|
+
requestedModel,
|
|
630
|
+
model: requestedModel,
|
|
631
|
+
provider: "combo",
|
|
632
|
+
comboId,
|
|
633
|
+
routeDecision: logCtx.routeDecision,
|
|
634
|
+
attempts: logCtx.attempts,
|
|
635
|
+
activeAttempt: attempt,
|
|
636
|
+
activeAttemptStartedAt: started,
|
|
637
|
+
resolvedModel: childLog.resolvedModel ?? childLog.model,
|
|
638
|
+
});
|
|
639
|
+
options.onCodexAuthContextResolved?.(resolvedAuth);
|
|
640
|
+
options.setTerminalOutcomeRecorder?.(terminalRecorder);
|
|
641
|
+
callbackGate.commit();
|
|
642
|
+
return response;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
callbackGate.discard();
|
|
646
|
+
if (response.status === 499) {
|
|
647
|
+
retainCancelledAttempt();
|
|
648
|
+
return clientCancelledResponse();
|
|
649
|
+
}
|
|
650
|
+
let failure: ConsumedComboFailure;
|
|
651
|
+
try {
|
|
652
|
+
failure = consumedChildFailure
|
|
653
|
+
?? await consumeComboFailure(response, options.abortSignal);
|
|
654
|
+
} catch (error) {
|
|
655
|
+
if (options.abortSignal?.aborted) {
|
|
656
|
+
retainCancelledAttempt();
|
|
657
|
+
return clientCancelledResponse();
|
|
658
|
+
}
|
|
659
|
+
finishRequestAttempt(attempt, 502, Date.now() - started, childLog.usage);
|
|
660
|
+
throw error;
|
|
661
|
+
}
|
|
662
|
+
if (options.abortSignal?.aborted) {
|
|
663
|
+
retainCancelledAttempt();
|
|
664
|
+
return clientCancelledResponse();
|
|
665
|
+
}
|
|
666
|
+
sealRequestAttemptIdentity(
|
|
667
|
+
attempt,
|
|
668
|
+
childLog.provider,
|
|
669
|
+
childLog.providerAdapter ?? attempt.adapter,
|
|
670
|
+
childLog.accountLogLabel,
|
|
671
|
+
);
|
|
672
|
+
finishRequestAttempt(
|
|
673
|
+
attempt,
|
|
674
|
+
failure.response.status,
|
|
675
|
+
Date.now() - started,
|
|
676
|
+
failure.usage,
|
|
677
|
+
);
|
|
678
|
+
attemptRetained = true;
|
|
679
|
+
lastFailure = failure.response;
|
|
680
|
+
lastFailedChildLog = childLog;
|
|
681
|
+
const failureDecision = comboFailureDecision(failure.response.status, failure.classificationText, {
|
|
682
|
+
code: failure.upstreamCode,
|
|
683
|
+
});
|
|
684
|
+
const wantsStream = (rawBody as { stream?: unknown } | null)?.stream === true;
|
|
685
|
+
// Local byte admission has its own diagnostic; do not relabel it as an upstream refusal.
|
|
686
|
+
const classifyOverflow = failure.response.status === 413
|
|
687
|
+
&& (wantsStream || (failure.upstreamCode !== "outbound_body_too_large"
|
|
688
|
+
&& failure.upstreamCode !== "translation_buffer_limit"));
|
|
689
|
+
lastFailureClassifiesOverflow = classifyOverflow;
|
|
690
|
+
if (storedPool401ReplayDispatched) {
|
|
691
|
+
if (failureDecision === "hop" && unreadableEncryptedAgentTask && !comboPayloadReadable) {
|
|
692
|
+
const recoveredTarget = await pickWithWait({
|
|
693
|
+
exclude: pick.attempted,
|
|
694
|
+
eligible: target => {
|
|
695
|
+
try {
|
|
696
|
+
const route = routeConcreteModel(config, `${target.provider}/${target.model}`);
|
|
697
|
+
return route.codexAccountMode === undefined
|
|
698
|
+
&& !isCanonicalOpenAiForwardProvider(route.provider);
|
|
699
|
+
} catch {
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
},
|
|
703
|
+
});
|
|
704
|
+
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
705
|
+
if (recoveredTarget && await recoverUnreadableEncryptedTask()) {
|
|
706
|
+
pick = recoveredTarget;
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
710
|
+
}
|
|
711
|
+
// Keep the spent Pool budget sticky even after a recovered routed child:
|
|
712
|
+
// no later failure may reopen ordinary combo/native account hopping.
|
|
713
|
+
adoptFailedChildLog(childLog);
|
|
714
|
+
if (classifyOverflow && failureDecision === "stop") {
|
|
715
|
+
return wantsStream
|
|
716
|
+
? streamingContextOverflowResponse(requestedModel, options.translatorBudget)
|
|
717
|
+
: jsonContextOverflowResponse();
|
|
718
|
+
}
|
|
719
|
+
return lastFailure;
|
|
720
|
+
}
|
|
721
|
+
if (failureDecision === "stop") {
|
|
722
|
+
adoptFailedChildLog(childLog);
|
|
723
|
+
if (classifyOverflow) {
|
|
724
|
+
return wantsStream
|
|
725
|
+
? streamingContextOverflowResponse(requestedModel, options.translatorBudget)
|
|
726
|
+
: jsonContextOverflowResponse();
|
|
727
|
+
}
|
|
728
|
+
return lastFailure;
|
|
729
|
+
}
|
|
730
|
+
console.warn(
|
|
731
|
+
`[combo] ${comboId}: ${targetKey(pick.target)} failed with ${failure.response.status} after ${Date.now() - started}ms`,
|
|
732
|
+
);
|
|
733
|
+
const failureNow = Date.now();
|
|
734
|
+
const attemptedTargets = pick.attempted;
|
|
735
|
+
const nextPick = advanceComboAfterFailure(config, pick, {
|
|
736
|
+
retryAfter: failure.retryAfter,
|
|
737
|
+
resetAt: failure.resetAt,
|
|
738
|
+
cooldownMs: combo.cooldownMs,
|
|
739
|
+
now: failureNow,
|
|
740
|
+
cooldownScope: comboFailureCooldownScope(failure.response.status, failure.classificationText, {
|
|
741
|
+
code: failure.upstreamCode,
|
|
742
|
+
}),
|
|
743
|
+
eligible: targetEligible,
|
|
744
|
+
status: failure.response.status,
|
|
745
|
+
code: failure.upstreamCode,
|
|
746
|
+
message: failure.classificationText,
|
|
747
|
+
});
|
|
748
|
+
if (nextPick) {
|
|
749
|
+
pick = nextPick;
|
|
750
|
+
} else {
|
|
751
|
+
pick = await pickWithWait({
|
|
752
|
+
exclude: pick.attempted,
|
|
753
|
+
eligible: targetEligible,
|
|
754
|
+
now: failureNow,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
if (!pick) {
|
|
758
|
+
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
759
|
+
if (onlyReplayIncompatibleTargetsRemain(attemptedTargets)) {
|
|
760
|
+
adoptFailedChildLog(childLog);
|
|
761
|
+
return targetIncompatibleResponse();
|
|
762
|
+
}
|
|
763
|
+
if (unreadableEncryptedAgentTask && !comboPayloadReadable) {
|
|
764
|
+
const recoveredTarget = await pickWithWait({
|
|
765
|
+
exclude: attemptedTargets,
|
|
766
|
+
now: failureNow,
|
|
767
|
+
});
|
|
768
|
+
if (recoveredTarget && await recoverUnreadableEncryptedTask()) {
|
|
769
|
+
pick = recoveredTarget;
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
// Waiting or recovery may have observed cancellation after the check above.
|
|
774
|
+
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
775
|
+
adoptFailedChildLog(childLog);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (
|
|
779
|
+
lastFailure?.status === 413
|
|
780
|
+
&& lastFailureClassifiesOverflow
|
|
781
|
+
) {
|
|
782
|
+
return (rawBody as { stream?: unknown } | null)?.stream === true
|
|
783
|
+
? streamingContextOverflowResponse(requestedModel, options.translatorBudget)
|
|
784
|
+
: jsonContextOverflowResponse();
|
|
785
|
+
}
|
|
786
|
+
return lastFailure!;
|
|
787
|
+
}
|