@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
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import type { Server } from "bun";
|
|
2
|
+
import { bridgeToResponsesSSE, buildResponseJSON, formatErrorResponse, type ResponsesTerminalStatus } from "../../bridge";
|
|
3
|
+
import {
|
|
4
|
+
getConfigPath,
|
|
5
|
+
multiAgentGuidanceEnabled,
|
|
6
|
+
resolveEnvValue,
|
|
7
|
+
} from "../../config";
|
|
8
|
+
import { parseRequest } from "../../responses/parser";
|
|
9
|
+
import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
|
|
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
|
+
import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
export function buildToolBridgeMaps(parsed: OcxParsedRequest): {
|
|
102
|
+
toolNsMap: Map<string, { namespace: string; name: string }>;
|
|
103
|
+
freeformToolNames: Set<string>;
|
|
104
|
+
toolSearchToolNames: Set<string>;
|
|
105
|
+
} {
|
|
106
|
+
const toolNsMap = new Map<string, { namespace: string; name: string }>();
|
|
107
|
+
const freeformToolNames = new Set<string>();
|
|
108
|
+
const toolSearchToolNames = new Set<string>();
|
|
109
|
+
for (const t of parsed.context.tools ?? []) {
|
|
110
|
+
if (t.namespace) toolNsMap.set(namespacedToolName(t.namespace, t.name), { namespace: t.namespace, name: t.name });
|
|
111
|
+
if (t.freeform) freeformToolNames.add(t.name);
|
|
112
|
+
if (t.toolSearch) toolSearchToolNames.add(t.name);
|
|
113
|
+
}
|
|
114
|
+
return { toolNsMap, freeformToolNames, toolSearchToolNames };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
export const PROACTIVE_MULTI_AGENT_MODE_TEXT = [
|
|
120
|
+
"Proactive multi-agent delegation is active.",
|
|
121
|
+
"Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies.",
|
|
122
|
+
"Delegate independent sub-tasks to sub-agents whenever parallel work would materially improve speed or quality — do not serialize work that can run concurrently.",
|
|
123
|
+
"Each sub-agent runs in its own context and can use all available tools; prefer spawning specialists over doing everything yourself.",
|
|
124
|
+
"This mode remains active until a later multi-agent mode developer message changes it.",
|
|
125
|
+
].join(" ");
|
|
126
|
+
|
|
127
|
+
export function isV1CollabSurface(parsed: OcxParsedRequest): boolean {
|
|
128
|
+
return collabSurface(parsed) === "v1";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
export function collabSurface(parsed: OcxParsedRequest): "v1" | "v2" | null {
|
|
134
|
+
let namespacedSpawn = false;
|
|
135
|
+
let flatSpawn = false;
|
|
136
|
+
let v1Only = false;
|
|
137
|
+
let v2Only = false;
|
|
138
|
+
for (const t of parsed.context.tools ?? []) {
|
|
139
|
+
if (t.name === "spawn_agent") {
|
|
140
|
+
if (t.namespace) namespacedSpawn = true;
|
|
141
|
+
else flatSpawn = true;
|
|
142
|
+
} else if (t.name === "send_input" || t.name === "resume_agent" || t.name === "close_agent") {
|
|
143
|
+
v1Only = true;
|
|
144
|
+
} else if (t.name === "send_message" || t.name === "followup_task" || t.name === "interrupt_agent" || t.name === "list_agents") {
|
|
145
|
+
v2Only = true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (!namespacedSpawn && !flatSpawn) return null; // no spawn_agent -> no collab surface
|
|
149
|
+
if (namespacedSpawn && flatSpawn) return null; // contradictory spawn shapes
|
|
150
|
+
if (v1Only && v2Only) return null; // contradictory companions
|
|
151
|
+
if (v1Only) return "v1";
|
|
152
|
+
if (v2Only) return "v2";
|
|
153
|
+
return namespacedSpawn ? "v1" : "v2"; // companionless fallbacks (legacy defaults)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
export interface MultiAgentGuidanceOptions {
|
|
159
|
+
multiAgentGuidanceEnabled?: boolean;
|
|
160
|
+
injectionModel?: string;
|
|
161
|
+
injectionEffort?: string;
|
|
162
|
+
subagentModels?: string[];
|
|
163
|
+
injectionPrompt?: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
export interface MultiAgentGuidanceDeps {
|
|
169
|
+
resolveEffectiveSubagentRoster?: (
|
|
170
|
+
configuredModels: readonly string[],
|
|
171
|
+
surface: SpawnAgentSurface,
|
|
172
|
+
) => EffectiveSubagentRoster | Promise<EffectiveSubagentRoster>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
export async function resolveEffectiveSubagentRoster(
|
|
178
|
+
configuredModels: readonly string[],
|
|
179
|
+
surface: SpawnAgentSurface,
|
|
180
|
+
): Promise<EffectiveSubagentRoster> {
|
|
181
|
+
const { effectiveSubagentRoster } = await import("../../codex/catalog");
|
|
182
|
+
return effectiveSubagentRoster(configuredModels, surface);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
export async function multiAgentGuidanceText(
|
|
188
|
+
parsed: OcxParsedRequest,
|
|
189
|
+
options: MultiAgentGuidanceOptions = {},
|
|
190
|
+
deps: MultiAgentGuidanceDeps = {},
|
|
191
|
+
): Promise<string | null> {
|
|
192
|
+
if (options.multiAgentGuidanceEnabled === false) return null;
|
|
193
|
+
const {
|
|
194
|
+
injectionModel,
|
|
195
|
+
injectionEffort,
|
|
196
|
+
subagentModels,
|
|
197
|
+
injectionPrompt,
|
|
198
|
+
} = options;
|
|
199
|
+
const surface = collabSurface(parsed);
|
|
200
|
+
if (surface === null) return null;
|
|
201
|
+
|
|
202
|
+
if (surface === "v2") {
|
|
203
|
+
// codex-rs supplies the Proactive text on v2; the proxy only adds model-designation
|
|
204
|
+
// guidance, and only when there is something concrete to designate: a configured
|
|
205
|
+
// injectionModel and/or a roster entry that resolves in the injected catalog.
|
|
206
|
+
const configuredForGuidance = [
|
|
207
|
+
...(subagentModels ?? []),
|
|
208
|
+
...(injectionModel ? [injectionModel] : []),
|
|
209
|
+
];
|
|
210
|
+
const resolveRoster = deps.resolveEffectiveSubagentRoster ?? resolveEffectiveSubagentRoster;
|
|
211
|
+
const effective = await resolveRoster(configuredForGuidance, "v2");
|
|
212
|
+
const rosterModels = effective.advertised.filter(candidate =>
|
|
213
|
+
(subagentModels ?? []).some(model => slugsEquivalent(model, candidate.model))
|
|
214
|
+
);
|
|
215
|
+
const roster = subagentRosterText(rosterModels);
|
|
216
|
+
const preferred = injectionModel
|
|
217
|
+
? effective.candidates.find(candidate => slugsEquivalent(injectionModel, candidate.model))
|
|
218
|
+
: undefined;
|
|
219
|
+
|
|
220
|
+
if (isInjectionDebugEnabled() && effective.excluded.length > 0) {
|
|
221
|
+
injectionDebugLog(`[opencodex] multi-agent guidance excluded: ${effective.excluded
|
|
222
|
+
.map(item => `${item.configured}:${item.reason}`)
|
|
223
|
+
.join(", ")}`);
|
|
224
|
+
}
|
|
225
|
+
if (!injectionModel && roster === "") return null;
|
|
226
|
+
if (injectionPrompt) {
|
|
227
|
+
return `<multi_agent_mode>${applyInjectionPlaceholders(injectionPrompt, injectionModel, injectionEffort, roster)}</multi_agent_mode>`;
|
|
228
|
+
}
|
|
229
|
+
if (!preferred && roster === "") return null;
|
|
230
|
+
let text = "When the active spawn_agent tool supports optional \"model\" or \"reasoning_effort\" overrides, "
|
|
231
|
+
+ "use only models listed for this collaboration surface. "
|
|
232
|
+
+ "When setting either override, set fork_turns to \"none\" "
|
|
233
|
+
+ "(or a positive turn count such as \"3\"; full-history forks reject overrides) "
|
|
234
|
+
+ "and make the task message self-contained.";
|
|
235
|
+
if (preferred) {
|
|
236
|
+
text += ` Preferred sub-agent: model "${preferred.model}"`
|
|
237
|
+
+ (injectionEffort ? `, reasoning_effort "${injectionEffort}"` : "")
|
|
238
|
+
+ " — use it unless the user names another.";
|
|
239
|
+
}
|
|
240
|
+
text += roster;
|
|
241
|
+
if (text.length > V2_GUIDANCE_CHAR_BUDGET) {
|
|
242
|
+
// Roster is the only unbounded part — drop it before breaking the budget.
|
|
243
|
+
text = text.slice(0, text.length - roster.length);
|
|
244
|
+
}
|
|
245
|
+
return `<multi_agent_mode>${text}</multi_agent_mode>`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const effort = parsed.options.reasoning;
|
|
249
|
+
// v1 keeps only the upstream-parity behavior: Proactive text at the top tier
|
|
250
|
+
// (ultra arrives as max on the wire). No designation/roster payload here.
|
|
251
|
+
if (effort !== "max" && effort !== "ultra") return null;
|
|
252
|
+
return `<multi_agent_mode>${PROACTIVE_MULTI_AGENT_MODE_TEXT}</multi_agent_mode>`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
export const V2_GUIDANCE_CHAR_BUDGET = 700;
|
|
258
|
+
|
|
259
|
+
export function applyInjectionPlaceholders(prompt: string, model?: string, effort?: string, roster?: string): string {
|
|
260
|
+
return prompt
|
|
261
|
+
.replaceAll("{{model}}", model ?? "")
|
|
262
|
+
.replaceAll("{{effort}}", effort ?? "")
|
|
263
|
+
.replaceAll("{{roster}}", roster ?? "");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
export function subagentRosterText(models: Array<{ model: string; efforts: string[] }>): string {
|
|
269
|
+
if (models.length === 0) return "";
|
|
270
|
+
const ladders = new Set(models.map(model => model.efforts.join("/")));
|
|
271
|
+
if (!ladders.has("") && ladders.size === 1) {
|
|
272
|
+
return ` Available models (reasoning_effort ${[...ladders][0]}): ${models
|
|
273
|
+
.map(model => `"${model.model}"`)
|
|
274
|
+
.join(", ")}.`;
|
|
275
|
+
}
|
|
276
|
+
const entries = models.map(model => model.efforts.length > 0
|
|
277
|
+
? `"${model.model}" (${model.efforts.join("/")})`
|
|
278
|
+
: `"${model.model}"`);
|
|
279
|
+
return ` Available models (valid reasoning_effort): ${entries.join(", ")}.`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
export function injectDeveloperMessage(parsed: OcxParsedRequest, text: string): void {
|
|
285
|
+
parsed.context.messages.push({ role: "developer", content: text, timestamp: Date.now() });
|
|
286
|
+
const raw = parsed._rawBody as { input?: unknown } | undefined;
|
|
287
|
+
if (raw && Array.isArray(raw.input)) {
|
|
288
|
+
const devItem = { type: "message", role: "developer", content: [{ type: "input_text", text }] };
|
|
289
|
+
// compaction_trigger must remain the final input item (codex-rs + ChatGPT backend both
|
|
290
|
+
// validate this). Insert the developer message BEFORE the trigger when present.
|
|
291
|
+
const last = raw.input[raw.input.length - 1];
|
|
292
|
+
if (last && typeof last === "object" && (last as { type?: string }).type === "compaction_trigger") {
|
|
293
|
+
raw.input.splice(raw.input.length - 1, 0, devItem);
|
|
294
|
+
} else {
|
|
295
|
+
raw.input.push(devItem);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import type { Server } from "bun";
|
|
2
|
+
import { bridgeToResponsesSSE, buildResponseJSON, formatErrorResponse, type ResponsesTerminalStatus } from "../../bridge";
|
|
3
|
+
import {
|
|
4
|
+
getConfigPath,
|
|
5
|
+
multiAgentGuidanceEnabled,
|
|
6
|
+
resolveEnvValue,
|
|
7
|
+
} from "../../config";
|
|
8
|
+
import { parseRequest } from "../../responses/parser";
|
|
9
|
+
import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
|
|
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
|
+
import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
|
|
99
|
+
|
|
100
|
+
import { decodeRequestErrorResponse, handleResponses, usesCodexForwardPoolAuth } from "./core";
|
|
101
|
+
import { fetchWithHeaderTimeout, providerFetch, safeHostLabel } from "./fetch-helpers";
|
|
102
|
+
|
|
103
|
+
export const COMPACT_RESPONSE_MAX_BYTES = 32 * 1024 * 1024;
|
|
104
|
+
|
|
105
|
+
export function compactResponseTooLargeError(): Response {
|
|
106
|
+
return new Response(JSON.stringify({
|
|
107
|
+
error: {
|
|
108
|
+
message: "Compact response exceeded 32 MiB",
|
|
109
|
+
type: "compact_response_too_large",
|
|
110
|
+
code: "compact_response_too_large",
|
|
111
|
+
},
|
|
112
|
+
}), { status: 502, headers: { "Content-Type": "application/json" } });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
export async function bufferCompactResponse(upstream: Response, signal: AbortSignal): Promise<Response> {
|
|
118
|
+
const reader = upstream.body?.getReader();
|
|
119
|
+
const contentType = upstream.headers.get("content-type") ?? "application/json";
|
|
120
|
+
if (!reader) return new Response(null, { status: upstream.status, headers: { "Content-Type": contentType } });
|
|
121
|
+
const declaredLength = Number(upstream.headers.get("content-length"));
|
|
122
|
+
if (Number.isFinite(declaredLength) && declaredLength > COMPACT_RESPONSE_MAX_BYTES) {
|
|
123
|
+
await reader.cancel("compact_response_too_large").catch(() => undefined);
|
|
124
|
+
return compactResponseTooLargeError();
|
|
125
|
+
}
|
|
126
|
+
const chunks: Uint8Array[] = [];
|
|
127
|
+
let total = 0;
|
|
128
|
+
try {
|
|
129
|
+
while (true) {
|
|
130
|
+
if (signal.aborted) {
|
|
131
|
+
await reader.cancel(signal.reason).catch(() => undefined);
|
|
132
|
+
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
133
|
+
}
|
|
134
|
+
const { done, value } = await reader.read();
|
|
135
|
+
if (done) break;
|
|
136
|
+
total += value.byteLength;
|
|
137
|
+
if (total > COMPACT_RESPONSE_MAX_BYTES) {
|
|
138
|
+
await reader.cancel("compact_response_too_large").catch(() => undefined);
|
|
139
|
+
return compactResponseTooLargeError();
|
|
140
|
+
}
|
|
141
|
+
chunks.push(value);
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
if (signal.aborted) return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
145
|
+
return formatErrorResponse(502, "upstream_error", "Failed to read compact response");
|
|
146
|
+
}
|
|
147
|
+
const body = new Uint8Array(total);
|
|
148
|
+
let offset = 0;
|
|
149
|
+
for (const chunk of chunks) {
|
|
150
|
+
body.set(chunk, offset);
|
|
151
|
+
offset += chunk.byteLength;
|
|
152
|
+
}
|
|
153
|
+
return new Response(body, { status: upstream.status, headers: { "Content-Type": contentType } });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
export async function handleResponsesCompact(
|
|
159
|
+
req: Request,
|
|
160
|
+
config: OcxConfig,
|
|
161
|
+
logCtx: RequestLogContext,
|
|
162
|
+
): Promise<Response> {
|
|
163
|
+
let body: unknown;
|
|
164
|
+
try {
|
|
165
|
+
body = await readJsonRequestBody(req);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
return decodeRequestErrorResponse(err, "responses-compact");
|
|
168
|
+
}
|
|
169
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
170
|
+
return formatErrorResponse(400, "invalid_request_error", "Invalid compaction request body");
|
|
171
|
+
}
|
|
172
|
+
const raw = body as { model?: unknown; input?: unknown };
|
|
173
|
+
if (typeof raw.model !== "string" || raw.model.length === 0) {
|
|
174
|
+
return formatErrorResponse(400, "invalid_request_error", "compaction request requires a model");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let route;
|
|
178
|
+
try {
|
|
179
|
+
route = routeModel(config, raw.model);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
|
|
182
|
+
}
|
|
183
|
+
const selectedModelId = route.modelId;
|
|
184
|
+
logCtx.requestedModel = raw.model;
|
|
185
|
+
logCtx.model = selectedModelId;
|
|
186
|
+
logCtx.provider = route.providerName;
|
|
187
|
+
logCtx.providerAdapter = route.provider.adapter;
|
|
188
|
+
const virtual = resolveOpenAiCompactModel(route.providerName, selectedModelId);
|
|
189
|
+
if (virtual) {
|
|
190
|
+
route.modelId = virtual.wireModelId;
|
|
191
|
+
logCtx.model = virtual.selectedModelId;
|
|
192
|
+
logCtx.resolvedModel = virtual.wireModelId;
|
|
193
|
+
} else {
|
|
194
|
+
logCtx.resolvedModel = route.modelId;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (route.codexAccountMode === "direct") {
|
|
198
|
+
try { validateForwardAdmissionCredential(req.headers, config); }
|
|
199
|
+
catch (err) {
|
|
200
|
+
if (err instanceof ForwardAdmissionCredentialError) return formatErrorResponse(401, "authentication_error", err.message);
|
|
201
|
+
throw err;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (route.provider.adapter === "openai-responses") {
|
|
206
|
+
// Native ChatGPT/OpenAI model: forward the compact request verbatim to the real backend.
|
|
207
|
+
// Resolve the SAME pool/thread auth context as /v1/responses — forwarding the caller's raw
|
|
208
|
+
// headers would run compaction on the wrong account (or 401) whenever a pool account is
|
|
209
|
+
// active for this thread while normal turns succeed.
|
|
210
|
+
let compactProvider = route.provider;
|
|
211
|
+
let authCtx: CodexAuthContext = { kind: "main", accountId: null };
|
|
212
|
+
const headers = new Headers({ "content-type": "application/json" });
|
|
213
|
+
try {
|
|
214
|
+
if (route.codexAccountMode) {
|
|
215
|
+
authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode);
|
|
216
|
+
const selected = headersForCodexAuthContext(req.headers, authCtx);
|
|
217
|
+
compactProvider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
|
|
218
|
+
for (const name of FORWARD_HEADERS) {
|
|
219
|
+
const value = selected.get(name);
|
|
220
|
+
if (value) headers.set(name, value);
|
|
221
|
+
}
|
|
222
|
+
const override = (compactProvider as { _codexAccountOverride?: { accessToken: string; chatgptAccountId: string } })._codexAccountOverride;
|
|
223
|
+
if (override) {
|
|
224
|
+
headers.set("authorization", `Bearer ${override.accessToken}`);
|
|
225
|
+
headers.set("chatgpt-account-id", override.chatgptAccountId);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} catch (err) {
|
|
229
|
+
if (err instanceof CodexAccountCooldownError) {
|
|
230
|
+
return formatErrorResponse(429, "rate_limit_error", "Selected Codex account is cooling down");
|
|
231
|
+
}
|
|
232
|
+
if (err instanceof CodexThreadAffinityExpiredError) {
|
|
233
|
+
return formatErrorResponse(409, "invalid_request_error", "Codex thread account affinity expired; start a new session");
|
|
234
|
+
}
|
|
235
|
+
if (err instanceof CodexAuthContextError) {
|
|
236
|
+
return formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication");
|
|
237
|
+
}
|
|
238
|
+
if (err instanceof CodexPoolAuthenticationError || err instanceof CodexDirectAuthenticationError) {
|
|
239
|
+
return formatErrorResponse(401, "authentication_error", err.message);
|
|
240
|
+
}
|
|
241
|
+
throw err;
|
|
242
|
+
}
|
|
243
|
+
const base = (compactProvider.baseUrl ?? "").replace(/\/$/, "");
|
|
244
|
+
if (compactProvider.apiKey) headers.set("authorization", `Bearer ${resolveEnvValue(compactProvider.apiKey)}`);
|
|
245
|
+
const { reasoning: _reasoning, ...compactBodyRaw } = raw as typeof raw & { reasoning?: unknown };
|
|
246
|
+
// The regular /v1/responses path applies sanitizeReasoningInputContent via the adapter's
|
|
247
|
+
// buildRequest, but the compact endpoint forwards directly. Apply the same sanitizer here
|
|
248
|
+
// so routed-model reasoning items (reasoning_text content) don't 400 the ChatGPT backend.
|
|
249
|
+
const compactBody = sanitizeReasoningInputContent(compactBodyRaw) as typeof compactBodyRaw;
|
|
250
|
+
const compactUrl = `${base}/responses/compact`;
|
|
251
|
+
const compactThreadId = req.headers.get("x-codex-parent-thread-id");
|
|
252
|
+
const connectMs = config.connectTimeoutMs ?? 200_000;
|
|
253
|
+
const recordCompactPoolOutcome = (outcome: CodexUpstreamOutcome, meta: { retryAfter?: string | null } = {}) => {
|
|
254
|
+
if (!usesCodexForwardPoolAuth(authCtx, route.provider)) return;
|
|
255
|
+
recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
|
|
256
|
+
...meta,
|
|
257
|
+
threadId: compactThreadId,
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
let upstream: Response;
|
|
261
|
+
try {
|
|
262
|
+
// Same connect timeout + keep-alive reset + transient-5xx recovery as /v1/responses —
|
|
263
|
+
// compact hits the same ChatGPT host and must soft-avoid / clear affinity (#186).
|
|
264
|
+
upstream = await fetchWithTransientRetry(
|
|
265
|
+
recovery => fetchWithHeaderTimeout(
|
|
266
|
+
compactUrl,
|
|
267
|
+
applyUpstreamRecoveryInit({
|
|
268
|
+
method: "POST",
|
|
269
|
+
headers,
|
|
270
|
+
body: JSON.stringify({ ...compactBody, model: route.modelId }),
|
|
271
|
+
}, recovery),
|
|
272
|
+
req.signal,
|
|
273
|
+
connectMs,
|
|
274
|
+
false,
|
|
275
|
+
providerFetch(compactProvider),
|
|
276
|
+
),
|
|
277
|
+
{ abortSignal: req.signal, label: safeHostLabel(compactUrl) },
|
|
278
|
+
);
|
|
279
|
+
} catch (err) {
|
|
280
|
+
if (req.signal.aborted) return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
|
|
281
|
+
const outcome = err instanceof Error && err.name === "TimeoutError" ? "timeout" : "connect_error";
|
|
282
|
+
recordCompactPoolOutcome(outcome);
|
|
283
|
+
return formatErrorResponse(502, "upstream_error", "Failed to connect to compact upstream");
|
|
284
|
+
}
|
|
285
|
+
const retryAfter = upstream.headers.get("retry-after");
|
|
286
|
+
const buffered = await bufferCompactResponse(upstream, req.signal);
|
|
287
|
+
// Record pool health only after the body is fully delivered (or definitively failed).
|
|
288
|
+
// A premature 200 would clear soft-avoid while the client still sees a buffer 502.
|
|
289
|
+
if (buffered.status === 499) {
|
|
290
|
+
return buffered;
|
|
291
|
+
}
|
|
292
|
+
if (upstream.ok && buffered.status >= 500) {
|
|
293
|
+
// The upstream account returned 200 — it is healthy. The buffering failure
|
|
294
|
+
// (oversized body exceeding COMPACT_RESPONSE_MAX_BYTES, or a rare mid-read
|
|
295
|
+
// reset on a small JSON payload) is a local proxy issue, not account flakiness.
|
|
296
|
+
// Record the upstream status so a deterministic payload-size limit does not
|
|
297
|
+
// soft-avoid a healthy account and rotate a thread for 30s.
|
|
298
|
+
recordCompactPoolOutcome(upstream.status, { retryAfter });
|
|
299
|
+
} else {
|
|
300
|
+
recordCompactPoolOutcome(upstream.status, { retryAfter });
|
|
301
|
+
}
|
|
302
|
+
return buffered;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ROUTED model: run the v2 synthetic-compaction turn internally (appends COMPACT_PROMPT, no
|
|
306
|
+
// tools) and decode the resulting ocx1 envelope into plain v1 replacement-history items.
|
|
307
|
+
const inputItems = Array.isArray(raw.input) ? (raw.input as unknown[]) : [];
|
|
308
|
+
const internalBody = {
|
|
309
|
+
...raw,
|
|
310
|
+
stream: false,
|
|
311
|
+
input: [...inputItems, { type: "compaction_trigger" }],
|
|
312
|
+
};
|
|
313
|
+
const internalHeaders = new Headers({ "content-type": "application/json" });
|
|
314
|
+
for (const name of FORWARD_HEADERS) {
|
|
315
|
+
const value = req.headers.get(name);
|
|
316
|
+
if (value) internalHeaders.set(name, value);
|
|
317
|
+
}
|
|
318
|
+
const internalReq = new Request("http://localhost/v1/responses", {
|
|
319
|
+
method: "POST",
|
|
320
|
+
headers: internalHeaders,
|
|
321
|
+
body: JSON.stringify(internalBody),
|
|
322
|
+
});
|
|
323
|
+
const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal });
|
|
324
|
+
if (!response.ok) return response;
|
|
325
|
+
let json: { output?: unknown[] };
|
|
326
|
+
try {
|
|
327
|
+
json = await response.json() as { output?: unknown[] };
|
|
328
|
+
} catch {
|
|
329
|
+
return formatErrorResponse(502, "server_error", "compaction turn returned a non-JSON response");
|
|
330
|
+
}
|
|
331
|
+
const compactionItem = (json.output ?? []).find(
|
|
332
|
+
(item): item is { type: string; encrypted_content?: string } =>
|
|
333
|
+
!!item && typeof item === "object" && (item as { type?: string }).type === "compaction",
|
|
334
|
+
);
|
|
335
|
+
const summary = compactionItem?.encrypted_content
|
|
336
|
+
? decodeCompactionSummary(compactionItem.encrypted_content) ?? ""
|
|
337
|
+
: "";
|
|
338
|
+
const output = buildCompactV1Output(extractCompactUserMessages(inputItems), summary);
|
|
339
|
+
return new Response(JSON.stringify({ output }), { headers: { "Content-Type": "application/json" } });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
|