@bitkyc08/opencodex 2.17.1-preview.20260814 → 2.19.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/gui/dist/assets/{index-DUCH59lJ.css → index-CQ7bIKee.css} +1 -1
- package/gui/dist/assets/{index-ta3-_hgj.js → index-D_JUZLEC.js} +16 -16
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/client-fingerprint.ts +14 -10
- package/src/adapters/cursor/live-transport.ts +17 -5
- package/src/adapters/cursor/protobuf-events.ts +662 -20
- package/src/adapters/cursor/tool-definitions.ts +12 -6
- package/src/adapters/google-antigravity-wire.ts +4 -3
- package/src/adapters/google.ts +22 -3
- package/src/bridge.ts +12 -2
- package/src/chat/inbound.ts +24 -1
- package/src/cli/index.ts +11 -0
- package/src/codex/app-server-processes.ts +3 -3
- package/src/codex/shim.ts +100 -5
- package/src/codex/user-identity.ts +36 -6
- package/src/config.ts +0 -2
- package/src/generated/compatibility-version.json +52 -44
- package/src/lib/errors.ts +27 -0
- package/src/lib/token-estimate.ts +19 -2
- package/src/lib/windows-elevation.ts +37 -0
- package/src/lib/windows-secret-acl.ts +7 -0
- package/src/lib/windows-text.ts +106 -0
- package/src/lib/windows-user-principal.ts +0 -2
- package/src/oauth/index.ts +1 -1
- package/src/oauth/store.ts +32 -18
- package/src/providers/antigravity-models.ts +25 -5
- package/src/providers/free-directory.ts +1 -1
- package/src/providers/registry.ts +6 -3
- package/src/responses/spill-store.ts +20 -1
- package/src/responses/state.ts +159 -3
- package/src/server/chat-completions.ts +4 -2
- package/src/server/effort-policy.ts +18 -0
- package/src/server/index.ts +5 -1
- package/src/server/management/logs-usage-routes.ts +7 -22
- package/src/server/request-log.ts +48 -3
- package/src/server/responses/core.ts +59 -15
- package/src/server/responses/encrypted-payload.ts +58 -38
- package/src/server/responses/fetch-helpers.ts +12 -4
- package/src/server/responses/input-admission.ts +169 -0
- package/src/server/responses/policy-fallback.ts +13 -2
- package/src/server/responses/ws-upstream.ts +115 -6
- package/src/service-manager-probe.ts +23 -37
- package/src/service.ts +233 -25
- package/src/tray/windows.ts +0 -2
- package/src/types.ts +10 -2
- package/src/update/job.ts +2 -2
- package/src/usage/summary.ts +21 -4
- package/src/vision/index.ts +21 -4
- package/src/web-search/index.ts +2 -1
|
@@ -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, summarizeUsage, type UsageRange, type UsageSummary, type UsageSurface } from "../../usage/summary";
|
|
58
|
+
import { parseRange, parseUsageSurface, 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";
|
|
@@ -87,14 +87,6 @@ import {
|
|
|
87
87
|
} from "./usage-summary-cache";
|
|
88
88
|
import { cacheApiKeyUsageFromSnapshot } from "./api-key-usage";
|
|
89
89
|
|
|
90
|
-
const USAGE_DAY_MS = 86_400_000;
|
|
91
|
-
function usageEntryMatchesSurface(entry: PersistedUsageEntry, surface: UsageSurface): boolean {
|
|
92
|
-
if (surface === "claude") return entry.surface === "claude" || entry.surface === "claude-desktop";
|
|
93
|
-
if (surface === "grok") return entry.surface === "grok";
|
|
94
|
-
if (surface === "codex") return entry.surface === undefined;
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
90
|
function nextLocalMidnight(now: number): number {
|
|
99
91
|
const next = new Date(now);
|
|
100
92
|
next.setHours(24, 0, 0, 0);
|
|
@@ -102,24 +94,16 @@ function nextLocalMidnight(now: number): number {
|
|
|
102
94
|
}
|
|
103
95
|
|
|
104
96
|
function usageSummaryExpiresAt(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
97
|
+
_entries: PersistedUsageEntry[],
|
|
98
|
+
_range: UsageRange,
|
|
99
|
+
_surface: UsageSurface,
|
|
108
100
|
now: number,
|
|
109
101
|
): number {
|
|
110
|
-
|
|
111
|
-
const windowMs = range === "7d" ? 7 * USAGE_DAY_MS : range === "30d" ? 30 * USAGE_DAY_MS : null;
|
|
112
|
-
if (windowMs === null) return expiresAt;
|
|
113
|
-
for (const entry of entries) {
|
|
114
|
-
if (!usageEntryMatchesSurface(entry, surface)) continue;
|
|
115
|
-
const expiry = entry.timestamp + windowMs;
|
|
116
|
-
if (expiry > now && expiry < expiresAt) expiresAt = expiry;
|
|
117
|
-
}
|
|
118
|
-
return expiresAt;
|
|
102
|
+
return nextLocalMidnight(now);
|
|
119
103
|
}
|
|
120
104
|
|
|
121
105
|
function refreshedUsageSummary<T extends UsageSummary & { historyTruncated: boolean }>(summary: T, range: UsageRange, now: number): T {
|
|
122
|
-
const since = range
|
|
106
|
+
const { since } = rangeWindow(range, now);
|
|
123
107
|
return { ...summary, since, generatedAt: now };
|
|
124
108
|
}
|
|
125
109
|
|
|
@@ -226,6 +210,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
226
210
|
&& cached.maxReadBytes === effectiveReadLimit
|
|
227
211
|
&& cached.overlayVersion === userCostOverlayVersion()
|
|
228
212
|
&& now < cached.freshUntil
|
|
213
|
+
&& now < cached.expiresAt
|
|
229
214
|
&& observedSize >= cached.lastSeenSize) {
|
|
230
215
|
return jsonResponse(refreshedUsageSummary(cached.summary, range, now));
|
|
231
216
|
}
|
|
@@ -38,6 +38,10 @@ import {
|
|
|
38
38
|
} from "../usage/debug";
|
|
39
39
|
import { matchesLogConversationId } from "./request-log-conversation";
|
|
40
40
|
import { enforceAppOwnedMemoryBudget, type RetainedStoreSnapshot } from "../lib/app-owned-memory";
|
|
41
|
+
import { capEstimateAtContextWindow } from "../lib/token-estimate";
|
|
42
|
+
import { inferCursorContextWindow } from "../adapters/cursor/discovery";
|
|
43
|
+
import { KIRO_MODEL_CONTEXT_WINDOWS, normalizeKiroModelId } from "../providers/kiro-models";
|
|
44
|
+
import { modelRecordValue } from "../reasoning-effort";
|
|
41
45
|
|
|
42
46
|
export interface RequestLogContext {
|
|
43
47
|
model: string;
|
|
@@ -839,6 +843,7 @@ export function addFinalRequestLog(
|
|
|
839
843
|
logCtx.providerAdapter ?? logCtx.provider,
|
|
840
844
|
logCtx.usage,
|
|
841
845
|
logCtx.usageLogInputTokens,
|
|
846
|
+
contextWindowForModel(logCtx.providerAdapter ?? logCtx.provider, logCtx.model),
|
|
842
847
|
);
|
|
843
848
|
const attempts = logCtx.attempts?.map(attempt => ({
|
|
844
849
|
...attempt,
|
|
@@ -960,15 +965,40 @@ interface FinalizedUsageResult {
|
|
|
960
965
|
totalTokens?: number;
|
|
961
966
|
}
|
|
962
967
|
|
|
968
|
+
/**
|
|
969
|
+
* Context window for the routed model, used to cap the token estimate (codex-router PR #140):
|
|
970
|
+
* a request the provider answered cannot have exceeded the window, so the estimate must never
|
|
971
|
+
* claim it did. The family is picked by the route ADAPTER, not the model id alone, because
|
|
972
|
+
* claude-family ids are shared between Kiro and Cursor with different windows. Kiro "auto" is
|
|
973
|
+
* a router with no fixed window and is never guessed; unknown adapters/models stay uncapped.
|
|
974
|
+
*/
|
|
975
|
+
function contextWindowForModel(adapter: string, modelId: string | undefined): number | undefined {
|
|
976
|
+
if (!modelId) return undefined;
|
|
977
|
+
if (adapter === "kiro" || adapter.startsWith("kiro-")) {
|
|
978
|
+
const normalized = normalizeKiroModelId(modelId);
|
|
979
|
+
if (normalized === "auto") return undefined;
|
|
980
|
+
return modelRecordValue(KIRO_MODEL_CONTEXT_WINDOWS, modelId)
|
|
981
|
+
?? modelRecordValue(KIRO_MODEL_CONTEXT_WINDOWS, normalized);
|
|
982
|
+
}
|
|
983
|
+
if (adapter === "cursor" || adapter.startsWith("cursor-")) {
|
|
984
|
+
return inferCursorContextWindow(modelId);
|
|
985
|
+
}
|
|
986
|
+
return undefined;
|
|
987
|
+
}
|
|
988
|
+
|
|
963
989
|
function finalizedUsage(
|
|
964
990
|
adapter: string,
|
|
965
991
|
usage: OcxUsage | undefined,
|
|
966
992
|
inputTokenEstimate: number | undefined,
|
|
993
|
+
contextWindow: number | undefined,
|
|
967
994
|
): FinalizedUsageResult {
|
|
995
|
+
// The ESTIMATE itself is capped at the model's context window (codex-router PR #140). The
|
|
996
|
+
// combined value below keeps its max(inputTokens, estimate) behavior — a provider-reported
|
|
997
|
+
// positive count is never reduced by this cap, only the estimate that could substitute it.
|
|
968
998
|
const estimate = typeof inputTokenEstimate === "number"
|
|
969
999
|
&& Number.isFinite(inputTokenEstimate)
|
|
970
1000
|
&& inputTokenEstimate >= 0
|
|
971
|
-
? inputTokenEstimate
|
|
1001
|
+
? capEstimateAtContextWindow(inputTokenEstimate, contextWindow)
|
|
972
1002
|
: undefined;
|
|
973
1003
|
const finalUsage = usageForFinalLog(adapter, usage);
|
|
974
1004
|
const usageFallback = !finalUsage && estimate !== undefined
|
|
@@ -980,7 +1010,16 @@ function finalizedUsage(
|
|
|
980
1010
|
inputTokens: Math.max(finalUsage.inputTokens, estimate),
|
|
981
1011
|
estimated: true,
|
|
982
1012
|
}
|
|
983
|
-
:
|
|
1013
|
+
: finalUsage
|
|
1014
|
+
// When the adapter alone produced an estimated count and no local estimate
|
|
1015
|
+
// exists, cap it at the context window — an adapter estimate above the window
|
|
1016
|
+
// misleads the usage dashboard. The combined branch (above) already caps the
|
|
1017
|
+
// ESTIMATE via capEstimateAtContextWindow, and Math.max preserves a real
|
|
1018
|
+
// provider-reported count, so it needs no further reduction.
|
|
1019
|
+
? (finalUsage.estimated && contextWindow !== undefined && finalUsage.inputTokens > contextWindow
|
|
1020
|
+
? { ...finalUsage, inputTokens: contextWindow }
|
|
1021
|
+
: finalUsage)
|
|
1022
|
+
: usageFallback;
|
|
984
1023
|
const totalTokens = usageTotalTokens(loggedUsage);
|
|
985
1024
|
return {
|
|
986
1025
|
status: usageStatusForFinalLog(loggedUsage),
|
|
@@ -1030,7 +1069,12 @@ export function noteAttemptSend(
|
|
|
1030
1069
|
if (typeof inputTokenEstimate === "number"
|
|
1031
1070
|
&& Number.isFinite(inputTokenEstimate)
|
|
1032
1071
|
&& inputTokenEstimate >= 0) {
|
|
1033
|
-
|
|
1072
|
+
// Store the ESTIMATE field already capped at the model's window (codex-router PR #140):
|
|
1073
|
+
// what gets persisted, and later merged into usage, never claims a count above the window.
|
|
1074
|
+
attempt.inputTokenEstimate = capEstimateAtContextWindow(
|
|
1075
|
+
inputTokenEstimate,
|
|
1076
|
+
contextWindowForModel(attempt.adapter, attempt.model),
|
|
1077
|
+
);
|
|
1034
1078
|
}
|
|
1035
1079
|
if (recovery && !attempt.recoveryKinds.includes(recovery)) {
|
|
1036
1080
|
attempt.recoveryKinds.push(recovery);
|
|
@@ -1047,6 +1091,7 @@ export function finishRequestAttempt(
|
|
|
1047
1091
|
attempt.adapter,
|
|
1048
1092
|
usage ?? attempt.usage,
|
|
1049
1093
|
attempt.inputTokenEstimate,
|
|
1094
|
+
contextWindowForModel(attempt.adapter, attempt.model),
|
|
1050
1095
|
);
|
|
1051
1096
|
attempt.status = status;
|
|
1052
1097
|
attempt.durationMs = Math.max(0, durationMs);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Server } from "bun";
|
|
2
2
|
import { bridgeToResponsesSSE, buildResponseJSON, formatErrorResponse, type ResponsesTerminalStatus } from "../../bridge";
|
|
3
3
|
import { formatPassthroughUpstreamError } from "./passthrough-error";
|
|
4
|
+
import { checkInputAdmission } from "./input-admission";
|
|
4
5
|
import { describeUpstreamConnectFailure } from "./upstream-error";
|
|
5
6
|
import {
|
|
6
7
|
getConfigPath,
|
|
@@ -76,7 +77,7 @@ import {
|
|
|
76
77
|
} from "../../oauth/anthropic-routing";
|
|
77
78
|
import { buildWebSearchTool, planWebSearch, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search";
|
|
78
79
|
import { buildImageTool, buildVideoTool, planImageBridge, planVideoBridge, runWithImageBridge, clampImageMaxRounds, IMAGE_GEN_TOOL_NAME, VIDEO_GEN_TOOL_NAME } from "../../images";
|
|
79
|
-
import { describeImagesInPlace, planVisionSidecar, resolveOpenAiVisionModel, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision";
|
|
80
|
+
import { describeImagesInPlace, isModelTextOnly, planVisionSidecar, resolveOpenAiVisionModel, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision";
|
|
80
81
|
import { createAdapterEventQueue, preflightAdapterEvents } from "../../adapters/run-turn-queue";
|
|
81
82
|
import {
|
|
82
83
|
applyCodexAuthContextToProvider,
|
|
@@ -190,6 +191,7 @@ import {
|
|
|
190
191
|
} from "../responses-terminal-repair";
|
|
191
192
|
import { isWin32EagerRewrite, selectEagerPath } from "../../lib/bun-stream-caps";
|
|
192
193
|
import { cancelBodyOnAbort } from "../../lib/abort";
|
|
194
|
+
import { isCodexWsUpstreamResponse, type BunRuntimeGateInput } from "./ws-upstream";
|
|
193
195
|
import {
|
|
194
196
|
createResponsesItemIdPayloadRewrite,
|
|
195
197
|
hasResponsesItemIdRepair,
|
|
@@ -418,6 +420,7 @@ interface CodexPoolAccountRetryArgs {
|
|
|
418
420
|
// needs the inbound scope or the retry could land on a different wire than the
|
|
419
421
|
// first attempt.
|
|
420
422
|
inboundWire?: InboundWire;
|
|
423
|
+
codexWsRuntimeIdentity?: BunRuntimeGateInput;
|
|
421
424
|
translatorBudget: TranslatorBudget;
|
|
422
425
|
turnAdmissionLease?: AdmissionLease;
|
|
423
426
|
};
|
|
@@ -590,7 +593,7 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
590
593
|
upstream.signal,
|
|
591
594
|
connectMs,
|
|
592
595
|
stream,
|
|
593
|
-
providerFetch(route.provider),
|
|
596
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity),
|
|
594
597
|
// Credential-bearing forward send: never follow a redirect into a
|
|
595
598
|
// dead-host rejection after the credential was seen (#914).
|
|
596
599
|
route.provider.authMode === "forward",
|
|
@@ -719,6 +722,8 @@ export interface ConsumedComboFailure {
|
|
|
719
722
|
|
|
720
723
|
export interface HandleResponsesOptions {
|
|
721
724
|
turnAdmissionLease?: AdmissionLease;
|
|
725
|
+
/** Called at most once after the complete client body is read and accepted for dispatch. */
|
|
726
|
+
onRequestBodyRead?: () => void;
|
|
722
727
|
forceEmptyResponseId?: boolean;
|
|
723
728
|
abortSignal?: AbortSignal;
|
|
724
729
|
/** One-shot TTFT callback: first non-empty model output observed (WP4). */
|
|
@@ -730,6 +735,8 @@ export interface HandleResponsesOptions {
|
|
|
730
735
|
onNativePassthroughCancel?: () => void;
|
|
731
736
|
/** Internal deterministic clock/timer seam for provider terminal repair. */
|
|
732
737
|
responsesTerminalRepairScheduler?: ResponsesTerminalRepairScheduler;
|
|
738
|
+
/** Internal deterministic runtime-identity seam for Codex upstream WS selection tests. */
|
|
739
|
+
codexWsRuntimeIdentity?: BunRuntimeGateInput;
|
|
733
740
|
/**
|
|
734
741
|
* When true, body `prompt_cache_key` is a Claude Desktop shared cache cohort
|
|
735
742
|
* (system/tools hash), not a per-session id — do not use it for Anthropic pool affinity.
|
|
@@ -1495,11 +1502,20 @@ async function handleResponsesInner(
|
|
|
1495
1502
|
try {
|
|
1496
1503
|
body = await readJsonRequestBody(req, translatorBudget);
|
|
1497
1504
|
} catch (err) {
|
|
1505
|
+
if (options.abortSignal?.aborted || req.signal.aborted) {
|
|
1506
|
+
return clientCancelledResponse();
|
|
1507
|
+
}
|
|
1498
1508
|
return decodeRequestErrorResponse(err, "responses");
|
|
1499
1509
|
}
|
|
1500
1510
|
const comboId = !options.comboAttempt ? comboIdFromRawBody(body, config) : null;
|
|
1501
1511
|
if (comboId && Object.hasOwn(config.combos ?? {}, comboId)) {
|
|
1502
|
-
|
|
1512
|
+
options.onRequestBodyRead?.();
|
|
1513
|
+
return handleComboResponses(req, body, comboId, config, logCtx, {
|
|
1514
|
+
...options,
|
|
1515
|
+
// The original request body was accepted above. Combo children are synthetic
|
|
1516
|
+
// replays and must not repeat the caller-owned timeout transition.
|
|
1517
|
+
onRequestBodyRead: undefined,
|
|
1518
|
+
});
|
|
1503
1519
|
}
|
|
1504
1520
|
let unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
|
|
1505
1521
|
(body as { input?: unknown } | undefined)?.input,
|
|
@@ -1555,6 +1571,7 @@ async function handleResponsesInner(
|
|
|
1555
1571
|
}
|
|
1556
1572
|
return formatErrorResponse(400, "invalid_request_error", err instanceof Error ? err.message : String(err));
|
|
1557
1573
|
}
|
|
1574
|
+
options.onRequestBodyRead?.();
|
|
1558
1575
|
const responseStateOptions = (force = false): { force?: boolean; clientThreadId?: string } => ({
|
|
1559
1576
|
...(force ? { force: true } : {}),
|
|
1560
1577
|
...(parsed._clientThreadId ? { clientThreadId: parsed._clientThreadId } : {}),
|
|
@@ -1837,6 +1854,24 @@ async function handleResponsesInner(
|
|
|
1837
1854
|
}
|
|
1838
1855
|
|
|
1839
1856
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
1857
|
+
// Refuse an input that cannot plausibly fit the model context window before spending auth,
|
|
1858
|
+
// circuit budget, or upstream bandwidth on a turn the provider will reject anyway (#1412).
|
|
1859
|
+
//
|
|
1860
|
+
// Compaction turns are exempt: Codex sends compaction_trigger BECAUSE context is full, so
|
|
1861
|
+
// refusing the turn that shrinks the context would deadlock the client against the very
|
|
1862
|
+
// limit this gate reports — it would be told to compact and then denied the compaction.
|
|
1863
|
+
if (parsed._compactionRequest !== true) {
|
|
1864
|
+
const inputAdmission = checkInputAdmission(parsed, route.provider, route.providerName, parsed.modelId);
|
|
1865
|
+
if (!inputAdmission.admitted) {
|
|
1866
|
+
return formatErrorResponse(
|
|
1867
|
+
413,
|
|
1868
|
+
"request_too_large",
|
|
1869
|
+
`Estimated input (~${inputAdmission.estimatedTokens} tokens) is far past the context window `
|
|
1870
|
+
+ `of ${parsed.modelId} (${inputAdmission.ceiling} tokens). Start a new session or choose a `
|
|
1871
|
+
+ `model with a larger context window.`,
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1840
1875
|
const preAuthHostKey = preAuthUpstreamHostCircuitKey(route, config);
|
|
1841
1876
|
if (preAuthHostKey) {
|
|
1842
1877
|
const admission = acquireUpstreamHostAdmission(
|
|
@@ -2060,7 +2095,7 @@ async function handleResponsesInner(
|
|
|
2060
2095
|
recordSidecarOutcome,
|
|
2061
2096
|
translatorBudget,
|
|
2062
2097
|
);
|
|
2063
|
-
} else if (
|
|
2098
|
+
} else if (isModelTextOnly(route.provider, route.modelId)) {
|
|
2064
2099
|
// Sidecar-covered model but NO plan (no forward provider / missing forwarded auth / sidecar
|
|
2065
2100
|
// disabled): fail closed — never forward raw images to a text-only upstream.
|
|
2066
2101
|
stripImagesInPlace(parsed, translatorBudget);
|
|
@@ -2265,7 +2300,8 @@ async function handleResponsesInner(
|
|
|
2265
2300
|
method: request.method,
|
|
2266
2301
|
headers: request.headers,
|
|
2267
2302
|
body: request.body,
|
|
2268
|
-
}, recovery), upstream.signal, connectMs, parsed.stream,
|
|
2303
|
+
}, recovery), upstream.signal, connectMs, parsed.stream,
|
|
2304
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity),
|
|
2269
2305
|
route.provider.authMode === "forward")
|
|
2270
2306
|
// Every real attempt response — including an intermediate 5xx the
|
|
2271
2307
|
// retry wrapper replaces — proves the host was reached (#914 review).
|
|
@@ -2326,7 +2362,8 @@ async function handleResponsesInner(
|
|
|
2326
2362
|
method: request.method,
|
|
2327
2363
|
headers: request.headers,
|
|
2328
2364
|
body: request.body,
|
|
2329
|
-
}, recovery), upstream.signal, connectMs, parsed.stream,
|
|
2365
|
+
}, recovery), upstream.signal, connectMs, parsed.stream,
|
|
2366
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity),
|
|
2330
2367
|
route.provider.authMode === "forward")
|
|
2331
2368
|
.then(res => {
|
|
2332
2369
|
settleObservedHostResponse();
|
|
@@ -2573,9 +2610,14 @@ async function handleResponsesInner(
|
|
|
2573
2610
|
needsClientRewrite,
|
|
2574
2611
|
config.streamMode ?? "auto",
|
|
2575
2612
|
);
|
|
2613
|
+
// A successful Codex WS upgrade is a push source. If it entered tee(),
|
|
2614
|
+
// the inspection branch could drain continuously while the slow client
|
|
2615
|
+
// branch retained bytes without a bound. Force the existing bounded,
|
|
2616
|
+
// single-reader relay before tee; HTTP fallback responses stay unmarked.
|
|
2617
|
+
const forceCodexWsEagerRelay = isCodexWsUpstreamResponse(upstreamResponse);
|
|
2576
2618
|
const inlineEagerRewrite = needsClientRewrite
|
|
2577
|
-
&& (win32EagerRewrite || eagerPath?.useEagerRelay === true);
|
|
2578
|
-
if (eagerPath?.useEagerRelay || win32EagerRewrite) {
|
|
2619
|
+
&& (forceCodexWsEagerRelay || win32EagerRewrite || eagerPath?.useEagerRelay === true);
|
|
2620
|
+
if (forceCodexWsEagerRelay || eagerPath?.useEagerRelay || win32EagerRewrite) {
|
|
2579
2621
|
const turnAc = new AbortController();
|
|
2580
2622
|
linkAbortSignal(upstream, turnAc.signal);
|
|
2581
2623
|
registerTurn(turnAc, options.turnAdmissionLease);
|
|
@@ -2633,9 +2675,9 @@ async function handleResponsesInner(
|
|
|
2633
2675
|
onDone: () => unregisterTurn(turnAc),
|
|
2634
2676
|
}, inlineEagerRewrite ? { rewriteBudget: translatorBudget } : undefined);
|
|
2635
2677
|
// When selected, this relay closes response.completed even if upstream
|
|
2636
|
-
// keeps the connection alive.
|
|
2637
|
-
//
|
|
2638
|
-
// the tee()+JS-pull chain.
|
|
2678
|
+
// keeps the connection alive. Marked Codex WS traffic, Windows
|
|
2679
|
+
// forced-rewrite traffic, and Darwin explicit eager traffic apply
|
|
2680
|
+
// client rewrites inline rather than via the tee()+JS-pull chain.
|
|
2639
2681
|
if (!headers.has("content-type")) headers.set("content-type", "text/event-stream");
|
|
2640
2682
|
return markEagerRelaySseResponse(
|
|
2641
2683
|
markNativePassthroughSseResponse(new Response(eagerBody, {
|
|
@@ -2925,7 +2967,7 @@ async function handleResponsesInner(
|
|
|
2925
2967
|
: clampImageMaxRounds(config.images?.videoMaxRounds ?? 2),
|
|
2926
2968
|
connectTimeoutMs: config.connectTimeoutMs ?? 200_000,
|
|
2927
2969
|
stallTimeoutSec: config.stallTimeoutSec,
|
|
2928
|
-
fetchImpl: providerFetch(route.provider),
|
|
2970
|
+
fetchImpl: providerFetch(route.provider, options.codexWsRuntimeIdentity),
|
|
2929
2971
|
onRequestBuilt: request => recordAdapterReasoning(logCtx, request),
|
|
2930
2972
|
...(vidPlan?.timeoutMs ? { videoTimeoutMs: vidPlan.timeoutMs } : {}),
|
|
2931
2973
|
onUsage: usage => {
|
|
@@ -3249,7 +3291,8 @@ async function handleResponsesInner(
|
|
|
3249
3291
|
method: builtInitialRequest.method,
|
|
3250
3292
|
headers: builtInitialRequest.headers,
|
|
3251
3293
|
body: builtInitialRequest.body,
|
|
3252
|
-
}, recovery), upstream.signal, connectMs, parsed.stream,
|
|
3294
|
+
}, recovery), upstream.signal, connectMs, parsed.stream,
|
|
3295
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity));
|
|
3253
3296
|
},
|
|
3254
3297
|
{ abortSignal: upstream.signal, label: safeHostLabel(builtInitialRequest.url) },
|
|
3255
3298
|
);
|
|
@@ -3328,7 +3371,8 @@ async function handleResponsesInner(
|
|
|
3328
3371
|
? await activeAdapter.fetchResponse(retryRequest, { abortSignal: upstream.signal, timeoutMs: connectMs, stream: parsed.stream })
|
|
3329
3372
|
: await fetchWithHeaderTimeout(retryRequest.url, {
|
|
3330
3373
|
method: retryRequest.method, headers: retryRequest.headers, body: retryRequest.body,
|
|
3331
|
-
}, upstream.signal, connectMs, parsed.stream,
|
|
3374
|
+
}, upstream.signal, connectMs, parsed.stream,
|
|
3375
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity));
|
|
3332
3376
|
} finally {
|
|
3333
3377
|
retryRequest.releaseBodyObservation?.();
|
|
3334
3378
|
}
|
|
@@ -3652,7 +3696,7 @@ async function handleResponsesInner(
|
|
|
3652
3696
|
upstream.signal,
|
|
3653
3697
|
connectMs,
|
|
3654
3698
|
nextParsed.stream,
|
|
3655
|
-
providerFetch(route.provider),
|
|
3699
|
+
providerFetch(route.provider, options.codexWsRuntimeIdentity),
|
|
3656
3700
|
);
|
|
3657
3701
|
},
|
|
3658
3702
|
{ abortSignal: upstream.signal, label: safeHostLabel(builtContinuationRequest.url) },
|
|
@@ -265,47 +265,67 @@ export function hasEncryptedContentPart(content: unknown): boolean {
|
|
|
265
265
|
export function sanitizeEncryptedContentInPlace(input: unknown): number {
|
|
266
266
|
if (!Array.isArray(input)) return 0;
|
|
267
267
|
let rewritten = 0;
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
268
|
+
type VisitFrame =
|
|
269
|
+
| { kind: "visit"; node: unknown }
|
|
270
|
+
| { kind: "array"; node: unknown[]; index: number }
|
|
271
|
+
| { kind: "object"; values: unknown[]; index: number }
|
|
272
|
+
| { kind: "agent"; message: Record<string, unknown>; rewrittenBefore: number };
|
|
273
|
+
const stack: VisitFrame[] = [{ kind: "visit", node: input }];
|
|
274
|
+
|
|
275
|
+
while (stack.length > 0) {
|
|
276
|
+
const frame = stack.pop()!;
|
|
277
|
+
if (frame.kind === "visit") {
|
|
278
|
+
if (Array.isArray(frame.node)) {
|
|
279
|
+
stack.push({ kind: "array", node: frame.node, index: 0 });
|
|
280
|
+
} else if (frame.node && typeof frame.node === "object") {
|
|
281
|
+
stack.push({ kind: "object", values: Object.values(frame.node), index: 0 });
|
|
282
|
+
}
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (frame.kind === "array") {
|
|
287
|
+
if (frame.index >= frame.node.length) continue;
|
|
288
|
+
const child = frame.node[frame.index] as unknown;
|
|
289
|
+
if (
|
|
290
|
+
child && typeof child === "object"
|
|
291
|
+
&& (child as { type?: unknown }).type === "encrypted_content"
|
|
292
|
+
&& typeof (child as { encrypted_content?: unknown }).encrypted_content === "string"
|
|
293
|
+
) {
|
|
294
|
+
const payload = (child as { encrypted_content: string }).encrypted_content;
|
|
295
|
+
if (!looksLikeBackendCiphertext(payload)) {
|
|
296
|
+
const parts = encryptedSlotParts(payload);
|
|
297
|
+
frame.node.splice(frame.index, 1, ...parts);
|
|
298
|
+
rewritten += 1;
|
|
299
|
+
stack.push({ kind: "array", node: frame.node, index: frame.index + parts.length });
|
|
300
|
+
continue;
|
|
300
301
|
}
|
|
301
302
|
}
|
|
302
|
-
|
|
303
|
+
stack.push({ kind: "array", node: frame.node, index: frame.index + 1 });
|
|
304
|
+
if (child && typeof child === "object" && (child as { type?: unknown }).type === "agent_message") {
|
|
305
|
+
stack.push({ kind: "agent", message: child as Record<string, unknown>, rewrittenBefore: rewritten });
|
|
306
|
+
}
|
|
307
|
+
stack.push({ kind: "visit", node: child });
|
|
308
|
+
continue;
|
|
303
309
|
}
|
|
304
|
-
|
|
305
|
-
|
|
310
|
+
|
|
311
|
+
if (frame.kind === "object") {
|
|
312
|
+
if (frame.index >= frame.values.length) continue;
|
|
313
|
+
stack.push({ kind: "object", values: frame.values, index: frame.index + 1 });
|
|
314
|
+
stack.push({ kind: "visit", node: frame.values[frame.index] });
|
|
315
|
+
continue;
|
|
306
316
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
317
|
+
|
|
318
|
+
if (
|
|
319
|
+
rewritten > frame.rewrittenBefore
|
|
320
|
+
&& frame.message.type === "agent_message"
|
|
321
|
+
&& !hasEncryptedContentPart(frame.message.content)
|
|
322
|
+
) {
|
|
323
|
+
frame.message.type = "message";
|
|
324
|
+
frame.message.role = "user";
|
|
325
|
+
delete frame.message.id;
|
|
326
|
+
delete frame.message.author;
|
|
327
|
+
delete frame.message.recipient;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
310
330
|
return rewritten;
|
|
311
331
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { Server } from "bun";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
codexWsUpstreamFetch,
|
|
4
|
+
currentBunRuntimeIdentity,
|
|
5
|
+
shouldUseCodexWsUpstream,
|
|
6
|
+
type BunRuntimeGateInput,
|
|
7
|
+
} from "./ws-upstream";
|
|
3
8
|
import { bridgeToResponsesSSE, buildResponseJSON, formatErrorResponse, type ResponsesTerminalStatus } from "../../bridge";
|
|
4
9
|
import {
|
|
5
10
|
getConfigPath,
|
|
@@ -131,14 +136,17 @@ export function safeOriginLabel(url: string): string {
|
|
|
131
136
|
|
|
132
137
|
|
|
133
138
|
|
|
134
|
-
export function providerFetch(
|
|
139
|
+
export function providerFetch(
|
|
140
|
+
provider: OcxProviderConfig,
|
|
141
|
+
runtime: BunRuntimeGateInput = currentBunRuntimeIdentity(),
|
|
142
|
+
): typeof globalThis.fetch {
|
|
135
143
|
const base = (provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? globalThis.fetch;
|
|
136
144
|
// ChatGPT Codex backend: streaming turns ride the responses_websockets
|
|
137
145
|
// transport (measured ~3s faster TTFT than the SSE POST queue); everything
|
|
138
146
|
// else keeps the provider's HTTP fetch. See ws-upstream.ts for the details.
|
|
139
147
|
const wrapped = (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) => {
|
|
140
|
-
if (typeof input === "string" && init && shouldUseCodexWsUpstream(input, init)) {
|
|
141
|
-
return codexWsUpstreamFetch(input, init, base);
|
|
148
|
+
if (typeof input === "string" && init && shouldUseCodexWsUpstream(input, init, runtime)) {
|
|
149
|
+
return codexWsUpstreamFetch(input, init, base, runtime);
|
|
142
150
|
}
|
|
143
151
|
return base(input, init);
|
|
144
152
|
};
|