@linxiraos/pi-ai 1.1.26 → 1.2.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 (237) hide show
  1. package/CHANGELOG.md +9 -8
  2. package/README.md +15 -0
  3. package/dist/types/auth/affinity.d.ts +33 -6
  4. package/dist/types/auth/cascade.d.ts +44 -7
  5. package/dist/types/auth/oauth.d.ts +9 -6
  6. package/dist/types/auth/policy.d.ts +1 -0
  7. package/dist/types/auth/refresh.d.ts +1 -2
  8. package/dist/types/auth/select.d.ts +6 -4
  9. package/dist/types/auth/sqlite-credential-store.d.ts +21 -1
  10. package/dist/types/auth/store.d.ts +8 -2
  11. package/dist/types/auth/types.d.ts +99 -15
  12. package/dist/types/auth-broker/client.d.ts +3 -3
  13. package/dist/types/auth-broker/discover.d.ts +1 -1
  14. package/dist/types/auth-broker/refresher.d.ts +6 -1
  15. package/dist/types/auth-broker/remote-store.d.ts +6 -4
  16. package/dist/types/auth-broker/server.d.ts +2 -0
  17. package/dist/types/auth-broker/snapshot-cache.d.ts +12 -0
  18. package/dist/types/auth-gateway/dispatch.d.ts +13 -5
  19. package/dist/types/auth-gateway/http.d.ts +0 -2
  20. package/dist/types/auth-gateway/index.d.ts +1 -0
  21. package/dist/types/auth-gateway/routes/embeddings.d.ts +2 -2
  22. package/dist/types/auth-gateway/routes/images.d.ts +3 -3
  23. package/dist/types/auth-gateway/routes/rerank.d.ts +2 -2
  24. package/dist/types/auth-gateway/routes/speech.d.ts +2 -2
  25. package/dist/types/auth-gateway/routes/systemone.d.ts +2 -2
  26. package/dist/types/auth-gateway/routes/transcriptions.d.ts +2 -2
  27. package/dist/types/auth-gateway/routes/video.d.ts +4 -4
  28. package/dist/types/auth-gateway/server.d.ts +22 -5
  29. package/dist/types/auth-gateway/session-state.d.ts +6 -0
  30. package/dist/types/auth-gateway/stdio.d.ts +12 -0
  31. package/dist/types/auth-storage.d.ts +7 -6
  32. package/dist/types/dialect/terminator-wait.d.ts +19 -0
  33. package/dist/types/env-api-key.d.ts +21 -0
  34. package/dist/types/error/auth.d.ts +10 -0
  35. package/dist/types/error/flags.d.ts +38 -0
  36. package/dist/types/images/index.d.ts +2 -3
  37. package/dist/types/images/shared.d.ts +6 -0
  38. package/dist/types/index.d.ts +2 -1
  39. package/dist/types/judgment/index.d.ts +2 -1
  40. package/dist/types/judgment/native.d.ts +80 -0
  41. package/dist/types/judgment/openai-decisions.d.ts +3 -0
  42. package/dist/types/judgment/question-file.d.ts +52 -0
  43. package/dist/types/judgment/systemone.d.ts +7 -0
  44. package/dist/types/judgment/types.d.ts +2 -2
  45. package/dist/types/judgment/wire.d.ts +36 -0
  46. package/dist/types/provider-details.d.ts +1 -1
  47. package/dist/types/provider-session-state.d.ts +2 -2
  48. package/dist/types/providers/anthropic-client.d.ts +10 -0
  49. package/dist/types/providers/anthropic-state.d.ts +2 -0
  50. package/dist/types/providers/anthropic.d.ts +38 -3
  51. package/dist/types/providers/aws-credentials.d.ts +4 -8
  52. package/dist/types/providers/connect-error-detail.d.ts +9 -0
  53. package/dist/types/providers/connect-frame.d.ts +44 -0
  54. package/dist/types/providers/cursor/interaction-query.d.ts +0 -2
  55. package/dist/types/providers/cursor.d.ts +8 -8
  56. package/dist/types/providers/google-auth.d.ts +4 -1
  57. package/dist/types/providers/google-shared.d.ts +24 -4
  58. package/dist/types/providers/inference-headers.d.ts +6 -0
  59. package/dist/types/providers/mock.d.ts +3 -1
  60. package/dist/types/providers/openai-codex/request-transformer.d.ts +2 -0
  61. package/dist/types/providers/openai-codex-responses.d.ts +1 -1
  62. package/dist/types/providers/openai-configuration-update.d.ts +20 -11
  63. package/dist/types/providers/openai-responses-wire.d.ts +1 -55
  64. package/dist/types/providers/openai-responses.d.ts +40 -3
  65. package/dist/types/providers/openai-shared.d.ts +30 -5
  66. package/dist/types/providers/pi-native-client.d.ts +1 -1
  67. package/dist/types/providers/transform-messages.d.ts +25 -0
  68. package/dist/types/providers/vision-guard.d.ts +1 -1
  69. package/dist/types/registry/oauth/cloudflare-workers-ai.d.ts +3 -0
  70. package/dist/types/registry/oauth/cursor.d.ts +10 -2
  71. package/dist/types/registry/oauth/index.d.ts +2 -0
  72. package/dist/types/registry/oauth/snowflake.d.ts +3 -0
  73. package/dist/types/registry/snowflake.d.ts +4 -0
  74. package/dist/types/stream.d.ts +0 -21
  75. package/dist/types/transcription/types.d.ts +2 -1
  76. package/dist/types/types.d.ts +116 -14
  77. package/dist/types/usage.d.ts +2 -0
  78. package/dist/types/utils/anthropic-auth.d.ts +21 -7
  79. package/dist/types/utils/aws-profile.d.ts +5 -0
  80. package/dist/types/utils/block-symbols.d.ts +14 -0
  81. package/dist/types/utils/dsml-leak.d.ts +29 -0
  82. package/dist/types/utils/glyph-codec.d.ts +1 -1
  83. package/dist/types/utils/idle-iterator.d.ts +11 -0
  84. package/dist/types/utils/leaked-thinking-stream.d.ts +1 -1
  85. package/dist/types/utils/resolve-peer.d.ts +2 -0
  86. package/dist/types/utils/retry-wait.d.ts +9 -0
  87. package/dist/types/utils/schema/normalize.d.ts +8 -3
  88. package/dist/types/utils/stream-markup-healing.d.ts +4 -4
  89. package/dist/types/utils/tool-call-arguments.d.ts +8 -0
  90. package/dist/types/utils.d.ts +6 -0
  91. package/package.json +6 -6
  92. package/src/auth/affinity.ts +130 -37
  93. package/src/auth/blocks.ts +94 -15
  94. package/src/auth/cascade.ts +112 -17
  95. package/src/auth/health.ts +3 -2
  96. package/src/auth/oauth.ts +25 -10
  97. package/src/auth/policy.ts +10 -1
  98. package/src/auth/pool.ts +0 -5
  99. package/src/auth/refresh.ts +17 -12
  100. package/src/auth/resets.ts +1 -1
  101. package/src/auth/rotation.ts +8 -2
  102. package/src/auth/select.ts +102 -30
  103. package/src/auth/sqlite-credential-store.ts +119 -49
  104. package/src/auth/store.ts +8 -2
  105. package/src/auth/types.ts +108 -17
  106. package/src/auth/usage-cache.ts +16 -5
  107. package/src/auth/usage.ts +2 -2
  108. package/src/auth-broker/client.ts +7 -4
  109. package/src/auth-broker/discover.ts +6 -5
  110. package/src/auth-broker/refresher.ts +8 -1
  111. package/src/auth-broker/remote-store.ts +200 -62
  112. package/src/auth-broker/server.ts +316 -130
  113. package/src/auth-broker/snapshot-cache.ts +142 -2
  114. package/src/auth-gateway/dispatch.ts +15 -7
  115. package/src/auth-gateway/http.ts +0 -8
  116. package/src/auth-gateway/index.ts +1 -0
  117. package/src/auth-gateway/routes/embeddings.ts +2 -2
  118. package/src/auth-gateway/routes/images.ts +4 -4
  119. package/src/auth-gateway/routes/rerank.ts +2 -2
  120. package/src/auth-gateway/routes/speech.ts +2 -2
  121. package/src/auth-gateway/routes/systemone.ts +22 -14
  122. package/src/auth-gateway/routes/transcriptions.ts +2 -2
  123. package/src/auth-gateway/routes/video.ts +7 -7
  124. package/src/auth-gateway/server.ts +175 -134
  125. package/src/auth-gateway/session-state.ts +51 -9
  126. package/src/auth-gateway/stdio.ts +132 -0
  127. package/src/auth-storage.ts +23 -9
  128. package/src/dialect/deepseek.ts +67 -8
  129. package/src/dialect/gemini.ts +8 -2
  130. package/src/dialect/hermes.ts +8 -2
  131. package/src/dialect/kimi.ts +8 -2
  132. package/src/dialect/qwen-xml.ts +8 -2
  133. package/src/dialect/qwen3.ts +8 -2
  134. package/src/dialect/terminator-wait.ts +42 -0
  135. package/src/env-api-key.ts +78 -0
  136. package/src/error/auth.ts +20 -0
  137. package/src/error/flags.ts +143 -24
  138. package/src/error/rate-limit.ts +8 -6
  139. package/src/images/google-antigravity.ts +0 -1
  140. package/src/images/index.ts +3 -17
  141. package/src/images/openai-hosted.ts +13 -4
  142. package/src/images/openai-images.ts +19 -13
  143. package/src/images/shared.ts +10 -2
  144. package/src/index.ts +2 -1
  145. package/src/judgment/index.ts +2 -1
  146. package/src/judgment/native.ts +333 -0
  147. package/src/judgment/openai-decisions-predicate.md +9 -0
  148. package/src/judgment/openai-decisions.ts +129 -0
  149. package/src/judgment/question-file.ts +90 -0
  150. package/src/judgment/systemone.ts +65 -0
  151. package/src/judgment/text.ts +9 -1
  152. package/src/judgment/types.ts +2 -2
  153. package/src/judgment/wire.ts +42 -0
  154. package/src/provider-details.ts +1 -1
  155. package/src/provider-session-state.ts +2 -2
  156. package/src/providers/amazon-bedrock.ts +19 -8
  157. package/src/providers/anthropic-client.ts +16 -1
  158. package/src/providers/anthropic-state.ts +6 -0
  159. package/src/providers/anthropic.ts +962 -238
  160. package/src/providers/apple-foundation-models.ts +10 -4
  161. package/src/providers/aws-credentials.ts +65 -5
  162. package/src/providers/azure-openai-responses.ts +1 -1
  163. package/src/providers/connect-error-detail.ts +21 -0
  164. package/src/providers/connect-frame.ts +107 -0
  165. package/src/providers/cursor/interaction-query.ts +1 -9
  166. package/src/providers/cursor.ts +590 -133
  167. package/src/providers/devin.ts +68 -58
  168. package/src/providers/factory-droid/gemini.ts +38 -51
  169. package/src/providers/factory-droid.ts +2 -0
  170. package/src/providers/gitlab-duo-workflow.ts +5 -2
  171. package/src/providers/gitlab-duo.ts +1 -0
  172. package/src/providers/google-auth.ts +10 -3
  173. package/src/providers/google-gemini-cli.ts +4 -0
  174. package/src/providers/google-shared.ts +134 -105
  175. package/src/providers/google-vertex.ts +0 -1
  176. package/src/providers/google.ts +1 -1
  177. package/src/providers/inference-headers.ts +19 -0
  178. package/src/providers/mock.ts +4 -0
  179. package/src/providers/ollama.ts +1 -1
  180. package/src/providers/openai-anthropic-shim.ts +1 -0
  181. package/src/providers/openai-codex/request-transformer.ts +16 -11
  182. package/src/providers/openai-codex-responses.ts +201 -76
  183. package/src/providers/openai-completions.ts +12 -2
  184. package/src/providers/openai-configuration-update.ts +54 -15
  185. package/src/providers/openai-responses-wire.ts +1 -55
  186. package/src/providers/openai-responses.ts +437 -33
  187. package/src/providers/openai-shared.ts +267 -72
  188. package/src/providers/pi-native-client.ts +2 -2
  189. package/src/providers/pi-native-server.ts +1 -0
  190. package/src/providers/systemone-server.ts +1 -1
  191. package/src/providers/transcriptions-server.ts +1 -1
  192. package/src/providers/transform-messages.ts +240 -84
  193. package/src/providers/vision-guard.ts +1 -1
  194. package/src/registry/aws.ts +51 -14
  195. package/src/registry/cloudflare-ai-gateway.ts +26 -3
  196. package/src/registry/hooks/api-key.ts +1 -0
  197. package/src/registry/hooks/custom.ts +2 -0
  198. package/src/registry/hooks/env.ts +2 -5
  199. package/src/registry/oauth/callback-server.ts +6 -1
  200. package/src/registry/oauth/cloudflare-ai-gateway.ts +2 -2
  201. package/src/registry/oauth/cloudflare-workers-ai.ts +32 -0
  202. package/src/registry/oauth/cursor.ts +80 -17
  203. package/src/registry/oauth/index.ts +5 -0
  204. package/src/registry/oauth/snowflake.ts +183 -0
  205. package/src/registry/registry.ts +2 -0
  206. package/src/registry/snowflake.ts +149 -0
  207. package/src/stream.ts +85 -107
  208. package/src/transcription/types.ts +2 -1
  209. package/src/types.ts +142 -15
  210. package/src/usage/claude.ts +19 -12
  211. package/src/usage/commandcode.ts +1 -1
  212. package/src/usage/cursor.ts +14 -25
  213. package/src/usage/github-copilot.ts +1 -1
  214. package/src/usage/kimi.ts +1 -1
  215. package/src/usage/minimax-code.ts +8 -1
  216. package/src/usage.ts +8 -0
  217. package/src/utils/anthropic-auth.ts +34 -10
  218. package/src/utils/aws-profile.ts +10 -4
  219. package/src/utils/block-symbols.ts +17 -0
  220. package/src/utils/dsml-leak.ts +109 -0
  221. package/src/utils/empty-completion-retry.ts +2 -3
  222. package/src/utils/glyph-codec.ts +200 -7
  223. package/src/utils/idle-iterator.ts +80 -35
  224. package/src/utils/leaked-thinking-stream.ts +47 -25
  225. package/src/utils/resolve-peer.ts +7 -0
  226. package/src/utils/retry-wait.ts +19 -0
  227. package/src/utils/schema/CONSTRAINTS.md +9 -7
  228. package/src/utils/schema/fields.ts +20 -0
  229. package/src/utils/schema/normalize.ts +65 -23
  230. package/src/utils/schema/wire.ts +3 -3
  231. package/src/utils/stream-markup-healing.ts +4 -4
  232. package/src/utils/thinking-loop.ts +36 -13
  233. package/src/utils/tool-call-arguments.ts +16 -0
  234. package/src/utils/transport-fetch.ts +1 -1
  235. package/src/utils.ts +41 -10
  236. package/dist/types/judgment/typesafe.d.ts +0 -49
  237. package/src/judgment/typesafe.ts +0 -183
