mixdog 0.9.148 → 0.9.149

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.
@@ -19,7 +19,12 @@ import { makeModelCache } from './model-cache.mjs';
19
19
 
20
20
  import { sendViaWebSocket } from './openai-oauth-ws.mjs';
21
21
  import { _combineUsageWithWarmup } from './openai-ws-events.mjs';
22
- import { acquireWebSocket, releaseWebSocket, hasPooledWebSocket } from './openai-ws-pool.mjs';
22
+ import {
23
+ acquireWebSocket,
24
+ releaseWebSocket,
25
+ hasPooledWebSocket,
26
+ WS_IDLE_MS,
27
+ } from './openai-ws-pool.mjs';
23
28
  import { _codexWsCompatibilityHeaders } from './openai-codex-metadata.mjs';
24
29
  import { resolveOpenAiTransportPolicy } from './openai-transport-policy.mjs';
25
30
  import {
@@ -78,8 +83,17 @@ export { _displayCodexModel };
78
83
  // Public test/integration entry retained alongside the transport module export.
79
84
  export { sendViaHttpSse };
80
85
  // --- Constants ---
81
- import { buildRequestBody } from './openai-responses-payload.mjs';
82
- export { buildRequestBody, convertMessagesToResponsesInput, toOpenAIResponsesTool, _convertMessagesToResponsesInputForTest } from './openai-responses-payload.mjs';
86
+ import {
87
+ buildCodexStartupPrewarmBody,
88
+ buildRequestBody,
89
+ } from './openai-responses-payload.mjs';
90
+ export {
91
+ buildCodexStartupPrewarmBody,
92
+ buildRequestBody,
93
+ convertMessagesToResponsesInput,
94
+ toOpenAIResponsesTool,
95
+ _convertMessagesToResponsesInputForTest,
96
+ } from './openai-responses-payload.mjs';
83
97
  const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
84
98
  // Exported for openai-oauth-http-sse.mjs (fallback transport headers/URL).
85
99
  export const CODEX_OAUTH_ORIGINATOR = 'codex_cli_rs';
@@ -358,6 +372,22 @@ async function refreshTokens(refreshToken) {
358
372
  clearTimeout(timeout);
359
373
  }
360
374
  }
375
+ // Identity of the STABLE prefix a startup prewarm actually warmed (model +
376
+ // instructions/tools, never the live transcript). A reservation may only be
377
+ // consumed by a request whose own prefix hashes the same: the first turn
378
+ // refreshes environment/tool surface after the prewarm was fired, and adopting
379
+ // a socket anchored on the older prefix costs the whole prewarm and forces the
380
+ // real request back to a full frame.
381
+ function _codexStartupPrefixHash(body) {
382
+ const prewarm = buildCodexStartupPrewarmBody(body);
383
+ return createHash('sha256').update(JSON.stringify({
384
+ model: prewarm?.model ?? null,
385
+ instructions: prewarm?.instructions ?? null,
386
+ tools: prewarm?.tools ?? null,
387
+ input: prewarm?.input ?? [],
388
+ })).digest('hex').slice(0, 24);
389
+ }
390
+
361
391
  // --- Build Responses API request ---
