@yansigit/opencodex 2.31.3 → 2.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-BG43zwVe.js +102 -0
- package/gui/dist/assets/index-CiSI-jrP.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +6 -8
- package/src/adapters/base.ts +2 -0
- package/src/adapters/command-code.ts +2 -3
- package/src/adapters/cursor/live-models.ts +8 -0
- package/src/adapters/cursor/native-exec-desktop.ts +16 -0
- package/src/adapters/cursor/tool-definitions.ts +1 -1
- package/src/adapters/google.ts +6 -7
- package/src/adapters/kiro.ts +0 -3
- package/src/adapters/openai-responses.ts +3 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-web-search.ts +7 -2
- package/src/bridge.ts +21 -15
- package/src/cli/dispatch.ts +50 -2
- package/src/cli/doctor.ts +24 -11
- package/src/cli/help.ts +4 -3
- package/src/cli/index.ts +11 -4
- package/src/cli/models.ts +13 -3
- package/src/cli/observe.ts +20 -5
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +7 -5
- package/src/cli/status.ts +2 -1
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +134 -0
- package/src/codex/app-server-processes.ts +3 -1
- package/src/codex/catalog/aggregation.ts +13 -1
- package/src/codex/catalog/effort.ts +24 -9
- package/src/codex/catalog/model-metadata.ts +566 -0
- package/src/codex/catalog/parsing.ts +33 -0
- package/src/codex/catalog/provider-fetch.ts +123 -32
- package/src/codex/catalog/sync.ts +5 -4
- package/src/codex/desktop-app-restart.ts +342 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject.ts +49 -21
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/combos/failover.ts +27 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +81 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +308 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config.ts +75 -812
- package/src/generated/compatibility-version.json +145 -85
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +141 -0
- package/src/integrations/ownership.ts +10 -0
- package/src/integrations/state.ts +44 -5
- package/src/integrations/writer.ts +6 -0
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/process-control.ts +2 -1
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/health.ts +1 -1
- package/src/providers/registry.ts +1 -1
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/custom-tool-compat.ts +132 -38
- package/src/responses/parser.ts +3 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/server/auth-cors.ts +9 -7
- package/src/server/index.ts +102 -21
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +1 -1
- package/src/server/management/config-routes.ts +4 -1
- package/src/server/management/context.ts +1 -1
- package/src/server/management/logs-usage-routes.ts +27 -6
- package/src/server/management/model-routes.ts +8 -4
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +2 -1
- package/src/server/request-log-conversation.ts +30 -0
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/combo-stream-preflight.ts +171 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +225 -94
- package/src/server/responses/fetch-helpers.ts +2 -97
- package/src/server/responses-custom-tool-repair.ts +41 -5
- package/src/server/responses-undeclared-tool-guard.ts +156 -15
- package/src/service.ts +8 -4
- package/src/types/request.ts +6 -1
- package/src/types/tools.ts +87 -11
- package/src/types.ts +1 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/log.ts +16 -8
- package/src/usage/summary.ts +201 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/executor.ts +10 -3
- package/gui/dist/assets/index-CGoDO3uO.css +0 -1
- package/gui/dist/assets/index-Cxt5fZMP.js +0 -102
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
type PersistedUsageEntry,
|
|
56
56
|
} from "../../usage/log";
|
|
57
57
|
import { getUsageDebugLogEntries } from "../../usage/debug";
|
|
58
|
-
import { parseRange, parseUsageSurface, rangeWindow, summarizeUsage, type UsageRange, type UsageSummary, type UsageSurface } from "../../usage/summary";
|
|
58
|
+
import { USAGE_RANGES, USAGE_SURFACES, parseRange, parseUsageSurface, projectUsageSummary, rangeWindow, summarizeUsage, type UsageRange, type UsageSummary, type UsageSurface } from "../../usage/summary";
|
|
59
59
|
import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
|
|
60
60
|
import { getProviderRegistryEntry } from "../../providers/registry";
|
|
61
61
|
import { getDebugLogEntries } from "../../lib/debug-log-buffer";
|
|
@@ -197,6 +197,17 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
197
197
|
if (url.pathname === "/api/usage" && req.method === "GET") {
|
|
198
198
|
const range = parseRange(url.searchParams.get("range"));
|
|
199
199
|
const surface = parseUsageSurface(url.searchParams.get("surface"));
|
|
200
|
+
// Applied to the OUTGOING payload only. A filtered summary must never reach
|
|
201
|
+
// the cache or the warm loop below: the key is `range:surface`, so a
|
|
202
|
+
// filtered entry stored under it would be served to the next unfiltered
|
|
203
|
+
// caller, dashboard included.
|
|
204
|
+
const filter = {
|
|
205
|
+
provider: url.searchParams.get("provider"),
|
|
206
|
+
model: url.searchParams.get("model"),
|
|
207
|
+
};
|
|
208
|
+
const project = <T extends UsageSummary>(summary: T, entries?: PersistedUsageEntry[]) =>
|
|
209
|
+
projectUsageSummary(summary, filter, entries);
|
|
210
|
+
const filterRequested = Boolean(filter.provider ?? filter.model);
|
|
200
211
|
const now = Date.now();
|
|
201
212
|
try {
|
|
202
213
|
const cacheKey = `${range}:${surface}`;
|
|
@@ -206,6 +217,12 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
206
217
|
const observedSize = observed?.size ?? 0;
|
|
207
218
|
const cached = getUsageSummaryCacheEntry(cacheKey);
|
|
208
219
|
if (cached
|
|
220
|
+
// A filtered response is re-summarised from entries, which a cached
|
|
221
|
+
// summary does not carry. Serving it from cache would mean projecting
|
|
222
|
+
// over collapsed breakdown rows again — the exact defect the
|
|
223
|
+
// re-summarisation replaced. The unfiltered cache stays warm either
|
|
224
|
+
// way; only the filtered caller pays for the read.
|
|
225
|
+
&& !filterRequested
|
|
209
226
|
&& cached.identityKey === identityKey
|
|
210
227
|
&& cached.maxReadBytes === effectiveReadLimit
|
|
211
228
|
&& cached.overlayVersion === userCostOverlayVersion()
|
|
@@ -214,7 +231,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
214
231
|
&& observedSize >= cached.lastSeenSize) {
|
|
215
232
|
return jsonResponse(refreshedUsageSummary(cached.summary, range, now));
|
|
216
233
|
}
|
|
217
|
-
if (cached) discardUsageSummaryCacheEntry(cacheKey);
|
|
234
|
+
if (cached && !filterRequested) discardUsageSummaryCacheEntry(cacheKey);
|
|
218
235
|
// Capture the overlay version BEFORE reading/computing: the cache entry
|
|
219
236
|
// must be stamped with the version the summary was priced under. Reading
|
|
220
237
|
// it again at stamp time could cache an old-price summary as current,
|
|
@@ -238,14 +255,18 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
238
255
|
// summary may mix old and new prices. Serve it uncached: the next
|
|
239
256
|
// request recomputes against the settled overlay instead of caching a
|
|
240
257
|
// mixed-price entry under either version.
|
|
241
|
-
return jsonResponse(summary);
|
|
258
|
+
return jsonResponse(project(summary, snapshot.entries));
|
|
242
259
|
}
|
|
243
260
|
const freshUntil = now + 60_000;
|
|
244
261
|
const snapshotIdentity = `${usageLogIdentityKey(snapshot.revision)}\0${effectiveReadLimit}`;
|
|
245
262
|
const revisionKey = `${usageLogRevisionKey(snapshot.revision)}\0${effectiveReadLimit}`;
|
|
246
263
|
const lastSeenSize = snapshot.revision?.size ?? 0;
|
|
247
|
-
|
|
248
|
-
|
|
264
|
+
// Derived from the canonical constants rather than re-listed: a subset
|
|
265
|
+
// literal type-checks perfectly happily, so a range added to the union
|
|
266
|
+
// and forgotten here would never be warmed and never invalidated
|
|
267
|
+
// alongside its siblings.
|
|
268
|
+
const ranges: readonly UsageRange[] = USAGE_RANGES;
|
|
269
|
+
const surfaces: readonly UsageSurface[] = USAGE_SURFACES;
|
|
249
270
|
for (const nextRange of ranges) {
|
|
250
271
|
for (const nextSurface of surfaces) {
|
|
251
272
|
const nextSummary = nextRange === range && nextSurface === surface ? summary : {
|
|
@@ -279,7 +300,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
279
300
|
effectiveReadLimit,
|
|
280
301
|
now,
|
|
281
302
|
);
|
|
282
|
-
return jsonResponse(summary);
|
|
303
|
+
return jsonResponse(project(summary, snapshot.entries));
|
|
283
304
|
} catch {
|
|
284
305
|
return jsonResponse({
|
|
285
306
|
range,
|
|
@@ -394,8 +394,10 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
394
394
|
}
|
|
395
395
|
|
|
396
396
|
if (url.pathname === "/api/custom-models" && req.method === "POST") {
|
|
397
|
-
let
|
|
398
|
-
try {
|
|
397
|
+
let parsedBody: unknown;
|
|
398
|
+
try { parsedBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
399
|
+
if (!isPlainRecord(parsedBody)) return jsonResponse({ error: "invalid JSON body" }, 400);
|
|
400
|
+
const body = parsedBody;
|
|
399
401
|
const provider = typeof body.provider === "string" ? body.provider.trim() : "";
|
|
400
402
|
const modelId = typeof body.modelId === "string" ? body.modelId.trim() : "";
|
|
401
403
|
if (!provider || !modelId) return jsonResponse({ error: "provider and modelId are required" }, 400);
|
|
@@ -441,8 +443,10 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
441
443
|
if (customPutMatch && req.method === "PUT") {
|
|
442
444
|
let id: string;
|
|
443
445
|
try { id = decodeURIComponent(customPutMatch[1]); } catch { return jsonResponse({ error: "invalid id encoding" }, 400); }
|
|
444
|
-
let
|
|
445
|
-
try {
|
|
446
|
+
let parsedBody: unknown;
|
|
447
|
+
try { parsedBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
448
|
+
if (!isPlainRecord(parsedBody)) return jsonResponse({ error: "invalid JSON body" }, 400);
|
|
449
|
+
const body = parsedBody;
|
|
446
450
|
const list = config.customModels ?? [];
|
|
447
451
|
const idx = list.findIndex(cm => cm.id === id);
|
|
448
452
|
if (idx === -1) return jsonResponse({ error: "not found" }, 404);
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
* Design of record: devlog/_fin/260803_integrations_toggle_all/030 (routes),
|
|
18
18
|
* 011 (Claude Code), 012 (Grok).
|
|
19
19
|
*/
|
|
20
|
-
import { loadConfig,
|
|
20
|
+
import { loadConfig, saveConfigPreservingClaudeCode } from "../../config";
|
|
21
|
+
import { readRuntimePort } from "../../config/process-state";
|
|
21
22
|
import { desktopVisibleNativeSlugs, filterCatalogVisibleModels, nativeContextLimits, nativeOpenAiContextWindow, visibleNativeSlugs } from "../../codex/catalog";
|
|
22
23
|
import { providerContextCap } from "../../providers/context-cap";
|
|
23
24
|
import { OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
stopServerListener,
|
|
34
34
|
} from "../lifecycle";
|
|
35
35
|
import { isServiceViable } from "../../service";
|
|
36
|
-
import { readRuntimePort } from "../../config";
|
|
36
|
+
import { readRuntimePort } from "../../config/process-state";
|
|
37
37
|
import { withProcessRuntimeProvenance } from "../../lib/bun-runtime";
|
|
38
38
|
import { selfLaunchArgv } from "../../lib/self-launch-argv";
|
|
39
39
|
import {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* (non-ocx, non-allowlisted) processes are never killed.
|
|
10
10
|
*/
|
|
11
11
|
import { execFileSync } from "node:child_process";
|
|
12
|
-
import { verifyPidIdentity } from "../config";
|
|
12
|
+
import { verifyPidIdentity } from "../config/process-state";
|
|
13
13
|
import { isProcessAlive, killProxy } from "../lib/process-control";
|
|
14
14
|
import { isPortAvailable, type WaitForPortOptions } from "./ports";
|
|
15
15
|
import { dropWindowsTcpRowsForLocalPort } from "./windows-tcp-drop";
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Lives outside cli.ts (which dispatches argv at module top level) so tests can import it.
|
|
11
11
|
*/
|
|
12
|
-
import { loadConfig
|
|
12
|
+
import { loadConfig } from "../config";
|
|
13
|
+
import { readAlivePid, readRuntimePort, verifyPidIdentity } from "../config/process-state";
|
|
13
14
|
import { directLocalHttpFetch } from "./direct-local-http";
|
|
14
15
|
|
|
15
16
|
export interface HealthzIdentity {
|
|
@@ -61,6 +61,36 @@ export function sessionIdHeaderFromRequest(headers: Headers): string | null {
|
|
|
61
61
|
return headers.get("session_id") ?? headers.get("session-id");
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function firstSanitizedConversationId(
|
|
65
|
+
...values: Array<string | null | undefined>
|
|
66
|
+
): string | undefined {
|
|
67
|
+
for (const value of values) {
|
|
68
|
+
const sanitized = sanitizeConversationIdInput(value);
|
|
69
|
+
if (sanitized) return sanitized;
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Conversation namespace for reasoning replay. Unlike the persisted log id, this stays the raw
|
|
76
|
+
* sanitized identity so mixed headers that carry the same conversation still hit one serving
|
|
77
|
+
* record. Do not hash, and do not prefer session_id over a true per-conversation thread/Cursor
|
|
78
|
+
* identity: session_id can be synthesized from a shared prompt_cache_key.
|
|
79
|
+
*/
|
|
80
|
+
export function reasoningReplayConversationIdFromResponsesRequest(input: {
|
|
81
|
+
clientThreadId?: string;
|
|
82
|
+
threadIdHeader?: string | null;
|
|
83
|
+
cursorConversationId?: string;
|
|
84
|
+
sessionIdHeader?: string | null;
|
|
85
|
+
}): string | undefined {
|
|
86
|
+
return firstSanitizedConversationId(
|
|
87
|
+
input.clientThreadId,
|
|
88
|
+
input.threadIdHeader,
|
|
89
|
+
input.cursorConversationId,
|
|
90
|
+
input.sessionIdHeader,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
64
94
|
export function conversationIdFromResponsesRequest(input: {
|
|
65
95
|
clientThreadId?: string;
|
|
66
96
|
sessionIdHeader?: string | null;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { formatErrorResponse } from "../../bridge";
|
|
2
|
+
import {
|
|
3
|
+
CodexAccountCooldownError,
|
|
4
|
+
codexMainProfileDrainingResponse,
|
|
5
|
+
cooldownErrorResponse,
|
|
6
|
+
CodexAuthContextError,
|
|
7
|
+
CodexDirectAuthenticationError,
|
|
8
|
+
CodexMainProfileDrainingError,
|
|
9
|
+
CodexMainSubstitutionUnavailableError,
|
|
10
|
+
CodexPoolAuthenticationError,
|
|
11
|
+
CodexThreadAffinityExpiredError,
|
|
12
|
+
} from "../../codex/auth-context";
|
|
13
|
+
|
|
14
|
+
export interface CodexAuthContextErrorResponseOptions {
|
|
15
|
+
accountSelector?: string;
|
|
16
|
+
now: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Shared HTTP contract for Codex auth-context failures on Responses surfaces. */
|
|
20
|
+
export function mapCodexAuthContextErrorToResponse(
|
|
21
|
+
error: unknown,
|
|
22
|
+
options: CodexAuthContextErrorResponseOptions,
|
|
23
|
+
): Response | undefined {
|
|
24
|
+
if (error instanceof CodexAccountCooldownError) {
|
|
25
|
+
return cooldownErrorResponse(error, options.now, options.accountSelector);
|
|
26
|
+
}
|
|
27
|
+
if (error instanceof CodexMainProfileDrainingError) {
|
|
28
|
+
return codexMainProfileDrainingResponse();
|
|
29
|
+
}
|
|
30
|
+
if (error instanceof CodexThreadAffinityExpiredError) {
|
|
31
|
+
return formatErrorResponse(
|
|
32
|
+
409,
|
|
33
|
+
"invalid_request_error",
|
|
34
|
+
"Codex thread account affinity expired; start a new session",
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
if (error instanceof CodexAuthContextError) {
|
|
38
|
+
return formatErrorResponse(
|
|
39
|
+
401,
|
|
40
|
+
"authentication_error",
|
|
41
|
+
"Selected Codex account needs reauthentication",
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (error instanceof CodexPoolAuthenticationError || error instanceof CodexDirectAuthenticationError) {
|
|
45
|
+
return formatErrorResponse(401, "authentication_error", error.message);
|
|
46
|
+
}
|
|
47
|
+
if (error instanceof CodexMainSubstitutionUnavailableError) {
|
|
48
|
+
return formatErrorResponse(
|
|
49
|
+
401,
|
|
50
|
+
"authentication_error",
|
|
51
|
+
"No usable Codex main credential to serve this request",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { ResponsesTerminalStatus } from "../../bridge";
|
|
2
|
+
import type { RequestLogContext } from "../request-log";
|
|
3
|
+
import { createSseInspector } from "../relay";
|
|
4
|
+
import { MAX_CLIENT_SSE_FRAME_BYTES } from "../sse-frame-buffer";
|
|
5
|
+
|
|
6
|
+
const COMBO_STREAM_PREFLIGHT_MAX_BYTES = MAX_CLIENT_SSE_FRAME_BYTES;
|
|
7
|
+
|
|
8
|
+
const PRE_OUTPUT_CONTROL_EVENTS = new Set([
|
|
9
|
+
"response.created",
|
|
10
|
+
"response.in_progress",
|
|
11
|
+
"response.queued",
|
|
12
|
+
"response.heartbeat",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const TERMINAL_EVENTS = new Set([
|
|
16
|
+
"response.completed",
|
|
17
|
+
"response.failed",
|
|
18
|
+
"response.incomplete",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Decide when replaying the request on another combo target would risk duplicating
|
|
23
|
+
* client-visible output or a tool-side effect. Unknown event types commit the child
|
|
24
|
+
* conservatively; only the small Responses lifecycle preamble remains replayable.
|
|
25
|
+
*/
|
|
26
|
+
export function comboStreamPayloadCommitsOutput(payload: unknown): boolean {
|
|
27
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return true;
|
|
28
|
+
const type = (payload as { type?: unknown }).type;
|
|
29
|
+
if (typeof type !== "string") return true;
|
|
30
|
+
return !PRE_OUTPUT_CONTROL_EVENTS.has(type) && !TERMINAL_EVENTS.has(type);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function replayBufferedResponse(
|
|
34
|
+
response: Response,
|
|
35
|
+
reader: ReadableStreamDefaultReader<Uint8Array>,
|
|
36
|
+
buffered: Uint8Array[],
|
|
37
|
+
): Response {
|
|
38
|
+
let index = 0;
|
|
39
|
+
const body = new ReadableStream<Uint8Array>({
|
|
40
|
+
async pull(controller) {
|
|
41
|
+
if (index < buffered.length) {
|
|
42
|
+
controller.enqueue(buffered[index++]!);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const next = await reader.read();
|
|
47
|
+
if (next.done) controller.close();
|
|
48
|
+
else controller.enqueue(next.value);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
try { controller.error(error); } catch { /* consumer already closed */ }
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
cancel(reason) {
|
|
54
|
+
reader.cancel(reason).catch(() => undefined);
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
return new Response(body, {
|
|
58
|
+
status: response.status,
|
|
59
|
+
statusText: response.statusText,
|
|
60
|
+
headers: response.headers,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function failedTerminalResponse(
|
|
65
|
+
response: Response,
|
|
66
|
+
terminalPayload: Record<string, unknown>,
|
|
67
|
+
logCtx: RequestLogContext,
|
|
68
|
+
): Response {
|
|
69
|
+
const nested = terminalPayload.response;
|
|
70
|
+
const terminalResponse = nested && typeof nested === "object" && !Array.isArray(nested)
|
|
71
|
+
? nested as Record<string, unknown>
|
|
72
|
+
: {};
|
|
73
|
+
const nestedError = terminalResponse.error;
|
|
74
|
+
const error = nestedError && typeof nestedError === "object" && !Array.isArray(nestedError)
|
|
75
|
+
? nestedError as Record<string, unknown>
|
|
76
|
+
: {
|
|
77
|
+
type: "upstream_error",
|
|
78
|
+
code: "upstream_server_error",
|
|
79
|
+
message: logCtx.upstreamError ?? "Provider stream failed before producing output",
|
|
80
|
+
};
|
|
81
|
+
const headers = new Headers(response.headers);
|
|
82
|
+
headers.set("content-type", "application/json");
|
|
83
|
+
headers.delete("content-length");
|
|
84
|
+
headers.delete("content-encoding");
|
|
85
|
+
const usage = terminalResponse.usage;
|
|
86
|
+
return new Response(JSON.stringify({
|
|
87
|
+
error,
|
|
88
|
+
// The combo classifier needs only the error and optional usage. Do not carry
|
|
89
|
+
// response ids, provider metadata, or future terminal fields into the client
|
|
90
|
+
// error envelope merely because they shared the terminal snapshot.
|
|
91
|
+
response: {
|
|
92
|
+
error,
|
|
93
|
+
...(usage && typeof usage === "object" && !Array.isArray(usage) ? { usage } : {}),
|
|
94
|
+
},
|
|
95
|
+
}), {
|
|
96
|
+
status: logCtx.terminalHttpStatus ?? 502,
|
|
97
|
+
headers,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type ComboStreamPreflightResult =
|
|
102
|
+
| { kind: "accepted"; response: Response }
|
|
103
|
+
| { kind: "failed"; response: Response };
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Buffer a combo child's downstream SSE only until the request becomes unsafe to
|
|
107
|
+
* replay or reaches a terminal. This owns exactly one body reader. The aggregate
|
|
108
|
+
* buffer is capped; hitting the cap commits the current target instead of growing
|
|
109
|
+
* memory or guessing that replay is safe.
|
|
110
|
+
*/
|
|
111
|
+
export async function preflightComboStreamResponse(
|
|
112
|
+
response: Response,
|
|
113
|
+
logCtx: RequestLogContext,
|
|
114
|
+
): Promise<ComboStreamPreflightResult> {
|
|
115
|
+
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
116
|
+
if (!response.ok || !response.body || !contentType.includes("text/event-stream")) {
|
|
117
|
+
return { kind: "accepted", response };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const reader = response.body.getReader();
|
|
121
|
+
const buffered: Uint8Array[] = [];
|
|
122
|
+
let bufferedBytes = 0;
|
|
123
|
+
let outputCommitted = false;
|
|
124
|
+
let terminalStatus: ResponsesTerminalStatus | undefined;
|
|
125
|
+
let failedPayload: Record<string, unknown> | undefined;
|
|
126
|
+
const inspector = createSseInspector({
|
|
127
|
+
logCtx,
|
|
128
|
+
onParsedPayload: payload => {
|
|
129
|
+
if (comboStreamPayloadCommitsOutput(payload)) outputCommitted = true;
|
|
130
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return;
|
|
131
|
+
if ((payload as { type?: unknown }).type === "response.failed") {
|
|
132
|
+
failedPayload = payload as Record<string, unknown>;
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
onTerminal: status => { terminalStatus = status; },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
for (;;) {
|
|
140
|
+
const next = await reader.read();
|
|
141
|
+
if (next.done) {
|
|
142
|
+
inspector.finish();
|
|
143
|
+
} else {
|
|
144
|
+
if (bufferedBytes + next.value.byteLength > COMBO_STREAM_PREFLIGHT_MAX_BYTES) {
|
|
145
|
+
// Keep the cap about memory the preflight allocates. The upstream chunk already exists;
|
|
146
|
+
// copying it before committing would transiently exceed the boundary for no
|
|
147
|
+
// replay benefit. Preserve it unsliced behind the already-bounded prefix.
|
|
148
|
+
return {
|
|
149
|
+
kind: "accepted",
|
|
150
|
+
response: replayBufferedResponse(response, reader, [...buffered, next.value]),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const retained = next.value.slice();
|
|
154
|
+
buffered.push(retained);
|
|
155
|
+
bufferedBytes += retained.byteLength;
|
|
156
|
+
inspector.feed(retained);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (terminalStatus === "failed" && !outputCommitted && failedPayload) {
|
|
160
|
+
await reader.cancel("retrying zero-output combo stream failure").catch(() => undefined);
|
|
161
|
+
return { kind: "failed", response: failedTerminalResponse(response, failedPayload, logCtx) };
|
|
162
|
+
}
|
|
163
|
+
if (next.done || terminalStatus !== undefined || outputCommitted
|
|
164
|
+
|| bufferedBytes >= COMBO_STREAM_PREFLIGHT_MAX_BYTES) {
|
|
165
|
+
return { kind: "accepted", response: replayBufferedResponse(response, reader, buffered) };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} finally {
|
|
169
|
+
inspector.dispose();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -42,17 +42,9 @@ import { describeImagesInPlace, planVisionSidecar, shouldResolveOpenAiVisionSide
|
|
|
42
42
|
import { createAdapterEventQueue, preflightAdapterEvents } from "../../adapters/run-turn-queue";
|
|
43
43
|
import {
|
|
44
44
|
applyCodexAuthContextToProvider,
|
|
45
|
-
CodexAccountCooldownError,
|
|
46
|
-
codexMainProfileDrainingResponse,
|
|
47
|
-
cooldownErrorResponse,
|
|
48
|
-
CodexAuthContextError,
|
|
49
|
-
CodexDirectAuthenticationError,
|
|
50
45
|
CodexMainProfileDrainingError,
|
|
51
|
-
CodexPoolAuthenticationError,
|
|
52
|
-
CodexThreadAffinityExpiredError,
|
|
53
46
|
headersForCodexAuthContext,
|
|
54
47
|
materializeCodexUpstreamAuth,
|
|
55
|
-
CodexMainSubstitutionUnavailableError,
|
|
56
48
|
isCodexAuthContextUsable,
|
|
57
49
|
resolveCodexAuthContext,
|
|
58
50
|
codexProbeLeaseId,
|
|
@@ -134,6 +126,7 @@ import {
|
|
|
134
126
|
usesCodexForwardPoolAuth,
|
|
135
127
|
} from "./core";
|
|
136
128
|
import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel } from "./fetch-helpers";
|
|
129
|
+
import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
|
|
137
130
|
|
|
138
131
|
export const COMPACT_RESPONSE_MAX_BYTES = 32 * 1024 * 1024;
|
|
139
132
|
|
|
@@ -395,19 +388,11 @@ export async function handleResponsesCompact(
|
|
|
395
388
|
}
|
|
396
389
|
}
|
|
397
390
|
} catch (err) {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
if (
|
|
403
|
-
return formatErrorResponse(409, "invalid_request_error", "Codex thread account affinity expired; start a new session");
|
|
404
|
-
}
|
|
405
|
-
if (err instanceof CodexAuthContextError) {
|
|
406
|
-
return formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication");
|
|
407
|
-
}
|
|
408
|
-
if (err instanceof CodexPoolAuthenticationError || err instanceof CodexDirectAuthenticationError) {
|
|
409
|
-
return formatErrorResponse(401, "authentication_error", err.message);
|
|
410
|
-
}
|
|
391
|
+
const response = mapCodexAuthContextErrorToResponse(err, {
|
|
392
|
+
accountSelector: route.codexAccountNamespace,
|
|
393
|
+
now: Date.now(),
|
|
394
|
+
});
|
|
395
|
+
if (response) return response;
|
|
411
396
|
throw err;
|
|
412
397
|
}
|
|
413
398
|
const base = isCanonicalOpenAiForwardProvider(compactProvider)
|