@xiaoso/dsh-tool-plus 0.1.0-beta.2 → 0.1.0-beta.4
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/README.en.md +122 -0
- package/README.md +3 -2
- package/THIRD_PARTY_NOTICES.md +4 -35
- package/lib/abort-B7izMMna.mjs +51 -0
- package/lib/abort-D1_Z5Ut5.mjs +51 -0
- package/lib/abort-dIkZBV7Q.mjs +51 -0
- package/lib/amazon-bedrock-B0nxT06F.mjs +917 -0
- package/lib/amazon-bedrock-D-shAr1X.mjs +917 -0
- package/lib/amazon-bedrock-WQhvjHYu.mjs +917 -0
- package/lib/anthropic-BWsZFzU8.mjs +262 -0
- package/lib/anthropic-CJCauvYz.mjs +3115 -0
- package/lib/anthropic-CMT1Qj1t.mjs +3115 -0
- package/lib/anthropic-CZghe_9U.mjs +3115 -0
- package/lib/anthropic-sCMH7JID.mjs +262 -0
- package/lib/anthropic-tTntD8BD.mjs +262 -0
- package/lib/api-key-login-BzqNdM2y.mjs +157 -0
- package/lib/api-key-login-CWVwvHT1.mjs +157 -0
- package/lib/api-key-login-CpzSezsi.mjs +157 -0
- package/lib/aws-credentials-8Mgn5MAy.mjs +867 -0
- package/lib/aws-credentials-CXryMobh.mjs +867 -0
- package/lib/aws-credentials-ChG79ey-.mjs +867 -0
- package/lib/azure-openai-responses-CNkHiLUh.mjs +274 -0
- package/lib/azure-openai-responses-Cgmc-rbL.mjs +274 -0
- package/lib/azure-openai-responses-DuukbD0F.mjs +274 -0
- package/lib/bun-shim.mjs +252 -12
- package/lib/bundled-references-CSuBco5p.mjs +55 -0
- package/lib/bundled-references-Cavaffah.mjs +55 -0
- package/lib/bundled-references-sLN0YdXU.mjs +55 -0
- package/lib/callback-server-BEFNf-jf.mjs +398 -0
- package/lib/callback-server-CQ5Jo8F-.mjs +398 -0
- package/lib/callback-server-DDSrZ6l_.mjs +398 -0
- package/lib/client.js +111 -41
- package/lib/cursor-B3zg7EHj.mjs +273 -0
- package/lib/cursor-B6TUEmgP.mjs +135 -0
- package/lib/cursor-BIQfBC6V.mjs +135 -0
- package/lib/cursor-D8gvtTL3.mjs +135 -0
- package/lib/cursor-DNk6eXDd.mjs +5470 -0
- package/lib/cursor-DkSX2h3H.mjs +5441 -0
- package/lib/cursor-MjSm74IJ.mjs +273 -0
- package/lib/cursor-aJrXeYCb.mjs +273 -0
- package/lib/cursor-mJQwUKXK.mjs +5441 -0
- package/lib/device-code-DBo3B5nh.mjs +51 -0
- package/lib/device-code-DIb0FU-H.mjs +51 -0
- package/lib/device-code-OI_tQcmc.mjs +51 -0
- package/lib/devin-BHZZyMhO.mjs +95 -0
- package/lib/devin-BK7DHeca.mjs +114 -0
- package/lib/devin-CfMp3cXS.mjs +95 -0
- package/lib/devin-CldyPD8K.mjs +95 -0
- package/lib/devin-CzRpTUOA.mjs +114 -0
- package/lib/devin-D9zR5B_R.mjs +542 -0
- package/lib/devin-DXOeNyU7.mjs +542 -0
- package/lib/devin-Q1yem3lp.mjs +114 -0
- package/lib/devin-vNZTNUON.mjs +542 -0
- package/lib/error-B3bevhrs.mjs +1449 -0
- package/lib/error-Dd8DIqs-.mjs +1449 -0
- package/lib/error-yCSG0mGC.mjs +1449 -0
- package/lib/event-stream-BDPi64mK.mjs +151 -0
- package/lib/event-stream-BbFZJy-z.mjs +151 -0
- package/lib/event-stream-loUa0s1T.mjs +151 -0
- package/lib/fs-promises-shim-Bx0fpxuk.mjs +57 -0
- package/lib/github-copilot-4GaMtQmg.mjs +208 -0
- package/lib/github-copilot-B1yiyVRh.mjs +104 -0
- package/lib/github-copilot-B2HGSIJK.mjs +104 -0
- package/lib/github-copilot-BWT3eAOF.mjs +208 -0
- package/lib/github-copilot-CYkjeTi3.mjs +208 -0
- package/lib/github-copilot-jscUZ8Hf.mjs +104 -0
- package/lib/gitlab-duo-CM7Ref1H.mjs +155 -0
- package/lib/gitlab-duo-akICyfeD.mjs +155 -0
- package/lib/gitlab-duo-gubO4FNw.mjs +155 -0
- package/lib/gitlab-duo-workflow-BGAF6JDw.mjs +91 -0
- package/lib/gitlab-duo-workflow-CSEgYk3N.mjs +91 -0
- package/lib/gitlab-duo-workflow-D78tdn8E.mjs +91 -0
- package/lib/google-BS4T25Pn.mjs +30 -0
- package/lib/google-TFe-7-of.mjs +30 -0
- package/lib/google-antigravity-DGmwC2KV.mjs +198 -0
- package/lib/google-antigravity-DNYhgTpA.mjs +198 -0
- package/lib/google-antigravity-Dlkdhahq.mjs +198 -0
- package/lib/google-auth-B6gjsZvg.mjs +232 -0
- package/lib/google-auth-De7_s3eo.mjs +232 -0
- package/lib/google-auth-DhqpOYrX.mjs +232 -0
- package/lib/google-gemini-cli-BG_LRdIV.mjs +850 -0
- package/lib/google-gemini-cli-CWu8HvXW.mjs +211 -0
- package/lib/google-gemini-cli-Cb97bzQc.mjs +850 -0
- package/lib/google-gemini-cli-Cqds_Q5T.mjs +850 -0
- package/lib/google-gemini-cli-Dsztq0N5.mjs +211 -0
- package/lib/google-gemini-cli-zJhiOcND.mjs +211 -0
- package/lib/google-oauth-shared-CuEHaDDj.mjs +147 -0
- package/lib/google-oauth-shared-CwNWYdmR.mjs +147 -0
- package/lib/google-oauth-shared-yqlyqPPC.mjs +147 -0
- package/lib/google-shared-CgKmmdO3.mjs +760 -0
- package/lib/google-shared-Dr-VG-ap.mjs +760 -0
- package/lib/google-shared-DzhCb9b0.mjs +760 -0
- package/lib/google-vertex-BDN2tLaD.mjs +99 -0
- package/lib/google-vertex-DopVOQOG.mjs +99 -0
- package/lib/google-vertex-kscbkyRb.mjs +99 -0
- package/lib/google-zyDOG9x7.mjs +30 -0
- package/lib/identity-Da1GPU9a.mjs +219 -0
- package/lib/identity-DnB63pS7.mjs +219 -0
- package/lib/identity-xRP7_eSR.mjs +219 -0
- package/lib/idle-iterator-Baoyn3Hp.mjs +366 -0
- package/lib/idle-iterator-C6XbWcuB.mjs +366 -0
- package/lib/idle-iterator-DUjKQEGT.mjs +366 -0
- package/lib/index.mjs +4608 -3935
- package/lib/kimi--wUOWER5.mjs +236 -0
- package/lib/kimi-DMVI-zrZ.mjs +236 -0
- package/lib/kimi-Dh0pv3WZ.mjs +236 -0
- package/lib/markit-BL3cH47U.mjs +1417 -0
- package/lib/markit-BNiNY6SE.mjs +1417 -0
- package/lib/markit-M0jhziqU.mjs +1417 -0
- package/lib/minimax-code-BK-3eBbz.mjs +53 -0
- package/lib/minimax-code-DCbuA6Yk.mjs +53 -0
- package/lib/minimax-code-Uq8Qu9LW.mjs +53 -0
- package/lib/models-CfT4coYT.mjs +104660 -0
- package/lib/models-D_XnT6ay.mjs +104660 -0
- package/lib/models-SGASygJ3.mjs +104660 -0
- package/lib/ollama-BQMGPxtV.mjs +589 -0
- package/lib/ollama-DPzBmq89.mjs +589 -0
- package/lib/ollama-DoBz5SFm.mjs +589 -0
- package/lib/openai-codex-Bi-4S4PX.mjs +255 -0
- package/lib/openai-codex-CQL_kvYL.mjs +255 -0
- package/lib/openai-codex-Ch8CkT3p.mjs +255 -0
- package/lib/openai-codex-responses-CvxL_FMF.mjs +3144 -0
- package/lib/openai-codex-responses-Cz-2qpC7.mjs +3144 -0
- package/lib/openai-codex-responses-pejZgdZo.mjs +3144 -0
- package/lib/openai-completions-CF8IyNtP.mjs +1360 -0
- package/lib/openai-completions-DIf361kF.mjs +1360 -0
- package/lib/openai-completions-i8doczAk.mjs +1360 -0
- package/lib/openai-reasoning-fallback-DGVVLgd1.mjs +286 -0
- package/lib/openai-reasoning-fallback-Nj6yB2rY.mjs +286 -0
- package/lib/openai-reasoning-fallback-OdJREP1o.mjs +286 -0
- package/lib/openai-responses-BqXJInHX.mjs +867 -0
- package/lib/openai-responses-DTkBiiy-.mjs +867 -0
- package/lib/openai-responses-Db_a-U4-.mjs +867 -0
- package/lib/opencode-DaJjcUbf.mjs +38 -0
- package/lib/opencode-DhMolSoB.mjs +38 -0
- package/lib/opencode-Rq_Q2vAO.mjs +38 -0
- package/lib/perplexity-9b-RQ0mV.mjs +194 -0
- package/lib/perplexity-C4iLVMUj.mjs +194 -0
- package/lib/perplexity-Uu7Jzg5b.mjs +194 -0
- package/lib/proxy-DDqrmjjC.mjs +190 -0
- package/lib/proxy-ZtUcl6r5.mjs +190 -0
- package/lib/proxy-pM_NIv-H.mjs +190 -0
- package/lib/request-debug-HJMoJncL.mjs +183 -0
- package/lib/sanitize-text-CfcXzw5X.mjs +33 -0
- package/lib/scrapers-BkFfcPHj.mjs +9198 -0
- package/lib/scrapers-CL-WgdU1.mjs +9198 -0
- package/lib/scrapers-CVptboMs.mjs +9198 -0
- package/lib/scrapers-CipyKyCH.mjs +9198 -0
- package/lib/scrapers-DGfrYhtw.mjs +9198 -0
- package/lib/scrapers-DSUPYoUF.mjs +9198 -0
- package/lib/scrapers-DZ2GXmp7.mjs +9198 -0
- package/lib/scrapers-nhQAvc8d.mjs +9198 -0
- package/lib/shell-snapshot-fn-env.sh +67 -43
- package/lib/src-BgpusnsB.mjs +5076 -0
- package/lib/src-DTid0g4K.mjs +5001 -0
- package/lib/src-lDmkSRbm.mjs +5006 -0
- package/lib/stream-BlldLkVq.mjs +10236 -0
- package/lib/stream-CRtexYLe.mjs +10236 -0
- package/lib/stream-C_RAoxJC.mjs +10236 -0
- package/lib/stream-markup-healing-2toHXWXM.mjs +491 -0
- package/lib/stream-markup-healing-DMsxBx1L.mjs +491 -0
- package/lib/stream-markup-healing-KMkiAA00.mjs +491 -0
- package/lib/turndown-DN7ByzEI.mjs +667 -0
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/config/fields.d.ts +1 -1
- package/lib/types/config/settings.d.ts +25 -3
- package/lib/types/config/truncate.d.ts +9 -5
- package/lib/types/tools/bash/background.d.ts +1 -1
- package/lib/types/tools/bash/bash-executor.d.ts +4 -0
- package/lib/types/tools/bash/bash-interceptor.d.ts +10 -8
- package/lib/types/tools/bash/cd-workdir.d.ts +35 -0
- package/lib/types/tools/bash/non-interactive-env.d.ts +2 -0
- package/lib/types/tools/bash/render.d.ts +4 -3
- package/lib/types/tools/bash/sanitize-text.d.ts +5 -0
- package/lib/types/tools/bash/shell-snapshot.d.ts +4 -1
- package/lib/types/tools/bash/shell-tokenize.d.ts +60 -0
- package/lib/types/tools/bash/streaming-output.d.ts +12 -212
- package/lib/types/tools/bash/which.d.ts +20 -0
- package/lib/types/tools/bash/windows-shell.d.ts +31 -0
- package/lib/types/tools/edit/adapter/index.d.ts +25 -3
- package/lib/types/tools/edit/adapter/tools/auto-generated-guard.d.ts +7 -17
- package/lib/types/tools/glob/adapter/index.d.ts +27 -1
- package/lib/types/tools/grep/adapter/index.d.ts +29 -1
- package/lib/types/tools/omp/edit/dispatch.d.ts +43 -0
- package/lib/types/tools/omp/edit/hashline/noop-loop-guard.d.ts +3 -1
- package/lib/types/tools/omp/sdk.d.ts +6 -0
- package/lib/types/tools/omp/tools/auto-generated-guard.d.ts +20 -0
- package/lib/types/tools/omp/tools/fetch.d.ts +2 -0
- package/lib/types/tools/omp/tools/read.d.ts +2 -8
- package/lib/types/tools/omp/tools/write.d.ts +4 -4
- package/lib/types/tools/omp/utils/image-loading.d.ts +10 -36
- package/lib/types/tools/read/adapter/index.d.ts +4 -1
- package/lib/types/tools/read-image/index.d.ts +4 -0
- package/lib/types/tools/shared/auto-generated-guard.dsh.d.ts +16 -4
- package/lib/types/tools/shared/bun-shim.d.ts +39 -3
- package/lib/types/tools/shared/fs-promises-shim.d.ts +3 -1
- package/lib/types/tools/shared/image-bridge.d.ts +98 -0
- package/lib/types/tools/shared/omp-prompt.d.ts +17 -9
- package/lib/types/tools/shared/session-state.d.ts +9 -3
- package/lib/utils-2Wq2eATG.mjs +8119 -0
- package/lib/utils-BnX8O3vi.mjs +12303 -0
- package/lib/utils-Bw139LTz.mjs +8119 -0
- package/lib/utils-CE8N8P4Q.mjs +12315 -0
- package/lib/utils-CZw4tVZQ.mjs +12315 -0
- package/lib/utils-C_MrQ_0z.mjs +12301 -0
- package/lib/utils-CiGN7OHR.mjs +12315 -0
- package/lib/utils-Cp76gCyk.mjs +8119 -0
- package/lib/utils-D0GRDgAr.mjs +12315 -0
- package/lib/utils-D0Z1fyoO.mjs +12299 -0
- package/lib/utils-D2YknRk4.mjs +51 -0
- package/lib/utils-DppGHXQB.mjs +51 -0
- package/lib/utils-DzXZDfYc.mjs +51 -0
- package/lib/utils-W8WdSBpX.mjs +12315 -0
- package/lib/vision-guard-01qSwFpf.mjs +92 -0
- package/lib/vision-guard-BDD4OrKo.mjs +92 -0
- package/lib/vision-guard-DzZGaOga.mjs +92 -0
- package/lib/wafer-CgtPHerf.mjs +17 -0
- package/lib/wafer-DRU0EjZX.mjs +17 -0
- package/lib/wafer-cBtrn6q9.mjs +17 -0
- package/lib/xai-oauth-Buk8XVvM.mjs +446 -0
- package/lib/xai-oauth-CMal8l1O.mjs +446 -0
- package/lib/xai-oauth-DPHDXKD7.mjs +446 -0
- package/lib/xiaomi-CZgwvX2G.mjs +157 -0
- package/lib/xiaomi-D2qT5cAC.mjs +157 -0
- package/lib/xiaomi-Dd87KSg5.mjs +157 -0
- package/lib/zai-7DWMkEqM.mjs +203 -0
- package/lib/zai-C59YnomG.mjs +203 -0
- package/lib/zai-CFeOt-53.mjs +203 -0
- package/lib/zip-Cg9ANN_U.mjs +1316 -0
- package/lib/zip-D36ZKsP8.mjs +1316 -0
- package/lib/zip-D6QXpvaJ.mjs +1316 -0
- package/package.json +14 -10
- package/presets/package.json +1 -1
- package/presets/tool-plus-ptc/preset.yml +1 -1
- package/presets/tool-plus-standard/preset.yml +1 -1
|
@@ -0,0 +1,3144 @@
|
|
|
1
|
+
import { installBunShim as __installBunShim } from "./bun-shim.mjs";
|
|
2
|
+
__installBunShim();
|
|
3
|
+
import { r as __exportAll } from "./fs-promises-shim-Bx0fpxuk.mjs";
|
|
4
|
+
import { B as USER_AGENT, C as error, I as $flag, P as $env, S as debug, g as parseStreamingJson, o as readSseJson, q as getInstallId, t as structuredCloneJSON, w as time } from "./src-BgpusnsB.mjs";
|
|
5
|
+
import { G as fetchWithRetry, V as ProviderHttpError, g as MissingApiKeyError, l as finalize, v as AbortError } from "./error-Dd8DIqs-.mjs";
|
|
6
|
+
import { A as NO_STRICT, C as kStreamingPartialJson, L as sanitizeSchemaForOpenAIResponses, a as normalizeSystemPrompts, c as sanitizeOpenAIResponsesAssistantFallbackItemsForReplay, d as stripOpenAIResponsesComputerLinkedReasoningIdsForReplay, h as clearStreamingPartialJson, j as adaptSchemaForStrict, k as toolWireSchema, l as sanitizeOpenAIResponsesAssistantHistoryItemsForReplay, m as transformMessages, n as getOpenAIResponsesHistoryItems, p as redactSensitiveInObject, r as getOpenAIResponsesHistoryPayload, t as createOpenAIResponsesHistoryPayload } from "./utils-Cp76gCyk.mjs";
|
|
7
|
+
import { t as asRecord } from "./type-guards-B9VVjRDK.mjs";
|
|
8
|
+
import { A as computerCallMetadata, B as finalizeCustomToolCallInputDone, E as buildResponsesDeltaInput, F as createSequentialCutoffSummaryState, H as finalizePendingResponsesToolCalls, K as getOpenAIPromptCacheKey, L as encodeResponsesToolCallId, M as convertResponsesInputContent, Q as isOpenAIResponsesProgressEvent, R as encodeTextSignatureV1, U as finalizeReasoningThinking, V as finalizeMessageText, W as finalizeToolCallArgumentsDone, X as hasExecutableIncompleteResponsesToolCalls, a as accumulateCustomToolCallInputDelta, at as populateResponsesUsageFromResponse, bt as escapeHarmonyControlTokens, c as appendMessageTextDelta, d as appendReasoningSummaryTextDelta, f as appendResponsesImageResult, j as convertResponsesAssistantMessage, l as appendReasoningSummaryPart, n as getEnvApiKey, nt as mapOpenAIResponsesStopReason, o as accumulateToolCallArgumentsDelta, p as appendResponsesToolResultMessages, r as isOfficialCodexApiUrl, rt as normalizeOpenAIPromptCacheKey, s as appendMessageContentPart, st as promoteResponsesToolUseStopReason, tt as mapOpenAIReasoningEffort, u as appendReasoningSummaryPartDone, x as applyReasoningSummaryDone, xt as isHarmonyDialectModel, y as applyOpenAIServiceTier, z as escapeReplayedControlTokens } from "./stream-CRtexYLe.mjs";
|
|
9
|
+
import { r as getProxyForUrl } from "./proxy-pM_NIv-H.mjs";
|
|
10
|
+
import { E as supportsCodexReasoningSummary, T as supportsAllTurnsReasoningContext, s as requireSupportedEffort, t as calculateCost } from "./models-SGASygJ3.mjs";
|
|
11
|
+
import { a as toNumber } from "./utils-D2YknRk4.mjs";
|
|
12
|
+
import "./identity-Da1GPU9a.mjs";
|
|
13
|
+
import { t as AssistantMessageEventStream } from "./event-stream-BbFZJy-z.mjs";
|
|
14
|
+
import { a as getCodexAccountId, i as OPENAI_HEADER_VALUES, n as CODEX_CLIENT_VERSION, r as OPENAI_HEADERS, t as CODEX_BASE_URL } from "./codex-P1dGpX7G.mjs";
|
|
15
|
+
import { n as getOpenAIStreamFirstEventTimeoutMs, o as iterateWithIdleTimeout, r as getOpenAIStreamIdleTimeoutMs, t as armPreResponseTimeout } from "./idle-iterator-DUjKQEGT.mjs";
|
|
16
|
+
import { n as isRequestDebugEnabled, t as createRequestDebugSession } from "./request-debug-HJMoJncL.mjs";
|
|
17
|
+
import { t as notifyRawSseEvent } from "./sse-debug-CYdjOC96.mjs";
|
|
18
|
+
import { t as compactGrammarDefinition } from "./grammar-D-NqNUxQ.mjs";
|
|
19
|
+
import { scheduler } from "node:timers/promises";
|
|
20
|
+
import { type } from "@oh-my-pi/omptype";
|
|
21
|
+
//#region node_modules/.pnpm/@oh-my-pi+pi-ai@17.3.5/node_modules/@oh-my-pi/pi-ai/src/providers/openai-codex/request-transformer.ts
|
|
22
|
+
/** Caller literal → catalog `Effort` bridge (the enum is nominal). */
|
|
23
|
+
const EFFORT_BY_NAME = {
|
|
24
|
+
minimal: "minimal",
|
|
25
|
+
low: "low",
|
|
26
|
+
medium: "medium",
|
|
27
|
+
high: "high",
|
|
28
|
+
xhigh: "xhigh",
|
|
29
|
+
max: "max"
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Resolve whether a Codex request uses the Responses Lite transport: an
|
|
33
|
+
* explicit option wins, then the `PI_CODEX_RESPONSES_LITE` env override
|
|
34
|
+
* (`1`/`true` forces Lite, `0`/`false` forces the full Responses body),
|
|
35
|
+
* otherwise the model's catalog flag (codex-rs `model_info.use_responses_lite`)
|
|
36
|
+
* decides.
|
|
37
|
+
*/
|
|
38
|
+
function resolveCodexResponsesLite(model, requested) {
|
|
39
|
+
if (requested !== void 0) return requested;
|
|
40
|
+
const env = $env.PI_CODEX_RESPONSES_LITE?.trim().toLowerCase();
|
|
41
|
+
if (env === "1" || env === "true") return true;
|
|
42
|
+
if (env === "0" || env === "false") return false;
|
|
43
|
+
return model.useResponsesLite === true;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether to request `stream_options.reasoning_summary_delivery =
|
|
47
|
+
* "sequential_cutoff"` (codex-rs `concurrent_reasoning_summaries`), enabled by
|
|
48
|
+
* `PI_CODEX_CONCURRENT_SUMMARIES=1`.
|
|
49
|
+
*
|
|
50
|
+
* Off by default because the mode cancels summary sections still in flight when
|
|
51
|
+
* the reasoning item closes: measured over 12 interleaved turns it halved
|
|
52
|
+
* visible thinking (0.83 vs 1.67 summary parts, 37 vs 69 chars per turn) and
|
|
53
|
+
* produced no summary at all on 3 of 12 turns. codex-rs ships it disabled too
|
|
54
|
+
* (`Stage::UnderDevelopment`, `default_enabled: false`).
|
|
55
|
+
*/
|
|
56
|
+
function concurrentSummariesEnabled() {
|
|
57
|
+
const env = $env.PI_CODEX_CONCURRENT_SUMMARIES?.trim().toLowerCase();
|
|
58
|
+
return env === "1" || env === "true";
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Clamp a user-facing effort to the model's ladder, then remap to the wire
|
|
62
|
+
* tier. User efforts map 1:1 onto wire tiers; the effort map only covers
|
|
63
|
+
* host quirks where a wire tier genuinely does not exist (e.g. `minimal→none`).
|
|
64
|
+
* A mapped value outside the Codex wire vocabulary is a broken compat/model
|
|
65
|
+
* effort map — fail loudly rather than silently sending a different tier.
|
|
66
|
+
*/
|
|
67
|
+
function mapCodexWireEffort(model, effort) {
|
|
68
|
+
const mapped = mapOpenAIReasoningEffort(model, model.compat, requireSupportedEffort(model, EFFORT_BY_NAME[effort]));
|
|
69
|
+
switch (mapped) {
|
|
70
|
+
case "none":
|
|
71
|
+
case "minimal":
|
|
72
|
+
case "low":
|
|
73
|
+
case "medium":
|
|
74
|
+
case "high":
|
|
75
|
+
case "xhigh":
|
|
76
|
+
case "max": return mapped;
|
|
77
|
+
default: throw new Error(`Effort map for ${model.provider}/${model.id} produced invalid Codex reasoning effort "${mapped}"`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function getReasoningConfig(model, effort, options) {
|
|
81
|
+
const config = { effort: effort === "none" ? "none" : mapCodexWireEffort(model, effort) };
|
|
82
|
+
if (options.reasoningSummary !== null && supportsCodexReasoningSummary(model.id)) config.summary = options.reasoningSummary ?? "auto";
|
|
83
|
+
return config;
|
|
84
|
+
}
|
|
85
|
+
function filterInput(input) {
|
|
86
|
+
if (!Array.isArray(input)) return input;
|
|
87
|
+
return input.filter((item) => item.type !== "item_reference").map((item) => {
|
|
88
|
+
if (item.type === "computer_call") return item;
|
|
89
|
+
if (item.id != null) {
|
|
90
|
+
const { id: _id, ...rest } = item;
|
|
91
|
+
return rest;
|
|
92
|
+
}
|
|
93
|
+
return item;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const CODEX_ORPHAN_OUTPUT_LIMIT = 16e3;
|
|
97
|
+
/** Placeholder output for a tool call whose result never landed in the input. */
|
|
98
|
+
const CODEX_INTERRUPTED_TOOL_OUTPUT = "[No tool output recorded: the tool call was interrupted before it produced a result.]";
|
|
99
|
+
function orphanFunctionOutputToMessage(item, callId) {
|
|
100
|
+
const itemRecord = item;
|
|
101
|
+
const toolName = typeof itemRecord.name === "string" ? itemRecord.name : "tool";
|
|
102
|
+
let text = "";
|
|
103
|
+
try {
|
|
104
|
+
const output = itemRecord.output;
|
|
105
|
+
text = typeof output === "string" ? output : JSON.stringify(output);
|
|
106
|
+
} catch {
|
|
107
|
+
text = String(itemRecord.output ?? "");
|
|
108
|
+
}
|
|
109
|
+
if (text.length > CODEX_ORPHAN_OUTPUT_LIMIT) text = `${text.slice(0, CODEX_ORPHAN_OUTPUT_LIMIT)}\n...[truncated]`;
|
|
110
|
+
return {
|
|
111
|
+
type: "message",
|
|
112
|
+
role: "assistant",
|
|
113
|
+
content: `[Previous ${toolName} result; call_id=${callId}]: ${text}`
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function toolCallKind(type) {
|
|
117
|
+
if (type === "function_call") return "function";
|
|
118
|
+
if (type === "custom_tool_call") return "custom";
|
|
119
|
+
if (type === "computer_call") return "computer";
|
|
120
|
+
}
|
|
121
|
+
function toolOutputKind(type) {
|
|
122
|
+
if (type === "function_call_output") return "function";
|
|
123
|
+
if (type === "custom_tool_call_output") return "custom";
|
|
124
|
+
if (type === "computer_call_output") return "computer";
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Repair both halves of unpaired tool exchanges so the Responses input grammar
|
|
128
|
+
* stays valid — the API rejects either orphan with a 400:
|
|
129
|
+
*
|
|
130
|
+
* - `function_call_output` / `custom_tool_call_output` with no matching call →
|
|
131
|
+
* folded into an assistant message (`400 No tool call found for … output`).
|
|
132
|
+
* Regression of #472 / #1351.
|
|
133
|
+
* - `function_call` / `custom_tool_call` with no matching `*_output` → a
|
|
134
|
+
* placeholder output is synthesized immediately after the call
|
|
135
|
+
* (`400 No tool output found for function call …`). Hit when the user
|
|
136
|
+
* branches/navigates the session tree to a node that ends on a tool call (the
|
|
137
|
+
* tool-result child is dropped from the reconstructed history) or when a turn
|
|
138
|
+
* is aborted/crashes after the call streamed but before its result persisted.
|
|
139
|
+
*/
|
|
140
|
+
function repairToolCallPairs(input) {
|
|
141
|
+
const callKinds = /* @__PURE__ */ new Map();
|
|
142
|
+
const outputKinds = /* @__PURE__ */ new Map();
|
|
143
|
+
for (const item of input) {
|
|
144
|
+
const callId = typeof item.call_id === "string" ? item.call_id : void 0;
|
|
145
|
+
if (callId === void 0) continue;
|
|
146
|
+
const callKind = toolCallKind(item.type);
|
|
147
|
+
const outputKind = toolOutputKind(item.type);
|
|
148
|
+
if (callKind) callKinds.set(callId, callKind);
|
|
149
|
+
if (outputKind) outputKinds.set(callId, outputKind);
|
|
150
|
+
}
|
|
151
|
+
const repaired = [];
|
|
152
|
+
for (const item of input) {
|
|
153
|
+
const callId = typeof item.call_id === "string" ? item.call_id : void 0;
|
|
154
|
+
const callKind = toolCallKind(item.type);
|
|
155
|
+
const outputKind = toolOutputKind(item.type);
|
|
156
|
+
if (outputKind && callId !== void 0 && callKinds.get(callId) !== outputKind) {
|
|
157
|
+
repaired.push(orphanFunctionOutputToMessage(item, callId));
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (callKind && callId !== void 0 && outputKinds.get(callId) !== callKind) {
|
|
161
|
+
if (callKind === "computer") {
|
|
162
|
+
repaired.push({
|
|
163
|
+
type: "message",
|
|
164
|
+
role: "assistant",
|
|
165
|
+
content: `[Computer call interrupted before a screenshot was recorded; call_id=${callId}]`
|
|
166
|
+
});
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
repaired.push(item, {
|
|
170
|
+
type: callKind === "custom" ? "custom_tool_call_output" : "function_call_output",
|
|
171
|
+
call_id: callId,
|
|
172
|
+
output: CODEX_INTERRUPTED_TOOL_OUTPUT
|
|
173
|
+
});
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
repaired.push(item);
|
|
177
|
+
}
|
|
178
|
+
return repaired;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Responses Lite requests must not pin image detail levels: codex-rs strips
|
|
182
|
+
* `detail` from every input image (message content and tool outputs) before
|
|
183
|
+
* sending, letting the server choose.
|
|
184
|
+
*/
|
|
185
|
+
function stripImageDetails(input) {
|
|
186
|
+
for (const item of input) {
|
|
187
|
+
if (!item || typeof item !== "object") continue;
|
|
188
|
+
const content = "content" in item ? item.content : void 0;
|
|
189
|
+
const output = "output" in item ? item.output : void 0;
|
|
190
|
+
for (const collection of [content, output]) {
|
|
191
|
+
if (!Array.isArray(collection)) continue;
|
|
192
|
+
for (const part of collection) {
|
|
193
|
+
if (!part || typeof part !== "object") continue;
|
|
194
|
+
if (!("type" in part) || part.type !== "input_image") continue;
|
|
195
|
+
if ("detail" in part) part.detail = void 0;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Applies the Responses Lite body contract in place (codex-rs
|
|
202
|
+
* `build_responses_request` with `use_responses_lite`): strips pinned image
|
|
203
|
+
* detail, forces parallel tool calling off, moves tools into a leading
|
|
204
|
+
* `additional_tools` developer item and the base instructions into a
|
|
205
|
+
* developer message, then omits top-level `instructions`/`tools`. Because the
|
|
206
|
+
* rewrite removes top-level `tools`, a forced hosted-tool choice (e.g.
|
|
207
|
+
* `{ type: "web_search" }`) would leave the backend unable to validate the
|
|
208
|
+
* choice against a tools collection and it rejects the request with HTTP 400
|
|
209
|
+
* (#5771). Native computer and named-function choices preserve exact forcing
|
|
210
|
+
* by isolating the selected declaration and using `"required"`; other hosted
|
|
211
|
+
* choices fall back to `"auto"`. Explicit string constraints such as `"none"`
|
|
212
|
+
* and `"required"` remain valid. Shared by normal turns and both remote-compaction
|
|
213
|
+
* paths — codex-rs routes `/responses/compact` through the same builder.
|
|
214
|
+
*/
|
|
215
|
+
function applyCodexResponsesLiteShape(body) {
|
|
216
|
+
const input = Array.isArray(body.input) ? body.input : [];
|
|
217
|
+
stripImageDetails(input);
|
|
218
|
+
body.parallel_tool_calls = false;
|
|
219
|
+
const declaredTools = Array.isArray(body.tools) ? body.tools : [];
|
|
220
|
+
let additionalTools = declaredTools;
|
|
221
|
+
if (body.tool_choice && typeof body.tool_choice === "object" && "type" in body.tool_choice) {
|
|
222
|
+
const choice = body.tool_choice;
|
|
223
|
+
const selected = declaredTools.find((tool) => {
|
|
224
|
+
if (tool === null || typeof tool !== "object" || !("type" in tool)) return false;
|
|
225
|
+
if (choice.type === "computer") return tool.type === "computer";
|
|
226
|
+
return choice.type === "function" && tool.type === "function" && "name" in choice && typeof choice.name === "string" && "name" in tool && tool.name === choice.name;
|
|
227
|
+
});
|
|
228
|
+
if (selected) {
|
|
229
|
+
additionalTools = [selected];
|
|
230
|
+
body.tool_choice = "required";
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const prefix = [{
|
|
234
|
+
type: "additional_tools",
|
|
235
|
+
role: "developer",
|
|
236
|
+
tools: additionalTools
|
|
237
|
+
}];
|
|
238
|
+
if (typeof body.instructions === "string" && body.instructions.length > 0) prefix.push({
|
|
239
|
+
type: "message",
|
|
240
|
+
role: "developer",
|
|
241
|
+
content: [{
|
|
242
|
+
type: "input_text",
|
|
243
|
+
text: body.instructions
|
|
244
|
+
}]
|
|
245
|
+
});
|
|
246
|
+
body.input = [...prefix, ...input];
|
|
247
|
+
if (body.tool_choice !== "none" && body.tool_choice !== "required") body.tool_choice = "auto";
|
|
248
|
+
delete body.instructions;
|
|
249
|
+
delete body.tools;
|
|
250
|
+
}
|
|
251
|
+
async function transformRequestBody(body, model, options = {}, prompt) {
|
|
252
|
+
body.store = false;
|
|
253
|
+
body.stream = true;
|
|
254
|
+
if (body.input && Array.isArray(body.input)) {
|
|
255
|
+
body.input = filterInput(body.input);
|
|
256
|
+
if (body.input) body.input = repairToolCallPairs(body.input);
|
|
257
|
+
}
|
|
258
|
+
if (prompt?.developerMessages && prompt.developerMessages.length > 0) {
|
|
259
|
+
const developerMessages = prompt.developerMessages.map((text) => ({
|
|
260
|
+
type: "message",
|
|
261
|
+
role: "developer",
|
|
262
|
+
content: [{
|
|
263
|
+
type: "input_text",
|
|
264
|
+
text
|
|
265
|
+
}]
|
|
266
|
+
}));
|
|
267
|
+
const input = Array.isArray(body.input) ? body.input : [];
|
|
268
|
+
body.input = [...developerMessages, ...input];
|
|
269
|
+
}
|
|
270
|
+
let finalInstruction = prompt?.developerMessages.findLast((text) => text.trim().length > 0);
|
|
271
|
+
if (finalInstruction === void 0 && Array.isArray(body.input)) for (let itemIndex = body.input.length - 1; itemIndex >= 0; itemIndex -= 1) {
|
|
272
|
+
const item = body.input[itemIndex];
|
|
273
|
+
if (item.role !== "developer" || !Array.isArray(item.content)) continue;
|
|
274
|
+
for (let partIndex = item.content.length - 1; partIndex >= 0; partIndex -= 1) {
|
|
275
|
+
const part = item.content[partIndex];
|
|
276
|
+
if (part && typeof part === "object" && "type" in part && part.type === "input_text" && "text" in part && typeof part.text === "string" && part.text.trim().length > 0) {
|
|
277
|
+
finalInstruction = part.text;
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (finalInstruction !== void 0) break;
|
|
282
|
+
}
|
|
283
|
+
if (finalInstruction === void 0 && typeof body.instructions === "string" && body.instructions.trim().length > 0) finalInstruction = body.instructions;
|
|
284
|
+
if (finalInstruction !== void 0) {
|
|
285
|
+
const input = Array.isArray(body.input) ? body.input : [];
|
|
286
|
+
let hasVisibleInput = false;
|
|
287
|
+
for (const item of input) if (item.role !== "developer") {
|
|
288
|
+
hasVisibleInput = true;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
if (!hasVisibleInput) body.input = [...input, {
|
|
292
|
+
type: "message",
|
|
293
|
+
role: "user",
|
|
294
|
+
content: [{
|
|
295
|
+
type: "input_text",
|
|
296
|
+
text: finalInstruction
|
|
297
|
+
}]
|
|
298
|
+
}];
|
|
299
|
+
}
|
|
300
|
+
const responsesLite = resolveCodexResponsesLite(model, options.responsesLite);
|
|
301
|
+
if (responsesLite) applyCodexResponsesLiteShape(body);
|
|
302
|
+
if (options.reasoningOff || options.reasoningEffort !== void 0 || responsesLite) {
|
|
303
|
+
const reasoningConfig = options.reasoningOff ? { effort: "none" } : options.reasoningEffort !== void 0 ? getReasoningConfig(model, options.reasoningEffort, options) : {};
|
|
304
|
+
body.reasoning = {
|
|
305
|
+
...body.reasoning,
|
|
306
|
+
...reasoningConfig
|
|
307
|
+
};
|
|
308
|
+
if (responsesLite) body.reasoning.context = "all_turns";
|
|
309
|
+
else if (options.reasoningContext !== void 0) {
|
|
310
|
+
if (options.reasoningContext === "all_turns" && !supportsAllTurnsReasoningContext(model.id)) delete body.reasoning.context;
|
|
311
|
+
else body.reasoning.context = options.reasoningContext;
|
|
312
|
+
}
|
|
313
|
+
} else delete body.reasoning;
|
|
314
|
+
if (model.reasoningMode && !options.reasoningOff) body.reasoning = {
|
|
315
|
+
...body.reasoning,
|
|
316
|
+
mode: model.reasoningMode
|
|
317
|
+
};
|
|
318
|
+
if (body.reasoning?.summary !== void 0 && concurrentSummariesEnabled()) body.stream_options = { reasoning_summary_delivery: "sequential_cutoff" };
|
|
319
|
+
else delete body.stream_options;
|
|
320
|
+
if (options.textVerbosity !== void 0) body.text = {
|
|
321
|
+
...body.text,
|
|
322
|
+
verbosity: options.textVerbosity
|
|
323
|
+
};
|
|
324
|
+
const include = Array.isArray(options.include) ? [...options.include] : [];
|
|
325
|
+
include.push("reasoning.encrypted_content");
|
|
326
|
+
body.include = Array.from(new Set(include));
|
|
327
|
+
delete body.max_output_tokens;
|
|
328
|
+
delete body.max_completion_tokens;
|
|
329
|
+
return body;
|
|
330
|
+
}
|
|
331
|
+
//#endregion
|
|
332
|
+
//#region node_modules/.pnpm/@oh-my-pi+pi-ai@17.3.5/node_modules/@oh-my-pi/pi-ai/src/providers/openai-codex/response-handler.ts
|
|
333
|
+
/** Non-2xx response from the Codex backend, with the parsed body retained. */
|
|
334
|
+
var CodexApiError = class CodexApiError extends ProviderHttpError {
|
|
335
|
+
info;
|
|
336
|
+
constructor(info, headers) {
|
|
337
|
+
super(info.friendlyMessage || info.message, info.status, {
|
|
338
|
+
headers,
|
|
339
|
+
code: info.code
|
|
340
|
+
});
|
|
341
|
+
this.name = "CodexApiError";
|
|
342
|
+
this.info = info;
|
|
343
|
+
}
|
|
344
|
+
static async fromResponse(response) {
|
|
345
|
+
return new CodexApiError(await parseCodexError(response), response.headers);
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
async function parseCodexError(response) {
|
|
349
|
+
const raw = await response.text();
|
|
350
|
+
let message = raw || response.statusText || "Request failed";
|
|
351
|
+
let friendlyMessage;
|
|
352
|
+
let rateLimits;
|
|
353
|
+
let errorCode;
|
|
354
|
+
try {
|
|
355
|
+
const err = JSON.parse(raw)?.error ?? {};
|
|
356
|
+
const headers = response.headers;
|
|
357
|
+
const primary = {
|
|
358
|
+
used_percent: toNumber(headers.get("x-codex-primary-used-percent")),
|
|
359
|
+
window_minutes: toInt(headers.get("x-codex-primary-window-minutes")),
|
|
360
|
+
resets_at: toInt(headers.get("x-codex-primary-reset-at"))
|
|
361
|
+
};
|
|
362
|
+
const secondary = {
|
|
363
|
+
used_percent: toNumber(headers.get("x-codex-secondary-used-percent")),
|
|
364
|
+
window_minutes: toInt(headers.get("x-codex-secondary-window-minutes")),
|
|
365
|
+
resets_at: toInt(headers.get("x-codex-secondary-reset-at"))
|
|
366
|
+
};
|
|
367
|
+
rateLimits = primary.used_percent !== void 0 || secondary.used_percent !== void 0 ? {
|
|
368
|
+
primary,
|
|
369
|
+
secondary
|
|
370
|
+
} : void 0;
|
|
371
|
+
const code = String(err.code ?? err.type ?? "");
|
|
372
|
+
errorCode = code || void 0;
|
|
373
|
+
const resetsAt = err.resets_at ?? primary.resets_at ?? secondary.resets_at;
|
|
374
|
+
const mins = resetsAt ? Math.max(0, Math.round((resetsAt * 1e3 - Date.now()) / 6e4)) : void 0;
|
|
375
|
+
if (/usage_limit_reached|usage_not_included/i.test(code)) {
|
|
376
|
+
const planType = err.plan_type;
|
|
377
|
+
friendlyMessage = `You have hit your ChatGPT usage limit${planType ? ` (${String(planType).toLowerCase()} plan)` : ""}.${mins !== void 0 ? ` Try again in ~${mins} min.` : ""}`.trim();
|
|
378
|
+
} else if (/rate_limit_exceeded/i.test(code) || response.status === 429) friendlyMessage = `ChatGPT rate limit exceeded.${mins !== void 0 ? ` Try again in ~${mins} min.` : ""}`.trim();
|
|
379
|
+
message = err.message || friendlyMessage || message;
|
|
380
|
+
} catch {}
|
|
381
|
+
return {
|
|
382
|
+
message,
|
|
383
|
+
status: response.status,
|
|
384
|
+
code: errorCode,
|
|
385
|
+
friendlyMessage,
|
|
386
|
+
rateLimits,
|
|
387
|
+
raw
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function toInt(v) {
|
|
391
|
+
if (v == null) return void 0;
|
|
392
|
+
const n = parseInt(v, 10);
|
|
393
|
+
return Number.isFinite(n) ? n : void 0;
|
|
394
|
+
}
|
|
395
|
+
//#endregion
|
|
396
|
+
//#region node_modules/.pnpm/@oh-my-pi+pi-ai@17.3.5/node_modules/@oh-my-pi/pi-ai/src/providers/openai-codex-responses.ts
|
|
397
|
+
var openai_codex_responses_exports = /* @__PURE__ */ __exportAll({
|
|
398
|
+
CodexWebSocketTransportError: () => CodexWebSocketTransportError,
|
|
399
|
+
buildTransformedCodexRequestBody: () => buildTransformedCodexRequestBody,
|
|
400
|
+
convertCodexResponsesMessages: () => convertMessages,
|
|
401
|
+
convertOpenAICodexResponsesTools: () => convertOpenAICodexResponsesTools,
|
|
402
|
+
createCodexProviderStreamError: () => createCodexProviderStreamError,
|
|
403
|
+
getCodexAttestationHeader: () => getCodexAttestationHeader,
|
|
404
|
+
isRetryableCodexFailureEvent: () => isRetryableCodexFailureEvent,
|
|
405
|
+
normalizeCodexToolChoice: () => normalizeCodexToolChoice,
|
|
406
|
+
resolveCodexResponsesUrl: () => resolveCodexResponsesUrl,
|
|
407
|
+
streamOpenAICodexResponses: () => streamOpenAICodexResponses
|
|
408
|
+
});
|
|
409
|
+
const CODEX_DEBUG = $flag("PI_CODEX_DEBUG");
|
|
410
|
+
const CODEX_MAX_RETRIES = 5;
|
|
411
|
+
const CODEX_RETRY_DELAY_MS = 500;
|
|
412
|
+
function resolveCodexSseMaxAttempts(value) {
|
|
413
|
+
if (value === void 0) return 6;
|
|
414
|
+
if (!Number.isFinite(value)) return 1;
|
|
415
|
+
return Math.max(1, Math.trunc(value));
|
|
416
|
+
}
|
|
417
|
+
const CODEX_WEBSOCKET_CONNECT_TIMEOUT_MS = 1e4;
|
|
418
|
+
const CODEX_WEBSOCKET_PING_INTERVAL_MS = Number($env.PI_CODEX_WEBSOCKET_PING_INTERVAL_MS || 1e4);
|
|
419
|
+
const CODEX_WEBSOCKET_PONG_TIMEOUT_MS = Number($env.PI_CODEX_WEBSOCKET_PONG_TIMEOUT_MS || 6e4);
|
|
420
|
+
const CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY = Number($env.PI_CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY || 4096);
|
|
421
|
+
/**
|
|
422
|
+
* Maximum quiet period (no inbound frames AND no observed pong) we'll trust a
|
|
423
|
+
* reused WebSocket for before forcing a fresh handshake. Codex backends and
|
|
424
|
+
* intermediaries occasionally evict idle sockets server-side without sending a
|
|
425
|
+
* FIN, leaving the local `readyState` as OPEN while the next `send()` becomes a
|
|
426
|
+
* write into a half-open buffer. Reusing such a socket parks the next request
|
|
427
|
+
* at `#nextMessage` until the first-event/idle timeout fires (issue #1450). The
|
|
428
|
+
* heartbeat below also catches dead sockets, but only after `pongTimeoutMs`
|
|
429
|
+
* (default 60s) and only while a request is active — this gate closes the door
|
|
430
|
+
* earlier and even when the gap between requests is purely client-side (tool
|
|
431
|
+
* execution, user typing, etc.). Set `PI_CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS=0`
|
|
432
|
+
* to disable.
|
|
433
|
+
*/
|
|
434
|
+
const CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS = Number($env.PI_CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS || 3e4);
|
|
435
|
+
/**
|
|
436
|
+
* Steady-state liveness ceiling for the Codex WebSocket transport. Distinct from
|
|
437
|
+
* the OMP-wide stream watchdog removed in #1392: a WebSocket can stay TCP-open
|
|
438
|
+
* indefinitely without exchanging frames (server crash after upgrade, half-open
|
|
439
|
+
* network path), so we still need a transport-internal cap to detect those
|
|
440
|
+
* states and trigger the WS→SSE fallback. Only applies AFTER the first event
|
|
441
|
+
* has arrived — slow first-token paths wait as long as the caller permits.
|
|
442
|
+
*/
|
|
443
|
+
const CODEX_WEBSOCKET_IDLE_TIMEOUT_MS = Number($env.PI_CODEX_WEBSOCKET_IDLE_TIMEOUT_MS || 3e5);
|
|
444
|
+
/**
|
|
445
|
+
* Maximum wait for the first WebSocket event before falling back to SSE.
|
|
446
|
+
* Unlike a stream watchdog, this triggers a transport switch (not a request
|
|
447
|
+
* failure) — the outer retry loop catches the timeout error and re-runs on
|
|
448
|
+
* SSE. Generous default so legitimately slow first-token providers still get
|
|
449
|
+
* a chance on the WS transport before falling through.
|
|
450
|
+
*/
|
|
451
|
+
const CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS = Number($env.PI_CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS || 3e5);
|
|
452
|
+
const CODEX_WEBSOCKET_RETRY_BUDGET = Number($env.PI_CODEX_WEBSOCKET_RETRY_BUDGET || CODEX_MAX_RETRIES);
|
|
453
|
+
const CODEX_WEBSOCKET_RETRY_DELAY_MS = Number($env.PI_CODEX_WEBSOCKET_RETRY_DELAY_MS || CODEX_RETRY_DELAY_MS);
|
|
454
|
+
const CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX = "Codex websocket transport error";
|
|
455
|
+
const CODEX_RETRYABLE_EVENT_CODES = /* @__PURE__ */ new Set([
|
|
456
|
+
"model_error",
|
|
457
|
+
"server_error",
|
|
458
|
+
"internal_error"
|
|
459
|
+
]);
|
|
460
|
+
const CODEX_RETRYABLE_EVENT_MESSAGE = /processing your request|retry your request|temporar(?:y|ily)|overloaded|service.?unavailable|internal error|server error/i;
|
|
461
|
+
const CODEX_PROVIDER_SESSION_STATE_KEY = "openai-codex-responses";
|
|
462
|
+
/**
|
|
463
|
+
* Resolve the `x-oai-attestation` header value for one upstream request.
|
|
464
|
+
* Gated on ChatGPT-OAuth credentials (a Codex JWT carries `chatgpt_account_id`;
|
|
465
|
+
* codex-rs gates on `auth.is_chatgpt_auth()`). A throwing hook degrades to no
|
|
466
|
+
* header rather than failing the request.
|
|
467
|
+
*/
|
|
468
|
+
async function getCodexAttestationHeader(accountId) {}
|
|
469
|
+
const X_CODEX_TURN_STATE_HEADER = "x-codex-turn-state";
|
|
470
|
+
const X_MODELS_ETAG_HEADER = "x-models-etag";
|
|
471
|
+
/** WebSocket frames cannot carry per-request HTTP headers; codex-rs mirrors the lite marker into `client_metadata` under this key. */
|
|
472
|
+
const CODEX_WS_RESPONSES_LITE_CLIENT_METADATA_KEY = "ws_request_header_x_openai_internal_codex_responses_lite";
|
|
473
|
+
/** `response.metadata` payload key carrying ChatGPT moderation metadata. */
|
|
474
|
+
const CODEX_MODERATION_METADATA_KEY = "openai_chatgpt_moderation_metadata";
|
|
475
|
+
/** Connection-level websocket failures that should immediately fall back to SSE without retrying. */
|
|
476
|
+
const CODEX_WEBSOCKET_FATAL_PATTERNS = [
|
|
477
|
+
"websocket error:",
|
|
478
|
+
"websocket closed before open",
|
|
479
|
+
"connection timeout"
|
|
480
|
+
];
|
|
481
|
+
/** Max total time to spend retrying 429s with server-provided delays (5 minutes). */
|
|
482
|
+
const CODEX_RATE_LIMIT_BUDGET_MS = 3e5;
|
|
483
|
+
const CODEX_ADDITIONAL_PROGRESS_EVENT_TYPES = /* @__PURE__ */ new Set(["response.done", "response.incomplete"]);
|
|
484
|
+
const CODEX_WHITESPACE_TOOL_CALL_ARGUMENT_DELTA_EVENT_LIMIT = 256;
|
|
485
|
+
const CODEX_WHITESPACE_TOOL_CALL_ARGUMENT_DELTA_CHAR_LIMIT = 16384;
|
|
486
|
+
const CODEX_WHITESPACE_LOOP_RETRY_LIMIT = 2;
|
|
487
|
+
const CODEX_WHITESPACE_LOOP_RETRY_DELAY_MS = 250;
|
|
488
|
+
function isCodexStreamProgressEvent(event) {
|
|
489
|
+
if (isOpenAIResponsesProgressEvent(event)) return true;
|
|
490
|
+
if (!event || typeof event !== "object") return false;
|
|
491
|
+
const type = event.type;
|
|
492
|
+
return typeof type === "string" && CODEX_ADDITIONAL_PROGRESS_EVENT_TYPES.has(type);
|
|
493
|
+
}
|
|
494
|
+
function extractCodexFrameResponseId(frame) {
|
|
495
|
+
const id = frame.response?.id;
|
|
496
|
+
return typeof id === "string" && id.length > 0 ? id : void 0;
|
|
497
|
+
}
|
|
498
|
+
function extractCodexFrameSequenceNumber(frame) {
|
|
499
|
+
const raw = frame.sequence_number;
|
|
500
|
+
return typeof raw === "number" && Number.isFinite(raw) ? Math.trunc(raw) : void 0;
|
|
501
|
+
}
|
|
502
|
+
function createCodexWebSocketTimeoutMessage(reason, details) {
|
|
503
|
+
const now = Date.now();
|
|
504
|
+
return `${reason} (last event: ${details.lastEventType ? `${details.lastEventType} ${Math.max(0, now - details.lastEventAt)}ms ago` : "none"}; last progress: ${details.lastProgressEventType ? `${details.lastProgressEventType} ${Math.max(0, now - details.lastProgressAt)}ms ago` : "none"})`;
|
|
505
|
+
}
|
|
506
|
+
const CODEX_RESERVED_METADATA_KEYS = {
|
|
507
|
+
installation_id: true,
|
|
508
|
+
[OPENAI_HEADERS.INSTALLATION_ID]: true,
|
|
509
|
+
session_id: true,
|
|
510
|
+
thread_id: true,
|
|
511
|
+
turn_id: true,
|
|
512
|
+
window_id: true,
|
|
513
|
+
[OPENAI_HEADERS.WINDOW_ID]: true,
|
|
514
|
+
[OPENAI_HEADERS.TURN_METADATA]: true,
|
|
515
|
+
[OPENAI_HEADERS.PARENT_THREAD_ID]: true,
|
|
516
|
+
[OPENAI_HEADERS.SUBAGENT]: true,
|
|
517
|
+
request_kind: true,
|
|
518
|
+
compaction: true,
|
|
519
|
+
code_mode_tool_names: true,
|
|
520
|
+
turn_started_at_unix_ms: true,
|
|
521
|
+
forked_from_thread_id: true,
|
|
522
|
+
parent_thread_id: true,
|
|
523
|
+
parent_turn_id: true,
|
|
524
|
+
subagent_kind: true,
|
|
525
|
+
thread_source: true,
|
|
526
|
+
sandbox: true,
|
|
527
|
+
workspaces: true
|
|
528
|
+
};
|
|
529
|
+
function createCodexMetadataSessionState(sessionId) {
|
|
530
|
+
return {
|
|
531
|
+
sessionId,
|
|
532
|
+
threadId: crypto.randomUUID(),
|
|
533
|
+
windowId: crypto.randomUUID()
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function getOrCreateCodexMetadataSessionState(sessionId, providerState) {
|
|
537
|
+
if (!providerState) return createCodexMetadataSessionState(sessionId);
|
|
538
|
+
const existing = providerState.metadataSessions.get(sessionId);
|
|
539
|
+
if (existing) return existing;
|
|
540
|
+
const created = createCodexMetadataSessionState(sessionId);
|
|
541
|
+
providerState.metadataSessions.set(sessionId, created);
|
|
542
|
+
return created;
|
|
543
|
+
}
|
|
544
|
+
function createCodexCompatibilityIdentity(session) {
|
|
545
|
+
return {
|
|
546
|
+
installationId: getInstallId(),
|
|
547
|
+
sessionId: session.sessionId,
|
|
548
|
+
threadId: session.threadId,
|
|
549
|
+
windowId: session.windowId
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
function resolveCodexStartNewTurn(session, requestKind, compaction, override) {
|
|
553
|
+
if (requestKind !== "compaction") {
|
|
554
|
+
if (requestKind === "turn") {
|
|
555
|
+
const reuseCompactionTurn = session.reuseTurnForNextRequest === true;
|
|
556
|
+
session.reuseTurnForNextRequest = false;
|
|
557
|
+
session.compactionOperationId = void 0;
|
|
558
|
+
if (reuseCompactionTurn) return false;
|
|
559
|
+
}
|
|
560
|
+
return override ?? requestKind === "turn";
|
|
561
|
+
}
|
|
562
|
+
if (!compaction) return override ?? false;
|
|
563
|
+
const startsNewOperation = session.compactionOperationId !== compaction.operationId;
|
|
564
|
+
if (startsNewOperation) session.reuseTurnForNextRequest = false;
|
|
565
|
+
session.compactionOperationId = compaction.operationId;
|
|
566
|
+
return override ?? (compaction.phase !== "mid_turn" && startsNewOperation);
|
|
567
|
+
}
|
|
568
|
+
function toAsciiJsonString(value) {
|
|
569
|
+
return JSON.stringify(value).replace(/[\x7f-\uffff]/g, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
570
|
+
}
|
|
571
|
+
function createCodexRequestMetadata(session, requestKind, options) {
|
|
572
|
+
if (options.startNewTurn || !session.turnId) {
|
|
573
|
+
session.turnId = crypto.randomUUID();
|
|
574
|
+
session.turnStartedAtUnixMs = options.turnStartedAtUnixMs;
|
|
575
|
+
}
|
|
576
|
+
const identity = createCodexCompatibilityIdentity(session);
|
|
577
|
+
const parentTurnId = options.parentTurnId?.trim() ? options.parentTurnId : void 0;
|
|
578
|
+
const extra = {};
|
|
579
|
+
const callerMetadata = options.clientMetadata;
|
|
580
|
+
if (callerMetadata) {
|
|
581
|
+
for (const key in callerMetadata) if (!CODEX_RESERVED_METADATA_KEYS[key]) extra[key] = callerMetadata[key];
|
|
582
|
+
}
|
|
583
|
+
const turnMetadata = {
|
|
584
|
+
installation_id: identity.installationId,
|
|
585
|
+
session_id: identity.sessionId,
|
|
586
|
+
thread_id: identity.threadId,
|
|
587
|
+
turn_id: session.turnId,
|
|
588
|
+
window_id: identity.windowId,
|
|
589
|
+
request_kind: requestKind
|
|
590
|
+
};
|
|
591
|
+
if (parentTurnId) turnMetadata.parent_turn_id = parentTurnId;
|
|
592
|
+
if (options.compaction) turnMetadata.compaction = {
|
|
593
|
+
trigger: options.compaction.trigger,
|
|
594
|
+
reason: options.compaction.reason,
|
|
595
|
+
implementation: options.compaction.implementation,
|
|
596
|
+
phase: options.compaction.phase,
|
|
597
|
+
strategy: options.compaction.strategy
|
|
598
|
+
};
|
|
599
|
+
if (session.turnStartedAtUnixMs !== void 0) turnMetadata.turn_started_at_unix_ms = session.turnStartedAtUnixMs;
|
|
600
|
+
for (const key in extra) turnMetadata[key] = extra[key];
|
|
601
|
+
const turnMetadataJson = toAsciiJsonString(turnMetadata);
|
|
602
|
+
const clientMetadata = {
|
|
603
|
+
[OPENAI_HEADERS.INSTALLATION_ID]: identity.installationId,
|
|
604
|
+
session_id: identity.sessionId,
|
|
605
|
+
thread_id: identity.threadId,
|
|
606
|
+
[OPENAI_HEADERS.WINDOW_ID]: identity.windowId,
|
|
607
|
+
turn_id: session.turnId
|
|
608
|
+
};
|
|
609
|
+
if (parentTurnId) clientMetadata.parent_turn_id = parentTurnId;
|
|
610
|
+
clientMetadata[OPENAI_HEADERS.TURN_METADATA] = turnMetadataJson;
|
|
611
|
+
return {
|
|
612
|
+
...identity,
|
|
613
|
+
turnId: session.turnId,
|
|
614
|
+
turnMetadataJson,
|
|
615
|
+
clientMetadata
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function applyCodexCompatibilityHeaders(headers, metadata) {
|
|
619
|
+
headers.set(OPENAI_HEADERS.SCOPED_SESSION_ID, metadata.sessionId);
|
|
620
|
+
headers.set(OPENAI_HEADERS.THREAD_ID, metadata.threadId);
|
|
621
|
+
headers.set(OPENAI_HEADERS.WINDOW_ID, metadata.windowId);
|
|
622
|
+
if (metadata.turnMetadataJson) headers.set(OPENAI_HEADERS.TURN_METADATA, metadata.turnMetadataJson);
|
|
623
|
+
else headers.delete(OPENAI_HEADERS.TURN_METADATA);
|
|
624
|
+
}
|
|
625
|
+
var CodexStreamRuntime = class {
|
|
626
|
+
eventStream;
|
|
627
|
+
requestBodyForState;
|
|
628
|
+
transport;
|
|
629
|
+
websocketState;
|
|
630
|
+
/**
|
|
631
|
+
* Items open on the wire keyed by `item.id`. `response.output_item.added`
|
|
632
|
+
* registers here; `output_item.done` removes. A keyed event whose `item_id`
|
|
633
|
+
* is not present is dropped rather than appended to a sibling.
|
|
634
|
+
*/
|
|
635
|
+
openItems = /* @__PURE__ */ new Map();
|
|
636
|
+
/**
|
|
637
|
+
* Items open on the wire keyed by `output_index` for streams whose function
|
|
638
|
+
* call items omit `id`; these still carry `output_index` on deltas/done.
|
|
639
|
+
*/
|
|
640
|
+
openItemsByOutputIndex = /* @__PURE__ */ new Map();
|
|
641
|
+
/**
|
|
642
|
+
* Most recently added open item for events that omit both `item_id` and
|
|
643
|
+
* `output_index`. Always tracks the latest `output_item.added`, including
|
|
644
|
+
* fully keyless items that never make it into the keyed maps; cleared when
|
|
645
|
+
* its item closes.
|
|
646
|
+
*/
|
|
647
|
+
currentEntry = null;
|
|
648
|
+
/** Convenience mirrors of {@link currentEntry} for legacy singleton handlers. */
|
|
649
|
+
currentItem = null;
|
|
650
|
+
currentBlock = null;
|
|
651
|
+
nativeOutputItems = [];
|
|
652
|
+
/** Sequential-cutoff summary sections/emitted text, global to the response (indices span reasoning items). */
|
|
653
|
+
cutoffSummaries = createSequentialCutoffSummaryState();
|
|
654
|
+
/** Summary deltas buffered while waiting to see whether atomic `.done` events arrive. */
|
|
655
|
+
pendingSummaryDeltas = /* @__PURE__ */ new Map();
|
|
656
|
+
websocketStreamRetries = 0;
|
|
657
|
+
providerRetryAttempt = 0;
|
|
658
|
+
sawTerminalEvent = false;
|
|
659
|
+
canSafelyReplayWebsocketOverSse = true;
|
|
660
|
+
whitespaceToolCallArgumentsDelta;
|
|
661
|
+
whitespaceLoopRetries = 0;
|
|
662
|
+
constructor(initial) {
|
|
663
|
+
this.eventStream = initial.eventStream;
|
|
664
|
+
this.requestBodyForState = initial.requestBodyForState;
|
|
665
|
+
this.transport = initial.transport;
|
|
666
|
+
this.websocketState = initial.websocketState;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Wipe per-attempt accumulator state before a recovery path replays the turn.
|
|
670
|
+
* Keeps {@link openItems} and the legacy singleton-current pointers in lockstep
|
|
671
|
+
* with {@link nativeOutputItems} so a stale delta from the failed attempt can't
|
|
672
|
+
* bind to a sibling on the retry.
|
|
673
|
+
*/
|
|
674
|
+
resetAccumulators() {
|
|
675
|
+
this.openItems.clear();
|
|
676
|
+
this.openItemsByOutputIndex.clear();
|
|
677
|
+
this.currentEntry = null;
|
|
678
|
+
this.currentItem = null;
|
|
679
|
+
this.currentBlock = null;
|
|
680
|
+
this.nativeOutputItems.length = 0;
|
|
681
|
+
this.pendingSummaryDeltas.clear();
|
|
682
|
+
this.cutoffSummaries = createSequentialCutoffSummaryState();
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Look up the open item a Codex stream event targets. `item_id` wins because it
|
|
686
|
+
* uniquely identifies a response item; `output_index` covers idless function
|
|
687
|
+
* call items. A keyed event whose target is already closed is dropped instead
|
|
688
|
+
* of being routed to a sibling. Only streams that omit both keys fall back to
|
|
689
|
+
* {@link currentEntry} — the most recently added item, including fully keyless
|
|
690
|
+
* ones that never reached the keyed maps.
|
|
691
|
+
*/
|
|
692
|
+
openItemForEvent(rawEvent) {
|
|
693
|
+
const itemId = typeof rawEvent.item_id === "string" ? rawEvent.item_id : "";
|
|
694
|
+
if (itemId) return this.openItems.get(itemId) ?? null;
|
|
695
|
+
const outputIndex = typeof rawEvent.output_index === "number" && Number.isFinite(rawEvent.output_index) ? Math.trunc(rawEvent.output_index) : void 0;
|
|
696
|
+
if (outputIndex !== void 0) return this.openItemsByOutputIndex.get(outputIndex) ?? null;
|
|
697
|
+
return this.currentEntry;
|
|
698
|
+
}
|
|
699
|
+
queueSummaryDelta(entry, delta) {
|
|
700
|
+
if (entry?.block?.type !== "thinking" || delta.length === 0) return;
|
|
701
|
+
const pending = this.pendingSummaryDeltas.get(entry) ?? [];
|
|
702
|
+
pending.push(delta);
|
|
703
|
+
this.pendingSummaryDeltas.set(entry, pending);
|
|
704
|
+
}
|
|
705
|
+
takeSummaryDeltas(entry) {
|
|
706
|
+
if (!entry) return [];
|
|
707
|
+
const pending = this.pendingSummaryDeltas.get(entry) ?? [];
|
|
708
|
+
this.pendingSummaryDeltas.delete(entry);
|
|
709
|
+
return pending;
|
|
710
|
+
}
|
|
711
|
+
closeOpenItem(entry) {
|
|
712
|
+
if (!entry) return;
|
|
713
|
+
if (entry.itemId) this.openItems.delete(entry.itemId);
|
|
714
|
+
if (entry.outputIndex !== void 0) this.openItemsByOutputIndex.delete(entry.outputIndex);
|
|
715
|
+
if (this.currentEntry === entry) {
|
|
716
|
+
this.currentEntry = null;
|
|
717
|
+
this.currentItem = null;
|
|
718
|
+
this.currentBlock = null;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
observeWhitespaceToolCallArgumentsDelta(rawEvent, delta) {
|
|
722
|
+
if (!isJsonWhitespaceOnly(delta)) {
|
|
723
|
+
this.whitespaceToolCallArgumentsDelta = void 0;
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const itemId = typeof rawEvent.item_id === "string" && rawEvent.item_id.length > 0 ? rawEvent.item_id : this.currentItem?.id ?? "";
|
|
727
|
+
const outputIndex = typeof rawEvent.output_index === "number" && Number.isFinite(rawEvent.output_index) ? Math.trunc(rawEvent.output_index) : void 0;
|
|
728
|
+
const sequenceNumber = typeof rawEvent.sequence_number === "number" && Number.isFinite(rawEvent.sequence_number) ? Math.trunc(rawEvent.sequence_number) : void 0;
|
|
729
|
+
let state = this.whitespaceToolCallArgumentsDelta;
|
|
730
|
+
if (!state || state.itemId !== itemId || state.outputIndex !== outputIndex) {
|
|
731
|
+
state = {
|
|
732
|
+
itemId,
|
|
733
|
+
outputIndex,
|
|
734
|
+
consecutiveEvents: 0,
|
|
735
|
+
consecutiveChars: 0,
|
|
736
|
+
firstSequenceNumber: sequenceNumber
|
|
737
|
+
};
|
|
738
|
+
this.whitespaceToolCallArgumentsDelta = state;
|
|
739
|
+
}
|
|
740
|
+
state.consecutiveEvents += 1;
|
|
741
|
+
state.consecutiveChars += delta.length;
|
|
742
|
+
state.lastSequenceNumber = sequenceNumber;
|
|
743
|
+
if (state.consecutiveEvents < CODEX_WHITESPACE_TOOL_CALL_ARGUMENT_DELTA_EVENT_LIMIT && state.consecutiveChars < CODEX_WHITESPACE_TOOL_CALL_ARGUMENT_DELTA_CHAR_LIMIT) return;
|
|
744
|
+
const itemLabel = itemId ? ` for item ${itemId}` : "";
|
|
745
|
+
const sequenceLabel = state.firstSequenceNumber === void 0 || state.lastSequenceNumber === void 0 ? "" : `, sequence ${state.firstSequenceNumber}..${state.lastSequenceNumber}`;
|
|
746
|
+
return { message: `Interrupted OpenAI Codex response after ${state.consecutiveEvents} consecutive whitespace-only tool-call argument delta events (${state.consecutiveChars} chars${sequenceLabel})${itemLabel}.` };
|
|
747
|
+
}
|
|
748
|
+
handleToolCallArgumentsDelta(rawEvent, stream, output) {
|
|
749
|
+
const delta = rawEvent.delta || "";
|
|
750
|
+
const interruption = this.observeWhitespaceToolCallArgumentsDelta(rawEvent, delta);
|
|
751
|
+
if (interruption) return interruption;
|
|
752
|
+
const entry = this.openItemForEvent(rawEvent);
|
|
753
|
+
if (!entry) return void 0;
|
|
754
|
+
if (entry.item.type !== "function_call" || entry.block?.type !== "toolCall") return void 0;
|
|
755
|
+
accumulateToolCallArgumentsDelta(entry.block, delta, stream, output, entry.contentIndex);
|
|
756
|
+
}
|
|
757
|
+
handleToolCallArgumentsDone(rawEvent) {
|
|
758
|
+
const entry = this.openItemForEvent(rawEvent);
|
|
759
|
+
if (entry?.item.type !== "function_call" || entry.block?.type !== "toolCall") return;
|
|
760
|
+
const args = rawEvent.arguments;
|
|
761
|
+
if (typeof args === "string") finalizeToolCallArgumentsDone(entry.block, args);
|
|
762
|
+
}
|
|
763
|
+
handleCustomToolCallInputDelta(rawEvent, stream, output) {
|
|
764
|
+
const delta = rawEvent.delta || "";
|
|
765
|
+
const interruption = this.observeWhitespaceToolCallArgumentsDelta(rawEvent, delta);
|
|
766
|
+
if (interruption) return interruption;
|
|
767
|
+
const entry = this.openItemForEvent(rawEvent);
|
|
768
|
+
if (!entry) return void 0;
|
|
769
|
+
if (entry.item.type !== "custom_tool_call" || entry.block?.type !== "toolCall") return void 0;
|
|
770
|
+
accumulateCustomToolCallInputDelta(entry.block, delta, stream, output, entry.contentIndex);
|
|
771
|
+
}
|
|
772
|
+
handleCustomToolCallInputDone(rawEvent) {
|
|
773
|
+
const entry = this.openItemForEvent(rawEvent);
|
|
774
|
+
if (entry?.item.type !== "custom_tool_call" || entry.block?.type !== "toolCall") return;
|
|
775
|
+
const input = rawEvent.input;
|
|
776
|
+
if (typeof input === "string") finalizeCustomToolCallInputDone(entry.block, input);
|
|
777
|
+
}
|
|
778
|
+
handleResponseCreated(rawEvent) {
|
|
779
|
+
const response = rawEvent.response;
|
|
780
|
+
const state = this.websocketState;
|
|
781
|
+
if (state && this.transport === "websocket" && typeof response?.id === "string" && response.id.length > 0) state.lastResponseId = response.id;
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
function createCodexProviderSessionState() {
|
|
785
|
+
const state = {
|
|
786
|
+
webSocketSessions: /* @__PURE__ */ new Map(),
|
|
787
|
+
webSocketPublicToPrivate: /* @__PURE__ */ new Map(),
|
|
788
|
+
metadataSessions: /* @__PURE__ */ new Map(),
|
|
789
|
+
close: () => {
|
|
790
|
+
for (const session of state.webSocketSessions.values()) session.connection?.close("session_disposed");
|
|
791
|
+
state.webSocketSessions.clear();
|
|
792
|
+
state.webSocketPublicToPrivate.clear();
|
|
793
|
+
state.metadataSessions.clear();
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
return state;
|
|
797
|
+
}
|
|
798
|
+
function isCodexProviderSessionState(state) {
|
|
799
|
+
return state !== void 0 && "webSocketSessions" in state && state.webSocketSessions instanceof Map && "webSocketPublicToPrivate" in state && state.webSocketPublicToPrivate instanceof Map && "metadataSessions" in state && state.metadataSessions instanceof Map;
|
|
800
|
+
}
|
|
801
|
+
function getCodexProviderSessionState(providerSessionState) {
|
|
802
|
+
if (!providerSessionState) return void 0;
|
|
803
|
+
const existing = providerSessionState.get(CODEX_PROVIDER_SESSION_STATE_KEY);
|
|
804
|
+
if (isCodexProviderSessionState(existing)) return existing;
|
|
805
|
+
const created = createCodexProviderSessionState();
|
|
806
|
+
providerSessionState.set(CODEX_PROVIDER_SESSION_STATE_KEY, created);
|
|
807
|
+
return created;
|
|
808
|
+
}
|
|
809
|
+
function isCodexWebSocketRetryableStreamError(error) {
|
|
810
|
+
if (!(error instanceof CodexWebSocketTransportError)) return false;
|
|
811
|
+
const message = error.message.toLowerCase();
|
|
812
|
+
return message.includes("websocket closed (") || message.includes("websocket closed before response completion") || message.includes("websocket connection is unavailable") || message.includes("websocket send failed") || message.includes("websocket ping failed") || message.includes("websocket pong timeout") || message.includes("websocket message queue exceeded") || message.includes("websocket request already in progress") || message.includes("idle timeout waiting for websocket") || message.includes("timeout waiting for first websocket event") || message.includes("syntaxerror") || message.includes("json");
|
|
813
|
+
}
|
|
814
|
+
function toCodexHeaderRecord(value) {
|
|
815
|
+
if (!value || typeof value !== "object") return null;
|
|
816
|
+
const headers = {};
|
|
817
|
+
for (const [key, entry] of Object.entries(value)) if (typeof entry === "string") headers[key] = entry;
|
|
818
|
+
else if (Array.isArray(entry) && entry.every((item) => typeof item === "string")) headers[key] = entry.join(",");
|
|
819
|
+
else if (typeof entry === "number" || typeof entry === "boolean") headers[key] = String(entry);
|
|
820
|
+
return Object.keys(headers).length > 0 ? headers : null;
|
|
821
|
+
}
|
|
822
|
+
function toCodexHeaders(value) {
|
|
823
|
+
if (!value) return void 0;
|
|
824
|
+
if (value instanceof Headers) return value;
|
|
825
|
+
if (Array.isArray(value)) try {
|
|
826
|
+
return new Headers(value);
|
|
827
|
+
} catch {
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
const record = toCodexHeaderRecord(value);
|
|
831
|
+
if (!record) return void 0;
|
|
832
|
+
return new Headers(record);
|
|
833
|
+
}
|
|
834
|
+
function updateCodexSessionMetadataFromHeaders(state, headers) {
|
|
835
|
+
if (!state || !headers) return;
|
|
836
|
+
const resolvedHeaders = headers instanceof Headers ? headers : new Headers(headers);
|
|
837
|
+
const turnState = resolvedHeaders.get(X_CODEX_TURN_STATE_HEADER);
|
|
838
|
+
if (turnState && turnState.length > 0) state.turnState = turnState;
|
|
839
|
+
const modelsEtag = resolvedHeaders.get(X_MODELS_ETAG_HEADER);
|
|
840
|
+
if (modelsEtag && modelsEtag.length > 0) state.modelsEtag = modelsEtag;
|
|
841
|
+
}
|
|
842
|
+
function extractCodexWebSocketHandshakeHeaders(socket, openEvent) {
|
|
843
|
+
const eventRecord = openEvent;
|
|
844
|
+
const eventResponse = eventRecord?.response;
|
|
845
|
+
const socketRecord = socket;
|
|
846
|
+
const socketResponse = socketRecord.response;
|
|
847
|
+
const socketHandshake = socketRecord.handshake;
|
|
848
|
+
return toCodexHeaders(eventRecord?.responseHeaders) ?? toCodexHeaders(eventRecord?.headers) ?? toCodexHeaders(eventResponse?.headers) ?? toCodexHeaders(socketRecord.responseHeaders) ?? toCodexHeaders(socketRecord.handshakeHeaders) ?? toCodexHeaders(socketResponse?.headers) ?? toCodexHeaders(socketHandshake?.headers);
|
|
849
|
+
}
|
|
850
|
+
function notifyCodexWebSocketInbound(observer, parsed, text) {
|
|
851
|
+
const type = typeof parsed.type === "string" ? parsed.type : null;
|
|
852
|
+
const raw = [`: ws ← ${type ?? "(untyped)"}`];
|
|
853
|
+
if (type) raw.push(`event: ${type}`);
|
|
854
|
+
raw.push(`data: ${text}`);
|
|
855
|
+
notifyRawSseEvent(observer, {
|
|
856
|
+
event: type,
|
|
857
|
+
data: text,
|
|
858
|
+
raw
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
function notifyCodexWebSocketOutbound(observer, request, payload) {
|
|
862
|
+
const type = typeof request.type === "string" ? request.type : null;
|
|
863
|
+
const raw = [`: ws → ${type ?? "(untyped)"}`];
|
|
864
|
+
if (type) raw.push(`event: ${type}`);
|
|
865
|
+
raw.push(`data: ${payload}`);
|
|
866
|
+
notifyRawSseEvent(observer, {
|
|
867
|
+
event: type,
|
|
868
|
+
data: payload,
|
|
869
|
+
raw
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
function notifyCodexWebSocketMalformed(observer, data, error) {
|
|
873
|
+
const text = typeof data === "string" ? data : "";
|
|
874
|
+
const raw = [`: ws ← (parse-error: ${error instanceof Error ? error.message : String(error)})`];
|
|
875
|
+
if (text) raw.push(`data: ${text}`);
|
|
876
|
+
notifyRawSseEvent(observer, {
|
|
877
|
+
event: "parse_error",
|
|
878
|
+
data: text,
|
|
879
|
+
raw
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
/** @internal Exported for tests. */
|
|
883
|
+
function normalizeCodexToolChoice(choice, tools = [], model) {
|
|
884
|
+
if (!choice) return void 0;
|
|
885
|
+
if (typeof choice === "string") return choice;
|
|
886
|
+
const allowFreeform = model ? model.applyPatchToolType === "freeform" : false;
|
|
887
|
+
const mapName = (name) => {
|
|
888
|
+
const directTool = tools.find((tool) => tool.name === name);
|
|
889
|
+
const customTool = allowFreeform ? tools.find((tool) => tool.customFormat && (tool.name === name || tool.customWireName === name)) : void 0;
|
|
890
|
+
const offeredTool = customTool ?? directTool;
|
|
891
|
+
if (!offeredTool) return void 0;
|
|
892
|
+
if (offeredTool.native?.type === "computer" && model?.supportsComputerUse === true) return { type: "computer" };
|
|
893
|
+
return customTool ? {
|
|
894
|
+
type: "custom",
|
|
895
|
+
name: customTool.customWireName ?? customTool.name
|
|
896
|
+
} : {
|
|
897
|
+
type: "function",
|
|
898
|
+
name: offeredTool.name
|
|
899
|
+
};
|
|
900
|
+
};
|
|
901
|
+
if (choice.type === "computer") {
|
|
902
|
+
const computer = tools.find((tool) => tool.native?.type === "computer");
|
|
903
|
+
if (!computer) return void 0;
|
|
904
|
+
return model?.supportsComputerUse === true ? { type: "computer" } : {
|
|
905
|
+
type: "function",
|
|
906
|
+
name: computer.name
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
if (choice.type === "function") {
|
|
910
|
+
if ("function" in choice && choice.function?.name) return mapName(choice.function.name);
|
|
911
|
+
if ("name" in choice && choice.name) return mapName(choice.name);
|
|
912
|
+
}
|
|
913
|
+
if (choice.type === "tool" && choice.name) return mapName(choice.name);
|
|
914
|
+
}
|
|
915
|
+
function unrollCodexComputerItems(items, supportsImageDetailOriginal) {
|
|
916
|
+
const replayItems = stripOpenAIResponsesComputerLinkedReasoningIdsForReplay(items);
|
|
917
|
+
const unrolled = [];
|
|
918
|
+
for (const item of replayItems) {
|
|
919
|
+
if (item.type === "computer_call") {
|
|
920
|
+
const actions = item.actions ?? (item.action ? [item.action] : []);
|
|
921
|
+
unrolled.push({
|
|
922
|
+
type: "function_call",
|
|
923
|
+
call_id: item.call_id,
|
|
924
|
+
name: "computer",
|
|
925
|
+
arguments: JSON.stringify({ actions }),
|
|
926
|
+
status: item.status
|
|
927
|
+
});
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
if (item.type === "computer_call_output") {
|
|
931
|
+
const image = typeof item.output.image_url === "string" && item.output.image_url.length > 0 ? {
|
|
932
|
+
type: "input_image",
|
|
933
|
+
detail: supportsImageDetailOriginal ? "original" : "auto",
|
|
934
|
+
image_url: item.output.image_url
|
|
935
|
+
} : typeof item.output.file_id === "string" && item.output.file_id.length > 0 ? {
|
|
936
|
+
type: "input_image",
|
|
937
|
+
detail: supportsImageDetailOriginal ? "original" : "auto",
|
|
938
|
+
file_id: item.output.file_id
|
|
939
|
+
} : void 0;
|
|
940
|
+
unrolled.push({
|
|
941
|
+
type: "function_call_output",
|
|
942
|
+
call_id: item.call_id,
|
|
943
|
+
output: image ? "(see attached image)" : ""
|
|
944
|
+
});
|
|
945
|
+
if (image) unrolled.push({
|
|
946
|
+
role: "user",
|
|
947
|
+
content: [{
|
|
948
|
+
type: "input_text",
|
|
949
|
+
text: "Attached image from computer tool result:"
|
|
950
|
+
}, image]
|
|
951
|
+
});
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
unrolled.push(item);
|
|
955
|
+
}
|
|
956
|
+
return unrolled;
|
|
957
|
+
}
|
|
958
|
+
function unrollCodexComputerAssistantMessage(message) {
|
|
959
|
+
let changed = false;
|
|
960
|
+
const content = message.content.map((block) => {
|
|
961
|
+
if (block.type !== "toolCall" || block.providerMetadata?.type !== "computer") return block;
|
|
962
|
+
changed = true;
|
|
963
|
+
const call = {
|
|
964
|
+
...block,
|
|
965
|
+
arguments: { actions: structuredCloneJSON(block.providerMetadata.actions) }
|
|
966
|
+
};
|
|
967
|
+
delete call.providerMetadata;
|
|
968
|
+
return call;
|
|
969
|
+
});
|
|
970
|
+
return changed ? {
|
|
971
|
+
...message,
|
|
972
|
+
content
|
|
973
|
+
} : message;
|
|
974
|
+
}
|
|
975
|
+
function unrollCodexComputerToolResult(message) {
|
|
976
|
+
if (message.providerMetadata?.type !== "computer") return message;
|
|
977
|
+
const result = { ...message };
|
|
978
|
+
delete result.providerMetadata;
|
|
979
|
+
return result;
|
|
980
|
+
}
|
|
981
|
+
function getCodexServiceTierCostMultiplier(model, serviceTier) {
|
|
982
|
+
switch (serviceTier) {
|
|
983
|
+
case "flex": return .5;
|
|
984
|
+
case "priority": return model.id === "gpt-5.5" ? 2.5 : 2;
|
|
985
|
+
default: return 1;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
function resolveCodexCostServiceTier(res, req) {
|
|
989
|
+
switch (res) {
|
|
990
|
+
case "flex": return "flex";
|
|
991
|
+
case "priority": return "priority";
|
|
992
|
+
default:
|
|
993
|
+
if (req === "flex" || req === "priority") return req;
|
|
994
|
+
return "default";
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
function applyCodexServiceTierPricing(model, usage, resTier, reqTier) {
|
|
998
|
+
const multiplier = getCodexServiceTierCostMultiplier(model, resolveCodexCostServiceTier(resTier, reqTier));
|
|
999
|
+
if (multiplier === 1) return;
|
|
1000
|
+
usage.cost.input *= multiplier;
|
|
1001
|
+
usage.cost.output *= multiplier;
|
|
1002
|
+
usage.cost.cacheRead *= multiplier;
|
|
1003
|
+
usage.cost.cacheWrite *= multiplier;
|
|
1004
|
+
usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
|
|
1005
|
+
}
|
|
1006
|
+
function resetOutputState(output) {
|
|
1007
|
+
output.content.length = 0;
|
|
1008
|
+
output.usage = {
|
|
1009
|
+
input: 0,
|
|
1010
|
+
output: 0,
|
|
1011
|
+
cacheRead: 0,
|
|
1012
|
+
cacheWrite: 0,
|
|
1013
|
+
totalTokens: 0,
|
|
1014
|
+
cost: {
|
|
1015
|
+
input: 0,
|
|
1016
|
+
output: 0,
|
|
1017
|
+
cacheRead: 0,
|
|
1018
|
+
cacheWrite: 0,
|
|
1019
|
+
total: 0
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
output.stopReason = "stop";
|
|
1023
|
+
output.stopDetails = void 0;
|
|
1024
|
+
}
|
|
1025
|
+
function createRequestSetup(options) {
|
|
1026
|
+
const requestAbortController = new AbortController();
|
|
1027
|
+
const requestSignal = options?.signal ? AbortSignal.any([options.signal, requestAbortController.signal]) : requestAbortController.signal;
|
|
1028
|
+
const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getOpenAIStreamIdleTimeoutMs();
|
|
1029
|
+
const websocketIdleTimeoutMs = options?.streamIdleTimeoutMs ?? CODEX_WEBSOCKET_IDLE_TIMEOUT_MS;
|
|
1030
|
+
const firstEventTimeoutMs = options?.streamFirstEventTimeoutMs ?? getOpenAIStreamFirstEventTimeoutMs(idleTimeoutMs);
|
|
1031
|
+
const websocketFirstEventTimeoutMs = options?.streamFirstEventTimeoutMs ?? CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS;
|
|
1032
|
+
const wrapCodexSseStream = (source) => iterateWithIdleTimeout(source, {
|
|
1033
|
+
idleTimeoutMs,
|
|
1034
|
+
firstItemTimeoutMs: firstEventTimeoutMs,
|
|
1035
|
+
firstItemErrorMessage: "OpenAI Codex SSE stream timed out while waiting for the first event",
|
|
1036
|
+
errorMessage: "OpenAI Codex SSE stream stalled while waiting for the next event",
|
|
1037
|
+
onIdle: () => requestAbortController.abort(),
|
|
1038
|
+
onFirstItemTimeout: () => requestAbortController.abort(),
|
|
1039
|
+
abortSignal: options?.signal,
|
|
1040
|
+
isProgressItem: isCodexStreamProgressEvent
|
|
1041
|
+
});
|
|
1042
|
+
return {
|
|
1043
|
+
requestAbortController,
|
|
1044
|
+
requestSignal,
|
|
1045
|
+
wrapCodexSseStream,
|
|
1046
|
+
firstEventTimeoutMs,
|
|
1047
|
+
websocketIdleTimeoutMs,
|
|
1048
|
+
websocketFirstEventTimeoutMs
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
function createCodexRequestContext(model, transformedBody, options, contextOptions) {
|
|
1052
|
+
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
|
1053
|
+
if (!apiKey) throw new MissingApiKeyError(model.provider);
|
|
1054
|
+
const accountId = getCodexAccountId(apiKey);
|
|
1055
|
+
const baseUrl = model.baseUrl || "https://chatgpt.com/backend-api";
|
|
1056
|
+
const url = resolveCodexResponsesUrl(baseUrl);
|
|
1057
|
+
const transportSessionId = normalizeOpenAIPromptCacheKey(options?.sessionId);
|
|
1058
|
+
const codexClientVersion = CODEX_CLIENT_VERSION;
|
|
1059
|
+
const requestHeaders = {
|
|
1060
|
+
...model.headers ?? {},
|
|
1061
|
+
...options?.headers ?? {}
|
|
1062
|
+
};
|
|
1063
|
+
const rawRequestDump = {
|
|
1064
|
+
provider: model.provider,
|
|
1065
|
+
api: model.api,
|
|
1066
|
+
model: model.id,
|
|
1067
|
+
method: "POST",
|
|
1068
|
+
url,
|
|
1069
|
+
body: transformedBody
|
|
1070
|
+
};
|
|
1071
|
+
const providerSessionState = getCodexProviderSessionState(options?.providerSessionState);
|
|
1072
|
+
const isolatedTransportState = contextOptions.isolateCompactionTransport && options?.codexCompaction ? createCodexProviderSessionState() : void 0;
|
|
1073
|
+
const transportProviderSessionState = isolatedTransportState ?? providerSessionState;
|
|
1074
|
+
const responsesLite = resolveCodexResponsesLite(model, options?.responsesLite);
|
|
1075
|
+
const sessionKey = getCodexWebSocketSessionKey(transportSessionId, model, accountId, apiKey, baseUrl, responsesLite);
|
|
1076
|
+
const publicSessionKey = transportSessionId ? `${baseUrl}:${model.id}:${transportSessionId}` : void 0;
|
|
1077
|
+
if (sessionKey && publicSessionKey) transportProviderSessionState?.webSocketPublicToPrivate.set(publicSessionKey, sessionKey);
|
|
1078
|
+
const sharedWebsocketState = sessionKey && providerSessionState ? isolatedTransportState ? providerSessionState.webSocketSessions.get(sessionKey) : getCodexWebSocketSessionState(sessionKey, providerSessionState) : void 0;
|
|
1079
|
+
const websocketState = sessionKey && isolatedTransportState ? getCodexWebSocketSessionState(sessionKey, isolatedTransportState) : sharedWebsocketState;
|
|
1080
|
+
if (isolatedTransportState && websocketState && sharedWebsocketState) {
|
|
1081
|
+
websocketState.disableWebsocket = sharedWebsocketState.disableWebsocket;
|
|
1082
|
+
websocketState.turnState = sharedWebsocketState.turnState;
|
|
1083
|
+
websocketState.modelsEtag = sharedWebsocketState.modelsEtag;
|
|
1084
|
+
}
|
|
1085
|
+
const metadataSession = getOrCreateCodexMetadataSessionState(transportSessionId ?? crypto.randomUUID(), providerSessionState);
|
|
1086
|
+
const compaction = options?.codexCompaction;
|
|
1087
|
+
const requestKind = compaction ? "compaction" : "turn";
|
|
1088
|
+
const startNewTurn = resolveCodexStartNewTurn(metadataSession, requestKind, compaction, contextOptions.startNewTurn);
|
|
1089
|
+
if (websocketState && startNewTurn) websocketState.turnState = void 0;
|
|
1090
|
+
const requestMetadata = createCodexRequestMetadata(metadataSession, requestKind, {
|
|
1091
|
+
startNewTurn,
|
|
1092
|
+
turnStartedAtUnixMs: compaction ? startNewTurn || !metadataSession.turnId ? Date.now() : void 0 : contextOptions.turnStartedAtUnixMs,
|
|
1093
|
+
clientMetadata: transformedBody.client_metadata,
|
|
1094
|
+
parentTurnId: options?.parentTurnId,
|
|
1095
|
+
compaction
|
|
1096
|
+
});
|
|
1097
|
+
transformedBody.client_metadata = requestMetadata.clientMetadata;
|
|
1098
|
+
return {
|
|
1099
|
+
apiKey,
|
|
1100
|
+
accountId,
|
|
1101
|
+
baseUrl,
|
|
1102
|
+
url,
|
|
1103
|
+
requestHeaders,
|
|
1104
|
+
transportSessionId,
|
|
1105
|
+
providerSessionState,
|
|
1106
|
+
isolatedTransportState,
|
|
1107
|
+
websocketState,
|
|
1108
|
+
responsesLite,
|
|
1109
|
+
requestMetadata,
|
|
1110
|
+
codexClientVersion,
|
|
1111
|
+
transformedBody,
|
|
1112
|
+
rawRequestDump
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
async function buildCodexRequestContext(model, context, options) {
|
|
1116
|
+
return createCodexRequestContext(model, await buildTransformedCodexRequestBody(model, context, options, getOpenAIPromptCacheKey(options)), options, {
|
|
1117
|
+
isolateCompactionTransport: true,
|
|
1118
|
+
startNewTurn: options?.codexCompaction ? void 0 : !isCodexWithinTurnContinuation(context),
|
|
1119
|
+
turnStartedAtUnixMs: options?.codexCompaction ? void 0 : getCodexTurnStartedAtUnixMs(context)
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
/** @internal Exported for tests. */
|
|
1123
|
+
async function buildTransformedCodexRequestBody(model, context, options, promptCacheKey = getOpenAIPromptCacheKey(options)) {
|
|
1124
|
+
const params = {
|
|
1125
|
+
model: model.requestModelId ?? model.id,
|
|
1126
|
+
input: convertMessages(model, context),
|
|
1127
|
+
stream: true,
|
|
1128
|
+
prompt_cache_key: promptCacheKey
|
|
1129
|
+
};
|
|
1130
|
+
applyOpenAIServiceTier(params, options?.serviceTier, model);
|
|
1131
|
+
if (context.tools && context.tools.length > 0) {
|
|
1132
|
+
params.tools = convertOpenAICodexResponsesTools(context.tools, model);
|
|
1133
|
+
if (options?.toolChoice) {
|
|
1134
|
+
const toolChoice = normalizeCodexToolChoice(options.toolChoice, context.tools, model);
|
|
1135
|
+
if (toolChoice) params.tool_choice = toolChoice;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
const systemPrompts = normalizeSystemPrompts(context.systemPrompt);
|
|
1139
|
+
if (systemPrompts.length > 0) params.instructions = systemPrompts[0];
|
|
1140
|
+
const developerMessages = systemPrompts.slice(1);
|
|
1141
|
+
if (options?.clientMetadata && Object.keys(options.clientMetadata).length > 0) params.client_metadata = { ...options.clientMetadata };
|
|
1142
|
+
return transformRequestBody(params, model, {
|
|
1143
|
+
reasoningEffort: options?.reasoning,
|
|
1144
|
+
reasoningOff: options?.forceReasoningOff,
|
|
1145
|
+
reasoningSummary: options?.reasoningSummary,
|
|
1146
|
+
reasoningContext: options?.reasoningContext,
|
|
1147
|
+
textVerbosity: options?.textVerbosity,
|
|
1148
|
+
include: options?.include,
|
|
1149
|
+
responsesLite: options?.responsesLite
|
|
1150
|
+
}, { developerMessages });
|
|
1151
|
+
}
|
|
1152
|
+
async function openInitialCodexEventStream(model, options, requestSetup, requestContext) {
|
|
1153
|
+
const { transformedBody, websocketState } = requestContext;
|
|
1154
|
+
if (websocketState && shouldUseCodexWebSocket(model, websocketState, options?.preferWebsockets)) {
|
|
1155
|
+
const websocketRetryBudget = CODEX_WEBSOCKET_RETRY_BUDGET;
|
|
1156
|
+
let websocketRetries = 0;
|
|
1157
|
+
while (true) try {
|
|
1158
|
+
return await openCodexWebSocketTransport(model, options, requestContext, requestSetup, websocketState, websocketRetries, options ? (event) => options.onSseEvent?.(event, model) : void 0);
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
if (!(error instanceof CodexWebSocketTransportError)) throw error;
|
|
1161
|
+
const fatalWebSocketMessage = error.message.toLowerCase();
|
|
1162
|
+
const isFatal = CODEX_WEBSOCKET_FATAL_PATTERNS.some((pattern) => fatalWebSocketMessage.includes(pattern.toLowerCase()));
|
|
1163
|
+
const activateFallback = isFatal || websocketRetries >= websocketRetryBudget;
|
|
1164
|
+
recordCodexWebSocketFailure(websocketState, activateFallback);
|
|
1165
|
+
CODEX_DEBUG && debug("[codex] codex websocket fallback", {
|
|
1166
|
+
error: error.message,
|
|
1167
|
+
retry: websocketRetries,
|
|
1168
|
+
retryBudget: websocketRetryBudget,
|
|
1169
|
+
activated: activateFallback,
|
|
1170
|
+
fatal: isFatal
|
|
1171
|
+
});
|
|
1172
|
+
if (!activateFallback) {
|
|
1173
|
+
websocketRetries += 1;
|
|
1174
|
+
await scheduler.wait(CODEX_WEBSOCKET_RETRY_DELAY_MS * Math.max(1, websocketRetries), { signal: requestSetup.requestSignal });
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
break;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
return openCodexSseTransport(model, requestContext, requestSetup, options, websocketState, transformedBody);
|
|
1181
|
+
}
|
|
1182
|
+
async function openCodexWebSocketTransport(model, options, requestContext, requestSetup, websocketState, retry, onSseEvent) {
|
|
1183
|
+
const canAppendBeforeRequest = websocketState.canAppend === true;
|
|
1184
|
+
const chainedBody = buildCodexChainedRequestBody(requestContext.transformedBody, websocketState);
|
|
1185
|
+
const websocketClientMetadata = { ...chainedBody.client_metadata ?? {} };
|
|
1186
|
+
if (requestContext.responsesLite) websocketClientMetadata[CODEX_WS_RESPONSES_LITE_CLIENT_METADATA_KEY] = "true";
|
|
1187
|
+
if (websocketState.turnState) websocketClientMetadata[X_CODEX_TURN_STATE_HEADER] = websocketState.turnState;
|
|
1188
|
+
let websocketRequest = {
|
|
1189
|
+
type: "response.create",
|
|
1190
|
+
...chainedBody,
|
|
1191
|
+
client_metadata: websocketClientMetadata
|
|
1192
|
+
};
|
|
1193
|
+
const replacementWebsocketRequest = await options?.onPayload?.(websocketRequest, model);
|
|
1194
|
+
if (replacementWebsocketRequest !== void 0) websocketRequest = replacementWebsocketRequest;
|
|
1195
|
+
recordCodexTurnRequestDiagnostics(websocketState, websocketRequest, "websocket", canAppendBeforeRequest);
|
|
1196
|
+
const websocketHeaders = createCodexHeaders(requestContext.requestHeaders, requestContext.accountId, requestContext.apiKey, requestContext.codexClientVersion, requestContext.transportSessionId, "websocket", websocketState, requestContext.responsesLite, requestContext.requestMetadata, await getCodexAttestationHeader(requestContext.accountId));
|
|
1197
|
+
const requestBodyForState = structuredCloneJSON(requestContext.transformedBody);
|
|
1198
|
+
if (websocketRequest.stream_options === void 0) delete requestBodyForState.stream_options;
|
|
1199
|
+
else requestBodyForState.stream_options = websocketRequest.stream_options;
|
|
1200
|
+
requestContext.rawRequestDump.body = websocketRequest;
|
|
1201
|
+
CODEX_DEBUG && debug("[codex] codex websocket request", {
|
|
1202
|
+
url: toWebSocketUrl(requestContext.url),
|
|
1203
|
+
model: requestContext.transformedBody.model,
|
|
1204
|
+
reasoningEffort: requestContext.transformedBody.reasoning?.effort ?? null,
|
|
1205
|
+
headers: redactHeaders(websocketHeaders),
|
|
1206
|
+
sentTurnStateHeader: websocketHeaders.has(X_CODEX_TURN_STATE_HEADER),
|
|
1207
|
+
sentModelsEtagHeader: websocketHeaders.has(X_MODELS_ETAG_HEADER),
|
|
1208
|
+
requestType: websocketRequest.type,
|
|
1209
|
+
retry,
|
|
1210
|
+
retryBudget: CODEX_WEBSOCKET_RETRY_BUDGET
|
|
1211
|
+
});
|
|
1212
|
+
return {
|
|
1213
|
+
eventStream: (await getOrCreateCodexWebSocketConnection(websocketState, toWebSocketUrl(requestContext.url), websocketHeaders, model.provider, requestSetup.requestSignal)).streamRequest(websocketRequest, {
|
|
1214
|
+
idleTimeoutMs: requestSetup.websocketIdleTimeoutMs,
|
|
1215
|
+
firstEventTimeoutMs: requestSetup.websocketFirstEventTimeoutMs
|
|
1216
|
+
}, requestSetup.requestSignal, onSseEvent),
|
|
1217
|
+
requestBodyForState,
|
|
1218
|
+
transport: "websocket"
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
function getCodexTurnStartedAtUnixMs(context) {
|
|
1222
|
+
for (let i = context.messages.length - 1; i >= 0; i--) {
|
|
1223
|
+
const message = context.messages[i];
|
|
1224
|
+
if (message?.role === "user" && Number.isFinite(message.timestamp)) return Math.trunc(message.timestamp);
|
|
1225
|
+
}
|
|
1226
|
+
return Date.now();
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* True when the request continues the current turn (everything after the
|
|
1230
|
+
* last assistant message is tool results), false when a new user turn starts.
|
|
1231
|
+
* Mirrors codex-rs, which scopes `x-codex-turn-state` to a single turn and
|
|
1232
|
+
* clears it when the next one begins.
|
|
1233
|
+
*/
|
|
1234
|
+
function isCodexWithinTurnContinuation(context) {
|
|
1235
|
+
for (let i = context.messages.length - 1; i >= 0; i--) {
|
|
1236
|
+
const role = context.messages[i]?.role;
|
|
1237
|
+
if (role === "toolResult") continue;
|
|
1238
|
+
return role === "assistant";
|
|
1239
|
+
}
|
|
1240
|
+
return false;
|
|
1241
|
+
}
|
|
1242
|
+
async function openCodexSseTransport(model, requestContext, requestSetup, options, state, body = requestContext.transformedBody) {
|
|
1243
|
+
const open = async (wireBody) => {
|
|
1244
|
+
requestContext.rawRequestDump.body = wireBody;
|
|
1245
|
+
return requestSetup.wrapCodexSseStream(await openCodexSseEventStream(requestContext.url, requestContext.requestHeaders, requestContext.accountId, requestContext.apiKey, requestContext.transportSessionId, wireBody, state, requestContext.responsesLite, requestContext.codexClientVersion, requestContext.requestMetadata, requestSetup.requestSignal, requestSetup.firstEventTimeoutMs, options?.codexSseMaxAttempts, (event) => options?.onSseEvent?.(event, model), options?.fetch));
|
|
1246
|
+
};
|
|
1247
|
+
const canAppendBeforeRequest = state?.canAppend === true;
|
|
1248
|
+
let wireBody = body;
|
|
1249
|
+
const replacementWireBody = await options?.onPayload?.(wireBody, model);
|
|
1250
|
+
if (replacementWireBody !== void 0) wireBody = replacementWireBody;
|
|
1251
|
+
recordCodexTurnRequestDiagnostics(state, wireBody, "sse", canAppendBeforeRequest);
|
|
1252
|
+
return {
|
|
1253
|
+
eventStream: await open(wireBody),
|
|
1254
|
+
requestBodyForState: structuredCloneJSON(wireBody),
|
|
1255
|
+
transport: "sse"
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
function isJsonWhitespaceOnly(value) {
|
|
1259
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1260
|
+
const code = value.charCodeAt(index);
|
|
1261
|
+
if (code !== 9 && code !== 10 && code !== 13 && code !== 32) return false;
|
|
1262
|
+
}
|
|
1263
|
+
return true;
|
|
1264
|
+
}
|
|
1265
|
+
function createOutputBlockForItem(item) {
|
|
1266
|
+
if (item.type === "reasoning") return {
|
|
1267
|
+
type: "thinking",
|
|
1268
|
+
thinking: ""
|
|
1269
|
+
};
|
|
1270
|
+
if (item.type === "message") {
|
|
1271
|
+
const phase = item.phase === "commentary" || item.phase === "final_answer" ? item.phase : void 0;
|
|
1272
|
+
return {
|
|
1273
|
+
type: "text",
|
|
1274
|
+
text: "",
|
|
1275
|
+
textSignature: encodeTextSignatureV1(item.id, phase)
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
if (item.type === "function_call") return {
|
|
1279
|
+
type: "toolCall",
|
|
1280
|
+
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
1281
|
+
name: item.name,
|
|
1282
|
+
arguments: {},
|
|
1283
|
+
[kStreamingPartialJson]: item.arguments || ""
|
|
1284
|
+
};
|
|
1285
|
+
if (item.type === "computer_call") return {
|
|
1286
|
+
type: "toolCall",
|
|
1287
|
+
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
1288
|
+
name: "computer",
|
|
1289
|
+
arguments: {},
|
|
1290
|
+
providerMetadata: computerCallMetadata(item),
|
|
1291
|
+
[kStreamingPartialJson]: ""
|
|
1292
|
+
};
|
|
1293
|
+
if (item.type === "custom_tool_call") return {
|
|
1294
|
+
type: "toolCall",
|
|
1295
|
+
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
1296
|
+
name: item.name,
|
|
1297
|
+
arguments: { input: item.input ?? "" },
|
|
1298
|
+
customWireName: item.name,
|
|
1299
|
+
[kStreamingPartialJson]: item.input ?? ""
|
|
1300
|
+
};
|
|
1301
|
+
return null;
|
|
1302
|
+
}
|
|
1303
|
+
function getOutputBlockStartEventType(block) {
|
|
1304
|
+
if (block.type === "thinking") return "thinking_start";
|
|
1305
|
+
if (block.type === "text") return "text_start";
|
|
1306
|
+
return "toolcall_start";
|
|
1307
|
+
}
|
|
1308
|
+
const CODEX_STALE_PREVIOUS_RESPONSE_CODES = {
|
|
1309
|
+
previous_response_not_found: true,
|
|
1310
|
+
codex_previous_response_stale: true
|
|
1311
|
+
};
|
|
1312
|
+
function isCodexStalePreviousResponseError(error) {
|
|
1313
|
+
if (!(error instanceof Error)) return false;
|
|
1314
|
+
if ("code" in error && typeof error.code === "string" && Object.hasOwn(CODEX_STALE_PREVIOUS_RESPONSE_CODES, error.code)) return true;
|
|
1315
|
+
return /previous[ _]?response/i.test(error.message) && /not[ _]?found|invalid|expired|stale|unsupported/i.test(error.message);
|
|
1316
|
+
}
|
|
1317
|
+
async function handleCodexStreamFailure(context, error) {
|
|
1318
|
+
const { output } = context;
|
|
1319
|
+
if (context.requestContext.websocketState) {
|
|
1320
|
+
resetCodexWebSocketAppendState(context.requestContext.websocketState);
|
|
1321
|
+
context.requestContext.websocketState.turnState = void 0;
|
|
1322
|
+
context.requestContext.websocketState.modelsEtag = void 0;
|
|
1323
|
+
}
|
|
1324
|
+
const result = await finalize(error, {
|
|
1325
|
+
api: context.model.api,
|
|
1326
|
+
signal: context.options?.signal,
|
|
1327
|
+
rawRequestDump: context.requestContext.rawRequestDump
|
|
1328
|
+
});
|
|
1329
|
+
output.stopReason = result.stopReason;
|
|
1330
|
+
output.errorStatus = result.status;
|
|
1331
|
+
output.errorId = result.id;
|
|
1332
|
+
output.errorMessage = result.message;
|
|
1333
|
+
output.duration = performance.now() - context.startTime;
|
|
1334
|
+
if (context.firstTokenTime) output.ttft = context.firstTokenTime - context.startTime;
|
|
1335
|
+
return output;
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Owns one `streamOpenAICodexResponses` call: the request scaffolding
|
|
1339
|
+
* (model/output/stream/options/request context) plus the per-attempt
|
|
1340
|
+
* {@link CodexStreamRuntime}. Drives the event loop in {@link process}, applies
|
|
1341
|
+
* the transport-fallback / retry recovery ladder, and emits the final message
|
|
1342
|
+
* in {@link finalize}. The runtime object is mutated in place across retries
|
|
1343
|
+
* (event stream and accumulators are swapped/reset), never reassigned.
|
|
1344
|
+
*/
|
|
1345
|
+
var CodexStreamProcessor = class {
|
|
1346
|
+
runtime;
|
|
1347
|
+
model;
|
|
1348
|
+
output;
|
|
1349
|
+
stream;
|
|
1350
|
+
options;
|
|
1351
|
+
requestSetup;
|
|
1352
|
+
requestContext;
|
|
1353
|
+
startTime;
|
|
1354
|
+
firstTokenTime;
|
|
1355
|
+
constructor(init) {
|
|
1356
|
+
this.runtime = init.runtime;
|
|
1357
|
+
this.model = init.model;
|
|
1358
|
+
this.output = init.output;
|
|
1359
|
+
this.stream = init.stream;
|
|
1360
|
+
this.options = init.options;
|
|
1361
|
+
this.requestSetup = init.requestSetup;
|
|
1362
|
+
this.requestContext = init.requestContext;
|
|
1363
|
+
this.startTime = init.startTime;
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Whether the request actually sent (post-`onPayload`) opted into
|
|
1367
|
+
* sequential-cutoff summary delivery. Atomic `.done` events are preferred;
|
|
1368
|
+
* incremental deltas remain buffered as a compatibility fallback when a
|
|
1369
|
+
* transport emits them instead.
|
|
1370
|
+
*/
|
|
1371
|
+
get #sequentialCutoffSummaries() {
|
|
1372
|
+
return this.runtime.requestBodyForState.stream_options?.reasoning_summary_delivery === "sequential_cutoff";
|
|
1373
|
+
}
|
|
1374
|
+
async process() {
|
|
1375
|
+
const { output, stream } = this;
|
|
1376
|
+
stream.push({
|
|
1377
|
+
type: "start",
|
|
1378
|
+
partial: output
|
|
1379
|
+
});
|
|
1380
|
+
while (true) try {
|
|
1381
|
+
let firstTokenTime = this.firstTokenTime;
|
|
1382
|
+
for await (const rawEvent of this.runtime.eventStream) {
|
|
1383
|
+
firstTokenTime = this.#handleStreamEvent(rawEvent, firstTokenTime);
|
|
1384
|
+
if (this.runtime.sawTerminalEvent) break;
|
|
1385
|
+
}
|
|
1386
|
+
return { firstTokenTime };
|
|
1387
|
+
} catch (error) {
|
|
1388
|
+
if (!await this.#recoverStreamError(error)) throw error;
|
|
1389
|
+
stream.push({
|
|
1390
|
+
type: "start",
|
|
1391
|
+
partial: output
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
#handleStreamEvent(rawEvent, firstTokenTime) {
|
|
1396
|
+
const { output, stream } = this;
|
|
1397
|
+
const eventType = typeof rawEvent.type === "string" ? rawEvent.type : "";
|
|
1398
|
+
if (!eventType) return firstTokenTime;
|
|
1399
|
+
if (eventType === "response.output_item.added") {
|
|
1400
|
+
this.runtime.whitespaceToolCallArgumentsDelta = void 0;
|
|
1401
|
+
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
1402
|
+
const item = rawEvent.item;
|
|
1403
|
+
this.runtime.currentItem = item;
|
|
1404
|
+
this.runtime.currentBlock = createOutputBlockForItem(item);
|
|
1405
|
+
let contentIndex = -1;
|
|
1406
|
+
if (this.runtime.currentBlock) {
|
|
1407
|
+
output.content.push(this.runtime.currentBlock);
|
|
1408
|
+
contentIndex = output.content.length - 1;
|
|
1409
|
+
}
|
|
1410
|
+
const itemId = typeof item.id === "string" ? item.id : void 0;
|
|
1411
|
+
const outputIndex = typeof rawEvent.output_index === "number" && Number.isFinite(rawEvent.output_index) ? Math.trunc(rawEvent.output_index) : void 0;
|
|
1412
|
+
const entry = {
|
|
1413
|
+
item,
|
|
1414
|
+
block: this.runtime.currentBlock,
|
|
1415
|
+
contentIndex,
|
|
1416
|
+
itemId,
|
|
1417
|
+
outputIndex
|
|
1418
|
+
};
|
|
1419
|
+
this.runtime.currentEntry = entry;
|
|
1420
|
+
if (itemId) this.runtime.openItems.set(itemId, entry);
|
|
1421
|
+
if (outputIndex !== void 0) this.runtime.openItemsByOutputIndex.set(outputIndex, entry);
|
|
1422
|
+
if (!this.runtime.currentBlock) return firstTokenTime;
|
|
1423
|
+
stream.push({
|
|
1424
|
+
type: getOutputBlockStartEventType(this.runtime.currentBlock),
|
|
1425
|
+
contentIndex,
|
|
1426
|
+
partial: output
|
|
1427
|
+
});
|
|
1428
|
+
return firstTokenTime;
|
|
1429
|
+
}
|
|
1430
|
+
if (eventType === "response.reasoning_summary_part.added") {
|
|
1431
|
+
if (this.#sequentialCutoffSummaries) return firstTokenTime;
|
|
1432
|
+
const entry = this.runtime.openItemForEvent(rawEvent);
|
|
1433
|
+
if (entry?.item.type === "reasoning") appendReasoningSummaryPart(entry.item, rawEvent.part);
|
|
1434
|
+
return firstTokenTime;
|
|
1435
|
+
}
|
|
1436
|
+
if (eventType === "response.reasoning_summary_text.delta") {
|
|
1437
|
+
const entry = this.runtime.openItemForEvent(rawEvent);
|
|
1438
|
+
const delta = typeof rawEvent.delta === "string" ? rawEvent.delta : "";
|
|
1439
|
+
if (this.#sequentialCutoffSummaries) {
|
|
1440
|
+
this.runtime.queueSummaryDelta(entry, delta);
|
|
1441
|
+
return firstTokenTime;
|
|
1442
|
+
}
|
|
1443
|
+
if (entry?.item.type === "reasoning" && entry.block?.type === "thinking") appendReasoningSummaryTextDelta(entry.item, entry.block, delta, stream, output, entry.contentIndex);
|
|
1444
|
+
return firstTokenTime;
|
|
1445
|
+
}
|
|
1446
|
+
if (eventType === "response.reasoning_summary_text.done") {
|
|
1447
|
+
if (!this.#sequentialCutoffSummaries) return firstTokenTime;
|
|
1448
|
+
const entry = this.runtime.openItemForEvent(rawEvent);
|
|
1449
|
+
if (entry?.item.type === "reasoning" && entry.block?.type === "thinking") {
|
|
1450
|
+
this.runtime.takeSummaryDeltas(entry);
|
|
1451
|
+
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
1452
|
+
const summaryIndex = typeof rawEvent.summary_index === "number" && Number.isFinite(rawEvent.summary_index) ? Math.trunc(rawEvent.summary_index) : 0;
|
|
1453
|
+
applyReasoningSummaryDone(this.runtime.cutoffSummaries, entry.block, typeof rawEvent.text === "string" ? rawEvent.text : "", summaryIndex, stream, output, entry.contentIndex);
|
|
1454
|
+
}
|
|
1455
|
+
return firstTokenTime;
|
|
1456
|
+
}
|
|
1457
|
+
if (eventType === "response.reasoning_text.delta") {
|
|
1458
|
+
const entry = this.runtime.openItemForEvent(rawEvent);
|
|
1459
|
+
const delta = typeof rawEvent.delta === "string" ? rawEvent.delta : "";
|
|
1460
|
+
if (entry?.item.type === "reasoning" && entry.block?.type === "thinking") {
|
|
1461
|
+
entry.block.thinking += delta;
|
|
1462
|
+
stream.push({
|
|
1463
|
+
type: "thinking_delta",
|
|
1464
|
+
contentIndex: entry.contentIndex,
|
|
1465
|
+
delta,
|
|
1466
|
+
partial: output
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
return firstTokenTime;
|
|
1470
|
+
}
|
|
1471
|
+
if (eventType === "response.reasoning_summary_part.done") {
|
|
1472
|
+
const entry = this.runtime.openItemForEvent(rawEvent);
|
|
1473
|
+
if (this.#sequentialCutoffSummaries) {
|
|
1474
|
+
if (entry && this.runtime.pendingSummaryDeltas.has(entry)) this.runtime.queueSummaryDelta(entry, "\n\n");
|
|
1475
|
+
return firstTokenTime;
|
|
1476
|
+
}
|
|
1477
|
+
if (entry?.item.type === "reasoning" && entry.block?.type === "thinking") appendReasoningSummaryPartDone(entry.item, entry.block, stream, output, entry.contentIndex);
|
|
1478
|
+
return firstTokenTime;
|
|
1479
|
+
}
|
|
1480
|
+
if (eventType === "response.content_part.added") {
|
|
1481
|
+
const entry = this.runtime.openItemForEvent(rawEvent);
|
|
1482
|
+
if (entry?.item.type === "message") appendMessageContentPart(entry.item, rawEvent.part);
|
|
1483
|
+
return firstTokenTime;
|
|
1484
|
+
}
|
|
1485
|
+
if (eventType === "response.output_text.delta" || eventType === "response.refusal.delta") {
|
|
1486
|
+
const entry = this.runtime.openItemForEvent(rawEvent);
|
|
1487
|
+
if (entry?.item.type === "message" && entry.block?.type === "text") appendMessageTextDelta(entry.item, entry.block, rawEvent.delta || "", stream, output, entry.contentIndex, eventType === "response.refusal.delta" ? "refusal" : "output_text");
|
|
1488
|
+
return firstTokenTime;
|
|
1489
|
+
}
|
|
1490
|
+
if (eventType === "response.function_call_arguments.delta") {
|
|
1491
|
+
const interruption = this.runtime.handleToolCallArgumentsDelta(rawEvent, stream, output);
|
|
1492
|
+
if (interruption) {
|
|
1493
|
+
this.runtime.websocketState?.connection?.close("degenerate-tool-call");
|
|
1494
|
+
throw new CodexWhitespaceToolCallLoopError(interruption.message);
|
|
1495
|
+
}
|
|
1496
|
+
return firstTokenTime;
|
|
1497
|
+
}
|
|
1498
|
+
if (eventType === "response.function_call_arguments.done") {
|
|
1499
|
+
this.runtime.whitespaceToolCallArgumentsDelta = void 0;
|
|
1500
|
+
this.runtime.handleToolCallArgumentsDone(rawEvent);
|
|
1501
|
+
return firstTokenTime;
|
|
1502
|
+
}
|
|
1503
|
+
if (eventType === "response.custom_tool_call_input.delta") {
|
|
1504
|
+
const interruption = this.runtime.handleCustomToolCallInputDelta(rawEvent, stream, output);
|
|
1505
|
+
if (interruption) {
|
|
1506
|
+
this.runtime.websocketState?.connection?.close("degenerate-tool-call");
|
|
1507
|
+
throw new CodexWhitespaceToolCallLoopError(interruption.message);
|
|
1508
|
+
}
|
|
1509
|
+
return firstTokenTime;
|
|
1510
|
+
}
|
|
1511
|
+
if (eventType === "response.custom_tool_call_input.done") {
|
|
1512
|
+
this.runtime.whitespaceToolCallArgumentsDelta = void 0;
|
|
1513
|
+
this.runtime.handleCustomToolCallInputDone(rawEvent);
|
|
1514
|
+
return firstTokenTime;
|
|
1515
|
+
}
|
|
1516
|
+
if (eventType === "response.output_item.done") {
|
|
1517
|
+
this.runtime.whitespaceToolCallArgumentsDelta = void 0;
|
|
1518
|
+
this.#handleOutputItemDone(rawEvent);
|
|
1519
|
+
return firstTokenTime;
|
|
1520
|
+
}
|
|
1521
|
+
if (eventType === "response.created") {
|
|
1522
|
+
this.runtime.handleResponseCreated(rawEvent);
|
|
1523
|
+
return firstTokenTime;
|
|
1524
|
+
}
|
|
1525
|
+
if (eventType === "response.completed" || eventType === "response.done" || eventType === "response.incomplete") {
|
|
1526
|
+
this.#handleResponseCompleted(rawEvent);
|
|
1527
|
+
return firstTokenTime;
|
|
1528
|
+
}
|
|
1529
|
+
if (eventType === "response.metadata") {
|
|
1530
|
+
updateCodexSessionMetadataFromHeaders(this.requestContext.websocketState, toCodexHeaders(rawEvent.headers));
|
|
1531
|
+
const moderation = asRecord(rawEvent.metadata)?.[CODEX_MODERATION_METADATA_KEY];
|
|
1532
|
+
if (moderation !== void 0) try {
|
|
1533
|
+
this.options?.onModerationMetadata?.(moderation);
|
|
1534
|
+
} catch {}
|
|
1535
|
+
return firstTokenTime;
|
|
1536
|
+
}
|
|
1537
|
+
if (eventType === "error" || eventType === "response.failed") throw createCodexProviderStreamError(rawEvent);
|
|
1538
|
+
return firstTokenTime;
|
|
1539
|
+
}
|
|
1540
|
+
#flushSummaryDeltas(entry) {
|
|
1541
|
+
if (entry?.block?.type !== "thinking") return;
|
|
1542
|
+
for (const delta of this.runtime.takeSummaryDeltas(entry)) {
|
|
1543
|
+
entry.block.thinking += delta;
|
|
1544
|
+
this.stream.push({
|
|
1545
|
+
type: "thinking_delta",
|
|
1546
|
+
contentIndex: entry.contentIndex,
|
|
1547
|
+
delta,
|
|
1548
|
+
partial: this.output
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
#handleOutputItemDone(rawEvent) {
|
|
1553
|
+
const { runtime, output, stream } = this;
|
|
1554
|
+
const rawItem = rawEvent.item;
|
|
1555
|
+
if (!rawItem || typeof rawItem !== "object") return;
|
|
1556
|
+
const item = structuredCloneJSON(rawItem);
|
|
1557
|
+
if (item.type === "image_generation_call" && item.result) item.status = "completed";
|
|
1558
|
+
runtime.nativeOutputItems.push(item);
|
|
1559
|
+
const itemId = "id" in item && typeof item.id === "string" ? item.id : "";
|
|
1560
|
+
const entry = (itemId ? runtime.openItems.get(itemId) : null) ?? runtime.openItemForEvent(rawEvent);
|
|
1561
|
+
const block = entry?.block ?? null;
|
|
1562
|
+
const contentIndex = entry?.contentIndex ?? output.content.length - 1;
|
|
1563
|
+
if (item.type === "image_generation_call" && item.result) {
|
|
1564
|
+
appendResponsesImageResult(output, stream, item.result);
|
|
1565
|
+
runtime.closeOpenItem(entry);
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
if (item.type === "reasoning" && block?.type === "thinking") {
|
|
1569
|
+
this.#flushSummaryDeltas(entry);
|
|
1570
|
+
block.thinking = finalizeReasoningThinking(item, block.thinking, this.#sequentialCutoffSummaries ? this.runtime.cutoffSummaries : void 0);
|
|
1571
|
+
block.thinkingSignature = JSON.stringify(item);
|
|
1572
|
+
stream.push({
|
|
1573
|
+
type: "thinking_end",
|
|
1574
|
+
contentIndex,
|
|
1575
|
+
content: block.thinking,
|
|
1576
|
+
partial: output
|
|
1577
|
+
});
|
|
1578
|
+
runtime.closeOpenItem(entry);
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
if (item.type === "message" && block?.type === "text") {
|
|
1582
|
+
block.text = finalizeMessageText(item, block.text);
|
|
1583
|
+
const phase = item.phase === "commentary" || item.phase === "final_answer" ? item.phase : void 0;
|
|
1584
|
+
block.textSignature = encodeTextSignatureV1(item.id, phase);
|
|
1585
|
+
stream.push({
|
|
1586
|
+
type: "text_end",
|
|
1587
|
+
contentIndex,
|
|
1588
|
+
content: block.text,
|
|
1589
|
+
partial: output
|
|
1590
|
+
});
|
|
1591
|
+
runtime.closeOpenItem(entry);
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
if (item.type === "function_call") {
|
|
1595
|
+
const toolCall = {
|
|
1596
|
+
type: "toolCall",
|
|
1597
|
+
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
1598
|
+
name: item.name,
|
|
1599
|
+
arguments: parseStreamingJson(item.arguments || "{}")
|
|
1600
|
+
};
|
|
1601
|
+
if (block?.type === "toolCall") {
|
|
1602
|
+
block.arguments = toolCall.arguments;
|
|
1603
|
+
clearStreamingPartialJson(block);
|
|
1604
|
+
}
|
|
1605
|
+
runtime.closeOpenItem(entry);
|
|
1606
|
+
runtime.canSafelyReplayWebsocketOverSse = false;
|
|
1607
|
+
stream.push({
|
|
1608
|
+
type: "toolcall_end",
|
|
1609
|
+
contentIndex,
|
|
1610
|
+
toolCall,
|
|
1611
|
+
partial: output
|
|
1612
|
+
});
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
if (item.type === "computer_call") {
|
|
1616
|
+
const toolCall = {
|
|
1617
|
+
type: "toolCall",
|
|
1618
|
+
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
1619
|
+
name: "computer",
|
|
1620
|
+
arguments: {},
|
|
1621
|
+
providerMetadata: computerCallMetadata(item)
|
|
1622
|
+
};
|
|
1623
|
+
let resolvedContentIndex = contentIndex;
|
|
1624
|
+
if (block?.type === "toolCall") {
|
|
1625
|
+
block.id = toolCall.id;
|
|
1626
|
+
block.providerMetadata = toolCall.providerMetadata;
|
|
1627
|
+
clearStreamingPartialJson(block);
|
|
1628
|
+
} else {
|
|
1629
|
+
output.content.push(toolCall);
|
|
1630
|
+
resolvedContentIndex = output.content.length - 1;
|
|
1631
|
+
}
|
|
1632
|
+
runtime.closeOpenItem(entry);
|
|
1633
|
+
runtime.canSafelyReplayWebsocketOverSse = false;
|
|
1634
|
+
stream.push({
|
|
1635
|
+
type: "toolcall_end",
|
|
1636
|
+
contentIndex: resolvedContentIndex,
|
|
1637
|
+
toolCall,
|
|
1638
|
+
partial: output
|
|
1639
|
+
});
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
if (item.type === "custom_tool_call") {
|
|
1643
|
+
const partial = block?.type === "toolCall" ? block[kStreamingPartialJson] : void 0;
|
|
1644
|
+
const rawInput = partial && partial.length > 0 ? partial : item.input ?? "";
|
|
1645
|
+
const toolCall = {
|
|
1646
|
+
type: "toolCall",
|
|
1647
|
+
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
1648
|
+
name: item.name,
|
|
1649
|
+
arguments: { input: rawInput },
|
|
1650
|
+
customWireName: item.name
|
|
1651
|
+
};
|
|
1652
|
+
if (block?.type === "toolCall") {
|
|
1653
|
+
block.arguments = { input: rawInput };
|
|
1654
|
+
clearStreamingPartialJson(block);
|
|
1655
|
+
}
|
|
1656
|
+
runtime.closeOpenItem(entry);
|
|
1657
|
+
runtime.canSafelyReplayWebsocketOverSse = false;
|
|
1658
|
+
stream.push({
|
|
1659
|
+
type: "toolcall_end",
|
|
1660
|
+
contentIndex,
|
|
1661
|
+
toolCall,
|
|
1662
|
+
partial: output
|
|
1663
|
+
});
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
#handleResponseCompleted(rawEvent) {
|
|
1668
|
+
const { runtime, model, output } = this;
|
|
1669
|
+
runtime.sawTerminalEvent = true;
|
|
1670
|
+
const rawResponse = rawEvent.response;
|
|
1671
|
+
const response = rawResponse && typeof rawResponse === "object" ? rawResponse : void 0;
|
|
1672
|
+
const responseId = response && "id" in response && typeof response.id === "string" ? response.id : void 0;
|
|
1673
|
+
const usage = response && "usage" in response ? parseCodexResponseUsage(response.usage) : void 0;
|
|
1674
|
+
const serviceTier = response && "service_tier" in response ? parseCodexServiceTier(response.service_tier) : void 0;
|
|
1675
|
+
const status = response && "status" in response ? parseCodexResponseStatus(response.status) : void 0;
|
|
1676
|
+
const endTurn = response && "end_turn" in response ? response.end_turn : void 0;
|
|
1677
|
+
populateResponsesUsageFromResponse(output, usage);
|
|
1678
|
+
recordCodexTurnUsageDiagnostics(runtime.websocketState, usage, output.usage);
|
|
1679
|
+
if (responseId) output.responseId = responseId;
|
|
1680
|
+
const state = runtime.websocketState;
|
|
1681
|
+
if (state) {
|
|
1682
|
+
if (runtime.transport !== "websocket") resetCodexWebSocketAppendState(state);
|
|
1683
|
+
else {
|
|
1684
|
+
state.lastRequest = structuredCloneJSON(runtime.requestBodyForState);
|
|
1685
|
+
const replayableResponseItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(structuredCloneJSON(runtime.nativeOutputItems));
|
|
1686
|
+
if (responseId && replayableResponseItems) {
|
|
1687
|
+
state.lastResponseId = responseId;
|
|
1688
|
+
state.lastResponseItems = replayableResponseItems;
|
|
1689
|
+
state.canAppend = rawEvent.type === "response.done" || rawEvent.type === "response.completed";
|
|
1690
|
+
} else state.canAppend = false;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
const incompleteDetails = response && "incomplete_details" in response && response.incomplete_details && typeof response.incomplete_details === "object" ? response.incomplete_details : void 0;
|
|
1694
|
+
const shouldPromoteIncompleteToolUse = status === "incomplete" && incompleteDetails !== void 0 && "reason" in incompleteDetails && incompleteDetails.reason === "max_output_tokens" && hasExecutableIncompleteResponsesToolCalls(output);
|
|
1695
|
+
finalizePendingResponsesToolCalls(output);
|
|
1696
|
+
calculateCost(model, output.usage);
|
|
1697
|
+
applyCodexServiceTierPricing(model, output.usage, serviceTier, runtime.requestBodyForState.service_tier);
|
|
1698
|
+
output.stopReason = mapOpenAIResponsesStopReason(status);
|
|
1699
|
+
promoteResponsesToolUseStopReason(output, endTurn === true ? true : endTurn === false ? false : void 0, shouldPromoteIncompleteToolUse);
|
|
1700
|
+
}
|
|
1701
|
+
async #recoverStreamError(error) {
|
|
1702
|
+
if (await this.#tryRecoverWhitespaceToolCallLoop(error)) return true;
|
|
1703
|
+
if (await this.#tryReconnectWebSocketOnConnectionLimit(error)) return true;
|
|
1704
|
+
if (await this.#tryRecoverPreviousResponseNotFound(error)) return true;
|
|
1705
|
+
if (await this.#tryReplayWebsocketFailureOverSse(error)) return true;
|
|
1706
|
+
if (await this.#tryRetryProviderError(error)) return true;
|
|
1707
|
+
return false;
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Recover from the degenerate whitespace-only tool-call argument loop
|
|
1711
|
+
* ({@link CodexWhitespaceToolCallLoopError}). The interrupted function call has
|
|
1712
|
+
* no usable arguments, so drop the partial turn and replay the request from
|
|
1713
|
+
* scratch — bounded by {@link CODEX_WHITESPACE_LOOP_RETRY_LIMIT}. Sampling
|
|
1714
|
+
* nondeterminism usually breaks the loop on a fresh attempt; once the budget is
|
|
1715
|
+
* exhausted the original error is surfaced (now without the junk tool call
|
|
1716
|
+
* polluting the message). Replay is refused once any visible content was already
|
|
1717
|
+
* delivered to the consumer — a finished tool call (`canSafelyReplayWebsocketOverSse`),
|
|
1718
|
+
* or any streamed text/commentary block still in `output.content` after the degenerate
|
|
1719
|
+
* tool call is dropped — because replaying re-emits already-streamed deltas.
|
|
1720
|
+
*/
|
|
1721
|
+
async #tryRecoverWhitespaceToolCallLoop(error) {
|
|
1722
|
+
if (!(error instanceof CodexWhitespaceToolCallLoopError)) return false;
|
|
1723
|
+
this.#dropTrailingDegenerateToolCall();
|
|
1724
|
+
if (this.runtime.whitespaceLoopRetries >= CODEX_WHITESPACE_LOOP_RETRY_LIMIT || !this.runtime.canSafelyReplayWebsocketOverSse || this.output.content.some((block) => block.type !== "thinking") || this.options?.signal?.aborted) return false;
|
|
1725
|
+
this.runtime.whitespaceLoopRetries += 1;
|
|
1726
|
+
const websocketState = this.requestContext.websocketState;
|
|
1727
|
+
if (websocketState) {
|
|
1728
|
+
resetCodexWebSocketAppendState(websocketState);
|
|
1729
|
+
websocketState.turnState = void 0;
|
|
1730
|
+
websocketState.modelsEtag = void 0;
|
|
1731
|
+
}
|
|
1732
|
+
CODEX_DEBUG && debug("[codex] retrying codex turn after whitespace-only tool-call argument loop", {
|
|
1733
|
+
retry: this.runtime.whitespaceLoopRetries,
|
|
1734
|
+
retryBudget: CODEX_WHITESPACE_LOOP_RETRY_LIMIT,
|
|
1735
|
+
transport: this.runtime.transport
|
|
1736
|
+
});
|
|
1737
|
+
this.runtime.resetAccumulators();
|
|
1738
|
+
this.runtime.sawTerminalEvent = false;
|
|
1739
|
+
this.runtime.whitespaceToolCallArgumentsDelta = void 0;
|
|
1740
|
+
resetOutputState(this.output);
|
|
1741
|
+
this.firstTokenTime = void 0;
|
|
1742
|
+
await scheduler.wait(CODEX_WHITESPACE_LOOP_RETRY_DELAY_MS * this.runtime.whitespaceLoopRetries, { signal: this.requestSetup.requestSignal });
|
|
1743
|
+
if (this.runtime.transport === "websocket" && websocketState) {
|
|
1744
|
+
await this.#reopenWebSocketStream(websocketState);
|
|
1745
|
+
return true;
|
|
1746
|
+
}
|
|
1747
|
+
await this.#reopenSseStream(websocketState);
|
|
1748
|
+
return true;
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* Pop the half-built degenerate tool-call block (the one whose arguments were
|
|
1752
|
+
* nothing but whitespace) off the output accumulator so it never surfaces in the
|
|
1753
|
+
* caller's message. Any legitimate content produced before it is preserved.
|
|
1754
|
+
*/
|
|
1755
|
+
#dropTrailingDegenerateToolCall() {
|
|
1756
|
+
const { runtime, output } = this;
|
|
1757
|
+
const block = runtime.currentBlock;
|
|
1758
|
+
if (block && block.type === "toolCall" && output.content[output.content.length - 1] === block) output.content.pop();
|
|
1759
|
+
runtime.closeOpenItem(runtime.currentEntry);
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1762
|
+
* Handles `websocket_connection_limit_reached` errors by closing the stale connection
|
|
1763
|
+
* and opening a fresh websocket. If content has already been emitted to the caller,
|
|
1764
|
+
* falls back to SSE replay (same as other WS failures) since we cannot safely
|
|
1765
|
+
* continue a partial response on a new connection. If a tool call was already
|
|
1766
|
+
* delivered (`canSafelyReplayWebsocketOverSse` is false), the error surfaces
|
|
1767
|
+
* instead — replaying would re-emit the same tool calls.
|
|
1768
|
+
*/
|
|
1769
|
+
async #tryReconnectWebSocketOnConnectionLimit(error) {
|
|
1770
|
+
if (!(error instanceof CodexProviderStreamError) || error.code !== "websocket_connection_limit_reached") return false;
|
|
1771
|
+
const websocketState = this.requestContext.websocketState;
|
|
1772
|
+
if (!websocketState || this.runtime.transport !== "websocket" || this.options?.signal?.aborted) return false;
|
|
1773
|
+
websocketState.connection?.close("connection_limit");
|
|
1774
|
+
websocketState.connection = void 0;
|
|
1775
|
+
resetCodexWebSocketAppendState(websocketState);
|
|
1776
|
+
if (this.output.content.length > 0 && !this.runtime.canSafelyReplayWebsocketOverSse) return false;
|
|
1777
|
+
CODEX_DEBUG && debug("[codex] codex websocket connection limit reached, reconnecting", {
|
|
1778
|
+
hadContent: this.output.content.length > 0,
|
|
1779
|
+
retry: this.runtime.websocketStreamRetries
|
|
1780
|
+
});
|
|
1781
|
+
if (this.output.content.length > 0) {
|
|
1782
|
+
this.runtime.resetAccumulators();
|
|
1783
|
+
resetOutputState(this.output);
|
|
1784
|
+
this.firstTokenTime = void 0;
|
|
1785
|
+
recordCodexWebSocketFailure(websocketState, true);
|
|
1786
|
+
await this.#reopenSseStream(websocketState);
|
|
1787
|
+
return true;
|
|
1788
|
+
}
|
|
1789
|
+
this.runtime.resetAccumulators();
|
|
1790
|
+
this.firstTokenTime = void 0;
|
|
1791
|
+
if (this.runtime.websocketStreamRetries >= CODEX_WEBSOCKET_RETRY_BUDGET) {
|
|
1792
|
+
recordCodexWebSocketFailure(websocketState, true);
|
|
1793
|
+
await this.#reopenSseStream(websocketState);
|
|
1794
|
+
return true;
|
|
1795
|
+
}
|
|
1796
|
+
this.runtime.websocketStreamRetries += 1;
|
|
1797
|
+
await scheduler.wait(CODEX_WEBSOCKET_RETRY_DELAY_MS * Math.max(1, this.runtime.websocketStreamRetries), { signal: this.requestSetup.requestSignal });
|
|
1798
|
+
await this.#reopenWebSocketStream(websocketState);
|
|
1799
|
+
return true;
|
|
1800
|
+
}
|
|
1801
|
+
async #tryRecoverPreviousResponseNotFound(error) {
|
|
1802
|
+
const websocketState = this.requestContext.websocketState;
|
|
1803
|
+
if (!isCodexStalePreviousResponseError(error) || !websocketState || this.output.content.length > 0 || this.options?.signal?.aborted || this.runtime.providerRetryAttempt >= CODEX_MAX_RETRIES) return false;
|
|
1804
|
+
if (this.runtime.transport !== "websocket") return false;
|
|
1805
|
+
this.runtime.providerRetryAttempt += 1;
|
|
1806
|
+
resetCodexWebSocketAppendState(websocketState);
|
|
1807
|
+
websocketState.turnState = void 0;
|
|
1808
|
+
websocketState.modelsEtag = void 0;
|
|
1809
|
+
this.runtime.resetAccumulators();
|
|
1810
|
+
this.runtime.sawTerminalEvent = false;
|
|
1811
|
+
resetOutputState(this.output);
|
|
1812
|
+
this.firstTokenTime = void 0;
|
|
1813
|
+
CODEX_DEBUG && debug("[codex] codex previous_response_id expired; retrying with full context", { retry: this.runtime.providerRetryAttempt });
|
|
1814
|
+
await this.#reopenWebSocketStream(websocketState);
|
|
1815
|
+
return true;
|
|
1816
|
+
}
|
|
1817
|
+
async #tryReplayWebsocketFailureOverSse(error) {
|
|
1818
|
+
const websocketState = this.requestContext.websocketState;
|
|
1819
|
+
if (!(this.runtime.transport === "websocket" && websocketState && isCodexWebSocketRetryableStreamError(error) && this.runtime.canSafelyReplayWebsocketOverSse && !this.runtime.sawTerminalEvent && !this.options?.signal?.aborted)) return false;
|
|
1820
|
+
const state = websocketState;
|
|
1821
|
+
const streamError = error instanceof Error ? error : new Error(String(error));
|
|
1822
|
+
const replayingBufferedOutputOverSse = this.output.content.length > 0;
|
|
1823
|
+
const fatalWebSocketMessage = streamError.message.toLowerCase();
|
|
1824
|
+
const isFatal = CODEX_WEBSOCKET_FATAL_PATTERNS.some((pattern) => fatalWebSocketMessage.includes(pattern.toLowerCase()));
|
|
1825
|
+
const activateFallback = replayingBufferedOutputOverSse || isFatal || this.runtime.websocketStreamRetries >= CODEX_WEBSOCKET_RETRY_BUDGET;
|
|
1826
|
+
recordCodexWebSocketFailure(state, activateFallback);
|
|
1827
|
+
CODEX_DEBUG && debug("[codex] codex websocket stream fallback", {
|
|
1828
|
+
error: streamError.message,
|
|
1829
|
+
retry: this.runtime.websocketStreamRetries,
|
|
1830
|
+
retryBudget: CODEX_WEBSOCKET_RETRY_BUDGET,
|
|
1831
|
+
activated: activateFallback,
|
|
1832
|
+
fatal: isFatal,
|
|
1833
|
+
replayedBufferedOutput: replayingBufferedOutputOverSse
|
|
1834
|
+
});
|
|
1835
|
+
if (!activateFallback) {
|
|
1836
|
+
this.runtime.websocketStreamRetries += 1;
|
|
1837
|
+
this.runtime.resetAccumulators();
|
|
1838
|
+
this.firstTokenTime = void 0;
|
|
1839
|
+
await scheduler.wait(CODEX_WEBSOCKET_RETRY_DELAY_MS * Math.max(1, this.runtime.websocketStreamRetries), { signal: this.requestSetup.requestSignal });
|
|
1840
|
+
await this.#reopenWebSocketStream(state);
|
|
1841
|
+
return true;
|
|
1842
|
+
}
|
|
1843
|
+
this.runtime.resetAccumulators();
|
|
1844
|
+
resetOutputState(this.output);
|
|
1845
|
+
this.firstTokenTime = void 0;
|
|
1846
|
+
await this.#reopenSseStream(state);
|
|
1847
|
+
return true;
|
|
1848
|
+
}
|
|
1849
|
+
async #tryRetryProviderError(error) {
|
|
1850
|
+
if (!(error instanceof CodexProviderStreamError && error.retryable) || this.output.content.length > 0 || this.runtime.providerRetryAttempt >= CODEX_MAX_RETRIES || this.options?.signal?.aborted) return false;
|
|
1851
|
+
this.runtime.providerRetryAttempt += 1;
|
|
1852
|
+
const websocketState = this.requestContext.websocketState;
|
|
1853
|
+
if (websocketState) {
|
|
1854
|
+
resetCodexWebSocketAppendState(websocketState);
|
|
1855
|
+
websocketState.turnState = void 0;
|
|
1856
|
+
websocketState.modelsEtag = void 0;
|
|
1857
|
+
}
|
|
1858
|
+
CODEX_DEBUG && debug("[codex] retrying codex provider stream error", {
|
|
1859
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1860
|
+
retry: this.runtime.providerRetryAttempt,
|
|
1861
|
+
retryBudget: CODEX_MAX_RETRIES,
|
|
1862
|
+
transport: this.runtime.transport
|
|
1863
|
+
});
|
|
1864
|
+
this.runtime.resetAccumulators();
|
|
1865
|
+
this.runtime.sawTerminalEvent = false;
|
|
1866
|
+
resetOutputState(this.output);
|
|
1867
|
+
this.firstTokenTime = void 0;
|
|
1868
|
+
await scheduler.wait(CODEX_RETRY_DELAY_MS * this.runtime.providerRetryAttempt, { signal: this.requestSetup.requestSignal });
|
|
1869
|
+
if (this.runtime.transport === "websocket" && websocketState) {
|
|
1870
|
+
await this.#reopenWebSocketStream(websocketState);
|
|
1871
|
+
return true;
|
|
1872
|
+
}
|
|
1873
|
+
await this.#reopenSseStream(websocketState);
|
|
1874
|
+
return true;
|
|
1875
|
+
}
|
|
1876
|
+
async #reopenWebSocketStream(state) {
|
|
1877
|
+
try {
|
|
1878
|
+
const next = await openCodexWebSocketTransport(this.model, this.options, this.requestContext, this.requestSetup, state, this.runtime.websocketStreamRetries, this.options ? (event) => this.options?.onSseEvent?.(event, this.model) : void 0);
|
|
1879
|
+
this.runtime.eventStream = next.eventStream;
|
|
1880
|
+
this.runtime.requestBodyForState = next.requestBodyForState;
|
|
1881
|
+
this.runtime.transport = next.transport;
|
|
1882
|
+
state.lastTransport = next.transport;
|
|
1883
|
+
} catch (error) {
|
|
1884
|
+
if (!(error instanceof CodexWebSocketTransportError)) throw error;
|
|
1885
|
+
recordCodexWebSocketFailure(state, true);
|
|
1886
|
+
CODEX_DEBUG && debug("[codex] codex websocket reopen failed, falling back to SSE", {
|
|
1887
|
+
error: error.message,
|
|
1888
|
+
retry: this.runtime.websocketStreamRetries
|
|
1889
|
+
});
|
|
1890
|
+
await this.#reopenSseStream(state);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
async #reopenSseStream(state) {
|
|
1894
|
+
const next = await openCodexSseTransport(this.model, this.requestContext, this.requestSetup, this.options, state);
|
|
1895
|
+
this.runtime.eventStream = next.eventStream;
|
|
1896
|
+
this.runtime.requestBodyForState = next.requestBodyForState;
|
|
1897
|
+
this.runtime.transport = next.transport;
|
|
1898
|
+
if (state) state.lastTransport = next.transport;
|
|
1899
|
+
}
|
|
1900
|
+
finalize(completion) {
|
|
1901
|
+
const { output } = this;
|
|
1902
|
+
if (this.options?.signal?.aborted) throw new AbortError();
|
|
1903
|
+
if (!this.runtime.sawTerminalEvent) {
|
|
1904
|
+
if (this.requestContext.websocketState) {
|
|
1905
|
+
resetCodexWebSocketAppendState(this.requestContext.websocketState);
|
|
1906
|
+
this.requestContext.websocketState.turnState = void 0;
|
|
1907
|
+
this.requestContext.websocketState.modelsEtag = void 0;
|
|
1908
|
+
}
|
|
1909
|
+
CODEX_DEBUG && debug("[codex] codex stream ended unexpectedly", {
|
|
1910
|
+
transport: this.runtime.transport,
|
|
1911
|
+
terminalEventSeen: this.runtime.sawTerminalEvent,
|
|
1912
|
+
unexpectedStreamEnd: true,
|
|
1913
|
+
sentTurnStateHeader: Boolean(this.requestContext.websocketState?.turnState),
|
|
1914
|
+
sentModelsEtagHeader: Boolean(this.requestContext.websocketState?.modelsEtag)
|
|
1915
|
+
});
|
|
1916
|
+
throw new CodexProviderStreamError("Codex stream ended before terminal completion event", false);
|
|
1917
|
+
}
|
|
1918
|
+
if (output.stopReason === "aborted" || output.stopReason === "error") throw new CodexProviderStreamError("Codex response failed", false);
|
|
1919
|
+
output.providerPayload = createOpenAIResponsesHistoryPayload(this.model.provider, this.runtime.nativeOutputItems);
|
|
1920
|
+
output.duration = performance.now() - this.startTime;
|
|
1921
|
+
if (completion.firstTokenTime) output.ttft = completion.firstTokenTime - this.startTime;
|
|
1922
|
+
return output;
|
|
1923
|
+
}
|
|
1924
|
+
};
|
|
1925
|
+
const streamOpenAICodexResponses = (model, context, options) => {
|
|
1926
|
+
const stream = new AssistantMessageEventStream();
|
|
1927
|
+
(async () => {
|
|
1928
|
+
const startTime = performance.now();
|
|
1929
|
+
const output = {
|
|
1930
|
+
role: "assistant",
|
|
1931
|
+
content: [],
|
|
1932
|
+
api: "openai-codex-responses",
|
|
1933
|
+
provider: model.provider,
|
|
1934
|
+
model: model.id,
|
|
1935
|
+
usage: {
|
|
1936
|
+
input: 0,
|
|
1937
|
+
output: 0,
|
|
1938
|
+
cacheRead: 0,
|
|
1939
|
+
cacheWrite: 0,
|
|
1940
|
+
totalTokens: 0,
|
|
1941
|
+
cost: {
|
|
1942
|
+
input: 0,
|
|
1943
|
+
output: 0,
|
|
1944
|
+
cacheRead: 0,
|
|
1945
|
+
cacheWrite: 0,
|
|
1946
|
+
total: 0
|
|
1947
|
+
}
|
|
1948
|
+
},
|
|
1949
|
+
stopReason: "stop",
|
|
1950
|
+
timestamp: Date.now()
|
|
1951
|
+
};
|
|
1952
|
+
const requestSetup = createRequestSetup(options);
|
|
1953
|
+
let processingContext;
|
|
1954
|
+
let requestContext;
|
|
1955
|
+
try {
|
|
1956
|
+
requestContext = await buildCodexRequestContext(model, context, options);
|
|
1957
|
+
const initialTransport = await openInitialCodexEventStream(model, options, requestSetup, requestContext);
|
|
1958
|
+
const runtime = new CodexStreamRuntime({
|
|
1959
|
+
...initialTransport,
|
|
1960
|
+
websocketState: requestContext.websocketState
|
|
1961
|
+
});
|
|
1962
|
+
if (requestContext.websocketState) requestContext.websocketState.lastTransport = initialTransport.transport;
|
|
1963
|
+
processingContext = new CodexStreamProcessor({
|
|
1964
|
+
runtime,
|
|
1965
|
+
model,
|
|
1966
|
+
output,
|
|
1967
|
+
stream,
|
|
1968
|
+
options,
|
|
1969
|
+
requestSetup,
|
|
1970
|
+
requestContext,
|
|
1971
|
+
startTime
|
|
1972
|
+
});
|
|
1973
|
+
const completion = await processingContext.process();
|
|
1974
|
+
processingContext.firstTokenTime = completion.firstTokenTime;
|
|
1975
|
+
const message = processingContext.finalize(completion);
|
|
1976
|
+
stream.push({
|
|
1977
|
+
type: "done",
|
|
1978
|
+
reason: message.stopReason,
|
|
1979
|
+
message
|
|
1980
|
+
});
|
|
1981
|
+
stream.end();
|
|
1982
|
+
} catch (error$1) {
|
|
1983
|
+
const failureContext = processingContext ?? {
|
|
1984
|
+
model,
|
|
1985
|
+
output,
|
|
1986
|
+
options,
|
|
1987
|
+
requestContext: requestContext ?? {
|
|
1988
|
+
apiKey: "",
|
|
1989
|
+
accountId: "",
|
|
1990
|
+
baseUrl: model.baseUrl || "https://chatgpt.com/backend-api",
|
|
1991
|
+
url: "",
|
|
1992
|
+
requestHeaders: {},
|
|
1993
|
+
codexClientVersion: "0.144.1",
|
|
1994
|
+
responsesLite: options?.responsesLite === true,
|
|
1995
|
+
transformedBody: { model: model.id },
|
|
1996
|
+
rawRequestDump: {
|
|
1997
|
+
provider: model.provider,
|
|
1998
|
+
api: output.api,
|
|
1999
|
+
model: model.id,
|
|
2000
|
+
method: "POST",
|
|
2001
|
+
url: "",
|
|
2002
|
+
body: { model: model.id }
|
|
2003
|
+
}
|
|
2004
|
+
},
|
|
2005
|
+
startTime
|
|
2006
|
+
};
|
|
2007
|
+
try {
|
|
2008
|
+
const failure = await handleCodexStreamFailure(failureContext, error$1);
|
|
2009
|
+
stream.push({
|
|
2010
|
+
type: "error",
|
|
2011
|
+
reason: failure.stopReason,
|
|
2012
|
+
error: failure
|
|
2013
|
+
});
|
|
2014
|
+
} catch (failureError) {
|
|
2015
|
+
error("Codex stream failure handler threw", { error: failureError instanceof Error ? failureError.message : String(failureError) });
|
|
2016
|
+
output.stopReason = "error";
|
|
2017
|
+
output.errorMessage ??= error$1 instanceof Error ? error$1.message : String(error$1);
|
|
2018
|
+
stream.push({
|
|
2019
|
+
type: "error",
|
|
2020
|
+
reason: "error",
|
|
2021
|
+
error: output
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
stream.end();
|
|
2025
|
+
} finally {
|
|
2026
|
+
requestContext?.isolatedTransportState?.close();
|
|
2027
|
+
}
|
|
2028
|
+
})();
|
|
2029
|
+
return stream;
|
|
2030
|
+
};
|
|
2031
|
+
function getCodexWebSocketSessionKey(normalizedSessionId, model, accountId, apiKey, baseUrl, responsesLite) {
|
|
2032
|
+
if (!normalizedSessionId) return void 0;
|
|
2033
|
+
const credentialKey = accountId ? `account:${accountId}` : `token:${Bun.hash(apiKey).toString(36)}`;
|
|
2034
|
+
const liteSuffix = responsesLite ? ":lite" : "";
|
|
2035
|
+
return `${credentialKey}:${baseUrl}:${model.id}:${normalizedSessionId}${liteSuffix}`;
|
|
2036
|
+
}
|
|
2037
|
+
function getCodexWebSocketSessionState(sessionKey, providerSessionState) {
|
|
2038
|
+
const existing = providerSessionState.webSocketSessions.get(sessionKey);
|
|
2039
|
+
if (existing) return existing;
|
|
2040
|
+
const created = {
|
|
2041
|
+
disableWebsocket: false,
|
|
2042
|
+
canAppend: false,
|
|
2043
|
+
fallbackCount: 0,
|
|
2044
|
+
prewarmed: false,
|
|
2045
|
+
stats: {
|
|
2046
|
+
fullContextRequests: 0,
|
|
2047
|
+
deltaRequests: 0,
|
|
2048
|
+
lastInputItems: 0
|
|
2049
|
+
}
|
|
2050
|
+
};
|
|
2051
|
+
providerSessionState.webSocketSessions.set(sessionKey, created);
|
|
2052
|
+
return created;
|
|
2053
|
+
}
|
|
2054
|
+
function resetCodexWebSocketAppendState(state) {
|
|
2055
|
+
state.canAppend = false;
|
|
2056
|
+
state.lastRequest = void 0;
|
|
2057
|
+
state.lastResponseId = void 0;
|
|
2058
|
+
state.lastResponseItems = void 0;
|
|
2059
|
+
}
|
|
2060
|
+
function recordCodexWebSocketFailure(state, activateFallback) {
|
|
2061
|
+
resetCodexWebSocketAppendState(state);
|
|
2062
|
+
if (state.connection && !state.connection.isConnecting()) {
|
|
2063
|
+
state.connection.close("fallback");
|
|
2064
|
+
state.connection = void 0;
|
|
2065
|
+
}
|
|
2066
|
+
state.lastFallbackAt = Date.now();
|
|
2067
|
+
if (activateFallback && !state.disableWebsocket) {
|
|
2068
|
+
state.disableWebsocket = true;
|
|
2069
|
+
state.fallbackCount += 1;
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
function getCodexWebSocketEnvValue() {
|
|
2073
|
+
if ($env.PI_CODEX_WEBSOCKET !== void 0) return $flag("PI_CODEX_WEBSOCKET");
|
|
2074
|
+
}
|
|
2075
|
+
function shouldUseCodexWebSocket(model, state, preferWebsockets) {
|
|
2076
|
+
if (model.preferWebsockets === false) return false;
|
|
2077
|
+
if (!state || state.disableWebsocket) return false;
|
|
2078
|
+
const envVal = getCodexWebSocketEnvValue();
|
|
2079
|
+
if (envVal !== void 0) return envVal;
|
|
2080
|
+
if (preferWebsockets === false) return false;
|
|
2081
|
+
return true;
|
|
2082
|
+
}
|
|
2083
|
+
const codexDiagnosticsTextEncoder = new TextEncoder();
|
|
2084
|
+
function jsonByteLength(value) {
|
|
2085
|
+
const json = JSON.stringify(value);
|
|
2086
|
+
return codexDiagnosticsTextEncoder.encode(json === void 0 ? "undefined" : json).byteLength;
|
|
2087
|
+
}
|
|
2088
|
+
function hashJson(value) {
|
|
2089
|
+
const json = JSON.stringify(value);
|
|
2090
|
+
return String(Bun.hash(json === void 0 ? "undefined" : json));
|
|
2091
|
+
}
|
|
2092
|
+
function parseCodexServiceTier(value) {
|
|
2093
|
+
switch (value) {
|
|
2094
|
+
case "auto":
|
|
2095
|
+
case "default":
|
|
2096
|
+
case "flex":
|
|
2097
|
+
case "scale":
|
|
2098
|
+
case "priority": return value;
|
|
2099
|
+
default: return;
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
function parseCodexResponseStatus(value) {
|
|
2103
|
+
switch (value) {
|
|
2104
|
+
case "completed":
|
|
2105
|
+
case "failed":
|
|
2106
|
+
case "in_progress":
|
|
2107
|
+
case "cancelled":
|
|
2108
|
+
case "queued":
|
|
2109
|
+
case "incomplete": return value;
|
|
2110
|
+
default: return;
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
function parseCodexResponseUsage(value) {
|
|
2114
|
+
if (!value || typeof value !== "object") return void 0;
|
|
2115
|
+
const usage = {};
|
|
2116
|
+
let hasUsage = false;
|
|
2117
|
+
if ("input_tokens" in value && typeof value.input_tokens === "number") {
|
|
2118
|
+
usage.input_tokens = value.input_tokens;
|
|
2119
|
+
hasUsage = true;
|
|
2120
|
+
}
|
|
2121
|
+
if ("output_tokens" in value && typeof value.output_tokens === "number") {
|
|
2122
|
+
usage.output_tokens = value.output_tokens;
|
|
2123
|
+
hasUsage = true;
|
|
2124
|
+
}
|
|
2125
|
+
if ("total_tokens" in value && typeof value.total_tokens === "number") {
|
|
2126
|
+
usage.total_tokens = value.total_tokens;
|
|
2127
|
+
hasUsage = true;
|
|
2128
|
+
}
|
|
2129
|
+
if ("prompt_cache_hit_tokens" in value && typeof value.prompt_cache_hit_tokens === "number") {
|
|
2130
|
+
usage.prompt_cache_hit_tokens = value.prompt_cache_hit_tokens;
|
|
2131
|
+
hasUsage = true;
|
|
2132
|
+
}
|
|
2133
|
+
if ("input_tokens_details" in value && value.input_tokens_details && typeof value.input_tokens_details === "object") {
|
|
2134
|
+
const details = value.input_tokens_details;
|
|
2135
|
+
const parsedDetails = {};
|
|
2136
|
+
let hasDetails = false;
|
|
2137
|
+
if ("cached_tokens" in details && typeof details.cached_tokens === "number") {
|
|
2138
|
+
parsedDetails.cached_tokens = details.cached_tokens;
|
|
2139
|
+
hasDetails = true;
|
|
2140
|
+
}
|
|
2141
|
+
if ("cache_write_tokens" in details && typeof details.cache_write_tokens === "number") {
|
|
2142
|
+
parsedDetails.cache_write_tokens = details.cache_write_tokens;
|
|
2143
|
+
hasDetails = true;
|
|
2144
|
+
}
|
|
2145
|
+
if ("orchestration_input_tokens" in details && typeof details.orchestration_input_tokens === "number") {
|
|
2146
|
+
parsedDetails.orchestration_input_tokens = details.orchestration_input_tokens;
|
|
2147
|
+
hasDetails = true;
|
|
2148
|
+
}
|
|
2149
|
+
if ("orchestration_input_cached_tokens" in details && typeof details.orchestration_input_cached_tokens === "number") {
|
|
2150
|
+
parsedDetails.orchestration_input_cached_tokens = details.orchestration_input_cached_tokens;
|
|
2151
|
+
hasDetails = true;
|
|
2152
|
+
}
|
|
2153
|
+
if (hasDetails) {
|
|
2154
|
+
usage.input_tokens_details = parsedDetails;
|
|
2155
|
+
hasUsage = true;
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
if ("output_tokens_details" in value && value.output_tokens_details && typeof value.output_tokens_details === "object") {
|
|
2159
|
+
const details = value.output_tokens_details;
|
|
2160
|
+
const parsedDetails = {};
|
|
2161
|
+
let hasDetails = false;
|
|
2162
|
+
if ("reasoning_tokens" in details && typeof details.reasoning_tokens === "number") {
|
|
2163
|
+
parsedDetails.reasoning_tokens = details.reasoning_tokens;
|
|
2164
|
+
hasDetails = true;
|
|
2165
|
+
}
|
|
2166
|
+
if ("orchestration_output_tokens" in details && typeof details.orchestration_output_tokens === "number") {
|
|
2167
|
+
parsedDetails.orchestration_output_tokens = details.orchestration_output_tokens;
|
|
2168
|
+
hasDetails = true;
|
|
2169
|
+
}
|
|
2170
|
+
if (hasDetails) {
|
|
2171
|
+
usage.output_tokens_details = parsedDetails;
|
|
2172
|
+
hasUsage = true;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
return hasUsage ? usage : void 0;
|
|
2176
|
+
}
|
|
2177
|
+
function describeCodexInputItemType(item) {
|
|
2178
|
+
if (item && typeof item === "object") {
|
|
2179
|
+
if ("type" in item && typeof item.type === "string") return item.type;
|
|
2180
|
+
if ("role" in item && typeof item.role === "string") return item.role;
|
|
2181
|
+
}
|
|
2182
|
+
return typeof item;
|
|
2183
|
+
}
|
|
2184
|
+
function createCodexOptionsHash(request) {
|
|
2185
|
+
const options = {};
|
|
2186
|
+
for (const key in request) {
|
|
2187
|
+
if (key === "input" || key === "previous_response_id" || key === "type" || key === "client_metadata") continue;
|
|
2188
|
+
options[key] = request[key];
|
|
2189
|
+
}
|
|
2190
|
+
return hashJson(options);
|
|
2191
|
+
}
|
|
2192
|
+
function buildCodexTurnRequestDiagnostics(request, transport, canAppendBeforeRequest) {
|
|
2193
|
+
const input = request.input;
|
|
2194
|
+
const inputItems = Array.isArray(input) ? input : [];
|
|
2195
|
+
const inputItemTypes = inputItems.map(describeCodexInputItemType);
|
|
2196
|
+
const promptCacheKey = typeof request.prompt_cache_key === "string" ? request.prompt_cache_key : void 0;
|
|
2197
|
+
const toolsHash = request.tools === void 0 ? void 0 : hashJson(request.tools);
|
|
2198
|
+
return {
|
|
2199
|
+
transport,
|
|
2200
|
+
previousResponseIdPresent: typeof request.previous_response_id === "string" && request.previous_response_id.length > 0,
|
|
2201
|
+
inputItemCount: inputItems.length,
|
|
2202
|
+
inputItemTypes,
|
|
2203
|
+
...inputItemTypes[0] ? { firstInputItemType: inputItemTypes[0] } : {},
|
|
2204
|
+
inputJsonBytes: jsonByteLength(inputItems),
|
|
2205
|
+
...promptCacheKey !== void 0 ? { promptCacheKey } : {},
|
|
2206
|
+
...toolsHash !== void 0 ? { toolsHash } : {},
|
|
2207
|
+
optionsHash: createCodexOptionsHash(request),
|
|
2208
|
+
canAppendBeforeRequest
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
function recordCodexTurnRequestDiagnostics(state, request, transport, canAppendBeforeRequest) {
|
|
2212
|
+
if (!state) return;
|
|
2213
|
+
const input = request.input;
|
|
2214
|
+
state.stats.lastInputItems = Array.isArray(input) ? input.length : 0;
|
|
2215
|
+
const previousResponseId = typeof request.previous_response_id === "string" ? request.previous_response_id : void 0;
|
|
2216
|
+
if (previousResponseId && previousResponseId.length > 0) {
|
|
2217
|
+
state.stats.deltaRequests += 1;
|
|
2218
|
+
state.stats.lastDeltaInputItems = state.stats.lastInputItems;
|
|
2219
|
+
state.stats.lastPreviousResponseId = previousResponseId;
|
|
2220
|
+
} else {
|
|
2221
|
+
state.stats.fullContextRequests += 1;
|
|
2222
|
+
state.stats.lastDeltaInputItems = void 0;
|
|
2223
|
+
state.stats.lastPreviousResponseId = void 0;
|
|
2224
|
+
}
|
|
2225
|
+
state.stats.lastTurn = { request: buildCodexTurnRequestDiagnostics(request, transport, canAppendBeforeRequest) };
|
|
2226
|
+
CODEX_DEBUG && debug("[codex] codex turn request diagnostics", { diagnostics: state.stats.lastTurn.request });
|
|
2227
|
+
}
|
|
2228
|
+
function recordCodexTurnUsageDiagnostics(state, rawUsage, displayedUsage) {
|
|
2229
|
+
if (!state?.stats.lastTurn || !rawUsage) return;
|
|
2230
|
+
const details = rawUsage.input_tokens_details;
|
|
2231
|
+
const outputDetails = rawUsage.output_tokens_details;
|
|
2232
|
+
const rawInputTokens = rawUsage.input_tokens ?? 0;
|
|
2233
|
+
const rawCachedTokens = details?.cached_tokens ?? rawUsage.prompt_cache_hit_tokens ?? 0;
|
|
2234
|
+
const usageDiagnostics = {
|
|
2235
|
+
rawInputTokens,
|
|
2236
|
+
rawCachedTokens,
|
|
2237
|
+
rawUncachedTokens: Math.max(0, rawInputTokens - rawCachedTokens),
|
|
2238
|
+
rawOutputTokens: rawUsage.output_tokens ?? 0,
|
|
2239
|
+
...typeof rawUsage.total_tokens === "number" ? { rawTotalTokens: rawUsage.total_tokens } : {},
|
|
2240
|
+
...typeof details?.orchestration_input_tokens === "number" ? { rawOrchestrationInputTokens: details.orchestration_input_tokens } : {},
|
|
2241
|
+
...typeof details?.orchestration_input_cached_tokens === "number" ? { rawOrchestrationCachedTokens: details.orchestration_input_cached_tokens } : {},
|
|
2242
|
+
...typeof outputDetails?.orchestration_output_tokens === "number" ? { rawOrchestrationOutputTokens: outputDetails.orchestration_output_tokens } : {},
|
|
2243
|
+
displayedInputTokens: displayedUsage.input,
|
|
2244
|
+
displayedOutputTokens: displayedUsage.output,
|
|
2245
|
+
displayedCacheReadTokens: displayedUsage.cacheRead,
|
|
2246
|
+
displayedCacheWriteTokens: displayedUsage.cacheWrite,
|
|
2247
|
+
displayedTotalTokens: displayedUsage.totalTokens,
|
|
2248
|
+
displayedOrchestrationInputTokens: displayedUsage.orchestration?.input ?? 0,
|
|
2249
|
+
displayedOrchestrationCacheReadTokens: displayedUsage.orchestration?.cacheRead ?? 0,
|
|
2250
|
+
displayedOrchestrationOutputTokens: displayedUsage.orchestration?.output ?? 0
|
|
2251
|
+
};
|
|
2252
|
+
state.stats.lastTurn = {
|
|
2253
|
+
...state.stats.lastTurn,
|
|
2254
|
+
usage: usageDiagnostics
|
|
2255
|
+
};
|
|
2256
|
+
CODEX_DEBUG && debug("[codex] codex turn diagnostics", { diagnostics: state.stats.lastTurn });
|
|
2257
|
+
}
|
|
2258
|
+
/**
|
|
2259
|
+
* Shape the next websocket turn's request body: when the session's append
|
|
2260
|
+
* baseline is intact (same options, strict history prefix), chain via
|
|
2261
|
+
* `previous_response_id` + delta-only `input`; otherwise break the chain and
|
|
2262
|
+
* replay the full transcript. SSE requests never chain — the HTTP endpoint's
|
|
2263
|
+
* request schema has no `previous_response_id` (codex-rs carries it only on
|
|
2264
|
+
* websocket `response.create` frames) and strict gateway validators 400 it
|
|
2265
|
+
* with `{"detail":"Unsupported parameter: previous_response_id"}`.
|
|
2266
|
+
*/
|
|
2267
|
+
function buildCodexChainedRequestBody(requestBody, state) {
|
|
2268
|
+
const chainable = state?.canAppend === true;
|
|
2269
|
+
const appendInput = chainable ? buildResponsesDeltaInput(state.lastRequest, state.lastResponseItems, requestBody) : null;
|
|
2270
|
+
if (appendInput && appendInput.length > 0 && state?.lastResponseId) return {
|
|
2271
|
+
...requestBody,
|
|
2272
|
+
previous_response_id: state.lastResponseId,
|
|
2273
|
+
input: appendInput
|
|
2274
|
+
};
|
|
2275
|
+
if (chainable && state) {
|
|
2276
|
+
CODEX_DEBUG && debug("[codex] codex append reset", {
|
|
2277
|
+
hadTurnStateHeader: Boolean(state.turnState),
|
|
2278
|
+
hadModelsEtagHeader: Boolean(state.modelsEtag)
|
|
2279
|
+
});
|
|
2280
|
+
resetCodexWebSocketAppendState(state);
|
|
2281
|
+
state.turnState = void 0;
|
|
2282
|
+
state.modelsEtag = void 0;
|
|
2283
|
+
}
|
|
2284
|
+
return requestBody;
|
|
2285
|
+
}
|
|
2286
|
+
function toWebSocketUrl(url) {
|
|
2287
|
+
const parsed = new URL(url);
|
|
2288
|
+
if (parsed.protocol === "https:") parsed.protocol = "wss:";
|
|
2289
|
+
else if (parsed.protocol === "http:") parsed.protocol = "ws:";
|
|
2290
|
+
return parsed.toString();
|
|
2291
|
+
}
|
|
2292
|
+
function headersToRecord(headers) {
|
|
2293
|
+
const result = {};
|
|
2294
|
+
for (const [key, value] of headers.entries()) result[key] = value;
|
|
2295
|
+
return result;
|
|
2296
|
+
}
|
|
2297
|
+
var CodexWebSocketConnection = class {
|
|
2298
|
+
#url;
|
|
2299
|
+
#headers;
|
|
2300
|
+
#proxy;
|
|
2301
|
+
#onHandshakeHeaders;
|
|
2302
|
+
#socket = null;
|
|
2303
|
+
#queue = [];
|
|
2304
|
+
#waiters = [];
|
|
2305
|
+
#connectPromise;
|
|
2306
|
+
#activeRequest = false;
|
|
2307
|
+
#streamObserver;
|
|
2308
|
+
#heartbeatInterval;
|
|
2309
|
+
#removePongListener;
|
|
2310
|
+
#handshakeHeaders;
|
|
2311
|
+
#debugResponseLog;
|
|
2312
|
+
/**
|
|
2313
|
+
* Wall-clock of the most recent inbound activity on this socket — any
|
|
2314
|
+
* decoded message, any pong, or the moment the handshake completed. Used
|
|
2315
|
+
* by {@link isHealthyForReuse} so we don't write a continuation frame into
|
|
2316
|
+
* a TCP-open-but-server-evicted socket whose `readyState` still says OPEN.
|
|
2317
|
+
*/
|
|
2318
|
+
#lastInboundAt = 0;
|
|
2319
|
+
/** Wall-clock of the last heartbeat ping we issued; 0 if none yet. */
|
|
2320
|
+
#lastPingAt = 0;
|
|
2321
|
+
/**
|
|
2322
|
+
* Most recent `response.id` accepted on this socket, retained across
|
|
2323
|
+
* requests. Lets the next request drop a trailing/duplicate frame from the
|
|
2324
|
+
* previous (cleanly-completed) response that outlived the queue drain.
|
|
2325
|
+
*/
|
|
2326
|
+
#lastSeenResponseId;
|
|
2327
|
+
constructor(url, headers, options) {
|
|
2328
|
+
this.#url = url;
|
|
2329
|
+
this.#headers = headers;
|
|
2330
|
+
this.#proxy = options.proxy;
|
|
2331
|
+
this.#onHandshakeHeaders = options.onHandshakeHeaders;
|
|
2332
|
+
}
|
|
2333
|
+
isOpen() {
|
|
2334
|
+
return this.#socket?.readyState === WebSocket.OPEN;
|
|
2335
|
+
}
|
|
2336
|
+
/** True while a handshake (possibly started by another caller) is still in flight. */
|
|
2337
|
+
isConnecting() {
|
|
2338
|
+
return this.#connectPromise !== void 0;
|
|
2339
|
+
}
|
|
2340
|
+
/**
|
|
2341
|
+
* Stricter variant of {@link isOpen} for the connection-pool reuse gate.
|
|
2342
|
+
* Refuses sockets that have been silent past {@link CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS}.
|
|
2343
|
+
*
|
|
2344
|
+
* Bun's `WebSocket` does not always surface server-side eviction (no
|
|
2345
|
+
* `onclose`, no `onerror`), so a socket can sit in readyState OPEN long
|
|
2346
|
+
* after the upstream has dropped it. Reusing such a socket sends the next
|
|
2347
|
+
* `response.create` into a half-open write buffer and parks the reader
|
|
2348
|
+
* until the first-event / idle timeout fires (issue #1450). Forcing a
|
|
2349
|
+
* reconnect on any suspect socket trades a sub-second handshake for a
|
|
2350
|
+
* 60–300 s stall.
|
|
2351
|
+
*/
|
|
2352
|
+
isHealthyForReuse() {
|
|
2353
|
+
if (!this.isOpen()) return false;
|
|
2354
|
+
const maxIdleMs = CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS;
|
|
2355
|
+
if (maxIdleMs <= 0) return true;
|
|
2356
|
+
if (this.#lastInboundAt === 0) return false;
|
|
2357
|
+
return Date.now() - this.#lastInboundAt <= maxIdleMs;
|
|
2358
|
+
}
|
|
2359
|
+
matchesAuth(headers) {
|
|
2360
|
+
return this.#headers.authorization === headers.authorization;
|
|
2361
|
+
}
|
|
2362
|
+
close(reason = "done") {
|
|
2363
|
+
if (this.#socket && (this.#socket.readyState === WebSocket.OPEN || this.#socket.readyState === WebSocket.CONNECTING)) this.#socket.close(1e3, reason);
|
|
2364
|
+
this.#socket = null;
|
|
2365
|
+
this.#stopHeartbeat();
|
|
2366
|
+
}
|
|
2367
|
+
async connect(signal) {
|
|
2368
|
+
if (this.isOpen()) return;
|
|
2369
|
+
if (this.#connectPromise) {
|
|
2370
|
+
time("codexWs:awaitSharedHandshake");
|
|
2371
|
+
await this.#connectPromise;
|
|
2372
|
+
return;
|
|
2373
|
+
}
|
|
2374
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
2375
|
+
this.#connectPromise = promise;
|
|
2376
|
+
const socket = new WebSocket(this.#url, {
|
|
2377
|
+
headers: this.#headers,
|
|
2378
|
+
proxy: this.#proxy
|
|
2379
|
+
});
|
|
2380
|
+
socket.binaryType = "nodebuffer";
|
|
2381
|
+
this.#socket = socket;
|
|
2382
|
+
let settled = false;
|
|
2383
|
+
let timeout;
|
|
2384
|
+
const clearPending = () => {
|
|
2385
|
+
if (timeout !== void 0) {
|
|
2386
|
+
clearTimeout(timeout);
|
|
2387
|
+
timeout = void 0;
|
|
2388
|
+
}
|
|
2389
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
2390
|
+
};
|
|
2391
|
+
const onAbort = () => {
|
|
2392
|
+
socket.close(1e3, "aborted");
|
|
2393
|
+
if (!settled) {
|
|
2394
|
+
settled = true;
|
|
2395
|
+
clearPending();
|
|
2396
|
+
reject(new CodexWebSocketTransportError(`request was aborted`));
|
|
2397
|
+
}
|
|
2398
|
+
};
|
|
2399
|
+
if (signal) {
|
|
2400
|
+
if (signal.aborted) onAbort();
|
|
2401
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
2402
|
+
}
|
|
2403
|
+
if (!settled) timeout = setTimeout(() => {
|
|
2404
|
+
socket.close(1e3, "connect-timeout");
|
|
2405
|
+
if (!settled) {
|
|
2406
|
+
settled = true;
|
|
2407
|
+
clearPending();
|
|
2408
|
+
reject(new CodexWebSocketTransportError(`connection timeout`));
|
|
2409
|
+
}
|
|
2410
|
+
}, CODEX_WEBSOCKET_CONNECT_TIMEOUT_MS);
|
|
2411
|
+
socket.onopen = (event) => {
|
|
2412
|
+
if (!settled) {
|
|
2413
|
+
settled = true;
|
|
2414
|
+
clearPending();
|
|
2415
|
+
this.#lastInboundAt = Date.now();
|
|
2416
|
+
this.#captureHandshakeHeaders(socket, event);
|
|
2417
|
+
this.#startHeartbeat(socket);
|
|
2418
|
+
resolve();
|
|
2419
|
+
}
|
|
2420
|
+
};
|
|
2421
|
+
socket.onerror = (event) => {
|
|
2422
|
+
const eventRecord = event;
|
|
2423
|
+
const error = new CodexWebSocketTransportError(`websocket error: ${typeof eventRecord.message === "string" && eventRecord.message || eventRecord.error instanceof Error && eventRecord.error.message || String(event.type)}`);
|
|
2424
|
+
if (!settled) {
|
|
2425
|
+
settled = true;
|
|
2426
|
+
clearPending();
|
|
2427
|
+
reject(error);
|
|
2428
|
+
return;
|
|
2429
|
+
}
|
|
2430
|
+
this.#push(error);
|
|
2431
|
+
};
|
|
2432
|
+
socket.onclose = (event) => {
|
|
2433
|
+
this.#socket = null;
|
|
2434
|
+
this.#stopHeartbeat();
|
|
2435
|
+
if (!settled) {
|
|
2436
|
+
settled = true;
|
|
2437
|
+
clearPending();
|
|
2438
|
+
reject(new CodexWebSocketTransportError(`websocket closed before open (${event.code})`));
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2441
|
+
this.#push(new CodexWebSocketTransportError(`websocket closed (${event.code})`));
|
|
2442
|
+
this.#push(null);
|
|
2443
|
+
};
|
|
2444
|
+
socket.onmessage = (event) => {
|
|
2445
|
+
this.#lastInboundAt = Date.now();
|
|
2446
|
+
this.#writeDebugWebSocketFrame(event.data);
|
|
2447
|
+
try {
|
|
2448
|
+
const text = typeof event.data === "string" ? event.data : Buffer.from(event.data).toString("utf-8");
|
|
2449
|
+
if (!text) return;
|
|
2450
|
+
const parsed = JSON.parse(text);
|
|
2451
|
+
if (parsed.type === "error" && typeof parsed.error === "object" && parsed.error) {
|
|
2452
|
+
const inner = parsed.error;
|
|
2453
|
+
if (typeof parsed.code !== "string" && typeof inner.code === "string") parsed.code = inner.code;
|
|
2454
|
+
if (typeof parsed.message !== "string" && typeof inner.message === "string") parsed.message = inner.message;
|
|
2455
|
+
}
|
|
2456
|
+
notifyCodexWebSocketInbound(this.#streamObserver, parsed, text);
|
|
2457
|
+
this.#push(parsed);
|
|
2458
|
+
} catch (error) {
|
|
2459
|
+
notifyCodexWebSocketMalformed(this.#streamObserver, event.data, error);
|
|
2460
|
+
this.#push(new CodexWebSocketTransportError(`${String(error)}`));
|
|
2461
|
+
}
|
|
2462
|
+
};
|
|
2463
|
+
time("codexWs:awaitTcpHandshake");
|
|
2464
|
+
try {
|
|
2465
|
+
await promise;
|
|
2466
|
+
} finally {
|
|
2467
|
+
this.#connectPromise = void 0;
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
async *streamRequest(request, timeouts, signal, onSseEvent) {
|
|
2471
|
+
if (!this.#socket || this.#socket.readyState !== WebSocket.OPEN) throw new CodexWebSocketTransportError(`websocket connection is unavailable`);
|
|
2472
|
+
if (this.#activeRequest) throw new CodexWebSocketTransportError(`websocket request already in progress`);
|
|
2473
|
+
if (signal?.aborted) throw new CodexWebSocketTransportError(`request was aborted`);
|
|
2474
|
+
this.#activeRequest = true;
|
|
2475
|
+
this.#streamObserver = onSseEvent;
|
|
2476
|
+
this.#dropStaleFrames();
|
|
2477
|
+
const onAbort = () => {
|
|
2478
|
+
this.close("aborted");
|
|
2479
|
+
this.#push(new CodexWebSocketTransportError(`request was aborted`));
|
|
2480
|
+
};
|
|
2481
|
+
if (signal) signal.addEventListener("abort", onAbort, { once: true });
|
|
2482
|
+
try {
|
|
2483
|
+
const debugSession = isRequestDebugEnabled() ? await createRequestDebugSession({
|
|
2484
|
+
protocol: "websocket",
|
|
2485
|
+
method: "POST",
|
|
2486
|
+
url: this.#url,
|
|
2487
|
+
headers: this.#headers,
|
|
2488
|
+
body: request
|
|
2489
|
+
}) : void 0;
|
|
2490
|
+
this.#debugResponseLog = debugSession ? await debugSession.openResponseLog("WebSocket 101 Switching Protocols", this.#handshakeHeaders) : void 0;
|
|
2491
|
+
const requestPayload = JSON.stringify(request);
|
|
2492
|
+
notifyCodexWebSocketOutbound(onSseEvent, request, requestPayload);
|
|
2493
|
+
const socket = this.#socket;
|
|
2494
|
+
if (!socket || socket.readyState !== WebSocket.OPEN) throw new CodexWebSocketTransportError(`websocket connection is unavailable`);
|
|
2495
|
+
try {
|
|
2496
|
+
socket.send(requestPayload);
|
|
2497
|
+
} catch (error) {
|
|
2498
|
+
throw new CodexWebSocketTransportError(`websocket send failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2499
|
+
}
|
|
2500
|
+
let sawFirstEvent = false;
|
|
2501
|
+
const { idleTimeoutMs, firstEventTimeoutMs } = timeouts;
|
|
2502
|
+
let lastProgressAt = Date.now();
|
|
2503
|
+
let lastProgressEventType;
|
|
2504
|
+
let lastEventAt = lastProgressAt;
|
|
2505
|
+
let lastEventType;
|
|
2506
|
+
let activeResponseId;
|
|
2507
|
+
let lastSequence;
|
|
2508
|
+
const priorResponseId = this.#lastSeenResponseId;
|
|
2509
|
+
while (true) {
|
|
2510
|
+
let timeoutMs;
|
|
2511
|
+
let timeoutReason;
|
|
2512
|
+
if (sawFirstEvent) {
|
|
2513
|
+
timeoutReason = createCodexWebSocketTimeoutMessage("idle timeout waiting for websocket", {
|
|
2514
|
+
lastEventAt,
|
|
2515
|
+
lastEventType,
|
|
2516
|
+
lastProgressAt,
|
|
2517
|
+
lastProgressEventType
|
|
2518
|
+
});
|
|
2519
|
+
if (idleTimeoutMs !== void 0 && idleTimeoutMs > 0) {
|
|
2520
|
+
timeoutMs = idleTimeoutMs - (Date.now() - lastProgressAt);
|
|
2521
|
+
if (timeoutMs <= 0) {
|
|
2522
|
+
CODEX_DEBUG && debug("[codex] codex websocket idle timeout", {
|
|
2523
|
+
lastEventType,
|
|
2524
|
+
lastProgressEventType,
|
|
2525
|
+
msSinceLastEvent: Date.now() - lastEventAt,
|
|
2526
|
+
msSinceLastProgress: Date.now() - lastProgressAt
|
|
2527
|
+
});
|
|
2528
|
+
throw new CodexWebSocketTransportError(`${timeoutReason}`);
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
} else {
|
|
2532
|
+
timeoutReason = createCodexWebSocketTimeoutMessage("timeout waiting for first websocket event", {
|
|
2533
|
+
lastEventAt,
|
|
2534
|
+
lastEventType,
|
|
2535
|
+
lastProgressAt,
|
|
2536
|
+
lastProgressEventType
|
|
2537
|
+
});
|
|
2538
|
+
if (firstEventTimeoutMs !== void 0 && firstEventTimeoutMs > 0) timeoutMs = firstEventTimeoutMs;
|
|
2539
|
+
}
|
|
2540
|
+
const next = await this.#nextMessage(timeoutMs, timeoutReason);
|
|
2541
|
+
if (next instanceof Error) throw next;
|
|
2542
|
+
if (next === null) throw new CodexWebSocketTransportError(`websocket closed before response completion`);
|
|
2543
|
+
const eventType = typeof next.type === "string" ? next.type : "";
|
|
2544
|
+
const frameResponseId = extractCodexFrameResponseId(next);
|
|
2545
|
+
const frameSequence = extractCodexFrameSequenceNumber(next);
|
|
2546
|
+
if (frameResponseId !== void 0) {
|
|
2547
|
+
if (activeResponseId === void 0) {
|
|
2548
|
+
if (priorResponseId !== void 0 && frameResponseId === priorResponseId) continue;
|
|
2549
|
+
activeResponseId = frameResponseId;
|
|
2550
|
+
} else if (frameResponseId !== activeResponseId) {
|
|
2551
|
+
this.close("stale-frame");
|
|
2552
|
+
throw new CodexWebSocketTransportError(`websocket frame for response ${frameResponseId} interleaved into active response ${activeResponseId}`);
|
|
2553
|
+
}
|
|
2554
|
+
this.#lastSeenResponseId = frameResponseId;
|
|
2555
|
+
}
|
|
2556
|
+
if (frameSequence !== void 0) {
|
|
2557
|
+
if (activeResponseId !== void 0 && lastSequence !== void 0 && frameSequence < lastSequence) {
|
|
2558
|
+
this.close("stale-frame");
|
|
2559
|
+
throw new CodexWebSocketTransportError(`websocket sequence_number ${frameSequence} regressed below ${lastSequence} within response ${activeResponseId}`);
|
|
2560
|
+
}
|
|
2561
|
+
lastSequence = frameSequence;
|
|
2562
|
+
}
|
|
2563
|
+
sawFirstEvent = true;
|
|
2564
|
+
lastEventAt = Date.now();
|
|
2565
|
+
lastEventType = eventType || void 0;
|
|
2566
|
+
if (isCodexStreamProgressEvent(next)) {
|
|
2567
|
+
lastProgressAt = lastEventAt;
|
|
2568
|
+
lastProgressEventType = lastEventType;
|
|
2569
|
+
}
|
|
2570
|
+
yield next;
|
|
2571
|
+
if (eventType === "response.completed" || eventType === "response.done" || eventType === "response.incomplete" || eventType === "response.failed" || eventType === "error") break;
|
|
2572
|
+
}
|
|
2573
|
+
} finally {
|
|
2574
|
+
this.#activeRequest = false;
|
|
2575
|
+
this.#streamObserver = void 0;
|
|
2576
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
2577
|
+
const debugResponseLog = this.#debugResponseLog;
|
|
2578
|
+
this.#debugResponseLog = void 0;
|
|
2579
|
+
await debugResponseLog?.close();
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
#captureHandshakeHeaders(socket, openEvent) {
|
|
2583
|
+
const headers = extractCodexWebSocketHandshakeHeaders(socket, openEvent);
|
|
2584
|
+
if (!headers) return;
|
|
2585
|
+
this.#handshakeHeaders = headers;
|
|
2586
|
+
this.#onHandshakeHeaders?.(headers);
|
|
2587
|
+
}
|
|
2588
|
+
#writeDebugWebSocketFrame(data) {
|
|
2589
|
+
const log = this.#debugResponseLog;
|
|
2590
|
+
if (!log) return;
|
|
2591
|
+
if (typeof data === "string") {
|
|
2592
|
+
log.write(data);
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
2595
|
+
if (data instanceof Uint8Array) {
|
|
2596
|
+
log.write(data);
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
if (data instanceof ArrayBuffer) {
|
|
2600
|
+
log.write(new Uint8Array(data));
|
|
2601
|
+
return;
|
|
2602
|
+
}
|
|
2603
|
+
log.write(String(data));
|
|
2604
|
+
}
|
|
2605
|
+
#startHeartbeat(socket) {
|
|
2606
|
+
this.#stopHeartbeat();
|
|
2607
|
+
const intervalMs = CODEX_WEBSOCKET_PING_INTERVAL_MS;
|
|
2608
|
+
if (intervalMs <= 0) return;
|
|
2609
|
+
this.#lastPingAt = 0;
|
|
2610
|
+
const socketEventTarget = socket;
|
|
2611
|
+
const onPong = () => {
|
|
2612
|
+
this.#lastInboundAt = Date.now();
|
|
2613
|
+
};
|
|
2614
|
+
if (typeof socketEventTarget.addEventListener === "function" && typeof socketEventTarget.removeEventListener === "function") {
|
|
2615
|
+
socketEventTarget.addEventListener("pong", onPong);
|
|
2616
|
+
this.#removePongListener = () => socketEventTarget.removeEventListener("pong", onPong);
|
|
2617
|
+
}
|
|
2618
|
+
this.#heartbeatInterval = setInterval(() => {
|
|
2619
|
+
if (this.#socket !== socket || socket.readyState !== WebSocket.OPEN) {
|
|
2620
|
+
this.#stopHeartbeat();
|
|
2621
|
+
return;
|
|
2622
|
+
}
|
|
2623
|
+
const pongTimeoutMs = CODEX_WEBSOCKET_PONG_TIMEOUT_MS;
|
|
2624
|
+
if (pongTimeoutMs > 0 && this.#lastPingAt > 0 && this.#lastPingAt > this.#lastInboundAt && Date.now() - this.#lastPingAt > pongTimeoutMs) {
|
|
2625
|
+
this.#failQueue(new CodexWebSocketTransportError(`websocket pong timeout`), "pong-timeout");
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
if (typeof socket.ping !== "function") {
|
|
2629
|
+
this.#stopHeartbeat();
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
try {
|
|
2633
|
+
socket.ping();
|
|
2634
|
+
this.#lastPingAt = Date.now();
|
|
2635
|
+
} catch (error) {
|
|
2636
|
+
this.#failQueue(new CodexWebSocketTransportError(`websocket ping failed: ${error instanceof Error ? error.message : String(error)}`), "ping-failed");
|
|
2637
|
+
}
|
|
2638
|
+
}, intervalMs);
|
|
2639
|
+
this.#heartbeatInterval.unref();
|
|
2640
|
+
}
|
|
2641
|
+
#stopHeartbeat() {
|
|
2642
|
+
if (this.#heartbeatInterval) {
|
|
2643
|
+
clearInterval(this.#heartbeatInterval);
|
|
2644
|
+
this.#heartbeatInterval = void 0;
|
|
2645
|
+
}
|
|
2646
|
+
if (this.#removePongListener) {
|
|
2647
|
+
this.#removePongListener();
|
|
2648
|
+
this.#removePongListener = void 0;
|
|
2649
|
+
}
|
|
2650
|
+
this.#lastPingAt = 0;
|
|
2651
|
+
}
|
|
2652
|
+
#failQueue(error, closeReason) {
|
|
2653
|
+
CODEX_DEBUG && debug("[codex] codex websocket transport failure", {
|
|
2654
|
+
error: error.message,
|
|
2655
|
+
closeReason
|
|
2656
|
+
});
|
|
2657
|
+
this.#queue.length = 0;
|
|
2658
|
+
this.#queue.push(error);
|
|
2659
|
+
this.close(closeReason);
|
|
2660
|
+
this.#wakeWaiters();
|
|
2661
|
+
}
|
|
2662
|
+
/**
|
|
2663
|
+
* Discard data frames from a previous request that remained in `#queue`
|
|
2664
|
+
* after the consumer broke out on the terminal event. Preserves any queued
|
|
2665
|
+
* transport error (from `onerror` / `onclose` / `#failQueue`) so the next
|
|
2666
|
+
* `#nextMessage` surfaces the death signal instead of waiting it out.
|
|
2667
|
+
*
|
|
2668
|
+
* Returns the number of frames dropped (test/debug visibility only).
|
|
2669
|
+
*/
|
|
2670
|
+
#dropStaleFrames() {
|
|
2671
|
+
if (this.#queue.length === 0) return 0;
|
|
2672
|
+
const surviving = this.#queue.filter((item) => item instanceof Error);
|
|
2673
|
+
const dropped = this.#queue.length - surviving.length;
|
|
2674
|
+
if (dropped === 0) return 0;
|
|
2675
|
+
this.#queue.length = 0;
|
|
2676
|
+
for (const item of surviving) this.#queue.push(item);
|
|
2677
|
+
CODEX_DEBUG && debug("[codex] codex websocket dropped stale frames before request", { dropped });
|
|
2678
|
+
return dropped;
|
|
2679
|
+
}
|
|
2680
|
+
#wakeWaiters() {
|
|
2681
|
+
for (;;) {
|
|
2682
|
+
const waiter = this.#waiters.shift();
|
|
2683
|
+
if (!waiter) break;
|
|
2684
|
+
waiter();
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
#push(item) {
|
|
2688
|
+
if (item instanceof Error) {
|
|
2689
|
+
this.#queue.push(item);
|
|
2690
|
+
this.#wakeWaiters();
|
|
2691
|
+
return;
|
|
2692
|
+
}
|
|
2693
|
+
if (item !== null && this.#queue.length >= CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY) {
|
|
2694
|
+
this.#failQueue(new CodexWebSocketTransportError(`websocket message queue exceeded ${CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY} items`), "queue-overflow");
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
this.#queue.push(item);
|
|
2698
|
+
const waiter = this.#waiters.shift();
|
|
2699
|
+
if (waiter) waiter();
|
|
2700
|
+
}
|
|
2701
|
+
async #nextMessage(timeoutMs, timeoutReason) {
|
|
2702
|
+
while (this.#queue.length === 0) {
|
|
2703
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
2704
|
+
this.#waiters.push(resolve);
|
|
2705
|
+
let timedOut = false;
|
|
2706
|
+
let timeout;
|
|
2707
|
+
if (timeoutMs !== void 0 && timeoutMs > 0) timeout = setTimeout(() => {
|
|
2708
|
+
timedOut = true;
|
|
2709
|
+
const waiterIndex = this.#waiters.indexOf(resolve);
|
|
2710
|
+
if (waiterIndex >= 0) this.#waiters.splice(waiterIndex, 1);
|
|
2711
|
+
resolve();
|
|
2712
|
+
}, timeoutMs);
|
|
2713
|
+
await promise;
|
|
2714
|
+
if (timeout) clearTimeout(timeout);
|
|
2715
|
+
if (timedOut && this.#queue.length === 0) return new CodexWebSocketTransportError(`${timeoutReason}`);
|
|
2716
|
+
}
|
|
2717
|
+
return this.#queue.shift() ?? null;
|
|
2718
|
+
}
|
|
2719
|
+
};
|
|
2720
|
+
async function getOrCreateCodexWebSocketConnection(state, url, headers, provider, signal) {
|
|
2721
|
+
const proxy = getProxyForUrl(provider, new URL(url));
|
|
2722
|
+
const headerRecord = headersToRecord(headers);
|
|
2723
|
+
for (let joinAttempt = 0; joinAttempt < 3; joinAttempt += 1) {
|
|
2724
|
+
const pending = state.connection;
|
|
2725
|
+
if (!pending || pending.isOpen() || !pending.isConnecting()) break;
|
|
2726
|
+
try {
|
|
2727
|
+
await pending.connect(signal);
|
|
2728
|
+
} catch {}
|
|
2729
|
+
}
|
|
2730
|
+
if (state.connection?.isOpen()) {
|
|
2731
|
+
if (!state.connection.matchesAuth(headerRecord)) {
|
|
2732
|
+
state.connection.close("token-refresh");
|
|
2733
|
+
resetCodexWebSocketAppendState(state);
|
|
2734
|
+
} else if (state.connection.isHealthyForReuse()) {
|
|
2735
|
+
time("codexWs:reuseOpenSocket");
|
|
2736
|
+
return state.connection;
|
|
2737
|
+
} else {
|
|
2738
|
+
CODEX_DEBUG && debug("[codex] codex websocket reuse rejected by health check", {});
|
|
2739
|
+
state.connection.close("stale-reuse");
|
|
2740
|
+
resetCodexWebSocketAppendState(state);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
state.connection?.close("reconnect");
|
|
2744
|
+
resetCodexWebSocketAppendState(state);
|
|
2745
|
+
time("codexWs:newSocket");
|
|
2746
|
+
state.connection = new CodexWebSocketConnection(url, headerRecord, {
|
|
2747
|
+
onHandshakeHeaders: (handshakeHeaders) => {
|
|
2748
|
+
updateCodexSessionMetadataFromHeaders(state, handshakeHeaders);
|
|
2749
|
+
},
|
|
2750
|
+
proxy
|
|
2751
|
+
});
|
|
2752
|
+
await state.connection.connect(signal);
|
|
2753
|
+
return state.connection;
|
|
2754
|
+
}
|
|
2755
|
+
/**
|
|
2756
|
+
* Compress an SSE request body with zstd. Returns `undefined` when
|
|
2757
|
+
* compression is disabled or fails, in which case the caller sends the
|
|
2758
|
+
* plain JSON string without a `content-encoding` header.
|
|
2759
|
+
*/
|
|
2760
|
+
function compressCodexRequestBody(bodyJson, baseUrl) {
|
|
2761
|
+
if (!isOfficialCodexApiUrl(baseUrl) || !$flag("PI_CODEX_ZSTD", true)) return void 0;
|
|
2762
|
+
try {
|
|
2763
|
+
return Bun.zstdCompressSync(bodyJson, { level: 3 });
|
|
2764
|
+
} catch (error) {
|
|
2765
|
+
CODEX_DEBUG && debug("[codex] codex request body compression failed", { error: error instanceof Error ? error.message : String(error) });
|
|
2766
|
+
return;
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
async function openCodexSseEventStream(url, requestHeaders, accountId, apiKey, sessionId, body, state, responsesLite, codexClientVersion, requestMetadata, signal, firstEventTimeoutMs, codexSseMaxAttempts, onSseEvent, fetchOverride) {
|
|
2770
|
+
const headers = createCodexHeaders(requestHeaders, accountId, apiKey, codexClientVersion, sessionId, "sse", state, responsesLite, requestMetadata, await getCodexAttestationHeader(accountId));
|
|
2771
|
+
let clearPreResponseTimeout;
|
|
2772
|
+
const fetchAttempt = async (input, init) => {
|
|
2773
|
+
try {
|
|
2774
|
+
return await (fetchOverride ?? fetch)(input, init);
|
|
2775
|
+
} finally {
|
|
2776
|
+
clearPreResponseTimeout?.();
|
|
2777
|
+
clearPreResponseTimeout = void 0;
|
|
2778
|
+
}
|
|
2779
|
+
};
|
|
2780
|
+
const bodyJson = JSON.stringify(body);
|
|
2781
|
+
const compressedBody = compressCodexRequestBody(bodyJson, url);
|
|
2782
|
+
if (compressedBody !== void 0) headers.set("content-encoding", "zstd");
|
|
2783
|
+
CODEX_DEBUG && debug("[codex] codex request", {
|
|
2784
|
+
url,
|
|
2785
|
+
model: body.model,
|
|
2786
|
+
headers: redactHeaders(headers),
|
|
2787
|
+
sentTurnStateHeader: headers.has(X_CODEX_TURN_STATE_HEADER),
|
|
2788
|
+
sentModelsEtagHeader: headers.has(X_MODELS_ETAG_HEADER)
|
|
2789
|
+
});
|
|
2790
|
+
const send = (requestBody) => fetchWithRetry(url, {
|
|
2791
|
+
method: "POST",
|
|
2792
|
+
headers,
|
|
2793
|
+
body: requestBody,
|
|
2794
|
+
signal,
|
|
2795
|
+
prepareInit: () => {
|
|
2796
|
+
const watchdog = armPreResponseTimeout(signal, firstEventTimeoutMs);
|
|
2797
|
+
clearPreResponseTimeout = watchdog.clear;
|
|
2798
|
+
return { signal: watchdog.signal };
|
|
2799
|
+
},
|
|
2800
|
+
maxAttempts: resolveCodexSseMaxAttempts(codexSseMaxAttempts),
|
|
2801
|
+
defaultDelayMs: (attempt) => CODEX_RETRY_DELAY_MS * (attempt + 1),
|
|
2802
|
+
maxDelayMs: CODEX_RATE_LIMIT_BUDGET_MS,
|
|
2803
|
+
fetch: fetchAttempt,
|
|
2804
|
+
timeout: false
|
|
2805
|
+
});
|
|
2806
|
+
let response;
|
|
2807
|
+
try {
|
|
2808
|
+
response = await send(compressedBody ?? bodyJson);
|
|
2809
|
+
if (compressedBody !== void 0 && (response.status === 400 || response.status === 415)) {
|
|
2810
|
+
const rejectedStatus = response.status;
|
|
2811
|
+
await response.body?.cancel();
|
|
2812
|
+
headers.delete("content-encoding");
|
|
2813
|
+
CODEX_DEBUG && debug("[codex] retrying request without zstd after encoding rejection", {
|
|
2814
|
+
url,
|
|
2815
|
+
status: rejectedStatus
|
|
2816
|
+
});
|
|
2817
|
+
response = await send(bodyJson);
|
|
2818
|
+
}
|
|
2819
|
+
} finally {
|
|
2820
|
+
clearPreResponseTimeout?.();
|
|
2821
|
+
}
|
|
2822
|
+
CODEX_DEBUG && debug("[codex] codex response", {
|
|
2823
|
+
url: response.url,
|
|
2824
|
+
status: response.status,
|
|
2825
|
+
statusText: response.statusText,
|
|
2826
|
+
contentType: response.headers.get("content-type") || null,
|
|
2827
|
+
cfRay: response.headers.get("cf-ray") || null
|
|
2828
|
+
});
|
|
2829
|
+
if (!response.ok) throw await CodexApiError.fromResponse(response);
|
|
2830
|
+
updateCodexSessionMetadataFromHeaders(state, response.headers);
|
|
2831
|
+
if (!response.body) throw new CodexProviderStreamError("No response body", false);
|
|
2832
|
+
return readSseJson(response.body, signal, (event) => onSseEvent?.({
|
|
2833
|
+
event: event.event,
|
|
2834
|
+
data: event.data,
|
|
2835
|
+
raw: [...event.raw]
|
|
2836
|
+
}, void 0));
|
|
2837
|
+
}
|
|
2838
|
+
function createCodexHeaders(initHeaders, accountId, accessToken, codexClientVersion, sessionId, transport = "sse", state, responsesLite = false, requestMetadata, attestation) {
|
|
2839
|
+
const headers = new Headers(initHeaders ?? {});
|
|
2840
|
+
headers.delete("x-api-key");
|
|
2841
|
+
headers.set("Authorization", `Bearer ${accessToken}`);
|
|
2842
|
+
if (accountId) headers.set(OPENAI_HEADERS.ACCOUNT_ID, accountId);
|
|
2843
|
+
if (attestation) headers.set(OPENAI_HEADERS.ATTESTATION, attestation);
|
|
2844
|
+
else headers.delete(OPENAI_HEADERS.ATTESTATION);
|
|
2845
|
+
const betaHeader = transport === "websocket" ? OPENAI_HEADER_VALUES.BETA_RESPONSES_WEBSOCKETS_V2 : OPENAI_HEADER_VALUES.BETA_RESPONSES;
|
|
2846
|
+
headers.delete(OPENAI_HEADERS.BETA);
|
|
2847
|
+
headers.delete("openai-beta");
|
|
2848
|
+
headers.set(OPENAI_HEADERS.BETA, betaHeader);
|
|
2849
|
+
headers.set(OPENAI_HEADERS.ORIGINATOR, OPENAI_HEADER_VALUES.ORIGINATOR_CODEX);
|
|
2850
|
+
headers.set(OPENAI_HEADERS.VERSION, codexClientVersion);
|
|
2851
|
+
headers.set("User-Agent", USER_AGENT);
|
|
2852
|
+
if (sessionId) {
|
|
2853
|
+
headers.set(OPENAI_HEADERS.CONVERSATION_ID, sessionId);
|
|
2854
|
+
headers.set(OPENAI_HEADERS.SESSION_ID, sessionId);
|
|
2855
|
+
headers.set("x-client-request-id", sessionId);
|
|
2856
|
+
} else {
|
|
2857
|
+
headers.delete(OPENAI_HEADERS.CONVERSATION_ID);
|
|
2858
|
+
headers.delete(OPENAI_HEADERS.SESSION_ID);
|
|
2859
|
+
headers.delete("x-client-request-id");
|
|
2860
|
+
}
|
|
2861
|
+
headers.delete(OPENAI_HEADERS.INSTALLATION_ID);
|
|
2862
|
+
if (requestMetadata) applyCodexCompatibilityHeaders(headers, requestMetadata);
|
|
2863
|
+
else {
|
|
2864
|
+
headers.delete(OPENAI_HEADERS.SCOPED_SESSION_ID);
|
|
2865
|
+
headers.delete(OPENAI_HEADERS.THREAD_ID);
|
|
2866
|
+
headers.delete(OPENAI_HEADERS.WINDOW_ID);
|
|
2867
|
+
headers.delete(OPENAI_HEADERS.TURN_METADATA);
|
|
2868
|
+
}
|
|
2869
|
+
if (state?.turnState) headers.set(X_CODEX_TURN_STATE_HEADER, state.turnState);
|
|
2870
|
+
else headers.delete(X_CODEX_TURN_STATE_HEADER);
|
|
2871
|
+
if (state?.modelsEtag) headers.set(X_MODELS_ETAG_HEADER, state.modelsEtag);
|
|
2872
|
+
else headers.delete(X_MODELS_ETAG_HEADER);
|
|
2873
|
+
if (responsesLite) headers.set(OPENAI_HEADERS.RESPONSES_LITE, "true");
|
|
2874
|
+
else headers.delete(OPENAI_HEADERS.RESPONSES_LITE);
|
|
2875
|
+
if (transport === "sse") {
|
|
2876
|
+
headers.set("accept", "text/event-stream");
|
|
2877
|
+
headers.set("content-type", "application/json");
|
|
2878
|
+
} else {
|
|
2879
|
+
headers.delete("accept");
|
|
2880
|
+
headers.delete("content-type");
|
|
2881
|
+
}
|
|
2882
|
+
return headers;
|
|
2883
|
+
}
|
|
2884
|
+
function redactHeaders(headers) {
|
|
2885
|
+
const redacted = {};
|
|
2886
|
+
for (const [key, value] of headers.entries()) {
|
|
2887
|
+
const lower = key.toLowerCase();
|
|
2888
|
+
if (lower === "authorization") {
|
|
2889
|
+
redacted[key] = "Bearer [redacted]";
|
|
2890
|
+
continue;
|
|
2891
|
+
}
|
|
2892
|
+
if (lower.includes("account") || lower.includes("session") || lower.includes("conversation") || lower.includes("thread") || lower.includes("window") || lower.includes("installation") || lower.startsWith("x-codex-turn") || lower === "x-client-request-id" || lower === "cookie") {
|
|
2893
|
+
redacted[key] = "[redacted]";
|
|
2894
|
+
continue;
|
|
2895
|
+
}
|
|
2896
|
+
redacted[key] = value;
|
|
2897
|
+
}
|
|
2898
|
+
return redacted;
|
|
2899
|
+
}
|
|
2900
|
+
/** Resolve a Codex Responses endpoint exactly as the chat and compaction transports do. */
|
|
2901
|
+
function resolveCodexResponsesUrl(baseUrl) {
|
|
2902
|
+
const normalized = (baseUrl && baseUrl.trim().length > 0 ? baseUrl : CODEX_BASE_URL).replace(/\/+$/, "");
|
|
2903
|
+
if (normalized.endsWith("/codex/responses")) return normalized;
|
|
2904
|
+
if (normalized.endsWith("/codex")) return `${normalized}/responses`;
|
|
2905
|
+
return `${normalized}/codex/responses`;
|
|
2906
|
+
}
|
|
2907
|
+
function convertMessages(model, context) {
|
|
2908
|
+
const messages = [];
|
|
2909
|
+
const normalizeToolCallId = (id) => {
|
|
2910
|
+
if (!id.includes("|")) return id;
|
|
2911
|
+
const [callId, itemId] = id.split("|");
|
|
2912
|
+
const sanitizedCallId = callId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
2913
|
+
let sanitizedItemId = itemId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
2914
|
+
if (!sanitizedItemId.startsWith("fc")) sanitizedItemId = `fc_${sanitizedItemId}`;
|
|
2915
|
+
let normalizedCallId = sanitizedCallId.length > 64 ? sanitizedCallId.slice(0, 64) : sanitizedCallId;
|
|
2916
|
+
let normalizedItemId = sanitizedItemId.length > 64 ? sanitizedItemId.slice(0, 64) : sanitizedItemId;
|
|
2917
|
+
normalizedCallId = normalizedCallId.replace(/_+$/, "");
|
|
2918
|
+
normalizedItemId = normalizedItemId.replace(/_+$/, "");
|
|
2919
|
+
return `${normalizedCallId}|${normalizedItemId}`;
|
|
2920
|
+
};
|
|
2921
|
+
const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
|
|
2922
|
+
const escapeControlTokens = isHarmonyDialectModel(model);
|
|
2923
|
+
let msgIndex = 0;
|
|
2924
|
+
const customCallIds = /* @__PURE__ */ new Set();
|
|
2925
|
+
const knownCallIds = /* @__PURE__ */ new Set();
|
|
2926
|
+
const computerCallIds = /* @__PURE__ */ new Set();
|
|
2927
|
+
for (const msg of transformedMessages) {
|
|
2928
|
+
if (msg.role === "user" || msg.role === "developer") {
|
|
2929
|
+
const providerPayload = msg.providerPayload;
|
|
2930
|
+
const historyItems = getOpenAIResponsesHistoryItems(providerPayload, model.provider);
|
|
2931
|
+
if (historyItems) {
|
|
2932
|
+
const redactedHistoryItems = redactSensitiveInObject(historyItems).result;
|
|
2933
|
+
const replayItems = model.supportsComputerUse === true ? redactedHistoryItems : unrollCodexComputerItems(redactedHistoryItems, model.compat.supportsImageDetailOriginal);
|
|
2934
|
+
for (const item of replayItems) {
|
|
2935
|
+
if (item.type === "custom_tool_call") customCallIds.add(item.call_id);
|
|
2936
|
+
if (item.type === "computer_call") computerCallIds.add(item.call_id);
|
|
2937
|
+
if ((item.type === "function_call" || item.type === "custom_tool_call") && item.call_id) knownCallIds.add(item.call_id);
|
|
2938
|
+
}
|
|
2939
|
+
messages.push(...escapeControlTokens ? escapeReplayedControlTokens(replayItems) : replayItems);
|
|
2940
|
+
msgIndex += 1;
|
|
2941
|
+
continue;
|
|
2942
|
+
}
|
|
2943
|
+
const normalizedContent = normalizeInputMessageContent(model, msg.content);
|
|
2944
|
+
if (normalizedContent.length === 0) continue;
|
|
2945
|
+
messages.push({
|
|
2946
|
+
role: msg.role,
|
|
2947
|
+
content: normalizedContent
|
|
2948
|
+
});
|
|
2949
|
+
msgIndex += 1;
|
|
2950
|
+
continue;
|
|
2951
|
+
}
|
|
2952
|
+
if (msg.role === "assistant") {
|
|
2953
|
+
const assistantMsg = msg;
|
|
2954
|
+
const providerPayload = assistantMsg.api === model.api && assistantMsg.model === model.id ? getOpenAIResponsesHistoryPayload(assistantMsg.providerPayload, model.provider, assistantMsg.provider) : void 0;
|
|
2955
|
+
const historyItems = providerPayload?.items;
|
|
2956
|
+
let suppressHiddenEmptyFallback = false;
|
|
2957
|
+
if (historyItems) {
|
|
2958
|
+
const sanitizedHistoryItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(historyItems);
|
|
2959
|
+
if (sanitizedHistoryItems) {
|
|
2960
|
+
const rawReplayItems = model.supportsComputerUse === true ? sanitizedHistoryItems : unrollCodexComputerItems(sanitizedHistoryItems, model.compat.supportsImageDetailOriginal);
|
|
2961
|
+
const replayItems = escapeControlTokens ? escapeReplayedControlTokens(rawReplayItems) : rawReplayItems;
|
|
2962
|
+
for (const item of replayItems) {
|
|
2963
|
+
if (item.type === "custom_tool_call") customCallIds.add(item.call_id);
|
|
2964
|
+
if (item.type === "computer_call") computerCallIds.add(item.call_id);
|
|
2965
|
+
if ((item.type === "function_call" || item.type === "custom_tool_call") && item.call_id) knownCallIds.add(item.call_id);
|
|
2966
|
+
}
|
|
2967
|
+
if (providerPayload?.dt) messages.push(...replayItems);
|
|
2968
|
+
else messages.splice(0, messages.length, ...replayItems);
|
|
2969
|
+
msgIndex += 1;
|
|
2970
|
+
continue;
|
|
2971
|
+
}
|
|
2972
|
+
suppressHiddenEmptyFallback = true;
|
|
2973
|
+
}
|
|
2974
|
+
const convertedOutputItems = convertResponsesAssistantMessage(model.supportsComputerUse === true ? assistantMsg : unrollCodexComputerAssistantMessage(assistantMsg), model, msgIndex, knownCallIds, !suppressHiddenEmptyFallback, customCallIds, false, true, void 0, computerCallIds);
|
|
2975
|
+
const outputItems = suppressHiddenEmptyFallback ? sanitizeOpenAIResponsesAssistantFallbackItemsForReplay(convertedOutputItems) : convertedOutputItems;
|
|
2976
|
+
if (outputItems.length > 0) messages.push(...escapeControlTokens ? escapeReplayedControlTokens(outputItems) : outputItems);
|
|
2977
|
+
msgIndex += 1;
|
|
2978
|
+
continue;
|
|
2979
|
+
}
|
|
2980
|
+
if (msg.role === "toolResult") appendResponsesToolResultMessages(messages, model.supportsComputerUse === true ? msg : unrollCodexComputerToolResult(msg), model, false, model.compat.supportsImageDetailOriginal, knownCallIds, customCallIds, true, computerCallIds);
|
|
2981
|
+
msgIndex += 1;
|
|
2982
|
+
}
|
|
2983
|
+
return messages;
|
|
2984
|
+
}
|
|
2985
|
+
function normalizeInputMessageContent(model, content) {
|
|
2986
|
+
const escapeControlTokens = isHarmonyDialectModel(model);
|
|
2987
|
+
if (typeof content === "string") {
|
|
2988
|
+
if (!content || content.trim() === "") return [];
|
|
2989
|
+
const text = content.toWellFormed();
|
|
2990
|
+
return [{
|
|
2991
|
+
type: "input_text",
|
|
2992
|
+
text: escapeControlTokens ? escapeHarmonyControlTokens(text) : text
|
|
2993
|
+
}];
|
|
2994
|
+
}
|
|
2995
|
+
return convertResponsesInputContent(content, model.input.includes("image"), model.compat.supportsImageDetailOriginal, escapeControlTokens) ?? [];
|
|
2996
|
+
}
|
|
2997
|
+
/** @internal Exported for tests. */
|
|
2998
|
+
function convertOpenAICodexResponsesTools(tools, model) {
|
|
2999
|
+
const allowFreeform = model.applyPatchToolType === "freeform";
|
|
3000
|
+
const payloads = [];
|
|
3001
|
+
for (const tool of tools) {
|
|
3002
|
+
if (tool.native?.type === "computer" && model.supportsComputerUse === true) {
|
|
3003
|
+
payloads.push({ type: "computer" });
|
|
3004
|
+
continue;
|
|
3005
|
+
}
|
|
3006
|
+
if (allowFreeform && tool.customFormat) {
|
|
3007
|
+
payloads.push({
|
|
3008
|
+
type: "custom",
|
|
3009
|
+
name: tool.customWireName ?? tool.name,
|
|
3010
|
+
description: tool.description || "",
|
|
3011
|
+
format: {
|
|
3012
|
+
type: "grammar",
|
|
3013
|
+
syntax: tool.customFormat.syntax,
|
|
3014
|
+
definition: compactGrammarDefinition(tool.customFormat.syntax, tool.customFormat.definition)
|
|
3015
|
+
}
|
|
3016
|
+
});
|
|
3017
|
+
continue;
|
|
3018
|
+
}
|
|
3019
|
+
const strict = !!(!NO_STRICT && tool.strict);
|
|
3020
|
+
const baseParameters = sanitizeSchemaForOpenAIResponses(toolWireSchema(tool));
|
|
3021
|
+
const { schema: parameters, strict: effectiveStrict } = adaptSchemaForStrict(baseParameters, strict);
|
|
3022
|
+
payloads.push({
|
|
3023
|
+
type: "function",
|
|
3024
|
+
name: tool.name,
|
|
3025
|
+
description: tool.description || "",
|
|
3026
|
+
parameters,
|
|
3027
|
+
...effectiveStrict ? { strict: true } : !NO_STRICT && tool.strict === false ? { strict: false } : {}
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
return payloads;
|
|
3031
|
+
}
|
|
3032
|
+
var CodexWebSocketTransportError = class extends Error {
|
|
3033
|
+
constructor(detail) {
|
|
3034
|
+
super(`${CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX}: ${detail}`);
|
|
3035
|
+
this.name = "CodexWebSocketTransportError";
|
|
3036
|
+
}
|
|
3037
|
+
};
|
|
3038
|
+
var CodexWhitespaceToolCallLoopError = class extends Error {
|
|
3039
|
+
constructor(message) {
|
|
3040
|
+
super(message);
|
|
3041
|
+
this.name = "CodexWhitespaceToolCallLoopError";
|
|
3042
|
+
}
|
|
3043
|
+
};
|
|
3044
|
+
var CodexProviderStreamError = class extends Error {
|
|
3045
|
+
retryable;
|
|
3046
|
+
code;
|
|
3047
|
+
constructor(message, retryable, code) {
|
|
3048
|
+
super(message);
|
|
3049
|
+
this.name = "CodexProviderStreamError";
|
|
3050
|
+
this.retryable = retryable;
|
|
3051
|
+
this.code = code;
|
|
3052
|
+
}
|
|
3053
|
+
};
|
|
3054
|
+
const optionalCodexString = type("unknown").pipe((raw) => {
|
|
3055
|
+
const out = type("string")(raw);
|
|
3056
|
+
return out instanceof type.errors ? void 0 : out;
|
|
3057
|
+
});
|
|
3058
|
+
const innerErrorDetailSchema = type({
|
|
3059
|
+
"code?": optionalCodexString,
|
|
3060
|
+
"type?": optionalCodexString,
|
|
3061
|
+
"message?": optionalCodexString
|
|
3062
|
+
});
|
|
3063
|
+
const codexErrorDetailSchema = type("unknown").pipe((raw) => {
|
|
3064
|
+
const out = innerErrorDetailSchema(raw);
|
|
3065
|
+
return out instanceof type.errors ? void 0 : out;
|
|
3066
|
+
});
|
|
3067
|
+
const innerFailureEventSchema = type({
|
|
3068
|
+
"type?": optionalCodexString,
|
|
3069
|
+
"code?": optionalCodexString,
|
|
3070
|
+
"message?": optionalCodexString,
|
|
3071
|
+
"status?": optionalCodexString,
|
|
3072
|
+
"error?": codexErrorDetailSchema,
|
|
3073
|
+
"response?": type("unknown").pipe((raw) => {
|
|
3074
|
+
const out = type({
|
|
3075
|
+
"error?": codexErrorDetailSchema,
|
|
3076
|
+
"message?": optionalCodexString,
|
|
3077
|
+
"status?": optionalCodexString
|
|
3078
|
+
})(raw);
|
|
3079
|
+
return out instanceof type.errors ? void 0 : out;
|
|
3080
|
+
})
|
|
3081
|
+
});
|
|
3082
|
+
const codexFailureEventSchema = type("unknown").pipe((raw) => {
|
|
3083
|
+
const out = innerFailureEventSchema(raw);
|
|
3084
|
+
return out instanceof type.errors ? {
|
|
3085
|
+
type: void 0,
|
|
3086
|
+
code: void 0,
|
|
3087
|
+
message: void 0,
|
|
3088
|
+
status: void 0,
|
|
3089
|
+
error: void 0,
|
|
3090
|
+
response: void 0
|
|
3091
|
+
} : out;
|
|
3092
|
+
});
|
|
3093
|
+
function isRetryableCodexFailureEvent(rawEvent) {
|
|
3094
|
+
const event = codexFailureEventSchema(rawEvent);
|
|
3095
|
+
if (event instanceof type.errors) return false;
|
|
3096
|
+
const error = event.error ?? event.response?.error;
|
|
3097
|
+
const code = error?.code ?? error?.type ?? event.code;
|
|
3098
|
+
if (code && CODEX_RETRYABLE_EVENT_CODES.has(code.toLowerCase())) return true;
|
|
3099
|
+
const message = error?.message ?? event.message ?? event.response?.message;
|
|
3100
|
+
return !!message && CODEX_RETRYABLE_EVENT_MESSAGE.test(message);
|
|
3101
|
+
}
|
|
3102
|
+
function createCodexProviderStreamError(rawEvent) {
|
|
3103
|
+
const event = codexFailureEventSchema(rawEvent);
|
|
3104
|
+
if (event instanceof type.errors) return new CodexProviderStreamError("Codex response failed", false);
|
|
3105
|
+
const nestedError = event.error ?? event.response?.error;
|
|
3106
|
+
const code = nestedError?.code ?? nestedError?.type ?? event.code ?? "";
|
|
3107
|
+
const message = event.message ?? "";
|
|
3108
|
+
return new CodexProviderStreamError(event.type === "error" ? formatCodexErrorEvent(rawEvent, code, message) : formatCodexFailure(rawEvent) ?? "Codex response failed", isRetryableCodexFailureEvent(rawEvent), code || void 0);
|
|
3109
|
+
}
|
|
3110
|
+
function formatCodexFailure(rawEvent) {
|
|
3111
|
+
const event = codexFailureEventSchema(rawEvent);
|
|
3112
|
+
if (event instanceof type.errors) return null;
|
|
3113
|
+
const error = event.error ?? event.response?.error;
|
|
3114
|
+
const message = error?.message ?? event.message ?? event.response?.message;
|
|
3115
|
+
const code = error?.code ?? error?.type ?? event.code;
|
|
3116
|
+
const status = event.response?.status ?? event.status;
|
|
3117
|
+
const meta = [];
|
|
3118
|
+
if (code) meta.push(`code=${code}`);
|
|
3119
|
+
if (status) meta.push(`status=${status}`);
|
|
3120
|
+
if (message) return `Codex response failed: ${message}${meta.length ? ` (${meta.join(", ")})` : ""}`;
|
|
3121
|
+
if (meta.length) return `Codex response failed (${meta.join(", ")})`;
|
|
3122
|
+
try {
|
|
3123
|
+
const rawEventJson = JSON.stringify(rawEvent);
|
|
3124
|
+
return `Codex response failed: ${rawEventJson.length <= 800 ? rawEventJson : `${rawEventJson.slice(0, 800)}…[truncated ${rawEventJson.length - 800}]`}`;
|
|
3125
|
+
} catch {
|
|
3126
|
+
return "Codex response failed";
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
function formatCodexErrorEvent(rawEvent, code, message) {
|
|
3130
|
+
const detail = formatCodexFailure(rawEvent);
|
|
3131
|
+
if (detail) return detail.replace("response failed", "error event");
|
|
3132
|
+
const meta = [];
|
|
3133
|
+
if (code) meta.push(`code=${code}`);
|
|
3134
|
+
if (message) meta.push(`message=${message}`);
|
|
3135
|
+
if (meta.length > 0) return `Codex error event (${meta.join(", ")})`;
|
|
3136
|
+
try {
|
|
3137
|
+
const rawEventJson = JSON.stringify(rawEvent);
|
|
3138
|
+
return `Codex error event: ${rawEventJson.length <= 800 ? rawEventJson : `${rawEventJson.slice(0, 800)}…[truncated ${rawEventJson.length - 800}]`}`;
|
|
3139
|
+
} catch {
|
|
3140
|
+
return "Codex error event";
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
//#endregion
|
|
3144
|
+
export { openai_codex_responses_exports as t };
|