362
392
  export class OpenAIOAuthProvider {
363
393
  // OpenAI input_tokens already INCLUDES cached_tokens (cached is a subset),
@@ -371,6 +401,8 @@ export class OpenAIOAuthProvider {
371
401
  // booleans here would let one unhealthy session force every other session
372
402
  // onto HTTP.
373
403
  _httpFallbackUntilByPoolKey = new Map();
404
+ _startupPrewarmByPoolKey = new Map();
405
+ _startupPrewarmReadyByPoolKey = new Map();
374
406
  config;
375
407
  constructor(config) {
376
408
  this.config = config || {};
@@ -531,14 +563,32 @@ export class OpenAIOAuthProvider {
531
563
  let auth = await _authP;
532
564
  await _verP;
533
565
  const body = await _bodyP;
534
- // poolKey != cacheKey by design (see openai-oauth-ws.mjs header note).
535
- // poolKey is per-session so parallel reviewer/worker callers each get
536
- // their own socket bucket. cacheKey is the Codex-style prompt_cache_key:
537
- // by default it is the session/thread identity (clamped to 64 chars) and
538
- // feeds both `body.prompt_cache_key` and the OAuth WS handshake
539
- // `session_id`, so each long-lived thread keeps a stable cache shard.
566
+ // poolKey != cacheKey by design. poolKey isolates socket/delta state per
567
+ // session. cacheKey is body.prompt_cache_key and affects prompt-cache
568
+ // routing only; Codex handshake identity comes from sendOpts and remains
569
+ // independent so a future cache-lane policy cannot merge conversations.
540
570
  const poolKey = opts.sessionId || null;
541
571
  const cacheKey = body.prompt_cache_key || resolveProviderCacheKey(opts, 'openai-oauth');
572
+ const startupPrefixHash = _codexStartupPrefixHash(body);
573
+ let startupPrewarmHandle = null;
574
+ if (poolKey && opts._startupPrewarmOnly !== true) {
575
+ const candidate = this._startupPrewarmReadyByPoolKey.get(poolKey) || null;
576
+ if (candidate) {
577
+ this._startupPrewarmReadyByPoolKey.delete(poolKey);
578
+ if (candidate._reservationTimer) {
579
+ clearTimeout(candidate._reservationTimer);
580
+ candidate._reservationTimer = null;
581
+ }
582
+ if (candidate.poolKey === poolKey
583
+ && candidate.cacheKey === cacheKey
584
+ && candidate.prefixHash === startupPrefixHash
585
+ && candidate.entry) {
586
+ startupPrewarmHandle = candidate;
587
+ } else if (candidate.entry) {
588
+ releaseWebSocket({ entry: candidate.entry, poolKey, keep: false });
589
+ }
590
+ }
591
+ }
542
592
  const iteration = Number.isFinite(Number(opts.iteration)) ? Number(opts.iteration) : null;
543
593
  const sendWs = typeof opts._sendViaWebSocketFn === 'function' ? opts._sendViaWebSocketFn : sendViaWebSocket;
544
594
  const sendHttp = typeof opts._sendViaHttpSseFn === 'function' ? opts._sendViaHttpSseFn : sendViaHttpSse;
@@ -644,21 +694,24 @@ export class OpenAIOAuthProvider {
644
694
  }
645
695
  return recordLiveModel(result);
646
696
  };
647
- const dispatchWs = (forceFresh = false, carriedWarmup = null) => sendWs({
648
- auth,
649
- body,
650
- sendOpts: opts,
651
- onStreamDelta,
652
- onToolCall,
653
- onTextDelta,
654
- onStageChange,
655
- externalSignal,
656
- poolKey,
657
- cacheKey,
658
- iteration,
659
- useModel,
660
- displayModel: _displayCodexModel,
661
- forceFresh,
697
+ const dispatchWs = (forceFresh = false, carriedWarmup = null) => {
698
+ const prewarmedHandle = forceFresh ? null : startupPrewarmHandle;
699
+ startupPrewarmHandle = null;
700
+ return sendWs({
701
+ auth,
702
+ body,
703
+ sendOpts: opts,
704
+ onStreamDelta,
705
+ onToolCall,
706
+ onTextDelta,
707
+ onStageChange,
708
+ externalSignal,
709
+ poolKey,
710
+ cacheKey,
711
+ iteration,
712
+ useModel,
713
+ displayModel: _displayCodexModel,
714
+ forceFresh,
662
715
  // Default refs-style recovery: keep using WS first. A transient
663
716
  // first-byte / mid-stream stall closes the bad socket and retries on
664
717
  // a fresh WS entry; only after the bounded WS retry budget is
@@ -678,10 +731,12 @@ export class OpenAIOAuthProvider {
678
731
  // exactly that. Every prewarmed call on record (72/72) kept its
679
732
  // chain continuous. MIXDOG_OPENAI_OAUTH_WS_WARMUP=0 disables it.
680
733
  warmupBody: _envFlag('MIXDOG_OPENAI_OAUTH_WS_WARMUP', true)
681
- ? { ...body, input: [], generate: false }
734
+ ? buildCodexStartupPrewarmBody(body)
682
735
  : null,
683
- _carriedWarmup: carriedWarmup,
684
- });
736
+ _carriedWarmup: carriedWarmup,
737
+ _prewarmedHandle: prewarmedHandle,
738
+ });
739
+ };
685
740
  const mustSurfaceFallbackError = (fallbackErr) => externalSignal?.aborted
686
741
  || fallbackErr?.name === 'AbortError'
687
742
  || fallbackErr?.code === 'ABORT_ERR'
@@ -695,6 +750,13 @@ export class OpenAIOAuthProvider {
695
750
  || httpFallbackActive()
696
751
  || _envFlag('MIXDOG_OPENAI_OAUTH_FORCE_HTTP_FALLBACK', false)
697
752
  ))) {
753
+ if (startupPrewarmHandle?.entry) {
754
+ releaseWebSocket({ entry: startupPrewarmHandle.entry, poolKey, keep: false });
755
+ startupPrewarmHandle = null;
756
+ }
757
+ if (opts._startupPrewarmOnly === true) {
758
+ return { startupPrewarm: false };
759
+ }
698
760
  return dispatchHttp('forced');
699
761
  }
700
762
 
@@ -704,9 +766,16 @@ export class OpenAIOAuthProvider {
704
766
  if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-send-start model=${useModel} agent=${_sendAgent} sessionHash=${createHash('sha256').update(String(_sendSessionId)).digest('hex').slice(0, 8)} iteration=${iteration ?? '(none)'}\n`); }
705
767
  const result = await dispatchWs(false);
706
768
  if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-send-end elapsed=${Date.now() - _t1}ms result=ok\n`); }
769
+ // Stamp the reservation with the prefix it warmed so the first real
770
+ // request can tell a matching anchor from a stale one.
771
+ if (opts._startupPrewarmOnly === true && result?.startupPrewarmHandle) {
772
+ result.startupPrewarmHandle.prefixHash = startupPrefixHash;
773
+ result.startupPrewarmHandle.promptWarmup = true;
774
+ }
707
775
  return recordLiveModel(result);
708
776
  } catch (err) {
709
777
  traceWsError(err, 'primary');
778
+ if (opts._startupPrewarmOnly === true) throw err;
710
779
  const status = err?.httpStatus;
711
780
  // Live-text invariant: if the WS attempt already relayed a
712
781
  // non-empty text chunk to the client, NO recovery path may reissue
@@ -782,35 +851,128 @@ export class OpenAIOAuthProvider {
782
851
  }
783
852
  }
