@bitkyc08/opencodex 2.7.35 → 2.7.36
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.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +4 -2
- package/README.ru.md +1 -1
- package/README.zh-CN.md +1 -1
- package/bin/ocx.mjs +52 -0
- package/gui/dist/assets/index-BpX-hoSd.css +1 -0
- package/gui/dist/assets/index-ZmFopEYw.js +52 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/cursor/cursor-errors.ts +38 -1
- package/src/adapters/cursor/discovery.ts +1 -0
- package/src/adapters/cursor/effort-map.ts +1 -0
- package/src/adapters/cursor/live-models.ts +22 -5
- package/src/adapters/cursor/live-transport.ts +82 -7
- package/src/adapters/cursor/transport.ts +2 -0
- package/src/adapters/cursor.ts +5 -2
- package/src/adapters/openai-responses.ts +64 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/help.ts +10 -0
- package/src/cli/index.ts +88 -9
- package/src/cli/internal-dispatch.ts +20 -0
- package/src/cli/status.ts +15 -4
- package/src/cli/tray-proxy.ts +52 -0
- package/src/codex/auth-api.ts +46 -5
- package/src/codex/autostart-health.ts +149 -0
- package/src/codex/catalog/aggregation.ts +268 -0
- package/src/codex/catalog/bundled.ts +188 -0
- package/src/codex/catalog/effort.ts +263 -0
- package/src/codex/catalog/metadata.ts +176 -0
- package/src/codex/catalog/parsing.ts +399 -0
- package/src/codex/catalog/provider-fetch.ts +609 -0
- package/src/codex/catalog/sync.ts +540 -0
- package/src/codex/catalog.ts +11 -2426
- package/src/codex/inject.ts +165 -3
- package/src/codex/shim.ts +141 -8
- package/src/codex/sync.ts +17 -2
- package/src/config.ts +23 -0
- package/src/lib/errors.ts +11 -0
- package/src/providers/antigravity-models.ts +33 -0
- package/src/providers/kiro-models.ts +2 -0
- package/src/providers/registry.ts +2 -2
- package/src/responses/state.ts +69 -6
- package/src/server/auth-cors.ts +3 -0
- package/src/server/management/agent-settings-routes.ts +536 -0
- package/src/server/management/combo-routes.ts +210 -0
- package/src/server/management/config-routes.ts +302 -0
- package/src/server/management/context.ts +21 -0
- package/src/server/management/logs-usage-routes.ts +176 -0
- package/src/server/management/model-routes.ts +253 -0
- package/src/server/management/oauth-account-routes.ts +301 -0
- package/src/server/management/provider-routes.ts +408 -0
- package/src/server/management/shared.ts +186 -0
- package/src/server/management-api.ts +23 -1806
- package/src/server/responses/collaboration.ts +300 -0
- package/src/server/responses/compact.ts +342 -0
- package/src/server/responses/core.ts +1498 -0
- package/src/server/responses/encrypted-payload.ts +231 -0
- package/src/server/responses/fetch-helpers.ts +157 -0
- package/src/server/responses.ts +9 -2172
- package/src/server/startup-action-control.ts +41 -0
- package/src/server/startup-health-cache.ts +100 -0
- package/src/server/windows-tray-control.ts +41 -0
- package/src/service.ts +171 -19
- package/src/tray/assets/opencodex-tray-offline.ico +0 -0
- package/src/tray/assets/opencodex-tray-online.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning.ico +0 -0
- package/src/tray/assets/opencodex-tray.png +0 -0
- package/src/tray/windows-tray.ps1 +290 -0
- package/src/tray/windows.ts +628 -0
- package/src/types.ts +5 -0
- package/src/update/index.ts +43 -0
- package/src/update/job.ts +46 -0
- package/src/update/tray-update-plan.d.mts +18 -0
- package/src/update/tray-update-plan.mjs +38 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +9 -2
- package/src/usage/summary.ts +42 -7
- package/gui/dist/assets/index-BunUANVE.js +0 -52
- package/gui/dist/assets/index-Sg-7L_oZ.css +0 -1
package/src/server/responses.ts
CHANGED
|
@@ -1,2172 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from "
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../adapters/openai-responses";
|
|
11
|
-
import { expandPreviousResponseInput, previousResponseProviderState, rememberResponseState } from "../responses/state";
|
|
12
|
-
import { routeModel } from "../router";
|
|
13
|
-
import {
|
|
14
|
-
advanceComboAfterFailure,
|
|
15
|
-
comboDefaultEffort,
|
|
16
|
-
comboFailureDecision,
|
|
17
|
-
comboIdFromRawBody,
|
|
18
|
-
concreteComboRequestBody,
|
|
19
|
-
getCombo,
|
|
20
|
-
isComboTargetInCooldown,
|
|
21
|
-
NoAvailableComboTargetsError,
|
|
22
|
-
noteComboSuccess,
|
|
23
|
-
parseRetryAfterMs,
|
|
24
|
-
pickComboTarget,
|
|
25
|
-
targetKey,
|
|
26
|
-
} from "../combos";
|
|
27
|
-
import { isInjectionDebugEnabled } from "../lib/debug-settings";
|
|
28
|
-
import { injectionDebugLog } from "../lib/injection-debug-log";
|
|
29
|
-
import { modelInList, namespacedToolName } from "../types";
|
|
30
|
-
import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig, OcxProviderContinuationState, OcxUsage } from "../types";
|
|
31
|
-
import {
|
|
32
|
-
forceRefreshOAuthAccessSnapshot,
|
|
33
|
-
getOAuthCredentialApiBaseUrl,
|
|
34
|
-
getOAuthCredentialProjectId,
|
|
35
|
-
getValidAccessTokenSnapshot,
|
|
36
|
-
type OAuthAccessSnapshot,
|
|
37
|
-
UnsupportedOAuthProviderError,
|
|
38
|
-
} from "../oauth";
|
|
39
|
-
import { buildWebSearchTool, planWebSearch, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../web-search";
|
|
40
|
-
import { describeImagesInPlace, planVisionSidecar, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../vision";
|
|
41
|
-
import { createAdapterEventQueue, preflightAdapterEvents } from "../adapters/run-turn-queue";
|
|
42
|
-
import {
|
|
43
|
-
applyCodexAuthContextToProvider,
|
|
44
|
-
CodexAccountCooldownError,
|
|
45
|
-
CodexAuthContextError,
|
|
46
|
-
CodexDirectAuthenticationError,
|
|
47
|
-
CodexPoolAuthenticationError,
|
|
48
|
-
CodexThreadAffinityExpiredError,
|
|
49
|
-
headersForCodexAuthContext,
|
|
50
|
-
isCodexAuthContextUsable,
|
|
51
|
-
resolveCodexAuthContext,
|
|
52
|
-
type CodexAuthContext,
|
|
53
|
-
} from "../codex/auth-context";
|
|
54
|
-
import {
|
|
55
|
-
formatCodexProviderForLog,
|
|
56
|
-
recordCodexUpstreamOutcome,
|
|
57
|
-
type CodexUpstreamOutcome,
|
|
58
|
-
} from "../codex/routing";
|
|
59
|
-
import { fetchWithResetRetry, fetchWithTransientRetry, applyUpstreamRecoveryInit } from "../lib/upstream-retry";
|
|
60
|
-
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "./auth-cors";
|
|
61
|
-
import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../providers/openai-sidecar";
|
|
62
|
-
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
|
|
63
|
-
import { slugsEquivalent } from "../providers/slug-codec";
|
|
64
|
-
import { applyOpenAiVirtualModel, resolveOpenAiCompactModel } from "../providers/openai-virtual-models";
|
|
65
|
-
import { isUsageDebugEnabled } from "../usage/debug";
|
|
66
|
-
import { readJsonRequestBody, DecompressedBodyTooLargeError, UnsupportedContentEncodingError } from "./request-decompress";
|
|
67
|
-
import { resolveAdapter, resolveWireProtocolOverride } from "./adapter-resolve";
|
|
68
|
-
import { hasKeyPoolFailover, rotateProviderTransportOn429 } from "../providers/key-failover";
|
|
69
|
-
import { shouldAttemptImageTierRetry } from "./image-retry";
|
|
70
|
-
import { resolveProviderTransport } from "../providers/xai-transport";
|
|
71
|
-
import type { WsData } from "./ws-bridge";
|
|
72
|
-
import { registerTurn, trackStreamLifetime, unregisterTurn } from "./lifecycle";
|
|
73
|
-
import { redactSecretString } from "../lib/redact";
|
|
74
|
-
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
75
|
-
import { supportedLadderFor } from "./effort-policy";
|
|
76
|
-
import {
|
|
77
|
-
beginRequestAttempt,
|
|
78
|
-
catalogModelSupportsServiceTier,
|
|
79
|
-
finishRequestAttempt,
|
|
80
|
-
inspectResponseLogJson,
|
|
81
|
-
noteAttemptSend,
|
|
82
|
-
readConfiguredCodexServiceTier,
|
|
83
|
-
requestLogSpeedLabel,
|
|
84
|
-
sealRequestAttemptIdentity,
|
|
85
|
-
usageFromResponsesPayload,
|
|
86
|
-
type RequestLogContext,
|
|
87
|
-
} from "./request-log";
|
|
88
|
-
import type { AttemptRecoveryKind } from "../usage/log";
|
|
89
|
-
import {
|
|
90
|
-
consumeForInspection,
|
|
91
|
-
consumeForResponseLogMetadata,
|
|
92
|
-
markNativePassthroughSseResponse,
|
|
93
|
-
relaySseWithFailedTail,
|
|
94
|
-
relayWithAbort,
|
|
95
|
-
sanitizePassthroughHeaders,
|
|
96
|
-
} from "./relay";
|
|
97
|
-
import { hasResponsesItemIdRepair, relaySseWithResponsesItemIdRepair } from "./responses-item-id-repair";
|
|
98
|
-
|
|
99
|
-
export function buildToolBridgeMaps(parsed: OcxParsedRequest): {
|
|
100
|
-
toolNsMap: Map<string, { namespace: string; name: string }>;
|
|
101
|
-
freeformToolNames: Set<string>;
|
|
102
|
-
toolSearchToolNames: Set<string>;
|
|
103
|
-
} {
|
|
104
|
-
const toolNsMap = new Map<string, { namespace: string; name: string }>();
|
|
105
|
-
const freeformToolNames = new Set<string>();
|
|
106
|
-
const toolSearchToolNames = new Set<string>();
|
|
107
|
-
for (const t of parsed.context.tools ?? []) {
|
|
108
|
-
if (t.namespace) toolNsMap.set(namespacedToolName(t.namespace, t.name), { namespace: t.namespace, name: t.name });
|
|
109
|
-
if (t.freeform) freeformToolNames.add(t.name);
|
|
110
|
-
if (t.toolSearch) toolSearchToolNames.add(t.name);
|
|
111
|
-
}
|
|
112
|
-
return { toolNsMap, freeformToolNames, toolSearchToolNames };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Verbatim upstream Proactive text (codex-rs core/src/context/multi_agent_mode_instructions.rs). */
|
|
116
|
-
const PROACTIVE_MULTI_AGENT_MODE_TEXT = [
|
|
117
|
-
"Proactive multi-agent delegation is active.",
|
|
118
|
-
"Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies.",
|
|
119
|
-
"Delegate independent sub-tasks to sub-agents whenever parallel work would materially improve speed or quality — do not serialize work that can run concurrently.",
|
|
120
|
-
"Each sub-agent runs in its own context and can use all available tools; prefer spawning specialists over doing everything yourself.",
|
|
121
|
-
"This mode remains active until a later multi-agent mode developer message changes it.",
|
|
122
|
-
].join(" ");
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* True when this turn runs the v1 collab surface, judged from the request's own tool list
|
|
126
|
-
* (codex registers exactly one surface per thread, core/src/tools/spec_plan.rs): v1 ships
|
|
127
|
-
* spawn_agent inside a namespace plus v1-only names (send_input/close_agent); v2 ships a
|
|
128
|
-
* flat spawn_agent. A flat spawn_agent vetoes so an ambiguous mix never counts as v1.
|
|
129
|
-
*/
|
|
130
|
-
export function isV1CollabSurface(parsed: OcxParsedRequest): boolean {
|
|
131
|
-
return collabSurface(parsed) === "v1";
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Which multi-agent collab surface this turn carries, judged from the request's own
|
|
136
|
-
* tool list. Real wire shapes (codex-rs spec_plan.rs add_collaboration_tools):
|
|
137
|
-
* - v1: tools under the "multi_agent_v1" namespace, always accompanied by v1-only
|
|
138
|
-
* names (send_input / resume_agent / close_agent).
|
|
139
|
-
* - v2 on namespace_tools providers (e.g. the ChatGPT backend): tools under the
|
|
140
|
-
* "collaboration" namespace (config.multi_agent_v2.tool_namespace — user-settable,
|
|
141
|
-
* so the name is not hardcoded here), with v2-only companions (send_message /
|
|
142
|
-
* followup_task / interrupt_agent / list_agents).
|
|
143
|
-
* - v2 without namespace support: a flat spawn_agent.
|
|
144
|
-
* Companion tools are the primary discriminator; a companionless namespaced spawn
|
|
145
|
-
* falls back to "v1" (legacy behavior) and a companionless flat spawn to "v2".
|
|
146
|
-
* Contradictory markers count as neither — never inject on unclear ground.
|
|
147
|
-
*/
|
|
148
|
-
export function collabSurface(parsed: OcxParsedRequest): "v1" | "v2" | null {
|
|
149
|
-
let namespacedSpawn = false;
|
|
150
|
-
let flatSpawn = false;
|
|
151
|
-
let v1Only = false;
|
|
152
|
-
let v2Only = false;
|
|
153
|
-
for (const t of parsed.context.tools ?? []) {
|
|
154
|
-
if (t.name === "spawn_agent") {
|
|
155
|
-
if (t.namespace) namespacedSpawn = true;
|
|
156
|
-
else flatSpawn = true;
|
|
157
|
-
} else if (t.name === "send_input" || t.name === "resume_agent" || t.name === "close_agent") {
|
|
158
|
-
v1Only = true;
|
|
159
|
-
} else if (t.name === "send_message" || t.name === "followup_task" || t.name === "interrupt_agent" || t.name === "list_agents") {
|
|
160
|
-
v2Only = true;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
if (!namespacedSpawn && !flatSpawn) return null; // no spawn_agent -> no collab surface
|
|
164
|
-
if (namespacedSpawn && flatSpawn) return null; // contradictory spawn shapes
|
|
165
|
-
if (v1Only && v2Only) return null; // contradictory companions
|
|
166
|
-
if (v1Only) return "v1";
|
|
167
|
-
if (v2Only) return "v2";
|
|
168
|
-
return namespacedSpawn ? "v1" : "v2"; // companionless fallbacks (legacy defaults)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Multi-agent guidance for this turn, or null when nothing applies.
|
|
173
|
-
*
|
|
174
|
-
* V1 surface: codex-rs only emits its Proactive delegation developer message on the
|
|
175
|
-
* v2 surface, so when a v1-surface turn arrives at the synthetic top tier (codex
|
|
176
|
-
* converts ultra -> max on the wire, so max arrival means the user picked the top
|
|
177
|
-
* rung) the proxy supplies the same one-liner, wrapped in codex's own
|
|
178
|
-
* <multi_agent_mode> tags. That is ALL v1 gets — no model designation, no roster
|
|
179
|
-
* (kept lean by request, devlog 260710): ultra-tier injection is sufficient there.
|
|
180
|
-
*
|
|
181
|
-
* V2 surface (flat spawn_agent — sol/terra under the default mode, EVERY model when
|
|
182
|
-
* `ocx v2 mode v2` forces the pins): codex-rs already emits its own Proactive text
|
|
183
|
-
* there, so the proxy never duplicates it — it adds only model-designation guidance,
|
|
184
|
-
* and only when it has something to designate: an injectionModel and/or a
|
|
185
|
-
* subagentModels roster entry that resolves in the injected catalog. v2 rejects
|
|
186
|
-
* model/effort overrides on a full-history fork (multi_agents_v2/spawn.rs
|
|
187
|
-
* reject_full_fork_spawn_overrides), so the prompt mandates fork_turns "none" or a
|
|
188
|
-
* partial fork plus a self-contained task message.
|
|
189
|
-
* Current Codex surfaces can expose model/reasoning_effort overrides directly or
|
|
190
|
-
* omit them. The proxy wording therefore stays schema-agnostic and advertises only
|
|
191
|
-
* the effective candidates described for this collaboration surface.
|
|
192
|
-
*
|
|
193
|
-
* The v2 body is budgeted to <= 700 chars (V2_GUIDANCE_CHAR_BUDGET): rules first,
|
|
194
|
-
* then the preferred model, then the compact roster of configured `subagentModels`
|
|
195
|
-
* with the effort ladder each advertises in the injected catalog (the list codex-rs
|
|
196
|
-
* validates spawn efforts against). A user-configured `injectionPrompt` replaces the
|
|
197
|
-
* v2 body with {{model}}/{{effort}}/{{roster}} placeholder substitution (own length,
|
|
198
|
-
* user-owned); firing gates are unchanged.
|
|
199
|
-
*/
|
|
200
|
-
import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../codex/catalog";
|
|
201
|
-
|
|
202
|
-
export interface MultiAgentGuidanceOptions {
|
|
203
|
-
multiAgentGuidanceEnabled?: boolean;
|
|
204
|
-
injectionModel?: string;
|
|
205
|
-
injectionEffort?: string;
|
|
206
|
-
subagentModels?: string[];
|
|
207
|
-
injectionPrompt?: string;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export interface MultiAgentGuidanceDeps {
|
|
211
|
-
resolveEffectiveSubagentRoster?: (
|
|
212
|
-
configuredModels: readonly string[],
|
|
213
|
-
surface: SpawnAgentSurface,
|
|
214
|
-
) => EffectiveSubagentRoster | Promise<EffectiveSubagentRoster>;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async function resolveEffectiveSubagentRoster(
|
|
218
|
-
configuredModels: readonly string[],
|
|
219
|
-
surface: SpawnAgentSurface,
|
|
220
|
-
): Promise<EffectiveSubagentRoster> {
|
|
221
|
-
const { effectiveSubagentRoster } = await import("../codex/catalog");
|
|
222
|
-
return effectiveSubagentRoster(configuredModels, surface);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
export async function multiAgentGuidanceText(
|
|
226
|
-
parsed: OcxParsedRequest,
|
|
227
|
-
options: MultiAgentGuidanceOptions = {},
|
|
228
|
-
deps: MultiAgentGuidanceDeps = {},
|
|
229
|
-
): Promise<string | null> {
|
|
230
|
-
if (options.multiAgentGuidanceEnabled === false) return null;
|
|
231
|
-
const {
|
|
232
|
-
injectionModel,
|
|
233
|
-
injectionEffort,
|
|
234
|
-
subagentModels,
|
|
235
|
-
injectionPrompt,
|
|
236
|
-
} = options;
|
|
237
|
-
const surface = collabSurface(parsed);
|
|
238
|
-
if (surface === null) return null;
|
|
239
|
-
|
|
240
|
-
if (surface === "v2") {
|
|
241
|
-
// codex-rs supplies the Proactive text on v2; the proxy only adds model-designation
|
|
242
|
-
// guidance, and only when there is something concrete to designate: a configured
|
|
243
|
-
// injectionModel and/or a roster entry that resolves in the injected catalog.
|
|
244
|
-
const configuredForGuidance = [
|
|
245
|
-
...(subagentModels ?? []),
|
|
246
|
-
...(injectionModel ? [injectionModel] : []),
|
|
247
|
-
];
|
|
248
|
-
const resolveRoster = deps.resolveEffectiveSubagentRoster ?? resolveEffectiveSubagentRoster;
|
|
249
|
-
const effective = await resolveRoster(configuredForGuidance, "v2");
|
|
250
|
-
const rosterModels = effective.advertised.filter(candidate =>
|
|
251
|
-
(subagentModels ?? []).some(model => slugsEquivalent(model, candidate.model))
|
|
252
|
-
);
|
|
253
|
-
const roster = subagentRosterText(rosterModels);
|
|
254
|
-
const preferred = injectionModel
|
|
255
|
-
? effective.candidates.find(candidate => slugsEquivalent(injectionModel, candidate.model))
|
|
256
|
-
: undefined;
|
|
257
|
-
|
|
258
|
-
if (isInjectionDebugEnabled() && effective.excluded.length > 0) {
|
|
259
|
-
injectionDebugLog(`[opencodex] multi-agent guidance excluded: ${effective.excluded
|
|
260
|
-
.map(item => `${item.configured}:${item.reason}`)
|
|
261
|
-
.join(", ")}`);
|
|
262
|
-
}
|
|
263
|
-
if (!injectionModel && roster === "") return null;
|
|
264
|
-
if (injectionPrompt) {
|
|
265
|
-
return `<multi_agent_mode>${applyInjectionPlaceholders(injectionPrompt, injectionModel, injectionEffort, roster)}</multi_agent_mode>`;
|
|
266
|
-
}
|
|
267
|
-
if (!preferred && roster === "") return null;
|
|
268
|
-
let text = "When the active spawn_agent tool supports optional \"model\" or \"reasoning_effort\" overrides, "
|
|
269
|
-
+ "use only models listed for this collaboration surface. "
|
|
270
|
-
+ "When setting either override, set fork_turns to \"none\" "
|
|
271
|
-
+ "(or a positive turn count such as \"3\"; full-history forks reject overrides) "
|
|
272
|
-
+ "and make the task message self-contained.";
|
|
273
|
-
if (preferred) {
|
|
274
|
-
text += ` Preferred sub-agent: model "${preferred.model}"`
|
|
275
|
-
+ (injectionEffort ? `, reasoning_effort "${injectionEffort}"` : "")
|
|
276
|
-
+ " — use it unless the user names another.";
|
|
277
|
-
}
|
|
278
|
-
text += roster;
|
|
279
|
-
if (text.length > V2_GUIDANCE_CHAR_BUDGET) {
|
|
280
|
-
// Roster is the only unbounded part — drop it before breaking the budget.
|
|
281
|
-
text = text.slice(0, text.length - roster.length);
|
|
282
|
-
}
|
|
283
|
-
return `<multi_agent_mode>${text}</multi_agent_mode>`;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const effort = parsed.options.reasoning;
|
|
287
|
-
// v1 keeps only the upstream-parity behavior: Proactive text at the top tier
|
|
288
|
-
// (ultra arrives as max on the wire). No designation/roster payload here.
|
|
289
|
-
if (effort !== "max" && effort !== "ultra") return null;
|
|
290
|
-
return `<multi_agent_mode>${PROACTIVE_MULTI_AGENT_MODE_TEXT}</multi_agent_mode>`;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/** Hard budget for the built-in v2 guidance body (user request: keep injection lean). */
|
|
294
|
-
export const V2_GUIDANCE_CHAR_BUDGET = 700;
|
|
295
|
-
|
|
296
|
-
/** {{model}}/{{effort}}/{{roster}} substitution for the user-configured injectionPrompt. */
|
|
297
|
-
function applyInjectionPlaceholders(prompt: string, model?: string, effort?: string, roster?: string): string {
|
|
298
|
-
return prompt
|
|
299
|
-
.replaceAll("{{model}}", model ?? "")
|
|
300
|
-
.replaceAll("{{effort}}", effort ?? "")
|
|
301
|
-
.replaceAll("{{roster}}", roster ?? "");
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Compact one-line roster of effective sub-agent candidates, or "" when empty.
|
|
306
|
-
* Efforts come from the injected catalog so only rungs codex-rs will actually
|
|
307
|
-
* accept are advertised.
|
|
308
|
-
*/
|
|
309
|
-
function subagentRosterText(models: Array<{ model: string; efforts: string[] }>): string {
|
|
310
|
-
if (models.length === 0) return "";
|
|
311
|
-
const ladders = new Set(models.map(model => model.efforts.join("/")));
|
|
312
|
-
if (!ladders.has("") && ladders.size === 1) {
|
|
313
|
-
return ` Available models (reasoning_effort ${[...ladders][0]}): ${models
|
|
314
|
-
.map(model => `"${model.model}"`)
|
|
315
|
-
.join(", ")}.`;
|
|
316
|
-
}
|
|
317
|
-
const entries = models.map(model => model.efforts.length > 0
|
|
318
|
-
? `"${model.model}" (${model.efforts.join("/")})`
|
|
319
|
-
: `"${model.model}"`);
|
|
320
|
-
return ` Available models (valid reasoning_effort): ${entries.join(", ")}.`;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Append a developer message to BOTH request shapes: parsed.context.messages feeds the
|
|
325
|
-
* routed adapters, while the ChatGPT passthrough serializes _rawBody verbatim (same
|
|
326
|
-
* dual-write contract as the mock-max clamp in handleResponses).
|
|
327
|
-
*/
|
|
328
|
-
export function injectDeveloperMessage(parsed: OcxParsedRequest, text: string): void {
|
|
329
|
-
parsed.context.messages.push({ role: "developer", content: text, timestamp: Date.now() });
|
|
330
|
-
const raw = parsed._rawBody as { input?: unknown } | undefined;
|
|
331
|
-
if (raw && Array.isArray(raw.input)) {
|
|
332
|
-
const devItem = { type: "message", role: "developer", content: [{ type: "input_text", text }] };
|
|
333
|
-
// compaction_trigger must remain the final input item (codex-rs + ChatGPT backend both
|
|
334
|
-
// validate this). Insert the developer message BEFORE the trigger when present.
|
|
335
|
-
const last = raw.input[raw.input.length - 1];
|
|
336
|
-
if (last && typeof last === "object" && (last as { type?: string }).type === "compaction_trigger") {
|
|
337
|
-
raw.input.splice(raw.input.length - 1, 0, devItem);
|
|
338
|
-
} else {
|
|
339
|
-
raw.input.push(devItem);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* True when an encrypted_content payload plausibly came from the ChatGPT backend
|
|
346
|
-
* (opaque base64-ish blob). codex-rs's `InterAgentCommunication::new_encrypted` performs
|
|
347
|
-
* NO local crypto — it just parks plaintext in the encrypted slot and relies on the
|
|
348
|
-
* backend to swap in real ciphertext. Under a routed (ocx-served) parent the backend
|
|
349
|
-
* never sees the parent turn, so the slot still holds plaintext when a native child
|
|
350
|
-
* replays it — and the backend then fails the turn with "Encrypted function output
|
|
351
|
-
* content could not be decrypted or decoded" (observed 260709 as 502 retry loops).
|
|
352
|
-
*/
|
|
353
|
-
function looksLikeBackendCiphertext(payload: string): boolean {
|
|
354
|
-
return payload.length >= 64 && /^[A-Za-z0-9+/=_-]+$/.test(payload);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Backend-minted ciphertext runs are Fernet tokens (base64url, version byte 0x80 ->
|
|
359
|
-
* literal "gAAAA" prefix). Used to carve embedded blobs out of MIXED slots: plugin
|
|
360
|
-
* hooks (e.g. codexclaw's leaf guard) prepend plaintext preambles to spawn messages
|
|
361
|
-
* whose task body is already backend-encrypted, producing a slot that is neither
|
|
362
|
-
* decryptable (backend) nor readable (model) as a whole.
|
|
363
|
-
*/
|
|
364
|
-
const FERNET_TOKEN_RUN = /gAAAA[A-Za-z0-9_-]{60,}={0,2}/g;
|
|
365
|
-
|
|
366
|
-
const AGENT_MESSAGE_ROUTING_ENVELOPE = /(?:^|\n)Message Type\s*:\s*NEW_TASK[^\n]*\nTask name\s*:[^\n]*\nSender\s*:[^\n]*\nPayload\s*:\s*(?:\n|$)/gi;
|
|
367
|
-
const AGENT_MESSAGE_CONTROL_PREAMBLE = /(?:^|\n)\[CXC-(?:LEAF-GUARD|SKILL-AFFORDANCE)\][\s\S]*?(?=\n{2,}|$)/g;
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* True when a V2 agent message contains a backend-minted Fernet task but no
|
|
371
|
-
* provider-readable task text. The routing envelope and hook-added control
|
|
372
|
-
* preambles are metadata, not actionable work. Inspect this before spawn-message
|
|
373
|
-
* sanitization splits mixed encrypted slots into plaintext and ciphertext parts.
|
|
374
|
-
*/
|
|
375
|
-
export function hasUnreadableEncryptedAgentTask(input: unknown): boolean {
|
|
376
|
-
if (!Array.isArray(input)) return false;
|
|
377
|
-
|
|
378
|
-
return input.some(item => {
|
|
379
|
-
if (!item || typeof item !== "object" || (item as { type?: unknown }).type !== "agent_message") {
|
|
380
|
-
return false;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const content = (item as { content?: unknown }).content;
|
|
384
|
-
if (!Array.isArray(content)) return false;
|
|
385
|
-
|
|
386
|
-
let hasFernetTask = false;
|
|
387
|
-
const readableParts: string[] = [];
|
|
388
|
-
for (const part of content) {
|
|
389
|
-
if (!part || typeof part !== "object") continue;
|
|
390
|
-
const record = part as { type?: unknown; text?: unknown; encrypted_content?: unknown };
|
|
391
|
-
if (
|
|
392
|
-
(record.type === "input_text" || record.type === "text" || record.type === "output_text")
|
|
393
|
-
&& typeof record.text === "string"
|
|
394
|
-
) {
|
|
395
|
-
readableParts.push(record.text);
|
|
396
|
-
continue;
|
|
397
|
-
}
|
|
398
|
-
if (record.type !== "encrypted_content" || typeof record.encrypted_content !== "string") {
|
|
399
|
-
continue;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const withoutFernet = record.encrypted_content.replace(FERNET_TOKEN_RUN, "\n\n");
|
|
403
|
-
if (withoutFernet !== record.encrypted_content) hasFernetTask = true;
|
|
404
|
-
readableParts.push(withoutFernet);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
if (!hasFernetTask) return false;
|
|
408
|
-
const readableTask = readableParts
|
|
409
|
-
.join("\n\n")
|
|
410
|
-
.replace(AGENT_MESSAGE_ROUTING_ENVELOPE, "\n")
|
|
411
|
-
.replace(AGENT_MESSAGE_CONTROL_PREAMBLE, "\n")
|
|
412
|
-
.trim();
|
|
413
|
-
return readableTask.length === 0;
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* Split a non-ciphertext encrypted slot into ordered parts: prose becomes input_text,
|
|
419
|
-
* embedded Fernet blobs stay encrypted_content so the backend can still decrypt the
|
|
420
|
-
* real task body. A slot with no embedded blob degrades to a single input_text part.
|
|
421
|
-
*/
|
|
422
|
-
function encryptedSlotParts(payload: string): Array<Record<string, string>> {
|
|
423
|
-
const parts: Array<Record<string, string>> = [];
|
|
424
|
-
let last = 0;
|
|
425
|
-
for (const match of payload.matchAll(FERNET_TOKEN_RUN)) {
|
|
426
|
-
const index = match.index ?? 0;
|
|
427
|
-
const before = payload.slice(last, index);
|
|
428
|
-
if (before.trim().length > 0) parts.push({ type: "input_text", text: before });
|
|
429
|
-
parts.push({ type: "encrypted_content", encrypted_content: match[0] });
|
|
430
|
-
last = index + match[0].length;
|
|
431
|
-
}
|
|
432
|
-
const rest = payload.slice(last);
|
|
433
|
-
if (rest.trim().length > 0) parts.push({ type: "input_text", text: rest });
|
|
434
|
-
return parts.length > 0 ? parts : [{ type: "input_text", text: payload }];
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
function hasEncryptedContentPart(content: unknown): boolean {
|
|
438
|
-
return Array.isArray(content) && content.some(part => (
|
|
439
|
-
part && typeof part === "object"
|
|
440
|
-
&& (part as { type?: unknown }).type === "encrypted_content"
|
|
441
|
-
));
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* Rewrite non-ciphertext `{type:"encrypted_content"}` parts into `{type:"input_text"}`
|
|
446
|
-
* throughout a request's input items (message content and function_call_output content
|
|
447
|
-
* arrays share the part shape, codex-rs protocol/models.rs). An `agent_message` whose
|
|
448
|
-
* payload becomes entirely plaintext is normalized to a user message so routed parsers
|
|
449
|
-
* that do not understand the internal item type still deliver the spawn task.
|
|
450
|
-
* Genuine backend blobs are left byte-identical so replay/cache semantics survive, and
|
|
451
|
-
* MIXED slots (plaintext preamble + embedded Fernet task body) are split so the backend
|
|
452
|
-
* decrypts the blob while the prose passes as text. Returns the number of parts rewritten.
|
|
453
|
-
*/
|
|
454
|
-
export function sanitizeEncryptedContentInPlace(input: unknown): number {
|
|
455
|
-
if (!Array.isArray(input)) return 0;
|
|
456
|
-
let rewritten = 0;
|
|
457
|
-
const visit = (node: unknown): number => {
|
|
458
|
-
const before = rewritten;
|
|
459
|
-
if (Array.isArray(node)) {
|
|
460
|
-
for (let i = 0; i < node.length; i += 1) {
|
|
461
|
-
const child = node[i] as unknown;
|
|
462
|
-
if (
|
|
463
|
-
child && typeof child === "object"
|
|
464
|
-
&& (child as { type?: unknown }).type === "encrypted_content"
|
|
465
|
-
&& typeof (child as { encrypted_content?: unknown }).encrypted_content === "string"
|
|
466
|
-
) {
|
|
467
|
-
const payload = (child as { encrypted_content: string }).encrypted_content;
|
|
468
|
-
if (!looksLikeBackendCiphertext(payload)) {
|
|
469
|
-
const parts = encryptedSlotParts(payload);
|
|
470
|
-
node.splice(i, 1, ...parts);
|
|
471
|
-
i += parts.length - 1;
|
|
472
|
-
rewritten += 1;
|
|
473
|
-
continue;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
const childRewrites = visit(child);
|
|
477
|
-
if (
|
|
478
|
-
childRewrites > 0
|
|
479
|
-
&& child && typeof child === "object"
|
|
480
|
-
&& (child as { type?: unknown }).type === "agent_message"
|
|
481
|
-
&& !hasEncryptedContentPart((child as { content?: unknown }).content)
|
|
482
|
-
) {
|
|
483
|
-
const message = child as { type: string; role?: string; id?: unknown; author?: unknown; recipient?: unknown };
|
|
484
|
-
message.type = "message";
|
|
485
|
-
message.role = "user";
|
|
486
|
-
delete message.id;
|
|
487
|
-
delete message.author;
|
|
488
|
-
delete message.recipient;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
return rewritten - before;
|
|
492
|
-
}
|
|
493
|
-
if (node && typeof node === "object") {
|
|
494
|
-
for (const value of Object.values(node)) visit(value);
|
|
495
|
-
}
|
|
496
|
-
return rewritten - before;
|
|
497
|
-
};
|
|
498
|
-
visit(input);
|
|
499
|
-
return rewritten;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
export function sidecarOutcomeRecorder(
|
|
503
|
-
config: OcxConfig,
|
|
504
|
-
authCtx: CodexAuthContext,
|
|
505
|
-
threadId?: string | null,
|
|
506
|
-
): ((outcome: CodexUpstreamOutcome) => void) | undefined {
|
|
507
|
-
return authCtx.kind === "pool" || authCtx.kind === "main-pool"
|
|
508
|
-
? outcome => recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, { threadId })
|
|
509
|
-
: undefined;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
/** Codex client hard-coded helper/shadow models: 0.145.0 uses gpt-5.6-luna; older clients gpt-5.4-mini. */
|
|
513
|
-
const DEFAULT_SHADOW_SOURCE_MODELS = ["gpt-5.4-mini", "gpt-5.6-luna"] as const;
|
|
514
|
-
|
|
515
|
-
/**
|
|
516
|
-
* True when `modelId` is a Codex client shadow/helper source model eligible for the
|
|
517
|
-
* shadowCallIntercept rewrite. Slash-prefixed ids (`openai/gpt-5.6-luna`) are deliberate
|
|
518
|
-
* routed requests, never client shadow calls — hard-excluded even for configured
|
|
519
|
-
* overrides. `configured` arrives unvalidated from disk (config.ts top-level parse is
|
|
520
|
-
* passthrough), so non-string entries are filtered rather than trusted.
|
|
521
|
-
*
|
|
522
|
-
* Known tradeoff (issue #311 review): matching is model-id based, so with the intercept
|
|
523
|
-
* enabled a FOREGROUND bare `gpt-5.6-luna` turn is also rewritten — the same blunt
|
|
524
|
-
* "ALL matching requests" semantics the feature has always documented for gpt-5.4-mini.
|
|
525
|
-
* The proxy has no reliable helper-call signal in the request today; users who run Luna
|
|
526
|
-
* as a foreground model can scope the intercept with `sourceModels: ["gpt-5.4-mini"]`.
|
|
527
|
-
*/
|
|
528
|
-
export function isShadowSourceModel(modelId: string, configured?: unknown): boolean {
|
|
529
|
-
if (modelId.includes("/")) return false;
|
|
530
|
-
const configuredStrings = Array.isArray(configured)
|
|
531
|
-
? configured.filter((v): v is string => typeof v === "string" && v.trim() !== "")
|
|
532
|
-
: [];
|
|
533
|
-
const prefixes = configuredStrings.length > 0 ? configuredStrings : DEFAULT_SHADOW_SOURCE_MODELS;
|
|
534
|
-
return prefixes.some(prefix => modelId.startsWith(prefix.trim()));
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
/** Account id to attribute log labels / upstream outcomes to (pool + rotation-injected main). */
|
|
538
|
-
export function codexLogAccountId(authCtx: CodexAuthContext): string | null {
|
|
539
|
-
return authCtx.kind === "pool" || authCtx.kind === "main-pool" ? authCtx.accountId : null;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
export function usesCodexForwardPoolAuth(
|
|
543
|
-
authCtx: CodexAuthContext,
|
|
544
|
-
provider: OcxProviderConfig,
|
|
545
|
-
): authCtx is Extract<CodexAuthContext, { kind: "pool" | "main-pool" }> {
|
|
546
|
-
return (authCtx.kind === "pool" || authCtx.kind === "main-pool")
|
|
547
|
-
&& provider.authMode === "forward" && provider.adapter === "openai-responses";
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
export function codexForwardTerminalOutcomeRecorder(
|
|
551
|
-
config: OcxConfig,
|
|
552
|
-
authCtx: CodexAuthContext,
|
|
553
|
-
provider: OcxProviderConfig,
|
|
554
|
-
logCtx?: RequestLogContext,
|
|
555
|
-
threadId?: string | null,
|
|
556
|
-
): ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined {
|
|
557
|
-
if (!usesCodexForwardPoolAuth(authCtx, provider)) return undefined;
|
|
558
|
-
return (status, httpStatusOverride) => {
|
|
559
|
-
if (status === "incomplete") {
|
|
560
|
-
// Normal limit/content-filter/stall terminal — the account served the
|
|
561
|
-
// request. Don't penalize account health; record success to clear any
|
|
562
|
-
// prior soft-avoid so a healthy account isn't stuck avoided.
|
|
563
|
-
recordCodexUpstreamOutcome(config, authCtx.accountId, 200, { threadId });
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
// status === "completed" or "failed": use the semantic HTTP status derived
|
|
567
|
-
// from the terminal SSE error payload (httpStatusFromTerminalError in
|
|
568
|
-
// request-log inspection) instead of collapsing every non-completed terminal
|
|
569
|
-
// to 502. A 400 invalid_request_error must not soft-avoid the account or
|
|
570
|
-
// rebind threads — only genuine transport/5xx failures should trigger
|
|
571
|
-
// transient health recording.
|
|
572
|
-
// httpStatusOverride: the combo WS path inspects SSE payloads into the parent
|
|
573
|
-
// logCtx, but this recorder closes over the child logCtx. The caller passes
|
|
574
|
-
// the parent's terminalHttpStatus so the semantic status is not lost.
|
|
575
|
-
const outcome = status === "completed"
|
|
576
|
-
? 200
|
|
577
|
-
: (httpStatusOverride ?? logCtx?.terminalHttpStatus ?? 502);
|
|
578
|
-
recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, { threadId });
|
|
579
|
-
};
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
/**
|
|
583
|
-
* Map a request-body read failure to an honest error response. `readJsonRequestBody` can fail three
|
|
584
|
-
* ways and they must not all collapse into "Invalid JSON body": an unsupported content-encoding
|
|
585
|
-
* (415), a body that inflates past the decompression cap (413 — the image-heavy case Codex hits when
|
|
586
|
-
* zstd-compressed screenshot history exceeds the limit), or a genuine JSON syntax error (400). The
|
|
587
|
-
* real decode error was previously swallowed, so log it before returning the generic 400.
|
|
588
|
-
*/
|
|
589
|
-
export function decodeRequestErrorResponse(err: unknown, label: string): Response {
|
|
590
|
-
if (err instanceof UnsupportedContentEncodingError) {
|
|
591
|
-
return formatErrorResponse(415, "invalid_request_error", err.message);
|
|
592
|
-
}
|
|
593
|
-
if (err instanceof DecompressedBodyTooLargeError) {
|
|
594
|
-
return formatErrorResponse(413, "invalid_request_error", err.message);
|
|
595
|
-
}
|
|
596
|
-
console.warn(`[${label}] request body decode/parse failed: ${err instanceof Error ? `${err.name}: ${err.message}` : String(err)}`);
|
|
597
|
-
return formatErrorResponse(400, "invalid_request_error", "Invalid JSON body");
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
function comboUnavailableResponse(message: string): Response {
|
|
601
|
-
return new Response(
|
|
602
|
-
JSON.stringify({
|
|
603
|
-
error: { message, type: "server_error", code: "combo_unavailable" },
|
|
604
|
-
}),
|
|
605
|
-
{ status: 503, headers: { "Content-Type": "application/json" } },
|
|
606
|
-
);
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
interface ConsumedComboFailure {
|
|
610
|
-
response: Response;
|
|
611
|
-
classificationText: string;
|
|
612
|
-
/** Valid numeric/date value used only for cooldown calculation. */
|
|
613
|
-
retryAfter?: string;
|
|
614
|
-
/** Reserved for 040 usage attribution without adding another body read. */
|
|
615
|
-
usage?: OcxUsage;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
interface HandleResponsesOptions {
|
|
619
|
-
forceEmptyResponseId?: boolean;
|
|
620
|
-
abortSignal?: AbortSignal;
|
|
621
|
-
/** One-shot TTFT callback: first non-empty model output observed (WP4). */
|
|
622
|
-
onFirstOutput?: () => void;
|
|
623
|
-
onCodexAuthContextResolved?: (context: CodexAuthContext | undefined) => void;
|
|
624
|
-
recordTerminalOutcomes?: boolean;
|
|
625
|
-
setTerminalOutcomeRecorder?: (recorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined) => void;
|
|
626
|
-
onNativePassthroughTerminal?: (status: ResponsesTerminalStatus) => void;
|
|
627
|
-
onNativePassthroughCancel?: () => void;
|
|
628
|
-
/** Internal recursion guard; callers outside this module must not set it. */
|
|
629
|
-
comboAttempt?: boolean;
|
|
630
|
-
/** 030-owned handoff when a child consumed the original failure under bounds. */
|
|
631
|
-
onConsumedComboFailure?: (failure: ConsumedComboFailure) => void;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
function clientCancelledResponse(): Response {
|
|
635
|
-
return formatErrorResponse(499, "client_cancelled", "Client cancelled request");
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
function sanitizedRetryAfter(value: string | null, now: number): string | undefined {
|
|
639
|
-
const trimmed = value?.trim();
|
|
640
|
-
if (!trimmed || trimmed.length > 128) return undefined;
|
|
641
|
-
return parseRetryAfterMs(trimmed, now) !== undefined ? trimmed : undefined;
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
async function consumeComboFailure(
|
|
645
|
-
response: Response,
|
|
646
|
-
signal?: AbortSignal,
|
|
647
|
-
now = Date.now(),
|
|
648
|
-
): Promise<ConsumedComboFailure> {
|
|
649
|
-
const fallback = `Provider error ${response.status}`;
|
|
650
|
-
let classificationText = fallback;
|
|
651
|
-
let usage: OcxUsage | undefined;
|
|
652
|
-
try {
|
|
653
|
-
const body = await readBoundedResponseBody(response, { signal });
|
|
654
|
-
usage = usageFromComboFailureText(body.text);
|
|
655
|
-
if (body.displaySafe) {
|
|
656
|
-
const safeText = redactSecretString(body.text).slice(0, 500);
|
|
657
|
-
if (safeText) classificationText = safeText;
|
|
658
|
-
}
|
|
659
|
-
} catch (error) {
|
|
660
|
-
if (signal?.aborted) throw error;
|
|
661
|
-
classificationText = fallback;
|
|
662
|
-
}
|
|
663
|
-
const message = classificationText === fallback
|
|
664
|
-
? fallback
|
|
665
|
-
: `${fallback}: ${classificationText}`;
|
|
666
|
-
const retryAfter = sanitizedRetryAfter(response.headers.get("retry-after"), now);
|
|
667
|
-
return {
|
|
668
|
-
response: formatErrorResponse(response.status, "upstream_error", message),
|
|
669
|
-
classificationText,
|
|
670
|
-
...(retryAfter !== undefined ? { retryAfter } : {}),
|
|
671
|
-
...(usage ? { usage } : {}),
|
|
672
|
-
};
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
function usageFromComboFailureText(text: string): OcxUsage | undefined {
|
|
676
|
-
try {
|
|
677
|
-
const payload = JSON.parse(text) as Record<string, unknown>;
|
|
678
|
-
const nested = payload.response;
|
|
679
|
-
const source = nested && typeof nested === "object" && !Array.isArray(nested)
|
|
680
|
-
? nested as Record<string, unknown>
|
|
681
|
-
: payload;
|
|
682
|
-
return usageFromResponsesPayload(source.usage);
|
|
683
|
-
} catch {
|
|
684
|
-
return undefined;
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function createChildPassthroughCallbackGate(options: HandleResponsesOptions) {
|
|
689
|
-
type Pending =
|
|
690
|
-
| { kind: "terminal"; status: ResponsesTerminalStatus }
|
|
691
|
-
| { kind: "cancel" };
|
|
692
|
-
let state: "pending" | "committed" | "discarded" = "pending";
|
|
693
|
-
let pending: Pending | undefined;
|
|
694
|
-
let accepted = false;
|
|
695
|
-
const publish = (value: Pending): void => {
|
|
696
|
-
if (value.kind === "terminal") options.onNativePassthroughTerminal?.(value.status);
|
|
697
|
-
else options.onNativePassthroughCancel?.();
|
|
698
|
-
};
|
|
699
|
-
const receive = (value: Pending): void => {
|
|
700
|
-
if (state === "discarded" || accepted) return;
|
|
701
|
-
accepted = true;
|
|
702
|
-
if (state === "committed") return publish(value);
|
|
703
|
-
pending ??= value;
|
|
704
|
-
};
|
|
705
|
-
return {
|
|
706
|
-
onTerminal: (status: ResponsesTerminalStatus) => receive({ kind: "terminal", status }),
|
|
707
|
-
onCancel: () => receive({ kind: "cancel" }),
|
|
708
|
-
commit: () => {
|
|
709
|
-
if (state !== "pending") return;
|
|
710
|
-
state = "committed";
|
|
711
|
-
if (pending) publish(pending);
|
|
712
|
-
pending = undefined;
|
|
713
|
-
},
|
|
714
|
-
discard: () => {
|
|
715
|
-
state = "discarded";
|
|
716
|
-
pending = undefined;
|
|
717
|
-
},
|
|
718
|
-
};
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
export function buildComboChildHeaders(parentHeaders: HeadersInit): Headers {
|
|
722
|
-
const childHeaders = new Headers(parentHeaders);
|
|
723
|
-
// Combo children re-serialize already-decoded JSON. Keeping transport metadata from
|
|
724
|
-
// the parent would make the child decoder treat plain JSON as compressed bytes.
|
|
725
|
-
childHeaders.delete("content-length");
|
|
726
|
-
childHeaders.delete("content-encoding");
|
|
727
|
-
return childHeaders;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
async function handleComboResponses(
|
|
731
|
-
req: Request,
|
|
732
|
-
rawBody: unknown,
|
|
733
|
-
comboId: string,
|
|
734
|
-
config: OcxConfig,
|
|
735
|
-
logCtx: RequestLogContext,
|
|
736
|
-
options: HandleResponsesOptions,
|
|
737
|
-
): Promise<Response> {
|
|
738
|
-
const requestedModel = typeof (rawBody as { model?: unknown } | null)?.model === "string"
|
|
739
|
-
? (rawBody as { model: string }).model
|
|
740
|
-
: `combo/${comboId}`;
|
|
741
|
-
Object.assign(logCtx, {
|
|
742
|
-
requestedModel,
|
|
743
|
-
model: requestedModel,
|
|
744
|
-
provider: "combo",
|
|
745
|
-
comboId,
|
|
746
|
-
});
|
|
747
|
-
const combo = getCombo(config, comboId);
|
|
748
|
-
if (!combo) {
|
|
749
|
-
return formatErrorResponse(404, "invalid_request_error", `Unknown combo: ${comboId}`);
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
const initialNow = Date.now();
|
|
753
|
-
let pick = pickComboTarget(config, comboId, {
|
|
754
|
-
eligible: target => !isComboTargetInCooldown(comboId, target, initialNow),
|
|
755
|
-
});
|
|
756
|
-
if (!pick) {
|
|
757
|
-
return comboUnavailableResponse(`No available targets for combo: ${comboId}`);
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
let lastFailure: Response | null = null;
|
|
761
|
-
while (pick) {
|
|
762
|
-
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
763
|
-
const childLog: RequestLogContext = {
|
|
764
|
-
model: pick.target.model,
|
|
765
|
-
provider: pick.target.provider,
|
|
766
|
-
};
|
|
767
|
-
const targetRoute = routeModel(config, `${pick.target.provider}/${pick.target.model}`);
|
|
768
|
-
const childBody = concreteComboRequestBody(
|
|
769
|
-
rawBody,
|
|
770
|
-
pick.target,
|
|
771
|
-
comboDefaultEffort(config, comboId),
|
|
772
|
-
supportedLadderFor({ provider: targetRoute.provider, modelId: targetRoute.modelId }),
|
|
773
|
-
);
|
|
774
|
-
const childHeaders = buildComboChildHeaders(req.headers);
|
|
775
|
-
const childRequest = new Request(req.url, {
|
|
776
|
-
method: req.method,
|
|
777
|
-
headers: childHeaders,
|
|
778
|
-
body: JSON.stringify(childBody),
|
|
779
|
-
});
|
|
780
|
-
let resolvedAuth: CodexAuthContext | undefined;
|
|
781
|
-
let terminalRecorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined;
|
|
782
|
-
const started = Date.now();
|
|
783
|
-
const attempt = beginRequestAttempt(
|
|
784
|
-
(logCtx.attempts?.length ?? 0) + 1,
|
|
785
|
-
pick.target.provider,
|
|
786
|
-
pick.target.model,
|
|
787
|
-
config.providers[pick.target.provider]!.adapter,
|
|
788
|
-
);
|
|
789
|
-
childLog.activeAttempt = attempt;
|
|
790
|
-
let attemptRetained = false;
|
|
791
|
-
const retainCancelledAttempt = (): void => {
|
|
792
|
-
if (attemptRetained) return;
|
|
793
|
-
sealRequestAttemptIdentity(
|
|
794
|
-
attempt,
|
|
795
|
-
childLog.provider,
|
|
796
|
-
childLog.providerAdapter ?? attempt.adapter,
|
|
797
|
-
);
|
|
798
|
-
finishRequestAttempt(attempt, 499, Date.now() - started, childLog.usage);
|
|
799
|
-
(logCtx.attempts ??= []).push(attempt);
|
|
800
|
-
attemptRetained = true;
|
|
801
|
-
};
|
|
802
|
-
let consumedChildFailure: ConsumedComboFailure | undefined;
|
|
803
|
-
const callbackGate = createChildPassthroughCallbackGate(options);
|
|
804
|
-
let response: Response;
|
|
805
|
-
try {
|
|
806
|
-
response = await handleResponses(childRequest, config, childLog, {
|
|
807
|
-
...options,
|
|
808
|
-
comboAttempt: true,
|
|
809
|
-
// Attempt-relative TTFT is recorded HERE (not via childLog.firstOutputMs — a later
|
|
810
|
-
// Object.assign(logCtx, childLog) would overwrite the request-relative value).
|
|
811
|
-
onFirstOutput: () => {
|
|
812
|
-
if (attempt.firstOutputMs === undefined) {
|
|
813
|
-
attempt.firstOutputMs = Math.max(0, Date.now() - started);
|
|
814
|
-
}
|
|
815
|
-
options.onFirstOutput?.();
|
|
816
|
-
},
|
|
817
|
-
onCodexAuthContextResolved: value => { resolvedAuth = value; },
|
|
818
|
-
setTerminalOutcomeRecorder: value => { terminalRecorder = value; },
|
|
819
|
-
onConsumedComboFailure: value => { consumedChildFailure = value; },
|
|
820
|
-
onNativePassthroughTerminal: callbackGate.onTerminal,
|
|
821
|
-
onNativePassthroughCancel: callbackGate.onCancel,
|
|
822
|
-
});
|
|
823
|
-
} catch (error) {
|
|
824
|
-
callbackGate.discard();
|
|
825
|
-
if (options.abortSignal?.aborted) {
|
|
826
|
-
retainCancelledAttempt();
|
|
827
|
-
return clientCancelledResponse();
|
|
828
|
-
}
|
|
829
|
-
throw error;
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
if (options.abortSignal?.aborted) {
|
|
833
|
-
callbackGate.discard();
|
|
834
|
-
retainCancelledAttempt();
|
|
835
|
-
return clientCancelledResponse();
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
if (response.ok) {
|
|
839
|
-
sealRequestAttemptIdentity(
|
|
840
|
-
attempt,
|
|
841
|
-
childLog.provider,
|
|
842
|
-
childLog.providerAdapter ?? attempt.adapter,
|
|
843
|
-
);
|
|
844
|
-
(logCtx.attempts ??= []).push(attempt);
|
|
845
|
-
attemptRetained = true;
|
|
846
|
-
noteComboSuccess(comboId, combo, pick.target);
|
|
847
|
-
Object.assign(logCtx, childLog, {
|
|
848
|
-
requestedModel,
|
|
849
|
-
model: requestedModel,
|
|
850
|
-
provider: "combo",
|
|
851
|
-
comboId,
|
|
852
|
-
attempts: logCtx.attempts,
|
|
853
|
-
activeAttempt: attempt,
|
|
854
|
-
activeAttemptStartedAt: started,
|
|
855
|
-
resolvedModel: childLog.resolvedModel ?? childLog.model,
|
|
856
|
-
});
|
|
857
|
-
options.onCodexAuthContextResolved?.(resolvedAuth);
|
|
858
|
-
options.setTerminalOutcomeRecorder?.(terminalRecorder);
|
|
859
|
-
callbackGate.commit();
|
|
860
|
-
return response;
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
callbackGate.discard();
|
|
864
|
-
if (response.status === 499) {
|
|
865
|
-
retainCancelledAttempt();
|
|
866
|
-
return clientCancelledResponse();
|
|
867
|
-
}
|
|
868
|
-
let failure: ConsumedComboFailure;
|
|
869
|
-
try {
|
|
870
|
-
failure = consumedChildFailure
|
|
871
|
-
?? await consumeComboFailure(response, options.abortSignal);
|
|
872
|
-
} catch (error) {
|
|
873
|
-
if (options.abortSignal?.aborted) {
|
|
874
|
-
retainCancelledAttempt();
|
|
875
|
-
return clientCancelledResponse();
|
|
876
|
-
}
|
|
877
|
-
throw error;
|
|
878
|
-
}
|
|
879
|
-
if (options.abortSignal?.aborted) {
|
|
880
|
-
retainCancelledAttempt();
|
|
881
|
-
return clientCancelledResponse();
|
|
882
|
-
}
|
|
883
|
-
sealRequestAttemptIdentity(
|
|
884
|
-
attempt,
|
|
885
|
-
childLog.provider,
|
|
886
|
-
childLog.providerAdapter ?? attempt.adapter,
|
|
887
|
-
);
|
|
888
|
-
finishRequestAttempt(
|
|
889
|
-
attempt,
|
|
890
|
-
response.status,
|
|
891
|
-
Date.now() - started,
|
|
892
|
-
failure.usage,
|
|
893
|
-
);
|
|
894
|
-
(logCtx.attempts ??= []).push(attempt);
|
|
895
|
-
attemptRetained = true;
|
|
896
|
-
lastFailure = failure.response;
|
|
897
|
-
if (comboFailureDecision(response.status, failure.classificationText) === "stop") {
|
|
898
|
-
Object.assign(logCtx, childLog, {
|
|
899
|
-
requestedModel,
|
|
900
|
-
model: requestedModel,
|
|
901
|
-
provider: "combo",
|
|
902
|
-
comboId,
|
|
903
|
-
attempts: logCtx.attempts,
|
|
904
|
-
activeAttempt: undefined,
|
|
905
|
-
activeAttemptStartedAt: undefined,
|
|
906
|
-
});
|
|
907
|
-
return lastFailure;
|
|
908
|
-
}
|
|
909
|
-
console.warn(
|
|
910
|
-
`[combo] ${comboId}: ${targetKey(pick.target)} failed with ${response.status} after ${Date.now() - started}ms`,
|
|
911
|
-
);
|
|
912
|
-
pick = advanceComboAfterFailure(config, pick, {
|
|
913
|
-
retryAfter: failure.retryAfter,
|
|
914
|
-
now: Date.now(),
|
|
915
|
-
});
|
|
916
|
-
}
|
|
917
|
-
return lastFailure!;
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
export async function handleResponses(
|
|
921
|
-
req: Request,
|
|
922
|
-
config: OcxConfig,
|
|
923
|
-
logCtx: RequestLogContext,
|
|
924
|
-
options: HandleResponsesOptions = {},
|
|
925
|
-
): Promise<Response> {
|
|
926
|
-
let body: unknown;
|
|
927
|
-
try {
|
|
928
|
-
body = await readJsonRequestBody(req);
|
|
929
|
-
} catch (err) {
|
|
930
|
-
return decodeRequestErrorResponse(err, "responses");
|
|
931
|
-
}
|
|
932
|
-
const comboId = !options.comboAttempt ? comboIdFromRawBody(body, config) : null;
|
|
933
|
-
if (comboId && Object.hasOwn(config.combos ?? {}, comboId)) {
|
|
934
|
-
return handleComboResponses(req, body, comboId, config, logCtx, options);
|
|
935
|
-
}
|
|
936
|
-
const originalBody = body;
|
|
937
|
-
body = expandPreviousResponseInput(body);
|
|
938
|
-
const previousResponseInputExpanded = body !== originalBody;
|
|
939
|
-
const unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
|
|
940
|
-
(body as { input?: unknown } | undefined)?.input,
|
|
941
|
-
);
|
|
942
|
-
|
|
943
|
-
// Spawn-message compatibility (both directions): agent_message task payloads ride in
|
|
944
|
-
// encrypted_content slots as plaintext. Rewrite them to input_text on the RAW body BEFORE
|
|
945
|
-
// parsing so every consumer sees the payload: parseRequest (routed/translated providers read
|
|
946
|
-
// the parsed messages) and the native passthrough (_rawBody is this same object, serialized
|
|
947
|
-
// verbatim). Genuine backend ciphertext is left byte-identical (looksLikeBackendCiphertext).
|
|
948
|
-
{
|
|
949
|
-
const rewritten = sanitizeEncryptedContentInPlace(
|
|
950
|
-
(body as { input?: unknown } | undefined)?.input,
|
|
951
|
-
);
|
|
952
|
-
if (rewritten > 0)
|
|
953
|
-
console.warn(
|
|
954
|
-
`[opencodex] rewrote ${rewritten} plaintext encrypted_content part(s) to input_text (spawn-message compatibility)`,
|
|
955
|
-
);
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
let parsed;
|
|
959
|
-
try {
|
|
960
|
-
parsed = parseRequest(body);
|
|
961
|
-
if (previousResponseInputExpanded) parsed._previousResponseInputExpanded = true;
|
|
962
|
-
parsed._providerContinuation = previousResponseProviderState(parsed.previousResponseId);
|
|
963
|
-
parsed._cursorConversationId = parsed._providerContinuation?.cursor?.conversationId;
|
|
964
|
-
} catch (err) {
|
|
965
|
-
return formatErrorResponse(400, "invalid_request_error", err instanceof Error ? err.message : String(err));
|
|
966
|
-
}
|
|
967
|
-
logCtx.requestedModel = parsed.modelId;
|
|
968
|
-
logCtx.requestedEffort = parsed.options.reasoning;
|
|
969
|
-
logCtx.requestedServiceTier = parsed.options.serviceTier;
|
|
970
|
-
logCtx.requestedSpeedLabel = requestLogSpeedLabel(parsed.options.serviceTier);
|
|
971
|
-
logCtx.configuredServiceTier = readConfiguredCodexServiceTier();
|
|
972
|
-
logCtx.configuredSpeedLabel = requestLogSpeedLabel(logCtx.configuredServiceTier);
|
|
973
|
-
|
|
974
|
-
// Shadow call intercept: rewrite Codex's hard-coded helper calls
|
|
975
|
-
// (gpt-5.4-mini on older clients, gpt-5.6-luna on 0.145.0+)
|
|
976
|
-
const _sci = config.shadowCallIntercept;
|
|
977
|
-
if (_sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
|
|
978
|
-
const _sciOriginal = parsed.modelId;
|
|
979
|
-
parsed.modelId = _sci.model;
|
|
980
|
-
if (parsed._rawBody && typeof parsed._rawBody === "object") {
|
|
981
|
-
(parsed._rawBody as { model?: string }).model = _sci.model;
|
|
982
|
-
}
|
|
983
|
-
// Force effort to low for shadow/helper calls (matching upstream behavior)
|
|
984
|
-
parsed.options.reasoning = "low";
|
|
985
|
-
if (parsed._rawBody && typeof parsed._rawBody === "object") {
|
|
986
|
-
(parsed._rawBody as Record<string, unknown>).reasoning = { effort: "low" };
|
|
987
|
-
}
|
|
988
|
-
(logCtx as unknown as Record<string, unknown>).shadowCallRewrittenFrom = _sciOriginal;
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
let route;
|
|
992
|
-
try {
|
|
993
|
-
route = routeModel(config, parsed.modelId);
|
|
994
|
-
} catch (err) {
|
|
995
|
-
if (err instanceof NoAvailableComboTargetsError) {
|
|
996
|
-
return comboUnavailableResponse(err.message);
|
|
997
|
-
}
|
|
998
|
-
return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
// The canonical ChatGPT backend can decrypt its V2 Fernet task tokens; routed
|
|
1002
|
-
// providers cannot. Reject the raw-input classification before adapter construction
|
|
1003
|
-
// or provider dispatch so an unreadable worker task cannot trigger a cost storm.
|
|
1004
|
-
if (!isCanonicalOpenAiForwardProvider(route.provider) && unreadableEncryptedAgentTask) {
|
|
1005
|
-
return formatErrorResponse(
|
|
1006
|
-
400,
|
|
1007
|
-
"invalid_request_error",
|
|
1008
|
-
"Routed V2 worker task is encrypted for the native ChatGPT backend and cannot be read by the selected provider. Use plaintext V2 agent-message delivery or select a native ChatGPT model.",
|
|
1009
|
-
);
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
// Apply the routed model id upstream: routing may strip a "<provider>/" namespace
|
|
1013
|
-
// (e.g. "opencode-go/deepseek-v4-pro" → "deepseek-v4-pro"). Adapters read parsed.modelId,
|
|
1014
|
-
// and the passthrough adapter serializes _rawBody, so rewrite both.
|
|
1015
|
-
if (route.modelId !== parsed.modelId) {
|
|
1016
|
-
if (parsed._rawBody && typeof parsed._rawBody === "object") {
|
|
1017
|
-
(parsed._rawBody as { model?: string }).model = route.modelId;
|
|
1018
|
-
}
|
|
1019
|
-
parsed.modelId = route.modelId;
|
|
1020
|
-
}
|
|
1021
|
-
logCtx.model = route.modelId;
|
|
1022
|
-
logCtx.provider = route.providerName;
|
|
1023
|
-
logCtx.providerAdapter = route.provider.adapter;
|
|
1024
|
-
|
|
1025
|
-
// Virtual model rewriting: Pro aliases → base model + reasoning.mode="pro".
|
|
1026
|
-
// Must run before effort caps/native clamps so the base model gets correct limits.
|
|
1027
|
-
applyOpenAiVirtualModel(parsed, route, logCtx);
|
|
1028
|
-
|
|
1029
|
-
// Fast mode override: when config.fastMode is explicitly set, inject or strip
|
|
1030
|
-
// service_tier for OpenAI-routed models. Undefined = passthrough (client decides).
|
|
1031
|
-
if (config.fastMode !== undefined && route.provider.adapter === "openai-responses") {
|
|
1032
|
-
const tier = config.fastMode ? "priority" : undefined;
|
|
1033
|
-
if (parsed._rawBody && typeof parsed._rawBody === "object") {
|
|
1034
|
-
if (tier) (parsed._rawBody as Record<string, unknown>).service_tier = tier;
|
|
1035
|
-
else delete (parsed._rawBody as Record<string, unknown>).service_tier;
|
|
1036
|
-
}
|
|
1037
|
-
parsed.options.serviceTier = tier;
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
// Multi-agent guidance shim: codex-rs emits its Proactive delegation developer
|
|
1041
|
-
// message only on the v2 surface. The proxy fills the gaps: the Proactive text
|
|
1042
|
-
// for v1 collab surfaces at the top tier (no model designation on v1), and the
|
|
1043
|
-
// sub-agent model/roster designation plus fork_turns override rules on v2.
|
|
1044
|
-
// The surface is judged from the request's own tool list. Runs BEFORE the
|
|
1045
|
-
// mock-max clamp below so the synthetic top tier (ultra arrives as max on the
|
|
1046
|
-
// codex wire) is still visible. Both request shapes are rewritten.
|
|
1047
|
-
{
|
|
1048
|
-
const guidance = await multiAgentGuidanceText(parsed, {
|
|
1049
|
-
multiAgentGuidanceEnabled: config.multiAgentGuidanceEnabled,
|
|
1050
|
-
injectionModel: config.injectionModel,
|
|
1051
|
-
injectionEffort: config.injectionEffort,
|
|
1052
|
-
subagentModels: config.subagentModels,
|
|
1053
|
-
injectionPrompt: config.injectionPrompt,
|
|
1054
|
-
});
|
|
1055
|
-
if (guidance) {
|
|
1056
|
-
injectDeveloperMessage(parsed, guidance);
|
|
1057
|
-
if (isInjectionDebugEnabled()) injectionDebugLog(`[opencodex] ${route.modelId}: multi-agent guidance injected (surface=${collabSurface(parsed)}, guidanceEnabled=${multiAgentGuidanceEnabled(config)}, ${guidance.length} chars)`);
|
|
1058
|
-
} else if (isInjectionDebugEnabled() && collabSurface(parsed) !== null) {
|
|
1059
|
-
injectionDebugLog(`[opencodex] ${route.modelId}: collab surface=${collabSurface(parsed)}, guidance silent (effort=${parsed.options.reasoning ?? "unset"}, injectionModel=${config.injectionModel ?? "unset"})`);
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
// Hard effort caps (effortCap / subagentEffortCap): enforcement companion to the advisory
|
|
1064
|
-
// injection above — spawn-arg prompting cannot stop codex-rs from inheriting the parent's
|
|
1065
|
-
// ultra-tier default on bare spawns (see src/server/effort-policy.ts). Runs BEFORE the
|
|
1066
|
-
// mock-max clamp so a capped effort is what nativeness clamping then validates; rewrites
|
|
1067
|
-
// both request shapes (same dual-write contract as the clamp below).
|
|
1068
|
-
// GATE: v2 feature only (effortCapAppliesTo) — v2-surface main turns plus header-marked
|
|
1069
|
-
// child turns admitted regardless of tool surface (depth-limited leaves carry no collab
|
|
1070
|
-
// tools while shallower children do, so tool sniffing alone would cap siblings
|
|
1071
|
-
// inconsistently); multiAgentMode "v1" disables caps entirely; compaction turns bypass
|
|
1072
|
-
// caps so routed compaction matches native /v1/responses/compact (which never enters
|
|
1073
|
-
// handleResponses).
|
|
1074
|
-
{
|
|
1075
|
-
const { applyEffortCap, effortCapAppliesTo, supportedLadderFor } = await import("./effort-policy");
|
|
1076
|
-
const surface = collabSurface(parsed);
|
|
1077
|
-
if (effortCapAppliesTo(surface, req.headers, config, parsed._compactionRequest === true)) {
|
|
1078
|
-
const capped = applyEffortCap(parsed, req.headers, config, supportedLadderFor(route));
|
|
1079
|
-
if (capped) {
|
|
1080
|
-
logCtx.requestedEffort = `${capped.from}->${capped.to}`;
|
|
1081
|
-
if (isInjectionDebugEnabled()) {
|
|
1082
|
-
injectionDebugLog(`[opencodex] ${route.modelId}: effort cap applied (${capped.from} -> ${capped.to}, ${capped.subagent ? "sub-agent" : "main"} turn)`);
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
} else if (isInjectionDebugEnabled() && (config.effortCap || config.subagentEffortCap)) {
|
|
1086
|
-
injectionDebugLog(`[opencodex] ${route.modelId}: effort cap skipped (surface=${surface ?? "none"}, v2 feature only)`);
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
// Mock-max clamp: native models whose real ladder stops below max (gpt-5.5/5.4/…)
|
|
1091
|
-
// receive `max` when the user picks Ultra (codex converts ultra->max client-side).
|
|
1092
|
-
// Clamp to the model's highest real effort BEFORE any adapter — the ChatGPT
|
|
1093
|
-
// passthrough serializes _rawBody verbatim, so both shapes must be rewritten.
|
|
1094
|
-
// GUARD: judge nativeness by BOTH the originally requested id (logCtx.requestedModel)
|
|
1095
|
-
// and the resolved provider identity. Routing strips the "<provider>/" namespace, and
|
|
1096
|
-
// some third-party providers expose bare `defaultModel` selectors, so route.modelId
|
|
1097
|
-
// alone can make a routed model masquerade as an off-snapshot native. Only the
|
|
1098
|
-
// canonical built-in ChatGPT forward provider should receive the native clamp.
|
|
1099
|
-
{
|
|
1100
|
-
const requestedModelId = logCtx.requestedModel ?? route.modelId;
|
|
1101
|
-
const { nativeEffortClamp, shouldApplyNativeEffortClamp } = await import("../codex/catalog");
|
|
1102
|
-
const clamped = shouldApplyNativeEffortClamp(route.providerName, route.provider, requestedModelId)
|
|
1103
|
-
? nativeEffortClamp(route.modelId, parsed.options.reasoning)
|
|
1104
|
-
: null;
|
|
1105
|
-
if (clamped) {
|
|
1106
|
-
parsed.options.reasoning = clamped;
|
|
1107
|
-
const raw = parsed._rawBody as { reasoning?: { effort?: string } } | undefined;
|
|
1108
|
-
if (raw?.reasoning && typeof raw.reasoning === "object") raw.reasoning.effort = clamped;
|
|
1109
|
-
logCtx.requestedEffort = `${logCtx.requestedEffort ?? "max"}->${clamped}`;
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
logCtx.modelSupportsServiceTier = catalogModelSupportsServiceTier(
|
|
1113
|
-
route.modelId,
|
|
1114
|
-
logCtx.requestedServiceTier ?? logCtx.configuredServiceTier,
|
|
1115
|
-
);
|
|
1116
|
-
|
|
1117
|
-
let authCtx: CodexAuthContext = { kind: "main", accountId: null };
|
|
1118
|
-
let selectedForwardHeaders: Headers;
|
|
1119
|
-
try {
|
|
1120
|
-
if (route.codexAccountMode === "direct") validateForwardAdmissionCredential(req.headers, config);
|
|
1121
|
-
if (route.codexAccountMode) {
|
|
1122
|
-
authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode);
|
|
1123
|
-
options.onCodexAuthContextResolved?.(authCtx);
|
|
1124
|
-
} else {
|
|
1125
|
-
options.onCodexAuthContextResolved?.(undefined);
|
|
1126
|
-
}
|
|
1127
|
-
selectedForwardHeaders = headersForCodexAuthContext(req.headers, authCtx);
|
|
1128
|
-
} catch (err) {
|
|
1129
|
-
if (err instanceof CodexAccountCooldownError) {
|
|
1130
|
-
return formatErrorResponse(429, "rate_limit_error", "Selected Codex account is cooling down");
|
|
1131
|
-
}
|
|
1132
|
-
if (err instanceof CodexThreadAffinityExpiredError) {
|
|
1133
|
-
return formatErrorResponse(409, "invalid_request_error", "Codex thread account affinity expired; start a new session");
|
|
1134
|
-
}
|
|
1135
|
-
if (err instanceof CodexAuthContextError) {
|
|
1136
|
-
const safeAccountLabel = formatCodexProviderForLog(route.providerName, err.accountId, config);
|
|
1137
|
-
console.error(`[codex-auth] Pool account ${safeAccountLabel} token failed; reauthentication required`);
|
|
1138
|
-
return formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication");
|
|
1139
|
-
}
|
|
1140
|
-
if (err instanceof CodexPoolAuthenticationError) {
|
|
1141
|
-
return formatErrorResponse(401, "authentication_error", err.message);
|
|
1142
|
-
}
|
|
1143
|
-
if (err instanceof CodexDirectAuthenticationError) {
|
|
1144
|
-
return formatErrorResponse(401, "authentication_error", err.message);
|
|
1145
|
-
}
|
|
1146
|
-
if (err instanceof ForwardAdmissionCredentialError) {
|
|
1147
|
-
return formatErrorResponse(401, "authentication_error", err.message);
|
|
1148
|
-
}
|
|
1149
|
-
throw err;
|
|
1150
|
-
}
|
|
1151
|
-
if (!isCodexAuthContextUsable(authCtx, config)) {
|
|
1152
|
-
return formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication");
|
|
1153
|
-
}
|
|
1154
|
-
route.provider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
|
|
1155
|
-
logCtx.provider = formatCodexProviderForLog(route.providerName, codexLogAccountId(authCtx), config);
|
|
1156
|
-
|
|
1157
|
-
// OAuth providers: swap in a fresh access token (auto-refreshed) as the Bearer key, so the
|
|
1158
|
-
// existing openai-chat / anthropic adapters authenticate with no change.
|
|
1159
|
-
const isOAuth401ReplayProvider = (route.providerName === "xai" || route.providerName === "github-copilot" || route.providerName === "kiro")
|
|
1160
|
-
&& route.provider.authMode === "oauth";
|
|
1161
|
-
let sentOAuthSnapshot: OAuthAccessSnapshot | undefined;
|
|
1162
|
-
if (route.provider.authMode === "oauth") {
|
|
1163
|
-
try {
|
|
1164
|
-
const resolved = await getValidAccessTokenSnapshot(route.providerName);
|
|
1165
|
-
if (isOAuth401ReplayProvider) sentOAuthSnapshot = resolved;
|
|
1166
|
-
route.provider = { ...route.provider, apiKey: resolved.accessToken };
|
|
1167
|
-
// Antigravity (cloud-code-assist) needs the discovered Cloud Code Assist project id in the
|
|
1168
|
-
// CCA envelope; the server injects only the bare token, so pull project from the credential.
|
|
1169
|
-
if (route.provider.googleMode === "cloud-code-assist" && !route.provider.project) {
|
|
1170
|
-
const projectId = getOAuthCredentialProjectId(route.providerName);
|
|
1171
|
-
if (projectId) route.provider = { ...route.provider, project: projectId };
|
|
1172
|
-
}
|
|
1173
|
-
} catch (err) {
|
|
1174
|
-
if (err instanceof UnsupportedOAuthProviderError) {
|
|
1175
|
-
return formatErrorResponse(
|
|
1176
|
-
400,
|
|
1177
|
-
"invalid_request_error",
|
|
1178
|
-
`${err.message}. Remove or reconfigure provider '${route.providerName}' in ${getConfigPath()}.`,
|
|
1179
|
-
);
|
|
1180
|
-
}
|
|
1181
|
-
return formatErrorResponse(401, "authentication_error", err instanceof Error ? err.message : String(err));
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
route.provider = resolveProviderTransport(
|
|
1185
|
-
route.providerName,
|
|
1186
|
-
route.provider,
|
|
1187
|
-
parsed.options.promptCacheKey,
|
|
1188
|
-
route.providerName === "github-copilot" ? getOAuthCredentialApiBaseUrl(route.providerName) : undefined,
|
|
1189
|
-
);
|
|
1190
|
-
const adapterProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider);
|
|
1191
|
-
const adapter = resolveAdapter(adapterProvider, config.cacheRetention);
|
|
1192
|
-
logCtx.providerAdapter = adapter.name;
|
|
1193
|
-
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, adapter.name);
|
|
1194
|
-
const isPassthrough = "passthrough" in adapter && !!adapter.passthrough;
|
|
1195
|
-
|
|
1196
|
-
if (adapter.name === "kiro" && parsed.previousResponseId && !parsed._previousResponseInputExpanded) {
|
|
1197
|
-
return formatErrorResponse(
|
|
1198
|
-
400,
|
|
1199
|
-
"invalid_request_error",
|
|
1200
|
-
"Kiro continuation state is missing; start a new session instead of reusing this previous_response_id.",
|
|
1201
|
-
);
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
let openAiSidecar: ResolvedOpenAiForwardSidecar | undefined;
|
|
1205
|
-
const needsOpenAiVision = shouldResolveOpenAiVisionSidecar(config, route.provider, route.modelId, parsed);
|
|
1206
|
-
const needsOpenAiSearch = shouldResolveOpenAiWebSearchSidecar(config, parsed, isPassthrough);
|
|
1207
|
-
if (needsOpenAiVision || needsOpenAiSearch) {
|
|
1208
|
-
try {
|
|
1209
|
-
openAiSidecar = await resolveFirstUsableOpenAiSidecar(
|
|
1210
|
-
listOpenAiForwardSidecarCandidates(config),
|
|
1211
|
-
req.headers,
|
|
1212
|
-
config,
|
|
1213
|
-
);
|
|
1214
|
-
} catch (err) {
|
|
1215
|
-
// Sidecars are optional helpers for an otherwise independent routed turn.
|
|
1216
|
-
// An unavailable/cooling/expired Multi credential disables the helper; it
|
|
1217
|
-
// must not turn a valid routed-provider request into a Codex-auth failure.
|
|
1218
|
-
if (
|
|
1219
|
-
!(err instanceof CodexPoolAuthenticationError)
|
|
1220
|
-
&& !(err instanceof CodexAuthContextError)
|
|
1221
|
-
&& !(err instanceof CodexAccountCooldownError)
|
|
1222
|
-
&& !(err instanceof CodexThreadAffinityExpiredError)
|
|
1223
|
-
) throw err;
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
// Vision sidecar: the routed model can't see images (provider.noVisionModels). Describe each
|
|
1228
|
-
// attached image through the selected sidecar backend and replace it with text BEFORE the main
|
|
1229
|
-
// call, so the text-only model can reason about it.
|
|
1230
|
-
const visionPlan = planVisionSidecar(config, route.provider, route.modelId, parsed, openAiSidecar);
|
|
1231
|
-
const recordSidecarOutcome = openAiSidecar?.recordOutcome;
|
|
1232
|
-
if (visionPlan) {
|
|
1233
|
-
await describeImagesInPlace(parsed, visionPlan, openAiSidecar?.headers ?? selectedForwardHeaders, options.abortSignal, recordSidecarOutcome);
|
|
1234
|
-
} else if (modelInList(route.provider.noVisionModels, route.modelId)) {
|
|
1235
|
-
// Sidecar-covered model but NO plan (no forward provider / missing forwarded auth / sidecar
|
|
1236
|
-
// disabled): fail closed — never forward raw images to a text-only upstream.
|
|
1237
|
-
stripImagesInPlace(parsed);
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
const recordTerminalOutcomes = options.recordTerminalOutcomes !== false;
|
|
1241
|
-
|
|
1242
|
-
const continuationStateForResponse = (
|
|
1243
|
-
emitted?: OcxProviderContinuationState,
|
|
1244
|
-
): OcxProviderContinuationState | undefined => {
|
|
1245
|
-
const cursorConversationId = parsed._cursorConversationId;
|
|
1246
|
-
const inherited = parsed._providerContinuation;
|
|
1247
|
-
if (!emitted && !inherited && !cursorConversationId) return undefined;
|
|
1248
|
-
return {
|
|
1249
|
-
...(inherited ?? {}),
|
|
1250
|
-
...(emitted ?? {}),
|
|
1251
|
-
...((inherited?.kiro || emitted?.kiro)
|
|
1252
|
-
? { kiro: { ...(inherited?.kiro ?? {}), ...(emitted?.kiro ?? {}) } }
|
|
1253
|
-
: {}),
|
|
1254
|
-
...(cursorConversationId
|
|
1255
|
-
? {
|
|
1256
|
-
cursor: {
|
|
1257
|
-
...(inherited?.cursor ?? {}),
|
|
1258
|
-
...(emitted?.cursor ?? {}),
|
|
1259
|
-
conversationId: cursorConversationId,
|
|
1260
|
-
},
|
|
1261
|
-
}
|
|
1262
|
-
: {}),
|
|
1263
|
-
};
|
|
1264
|
-
};
|
|
1265
|
-
|
|
1266
|
-
// Remote compaction v2 on a ROUTED model: Codex sent `compaction_trigger` and requires exactly
|
|
1267
|
-
// one `{type:"compaction"}` output item (codex-rs compact_remote_v2.rs). Passthrough handles it
|
|
1268
|
-
// natively upstream; here we run the routed model as a plain summarizer — no tools, no web-search
|
|
1269
|
-
// sidecar — and the bridge appends the synthetic compaction item (src/responses/compaction.ts).
|
|
1270
|
-
const routedCompaction = parsed._compactionRequest === true && !("passthrough" in adapter && adapter.passthrough);
|
|
1271
|
-
if (routedCompaction) {
|
|
1272
|
-
delete parsed.context.tools;
|
|
1273
|
-
delete parsed._webSearch;
|
|
1274
|
-
delete parsed.options.toolChoice;
|
|
1275
|
-
delete parsed.options.parallelToolCalls;
|
|
1276
|
-
parsed.context.messages.push({ role: "user", content: COMPACT_PROMPT, timestamp: Date.now() });
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
if ("passthrough" in adapter && adapter.passthrough) {
|
|
1280
|
-
// Local continuation cache for the ChatGPT passthrough. Codex WS turns chain with
|
|
1281
|
-
// previous_response_id, ocx converts them to internal HTTP requests, and the ChatGPT Codex
|
|
1282
|
-
// REST backend rejects the parameter — the adapter strips it in forward mode, so the ONLY
|
|
1283
|
-
// way a chained turn keeps its earlier context is the local replay expansion. Record
|
|
1284
|
-
// completed passthrough responses (force bypasses Codex's blanket store:false) so the next
|
|
1285
|
-
// turn's expansion hits. Never record a body whose own previous_response_id failed to
|
|
1286
|
-
// expand: its input is a delta, and storing it would replay a truncated conversation.
|
|
1287
|
-
// Compaction turns are excluded: _rawBody still carries the full pre-compaction history and
|
|
1288
|
-
// recording it would let a later expansion rehydrate the chain Codex just replaced.
|
|
1289
|
-
const passthroughRecordEligible = parsed._compactionRequest !== true
|
|
1290
|
-
&& (!parsed.previousResponseId || parsed._previousResponseInputExpanded === true);
|
|
1291
|
-
const rememberPassthroughResponse = passthroughRecordEligible
|
|
1292
|
-
? (response: { id?: unknown; output?: unknown; status?: unknown }) =>
|
|
1293
|
-
rememberResponseState(parsed._rawBody, response, undefined, { force: true })
|
|
1294
|
-
: undefined;
|
|
1295
|
-
if (parsed.previousResponseId && !parsed._previousResponseInputExpanded) {
|
|
1296
|
-
console.warn(
|
|
1297
|
-
`[responses] previous_response_id ${parsed.previousResponseId} not found in local replay state `
|
|
1298
|
-
+ `(model ${parsed.modelId}); forwarding without it — earlier turns may be missing from this request`,
|
|
1299
|
-
);
|
|
1300
|
-
}
|
|
1301
|
-
const request = await adapter.buildRequest(parsed, { headers: selectedForwardHeaders });
|
|
1302
|
-
const passthroughEstimate = typeof request.usageLog?.inputTokens === "number"
|
|
1303
|
-
? request.usageLog.inputTokens
|
|
1304
|
-
: undefined;
|
|
1305
|
-
if (passthroughEstimate !== undefined) {
|
|
1306
|
-
logCtx.usageLogInputTokens = passthroughEstimate;
|
|
1307
|
-
}
|
|
1308
|
-
// Abort the upstream if the client disconnects. A directly-relayed body does not propagate the
|
|
1309
|
-
// consumer's cancel to a signalled fetch, so we pass the signal and relay through relayWithAbort,
|
|
1310
|
-
// whose cancel() aborts the upstream — preventing leaked connections (RC2, passthrough path).
|
|
1311
|
-
const upstream = new AbortController();
|
|
1312
|
-
linkAbortSignal(upstream, options.abortSignal);
|
|
1313
|
-
const connectMs = config.connectTimeoutMs ?? 200_000;
|
|
1314
|
-
let upstreamResponse: Response;
|
|
1315
|
-
try {
|
|
1316
|
-
// Transient-5xx pre-stream retry (devlog/_plan/260716_claudecode_hardening/010):
|
|
1317
|
-
// the ChatGPT backend emits transient 502/520s that an immediate retry absorbs.
|
|
1318
|
-
// Body is a replayable string; nothing has streamed to the client yet.
|
|
1319
|
-
upstreamResponse = await fetchWithTransientRetry(
|
|
1320
|
-
recovery => {
|
|
1321
|
-
noteAttemptSend(logCtx.activeAttempt, passthroughEstimate, recovery);
|
|
1322
|
-
return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({
|
|
1323
|
-
method: request.method,
|
|
1324
|
-
headers: request.headers,
|
|
1325
|
-
body: request.body,
|
|
1326
|
-
}, recovery), upstream.signal, connectMs, parsed.stream, providerFetch(route.provider));
|
|
1327
|
-
},
|
|
1328
|
-
{ abortSignal: upstream.signal, label: safeHostLabel(request.url) },
|
|
1329
|
-
);
|
|
1330
|
-
} catch (err) {
|
|
1331
|
-
upstream.abort();
|
|
1332
|
-
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
1333
|
-
const outcome = err instanceof Error && err.name === "TimeoutError" ? "timeout" : "connect_error";
|
|
1334
|
-
if (usesCodexForwardPoolAuth(authCtx, route.provider)) {
|
|
1335
|
-
recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
|
|
1336
|
-
threadId: req.headers.get("x-codex-parent-thread-id"),
|
|
1337
|
-
});
|
|
1338
|
-
}
|
|
1339
|
-
const msg = outcome === "timeout"
|
|
1340
|
-
? `Provider connect timeout after ${connectMs}ms`
|
|
1341
|
-
: `Provider unreachable: ${err instanceof Error ? err.message : String(err)}`;
|
|
1342
|
-
return formatErrorResponse(502, "upstream_error", msg);
|
|
1343
|
-
}
|
|
1344
|
-
const headers = sanitizePassthroughHeaders(upstreamResponse.headers);
|
|
1345
|
-
const resolvedModel = headers.get("openai-model")?.trim();
|
|
1346
|
-
if (resolvedModel) logCtx.resolvedModel = resolvedModel;
|
|
1347
|
-
if (isUsageDebugEnabled()) {
|
|
1348
|
-
const upstreamContentType = upstreamResponse.headers.get("content-type");
|
|
1349
|
-
if (upstreamContentType) logCtx.usageDebugContentType = upstreamContentType;
|
|
1350
|
-
}
|
|
1351
|
-
// The chatgpt backend may omit Content-Type on SSE responses. Fall back to
|
|
1352
|
-
// treating a successful body as SSE when the caller requested streaming.
|
|
1353
|
-
const passthroughCt = headers.get("content-type")?.toLowerCase();
|
|
1354
|
-
const isEventStream = passthroughCt?.includes("text/event-stream")
|
|
1355
|
-
|| (upstreamResponse.ok && !!upstreamResponse.body && !passthroughCt && parsed.stream);
|
|
1356
|
-
const terminalRecorder = codexForwardTerminalOutcomeRecorder(
|
|
1357
|
-
config,
|
|
1358
|
-
authCtx,
|
|
1359
|
-
route.provider,
|
|
1360
|
-
logCtx,
|
|
1361
|
-
req.headers.get("x-codex-parent-thread-id"),
|
|
1362
|
-
);
|
|
1363
|
-
const terminalBodyWillRecord = !!terminalRecorder && upstreamResponse.ok && isEventStream;
|
|
1364
|
-
// Capture quota from upstream response for multi-account tracking
|
|
1365
|
-
if (usesCodexForwardPoolAuth(authCtx, route.provider)) {
|
|
1366
|
-
// primary was the 5h window; it now carries weekly data for GPT plans.
|
|
1367
|
-
// Prefer primary when present, fall back to secondary for compatibility.
|
|
1368
|
-
const primaryRaw = upstreamResponse.headers.get("x-codex-primary-used-percent");
|
|
1369
|
-
const secondaryRaw = upstreamResponse.headers.get("x-codex-secondary-used-percent");
|
|
1370
|
-
const weeklyRaw = primaryRaw ?? secondaryRaw;
|
|
1371
|
-
const monthlyRaw = upstreamResponse.headers.get("x-codex-tertiary-used-percent");
|
|
1372
|
-
const primaryResetRaw = upstreamResponse.headers.get("x-codex-primary-reset-at");
|
|
1373
|
-
const secondaryResetRaw = upstreamResponse.headers.get("x-codex-secondary-reset-at");
|
|
1374
|
-
const weeklyResetRaw = primaryRaw ? primaryResetRaw : secondaryResetRaw;
|
|
1375
|
-
const monthlyResetRaw = upstreamResponse.headers.get("x-codex-tertiary-reset-at");
|
|
1376
|
-
const retryAfterRaw = upstreamResponse.headers.get("retry-after");
|
|
1377
|
-
if (weeklyRaw || monthlyRaw) {
|
|
1378
|
-
const { updateAccountQuota } = await import("../codex/auth-api");
|
|
1379
|
-
updateAccountQuota(
|
|
1380
|
-
authCtx.accountId,
|
|
1381
|
-
weeklyRaw,
|
|
1382
|
-
weeklyResetRaw,
|
|
1383
|
-
monthlyRaw,
|
|
1384
|
-
monthlyResetRaw,
|
|
1385
|
-
);
|
|
1386
|
-
}
|
|
1387
|
-
if (terminalBodyWillRecord) {
|
|
1388
|
-
options.setTerminalOutcomeRecorder?.((status, httpStatusOverride) => {
|
|
1389
|
-
terminalRecorder(status, httpStatusOverride);
|
|
1390
|
-
options.onNativePassthroughTerminal?.(status);
|
|
1391
|
-
});
|
|
1392
|
-
} else {
|
|
1393
|
-
recordCodexUpstreamOutcome(config, authCtx.accountId, upstreamResponse.status, {
|
|
1394
|
-
retryAfter: retryAfterRaw,
|
|
1395
|
-
resetAt: [primaryResetRaw, secondaryResetRaw, monthlyResetRaw].filter(Boolean),
|
|
1396
|
-
threadId: req.headers.get("x-codex-parent-thread-id"),
|
|
1397
|
-
});
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
// Bun#32111 workaround: passthrough SSE uses tee()+native relay to avoid the
|
|
1402
|
-
// async-pull segfault on Windows. Branch[0] goes directly to the Response (Bun
|
|
1403
|
-
// native relay, never enters JS Sink.write); branch[1] is consumed in the
|
|
1404
|
-
// background for terminal-outcome/quota inspection only.
|
|
1405
|
-
if (upstreamResponse.ok && isEventStream && upstreamResponse.body) {
|
|
1406
|
-
const [nativeBody, inspectBody] = upstreamResponse.body.tee();
|
|
1407
|
-
const repairConfig = route.provider.responsesItemIdRepair;
|
|
1408
|
-
const turnAc = new AbortController();
|
|
1409
|
-
linkAbortSignal(upstream, turnAc.signal);
|
|
1410
|
-
registerTurn(turnAc);
|
|
1411
|
-
if (recordTerminalOutcomes) {
|
|
1412
|
-
// A real terminal was parsed from the (teed) inspection stream — record it as the outcome
|
|
1413
|
-
// even if the client has already disconnected: the turn genuinely reached that terminal, so
|
|
1414
|
-
// it must log as completed/failed, not be dropped or downgraded to a cancel (#44). A pure
|
|
1415
|
-
// client-cancel (no terminal seen) is finalized separately via consumeForInspection's onCancel.
|
|
1416
|
-
const reportNativeTerminal = (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
|
|
1417
|
-
terminalRecorder?.(status, httpStatusOverride);
|
|
1418
|
-
options.onNativePassthroughTerminal?.(status);
|
|
1419
|
-
};
|
|
1420
|
-
consumeForInspection(
|
|
1421
|
-
inspectBody,
|
|
1422
|
-
reportNativeTerminal,
|
|
1423
|
-
turnAc.signal,
|
|
1424
|
-
() => unregisterTurn(turnAc),
|
|
1425
|
-
logCtx,
|
|
1426
|
-
() => options.onNativePassthroughCancel?.(),
|
|
1427
|
-
rememberPassthroughResponse,
|
|
1428
|
-
options.onFirstOutput,
|
|
1429
|
-
);
|
|
1430
|
-
} else {
|
|
1431
|
-
consumeForResponseLogMetadata(
|
|
1432
|
-
inspectBody,
|
|
1433
|
-
logCtx,
|
|
1434
|
-
turnAc.signal,
|
|
1435
|
-
() => unregisterTurn(turnAc),
|
|
1436
|
-
rememberPassthroughResponse,
|
|
1437
|
-
options.onFirstOutput,
|
|
1438
|
-
);
|
|
1439
|
-
}
|
|
1440
|
-
if (!headers.has("content-type")) headers.set("content-type", "text/event-stream");
|
|
1441
|
-
// win32 must keep the pure native relay (Bun#32111 JS-sink segfault); elsewhere a JS pull
|
|
1442
|
-
// relay is established practice (relayWithAbort, relaySseWithHeartbeat) and lets a
|
|
1443
|
-
// mid-stream reset end with a clean response.failed terminal instead of a raw socket error.
|
|
1444
|
-
const repairedBody = hasResponsesItemIdRepair(repairConfig)
|
|
1445
|
-
? relaySseWithResponsesItemIdRepair(nativeBody, repairConfig!)
|
|
1446
|
-
: nativeBody;
|
|
1447
|
-
const clientBody = process.platform === "win32" && !hasResponsesItemIdRepair(repairConfig)
|
|
1448
|
-
? nativeBody
|
|
1449
|
-
: relaySseWithFailedTail(repairedBody, upstream);
|
|
1450
|
-
return markNativePassthroughSseResponse(new Response(clientBody, {
|
|
1451
|
-
status: upstreamResponse.status,
|
|
1452
|
-
headers,
|
|
1453
|
-
}));
|
|
1454
|
-
}
|
|
1455
|
-
if (headers.get("content-type")?.toLowerCase().includes("application/json")) {
|
|
1456
|
-
if (!upstreamResponse.ok && options.comboAttempt) {
|
|
1457
|
-
const failure = await consumeComboFailure(upstreamResponse, options.abortSignal);
|
|
1458
|
-
options.onConsumedComboFailure?.(failure);
|
|
1459
|
-
return failure.response;
|
|
1460
|
-
}
|
|
1461
|
-
const text = await upstreamResponse.text();
|
|
1462
|
-
inspectResponseLogJson(logCtx, text);
|
|
1463
|
-
if (upstreamResponse.ok && rememberPassthroughResponse) {
|
|
1464
|
-
try {
|
|
1465
|
-
rememberPassthroughResponse(JSON.parse(text) as { id?: unknown; output?: unknown; status?: unknown });
|
|
1466
|
-
} catch { /* non-JSON despite content-type; recording is best-effort */ }
|
|
1467
|
-
}
|
|
1468
|
-
return new Response(text, {
|
|
1469
|
-
status: upstreamResponse.status,
|
|
1470
|
-
statusText: upstreamResponse.statusText,
|
|
1471
|
-
headers,
|
|
1472
|
-
});
|
|
1473
|
-
}
|
|
1474
|
-
const body = relayWithAbort(upstreamResponse.body, upstream);
|
|
1475
|
-
const turnAc = new AbortController();
|
|
1476
|
-
const tracked = body ? trackStreamLifetime(body, turnAc) : null;
|
|
1477
|
-
return new Response(tracked, {
|
|
1478
|
-
status: upstreamResponse.status,
|
|
1479
|
-
headers,
|
|
1480
|
-
});
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
if (adapter.runTurn) {
|
|
1484
|
-
const runTurnAbort = new AbortController();
|
|
1485
|
-
linkAbortSignal(runTurnAbort, options.abortSignal);
|
|
1486
|
-
const queue = createAdapterEventQueue();
|
|
1487
|
-
const runTurn = async (): Promise<void> => {
|
|
1488
|
-
try {
|
|
1489
|
-
noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens);
|
|
1490
|
-
await adapter.runTurn?.(
|
|
1491
|
-
parsed,
|
|
1492
|
-
{ headers: selectedForwardHeaders, abortSignal: runTurnAbort.signal },
|
|
1493
|
-
queue.push,
|
|
1494
|
-
);
|
|
1495
|
-
} catch (err) {
|
|
1496
|
-
queue.push({
|
|
1497
|
-
type: "error",
|
|
1498
|
-
message: err instanceof Error ? err.message : String(err),
|
|
1499
|
-
});
|
|
1500
|
-
} finally {
|
|
1501
|
-
queue.close();
|
|
1502
|
-
}
|
|
1503
|
-
};
|
|
1504
|
-
|
|
1505
|
-
const { toolNsMap, freeformToolNames, toolSearchToolNames } = buildToolBridgeMaps(parsed);
|
|
1506
|
-
if (parsed.stream) {
|
|
1507
|
-
void runTurn();
|
|
1508
|
-
let eventSource: AsyncIterable<AdapterEvent> = queue.stream();
|
|
1509
|
-
if (options.comboAttempt) {
|
|
1510
|
-
const preflight = await preflightAdapterEvents(eventSource);
|
|
1511
|
-
if (preflight.error || preflight.empty) {
|
|
1512
|
-
runTurnAbort.abort();
|
|
1513
|
-
queue.close();
|
|
1514
|
-
const message = preflight.error?.message ?? "Adapter ended before producing a response";
|
|
1515
|
-
return formatErrorResponse(502, "upstream_error", redactSecretString(message));
|
|
1516
|
-
}
|
|
1517
|
-
eventSource = preflight.stream;
|
|
1518
|
-
}
|
|
1519
|
-
const sseStream = bridgeToResponsesSSE(
|
|
1520
|
-
eventSource, parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
|
|
1521
|
-
() => {
|
|
1522
|
-
runTurnAbort.abort();
|
|
1523
|
-
queue.close();
|
|
1524
|
-
}, 2_000,
|
|
1525
|
-
{
|
|
1526
|
-
...(options.forceEmptyResponseId ? { responseId: "" } : {}),
|
|
1527
|
-
stallTimeoutSec: config.stallTimeoutSec,
|
|
1528
|
-
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
1529
|
-
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
1530
|
-
...(routedCompaction ? { compaction: true } : {}),
|
|
1531
|
-
...(routedCompaction ? {} : {
|
|
1532
|
-
onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) =>
|
|
1533
|
-
rememberResponseState(
|
|
1534
|
-
parsed._rawBody,
|
|
1535
|
-
response,
|
|
1536
|
-
continuationStateForResponse(providerState),
|
|
1537
|
-
adapter.name === "kiro" ? { force: true } : undefined,
|
|
1538
|
-
),
|
|
1539
|
-
}),
|
|
1540
|
-
},
|
|
1541
|
-
);
|
|
1542
|
-
const bridgeTurnAc = new AbortController();
|
|
1543
|
-
const trackedSse = trackStreamLifetime(sseStream, bridgeTurnAc);
|
|
1544
|
-
return new Response(trackedSse, {
|
|
1545
|
-
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no" },
|
|
1546
|
-
});
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
await runTurn();
|
|
1550
|
-
const events = await queue.collect();
|
|
1551
|
-
if (options.comboAttempt) {
|
|
1552
|
-
const firstMeaningful = events.find(event => event.type !== "heartbeat");
|
|
1553
|
-
if (!firstMeaningful || firstMeaningful.type === "error") {
|
|
1554
|
-
const message = firstMeaningful?.type === "error"
|
|
1555
|
-
? firstMeaningful.message
|
|
1556
|
-
: "Adapter ended before producing a response";
|
|
1557
|
-
return formatErrorResponse(502, "upstream_error", redactSecretString(message));
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
let providerState: OcxProviderContinuationState | undefined;
|
|
1561
|
-
const json = buildResponseJSON(events, parsed.modelId, {
|
|
1562
|
-
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
1563
|
-
toolNsMap,
|
|
1564
|
-
freeformToolNames,
|
|
1565
|
-
toolSearchToolNames,
|
|
1566
|
-
...(routedCompaction ? { compaction: true } : {}),
|
|
1567
|
-
onProviderState: state => { providerState = state; },
|
|
1568
|
-
});
|
|
1569
|
-
if (!routedCompaction) {
|
|
1570
|
-
rememberResponseState(
|
|
1571
|
-
parsed._rawBody,
|
|
1572
|
-
json,
|
|
1573
|
-
continuationStateForResponse(providerState),
|
|
1574
|
-
adapter.name === "kiro" ? { force: true } : undefined,
|
|
1575
|
-
);
|
|
1576
|
-
}
|
|
1577
|
-
return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
// Web-search sidecar: Codex enabled web_search but this is a routed (non-OpenAI) model that can't
|
|
1581
|
-
// run it server-side. Expose web_search as a function tool and run searches via the gpt-mini sidecar
|
|
1582
|
-
// through the ChatGPT passthrough, looping until the model answers. Otherwise take the normal path.
|
|
1583
|
-
const wsPlan = planWebSearch(config, parsed, false, route.provider, route.modelId, openAiSidecar);
|
|
1584
|
-
if (wsPlan) {
|
|
1585
|
-
parsed.context.tools = [...(parsed.context.tools ?? []), buildWebSearchTool()];
|
|
1586
|
-
noteAttemptSend(logCtx.activeAttempt, logCtx.usageLogInputTokens);
|
|
1587
|
-
const wsResponse = await runWithWebSearch({
|
|
1588
|
-
parsed, adapter,
|
|
1589
|
-
backend: wsPlan.backend,
|
|
1590
|
-
forwardProvider: wsPlan.forwardSidecar?.provider,
|
|
1591
|
-
anthropicSidecar: wsPlan.anthropicSidecar,
|
|
1592
|
-
hostedTool: wsPlan.hostedTool,
|
|
1593
|
-
selectedForwardHeaders: wsPlan.forwardSidecar?.headers ?? selectedForwardHeaders,
|
|
1594
|
-
settings: wsPlan.settings,
|
|
1595
|
-
maxSearches: wsPlan.maxSearches,
|
|
1596
|
-
forceEmptyResponseId: true,
|
|
1597
|
-
abortSignal: options.abortSignal,
|
|
1598
|
-
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
1599
|
-
recordSidecarOutcome: wsPlan.forwardSidecar?.recordOutcome,
|
|
1600
|
-
connectTimeoutMs: config.connectTimeoutMs ?? 200_000,
|
|
1601
|
-
routedModelStallTimeoutMs: wsPlan.routedModelStallTimeoutMs,
|
|
1602
|
-
stallTimeoutSec: wsPlan.stallTimeoutSec,
|
|
1603
|
-
on429: retryAfter => {
|
|
1604
|
-
const rotated = rotateProviderTransportOn429(config, route.providerName, {
|
|
1605
|
-
retryAfter,
|
|
1606
|
-
now: Date.now(),
|
|
1607
|
-
attemptedKey: route.provider.apiKey,
|
|
1608
|
-
promptCacheKey: parsed.options.promptCacheKey,
|
|
1609
|
-
});
|
|
1610
|
-
if (!rotated) return null;
|
|
1611
|
-
route.provider = rotated;
|
|
1612
|
-
return resolveAdapter(
|
|
1613
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider),
|
|
1614
|
-
config.cacheRetention,
|
|
1615
|
-
);
|
|
1616
|
-
},
|
|
1617
|
-
});
|
|
1618
|
-
// Register the sidecar stream as an active turn so drainAndShutdown waits for (or aborts)
|
|
1619
|
-
// in-flight web-search turns instead of skipping them during graceful shutdown.
|
|
1620
|
-
if (wsResponse.body) {
|
|
1621
|
-
const wsTurnAc = new AbortController();
|
|
1622
|
-
return new Response(trackStreamLifetime(wsResponse.body, wsTurnAc), {
|
|
1623
|
-
status: wsResponse.status,
|
|
1624
|
-
headers: wsResponse.headers,
|
|
1625
|
-
});
|
|
1626
|
-
}
|
|
1627
|
-
return wsResponse;
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
const upstream = new AbortController();
|
|
1631
|
-
const cleanupUpstreamAbort = linkAbortSignal(upstream, options.abortSignal);
|
|
1632
|
-
const connectMs = config.connectTimeoutMs ?? 200_000;
|
|
1633
|
-
let activeAdapter = adapter;
|
|
1634
|
-
|
|
1635
|
-
const request = await activeAdapter.buildRequest(parsed, { headers: selectedForwardHeaders });
|
|
1636
|
-
const inputTokenEstimate = typeof request.usageLog?.inputTokens === "number"
|
|
1637
|
-
? request.usageLog.inputTokens
|
|
1638
|
-
: undefined;
|
|
1639
|
-
if (inputTokenEstimate !== undefined) logCtx.usageLogInputTokens = inputTokenEstimate;
|
|
1640
|
-
let upstreamResponse: Response;
|
|
1641
|
-
try {
|
|
1642
|
-
if (activeAdapter.fetchResponse) {
|
|
1643
|
-
noteAttemptSend(logCtx.activeAttempt, inputTokenEstimate);
|
|
1644
|
-
upstreamResponse = await activeAdapter.fetchResponse(request, {
|
|
1645
|
-
abortSignal: upstream.signal,
|
|
1646
|
-
timeoutMs: connectMs,
|
|
1647
|
-
stream: parsed.stream,
|
|
1648
|
-
});
|
|
1649
|
-
} else {
|
|
1650
|
-
upstreamResponse = await fetchWithResetRetry(
|
|
1651
|
-
recovery => {
|
|
1652
|
-
noteAttemptSend(logCtx.activeAttempt, inputTokenEstimate, recovery);
|
|
1653
|
-
return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({
|
|
1654
|
-
method: request.method,
|
|
1655
|
-
headers: request.headers,
|
|
1656
|
-
body: request.body,
|
|
1657
|
-
}, recovery), upstream.signal, connectMs, parsed.stream, providerFetch(route.provider));
|
|
1658
|
-
},
|
|
1659
|
-
{ abortSignal: upstream.signal, label: safeHostLabel(request.url) },
|
|
1660
|
-
);
|
|
1661
|
-
}
|
|
1662
|
-
} catch (err) {
|
|
1663
|
-
cleanupUpstreamAbort();
|
|
1664
|
-
upstream.abort();
|
|
1665
|
-
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
1666
|
-
const msg = err instanceof Error && err.name === "TimeoutError"
|
|
1667
|
-
? `Provider connect timeout after ${connectMs}ms`
|
|
1668
|
-
: `Provider unreachable: ${err instanceof Error ? err.message : String(err)}`;
|
|
1669
|
-
return formatErrorResponse(502, "upstream_error", msg);
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
if (!upstreamResponse.ok) {
|
|
1673
|
-
// Recovery loop: multi-key 429 failover + at most ONE anthropic 413 tightened retry
|
|
1674
|
-
// (devlog/260714_image_normalization_pipeline/030). One mutable activeAdapter serves
|
|
1675
|
-
// both paths so a 429→413 sequence never rebuilds against a stale pre-rotation
|
|
1676
|
-
// adapter, and imageTierBias — once armed — rides EVERY subsequent rebuild so a
|
|
1677
|
-
// 413→429 rotation cannot silently undo the tightening.
|
|
1678
|
-
let imageTierBias = 0;
|
|
1679
|
-
let imageRetryAttempted = false;
|
|
1680
|
-
let oauth401ReplayAttempted = false;
|
|
1681
|
-
const rebuildAndRefetch = async (
|
|
1682
|
-
recovery: AttemptRecoveryKind,
|
|
1683
|
-
): Promise<Response | { failed: Response }> => {
|
|
1684
|
-
const retryRequest = await activeAdapter.buildRequest(parsed, {
|
|
1685
|
-
headers: selectedForwardHeaders,
|
|
1686
|
-
...(imageTierBias > 0 ? { imageTierBias } : {}),
|
|
1687
|
-
});
|
|
1688
|
-
const retryEstimate = typeof retryRequest.usageLog?.inputTokens === "number"
|
|
1689
|
-
? retryRequest.usageLog.inputTokens
|
|
1690
|
-
: undefined;
|
|
1691
|
-
if (retryEstimate !== undefined) logCtx.usageLogInputTokens = retryEstimate;
|
|
1692
|
-
logCtx.providerAdapter = activeAdapter.name;
|
|
1693
|
-
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, activeAdapter.name);
|
|
1694
|
-
noteAttemptSend(logCtx.activeAttempt, retryEstimate, recovery);
|
|
1695
|
-
try {
|
|
1696
|
-
return activeAdapter.fetchResponse
|
|
1697
|
-
? await activeAdapter.fetchResponse(retryRequest, { abortSignal: upstream.signal, timeoutMs: connectMs, stream: parsed.stream })
|
|
1698
|
-
: await fetchWithHeaderTimeout(retryRequest.url, {
|
|
1699
|
-
method: retryRequest.method, headers: retryRequest.headers, body: retryRequest.body,
|
|
1700
|
-
}, upstream.signal, connectMs, parsed.stream, providerFetch(route.provider));
|
|
1701
|
-
} catch (err) {
|
|
1702
|
-
cleanupUpstreamAbort();
|
|
1703
|
-
upstream.abort();
|
|
1704
|
-
if (options.abortSignal?.aborted) {
|
|
1705
|
-
return { failed: clientCancelledResponse() };
|
|
1706
|
-
}
|
|
1707
|
-
const msg = err instanceof Error && err.name === "TimeoutError"
|
|
1708
|
-
? `Provider connect timeout after ${connectMs}ms`
|
|
1709
|
-
: `Provider unreachable: ${err instanceof Error ? err.message : String(err)}`;
|
|
1710
|
-
return { failed: formatErrorResponse(502, "upstream_error", msg) };
|
|
1711
|
-
}
|
|
1712
|
-
};
|
|
1713
|
-
recovery: for (;;) {
|
|
1714
|
-
if (
|
|
1715
|
-
upstreamResponse.status === 401
|
|
1716
|
-
&& isOAuth401ReplayProvider
|
|
1717
|
-
&& sentOAuthSnapshot
|
|
1718
|
-
&& !oauth401ReplayAttempted
|
|
1719
|
-
) {
|
|
1720
|
-
oauth401ReplayAttempted = true;
|
|
1721
|
-
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
1722
|
-
let refreshed: OAuthAccessSnapshot;
|
|
1723
|
-
try {
|
|
1724
|
-
refreshed = await forceRefreshOAuthAccessSnapshot(sentOAuthSnapshot);
|
|
1725
|
-
} catch (err) {
|
|
1726
|
-
cleanupUpstreamAbort();
|
|
1727
|
-
return formatErrorResponse(401, "authentication_error", err instanceof Error ? err.message : String(err));
|
|
1728
|
-
}
|
|
1729
|
-
sentOAuthSnapshot = refreshed;
|
|
1730
|
-
const refreshedProvider = resolveProviderTransport(
|
|
1731
|
-
route.providerName,
|
|
1732
|
-
{ ...route.provider, apiKey: refreshed.accessToken },
|
|
1733
|
-
parsed.options.promptCacheKey,
|
|
1734
|
-
route.providerName === "github-copilot" ? getOAuthCredentialApiBaseUrl(route.providerName) : undefined,
|
|
1735
|
-
);
|
|
1736
|
-
route.provider = refreshedProvider;
|
|
1737
|
-
activeAdapter = resolveAdapter(
|
|
1738
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider),
|
|
1739
|
-
config.cacheRetention,
|
|
1740
|
-
);
|
|
1741
|
-
const result = await rebuildAndRefetch("oauth-401");
|
|
1742
|
-
if ("failed" in result) return result.failed;
|
|
1743
|
-
upstreamResponse = result;
|
|
1744
|
-
continue recovery;
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
// Multi-key 429 failover: rotate to the next pool key (cooldown-aware) and retry the
|
|
1748
|
-
// SAME request once per remaining key. OAuth/forward providers and single-key pools
|
|
1749
|
-
// return null immediately, so this stays a no-op for them (src/providers/key-failover.ts).
|
|
1750
|
-
while (upstreamResponse.status === 429 && hasKeyPoolFailover(route.provider)) {
|
|
1751
|
-
const rotated = rotateProviderTransportOn429(config, route.providerName, {
|
|
1752
|
-
retryAfter: upstreamResponse.headers.get("retry-after"),
|
|
1753
|
-
now: Date.now(),
|
|
1754
|
-
attemptedKey: route.provider.apiKey,
|
|
1755
|
-
promptCacheKey: parsed.options.promptCacheKey,
|
|
1756
|
-
});
|
|
1757
|
-
if (!rotated) break;
|
|
1758
|
-
// Release the failed response's socket before retrying; unread bodies otherwise linger
|
|
1759
|
-
// until runtime cleanup (one per rotated key under a rate-limit storm).
|
|
1760
|
-
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
1761
|
-
route.provider = rotated;
|
|
1762
|
-
activeAdapter = resolveAdapter(
|
|
1763
|
-
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider),
|
|
1764
|
-
config.cacheRetention,
|
|
1765
|
-
);
|
|
1766
|
-
const result = await rebuildAndRefetch("key-429");
|
|
1767
|
-
if ("failed" in result) return result.failed;
|
|
1768
|
-
upstreamResponse = result;
|
|
1769
|
-
}
|
|
1770
|
-
// Anthropic 413 request_too_large: rebuild once with every image one tier lower
|
|
1771
|
-
// (spiral guard: single attempt). The biased response re-enters the 429 check above.
|
|
1772
|
-
if (shouldAttemptImageTierRetry({
|
|
1773
|
-
status: upstreamResponse.status,
|
|
1774
|
-
adapterName: activeAdapter.name,
|
|
1775
|
-
parsed,
|
|
1776
|
-
alreadyAttempted: imageRetryAttempted,
|
|
1777
|
-
})) {
|
|
1778
|
-
imageRetryAttempted = true;
|
|
1779
|
-
imageTierBias = 1;
|
|
1780
|
-
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
1781
|
-
const result = await rebuildAndRefetch("image-413");
|
|
1782
|
-
if ("failed" in result) return result.failed;
|
|
1783
|
-
upstreamResponse = result;
|
|
1784
|
-
continue recovery;
|
|
1785
|
-
}
|
|
1786
|
-
break;
|
|
1787
|
-
}
|
|
1788
|
-
if (!upstreamResponse.ok) {
|
|
1789
|
-
if (options.comboAttempt) {
|
|
1790
|
-
const failure = await consumeComboFailure(upstreamResponse, options.abortSignal)
|
|
1791
|
-
.finally(cleanupUpstreamAbort);
|
|
1792
|
-
options.onConsumedComboFailure?.(failure);
|
|
1793
|
-
return failure.response;
|
|
1794
|
-
}
|
|
1795
|
-
const errorText = await upstreamResponse.text().catch(() => "unknown error");
|
|
1796
|
-
cleanupUpstreamAbort();
|
|
1797
|
-
// Upstreams occasionally echo request details in error bodies — scrub token-shaped
|
|
1798
|
-
// material before it reaches the client-facing error surface.
|
|
1799
|
-
return formatErrorResponse(upstreamResponse.status, "upstream_error", `Provider error ${upstreamResponse.status}: ${redactSecretString(errorText.slice(0, 500))}`);
|
|
1800
|
-
}
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
|
-
if (parsed.stream) {
|
|
1804
|
-
const eventStream = activeAdapter.parseStream(upstreamResponse);
|
|
1805
|
-
const { toolNsMap, freeformToolNames, toolSearchToolNames } = buildToolBridgeMaps(parsed);
|
|
1806
|
-
const sseStream = bridgeToResponsesSSE(
|
|
1807
|
-
eventStream, parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
|
|
1808
|
-
() => upstream.abort(), 2_000,
|
|
1809
|
-
{
|
|
1810
|
-
...(options.forceEmptyResponseId ? { responseId: "" } : {}),
|
|
1811
|
-
stallTimeoutSec: config.stallTimeoutSec,
|
|
1812
|
-
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
1813
|
-
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
1814
|
-
...(routedCompaction ? { compaction: true } : {}),
|
|
1815
|
-
// Compaction turns must NOT enter the continuation cache: _rawBody still holds the full
|
|
1816
|
-
// PRE-compaction history, and a later previous_response_id expansion would rehydrate the
|
|
1817
|
-
// giant stale chain Codex just replaced.
|
|
1818
|
-
...(routedCompaction ? {} : {
|
|
1819
|
-
onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) =>
|
|
1820
|
-
rememberResponseState(
|
|
1821
|
-
parsed._rawBody,
|
|
1822
|
-
response,
|
|
1823
|
-
continuationStateForResponse(providerState),
|
|
1824
|
-
activeAdapter.name === "kiro" ? { force: true } : undefined,
|
|
1825
|
-
),
|
|
1826
|
-
}),
|
|
1827
|
-
},
|
|
1828
|
-
);
|
|
1829
|
-
const bridgeTurnAc = new AbortController();
|
|
1830
|
-
const trackedSse = trackStreamLifetime(sseStream, bridgeTurnAc, cleanupUpstreamAbort);
|
|
1831
|
-
return new Response(trackedSse, {
|
|
1832
|
-
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no" },
|
|
1833
|
-
});
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
if (activeAdapter.parseResponse) {
|
|
1837
|
-
let events: AdapterEvent[];
|
|
1838
|
-
try {
|
|
1839
|
-
events = await activeAdapter.parseResponse(upstreamResponse);
|
|
1840
|
-
} finally {
|
|
1841
|
-
cleanupUpstreamAbort();
|
|
1842
|
-
}
|
|
1843
|
-
const { toolNsMap, freeformToolNames, toolSearchToolNames } = buildToolBridgeMaps(parsed);
|
|
1844
|
-
let providerState: OcxProviderContinuationState | undefined;
|
|
1845
|
-
const json = buildResponseJSON(events, parsed.modelId, {
|
|
1846
|
-
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
1847
|
-
toolNsMap,
|
|
1848
|
-
freeformToolNames,
|
|
1849
|
-
toolSearchToolNames,
|
|
1850
|
-
...(routedCompaction ? { compaction: true } : {}),
|
|
1851
|
-
onProviderState: state => { providerState = state; },
|
|
1852
|
-
});
|
|
1853
|
-
// See the streaming branch: compaction turns skip the continuation cache.
|
|
1854
|
-
if (!routedCompaction) {
|
|
1855
|
-
rememberResponseState(
|
|
1856
|
-
parsed._rawBody,
|
|
1857
|
-
json,
|
|
1858
|
-
continuationStateForResponse(providerState),
|
|
1859
|
-
activeAdapter.name === "kiro" ? { force: true } : undefined,
|
|
1860
|
-
);
|
|
1861
|
-
}
|
|
1862
|
-
return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
return formatErrorResponse(400, "invalid_request_error", "Non-streaming not supported by this adapter");
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
|
-
export function linkAbortSignal(upstream: AbortController, signal?: AbortSignal): () => void {
|
|
1869
|
-
if (!signal) return () => {};
|
|
1870
|
-
if (signal.aborted) {
|
|
1871
|
-
upstream.abort(signal.reason);
|
|
1872
|
-
return () => {};
|
|
1873
|
-
}
|
|
1874
|
-
const onAbort = () => upstream.abort(signal.reason);
|
|
1875
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
1876
|
-
return () => signal.removeEventListener("abort", onAbort);
|
|
1877
|
-
}
|
|
1878
|
-
|
|
1879
|
-
/**
|
|
1880
|
-
* Remote compaction v1 (`POST /v1/responses/compact`). Codex uses this whenever the provider
|
|
1881
|
-
* "is openai" and Feature::RemoteCompactionV2 is OFF (the default) — under Design B that is the
|
|
1882
|
-
* proxy. The response is a unary `{"output":[ResponseItem...]}` that codex installs as the
|
|
1883
|
-
* REPLACEMENT history (compact_remote.rs). Passthrough forwards to the real ChatGPT backend;
|
|
1884
|
-
* routed models run the same summarizer used for v2 and convert the summary to v1 history items.
|
|
1885
|
-
*/
|
|
1886
|
-
export const COMPACT_RESPONSE_MAX_BYTES = 32 * 1024 * 1024;
|
|
1887
|
-
|
|
1888
|
-
function compactResponseTooLargeError(): Response {
|
|
1889
|
-
return new Response(JSON.stringify({
|
|
1890
|
-
error: {
|
|
1891
|
-
message: "Compact response exceeded 32 MiB",
|
|
1892
|
-
type: "compact_response_too_large",
|
|
1893
|
-
code: "compact_response_too_large",
|
|
1894
|
-
},
|
|
1895
|
-
}), { status: 502, headers: { "Content-Type": "application/json" } });
|
|
1896
|
-
}
|
|
1897
|
-
|
|
1898
|
-
/** Exported for tests: owns the compact client-cancel branch (499 client_cancelled). */
|
|
1899
|
-
export async function bufferCompactResponse(upstream: Response, signal: AbortSignal): Promise<Response> {
|
|
1900
|
-
const reader = upstream.body?.getReader();
|
|
1901
|
-
const contentType = upstream.headers.get("content-type") ?? "application/json";
|
|
1902
|
-
if (!reader) return new Response(null, { status: upstream.status, headers: { "Content-Type": contentType } });
|
|
1903
|
-
const declaredLength = Number(upstream.headers.get("content-length"));
|
|
1904
|
-
if (Number.isFinite(declaredLength) && declaredLength > COMPACT_RESPONSE_MAX_BYTES) {
|
|
1905
|
-
await reader.cancel("compact_response_too_large").catch(() => undefined);
|
|
1906
|
-
return compactResponseTooLargeError();
|
|
1907
|
-
}
|
|
1908
|
-
const chunks: Uint8Array[] = [];
|
|
1909
|
-
let total = 0;
|
|
1910
|
-
try {
|
|
1911
|
-
while (true) {
|
|
1912
|
-
if (signal.aborted) {
|
|
1913
|
-
await reader.cancel(signal.reason).catch(() => undefined);
|
|
1914
|
-
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
1915
|
-
}
|
|
1916
|
-
const { done, value } = await reader.read();
|
|
1917
|
-
if (done) break;
|
|
1918
|
-
total += value.byteLength;
|
|
1919
|
-
if (total > COMPACT_RESPONSE_MAX_BYTES) {
|
|
1920
|
-
await reader.cancel("compact_response_too_large").catch(() => undefined);
|
|
1921
|
-
return compactResponseTooLargeError();
|
|
1922
|
-
}
|
|
1923
|
-
chunks.push(value);
|
|
1924
|
-
}
|
|
1925
|
-
} catch {
|
|
1926
|
-
if (signal.aborted) return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
1927
|
-
return formatErrorResponse(502, "upstream_error", "Failed to read compact response");
|
|
1928
|
-
}
|
|
1929
|
-
const body = new Uint8Array(total);
|
|
1930
|
-
let offset = 0;
|
|
1931
|
-
for (const chunk of chunks) {
|
|
1932
|
-
body.set(chunk, offset);
|
|
1933
|
-
offset += chunk.byteLength;
|
|
1934
|
-
}
|
|
1935
|
-
return new Response(body, { status: upstream.status, headers: { "Content-Type": contentType } });
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
export async function handleResponsesCompact(
|
|
1939
|
-
req: Request,
|
|
1940
|
-
config: OcxConfig,
|
|
1941
|
-
logCtx: RequestLogContext,
|
|
1942
|
-
): Promise<Response> {
|
|
1943
|
-
let body: unknown;
|
|
1944
|
-
try {
|
|
1945
|
-
body = await readJsonRequestBody(req);
|
|
1946
|
-
} catch (err) {
|
|
1947
|
-
return decodeRequestErrorResponse(err, "responses-compact");
|
|
1948
|
-
}
|
|
1949
|
-
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
1950
|
-
return formatErrorResponse(400, "invalid_request_error", "Invalid compaction request body");
|
|
1951
|
-
}
|
|
1952
|
-
const raw = body as { model?: unknown; input?: unknown };
|
|
1953
|
-
if (typeof raw.model !== "string" || raw.model.length === 0) {
|
|
1954
|
-
return formatErrorResponse(400, "invalid_request_error", "compaction request requires a model");
|
|
1955
|
-
}
|
|
1956
|
-
|
|
1957
|
-
let route;
|
|
1958
|
-
try {
|
|
1959
|
-
route = routeModel(config, raw.model);
|
|
1960
|
-
} catch (err) {
|
|
1961
|
-
return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
|
|
1962
|
-
}
|
|
1963
|
-
const selectedModelId = route.modelId;
|
|
1964
|
-
logCtx.requestedModel = raw.model;
|
|
1965
|
-
logCtx.model = selectedModelId;
|
|
1966
|
-
logCtx.provider = route.providerName;
|
|
1967
|
-
logCtx.providerAdapter = route.provider.adapter;
|
|
1968
|
-
const virtual = resolveOpenAiCompactModel(route.providerName, selectedModelId);
|
|
1969
|
-
if (virtual) {
|
|
1970
|
-
route.modelId = virtual.wireModelId;
|
|
1971
|
-
logCtx.model = virtual.selectedModelId;
|
|
1972
|
-
logCtx.resolvedModel = virtual.wireModelId;
|
|
1973
|
-
} else {
|
|
1974
|
-
logCtx.resolvedModel = route.modelId;
|
|
1975
|
-
}
|
|
1976
|
-
|
|
1977
|
-
if (route.codexAccountMode === "direct") {
|
|
1978
|
-
try { validateForwardAdmissionCredential(req.headers, config); }
|
|
1979
|
-
catch (err) {
|
|
1980
|
-
if (err instanceof ForwardAdmissionCredentialError) return formatErrorResponse(401, "authentication_error", err.message);
|
|
1981
|
-
throw err;
|
|
1982
|
-
}
|
|
1983
|
-
}
|
|
1984
|
-
|
|
1985
|
-
if (route.provider.adapter === "openai-responses") {
|
|
1986
|
-
// Native ChatGPT/OpenAI model: forward the compact request verbatim to the real backend.
|
|
1987
|
-
// Resolve the SAME pool/thread auth context as /v1/responses — forwarding the caller's raw
|
|
1988
|
-
// headers would run compaction on the wrong account (or 401) whenever a pool account is
|
|
1989
|
-
// active for this thread while normal turns succeed.
|
|
1990
|
-
let compactProvider = route.provider;
|
|
1991
|
-
let authCtx: CodexAuthContext = { kind: "main", accountId: null };
|
|
1992
|
-
const headers = new Headers({ "content-type": "application/json" });
|
|
1993
|
-
try {
|
|
1994
|
-
if (route.codexAccountMode) {
|
|
1995
|
-
authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode);
|
|
1996
|
-
const selected = headersForCodexAuthContext(req.headers, authCtx);
|
|
1997
|
-
compactProvider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
|
|
1998
|
-
for (const name of FORWARD_HEADERS) {
|
|
1999
|
-
const value = selected.get(name);
|
|
2000
|
-
if (value) headers.set(name, value);
|
|
2001
|
-
}
|
|
2002
|
-
const override = (compactProvider as { _codexAccountOverride?: { accessToken: string; chatgptAccountId: string } })._codexAccountOverride;
|
|
2003
|
-
if (override) {
|
|
2004
|
-
headers.set("authorization", `Bearer ${override.accessToken}`);
|
|
2005
|
-
headers.set("chatgpt-account-id", override.chatgptAccountId);
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2008
|
-
} catch (err) {
|
|
2009
|
-
if (err instanceof CodexAccountCooldownError) {
|
|
2010
|
-
return formatErrorResponse(429, "rate_limit_error", "Selected Codex account is cooling down");
|
|
2011
|
-
}
|
|
2012
|
-
if (err instanceof CodexThreadAffinityExpiredError) {
|
|
2013
|
-
return formatErrorResponse(409, "invalid_request_error", "Codex thread account affinity expired; start a new session");
|
|
2014
|
-
}
|
|
2015
|
-
if (err instanceof CodexAuthContextError) {
|
|
2016
|
-
return formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication");
|
|
2017
|
-
}
|
|
2018
|
-
if (err instanceof CodexPoolAuthenticationError || err instanceof CodexDirectAuthenticationError) {
|
|
2019
|
-
return formatErrorResponse(401, "authentication_error", err.message);
|
|
2020
|
-
}
|
|
2021
|
-
throw err;
|
|
2022
|
-
}
|
|
2023
|
-
const base = (compactProvider.baseUrl ?? "").replace(/\/$/, "");
|
|
2024
|
-
if (compactProvider.apiKey) headers.set("authorization", `Bearer ${resolveEnvValue(compactProvider.apiKey)}`);
|
|
2025
|
-
const { reasoning: _reasoning, ...compactBodyRaw } = raw as typeof raw & { reasoning?: unknown };
|
|
2026
|
-
// The regular /v1/responses path applies sanitizeReasoningInputContent via the adapter's
|
|
2027
|
-
// buildRequest, but the compact endpoint forwards directly. Apply the same sanitizer here
|
|
2028
|
-
// so routed-model reasoning items (reasoning_text content) don't 400 the ChatGPT backend.
|
|
2029
|
-
const compactBody = sanitizeReasoningInputContent(compactBodyRaw) as typeof compactBodyRaw;
|
|
2030
|
-
const compactUrl = `${base}/responses/compact`;
|
|
2031
|
-
const compactThreadId = req.headers.get("x-codex-parent-thread-id");
|
|
2032
|
-
const connectMs = config.connectTimeoutMs ?? 200_000;
|
|
2033
|
-
const recordCompactPoolOutcome = (outcome: CodexUpstreamOutcome, meta: { retryAfter?: string | null } = {}) => {
|
|
2034
|
-
if (!usesCodexForwardPoolAuth(authCtx, route.provider)) return;
|
|
2035
|
-
recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
|
|
2036
|
-
...meta,
|
|
2037
|
-
threadId: compactThreadId,
|
|
2038
|
-
});
|
|
2039
|
-
};
|
|
2040
|
-
let upstream: Response;
|
|
2041
|
-
try {
|
|
2042
|
-
// Same connect timeout + keep-alive reset + transient-5xx recovery as /v1/responses —
|
|
2043
|
-
// compact hits the same ChatGPT host and must soft-avoid / clear affinity (#186).
|
|
2044
|
-
upstream = await fetchWithTransientRetry(
|
|
2045
|
-
recovery => fetchWithHeaderTimeout(
|
|
2046
|
-
compactUrl,
|
|
2047
|
-
applyUpstreamRecoveryInit({
|
|
2048
|
-
method: "POST",
|
|
2049
|
-
headers,
|
|
2050
|
-
body: JSON.stringify({ ...compactBody, model: route.modelId }),
|
|
2051
|
-
}, recovery),
|
|
2052
|
-
req.signal,
|
|
2053
|
-
connectMs,
|
|
2054
|
-
false,
|
|
2055
|
-
providerFetch(compactProvider),
|
|
2056
|
-
),
|
|
2057
|
-
{ abortSignal: req.signal, label: safeHostLabel(compactUrl) },
|
|
2058
|
-
);
|
|
2059
|
-
} catch (err) {
|
|
2060
|
-
if (req.signal.aborted) return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
2061
|
-
const outcome = err instanceof Error && err.name === "TimeoutError" ? "timeout" : "connect_error";
|
|
2062
|
-
recordCompactPoolOutcome(outcome);
|
|
2063
|
-
return formatErrorResponse(502, "upstream_error", "Failed to connect to compact upstream");
|
|
2064
|
-
}
|
|
2065
|
-
const retryAfter = upstream.headers.get("retry-after");
|
|
2066
|
-
const buffered = await bufferCompactResponse(upstream, req.signal);
|
|
2067
|
-
// Record pool health only after the body is fully delivered (or definitively failed).
|
|
2068
|
-
// A premature 200 would clear soft-avoid while the client still sees a buffer 502.
|
|
2069
|
-
if (buffered.status === 499) {
|
|
2070
|
-
return buffered;
|
|
2071
|
-
}
|
|
2072
|
-
if (upstream.ok && buffered.status >= 500) {
|
|
2073
|
-
// The upstream account returned 200 — it is healthy. The buffering failure
|
|
2074
|
-
// (oversized body exceeding COMPACT_RESPONSE_MAX_BYTES, or a rare mid-read
|
|
2075
|
-
// reset on a small JSON payload) is a local proxy issue, not account flakiness.
|
|
2076
|
-
// Record the upstream status so a deterministic payload-size limit does not
|
|
2077
|
-
// soft-avoid a healthy account and rotate a thread for 30s.
|
|
2078
|
-
recordCompactPoolOutcome(upstream.status, { retryAfter });
|
|
2079
|
-
} else {
|
|
2080
|
-
recordCompactPoolOutcome(upstream.status, { retryAfter });
|
|
2081
|
-
}
|
|
2082
|
-
return buffered;
|
|
2083
|
-
}
|
|
2084
|
-
|
|
2085
|
-
// ROUTED model: run the v2 synthetic-compaction turn internally (appends COMPACT_PROMPT, no
|
|
2086
|
-
// tools) and decode the resulting ocx1 envelope into plain v1 replacement-history items.
|
|
2087
|
-
const inputItems = Array.isArray(raw.input) ? (raw.input as unknown[]) : [];
|
|
2088
|
-
const internalBody = {
|
|
2089
|
-
...raw,
|
|
2090
|
-
stream: false,
|
|
2091
|
-
input: [...inputItems, { type: "compaction_trigger" }],
|
|
2092
|
-
};
|
|
2093
|
-
const internalHeaders = new Headers({ "content-type": "application/json" });
|
|
2094
|
-
for (const name of FORWARD_HEADERS) {
|
|
2095
|
-
const value = req.headers.get(name);
|
|
2096
|
-
if (value) internalHeaders.set(name, value);
|
|
2097
|
-
}
|
|
2098
|
-
const internalReq = new Request("http://localhost/v1/responses", {
|
|
2099
|
-
method: "POST",
|
|
2100
|
-
headers: internalHeaders,
|
|
2101
|
-
body: JSON.stringify(internalBody),
|
|
2102
|
-
});
|
|
2103
|
-
const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal });
|
|
2104
|
-
if (!response.ok) return response;
|
|
2105
|
-
let json: { output?: unknown[] };
|
|
2106
|
-
try {
|
|
2107
|
-
json = await response.json() as { output?: unknown[] };
|
|
2108
|
-
} catch {
|
|
2109
|
-
return formatErrorResponse(502, "server_error", "compaction turn returned a non-JSON response");
|
|
2110
|
-
}
|
|
2111
|
-
const compactionItem = (json.output ?? []).find(
|
|
2112
|
-
(item): item is { type: string; encrypted_content?: string } =>
|
|
2113
|
-
!!item && typeof item === "object" && (item as { type?: string }).type === "compaction",
|
|
2114
|
-
);
|
|
2115
|
-
const summary = compactionItem?.encrypted_content
|
|
2116
|
-
? decodeCompactionSummary(compactionItem.encrypted_content) ?? ""
|
|
2117
|
-
: "";
|
|
2118
|
-
const output = buildCompactV1Output(extractCompactUserMessages(inputItems), summary);
|
|
2119
|
-
return new Response(JSON.stringify({ output }), { headers: { "Content-Type": "application/json" } });
|
|
2120
|
-
}
|
|
2121
|
-
|
|
2122
|
-
export function disableResponsesRequestTimeout(req: Request, server: Pick<Server<WsData>, "timeout"> | undefined): boolean {
|
|
2123
|
-
if (!server) return false;
|
|
2124
|
-
try {
|
|
2125
|
-
server.timeout(req, 0);
|
|
2126
|
-
return true;
|
|
2127
|
-
} catch {
|
|
2128
|
-
return false;
|
|
2129
|
-
}
|
|
2130
|
-
}
|
|
2131
|
-
|
|
2132
|
-
/** Host-only label for retry logs — never leaks path/query/credentials. */
|
|
2133
|
-
export function safeHostLabel(url: string): string {
|
|
2134
|
-
try {
|
|
2135
|
-
return new URL(url).host;
|
|
2136
|
-
} catch {
|
|
2137
|
-
return "upstream";
|
|
2138
|
-
}
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
function providerFetch(provider: OcxProviderConfig): typeof globalThis.fetch {
|
|
2142
|
-
return (provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? globalThis.fetch;
|
|
2143
|
-
}
|
|
2144
|
-
|
|
2145
|
-
export async function fetchWithHeaderTimeout(
|
|
2146
|
-
url: string,
|
|
2147
|
-
init: Omit<RequestInit, "signal">,
|
|
2148
|
-
abortSignal: AbortSignal,
|
|
2149
|
-
timeoutMs: number,
|
|
2150
|
-
preferIdentityEncoding = false,
|
|
2151
|
-
executor: typeof globalThis.fetch = globalThis.fetch,
|
|
2152
|
-
): Promise<Response> {
|
|
2153
|
-
const timeout = new AbortController();
|
|
2154
|
-
const timer = setTimeout(() => {
|
|
2155
|
-
if (!timeout.signal.aborted) timeout.abort(new DOMException("Timeout elapsed", "TimeoutError"));
|
|
2156
|
-
}, timeoutMs);
|
|
2157
|
-
const headers = new Headers(init.headers);
|
|
2158
|
-
// Compressed SSE can be held until the decompressor has a complete block. Streaming calls
|
|
2159
|
-
// default to identity for low-latency frame delivery, while an explicit caller choice wins.
|
|
2160
|
-
if (preferIdentityEncoding && !headers.has("accept-encoding")) {
|
|
2161
|
-
headers.set("accept-encoding", "identity");
|
|
2162
|
-
}
|
|
2163
|
-
try {
|
|
2164
|
-
return await executor(url, {
|
|
2165
|
-
...init,
|
|
2166
|
-
headers,
|
|
2167
|
-
signal: AbortSignal.any([abortSignal, timeout.signal]),
|
|
2168
|
-
});
|
|
2169
|
-
} finally {
|
|
2170
|
-
clearTimeout(timer);
|
|
2171
|
-
}
|
|
2172
|
-
}
|
|
1
|
+
// AUTO-SPLIT facade: original responses.ts body moved into ./responses/* modules.
|
|
2
|
+
// Public surface preserved exactly; importers keep using "src/server/responses".
|
|
3
|
+
export { buildToolBridgeMaps, isV1CollabSurface, collabSurface, multiAgentGuidanceText, V2_GUIDANCE_CHAR_BUDGET, injectDeveloperMessage } from "./responses/collaboration";
|
|
4
|
+
export type { MultiAgentGuidanceOptions, MultiAgentGuidanceDeps } from "./responses/collaboration";
|
|
5
|
+
export { hasUnreadableEncryptedAgentTask, sanitizeEncryptedContentInPlace } from "./responses/encrypted-payload";
|
|
6
|
+
export { COMPACT_RESPONSE_MAX_BYTES, bufferCompactResponse, handleResponsesCompact } from "./responses/compact";
|
|
7
|
+
export { disableResponsesRequestTimeout, safeHostLabel, fetchWithHeaderTimeout } from "./responses/fetch-helpers";
|
|
8
|
+
export { sidecarOutcomeRecorder, isShadowSourceModel, codexLogAccountId, usesCodexForwardPoolAuth, codexForwardTerminalOutcomeRecorder, decodeRequestErrorResponse, buildComboChildHeaders, handleResponses, linkAbortSignal } from "./responses/core";
|
|
9
|
+
export { adapterNeedsForcedContinuation } from "./responses/core";
|