@@ -14,6 +14,7 @@ import {
14
14
  $env,
15
15
  $flag,
16
16
  asRecord,
17
+ cloneJsonTree,
17
18
  fetchWithRetry,
18
19
  getInstallId,
19
20
  logger,
@@ -22,8 +23,9 @@ import {
22
23
  USER_AGENT,
23
24
  } from "@linxiraos/pi-utils";
24
25
  import * as AIError from "../error";
25
- import { parseToolCallArguments } from "../utils/tool-call-arguments";
26
- import { getEnvApiKey, isOfficialCodexApiUrl } from "../stream";
26
+ import { parseToolCallArguments, replayableToolCallArguments } from "../utils/tool-call-arguments";
27
+ import { getEnvApiKey } from "../env-api-key";
28
+ import { isOfficialCodexApiUrl } from "../stream";
27
29
  import type {
28
30
  Api,
29
31
  AssistantMessage,
@@ -45,7 +47,9 @@ import type {
45
47
  ToolResultMessage,
46
48
  Usage,
47
49
  } from "../types";
50
+ import { getPremiumServiceTierRequests, parseServiceTier } from "../types";
48
51
  import {
52
+ clampOpenAIResponsesImageDetailForReplay,
49
53
  createOpenAIResponsesHistoryPayload,
50
54
  dropMalformedOpenAIResponsesToolCalls,
51
55
  getOpenAIResponsesHistoryItems,
@@ -77,6 +81,7 @@ import {
77
81
  type CodexRequestOptions,
78
82
  type InputItem,
79
83
  type ReasoningConfig,
84
+ lowestCodexWireEffort,
80
85
  type RequestBody,
81
86
  resolveCodexResponsesLite,
82
87
  sanitizeCodexCallId,
@@ -97,9 +102,10 @@ import { getOpenAICodexWebSocketEnvValue, isOpenAICodexWebSocketPreferred } from
97
102
  export { setCodexAttestationProvider } from "./openai-codex-attestation";
98
103
  export type { CodexAttestationProvider } from "./openai-codex-attestation";
99
104
  import {
100
- getOpenAIEffortControlState,
101
105
  type OpenAIEffortControlState,
102
106
  planStableOpenAIEffort,
107
+ releaseOpenAIEffortControlSession,
108
+ resolveOpenAIEffortControlState,
103
109
  } from "./openai-configuration-update";
104
110
  import type {
105
111
  ResponseComputerToolCall,
@@ -144,6 +150,7 @@ import {
144
150
  normalizeOpenAIPromptCacheKey,
145
151
  populateResponsesUsageFromResponse,
146
152
  promoteResponsesToolUseStopReason,
153
+ settleReasoningSummary,
147
154
  type SequentialCutoffSummaryState,
148
155
  } from "./openai-shared";
149
156
  import { redactSensitiveInObject, transformMessages } from "./transform-messages";
@@ -273,7 +280,14 @@ const CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS = Number($env.PI_CODEX_WEBSOCKET_FI
273
280
  const CODEX_WEBSOCKET_RETRY_BUDGET = Number($env.PI_CODEX_WEBSOCKET_RETRY_BUDGET || CODEX_MAX_RETRIES);
274
281
  const CODEX_WEBSOCKET_RETRY_DELAY_MS = Number($env.PI_CODEX_WEBSOCKET_RETRY_DELAY_MS || CODEX_RETRY_DELAY_MS);
275
282
  const CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX = "Codex websocket transport error";
276
- const CODEX_RETRYABLE_EVENT_CODES = new Set(["model_error", "server_error", "internal_error"]);
283
+ // The native-lane steering rejection is replayable: the session stops steering
284
+ // first, so the replay cannot trip it again.
285
+ const CODEX_RETRYABLE_EVENT_CODES = new Set([
286
+ "model_error",
287
+ "server_error",
288
+ "internal_error",
289
+ AIError.CODEX_NATIVE_LANE_STEER_REJECTED_CODE,
290
+ ]);
277
291
  const CODEX_RETRYABLE_EVENT_MESSAGE =
278
292
  /processing your request|retry your request|temporar(?:y|ily)|overloaded|service.?unavailable|internal error|server error/i;
279
293
  const CODEX_PROVIDER_SESSION_STATE_KEY = "openai-codex-responses";
@@ -423,6 +437,7 @@ export interface OpenAICodexWebSocketDebugStats {
423
437
  * is historical — SSE-only sessions use it too.
424
438
  */
425
439
  type CodexWebSocketSessionState = {
440
+ sessionId: string;
426
441
  disableWebsocket: boolean;
427
442
  lastRequest?: RequestBody;
428
443
  /** Last completed response; an in-progress response cannot replace the retry baseline. */
@@ -437,6 +452,8 @@ type CodexWebSocketSessionState = {
437
452
  * pending tool output; see {@link planSteeredRequest}.
438
453
  */
439
454
  acceptedSteering?: { connection: CodexWebSocketConnection; steers: CodexAcceptedSteer[] };
455
+ /** The server refused steering for this session (native turn lane); later responses are not steered. */
456
+ steeringUnsupported?: boolean;
440
457
  lastTransport?: CodexTransport;
441
458
  fallbackCount: number;
442
459
  lastFallbackAt?: number;
@@ -473,6 +490,12 @@ interface CodexMetadataSessionState {
473
490
  interface CodexCompatibilityIdentity {
474
491
  installationId: string;
475
492
  sessionId: string;
493
+ /**
494
+ * Session the backend derives prompt-cache affinity from, sent in every
495
+ * session header: the parent conversation's for a side request
496
+ * (`parentSessionId`), else `sessionId`. Turn metadata keeps `sessionId`.
497
+ */
498
+ affinitySessionId?: string;
476
499
  threadId: string;
477
500
  windowId: string;
478
501
  /** Header projection: identity fields only, never the Code Mode snapshot. */
@@ -611,6 +634,7 @@ function createCodexRequestMetadata(
611
634
  parentTurnId?: string;
612
635
  compaction?: CodexCompactionRequestContext;
613
636
  toolNamespacesInfo?: unknown;
637
+ affinitySessionId?: string;
614
638
  },
615
639
  ): CodexRequestMetadata {
616
640
  if (options.startNewTurn || !session.turnId) {
@@ -674,6 +698,7 @@ function createCodexRequestMetadata(
674
698
  clientMetadata[OPENAI_HEADERS.TURN_METADATA] = turnMetadataJson;
675
699
  return {
676
700
  ...identity,
701
+ affinitySessionId: options.affinitySessionId,
677
702
  turnId: session.turnId,
678
703
  turnMetadataJson,
679
704
  turnMetadataHeaderJson,
@@ -682,7 +707,7 @@ function createCodexRequestMetadata(
682
707
  }
683
708
 
684
709
  function applyCodexCompatibilityHeaders(headers: Headers, metadata: CodexCompatibilityIdentity): void {
685
- headers.set(OPENAI_HEADERS.SCOPED_SESSION_ID, metadata.sessionId);
710
+ headers.set(OPENAI_HEADERS.SCOPED_SESSION_ID, metadata.affinitySessionId ?? metadata.sessionId);
686
711
  headers.set(OPENAI_HEADERS.THREAD_ID, metadata.threadId);
687
712
  headers.set(OPENAI_HEADERS.WINDOW_ID, metadata.windowId);
688
713
  if (metadata.turnMetadataHeaderJson) {
@@ -1045,6 +1070,20 @@ function createCodexProviderSessionState(): CodexProviderSessionState {
1045
1070
  webSocketPublicToPrivate: new Map(),
1046
1071
  metadataSessions: new Map(),
1047
1072
  effortControls: new Map(),
1073
+ releaseSession: sessionId => {
1074
+ const normalizedSessionId = normalizeOpenAIPromptCacheKey(sessionId);
1075
+ if (!normalizedSessionId) return;
1076
+ for (const [key, session] of state.webSocketSessions) {
1077
+ if (session.sessionId !== normalizedSessionId) continue;
1078
+ session.connection?.close("session_disposed");
1079
+ state.webSocketSessions.delete(key);
1080
+ for (const [publicKey, privateKey] of state.webSocketPublicToPrivate) {
1081
+ if (privateKey === key) state.webSocketPublicToPrivate.delete(publicKey);
1082
+ }
1083
+ }
1084
+ state.metadataSessions.delete(normalizedSessionId);
1085
+ releaseOpenAIEffortControlSession(state.effortControls, normalizedSessionId);
1086
+ },
1048
1087
  close: () => {
1049
1088
  for (const session of state.webSocketSessions.values()) {
1050
1089
  session.connection?.close("session_disposed");
@@ -1326,36 +1365,46 @@ function getCodexServiceTierCostMultiplier(
1326
1365
  model: Pick<Model<"openai-codex-responses">, "serviceTierCost">,
1327
1366
  serviceTier: ServiceTier | "default" | undefined,
1328
1367
  ): number {
1329
- // `ultrafast` has no published price (API preview, Codex credits), so it is
1330
- // shown at 1x rather than an invented multiplier.
1331
- if (serviceTier !== "flex" && serviceTier !== "priority") return 1;
1368
+ // The subscription route draws on included plan usage, so its cost is the
1369
+ // usage-equivalent: each tier's multiplier comes from the catalog table, which
1370
+ // uses OpenAI's included-usage rates (Astra: Fast 2.5x, Ultrafast 8x). A tier
1371
+ // with no entry stays at 1x rather than an invented multiplier.
1372
+ if (serviceTier !== "flex" && serviceTier !== "priority" && serviceTier !== "ultrafast") return 1;
1332
1373
  return model.serviceTierCost?.[serviceTier] ?? 1;
1333
1374
  }
1334
1375
 
1335
1376
  /**
1336
- * The tier a Codex response was billed at. The response echo is authoritative
1337
- * whenever it reports a tier (the backend may serve a requested priority/flex
1338
- * turn as `default`); the requested tier is used only when the echo is absent.
1377
+ * The tier a Codex turn was served at. The Codex backend echoes
1378
+ * `service_tier: "default"` on turns it serves at the sent tier (a `priority`
1379
+ * turn decodes at Fast speed yet reports `default`), so a `default` or `auto`
1380
+ * echo carries no information and, like an absent one, resolves to the sent
1381
+ * tier, or `default` when none was sent. Any other echo (`flex`, `priority`,
1382
+ * `ultrafast`, `scale`) is authoritative. The tier's identity is preserved even
1383
+ * when it has no pricing entry (`scale`), because the recorded tier also drives
1384
+ * premium-request and speed accounting. The echo cannot reveal a genuine
1385
+ * downgrade, so a downgraded turn is recorded at the sent tier.
1339
1386
  */
1340
- function resolveCodexCostServiceTier(res: ServiceTier | undefined, req?: unknown): ServiceTier | "default" | undefined {
1341
- const served = res ?? req;
1342
- return served === "flex" || served === "priority" ? served : "default";
1387
+ function resolveCodexCostServiceTier(res: ServiceTier | undefined, req?: unknown): ServiceTier {
1388
+ if (res !== undefined && res !== "default" && res !== "auto") return res;
1389
+ return parseServiceTier(req) ?? "default";
1343
1390
  }
1344
1391
 
1345
1392
  function applyCodexServiceTierPricing(
1346
- model: Pick<Model<"openai-codex-responses">, "serviceTierCost">,
1393
+ model: Pick<Model<"openai-codex-responses">, "provider" | "api" | "identity" | "serviceTierCost">,
1347
1394
  usage: AssistantMessage["usage"],
1348
1395
  resTier: ServiceTier | undefined,
1349
1396
  reqTier: unknown,
1350
- ): void {
1351
- const resolvedTier = resolveCodexCostServiceTier(resTier, reqTier);
1397
+ ): ServiceTier {
1398
+ const resolvedTier = resolveCodexCostServiceTier(resTier, reqTier) ?? "default";
1399
+ usage.premiumRequests ??= getPremiumServiceTierRequests(resolvedTier, model, { served: true });
1352
1400
  const multiplier = getCodexServiceTierCostMultiplier(model, resolvedTier);
1353
- if (multiplier === 1) return;
1401
+ if (multiplier === 1) return resolvedTier;
1354
1402
  usage.cost.input *= multiplier;
1355
1403
  usage.cost.output *= multiplier;
1356
1404
  usage.cost.cacheRead *= multiplier;
1357
1405
  usage.cost.cacheWrite *= multiplier;
1358
1406
  usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
1407
+ return resolvedTier;
1359
1408
  }
1360
1409
 
1361
1410
  function resetOutputState(output: AssistantMessage): void {
@@ -1449,14 +1498,14 @@ function createCodexRequestContext(
1449
1498
  transportProviderSessionState?.webSocketPublicToPrivate.set(publicSessionKey, sessionKey);
1450
1499
  }
1451
1500
  const sharedWebsocketState =
1452
- sessionKey && providerSessionState
1501
+ sessionKey && transportSessionId && providerSessionState
1453
1502
  ? isolatedTransportState
1454
1503
  ? providerSessionState.webSocketSessions.get(sessionKey)
1455
- : getCodexWebSocketSessionState(sessionKey, providerSessionState)
1504
+ : getCodexWebSocketSessionState(sessionKey, transportSessionId, providerSessionState)
1456
1505
  : undefined;
1457
1506
  const websocketState =
1458
- sessionKey && isolatedTransportState
1459
- ? getCodexWebSocketSessionState(sessionKey, isolatedTransportState)
1507
+ sessionKey && transportSessionId && isolatedTransportState
1508
+ ? getCodexWebSocketSessionState(sessionKey, transportSessionId, isolatedTransportState)
1460
1509
  : sharedWebsocketState;
1461
1510
  if (isolatedTransportState && websocketState && sharedWebsocketState) {
1462
1511
  websocketState.disableWebsocket = sharedWebsocketState.disableWebsocket;
@@ -1483,6 +1532,7 @@ function createCodexRequestContext(
1483
1532
  parentTurnId: options?.parentTurnId,
1484
1533
  compaction,
1485
1534
  toolNamespacesInfo: options?.toolNamespacesInfo,
1535
+ affinitySessionId: normalizeOpenAIPromptCacheKey(options?.parentSessionId),
1486
1536
  });
1487
1537
  transformedBody.client_metadata = requestMetadata.clientMetadata;
1488
1538
  return {
@@ -1578,7 +1628,7 @@ export async function buildTransformedCodexRequestBody(
1578
1628
 
1579
1629
  /**
1580
1630
  * Keep the request-level effort byte-stable across a conversation and carry
1581
- * later changes as `configuration_update` items (GPT-6 Astra). Requires a
1631
+ * later changes as `configuration_update` items (GPT-6 family). Requires a
1582
1632
  * session id and provider session state to remember the baseline; without
1583
1633
  * them every request stands alone and sends its own effort.
1584
1634
  */
@@ -1593,8 +1643,17 @@ function applyCodexStableEffort(
1593
1643
  const providerState = getCodexProviderSessionState(options?.providerSessionState);
1594
1644
  const sessionId = normalizeOpenAIPromptCacheKey(options?.sessionId);
1595
1645
  if (!providerState || !sessionId) return;
1596
- const state = getOpenAIEffortControlState(providerState.effortControls, `${model.id}\u0000${sessionId}`);
1597
- body.reasoning = { ...body.reasoning, effort: planStableOpenAIEffort(state, body.input, effort) };
1646
+ const state = resolveOpenAIEffortControlState(
1647
+ providerState.effortControls,
1648
+ id => `${model.id}\u0000${id}`,
1649
+ sessionId,
1650
+ normalizeOpenAIPromptCacheKey(options?.parentSessionId),
1651
+ );
1652
+ const lowest = options?.minimizeEffort ? lowestCodexWireEffort(model) : undefined;
1653
+ body.reasoning = {
1654
+ ...body.reasoning,
1655
+ effort: planStableOpenAIEffort(state, body.input, effort, lowest === "none" ? undefined : lowest),
1656
+ };
1598
1657
  }
1599
1658
 
1600
1659
  async function openInitialCodexEventStream(
@@ -1858,7 +1917,7 @@ async function openCodexWebSocketTransport(
1858
1917
  requestSetup.requestSignal,
1859
1918
  onSseEvent,
1860
1919
  ),
1861
- requestBodyForState: structuredCloneJSON(requestContext.transformedBody),
1920
+ requestBodyForState: cloneJsonTree(requestContext.transformedBody),
1862
1921
  transport: "websocket",
1863
1922
  };
1864
1923
  }
@@ -1902,15 +1961,21 @@ async function openCodexWebSocketTransport(
1902
1961
  websocketRequest = replacementWebsocketRequest as typeof websocketRequest;
1903
1962
  }
1904
1963
  recordCodexTurnRequestDiagnostics(websocketState, websocketRequest, "websocket", canAppendBeforeRequest);
1905
- const requestBodyForState = structuredCloneJSON(requestContext.transformedBody);
1906
- // `onPayload` may rewrite the outgoing frame (e.g. drop `stream_options`);
1907
- // recorded state must reflect what was actually sent — the sequential-cutoff
1908
- // summary decoder keys off it.
1964
+ const requestBodyForState = cloneJsonTree(requestContext.transformedBody);
1965
+ // `onPayload` may rewrite the outgoing frame (e.g. drop `stream_options` or
1966
+ // change `service_tier`); recorded state must reflect what was actually
1967
+ // sent — the sequential-cutoff summary decoder and the served-tier
1968
+ // fallback key off it.
1909
1969
  if (websocketRequest.stream_options === undefined) {
1910
1970
  delete requestBodyForState.stream_options;
1911
1971
  } else {
1912
1972
  requestBodyForState.stream_options = websocketRequest.stream_options;
1913
1973
  }
1974
+ if (websocketRequest.service_tier === undefined) {
1975
+ delete requestBodyForState.service_tier;
1976
+ } else {
1977
+ requestBodyForState.service_tier = websocketRequest.service_tier;
1978
+ }
1914
1979
  requestContext.rawRequestDump.body = websocketRequest;
1915
1980
  CODEX_DEBUG &&
1916
1981
  logger.debug("[codex] codex websocket request", {
@@ -2005,7 +2070,14 @@ async function openCodexSseTransport(
2005
2070
  wireBody = replacementWireBody as RequestBody;
2006
2071
  }
2007
2072
  recordCodexTurnRequestDiagnostics(state, wireBody, "sse", canAppendBeforeRequest);
2008
- return { eventStream: await open(wireBody), requestBodyForState: structuredCloneJSON(wireBody), transport: "sse" };
2073
+ // SSE turns never chain, so later reads need only the per-turn knobs (summary
2074
+ // delivery, service tier); copying the whole transcript would be wasted work.
2075
+ const requestBodyForState: RequestBody = {
2076
+ model: wireBody.model,
2077
+ stream_options: wireBody.stream_options ? { ...wireBody.stream_options } : undefined,
2078
+ service_tier: wireBody.service_tier,
2079
+ };
2080
+ return { eventStream: await open(wireBody), requestBodyForState, transport: "sse" };
2009
2081
  }
2010
2082
 
2011
2083
  function isJsonWhitespaceOnly(value: string): boolean {
@@ -2454,11 +2526,13 @@ class CodexStreamProcessor {
2454
2526
  /** Start delivering caller steering into the response `response.created` announced. */
2455
2527
  #startSteering(rawEvent: Record<string, unknown>): void {
2456
2528
  const source = this.options?.liveSteering;
2457
- const connection = this.runtime.websocketState?.connection;
2529
+ const state = this.runtime.websocketState;
2530
+ const connection = state?.connection;
2458
2531
  const responseId = asRecord(rawEvent.response)?.id;
2459
2532
  if (
2460
2533
  !source ||
2461
2534
  !connection ||
2535
+ state?.steeringUnsupported ||
2462
2536
  this.#steerPump ||
2463
2537
  typeof responseId !== "string" ||
2464
2538
  this.runtime.transport !== "websocket" ||
@@ -2516,6 +2590,7 @@ class CodexStreamProcessor {
2516
2590
  if (entry?.block?.type !== "thinking") return;
2517
2591
  for (const delta of this.runtime.takeSummaryDeltas(entry)) {
2518
2592
  entry.block.thinking += delta;
2593
+ entry.block.summary = true;
2519
2594
  this.stream.push({
2520
2595
  type: "thinking_delta",
2521
2596
  contentIndex: entry.contentIndex,
@@ -2528,7 +2603,7 @@ class CodexStreamProcessor {
2528
2603
  const { runtime, output, stream } = this;
2529
2604
  const rawItem = rawEvent.item;
2530
2605
  if (!rawItem || typeof rawItem !== "object") return;
2531
- const item = structuredCloneJSON(rawItem) as CodexEventItem;
2606
+ const item = cloneJsonTree(rawItem) as CodexEventItem;
2532
2607
  if (item.type === "image_generation_call" && item.result) item.status = "completed";
2533
2608
 
2534
2609
  // Match the finalization to the OPEN ITEM that started this block, not the
@@ -2558,6 +2633,7 @@ class CodexStreamProcessor {
2558
2633
  this.#sequentialCutoffSummaries ? this.runtime.cutoffSummaries : undefined,
2559
2634
  );
2560
2635
  block.thinkingSignature = JSON.stringify(item);
2636
+ settleReasoningSummary(item, block);
2561
2637
  stream.push({
2562
2638
  type: "thinking_end",
2563
2639
  contentIndex,
@@ -2583,12 +2659,19 @@ class CodexStreamProcessor {
2583
2659
  }
2584
2660
 
2585
2661
  if (item.type === "function_call") {
2662
+ const partial = block?.type === "toolCall" ? block[kStreamingPartialJson] : undefined;
2586
2663
  const toolCall: ToolCall = {
2587
2664
  type: "toolCall",
2588
2665
  id: encodeResponsesToolCallId(item.call_id, item.id),
2589
2666
  name: item.name,
2590
- arguments: parseToolCallArguments(item.arguments),
2667
+ // An empty terminal item falls back to what `.delta`/`.done` already delivered.
2668
+ arguments: item.arguments
2669
+ ? parseToolCallArguments(item.arguments)
2670
+ : block?.type === "toolCall" && !partial
2671
+ ? block.arguments
2672
+ : parseToolCallArguments(partial),
2591
2673
  };
2674
+ item.arguments = replayableToolCallArguments(item.arguments, toolCall.arguments);
2592
2675
  if (block?.type === "toolCall") {
2593
2676
  // Persist the authoritative final args on the stored block; the throttled
2594
2677
  // delta parser may have left block.arguments stale (often `{}`).
@@ -2654,8 +2737,7 @@ class CodexStreamProcessor {
2654
2737
  const response = rawResponse && typeof rawResponse === "object" ? rawResponse : undefined;
2655
2738
  const responseId = response && "id" in response && typeof response.id === "string" ? response.id : undefined;
2656
2739
  const usage = response && "usage" in response ? parseCodexResponseUsage(response.usage) : undefined;
2657
- const serviceTier =
2658
- response && "service_tier" in response ? parseCodexServiceTier(response.service_tier) : undefined;
2740
+ const serviceTier = response && "service_tier" in response ? parseServiceTier(response.service_tier) : undefined;
2659
2741
  const status = response && "status" in response ? parseCodexResponseStatus(response.status) : undefined;
2660
2742
  const endTurn = response && "end_turn" in response ? response.end_turn : undefined;
2661
2743
 
@@ -2688,10 +2770,13 @@ class CodexStreamProcessor {
2688
2770
  // baseline, which no longer matches the transcript.
2689
2771
  resetCodexWebSocketAppendState(state);
2690
2772
  } else {
2691
- state.lastRequest = structuredCloneJSON(runtime.requestBodyForState);
2773
+ // requestBodyForState is already a private copy and is not mutated after
2774
+ // the request, so the append baseline takes ownership of it.
2775
+ state.lastRequest = runtime.requestBodyForState;
2692
2776
  const nativeOutputItems = runtime.finalizeNativeOutputItems();
2693
2777
  const replayableResponseItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(
2694
- structuredCloneJSON(nativeOutputItems),
2778
+ cloneJsonTree(nativeOutputItems),
2779
+ { supportsImageDetailOriginal: model.compat.supportsImageDetailOriginal },
2695
2780
  );
2696
2781
  if (responseId && replayableResponseItems && replayableResponseItems.length === nativeOutputItems.length) {
2697
2782
  state.lastResponseId = responseId;
@@ -2713,7 +2798,12 @@ class CodexStreamProcessor {
2713
2798
  finalizePendingResponsesToolCalls(output);
2714
2799
 
2715
2800
  calculateCost(model, output.usage, output.timestamp);
2716
- applyCodexServiceTierPricing(model, output.usage, serviceTier, runtime.requestBodyForState.service_tier);
2801
+ output.serviceTier = applyCodexServiceTierPricing(
2802
+ model,
2803
+ output.usage,
2804
+ serviceTier,
2805
+ runtime.requestBodyForState.service_tier,
2806
+ );
2717
2807
  output.stopReason = mapOpenAIResponsesStopReason(steered ? "completed" : status);
2718
2808
  promoteResponsesToolUseStopReason(
2719
2809
  output,
@@ -2722,7 +2812,28 @@ class CodexStreamProcessor {
2722
2812
  );
2723
2813
  }
2724
2814
 
2815
+ /**
2816
+ * The native turn lane rejected our steering and dropped the response with
2817
+ * its socket: stop steering this session and forget the dead socket so the
2818
+ * provider retry replays on a fresh one.
2819
+ */
2820
+ #stopSteeringOnNativeLaneRejection(error: unknown): void {
2821
+ const state = this.runtime.websocketState;
2822
+ if (
2823
+ !state ||
2824
+ !(error instanceof CodexProviderStreamError) ||
2825
+ error.code !== AIError.CODEX_NATIVE_LANE_STEER_REJECTED_CODE
2826
+ ) {
2827
+ return;
2828
+ }
2829
+ state.steeringUnsupported = true;
2830
+ state.connection?.close("native-lane-steer-rejected");
2831
+ state.connection = undefined;
2832
+ resetCodexWebSocketAppendState(state);
2833
+ }
2834
+
2725
2835
  async #recoverStreamError(error: unknown): Promise<boolean> {
2836
+ this.#stopSteeringOnNativeLaneRejection(error);
2726
2837
  if (
2727
2838
  error instanceof CodexSteerCommitError &&
2728
2839
  this.runtime.websocketState &&
@@ -3277,7 +3388,14 @@ export async function prewarmOpenAICodexResponses(
3277
3388
  model: Model<"openai-codex-responses">,
3278
3389
  options?: Pick<
3279
3390
  OpenAICodexResponsesOptions,
3280
- "apiKey" | "headers" | "sessionId" | "signal" | "preferWebsockets" | "providerSessionState" | "responsesLite"
3391
+ | "apiKey"
3392
+ | "headers"
3393
+ | "sessionId"
3394
+ | "signal"
3395
+ | "preferWebsockets"
3396
+ | "providerSessionState"
3397
+ | "responsesLite"
3398
+ | "serviceTier"
3281
3399
  >,
3282
3400
  ): Promise<void> {
3283
3401
  const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
@@ -3294,17 +3412,18 @@ export async function prewarmOpenAICodexResponses(
3294
3412
  if (publicSessionKey && sessionKey) {
3295
3413
  providerSessionState?.webSocketPublicToPrivate.set(publicSessionKey, sessionKey);
3296
3414
  }
3297
- if (!sessionKey || !providerSessionState) return;
3298
- const state = getCodexWebSocketSessionState(sessionKey, providerSessionState);
3415
+ if (!sessionKey || !transportSessionId || !providerSessionState) return;
3416
+ const state = getCodexWebSocketSessionState(sessionKey, transportSessionId, providerSessionState);
3299
3417
  if (!shouldUseCodexWebSocket(model, state, options?.preferWebsockets)) return;
3300
- const metadataSession = getOrCreateCodexMetadataSessionState(
3301
- transportSessionId ?? crypto.randomUUID(),
3302
- providerSessionState,
3303
- );
3418
+ const metadataSession = getOrCreateCodexMetadataSessionState(transportSessionId, providerSessionState);
3304
3419
  const turnState = getOrCreateCodexTurnState(metadataSession, sessionKey);
3305
3420
  const codexClientVersion = CODEX_CLIENT_VERSION;
3306
3421
  const requestIdentity = createCodexCompatibilityIdentity(metadataSession);
3307
3422
  const attestation = await getCodexAttestationHeader(accountId);
3423
+ const routedRequest: Pick<RequestBody, "model" | "service_tier"> = {
3424
+ model: model.requestModelId ?? model.id,
3425
+ };
3426
+ applyOpenAIServiceTier(routedRequest, options?.serviceTier, model);
3308
3427
  const headers = logger.time(
3309
3428
  "prewarmCodex:createHeaders",
3310
3429
  createCodexHeaders,
@@ -3319,6 +3438,7 @@ export async function prewarmOpenAICodexResponses(
3319
3438
  responsesLite,
3320
3439
  requestIdentity,
3321
3440
  attestation,
3441
+ routedRequest,
3322
3442
  );
3323
3443
  await logger.time(
3324
3444
  "prewarmCodex:establishWs",
@@ -3351,11 +3471,13 @@ function getCodexWebSocketSessionKey(
3351
3471
 
3352
3472
  function getCodexWebSocketSessionState(
3353
3473
  sessionKey: string,
3474
+ sessionId: string,
3354
3475
  providerSessionState: CodexProviderSessionState,
3355
3476
  ): CodexWebSocketSessionState {
3356
3477
  const existing = providerSessionState.webSocketSessions.get(sessionKey);
3357
3478
  if (existing) return existing;
3358
3479
  const created: CodexWebSocketSessionState = {
3480
+ sessionId,
3359
3481
  disableWebsocket: false,
3360
3482
  canAppend: false,
3361
3483
  fallbackCount: 0,
@@ -3510,32 +3632,11 @@ export function getOpenAICodexTransportDetails(
3510
3632
  };
3511
3633
  }
3512
3634
 
3513
- const codexDiagnosticsTextEncoder = new TextEncoder();
3514
-
3515
- function jsonByteLength(value: unknown): number {
3516
- const json = JSON.stringify(value);
3517
- return codexDiagnosticsTextEncoder.encode(json === undefined ? "undefined" : json).byteLength;
3518
- }
3519
-
3520
3635
  function hashJson(value: unknown): string {
3521
3636
  const json = JSON.stringify(value);
3522
3637
  return String(Bun.hash(json === undefined ? "undefined" : json));
3523
3638
  }
3524
3639
 
3525
- function parseCodexServiceTier(value: unknown): ServiceTier | undefined {
3526
- switch (value) {
3527
- case "auto":
3528
- case "default":
3529
- case "flex":
3530
- case "scale":
3531
- case "priority":
3532
- case "ultrafast":
3533
- return value;
3534
- default:
3535
- return undefined;
3536
- }
3537
- }
3538
-
3539
3640
  function parseCodexResponseStatus(value: unknown): ResponseStatus | undefined {
3540
3641
  switch (value) {
3541
3642
  case "completed":
@@ -3662,7 +3763,7 @@ function buildCodexTurnRequestDiagnostics(
3662
3763
  inputItemCount: inputItems.length,
3663
3764
  inputItemTypes,
3664
3765
  ...(inputItemTypes[0] ? { firstInputItemType: inputItemTypes[0] } : {}),
3665
- inputJsonBytes: jsonByteLength(inputItems),
3766
+ inputJsonBytes: Buffer.byteLength(JSON.stringify(inputItems), "utf8"),
3666
3767
  ...(promptCacheKey !== undefined ? { promptCacheKey } : {}),
3667
3768
  ...(toolsHash !== undefined ? { toolsHash } : {}),
3668
3769
  optionsHash: createCodexOptionsHash(request),
@@ -3690,6 +3791,8 @@ function recordCodexTurnRequestDiagnostics(
3690
3791
  state.stats.lastDeltaInputItems = undefined;
3691
3792
  state.stats.lastPreviousResponseId = undefined;
3692
3793
  }
3794
+ // Measured at send time: the request object may be reused or mutated by an
3795
+ // `onPayload` hook afterwards, and retaining it would pin the transcript.
3693
3796
  state.stats.lastTurn = {
3694
3797
  request: buildCodexTurnRequestDiagnostics(request, transport, canAppendBeforeRequest),
3695
3798
  };
@@ -4043,6 +4146,11 @@ class CodexWebSocketConnection {
4043
4146
  this.#handleSteerEvent(parsed);
4044
4147
  return;
4045
4148
  }
4149
+ // The native lane answers `response.steer` with a plain `error` that
4150
+ // also ends the response: refuse the submission, then fail the stream.
4151
+ if (parsed.type === "error" && parsed.code === AIError.CODEX_NATIVE_LANE_STEER_REJECTED_CODE) {
4152
+ this.#refuseSteerWaiters(parsed.code, typeof parsed.message === "string" ? parsed.message : undefined);
4153
+ }
4046
4154
  this.#push(parsed);
4047
4155
  } catch (error) {
4048
4156
  notifyCodexWebSocketMalformed(this.#streamObserver, event.data, error);
@@ -4336,6 +4444,12 @@ class CodexWebSocketConnection {
4336
4444
  return index < 0 ? undefined : this.#steerWaiters.splice(index, 1)[0];
4337
4445
  }
4338
4446
 
4447
+ #refuseSteerWaiters(code: string, message: string | undefined): void {
4448
+ const waiters = this.#steerWaiters;
4449
+ this.#steerWaiters = [];
4450
+ for (const waiter of waiters) waiter.resolve({ accepted: false, code, message });
4451
+ }
4452
+
4339
4453
  #rejectSteerWaiters(reason: string): void {
4340
4454
  const waiters = this.#steerWaiters;
4341
4455
  if (waiters.length === 0) return;
@@ -4593,10 +4707,12 @@ async function getOrCreateCodexWebSocketConnection(
4593
4707
  * compression is disabled or fails, in which case the caller sends the
4594
4708
  * plain JSON string without a `content-encoding` header.
4595
4709
  */
4596
- function compressCodexRequestBody(bodyJson: string, baseUrl: string): Uint8Array | undefined {
4710
+ async function compressCodexRequestBody(bodyJson: string, baseUrl: string): Promise<Uint8Array | undefined> {
4597
4711
  if (!isOfficialCodexApiUrl(baseUrl) || !$flag("PI_CODEX_ZSTD", true)) return undefined;
4598
4712
  try {
4599
- return Bun.zstdCompressSync(bodyJson, { level: 3 });
4713
+ // Off the main thread: SSE sends the full transcript, so multi-MB bodies
4714
+ // would otherwise block the event loop for the whole compression.
4715
+ return await Bun.zstdCompress(bodyJson, { level: 3 });
4600
4716
  } catch (error) {
4601
4717
  CODEX_DEBUG &&
4602
4718
  logger.debug("[codex] codex request body compression failed", {
@@ -4663,7 +4779,7 @@ async function openCodexSseEventStream(
4663
4779
  }
4664
4780
  };
4665
4781
  const bodyJson = JSON.stringify(body);
4666
- const compressedBody = compressCodexRequestBody(bodyJson, url);
4782
+ const compressedBody = await compressCodexRequestBody(bodyJson, url);
4667
4783
  if (compressedBody !== undefined) {
4668
4784
  headers.set("content-encoding", "zstd");
4669
4785
  }
@@ -4794,8 +4910,11 @@ function createCodexHeaders(
4794
4910
  headers.set(OPENAI_HEADERS.VERSION, codexClientVersion);
4795
4911
  headers.set("User-Agent", USER_AGENT);
4796
4912
  if (sessionId) {
4797
- headers.set(OPENAI_HEADERS.CONVERSATION_ID, sessionId);
4798
- headers.set(OPENAI_HEADERS.SESSION_ID, sessionId);
4913
+ // The backend derives prompt-cache affinity from the session headers
4914
+ // (codex-rs `responses_session_id`); a side request shares its parent's.
4915
+ const affinitySessionId = requestMetadata?.affinitySessionId ?? sessionId;
4916
+ headers.set(OPENAI_HEADERS.CONVERSATION_ID, affinitySessionId);
4917
+ headers.set(OPENAI_HEADERS.SESSION_ID, affinitySessionId);
4799
4918
  headers.set("x-client-request-id", sessionId);
4800
4919
  } else {
4801
4920
  headers.delete(OPENAI_HEADERS.CONVERSATION_ID);
@@ -4909,10 +5028,14 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
4909
5028
  if (historyItems) {
4910
5029
  const redactedHistoryItems = redactSensitiveInObject(historyItems).result as Array<ResponseInput[number]>;
4911
5030
  const sanitizedHistoryItems = dropMalformedOpenAIResponsesToolCalls(redactedHistoryItems);
5031
+ const clampedHistoryItems = clampOpenAIResponsesImageDetailForReplay(
5032
+ sanitizedHistoryItems,
5033
+ model.compat.supportsImageDetailOriginal,
5034
+ );
4912
5035
  const replayItems =
4913
5036
  model.supportsComputerUse === true
4914
- ? sanitizedHistoryItems
4915
- : unrollCodexComputerItems(sanitizedHistoryItems, model.compat.supportsImageDetailOriginal);
5037
+ ? clampedHistoryItems
5038
+ : unrollCodexComputerItems(clampedHistoryItems, model.compat.supportsImageDetailOriginal);
4916
5039
  for (const item of replayItems) {
4917
5040
  if (item.type === "custom_tool_call") {
4918
5041
  customCallIds.add(item.call_id);
@@ -4946,7 +5069,9 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
4946
5069
  const historyItems = providerPayload?.items as Array<Record<string, unknown>> | undefined;
4947
5070
  let suppressHiddenEmptyFallback = false;
4948
5071
  if (historyItems) {
4949
- const sanitizedHistoryItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(historyItems);
5072
+ const sanitizedHistoryItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(historyItems, {
5073
+ supportsImageDetailOriginal: model.compat.supportsImageDetailOriginal,
5074
+ });
4950
5075
  if (sanitizedHistoryItems) {
4951
5076
  const rawReplayItems =
4952
5077
  model.supportsComputerUse === true