784
853
  /**
785
- * Spawn-time transport prewarm (codex prewarm_websocket parity): open the
786
- * session's WS socket while the caller is still assembling the session /
787
- * first prompt, so the first real request reuses an already-open pooled
788
- * socket instead of paying TLS + upgrade on its critical path.
854
+ * Session-startup Responses prewarm. With a materialized session this sends
855
+ * Codex's generate:false request (stable instructions/tools, empty input)
856
+ * and leaves the response/socket state pooled for the first real turn.
857
+ * Legacy callers without a materialized prompt retain the connection-only
858
+ * prewarm path.
789
859
  *
790
860
  * Best-effort by contract: every failure returns false and leaves the
791
- * lazy per-send handshake untouched. Only runs when the thread-scoped
792
- * prompt_cache_key derivation is active — with it disabled the real
793
- * cacheKey depends on the built request body (instructions/tools hash),
794
- * which does not exist yet at spawn prep, and a mismatched socket would
795
- * just be evicted as incompatible on first acquire.
861
+ * lazy per-send warmup untouched.
796
862
  */
797
863
  async prewarmWsTransportForSession(opts = {}, seams = {}) {
798
- const _acquire = seams._acquire || acquireWebSocket;
799
- const _release = seams._release || releaseWebSocket;
800
- const _hasPooled = seams._hasPooled || hasPooledWebSocket;
801
- const _warmVersion = seams._warmVersion || warmCodexClientVersion;
802
864
  const poolKey = opts.sessionId || null;
803
865
  if (!poolKey) return false;
804
- try {
866
+ const session = opts.session && typeof opts.session === 'object' ? opts.session : null;
867
+ const messages = Array.isArray(opts.messages)
868
+ ? opts.messages
869
+ : (Array.isArray(session?.messages) ? session.messages : null);
870
+ const tools = Array.isArray(opts.tools)
871
+ ? opts.tools
872
+ : (Array.isArray(session?.tools) ? session.tools : []);
873
+ const model = opts.model || session?.model || null;
874
+ const promptWarmup = !!(messages
875
+ && model
876
+ && _envFlag('MIXDOG_OPENAI_OAUTH_WS_WARMUP', true));
877
+ // A connection-only reservation (session create, before the prompt is
878
+ // materialized) must not satisfy the first turn's prompt prewarm.
879
+ const readyHandle = this._startupPrewarmReadyByPoolKey.get(poolKey) || null;
880
+ if (readyHandle && (!promptWarmup || readyHandle.promptWarmup === true)) return true;
881
+ const existing = this._startupPrewarmByPoolKey.get(poolKey);
882
+ if (existing) {
883
+ if (!promptWarmup || existing.promptWarmup) return existing.task;
884
+ try { await existing.task; } catch {}
885
+ if (this._startupPrewarmByPoolKey.get(poolKey) === existing) {
886
+ this._startupPrewarmByPoolKey.delete(poolKey);
887
+ }
888
+ return this.prewarmWsTransportForSession(opts, seams);
889
+ }
890
+ const record = { promptWarmup, task: null };
891
+ const task = (async () => {
892
+ const _acquire = seams._acquire || acquireWebSocket;
893
+ const _release = seams._release || releaseWebSocket;
894
+ const _hasPooled = seams._hasPooled || hasPooledWebSocket;
895
+ const _warmVersion = seams._warmVersion || warmCodexClientVersion;
896
+ const _send = seams._send || ((messages, model, tools, sendOpts) =>
897
+ this.send(messages, model, tools, sendOpts));
805
898
  const transportPolicy = resolveOpenAiTransportPolicy();
806
899
  if (transportPolicy.transport === 'http'
807
900
  || _envFlag('MIXDOG_OPENAI_OAUTH_FORCE_HTTP_FALLBACK', false)) return false;
901
+ if (promptWarmup) {
902
+ const {
903
+ messages: _messages,
904
+ tools: _tools,
905
+ model: _model,
906
+ ...baseSendOpts
907
+ } = opts;
908
+ const codexSessionId = baseSendOpts.codexSessionId
909
+ || session?.codexWireSessionId
910
+ || null;
911
+ const sendOpts = {
912
+ ...baseSendOpts,
913
+ sessionId: poolKey,
914
+ session,
915
+ effort: baseSendOpts.effort ?? session?.effort ?? null,
916
+ fast: baseSendOpts.fast === true || session?.fast === true,
917
+ modelParameters: baseSendOpts.modelParameters || session?.modelParameters || {},
918
+ promptCacheKey: baseSendOpts.promptCacheKey || session?.promptCacheKey || null,
919
+ ...(codexSessionId ? {
920
+ codexSessionId,
921
+ codexThreadId: baseSendOpts.codexThreadId || codexSessionId,
922
+ threadId: baseSendOpts.threadId || codexSessionId,
923
+ } : {}),
924
+ requestKind: 'prewarm',
925
+ codexRequestKind: 'prewarm',
926
+ _startupPrewarmOnly: true,
927
+ };
928
+ const _t0 = Date.now();
929
+ const result = await _send(messages, model, tools, sendOpts);
930
+ const handle = result?.startupPrewarmHandle || null;
931
+ const ready = result?.startupPrewarm === true && !!handle?.entry;
932
+ if (ready) {
933
+ const previous = this._startupPrewarmReadyByPoolKey.get(poolKey);
934
+ if (previous?.entry && previous !== handle) {
935
+ if (previous._reservationTimer) clearTimeout(previous._reservationTimer);
936
+ releaseWebSocket({ entry: previous.entry, poolKey, keep: false });
937
+ }
938
+ this._startupPrewarmReadyByPoolKey.set(poolKey, handle);
939
+ handle._reservationTimer = setTimeout(() => {
940
+ if (this._startupPrewarmReadyByPoolKey.get(poolKey) !== handle) return;
941
+ this._startupPrewarmReadyByPoolKey.delete(poolKey);
942
+ handle._reservationTimer = null;
943
+ releaseWebSocket({ entry: handle.entry, poolKey, keep: false });
944
+ }, WS_IDLE_MS);
945
+ try { handle._reservationTimer.unref?.(); } catch {}
946
+ try {
947
+ handle.entry.socket?.once?.('close', () => {
948
+ if (handle._reservationTimer) {
949
+ clearTimeout(handle._reservationTimer);
950
+ handle._reservationTimer = null;
951
+ }
952
+ if (this._startupPrewarmReadyByPoolKey.get(poolKey) === handle) {
953
+ this._startupPrewarmReadyByPoolKey.delete(poolKey);
954
+ }
955
+ });
956
+ } catch {}
957
+ }
958
+ try {
959
+ appendAgentTrace({
960
+ sessionId: poolKey,
961
+ kind: 'spawn_ws_prewarm',
962
+ provider: 'openai-oauth',
963
+ transport: 'websocket',
964
+ payload: {
965
+ elapsed_ms: Date.now() - _t0,
966
+ prompt_warmup: true,
967
+ ready,
968
+ },
969
+ });
970
+ } catch {}
971
+ return ready;
972
+ }
808
973
  const threadKeyGate = String(process.env.MIXDOG_OAI_CODEX_THREAD_CACHE_KEY || '').toLowerCase();
809
974
  if (threadKeyGate === '0' || threadKeyGate === 'false') return false;
810
975
  if (_hasPooled(poolKey)) return true;
811
- // Identical derivation to buildRequestBody's prompt_cache_key
812
- // (thread-scoped branch), so the prewarmed socket's handshake
813
- // session_id and pool-compatibility key match the first real send.
814
976
  const cacheKey = buildStableProviderPromptCacheKey('openai-oauth', opts);
815
977
  const [auth] = await Promise.all([this.ensureAuth(), _warmVersion()]);
816
978
  const codexHeaders = _codexWsCompatibilityHeaders({
@@ -818,8 +980,6 @@ export class OpenAIOAuthProvider {
818
980
  cacheKey,
819
981
  sendOpts: opts,
820
982
  model: opts.model,
821
- // Same tier the request body will carry, so the prewarm
822
- // handshake routes to the node the first real send wants.
823
983
  serviceTier: opts.fast === true && codexModelSupportsServiceTier(opts.model, 'priority')
824
984
  ? 'priority'
825
985
  : '',
@@ -840,18 +1000,28 @@ export class OpenAIOAuthProvider {
840
1000
  kind: 'spawn_ws_prewarm',
841
1001
  provider: 'openai-oauth',
842
1002
  transport: 'websocket',
843
- payload: { elapsed_ms: Date.now() - _t0, reused: acquired.reused === true },
1003
+ payload: {
1004
+ elapsed_ms: Date.now() - _t0,
1005
+ reused: acquired.reused === true,
1006
+ prompt_warmup: false,
1007
+ },
844
1008
  });
845
1009
  } catch {}
846
- if (process.env.MIXDOG_DEBUG_AGENT) {
847
- process.stderr.write(`[agent-trace] spawn-ws-prewarm ok poolKey=${createHash('sha256').update(String(poolKey)).digest('hex').slice(0, 8)} elapsed=${Date.now() - _t0}ms\n`);
848
- }
849
1010
  return true;
1011
+ })();
1012
+ record.task = task;
1013
+ this._startupPrewarmByPoolKey.set(poolKey, record);
1014
+ try {
1015
+ return await task;
850
1016
  } catch (err) {
851
1017
  if (process.env.MIXDOG_DEBUG_AGENT) {
852
1018
  process.stderr.write(`[agent-trace] spawn-ws-prewarm failed err=${String(err?.message || err).slice(0, 160)}\n`);
853
1019
  }
854
1020
  return false;
1021
+ } finally {
1022
+ if (this._startupPrewarmByPoolKey.get(poolKey) === record) {
1023
+ this._startupPrewarmByPoolKey.delete(poolKey);
1024
+ }
855
1025
  }
856
1026
  }
857
1027
  async listModels() {
@@ -71,6 +71,7 @@ import {
71
71
  _compareVersion,
72
72
  _isMainCodexFamily,
73
73
  _markLatestCodex,
74
+ _codexUsesResponsesLite,
74
75
  } from './openai-codex-model.mjs';
75
76
 
76
77
  // Public test/integration entry retained alongside the transport module export.
@@ -104,9 +105,18 @@ export function convertMessagesToResponsesInput(messages, opts = {}) {
104
105
  const pendingToolMedia = [];
105
106
  const customToolCallNameById = new Map();
106
107
  const replayEncryptedReasoning = opts.replayEncryptedReasoning === true;
107
- const wireParity = process.env.MIXDOG_OAI_CODEX_WIRE_PARITY === '1';
108
+ const wireParity = opts.codexWireParity === true;
109
+ const wireMessageMetadata = opts.codexMessageMetadata
110
+ && typeof opts.codexMessageMetadata === 'object'
111
+ ? opts.codexMessageMetadata
112
+ : {};
108
113
  const wireMessage = (role, content) => (wireParity
109
- ? { type: 'message', role, content, internal_chat_message_metadata_passthrough: {} }
114
+ ? {
115
+ type: 'message',
116
+ role,
117
+ content,
118
+ internal_chat_message_metadata_passthrough: wireMessageMetadata,
119
+ }
110
120
  : { role, content });
111
121
  // `phase` replays each retained item on the side of the assistant text it
112
122
  // was emitted on, so the rebuilt turn keeps the response's own item order.
@@ -243,6 +253,55 @@ function _codexModelSupportsReasoningSummaries(id) {
243
253
  return true;
244
254
  }
245
255
 
256
+ function _codexModelUsesResponsesLite(id, opts = {}) {
257
+ if (typeof opts.useResponsesLite === 'boolean') return opts.useResponsesLite;
258
+ const override = String(process.env.MIXDOG_OAI_RESPONSES_LITE || '').trim().toLowerCase();
259
+ if (['1', 'true', 'yes', 'on'].includes(override)) return true;
260
+ if (['0', 'false', 'no', 'off'].includes(override)) return false;
261
+ const info = _findCachedCodexModel(id);
262
+ return _codexUsesResponsesLite(id, info);
263
+ }
264
+
265
+ function _responsesLiteTools(tools) {
266
+ const out = [];
267
+ const functions = [];
268
+ let functionsIndex = null;
269
+ for (const tool of Array.isArray(tools) ? tools : []) {
270
+ if (tool?.type === 'function' || tool?.type === 'custom') {
271
+ if (functionsIndex == null) functionsIndex = out.length;
272
+ functions.push(tool);
273
+ continue;
274
+ }
275
+ if (tool?.type === 'namespace' && tool?.name === 'functions') {
276
+ if (functionsIndex == null) functionsIndex = out.length;
277
+ if (Array.isArray(tool.tools)) functions.push(...tool.tools);
278
+ continue;
279
+ }
280
+ out.push(tool);
281
+ }
282
+ if (functions.length) {
283
+ out.splice(functionsIndex, 0, {
284
+ type: 'namespace',
285
+ name: 'functions',
286
+ description: '',
287
+ tools: functions,
288
+ });
289
+ }
290
+ return out;
291
+ }
292
+
293
+ export function buildCodexStartupPrewarmBody(body) {
294
+ const input = Array.isArray(body?.input) ? body.input : [];
295
+ const stableInput = [];
296
+ if (input[0]?.type === 'additional_tools' && input[0]?.role === 'developer') {
297
+ stableInput.push(input[0]);
298
+ if (input[1]?.type === 'message' && input[1]?.role === 'developer') {
299
+ stableInput.push(input[1]);
300
+ }
301
+ }
302
+ return { ...body, input: stableInput, generate: false };
303
+ }
304
+
246
305
  // Effort normalization: `ultra` collapses to
247
306
  // `max` on the wire — the openai-oauth backend does not accept `ultra`. Every
248
307
  // other effort passes through unchanged; empty/unknown falls back to medium.
@@ -281,6 +340,8 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
281
340
  .join('\n\n---\n\n');
282
341
  const opts = sendOpts || {};
283
342
  const promptCacheProvider = opts.promptCacheProvider || 'openai-oauth';
343
+ const useResponsesLite = promptCacheProvider === 'openai-oauth'
344
+ && _codexModelUsesResponsesLite(model, opts);
284
345
  // Recovery-only encrypted-reasoning replay is DEFAULT ON for the OAuth
285
346
  // backend (validated 2026-08-11: smoke wire parity on normal chains +
286
347
  // live full-frame acceptance + full-run A/B). The per-socket policy in
@@ -297,6 +358,10 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
297
358
  model,
298
359
  nativeToolSearchProvider: promptCacheProvider,
299
360
  replayEncryptedReasoning,
361
+ codexWireParity: promptCacheProvider === 'openai-oauth',
362
+ codexMessageMetadata: (opts.turnId || opts.turn_id)
363
+ ? { turn_id: opts.turnId || opts.turn_id }
364
+ : {},
300
365
  });
301
366
  if (environmentText) {
302
367
  // Leading input item, after the cached prefix instead of inside it.
@@ -310,6 +375,13 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
310
375
  type: 'input_text',
311
376
  text: `<environment_context>\n${environmentText}\n</environment_context>`,
312
377
  }],
378
+ ...(promptCacheProvider === 'openai-oauth'
379
+ ? {
380
+ internal_chat_message_metadata_passthrough: (opts.turnId || opts.turn_id)
381
+ ? { turn_id: opts.turnId || opts.turn_id }
382
+ : {},
383
+ }
384
+ : {}),
313
385
  });
