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.
- package/package.json +1 -1
- package/src/rules/shared/30-exploration.md +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +32 -9
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +78 -33
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +21 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +92 -29
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +220 -50
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +112 -9
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +7 -27
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +11 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +43 -5
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +2 -2
- package/src/runtime/shared/pristine-execution-contract.json +0 -1
- package/src/session-runtime/session-lifecycle.mjs +9 -0
- package/src/standalone/agent-tool/spawn-flow.mjs +4 -4
|
@@ -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 {
|
|
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 {
|
|
82
|
-
|
|
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
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
//
|
|
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) =>
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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
|
-
?
|
|
734
|
+
? buildCodexStartupPrewarmBody(body)
|
|
682
735
|
: null,
|
|
683
|
-
|
|
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
|
-
*
|
|
786
|
-
*
|
|
787
|
-
*
|
|
788
|
-
*
|
|
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
|
|
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
|
-
|
|
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: {
|
|
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 =
|
|
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
|
-
? {
|
|
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,
|
|
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
|
-
...(
|
|
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,
|
|
404
|
-
// prompt_cache_key, text. service_tier is only present when
|
|
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:
|
|
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,
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
//
|
|
424
|
-
|
|
425
|
-
const threadId =
|
|
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
|
-
|
|
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
|