@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.
Files changed (50) hide show
  1. package/gui/dist/assets/{index-DUCH59lJ.css → index-CQ7bIKee.css} +1 -1
  2. package/gui/dist/assets/{index-ta3-_hgj.js → index-D_JUZLEC.js} +16 -16
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +1 -1
  5. package/src/adapters/client-fingerprint.ts +14 -10
  6. package/src/adapters/cursor/live-transport.ts +17 -5
  7. package/src/adapters/cursor/protobuf-events.ts +662 -20
  8. package/src/adapters/cursor/tool-definitions.ts +12 -6
  9. package/src/adapters/google-antigravity-wire.ts +4 -3
  10. package/src/adapters/google.ts +22 -3
  11. package/src/bridge.ts +12 -2
  12. package/src/chat/inbound.ts +24 -1
  13. package/src/cli/index.ts +11 -0
  14. package/src/codex/app-server-processes.ts +3 -3
  15. package/src/codex/shim.ts +100 -5
  16. package/src/codex/user-identity.ts +36 -6
  17. package/src/config.ts +0 -2
  18. package/src/generated/compatibility-version.json +52 -44
  19. package/src/lib/errors.ts +27 -0
  20. package/src/lib/token-estimate.ts +19 -2
  21. package/src/lib/windows-elevation.ts +37 -0
  22. package/src/lib/windows-secret-acl.ts +7 -0
  23. package/src/lib/windows-text.ts +106 -0
  24. package/src/lib/windows-user-principal.ts +0 -2
  25. package/src/oauth/index.ts +1 -1
  26. package/src/oauth/store.ts +32 -18
  27. package/src/providers/antigravity-models.ts +25 -5
  28. package/src/providers/free-directory.ts +1 -1
  29. package/src/providers/registry.ts +6 -3
  30. package/src/responses/spill-store.ts +20 -1
  31. package/src/responses/state.ts +159 -3
  32. package/src/server/chat-completions.ts +4 -2
  33. package/src/server/effort-policy.ts +18 -0
  34. package/src/server/index.ts +5 -1
  35. package/src/server/management/logs-usage-routes.ts +7 -22
  36. package/src/server/request-log.ts +48 -3
  37. package/src/server/responses/core.ts +59 -15
  38. package/src/server/responses/encrypted-payload.ts +58 -38
  39. package/src/server/responses/fetch-helpers.ts +12 -4
  40. package/src/server/responses/input-admission.ts +169 -0
  41. package/src/server/responses/policy-fallback.ts +13 -2
  42. package/src/server/responses/ws-upstream.ts +115 -6
  43. package/src/service-manager-probe.ts +23 -37
  44. package/src/service.ts +233 -25
  45. package/src/tray/windows.ts +0 -2
  46. package/src/types.ts +10 -2
  47. package/src/update/job.ts +2 -2
  48. package/src/usage/summary.ts +21 -4
  49. package/src/vision/index.ts +21 -4
  50. 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
- entries: PersistedUsageEntry[],
106
- range: UsageRange,
107
- surface: UsageSurface,
97
+ _entries: PersistedUsageEntry[],
98
+ _range: UsageRange,
99
+ _surface: UsageSurface,
108
100
  now: number,
109
101
  ): number {
110
- let expiresAt = nextLocalMidnight(now);
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 === "7d" ? now - 7 * USAGE_DAY_MS : range === "30d" ? now - 30 * USAGE_DAY_MS : null;
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
- : (finalUsage ?? usageFallback);
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
- attempt.inputTokenEstimate = inputTokenEstimate;
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
- return handleComboResponses(req, body, comboId, config, logCtx, options);
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 (modelInList(route.provider.noVisionModels, route.modelId)) {
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, providerFetch(route.provider),
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, providerFetch(route.provider),
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. Windows forced-rewrite traffic and Darwin
2637
- // explicit eager traffic apply client rewrites inline rather than via
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, providerFetch(route.provider));
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, providerFetch(route.provider));
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
- const visit = (node: unknown): number => {
269
- const before = rewritten;
270
- if (Array.isArray(node)) {
271
- for (let i = 0; i < node.length; i += 1) {
272
- const child = node[i] as unknown;
273
- if (
274
- child && typeof child === "object"
275
- && (child as { type?: unknown }).type === "encrypted_content"
276
- && typeof (child as { encrypted_content?: unknown }).encrypted_content === "string"
277
- ) {
278
- const payload = (child as { encrypted_content: string }).encrypted_content;
279
- if (!looksLikeBackendCiphertext(payload)) {
280
- const parts = encryptedSlotParts(payload);
281
- node.splice(i, 1, ...parts);
282
- i += parts.length - 1;
283
- rewritten += 1;
284
- continue;
285
- }
286
- }
287
- const childRewrites = visit(child);
288
- if (
289
- childRewrites > 0
290
- && child && typeof child === "object"
291
- && (child as { type?: unknown }).type === "agent_message"
292
- && !hasEncryptedContentPart((child as { content?: unknown }).content)
293
- ) {
294
- const message = child as { type: string; role?: string; id?: unknown; author?: unknown; recipient?: unknown };
295
- message.type = "message";
296
- message.role = "user";
297
- delete message.id;
298
- delete message.author;
299
- delete message.recipient;
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
- return rewritten - before;
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
- if (node && typeof node === "object") {
305
- for (const value of Object.values(node)) visit(value);
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
- return rewritten - before;
308
- };
309
- visit(input);
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 { codexWsUpstreamFetch, shouldUseCodexWsUpstream } from "./ws-upstream";
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(provider: OcxProviderConfig): typeof globalThis.fetch {
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
  };