314
386
  }
315
387
  // Match the request body shape the OAuth backend expects so the
@@ -322,9 +394,11 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
322
394
  const value = String(item || '').trim();
323
395
  if (value && !include.includes(value)) include.push(value);
324
396
  }
397
+ const supportsReasoningSummary = _codexModelSupportsReasoningSummaries(model);
325
398
  // Field order MIRRORS the reference request struct:
326
399
  // model, instructions, input, tools, tool_choice, parallel_tool_calls,
327
- // reasoning, store, stream, include, service_tier, prompt_cache_key, text.
400
+ // reasoning, store, stream, stream_options, include, service_tier,
401
+ // prompt_cache_key, text.
328
402
  // JSON serialization order is load-bearing for the server prompt cache
329
403
  // (exact-prefix match): matching that byte layout keeps our requests on
330
404
  // the same cache-routing shape the backend warms. tools/service_tier/
@@ -346,10 +420,18 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
346
420
  // gpt-5.5. Match the observed bytes.
347
421
  reasoning: {
348
422
  effort: _normalizeReasoningEffort(opts.effort),
349
- ...(replayEncryptedReasoning ? { context: 'all_turns' } : {}),
423
+ ...(supportsReasoningSummary ? { summary: 'auto' } : {}),
424
+ ...(useResponsesLite ? { context: 'all_turns' } : {}),
350
425
  },
