@bitkyc08/opencodex 2.65.0-preview.20260925 → 2.66.0-preview.20260925
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/README.md +14 -0
- package/gui/dist/assets/App-BVjA0T6O.js +51 -0
- package/gui/dist/assets/App-gkoJIEOV.css +1 -0
- package/gui/dist/assets/{Tray-DUvc_Wul.js → Tray-BCFtiWwD.js} +1 -1
- package/gui/dist/assets/index-ComVsdSk.css +1 -0
- package/gui/dist/assets/index-vO4dgfiU.js +86 -0
- package/gui/dist/assets/{tray-data-f0lTZ4sF.js → tray-data-C4FiNaxk.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +80 -0
- package/src/adapters/anthropic/passthrough.ts +221 -0
- package/src/adapters/anthropic-image-codec.ts +56 -19
- package/src/adapters/anthropic-image-normalize.ts +20 -10
- package/src/adapters/anthropic.ts +55 -24
- package/src/adapters/coding-agent/protocol.ts +29 -19
- package/src/adapters/cursor/live-transport.ts +18 -3
- package/src/adapters/cursor/native-exec-shell.ts +18 -63
- package/src/adapters/cursor/native-exec.ts +5 -2
- package/src/adapters/cursor/native-foreground-shell.ts +44 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -1
- package/src/adapters/exec-tool-result-normalize.ts +4 -1
- package/src/adapters/google.ts +12 -0
- package/src/adapters/ollama-native.ts +32 -1
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +131 -34
- package/src/adapters/openai-chat.ts +6 -4
- package/src/adapters/openai-responses/passthrough.ts +1 -0
- package/src/adapters/openai-responses/reasoning.ts +45 -1
- package/src/bridge/response-json.ts +10 -2
- package/src/chat/outbound.ts +104 -52
- package/src/claude/claude-code-block.ts +16 -0
- package/src/claude/context-windows.ts +52 -4
- package/src/claude/desktop-first-party.ts +25 -8
- package/src/claude/inbound.ts +22 -0
- package/src/claude/intercept/connect-proxy.ts +37 -4
- package/src/claude/intercept/proxy-auth.ts +166 -0
- package/src/claude/intercept/runtime.ts +21 -0
- package/src/claude/intercept/settings.ts +33 -12
- package/src/claude/outbound.ts +45 -27
- package/src/cli/account-extended.ts +47 -15
- package/src/cli/account-target.ts +115 -0
- package/src/cli/account.ts +20 -28
- package/src/cli/agent.ts +10 -1
- package/src/cli/api-protocols.ts +206 -0
- package/src/cli/capabilities.ts +86 -0
- package/src/cli/combo.ts +2 -2
- package/src/cli/connect.ts +127 -3
- package/src/cli/dispatch.ts +8 -0
- package/src/cli/doctor.ts +33 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/link.ts +288 -0
- package/src/cli/registry.ts +23 -0
- package/src/cli/runtime-api.ts +79 -0
- package/src/cli/system-command.ts +21 -2
- package/src/client/connect.ts +125 -23
- package/src/client/hub-relay.ts +12 -8
- package/src/client/link-join.ts +323 -0
- package/src/client/link-relay.ts +241 -0
- package/src/client/link-state.ts +110 -0
- package/src/client/link-teardown.ts +63 -0
- package/src/client/link-tunnel.ts +465 -0
- package/src/client/machine-listener.ts +14 -5
- package/src/client/runtime.ts +91 -42
- package/src/client/state.ts +4 -0
- package/src/codex/catalog/build-entries.ts +4 -0
- package/src/codex/catalog/derive-entry.ts +5 -0
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +4 -1
- package/src/codex/desktop-switches.ts +88 -8
- package/src/codex/inject/plan.ts +16 -3
- package/src/codex/inject.ts +3 -0
- package/src/codex/internal/catalog-writer.ts +13 -2
- package/src/combos/failover.ts +3 -2
- package/src/combos/index.ts +12 -0
- package/src/combos/jev.ts +646 -0
- package/src/combos/request.ts +5 -0
- package/src/combos/resolve.ts +17 -15
- package/src/combos/types.ts +47 -3
- package/src/config/atomic-write.ts +3 -0
- package/src/config/live-reconcile.ts +20 -4
- package/src/config/persist-unlocked.ts +20 -7
- package/src/config/schema/config-schema.ts +16 -0
- package/src/config/schema/leaf-validators.ts +38 -1
- package/src/generated/compatibility-version.json +416 -128
- package/src/integrations/omp-yaml-source.ts +109 -4
- package/src/lab/automation/orchestrator.ts +60 -4
- package/src/lab/events/limits.ts +1 -1
- package/src/lib/bounded-body.ts +55 -38
- package/src/lib/config-ownership.ts +11 -22
- package/src/lib/local-desktop-snapshot-capability.ts +56 -0
- package/src/lib/local-management-capability.ts +25 -3
- package/src/lib/optional-shutdown-hooks.ts +17 -0
- package/src/lib/state-store-registrations.ts +2 -2
- package/src/lib/windows-elevation.ts +62 -25
- package/src/lib/windows-secret-acl.ts +114 -23
- package/src/link/admission-wait.ts +73 -0
- package/src/link/compensation.ts +100 -0
- package/src/link/fingerprint.ts +21 -0
- package/src/link/paths.ts +19 -0
- package/src/link/ports.ts +12 -0
- package/src/link/routes.ts +32 -0
- package/src/link/ssh-argv.ts +170 -0
- package/src/link/ssh-config.ts +152 -0
- package/src/link/ssh-runner.ts +161 -0
- package/src/link/status-projection.ts +96 -0
- package/src/link/store.ts +139 -0
- package/src/link/supervisor.ts +404 -0
- package/src/link/tunnel-state.ts +91 -0
- package/src/protocols/baseline.ts +88 -0
- package/src/protocols/codecs/chat.ts +17 -0
- package/src/protocols/codecs/messages.ts +18 -0
- package/src/protocols/codecs/responses.ts +17 -0
- package/src/protocols/contract.ts +185 -0
- package/src/protocols/dto.ts +267 -0
- package/src/protocols/encoders/adapter-events.ts +876 -0
- package/src/protocols/encoders/chat.ts +243 -0
- package/src/protocols/encoders/messages.ts +462 -0
- package/src/protocols/envelope.ts +43 -0
- package/src/protocols/features.ts +295 -0
- package/src/protocols/guard.ts +33 -0
- package/src/protocols/opaque-state.ts +132 -0
- package/src/protocols/path.ts +39 -0
- package/src/protocols/plan-snapshot.ts +226 -0
- package/src/protocols/plan.ts +192 -0
- package/src/protocols/provider-summary.ts +101 -0
- package/src/protocols/settings.ts +118 -0
- package/src/protocols/shadow-plan.ts +36 -0
- package/src/protocols/shadow.ts +62 -0
- package/src/protocols/trace.ts +335 -0
- package/src/providers/model-rename-migration.ts +3 -3
- package/src/providers/registry/entries-core.ts +6 -1
- package/src/providers/registry/entries-extended.ts +15 -0
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/types.ts +6 -0
- package/src/providers/registry.ts +1 -1
- package/src/remote-control/workspace-hub.ts +5 -4
- package/src/responses/apply-patch-envelope.ts +6 -1
- package/src/responses/code-mode-shell-input.ts +4 -2
- package/src/responses/freeform-wrapper-scan.ts +51 -24
- package/src/responses/progressive-freeform-input.ts +14 -20
- package/src/responses/spill-store.ts +187 -33
- package/src/responses/state/snapshot-select.ts +49 -0
- package/src/responses/state/spill-inspect.ts +46 -0
- package/src/responses/state/spill-queue.ts +16 -0
- package/src/responses/state.ts +51 -27
- package/src/server/audio-client.ts +8 -3
- package/src/server/audio-upstream.ts +9 -4
- package/src/server/auth-cors.ts +25 -8
- package/src/server/chat-completions.ts +71 -11
- package/src/server/chat-native-eligibility.ts +74 -0
- package/src/server/chat-native.ts +139 -74
- package/src/server/claude-messages.ts +140 -22
- package/src/server/hub-usage.ts +4 -3
- package/src/server/index/link-listener.ts +195 -0
- package/src/server/index/optional-listeners.ts +115 -0
- package/src/server/index/serve-options.ts +44 -10
- package/src/server/index.ts +10 -9
- package/src/server/inference/attempt.ts +51 -0
- package/src/server/inference/client-encoder-delivery.ts +235 -0
- package/src/server/inference/client-wire-log.ts +49 -0
- package/src/server/inference/client-wire.ts +69 -0
- package/src/server/inference/context.ts +15 -0
- package/src/server/inference/final-log.ts +37 -0
- package/src/server/local-desktop-snapshot-auth.ts +57 -0
- package/src/server/management/agent-settings-routes.ts +15 -13
- package/src/server/management/api-access.ts +11 -1
- package/src/server/management/config-routes.ts +3 -6
- package/src/server/management/context.ts +25 -0
- package/src/server/management/link-routes.ts +535 -0
- package/src/server/management/logs-usage-routes.ts +31 -1
- package/src/server/management/native-integration-routes.ts +6 -11
- package/src/server/management/oauth-account-routes.ts +42 -16
- package/src/server/management/protocol-routes.ts +159 -0
- package/src/server/management/protocol-settings-patch.ts +145 -0
- package/src/server/management/provider-patch-transaction.ts +61 -0
- package/src/server/management/provider-routes.ts +39 -18
- package/src/server/management/route-registry.ts +12 -0
- package/src/server/management/sidebar-routes.ts +8 -3
- package/src/server/management/system-routes.ts +18 -1
- package/src/server/management/usage-aggregate-cache.ts +206 -6
- package/src/server/management-api.ts +26 -1
- package/src/server/management-auth.ts +19 -5
- package/src/server/messages-native-eligibility.ts +161 -0
- package/src/server/messages-native-oauth.ts +149 -0
- package/src/server/messages-native.ts +790 -0
- package/src/server/relay.ts +9 -0
- package/src/server/request-log-filter.ts +92 -0
- package/src/server/request-log.ts +31 -70
- package/src/server/responses/adapter-delivery.ts +43 -16
- package/src/server/responses/agent-task-recovery.ts +90 -22
- package/src/server/responses/codex-ws-exchange.ts +4 -0
- package/src/server/responses/codex-ws-wire.ts +10 -1
- package/src/server/responses/core-combo-native.ts +323 -0
- package/src/server/responses/core-combo.ts +224 -13
- package/src/server/responses/core-options.ts +22 -0
- package/src/server/responses/core.ts +14 -26
- package/src/server/responses/request-sidecar-auth.ts +8 -3
- package/src/server/responses/run-turn-execution.ts +257 -63
- package/src/server/responses/sidecar-execution.ts +7 -4
- package/src/server/system-env.ts +7 -0
- package/src/service/diagnostics.ts +7 -1
- package/src/service/launchd.ts +26 -37
- package/src/service/windows-ops.ts +24 -20
- package/src/types/config.ts +58 -1
- package/src/types.ts +2 -0
- package/src/update/transactional-install.d.mts +0 -1
- package/src/update/transactional-install.mjs +17 -17
- package/src/usage/jev-stats.ts +495 -0
- package/src/usage/log.ts +24 -3
- package/src/web-search/run-turn-loop.ts +568 -0
- package/gui/dist/assets/App-8NMiZxT0.css +0 -1
- package/gui/dist/assets/App-CsYvvpr3.js +0 -50
- package/gui/dist/assets/index--EWgGQvZ.css +0 -1
- package/gui/dist/assets/index-BB0iHG8-.js +0 -86
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native Chat candidates inside the combo loop (PF-07).
|
|
3
|
+
*
|
|
4
|
+
* The Chat ingress hands the combo a `ComboProtocolSource` only for a combo route with
|
|
5
|
+
* `protocols.rollout.nativeChatCombos` on; without one nothing here runs. For the candidate the
|
|
6
|
+
* combo is about to dispatch, the concrete route is settled the way the Chat ingress settles a
|
|
7
|
+
* single route, and the native Chat lane's own eligibility rule decides whether the child goes
|
|
8
|
+
* native. A native child runs `runNativeChatAttempt` (through the source) on the attempt the combo
|
|
9
|
+
* already opened, with the combo's per-target budget, the client's abort signal and the turn
|
|
10
|
+
* lease, built from a fresh copy of the source body; every other candidate keeps the bridge.
|
|
11
|
+
* Under `unrepresentable: "reject"` a candidate whose path would drop a requested feature is never
|
|
12
|
+
* picked, and a combo whose every enabled candidate is skipped answers the ingress refusal.
|
|
13
|
+
*
|
|
14
|
+
* No `./` sibling import on purpose: `core-options.ts` names this module's types, so an edge back
|
|
15
|
+
* into the owner graph would close a cycle.
|
|
16
|
+
*/
|
|
17
|
+
import type { OcxConfig } from "../../types";
|
|
18
|
+
import type { RouteResult } from "../../router";
|
|
19
|
+
import type { DataPlaneAdmission } from "../auth-cors";
|
|
20
|
+
import type { AdmissionLease } from "../../lib/admission";
|
|
21
|
+
import type { RequestExecutionBudget } from "../../lib/request-execution-budget";
|
|
22
|
+
import type { TransientSendBudget } from "../../lib/upstream-retry";
|
|
23
|
+
import type { ResponsesTerminalStatus } from "../../bridge";
|
|
24
|
+
import type { NativeChatFinishLog } from "../chat-native";
|
|
25
|
+
import type { InferenceAttempt } from "../inference/attempt";
|
|
26
|
+
import type { ProtocolEnvelope } from "../../protocols/envelope";
|
|
27
|
+
import type { ProtocolFeature } from "../../protocols/features";
|
|
28
|
+
import type { PersistedUsageAttempt } from "../../usage/log";
|
|
29
|
+
import {
|
|
30
|
+
finishRequestAttempt,
|
|
31
|
+
sealRequestAttemptIdentity,
|
|
32
|
+
type RequestLogContext,
|
|
33
|
+
} from "../request-log";
|
|
34
|
+
import { captureRouteStaticPolicy, routeConcreteModel } from "../../router";
|
|
35
|
+
import { comboDefaultEffort, concreteComboRequestBody, getCombo } from "../../combos";
|
|
36
|
+
import { supportedLadderFor } from "../effort-policy";
|
|
37
|
+
import { resolveWireProtocolOverride } from "../adapter-resolve";
|
|
38
|
+
import { resolveOpenCodeGoTransport } from "../../providers/opencode-go-transport";
|
|
39
|
+
import { getOrAllocateRequestSessionLane } from "../request-log-conversation";
|
|
40
|
+
import { assertRouteAllowedByScope, resolveAdmissionModelScope } from "../admission-model-scope";
|
|
41
|
+
import { isNativeChatRouteEligible } from "../chat-native-eligibility";
|
|
42
|
+
import { chatCompletionsErrorResponse } from "../../chat/outbound";
|
|
43
|
+
import { isRequestExecutionBudget } from "../../lib/request-execution-budget";
|
|
44
|
+
import { markResponseNonReplayable } from "../../lib/upstream-retry";
|
|
45
|
+
import { redactSecretString } from "../../lib/redact";
|
|
46
|
+
import { upstreamWireForAdapter } from "../../protocols/contract";
|
|
47
|
+
import { checkRepresentable, unrepresentableMessage } from "../../protocols/guard";
|
|
48
|
+
import { deliveryModeForLane, requestPathForLane } from "../../protocols/path";
|
|
49
|
+
import { resolveProtocolSettings } from "../../protocols/settings";
|
|
50
|
+
import { addProtocolEntryReason, markAttemptProtocolPath, markProtocolBlocked } from "../../protocols/trace";
|
|
51
|
+
import { markClientWire } from "../inference/client-wire";
|
|
52
|
+
|
|
53
|
+
type Rec = Record<string, unknown>;
|
|
54
|
+
type ComboTarget = { provider: string; model: string };
|
|
55
|
+
|
|
56
|
+
/** What the source needs to run one native child on the combo's open attempt. */
|
|
57
|
+
export interface NativeComboChildRun {
|
|
58
|
+
route: RouteResult;
|
|
59
|
+
/** A fresh copy of the source Chat body, never another candidate's rewritten one. */
|
|
60
|
+
body: Rec;
|
|
61
|
+
childLog: RequestLogContext;
|
|
62
|
+
attemptHandle: InferenceAttempt;
|
|
63
|
+
sendBudget: RequestExecutionBudget;
|
|
64
|
+
turnAdmissionLease?: AdmissionLease;
|
|
65
|
+
onFirstOutput: () => void;
|
|
66
|
+
/** Reports to the combo's child callbacks; it never writes the parent's final row itself. */
|
|
67
|
+
finishLog: NativeChatFinishLog;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Supplied by the Chat ingress for a combo route when `nativeChatCombos` is on. */
|
|
71
|
+
export interface ComboProtocolSource {
|
|
72
|
+
readonly inbound: "chat";
|
|
73
|
+
readonly envelope: ProtocolEnvelope;
|
|
74
|
+
/** Runs `runNativeChatAttempt`; the response it returns is in the Chat wire. */
|
|
75
|
+
dispatchNativeChild(input: NativeComboChildRun): Promise<Response>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The native plan for one dispatch: the settled route and its own fresh body. */
|
|
79
|
+
export interface NativeComboChildPlan {
|
|
80
|
+
route: RouteResult;
|
|
81
|
+
body: Rec;
|
|
82
|
+
sendBudget: RequestExecutionBudget;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ComboProtocolLanes {
|
|
86
|
+
/** Pick predicate. False only for a candidate the `reject` policy skips. */
|
|
87
|
+
pickable(target: ComboTarget): boolean;
|
|
88
|
+
/** The ingress refusal when every enabled candidate was skipped; otherwise undefined. */
|
|
89
|
+
refusal(): Response | undefined;
|
|
90
|
+
/** The native plan for the candidate about to dispatch, or undefined to keep the bridge. */
|
|
91
|
+
nativeChild(
|
|
92
|
+
target: ComboTarget,
|
|
93
|
+
targetRoute: RouteResult,
|
|
94
|
+
targetSendBudget: TransientSendBudget | undefined,
|
|
95
|
+
): NativeComboChildPlan | undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface CandidateVerdict {
|
|
99
|
+
skip?: ProtocolFeature[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function createComboProtocolLanes(input: {
|
|
103
|
+
source: ComboProtocolSource | undefined;
|
|
104
|
+
req: Request;
|
|
105
|
+
config: OcxConfig;
|
|
106
|
+
logCtx: RequestLogContext;
|
|
107
|
+
admission: DataPlaneAdmission | undefined;
|
|
108
|
+
comboId: string;
|
|
109
|
+
targets: readonly ComboTarget[];
|
|
110
|
+
}): ComboProtocolLanes | undefined {
|
|
111
|
+
const { source, req, config, logCtx, admission, comboId, targets } = input;
|
|
112
|
+
if (!source) return undefined;
|
|
113
|
+
const envelope = source.envelope;
|
|
114
|
+
const reject = resolveProtocolSettings(config).unrepresentable === "reject";
|
|
115
|
+
const selector = (target: ComboTarget): string => `${target.provider}/${target.model}`;
|
|
116
|
+
|
|
117
|
+
// The Chat ingress's own settlement of a single route, applied to the concrete target: the
|
|
118
|
+
// key's model scope, the static policy captured for a Chat inbound, the wire override and the
|
|
119
|
+
// OpenCode Go transport. A refusal here keeps the bridge, which reports it in its own shape.
|
|
120
|
+
const settle = (target: ComboTarget, targetRoute: RouteResult): RouteResult | undefined => {
|
|
121
|
+
try {
|
|
122
|
+
const route: RouteResult = { ...targetRoute };
|
|
123
|
+
assertRouteAllowedByScope(resolveAdmissionModelScope(config, admission), selector(target), route);
|
|
124
|
+
const routedProvider = route.provider;
|
|
125
|
+
route.staticPolicy = captureRouteStaticPolicy(
|
|
126
|
+
route.providerName, route.modelId, routedProvider, route.staticPolicy.effectiveAlias, "chat",
|
|
127
|
+
);
|
|
128
|
+
const wireProvider = resolveWireProtocolOverride(route.providerName, route.modelId, routedProvider, "chat", route.staticPolicy);
|
|
129
|
+
route.provider = resolveOpenCodeGoTransport(wireProvider, getOrAllocateRequestSessionLane(req), routedProvider);
|
|
130
|
+
return route;
|
|
131
|
+
} catch {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Eligibility reads a body, and a body is a charged copy, so the adapter is checked first:
|
|
137
|
+
// a candidate that can never go native costs no copy at all.
|
|
138
|
+
const nativeBody = (target: ComboTarget, route: RouteResult | undefined): Rec | undefined => {
|
|
139
|
+
if (!route || route.provider.adapter !== "openai-chat") return undefined;
|
|
140
|
+
const body = envelope.freshBody();
|
|
141
|
+
// The concrete selector, as the bridge child's body carries it; the wire model comes from
|
|
142
|
+
// the route either way, and the pinned-effort lookup reads this.
|
|
143
|
+
body.model = selector(target);
|
|
144
|
+
return isNativeChatRouteEligible(route, body, config) ? body : undefined;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const verdicts = new Map<string, CandidateVerdict>();
|
|
148
|
+
let skipRecorded = false;
|
|
149
|
+
const judge = (target: ComboTarget): CandidateVerdict => {
|
|
150
|
+
const key = selector(target);
|
|
151
|
+
const cached = verdicts.get(key);
|
|
152
|
+
if (cached) return cached;
|
|
153
|
+
let verdict: CandidateVerdict = {};
|
|
154
|
+
let targetRoute: RouteResult | undefined;
|
|
155
|
+
try {
|
|
156
|
+
targetRoute = routeConcreteModel(config, key);
|
|
157
|
+
} catch {
|
|
158
|
+
// Not evidence about the path; dispatch keeps the existing routing failure surface.
|
|
159
|
+
targetRoute = undefined;
|
|
160
|
+
}
|
|
161
|
+
if (targetRoute) {
|
|
162
|
+
const settled = settle(target, targetRoute);
|
|
163
|
+
const native = nativeBody(target, settled) !== undefined;
|
|
164
|
+
const upstream = native ? "chat" : upstreamWireForAdapter((settled ?? targetRoute).provider.adapter);
|
|
165
|
+
const checked = checkRepresentable({
|
|
166
|
+
inbound: "chat",
|
|
167
|
+
requestPath: requestPathForLane("chat", native ? "native" : "bridge", upstream),
|
|
168
|
+
features: envelope.features(),
|
|
169
|
+
policy: "reject",
|
|
170
|
+
});
|
|
171
|
+
if (!checked.ok) {
|
|
172
|
+
verdict = { skip: checked.features };
|
|
173
|
+
if (!skipRecorded) {
|
|
174
|
+
skipRecorded = true;
|
|
175
|
+
addProtocolEntryReason(logCtx, "feature-unrepresentable");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
verdicts.set(key, verdict);
|
|
180
|
+
return verdict;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
pickable: target => !reject || judge(target).skip === undefined,
|
|
185
|
+
refusal: () => {
|
|
186
|
+
if (!reject) return undefined;
|
|
187
|
+
const enabled = targets.filter(target => {
|
|
188
|
+
const provider = config.providers[target.provider];
|
|
189
|
+
return provider !== undefined && provider.disabled !== true;
|
|
190
|
+
});
|
|
191
|
+
if (enabled.length === 0) return undefined;
|
|
192
|
+
const skipped = enabled.map(judge);
|
|
193
|
+
if (skipped.some(verdict => verdict.skip === undefined)) return undefined;
|
|
194
|
+
const features = [...new Set(skipped.flatMap(verdict => verdict.skip ?? []))];
|
|
195
|
+
// The same refusal the ingress gives a single route: 400 in the Chat shape, feature keys
|
|
196
|
+
// only, a blocked trace, and no upstream send.
|
|
197
|
+
markProtocolBlocked(logCtx, { inbound: "chat", reasonCodes: ["feature-unrepresentable"], features });
|
|
198
|
+
logCtx.errorCode = "unsupported_feature";
|
|
199
|
+
return markClientWire(chatCompletionsErrorResponse(
|
|
200
|
+
400, unrepresentableMessage(features), "invalid_request_error", "unsupported_feature",
|
|
201
|
+
), "chat");
|
|
202
|
+
},
|
|
203
|
+
nativeChild: (target, targetRoute, targetSendBudget) => {
|
|
204
|
+
// Without the request's execution budget a native child could not share its sends, so it
|
|
205
|
+
// keeps the bridge rather than opening a tracker of its own.
|
|
206
|
+
if (!isRequestExecutionBudget(targetSendBudget)) return undefined;
|
|
207
|
+
const route = settle(target, targetRoute);
|
|
208
|
+
const body = nativeBody(target, route);
|
|
209
|
+
if (!route || !body) return undefined;
|
|
210
|
+
applyComboEffort(body, config, comboId, target, targetRoute);
|
|
211
|
+
return { route, body, sendBudget: targetSendBudget };
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The combo's reasoning-effort policy, applied to the Chat spelling of the same field.
|
|
218
|
+
*
|
|
219
|
+
* The bridge child gets it from `concreteComboRequestBody` on the Responses body; a native child
|
|
220
|
+
* must not lose a forced or default effort just because it skipped that body. The same function
|
|
221
|
+
* decides, through a one-field Responses view, so the two lanes cannot disagree.
|
|
222
|
+
*/
|
|
223
|
+
function applyComboEffort(
|
|
224
|
+
body: Rec,
|
|
225
|
+
config: OcxConfig,
|
|
226
|
+
comboId: string,
|
|
227
|
+
target: ComboTarget,
|
|
228
|
+
targetRoute: RouteResult,
|
|
229
|
+
): void {
|
|
230
|
+
const combo = getCombo(config, comboId);
|
|
231
|
+
if (!combo) return;
|
|
232
|
+
const hadEffort = Object.hasOwn(body, "reasoning_effort");
|
|
233
|
+
const shaped = concreteComboRequestBody(
|
|
234
|
+
hadEffort ? { reasoning: { effort: body.reasoning_effort } } : {},
|
|
235
|
+
target,
|
|
236
|
+
comboDefaultEffort(config, comboId),
|
|
237
|
+
supportedLadderFor({ provider: targetRoute.provider, modelId: targetRoute.modelId }),
|
|
238
|
+
combo.reasoningEffortMode,
|
|
239
|
+
combo.defaultEffortMode,
|
|
240
|
+
);
|
|
241
|
+
const reasoning = shaped.reasoning;
|
|
242
|
+
if (reasoning === undefined) {
|
|
243
|
+
if (hadEffort) delete body.reasoning_effort;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const effort = (reasoning as { effort?: unknown }).effort;
|
|
247
|
+
if (typeof effort === "string") body.reasoning_effort = effort;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** The combo's child callbacks, gated so a discarded attempt never reaches the parent. */
|
|
251
|
+
export interface ComboChildCallbacks {
|
|
252
|
+
onTerminal(status: ResponsesTerminalStatus): void;
|
|
253
|
+
onCancel(): void;
|
|
254
|
+
onResponseComplete(model: string): void;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Run one native child on the attempt the combo opened, and mark its answer as Chat wire.
|
|
259
|
+
*
|
|
260
|
+
* The child's outcomes map onto the combo's existing child callbacks: a terminal or cancellation
|
|
261
|
+
* after the response was handed over publishes through the gate (and so reaches the parent's
|
|
262
|
+
* final-row owner only once the combo commits), while a failure answered before that is left to
|
|
263
|
+
* the combo's ordinary failure path. A non-OK answer produced after output was observed is marked
|
|
264
|
+
* non-replayable, so the combo stops instead of re-running a turn that already ran upstream.
|
|
265
|
+
*/
|
|
266
|
+
export async function dispatchNativeComboChild(input: {
|
|
267
|
+
source: ComboProtocolSource;
|
|
268
|
+
plan: NativeComboChildPlan;
|
|
269
|
+
logCtx: RequestLogContext;
|
|
270
|
+
childLog: RequestLogContext;
|
|
271
|
+
attempt: PersistedUsageAttempt;
|
|
272
|
+
startedAt: number;
|
|
273
|
+
turnAdmissionLease?: AdmissionLease;
|
|
274
|
+
onFirstOutput: () => void;
|
|
275
|
+
callbacks: ComboChildCallbacks;
|
|
276
|
+
}): Promise<Response> {
|
|
277
|
+
const { plan, logCtx, childLog, attempt, startedAt, callbacks } = input;
|
|
278
|
+
childLog.providerAdapter = plan.route.provider.adapter;
|
|
279
|
+
markAttemptProtocolPath(attempt, {
|
|
280
|
+
mode: deliveryModeForLane("chat", "native", "chat"),
|
|
281
|
+
requestPath: requestPathForLane("chat", "native", "chat"),
|
|
282
|
+
});
|
|
283
|
+
let outputSeen = false;
|
|
284
|
+
const finishLog: NativeChatFinishLog = (status, message, closeReason = "non_stream") => {
|
|
285
|
+
if (message) childLog.upstreamError = redactSecretString(message).slice(0, 500);
|
|
286
|
+
if (closeReason !== "non_stream") {
|
|
287
|
+
// A streamed body ends after the combo merged this child into the parent row, so what the
|
|
288
|
+
// stream learned last is carried across here before the parent's row is written.
|
|
289
|
+
if (childLog.usage !== undefined) logCtx.usage = childLog.usage;
|
|
290
|
+
if (closeReason === "client_cancel") return callbacks.onCancel();
|
|
291
|
+
}
|
|
292
|
+
if (status < 400) {
|
|
293
|
+
callbacks.onTerminal("completed");
|
|
294
|
+
callbacks.onResponseComplete(plan.route.modelId);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (closeReason === "terminal") {
|
|
298
|
+
childLog.terminalHttpStatus = status;
|
|
299
|
+
callbacks.onTerminal("failed");
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
const attemptHandle: InferenceAttempt = {
|
|
303
|
+
attempt,
|
|
304
|
+
startedAt,
|
|
305
|
+
seal: label => sealRequestAttemptIdentity(attempt, childLog.provider, childLog.providerAdapter ?? attempt.adapter, label),
|
|
306
|
+
finish: (status, usage) => finishRequestAttempt(attempt, status, Date.now() - startedAt, usage),
|
|
307
|
+
};
|
|
308
|
+
const response = await input.source.dispatchNativeChild({
|
|
309
|
+
route: plan.route,
|
|
310
|
+
body: plan.body,
|
|
311
|
+
childLog,
|
|
312
|
+
attemptHandle,
|
|
313
|
+
sendBudget: plan.sendBudget,
|
|
314
|
+
...(input.turnAdmissionLease ? { turnAdmissionLease: input.turnAdmissionLease } : {}),
|
|
315
|
+
onFirstOutput: () => {
|
|
316
|
+
outputSeen = true;
|
|
317
|
+
input.onFirstOutput();
|
|
318
|
+
},
|
|
319
|
+
finishLog,
|
|
320
|
+
});
|
|
321
|
+
if (!response.ok && outputSeen) markResponseNonReplayable(response);
|
|
322
|
+
return markClientWire(response, "chat");
|
|
323
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
isCodexReasoningEffort,
|
|
3
|
+
isDeclaredReasoningEffort,
|
|
4
|
+
resolveEffortAtOrBelow,
|
|
5
|
+
} from "../../reasoning-effort";
|
|
2
6
|
import { recordAttemptRequestedEffort } from "../request-log";
|
|
3
7
|
import {
|
|
4
8
|
CODEX_TEXT_GUARDED_BUDGET_POLICY,
|
|
@@ -9,13 +13,14 @@ import type {
|
|
|
9
13
|
RequestExecutionBudgetPolicy,
|
|
10
14
|
RequestExecutionBudget,
|
|
11
15
|
} from "../../lib/request-execution-budget";
|
|
12
|
-
import type { OcxConfig } from "../../types";
|
|
16
|
+
import type { OcxComboDefaultEffort, OcxConfig } from "../../types";
|
|
13
17
|
import type { RequestLogContext } from "../request-log";
|
|
14
18
|
import type { HandleResponsesOptions, ResponsesDispatchers, ConsumedComboFailure } from "./core-options";
|
|
15
19
|
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
16
20
|
import {
|
|
17
21
|
getCombo,
|
|
18
22
|
comboRequestHasImageInput,
|
|
23
|
+
pickComboTarget,
|
|
19
24
|
pickComboTargetWithWait,
|
|
20
25
|
targetKey,
|
|
21
26
|
concreteComboRequestBody,
|
|
@@ -25,6 +30,11 @@ import {
|
|
|
25
30
|
comboFailureDecision,
|
|
26
31
|
advanceComboAfterFailure,
|
|
27
32
|
comboFailureCooldownScope,
|
|
33
|
+
JEV_PROVIDER_ID,
|
|
34
|
+
resolveJevDecision,
|
|
35
|
+
type ComboPick,
|
|
36
|
+
type JevCandidate,
|
|
37
|
+
type JevDecision,
|
|
28
38
|
} from "../../combos";
|
|
29
39
|
import { formatErrorResponse } from "../../bridge";
|
|
30
40
|
import { SEND_BUDGET_EXHAUSTED_CODE } from "../../lib/errors";
|
|
@@ -65,6 +75,7 @@ import type { ResponsesTerminalStatus } from "../../bridge";
|
|
|
65
75
|
import { beginRequestAttempt, sealRequestAttemptIdentity, finishRequestAttempt } from "../request-log";
|
|
66
76
|
import { rememberComboForLane } from "./combo-session-recall";
|
|
67
77
|
import { runTurnAdapterSseResponses } from "./core-lifetime";
|
|
78
|
+
import { normalizePersistedJevDecision } from "../../usage/jev-stats";
|
|
68
79
|
import {
|
|
69
80
|
isNativePassthroughSseResponse,
|
|
70
81
|
isEagerRelaySseResponse,
|
|
@@ -75,6 +86,8 @@ import { preflightComboStreamResponse } from "./combo-stream-preflight";
|
|
|
75
86
|
import { streamingContextOverflowResponse, jsonContextOverflowResponse } from "./context-overflow";
|
|
76
87
|
import { mandatoryResponsesReasoningReplayUnavailable } from "./core-replay";
|
|
77
88
|
import { settleOperatorReplacement } from "../../lib/upstream-retry";
|
|
89
|
+
import { createComboProtocolLanes, dispatchNativeComboChild } from "./core-combo-native";
|
|
90
|
+
import { clientWireOf } from "../inference/client-wire";
|
|
78
91
|
|
|
79
92
|
/**
|
|
80
93
|
* Sends one combo target may run on its own before the ladder moves on. A target is a whole
|
|
@@ -166,6 +179,65 @@ export function comboTargetSendBudget(
|
|
|
166
179
|
});
|
|
167
180
|
}
|
|
168
181
|
|
|
182
|
+
interface JevComboChoice {
|
|
183
|
+
pick: ComboPick;
|
|
184
|
+
candidate: JevCandidate;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Enumerate the current ordinary Combo eligibility set without retaining attempted picks. */
|
|
188
|
+
function eligibleJevComboChoices(
|
|
189
|
+
config: OcxConfig,
|
|
190
|
+
comboId: string,
|
|
191
|
+
eligible: (target: NonNullable<ReturnType<typeof getCombo>>["targets"][number]) => boolean,
|
|
192
|
+
now: number,
|
|
193
|
+
): JevComboChoice[] {
|
|
194
|
+
const combo = getCombo(config, comboId);
|
|
195
|
+
if (!combo) return [];
|
|
196
|
+
const excluded = new Set<string>();
|
|
197
|
+
const choices: JevComboChoice[] = [];
|
|
198
|
+
while (excluded.size < combo.targets.length) {
|
|
199
|
+
const pick = pickComboTarget(config, comboId, { exclude: excluded, eligible, now });
|
|
200
|
+
if (!pick) break;
|
|
201
|
+
const key = targetKey(pick.target);
|
|
202
|
+
excluded.add(key);
|
|
203
|
+
// The TypeSafe row owns a decision credential, not an inference transport.
|
|
204
|
+
if (pick.target.provider === JEV_PROVIDER_ID) continue;
|
|
205
|
+
let ladder: string[] | undefined;
|
|
206
|
+
try {
|
|
207
|
+
const route = routeConcreteModel(config, key);
|
|
208
|
+
ladder = supportedLadderFor({ provider: route.provider, modelId: route.modelId });
|
|
209
|
+
} catch {
|
|
210
|
+
// Preserve the existing routing-failure surface. Unknown capability becomes the explicit
|
|
211
|
+
// no-effort choice rather than broadening JEV's effort allowlist.
|
|
212
|
+
ladder = undefined;
|
|
213
|
+
}
|
|
214
|
+
const supportedEfforts = (ladder ?? []).filter(isCodexReasoningEffort) as OcxComboDefaultEffort[];
|
|
215
|
+
const configuredEfforts = pick.target.reasoningEfforts;
|
|
216
|
+
const reasoningEfforts = configuredEfforts === undefined
|
|
217
|
+
? supportedEfforts
|
|
218
|
+
: configuredEfforts.filter(effort => supportedEfforts.includes(effort));
|
|
219
|
+
// An explicit allowlist is restrictive. If catalog capabilities drift until no configured
|
|
220
|
+
// effort remains supported, omit the target instead of silently broadening JEV's choices.
|
|
221
|
+
if (configuredEfforts !== undefined && reasoningEfforts.length === 0) continue;
|
|
222
|
+
choices.push({
|
|
223
|
+
pick: { ...pick, attempted: [key] },
|
|
224
|
+
candidate: {
|
|
225
|
+
key,
|
|
226
|
+
provider: pick.target.provider,
|
|
227
|
+
model: pick.target.model,
|
|
228
|
+
reasoningEfforts,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
// #5691: the synchronous pick above does not defer emergency-only targets, so apply the
|
|
233
|
+
// same rule here — withhold them from JEV while any normal target is offered, never when
|
|
234
|
+
// they are all that remains.
|
|
235
|
+
if (combo.cooldownWaitPolicy === "before-last-resort" && choices.some(choice => !choice.pick.target.lastResort)) {
|
|
236
|
+
return choices.filter(choice => !choice.pick.target.lastResort);
|
|
237
|
+
}
|
|
238
|
+
return choices;
|
|
239
|
+
}
|
|
240
|
+
|
|
169
241
|
|
|
170
242
|
/** Dispatch a Responses combo within its shared send budget and preserve terminal child failures. */
|
|
171
243
|
export async function executeComboResponses(
|
|
@@ -190,6 +262,17 @@ export async function executeComboResponses(
|
|
|
190
262
|
if (!combo) {
|
|
191
263
|
return formatErrorResponse(404, "invalid_request_error", `Unknown combo: ${comboId}`);
|
|
192
264
|
}
|
|
265
|
+
// PF-07: present only for a Chat combo with `nativeChatCombos` on; otherwise every child
|
|
266
|
+
// takes the bridge below exactly as before.
|
|
267
|
+
const protocolLanes = createComboProtocolLanes({
|
|
268
|
+
source: options.protocolSource,
|
|
269
|
+
req,
|
|
270
|
+
config,
|
|
271
|
+
logCtx,
|
|
272
|
+
admission: options.admission,
|
|
273
|
+
comboId,
|
|
274
|
+
targets: combo.targets,
|
|
275
|
+
});
|
|
193
276
|
// The ladder's own scope, derived from what this combo DECLARES. It shares the request-wide
|
|
194
277
|
// counter with the holder that arrived on options -- a combo child already inherited that
|
|
195
278
|
// counter, but nothing read it as a limit across targets -- while its transition and
|
|
@@ -298,7 +381,10 @@ export async function executeComboResponses(
|
|
|
298
381
|
const payloadEligible = (target: (typeof combo.targets)[number]): boolean =>
|
|
299
382
|
comboPayloadReadable || !unreadableEncryptedAgentTask || canDecryptUnreadableAgentTask(target);
|
|
300
383
|
const targetEligible = (target: (typeof combo.targets)[number]): boolean =>
|
|
301
|
-
|
|
384
|
+
(combo.strategy !== "jev" || target.provider !== JEV_PROVIDER_ID)
|
|
385
|
+
&& payloadEligible(target)
|
|
386
|
+
&& reasoningReplayEligible(target)
|
|
387
|
+
&& (protocolLanes?.pickable(target) ?? true);
|
|
302
388
|
const onlyReplayIncompatibleTargetsRemain = (excluded: Iterable<string> = []): boolean => {
|
|
303
389
|
const excludedKeys = new Set(excluded);
|
|
304
390
|
const remaining = combo.targets.filter(target => {
|
|
@@ -398,11 +484,78 @@ export async function executeComboResponses(
|
|
|
398
484
|
}
|
|
399
485
|
|
|
400
486
|
if (!pick) {
|
|
487
|
+
// Every enabled candidate skipped as unrepresentable: the ingress refusal, with no send.
|
|
488
|
+
const protocolRefusal = protocolLanes?.refusal();
|
|
489
|
+
if (protocolRefusal) return protocolRefusal;
|
|
401
490
|
if (onlyReplayIncompatibleTargetsRemain()) return targetIncompatibleResponse();
|
|
402
491
|
return options.abortSignal?.aborted
|
|
403
492
|
? clientCancelledResponse()
|
|
404
493
|
: comboUnavailable(comboId);
|
|
405
494
|
}
|
|
495
|
+
let jevDecision: JevDecision | undefined;
|
|
496
|
+
if (combo.strategy === "jev") {
|
|
497
|
+
const choices = eligibleJevComboChoices(config, comboId, targetEligible, Date.now());
|
|
498
|
+
const first = choices[0];
|
|
499
|
+
if (!first) return comboUnavailable(comboId);
|
|
500
|
+
const resolvedFailOpenEffort = resolveEffortAtOrBelow(
|
|
501
|
+
"medium",
|
|
502
|
+
first.candidate.reasoningEfforts,
|
|
503
|
+
);
|
|
504
|
+
const fallback: Pick<JevDecision, "targetKey" | "effort"> = {
|
|
505
|
+
targetKey: first.candidate.key,
|
|
506
|
+
effort: resolvedFailOpenEffort && isCodexReasoningEffort(resolvedFailOpenEffort)
|
|
507
|
+
? resolvedFailOpenEffort as OcxComboDefaultEffort
|
|
508
|
+
: null,
|
|
509
|
+
};
|
|
510
|
+
const decisionStartedAt = Date.now();
|
|
511
|
+
let decision: JevDecision;
|
|
512
|
+
try {
|
|
513
|
+
decision = await resolveJevDecision({
|
|
514
|
+
body,
|
|
515
|
+
candidates: choices.map(choice => choice.candidate),
|
|
516
|
+
fallback,
|
|
517
|
+
config,
|
|
518
|
+
signal: options.abortSignal,
|
|
519
|
+
});
|
|
520
|
+
} catch (error) {
|
|
521
|
+
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
522
|
+
decision = {
|
|
523
|
+
...fallback,
|
|
524
|
+
gate: "network",
|
|
525
|
+
latencyMs: Math.max(0, Date.now() - decisionStartedAt),
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
jevDecision = decision;
|
|
529
|
+
const selected = choices.find(choice => choice.candidate.key === decision.targetKey) ?? first;
|
|
530
|
+
pick = { ...selected.pick, attempted: [targetKey(selected.pick.target)] };
|
|
531
|
+
logCtx.jevDecision = normalizePersistedJevDecision({
|
|
532
|
+
version: 1,
|
|
533
|
+
comboId,
|
|
534
|
+
selected: {
|
|
535
|
+
provider: selected.pick.target.provider,
|
|
536
|
+
model: selected.pick.target.model,
|
|
537
|
+
effort: decision.effort,
|
|
538
|
+
},
|
|
539
|
+
gate: decision.gate,
|
|
540
|
+
latencyMs: decision.latencyMs,
|
|
541
|
+
...(decision.confidence !== undefined ? { confidence: decision.confidence } : {}),
|
|
542
|
+
...(decision.chosenProbability !== undefined
|
|
543
|
+
? { chosenProbability: decision.chosenProbability }
|
|
544
|
+
: {}),
|
|
545
|
+
...(decision.usage ? { usage: decision.usage } : {}),
|
|
546
|
+
});
|
|
547
|
+
console.debug("[combo] JEV decision", {
|
|
548
|
+
targetKey: decision.targetKey,
|
|
549
|
+
effort: decision.effort,
|
|
550
|
+
gate: decision.gate,
|
|
551
|
+
latencyMs: decision.latencyMs,
|
|
552
|
+
...(decision.confidence !== undefined ? { confidence: decision.confidence } : {}),
|
|
553
|
+
...(decision.chosenProbability !== undefined
|
|
554
|
+
? { chosenProbability: decision.chosenProbability }
|
|
555
|
+
: {}),
|
|
556
|
+
...(decision.usage ? { usage: decision.usage } : {}),
|
|
557
|
+
});
|
|
558
|
+
}
|
|
406
559
|
// One immutable combo selection trace, before any child dispatch; child
|
|
407
560
|
// adoption below must never replace it with a concrete child route trace.
|
|
408
561
|
logCtx.routeDecision = comboRouteDecisionTrace(config, comboId, pick, requestedModel);
|
|
@@ -475,14 +628,32 @@ export async function executeComboResponses(
|
|
|
475
628
|
...(logCtx.surface ? { surface: logCtx.surface } : {}),
|
|
476
629
|
};
|
|
477
630
|
const targetRoute = routeConcreteModel(config, `${pick.target.provider}/${pick.target.model}`);
|
|
631
|
+
const targetReasoningEfforts = supportedLadderFor({
|
|
632
|
+
provider: targetRoute.provider,
|
|
633
|
+
modelId: targetRoute.modelId,
|
|
634
|
+
});
|
|
635
|
+
const initialJevDecision = firstComboTarget ? jevDecision : undefined;
|
|
478
636
|
const childBody = concreteComboRequestBody(
|
|
479
637
|
body,
|
|
480
638
|
pick.target,
|
|
481
|
-
comboDefaultEffort(config, comboId),
|
|
482
|
-
|
|
639
|
+
initialJevDecision ? initialJevDecision.effort : comboDefaultEffort(config, comboId),
|
|
640
|
+
initialJevDecision?.effort === null ? [] : targetReasoningEfforts,
|
|
483
641
|
combo.reasoningEffortMode,
|
|
484
|
-
combo.defaultEffortMode,
|
|
642
|
+
initialJevDecision !== undefined && initialJevDecision.effort !== null ? "force" : combo.defaultEffortMode,
|
|
485
643
|
);
|
|
644
|
+
if (initialJevDecision) {
|
|
645
|
+
delete childBody.service_tier;
|
|
646
|
+
if (initialJevDecision.effort !== null) {
|
|
647
|
+
const childReasoning = childBody.reasoning;
|
|
648
|
+
const preservedReasoning = childReasoning && typeof childReasoning === "object" && !Array.isArray(childReasoning)
|
|
649
|
+
? childReasoning as Record<string, unknown>
|
|
650
|
+
: {};
|
|
651
|
+
childBody.reasoning = { ...preservedReasoning, effort: initialJevDecision.effort };
|
|
652
|
+
delete childBody.reasoning_effort;
|
|
653
|
+
delete childBody.thinking_budget;
|
|
654
|
+
delete childBody.thinking;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
486
657
|
const childHeaders = buildComboChildHeaders(req.headers);
|
|
487
658
|
const childRequest = new Request(req.url, {
|
|
488
659
|
method: req.method,
|
|
@@ -550,14 +721,40 @@ export async function executeComboResponses(
|
|
|
550
721
|
let response: Response;
|
|
551
722
|
try {
|
|
552
723
|
const currentTargetProvider = pick.target.provider;
|
|
553
|
-
const
|
|
554
|
-
|
|
724
|
+
const remainingTargets = combo.strategy === "jev"
|
|
725
|
+
? combo.targets.filter(target => !pick!.attempted.includes(targetKey(target)))
|
|
726
|
+
: combo.targets.slice(pick.targetIndex + 1);
|
|
727
|
+
const deferCodexResetDerivedCooldown = (combo.strategy === "failover" || combo.strategy === "jev")
|
|
728
|
+
&& remainingTargets.some(target =>
|
|
555
729
|
target.provider === currentTargetProvider
|
|
556
730
|
&& targetEligible(target)
|
|
557
731
|
&& !isComboTargetInCooldown(comboId, target),
|
|
558
732
|
);
|
|
559
|
-
|
|
733
|
+
const nativeChild = protocolLanes?.nativeChild(pick.target, targetRoute, targetSendBudget);
|
|
734
|
+
response = nativeChild ? await dispatchNativeComboChild({
|
|
735
|
+
source: options.protocolSource!,
|
|
736
|
+
plan: nativeChild,
|
|
737
|
+
logCtx,
|
|
738
|
+
childLog,
|
|
739
|
+
attempt,
|
|
740
|
+
startedAt: started,
|
|
741
|
+
...(options.turnAdmissionLease ? { turnAdmissionLease: options.turnAdmissionLease } : {}),
|
|
742
|
+
// Attempt-relative TTFT, recorded here for the same reason as the bridge child below.
|
|
743
|
+
onFirstOutput: () => {
|
|
744
|
+
if (attempt.firstOutputMs === undefined) {
|
|
745
|
+
attempt.firstOutputMs = Math.max(0, Date.now() - started);
|
|
746
|
+
}
|
|
747
|
+
options.onFirstOutput?.();
|
|
748
|
+
},
|
|
749
|
+
callbacks: {
|
|
750
|
+
onTerminal: callbackGate.onTerminal,
|
|
751
|
+
onCancel: callbackGate.onCancel,
|
|
752
|
+
onResponseComplete: callbackGate.onResponseComplete,
|
|
753
|
+
},
|
|
754
|
+
}) : await requestDispatchers.handleResponses(childRequest, config, childLog, {
|
|
560
755
|
...options,
|
|
756
|
+
// A bridge child is a concrete route; the native source belongs to this loop only.
|
|
757
|
+
...(options.protocolSource ? { protocolSource: undefined } : {}),
|
|
561
758
|
// After the spread: the child must run on THIS target's ladder, not on the holder the
|
|
562
759
|
// parent arrived with.
|
|
563
760
|
sendBudget: targetSendBudget,
|
|
@@ -597,7 +794,9 @@ export async function executeComboResponses(
|
|
|
597
794
|
return clientCancelledResponse();
|
|
598
795
|
}
|
|
599
796
|
|
|
600
|
-
|
|
797
|
+
// A native Chat child reports a pre-stream failure by status before any byte, so its body
|
|
798
|
+
// is never peeked; a non-OK one takes the ordinary failure path below.
|
|
799
|
+
if (response.ok && !runTurnAdapterSseResponses.has(response) && clientWireOf(response) !== "chat") {
|
|
601
800
|
const nativePassthrough = isNativePassthroughSseResponse(response);
|
|
602
801
|
const eagerRelay = isEagerRelaySseResponse(response);
|
|
603
802
|
let preflight;
|
|
@@ -754,19 +953,30 @@ export async function executeComboResponses(
|
|
|
754
953
|
);
|
|
755
954
|
const failureNow = Date.now();
|
|
756
955
|
const attemptedTargets = pick.attempted;
|
|
956
|
+
const failureCooldownScope = comboFailureCooldownScope(failure.response.status, failure.classificationText, {
|
|
957
|
+
code: failure.upstreamCode,
|
|
958
|
+
});
|
|
959
|
+
const failedTargetKey = targetKey(pick.target);
|
|
960
|
+
let failedTargetCooldownRecorded = false;
|
|
757
961
|
const nextPick = advanceComboAfterFailure(config, pick, {
|
|
758
962
|
retryAfter: failure.retryAfter,
|
|
759
963
|
resetAt: failure.resetAt,
|
|
760
964
|
cooldownMs: combo.cooldownMs,
|
|
761
965
|
now: failureNow,
|
|
762
|
-
cooldownScope:
|
|
763
|
-
code: failure.upstreamCode,
|
|
764
|
-
}),
|
|
966
|
+
cooldownScope: failureCooldownScope,
|
|
765
967
|
eligible: targetEligible,
|
|
766
968
|
status: failure.response.status,
|
|
767
969
|
code: failure.upstreamCode,
|
|
768
970
|
message: failure.classificationText,
|
|
971
|
+
onCooldownRecorded: target => {
|
|
972
|
+
failedTargetCooldownRecorded ||= targetKey(target) === failedTargetKey;
|
|
973
|
+
},
|
|
769
974
|
});
|
|
975
|
+
// A sibling cooldown is not this failure's write: stale-generation removal can
|
|
976
|
+
// refuse recording even for a cooldown-producing classification. Require both.
|
|
977
|
+
const failedTargetCooled = failureCooldownScope !== "none"
|
|
978
|
+
&& failedTargetCooldownRecorded
|
|
979
|
+
&& isComboTargetInCooldown(comboId, pick.target, failureNow);
|
|
770
980
|
// Same target selector as the exclusionary pick below, minus `exclude`: the only
|
|
771
981
|
// difference is deliberate and is the whole point of the single-target retry.
|
|
772
982
|
const retryAfterCooldown = () =>
|
|
@@ -797,6 +1007,7 @@ export async function executeComboResponses(
|
|
|
797
1007
|
&& combo.targets.length === 1
|
|
798
1008
|
&& combo.waitForCooldownMs > 0
|
|
799
1009
|
&& comboTargetsDispatched <= 1
|
|
1010
|
+
&& failedTargetCooled
|
|
800
1011
|
&& !options.abortSignal?.aborted
|
|
801
1012
|
) {
|
|
802
1013
|
pick = await retryAfterCooldown();
|