351
426
  store: process.env.MIXDOG_OAI_STORE === 'true' ? true : false,
352
427
  stream: true,
428
+ ...(promptCacheProvider === 'openai-oauth' && supportsReasoningSummary
429
+ ? {
430
+ stream_options: {
431
+ reasoning_summary_delivery: 'sequential_cutoff',
432
+ },
433
+ }
434
+ : {}),
353
435
  include,
354
436
  };
355
437
  const maxOutputTokens = Number(opts.maxOutputTokens ?? opts.outputTokens ?? opts.max_output_tokens);
@@ -379,6 +461,25 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
379
461
  const toolsList = (functionTools.length || nativeTools.length)
380
462
  ? [...nativeTools, ...functionTools]
381
463
  : null;
464
+ const liteTools = useResponsesLite ? _responsesLiteTools(toolsList || []) : null;
465
+ const wireInput = useResponsesLite
466
+ ? [
467
+ {
468
+ type: 'additional_tools',
469
+ role: 'developer',
470
+ tools: liteTools,
471
+ },
472
+ ...(instructions
473
+ ? [{
474
+ type: 'message',
475
+ role: 'developer',
476
+ content: [{ type: 'input_text', text: instructions }],
477
+ }]
478
+ : []),
479
+ ...input,
480
+ ]
481
+ : input;
482
+ if (useResponsesLite) body.parallel_tool_calls = false;
382
483
  const promptCacheLane = opts.promptCacheLane || resolveProviderPromptCacheLane(promptCacheProvider, opts);
383
484
  const promptCacheKey = buildStableProviderPromptCacheKey(promptCacheProvider, opts, {
384
485
  model,
@@ -400,18 +501,20 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
400
501
  : null) || 'low';
401
502
  // Rebuild the body in codex struct order so JSON serialization is
402
503
  // byte-compatible with codex: ... input, tools, tool_choice,
403
- // parallel_tool_calls, reasoning, store, stream, include, service_tier,
404
- // prompt_cache_key, text. service_tier is only present when fast set it.
504
+ // parallel_tool_calls, reasoning, store, stream, stream_options, include,
505
+ // service_tier, prompt_cache_key, text. service_tier is only present when
506
+ // fast set it.
405
507
  const ordered = {
406
508
  model: body.model,
407
- instructions: body.instructions,
408
- input: body.input,
409
- ...(toolsList ? { tools: toolsList } : {}),
509
+ ...(!useResponsesLite ? { instructions: body.instructions } : {}),
510
+ input: wireInput,
511
+ ...(!useResponsesLite && toolsList ? { tools: toolsList } : {}),
410
512
  tool_choice: body.tool_choice,
411
513
  parallel_tool_calls: body.parallel_tool_calls,
412
514
  reasoning: body.reasoning,
413
515
  store: body.store,
414
516
  stream: body.stream,
517
+ ...(body.stream_options ? { stream_options: body.stream_options } : {}),
415
518
  include: body.include,
416
519
  ...(body.service_tier ? { service_tier: body.service_tier } : {}),
417
520
  prompt_cache_key: promptCacheKey,
@@ -25,7 +25,7 @@ import {
25
25
 
26
26
  // Human-readable transport label for handshake/acquire error messages. Shared
27
27
  // with openai-oauth-ws.mjs (stream-side errors use the same labels).
28
- import { _envOn, _codexDashedId, _codexDashedIdV7, _codexBetaFeatures, _dumpHandshakeHeaders, _dumpFrame, _formatRedactedHeaders, _cfCookieHeader, _cfCookieCapture } from './openai-ws-headers.mjs';
28
+ import { _envOn, _codexBetaFeatures, _dumpHandshakeHeaders, _dumpFrame, _formatRedactedHeaders, _cfCookieHeader, _cfCookieCapture } from './openai-ws-headers.mjs';
29
29
  export function _wsErrLabel(p) {
30
30
  if (p === 'xai') return 'xAI WS';
31
31
  if (p === 'openai-direct' || p === 'openai') return 'OpenAI WS';
@@ -410,24 +410,15 @@ function _buildHandshakeHeaders({ auth, sessionToken, turnState, cacheKey: _cach
410
410
  'x-codex-beta-features': _codexBetaFeatures(),
411
411
  };
412
412
  const isOpenAiOauth = auth.type !== 'xai' && auth.type !== 'openai-direct';
413
- // Default mode preserves the measured Mixdog underscore session_id cache
414
- // key. Strict wire parity instead follows Codex exactly: dashed
415
- // session-id/thread-id only, UUIDv7-shaped ids, and x-client-request-id.
416
- const wireParity = process.env.MIXDOG_OAI_CODEX_WIRE_PARITY === '1';
417
- if (sessionToken && !wireParity) {
418
- headers['session_id'] = String(sessionToken);
419
- }
420
413
  if (isOpenAiOauth && (sessionToken || _cacheKey)) {
421
- const uuidIds = wireParity;
422
- const shapeId = wireParity ? _codexDashedIdV7 : _codexDashedId;
423
- // Underscore `session_id` above is left as-is (backend prefix-dedupe
424
- // key); only the dashed codex pair is reshaped under the opt-in.
425
- const threadId = uuidIds ? shapeId(String(_cacheKey || sessionToken)) : String(_cacheKey || sessionToken);
426
- const sessionId = uuidIds ? shapeId(String(sessionToken || _cacheKey)) : String(sessionToken || _cacheKey);
414
+ // Codex uses one caller-owned UUIDv7 identity everywhere. Prefer the
415
+ // metadata projection from the session; prompt_cache_key/sessionToken
416
+ // is the exact same UUID and remains a defensive fallback.
417
+ const sessionId = String(codexHeaders?.['session-id'] || sessionToken || _cacheKey);
418
+ const threadId = String(codexHeaders?.['thread-id'] || _cacheKey || sessionId);
427
419
  headers['session-id'] = sessionId;
428
420
  headers['thread-id'] = threadId;
429
- // The reference client sets x-client-request-id to the dashed thread id.
430
- headers['x-client-request-id'] = threadId;
421
+ headers['x-client-request-id'] = String(codexHeaders?.['x-client-request-id'] || threadId);
431
422
  if (codexHeaders && typeof codexHeaders === 'object') {
432
423
  for (const [key, value] of Object.entries(codexHeaders)) {
433
424
  if (typeof key === 'string' && typeof value === 'string' && value) {
@@ -435,17 +426,6 @@ function _buildHandshakeHeaders({ auth, sessionToken, turnState, cacheKey: _cach
435
426
  }
436
427
  }
437
428
  }
438
- // Reshaped-id realignment: under wire parity the thread-id above becomes
439
- // a Codex UUIDv7 (shapeId), while _codexWsCompatibilityHeaders derives
440
- // x-codex-parent-thread-id from the RAW dashed thread_id — the merged
441
- // codexHeaders would then carry a parent-thread id that no longer
442
- // matches the thread-id on the same handshake. Realign it to the
443
- // reshaped value (single source of truth: the pool owns the wire id
444
- // shape). With no reshaping there is no mismatch, so codex's raw value
445
- // stays untouched.
446
- if (uuidIds && headers['x-codex-parent-thread-id']) {
447
- headers['x-codex-parent-thread-id'] = threadId;
448
- }
449
429
  } else {
450
430
  // xAI/direct keep a per-request value so their server-side traces stay
451
431
  // distinguishable across reconnects.
@@ -698,6 +698,17 @@ function _classifyMidstreamWs(err, state, attemptIndex, policy) {
698
698
  if (err?.wsSendFailed || state.wsSendFailed) {
699
699
  return _allowMidstream('ws_send_failed', attemptIndex, policy)
700
700
  }
701
+ // The Responses WebSocket emits its hard 60-minute retirement as a top-level
702
+ // `event.error`, not `response.failed`. openai-ws-stream preserves that typed
703
+ // object on err.payload; recognize only the documented code here so unknown
704
+ // pre-response error events remain terminal rather than becoming text-based
705
+ // retry guesses.
706
+ for (const field of [err?.payload?.code, err?.payload?.type]) {
707
+ const key = typeof field === 'string' ? field.trim().toLowerCase() : ''
708
+ if (key === WEBSOCKET_CONNECTION_LIMIT) {
709
+ return _allowMidstream('websocket_connection_limit', attemptIndex, policy)
710
+ }
711
+ }
701
712
  // Stall / local-close-4000 must be classified as RETRYABLE before the
702
713
  // pre-`response.created` deny gate below. A first-meaningful-frame timeout
703
714
  // fires with sawResponseCreated=false + close 4000 + StreamStalledError, so