@nextclaw/nextclaw-ncp-runtime-codex-sdk 0.1.33 → 0.1.35

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.
@@ -0,0 +1,4 @@
1
+ //#region src/codex-model-provider.d.ts
2
+ declare function buildCodexBridgeModelProviderId(externalModelProvider: string): string;
3
+ //#endregion
4
+ export { buildCodexBridgeModelProviderId };
@@ -0,0 +1,8 @@
1
+ //#region src/codex-model-provider.ts
2
+ function buildCodexBridgeModelProviderId(externalModelProvider) {
3
+ const normalized = externalModelProvider.trim();
4
+ if (!normalized) return "nextclaw-codex-bridge";
5
+ return `nextclaw-codex-bridge-${normalized}`;
6
+ }
7
+ //#endregion
8
+ export { buildCodexBridgeModelProviderId };
@@ -0,0 +1,106 @@
1
+ import { readArray, readRawString, readRecord, readString } from "./codex-openai-responses-bridge-shared.utils.js";
2
+ import { normalizeAssistantText } from "@nextclaw/ncp";
3
+ //#region src/codex-openai-responses-bridge-assistant-output.utils.ts
4
+ function extractAssistantText(content) {
5
+ if (typeof content === "string") return content;
6
+ if (!Array.isArray(content)) return "";
7
+ return content.map((entry) => {
8
+ const record = readRecord(entry);
9
+ if (!record) return "";
10
+ const type = readString(record.type);
11
+ if (type === "text" || type === "output_text") return readString(record.text) ?? "";
12
+ return "";
13
+ }).filter(Boolean).join("");
14
+ }
15
+ function readReasoningRecordText(value) {
16
+ const record = readRecord(value);
17
+ if (!record) return "";
18
+ return readRawString(record.text) ?? readRawString(record.content) ?? readRawString(record.reasoning) ?? readRawString(record.reasoning_content) ?? readRawString(record.thinking) ?? "";
19
+ }
20
+ function extractReasoningDetailsText(value) {
21
+ const rawString = readRawString(value);
22
+ if (rawString !== void 0) return {
23
+ present: true,
24
+ text: rawString
25
+ };
26
+ const record = readRecord(value);
27
+ if (record) return {
28
+ present: true,
29
+ text: readReasoningRecordText(record)
30
+ };
31
+ if (!Array.isArray(value)) return;
32
+ return {
33
+ present: true,
34
+ text: readArray(value).map((entry) => readReasoningRecordText(entry)).join("")
35
+ };
36
+ }
37
+ function extractContentReasoningText(content) {
38
+ if (!Array.isArray(content)) return "";
39
+ return content.map((entry) => {
40
+ const record = readRecord(entry);
41
+ if (!record) return "";
42
+ const type = readString(record.type);
43
+ if (type === "reasoning" || type === "reasoning_text" || type === "thinking" || type === "thinking_text") return readReasoningRecordText(record);
44
+ return "";
45
+ }).join("");
46
+ }
47
+ function extractExplicitReasoning(message) {
48
+ const candidates = [
49
+ message?.reasoning_content,
50
+ message?.reasoning,
51
+ message?.thinking,
52
+ message?.reasoning_details
53
+ ];
54
+ for (const candidate of candidates) {
55
+ const extracted = extractReasoningDetailsText(candidate);
56
+ if (extracted) return extracted;
57
+ }
58
+ const contentReasoning = extractContentReasoningText(message?.content);
59
+ return contentReasoning ? {
60
+ present: true,
61
+ text: contentReasoning
62
+ } : void 0;
63
+ }
64
+ function extractAssistantOutput(message) {
65
+ const rawText = extractAssistantText(message?.content);
66
+ const normalized = normalizeAssistantText(rawText, "think-tags");
67
+ const explicitReasoning = extractExplicitReasoning(message);
68
+ const reasoning = explicitReasoning?.text ?? readString(normalized.reasoning) ?? "";
69
+ const reasoningPresent = explicitReasoning?.present === true || Boolean(normalized.reasoning);
70
+ return {
71
+ text: explicitReasoning?.present === true ? readString(normalized.text) ?? readString(rawText) ?? "" : normalized.reasoning ? readString(normalized.text) ?? "" : readString(rawText) ?? "",
72
+ reasoning,
73
+ reasoningPresent
74
+ };
75
+ }
76
+ function buildAssistantOutputItems(params) {
77
+ const { reasoning, reasoningPresent, text } = extractAssistantOutput(params.message);
78
+ const outputItems = [];
79
+ if (reasoningPresent) outputItems.push({
80
+ type: "reasoning",
81
+ id: `${params.responseId}:reasoning:0`,
82
+ summary: [{
83
+ type: "summary_text",
84
+ text: reasoning
85
+ }],
86
+ content: [{
87
+ type: "reasoning_text",
88
+ text: reasoning
89
+ }],
90
+ status: "completed"
91
+ });
92
+ if (text) outputItems.push({
93
+ type: "message",
94
+ id: `${params.responseId}:message:${outputItems.length}`,
95
+ role: "assistant",
96
+ status: "completed",
97
+ content: [{
98
+ type: "output_text",
99
+ text,
100
+ annotations: []
101
+ }]
102
+ });
103
+ return outputItems;
104
+ }
105
+ //#endregion
106
+ export { buildAssistantOutputItems };
@@ -0,0 +1,79 @@
1
+ import { readRecord, readString } from "./codex-openai-responses-bridge-shared.utils.js";
2
+ //#region src/codex-openai-responses-bridge-message-content.utils.ts
3
+ function normalizeTextPart(value) {
4
+ const record = readRecord(value);
5
+ if (!record) return "";
6
+ const type = readString(record.type);
7
+ if (type !== "input_text" && type !== "output_text") return "";
8
+ return readString(record.text) ?? "";
9
+ }
10
+ function normalizeImageUrl(value) {
11
+ const record = readRecord(value);
12
+ if (!record || readString(record.type) !== "input_image") return null;
13
+ const source = readRecord(record.source);
14
+ if (!source) return null;
15
+ if (readString(source.type) === "url") return readString(source.url) ?? null;
16
+ if (readString(source.type) === "base64") {
17
+ const mediaType = readString(source.media_type) ?? "application/octet-stream";
18
+ const data = readString(source.data);
19
+ if (!data) return null;
20
+ return `data:${mediaType};base64,${data}`;
21
+ }
22
+ return null;
23
+ }
24
+ function normalizeToolOutput(value) {
25
+ if (typeof value === "string") return value;
26
+ if (Array.isArray(value)) {
27
+ const text = value.map((entry) => normalizeTextPart(entry)).filter(Boolean).join("");
28
+ if (text) return text;
29
+ }
30
+ try {
31
+ return JSON.stringify(value ?? "");
32
+ } catch {
33
+ return String(value ?? "");
34
+ }
35
+ }
36
+ function buildChatContent(content) {
37
+ if (typeof content === "string") return content;
38
+ if (!Array.isArray(content)) return null;
39
+ const chatContent = [];
40
+ for (const entry of content) {
41
+ const text = normalizeTextPart(entry);
42
+ if (text) {
43
+ chatContent.push({
44
+ type: "text",
45
+ text
46
+ });
47
+ continue;
48
+ }
49
+ const imageUrl = normalizeImageUrl(entry);
50
+ if (imageUrl) chatContent.push({
51
+ type: "image_url",
52
+ image_url: { url: imageUrl }
53
+ });
54
+ }
55
+ if (chatContent.length === 0) return null;
56
+ if (chatContent.every((entry) => entry.type === "text")) return chatContent.map((entry) => readString(entry.text) ?? "").join("\n");
57
+ return chatContent;
58
+ }
59
+ function mergeChatContent(left, right) {
60
+ if (left === null) return right;
61
+ if (right === null) return left;
62
+ if (typeof left === "string" && typeof right === "string") return [left, right].filter((value) => value.length > 0).join("\n\n");
63
+ const normalizedLeft = typeof left === "string" ? [{
64
+ type: "text",
65
+ text: left
66
+ }] : left;
67
+ const normalizedRight = typeof right === "string" ? [{
68
+ type: "text",
69
+ text: right
70
+ }] : right;
71
+ return [...normalizedLeft, ...normalizedRight];
72
+ }
73
+ function readAssistantMessageText(content) {
74
+ if (typeof content === "string") return content;
75
+ if (!Array.isArray(content)) return "";
76
+ return content.filter((entry) => entry.type === "text").map((entry) => readString(entry.text) ?? "").join("\n");
77
+ }
78
+ //#endregion
79
+ export { buildChatContent, mergeChatContent, normalizeToolOutput, readAssistantMessageText };
@@ -0,0 +1,14 @@
1
+ //#region src/codex-openai-responses-bridge-shared.utils.d.ts
2
+ type CodexOpenAiResponsesBridgeConfig = {
3
+ upstreamApiBase: string;
4
+ upstreamApiKey?: string;
5
+ upstreamExtraHeaders?: Record<string, string>;
6
+ defaultModel?: string;
7
+ modelPrefixes?: string[];
8
+ upstreamReasoningSplit?: boolean;
9
+ };
10
+ type CodexOpenAiResponsesBridgeResult = {
11
+ baseUrl: string;
12
+ };
13
+ //#endregion
14
+ export { CodexOpenAiResponsesBridgeConfig, CodexOpenAiResponsesBridgeResult };
@@ -0,0 +1,36 @@
1
+ //#region src/codex-openai-responses-bridge-shared.utils.ts
2
+ function readString(value) {
3
+ if (typeof value !== "string") return;
4
+ return value.trim() || void 0;
5
+ }
6
+ function readRawString(value) {
7
+ return typeof value === "string" ? value : void 0;
8
+ }
9
+ function readBoolean(value) {
10
+ return typeof value === "boolean" ? value : void 0;
11
+ }
12
+ function readNumber(value) {
13
+ if (typeof value !== "number" || !Number.isFinite(value)) return;
14
+ return value;
15
+ }
16
+ function readRecord(value) {
17
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
18
+ return value;
19
+ }
20
+ function readArray(value) {
21
+ return Array.isArray(value) ? value : [];
22
+ }
23
+ function withTrailingSlash(value) {
24
+ return value.endsWith("/") ? value : `${value}/`;
25
+ }
26
+ function writeSseEvent(response, eventType, payload) {
27
+ response.write(`event: ${eventType}\n`);
28
+ response.write(`data: ${JSON.stringify(payload)}\n\n`);
29
+ }
30
+ function nextSequenceNumber(state) {
31
+ const nextValue = state.value;
32
+ state.value += 1;
33
+ return nextValue;
34
+ }
35
+ //#endregion
36
+ export { nextSequenceNumber, readArray, readBoolean, readNumber, readRawString, readRecord, readString, withTrailingSlash, writeSseEvent };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import { CodexLiveOutputChannel, CodexLiveOutputEvent, CodexLiveOutputStream } from "./services/codex-live-output-stream.service.js";
2
2
  import { CodexAssetContentPathResolver, CodexThreadInput, buildCodexInputBuilder } from "./codex-input.utils.js";
3
+ import { buildCodexBridgeModelProviderId } from "./codex-model-provider.js";
4
+ import { CodexOpenAiResponsesBridgeResult } from "./codex-openai-responses-bridge-shared.utils.js";
5
+ import { CodexOpenAiResponsesBridgeRuntimeConfig, ensureCodexOpenAiResponsesBridge } from "./utils/codex-openai-responses-bridge.utils.js";
3
6
  import { NcpAgentConversationStateManager, NcpAgentRunInput, NcpAgentRunOptions, NcpAgentRuntime, NcpEndpointEvent } from "@nextclaw/ncp";
4
7
  import { CodexOptions, ThreadOptions } from "@openai/codex-sdk";
5
8
 
@@ -39,4 +42,4 @@ declare class CodexSdkNcpAgentRuntime implements NcpAgentRuntime {
39
42
  private updateThreadId;
40
43
  }
41
44
  //#endregion
42
- export { type CodexAssetContentPathResolver, type CodexLiveOutputChannel, type CodexLiveOutputEvent, CodexLiveOutputStream, CodexSdkNcpAgentRuntime, CodexSdkNcpAgentRuntimeConfig, buildCodexInputBuilder };
45
+ export { type CodexAssetContentPathResolver, type CodexLiveOutputChannel, type CodexLiveOutputEvent, CodexLiveOutputStream, type CodexOpenAiResponsesBridgeResult, type CodexOpenAiResponsesBridgeRuntimeConfig, CodexSdkNcpAgentRuntime, CodexSdkNcpAgentRuntimeConfig, buildCodexBridgeModelProviderId, buildCodexInputBuilder, ensureCodexOpenAiResponsesBridge };
package/dist/index.js CHANGED
@@ -3,6 +3,8 @@ import { CodexNcpRunEventEmitter } from "./services/codex-ncp-run-event-emitter.
3
3
  import { mapCodexItemEvent } from "./utils/codex-sdk-ncp-event-mapper.utils.js";
4
4
  import { buildCodexCliEnv } from "./codex-cli-env.js";
5
5
  import { buildCodexInputBuilder, buildCodexTurnInputFromRunInput } from "./codex-input.utils.js";
6
+ import { buildCodexBridgeModelProviderId } from "./codex-model-provider.js";
7
+ import { ensureCodexOpenAiResponsesBridge } from "./utils/codex-openai-responses-bridge.utils.js";
6
8
  import { CodexLiveOutputStream } from "./services/codex-live-output-stream.service.js";
7
9
  import { createRequire } from "node:module";
8
10
  import "@nextclaw/ncp";
@@ -44,7 +46,7 @@ var CodexSdkNcpAgentRuntime = class {
44
46
  run = async function* (input, options) {
45
47
  const signal = options?.signal;
46
48
  const messageId = createId("codex-message");
47
- const runId = createId("codex-run");
49
+ const runId = input.runId ?? createId("codex-run");
48
50
  const itemTextById = /* @__PURE__ */ new Map();
49
51
  const toolStateById = /* @__PURE__ */ new Map();
50
52
  yield* this.eventEmitter.emitRunStarted(input.sessionId, messageId, runId);
@@ -168,4 +170,4 @@ var CodexSdkNcpAgentRuntime = class {
168
170
  };
169
171
  };
170
172
  //#endregion
171
- export { CodexLiveOutputStream, CodexSdkNcpAgentRuntime, buildCodexInputBuilder };
173
+ export { CodexLiveOutputStream, CodexSdkNcpAgentRuntime, buildCodexBridgeModelProviderId, buildCodexInputBuilder, ensureCodexOpenAiResponsesBridge };
@@ -0,0 +1,218 @@
1
+ import { readArray, readBoolean, readNumber, readRecord, readString, withTrailingSlash } from "../codex-openai-responses-bridge-shared.utils.js";
2
+ import { buildChatContent, mergeChatContent, normalizeToolOutput, readAssistantMessageText } from "../codex-openai-responses-bridge-message-content.utils.js";
3
+ //#region src/utils/codex-openai-responses-bridge-request.utils.ts
4
+ function stripModelPrefix(model, prefixes) {
5
+ const normalizedModel = model.trim();
6
+ for (const prefix of prefixes) {
7
+ const normalizedPrefix = prefix.trim().toLowerCase();
8
+ if (!normalizedPrefix) continue;
9
+ const candidatePrefix = `${normalizedPrefix}/`;
10
+ if (normalizedModel.toLowerCase().startsWith(candidatePrefix)) return normalizedModel.slice(candidatePrefix.length);
11
+ }
12
+ return normalizedModel;
13
+ }
14
+ function resolveUpstreamModel(requestedModel, config) {
15
+ const prefixes = (config.modelPrefixes ?? []).filter((value) => value.trim().length > 0);
16
+ const model = stripModelPrefix(readString(requestedModel) ?? "", prefixes) || stripModelPrefix(config.defaultModel ?? "", prefixes);
17
+ if (!model) throw new Error("Codex bridge could not resolve an upstream model.");
18
+ return model;
19
+ }
20
+ function buildOpenAiMessages(input, instructions) {
21
+ return new OpenAiMessagesBuilder(input, instructions).build();
22
+ }
23
+ var OpenAiMessagesBuilder = class {
24
+ messages = [];
25
+ assistantTextParts = [];
26
+ assistantToolCalls = [];
27
+ assistantReasoningContent = {
28
+ present: false,
29
+ value: ""
30
+ };
31
+ systemContent;
32
+ constructor(input, instructions) {
33
+ this.input = input;
34
+ this.systemContent = readString(instructions) ?? null;
35
+ }
36
+ build = () => {
37
+ if (typeof this.input === "string") {
38
+ this.messages.push({
39
+ role: "user",
40
+ content: this.input
41
+ });
42
+ return this.withSystemMessage();
43
+ }
44
+ for (const rawItem of readArray(this.input)) {
45
+ const item = readRecord(rawItem);
46
+ if (!item) continue;
47
+ this.appendItem(item);
48
+ }
49
+ this.flushAssistant();
50
+ return this.withSystemMessage();
51
+ };
52
+ appendItem = (item) => {
53
+ const type = readString(item.type);
54
+ if (type === "message") this.appendMessageInputItem(item);
55
+ else if (type === "reasoning") this.appendReasoningItem(item);
56
+ else if (type === "function_call") this.appendFunctionCallItem(item);
57
+ else if (type === "function_call_output") this.appendFunctionCallOutputItem(item);
58
+ };
59
+ appendMessageInputItem = (item) => {
60
+ const role = readString(item.role);
61
+ const content = buildChatContent(item.content);
62
+ if (role === "assistant") {
63
+ const text = readAssistantMessageText(content);
64
+ if (text.trim()) this.assistantTextParts.push(text);
65
+ return;
66
+ }
67
+ this.flushAssistant();
68
+ const normalizedRole = role === "developer" ? "system" : role;
69
+ if (normalizedRole === "system") this.systemContent = mergeChatContent(this.systemContent, content);
70
+ else if (normalizedRole === "user" && content !== null) this.messages.push({
71
+ role: "user",
72
+ content
73
+ });
74
+ };
75
+ appendReasoningItem = (item) => {
76
+ this.assistantReasoningContent = {
77
+ present: true,
78
+ value: readArray(item.content).map((entry) => {
79
+ const record = readRecord(entry);
80
+ if (!record || readString(record.type) !== "reasoning_text") return "";
81
+ return typeof record.text === "string" ? record.text : "";
82
+ }).join("")
83
+ };
84
+ };
85
+ appendFunctionCallItem = (item) => {
86
+ const name = readString(item.name);
87
+ const argumentsText = readString(item.arguments) ?? "{}";
88
+ if (!name) return;
89
+ const callId = readString(item.call_id) ?? readString(item.id) ?? `call_${this.assistantToolCalls.length}`;
90
+ this.assistantToolCalls.push({
91
+ id: callId,
92
+ type: "function",
93
+ function: {
94
+ name,
95
+ arguments: argumentsText
96
+ }
97
+ });
98
+ };
99
+ appendFunctionCallOutputItem = (item) => {
100
+ this.flushAssistant();
101
+ const callId = readString(item.call_id);
102
+ if (!callId) return;
103
+ this.messages.push({
104
+ role: "tool",
105
+ tool_call_id: callId,
106
+ content: normalizeToolOutput(item.output)
107
+ });
108
+ };
109
+ flushAssistant = () => {
110
+ if (this.assistantTextParts.length === 0 && this.assistantToolCalls.length === 0 && !this.assistantReasoningContent.present) return;
111
+ this.messages.push({
112
+ role: "assistant",
113
+ content: this.assistantTextParts.join("\n").trim() || null,
114
+ ...this.assistantReasoningContent.present ? { reasoning_content: this.assistantReasoningContent.value } : {},
115
+ ...this.assistantToolCalls.length > 0 ? { tool_calls: structuredClone(this.assistantToolCalls) } : {}
116
+ });
117
+ this.assistantTextParts.length = 0;
118
+ this.assistantToolCalls.length = 0;
119
+ this.assistantReasoningContent = {
120
+ present: false,
121
+ value: ""
122
+ };
123
+ };
124
+ withSystemMessage = () => this.systemContent === null ? this.messages : [{
125
+ role: "system",
126
+ content: this.systemContent
127
+ }, ...this.messages];
128
+ };
129
+ function toOpenAiTools(value) {
130
+ const tools = [];
131
+ for (const entry of readArray(value)) {
132
+ const tool = readRecord(entry);
133
+ const type = readString(tool?.type);
134
+ const fn = readRecord(tool?.function);
135
+ const name = readString(fn?.name) ?? readString(tool?.name);
136
+ if (type !== "function" || !name) continue;
137
+ const description = (fn ? readString(fn.description) : void 0) ?? readString(tool?.description);
138
+ const parameters = (fn ? readRecord(fn.parameters) : void 0) ?? readRecord(tool?.parameters);
139
+ const strict = (fn ? readBoolean(fn.strict) : void 0) ?? readBoolean(tool?.strict);
140
+ tools.push({
141
+ type: "function",
142
+ function: {
143
+ name,
144
+ ...description ? { description } : {},
145
+ parameters: parameters ?? {
146
+ type: "object",
147
+ properties: {}
148
+ },
149
+ ...strict !== void 0 ? { strict } : {}
150
+ }
151
+ });
152
+ }
153
+ return tools.length > 0 ? tools : void 0;
154
+ }
155
+ function toOpenAiToolChoice(value) {
156
+ if (value === "auto" || value === "none" || value === "required") return value;
157
+ const record = readRecord(value);
158
+ const name = readString(readRecord(record?.function)?.name) ?? readString(record?.name);
159
+ if (readString(record?.type) === "function" && name) return {
160
+ type: "function",
161
+ function: { name }
162
+ };
163
+ }
164
+ async function callOpenAiCompatibleUpstream(params) {
165
+ const { model, request } = buildOpenAiCompatibleUpstreamRequest({
166
+ config: params.config,
167
+ body: params.body,
168
+ stream: false
169
+ });
170
+ const upstreamResponse = await fetch(request.url, request.init);
171
+ const rawText = await upstreamResponse.text();
172
+ let parsed;
173
+ try {
174
+ parsed = JSON.parse(rawText);
175
+ } catch {
176
+ throw new Error(`Bridge upstream returned invalid JSON: ${rawText.slice(0, 240)}`);
177
+ }
178
+ if (!upstreamResponse.ok) throw new Error(readString(parsed.error?.message) ?? rawText.slice(0, 240) ?? `HTTP ${upstreamResponse.status}`);
179
+ return {
180
+ model,
181
+ response: parsed
182
+ };
183
+ }
184
+ function buildOpenAiCompatibleUpstreamRequest(params) {
185
+ const { body, config, stream } = params;
186
+ const model = resolveUpstreamModel(body.model, config);
187
+ const upstreamUrl = new URL("chat/completions", withTrailingSlash(config.upstreamApiBase));
188
+ const tools = toOpenAiTools(body.tools);
189
+ const toolChoice = toOpenAiToolChoice(body.tool_choice);
190
+ return {
191
+ model,
192
+ request: {
193
+ url: upstreamUrl.toString(),
194
+ init: {
195
+ method: "POST",
196
+ headers: {
197
+ "Content-Type": "application/json",
198
+ ...config.upstreamApiKey ? { Authorization: `Bearer ${config.upstreamApiKey}` } : {},
199
+ ...config.upstreamExtraHeaders ?? {}
200
+ },
201
+ body: JSON.stringify({
202
+ model,
203
+ messages: buildOpenAiMessages(body.input, body.instructions),
204
+ ...tools ? { tools } : {},
205
+ ...toolChoice ? { tool_choice: toolChoice } : {},
206
+ ...config.upstreamReasoningSplit ? { reasoning_split: true } : {},
207
+ ...stream ? {
208
+ stream: true,
209
+ stream_options: { include_usage: true }
210
+ } : {},
211
+ ...typeof body.max_output_tokens === "number" ? { max_tokens: Math.max(1, Math.trunc(readNumber(body.max_output_tokens) ?? 1)) } : {}
212
+ })
213
+ }
214
+ }
215
+ };
216
+ }
217
+ //#endregion
218
+ export { buildOpenAiCompatibleUpstreamRequest, callOpenAiCompatibleUpstream };
@@ -0,0 +1,131 @@
1
+ import { readArray, readNumber, readRecord, readString, writeSseEvent } from "../codex-openai-responses-bridge-shared.utils.js";
2
+ import { buildAssistantOutputItems } from "../codex-openai-responses-bridge-assistant-output.utils.js";
3
+ //#region src/utils/codex-openai-responses-bridge-stream.utils.ts
4
+ function buildOpenResponsesOutputItems(response, responseId) {
5
+ const message = response.choices?.[0]?.message;
6
+ if (!message) return [];
7
+ const outputItems = buildAssistantOutputItems({
8
+ message,
9
+ responseId
10
+ });
11
+ readArray(message.tool_calls).forEach((entry, index) => {
12
+ const toolCall = readRecord(entry);
13
+ const fn = readRecord(toolCall?.function);
14
+ const name = readString(fn?.name);
15
+ const argumentsText = readString(fn?.arguments) ?? "{}";
16
+ if (!name) return;
17
+ const callId = readString(toolCall?.id) ?? `${responseId}:call:${index}`;
18
+ outputItems.push({
19
+ type: "function_call",
20
+ id: `${responseId}:function:${index}`,
21
+ call_id: callId,
22
+ name,
23
+ arguments: argumentsText,
24
+ status: "completed"
25
+ });
26
+ });
27
+ return outputItems;
28
+ }
29
+ function normalizeUsageValue(value) {
30
+ const numericValue = readNumber(value);
31
+ if (numericValue !== void 0) return Math.max(0, Math.trunc(numericValue));
32
+ const record = readRecord(value);
33
+ if (!record) return null;
34
+ const next = {};
35
+ for (const [key, entry] of Object.entries(record)) {
36
+ const normalizedEntry = normalizeUsageValue(entry);
37
+ if (normalizedEntry !== null) next[key] = normalizedEntry;
38
+ }
39
+ return Object.keys(next).length > 0 ? next : null;
40
+ }
41
+ function normalizeUsageRecord(rawUsage) {
42
+ const usage = readRecord(rawUsage);
43
+ if (!usage) return {};
44
+ const next = {};
45
+ for (const [key, value] of Object.entries(usage)) {
46
+ const normalizedValue = normalizeUsageValue(value);
47
+ if (normalizedValue === null) continue;
48
+ if (key === "prompt_tokens") {
49
+ next.input_tokens = normalizedValue;
50
+ continue;
51
+ }
52
+ if (key === "completion_tokens") {
53
+ next.output_tokens = normalizedValue;
54
+ continue;
55
+ }
56
+ if (key === "prompt_tokens_details") {
57
+ next.input_tokens_details = normalizedValue;
58
+ continue;
59
+ }
60
+ if (key === "completion_tokens_details") {
61
+ next.output_tokens_details = normalizedValue;
62
+ continue;
63
+ }
64
+ next[key] = normalizedValue;
65
+ }
66
+ return next;
67
+ }
68
+ function buildUsage(response) {
69
+ const promptTokens = Math.max(0, Math.trunc(readNumber(response.usage?.prompt_tokens) ?? 0));
70
+ const completionTokens = Math.max(0, Math.trunc(readNumber(response.usage?.completion_tokens) ?? 0));
71
+ const totalTokens = Math.max(0, Math.trunc(readNumber(response.usage?.total_tokens) ?? promptTokens + completionTokens));
72
+ return {
73
+ ...normalizeUsageRecord(response.usage),
74
+ input_tokens: promptTokens,
75
+ output_tokens: completionTokens,
76
+ total_tokens: totalTokens
77
+ };
78
+ }
79
+ function buildResponseResource(params) {
80
+ const { model, outputItems, responseId, status, usage } = params;
81
+ return {
82
+ id: responseId,
83
+ object: "response",
84
+ created_at: Math.floor(Date.now() / 1e3),
85
+ status: status ?? "completed",
86
+ model,
87
+ output: outputItems,
88
+ usage,
89
+ error: null
90
+ };
91
+ }
92
+ function writeStreamError(response, message) {
93
+ new StreamErrorWriter(response, message).write();
94
+ }
95
+ var StreamErrorWriter = class {
96
+ constructor(response, message) {
97
+ this.response = response;
98
+ this.message = message;
99
+ }
100
+ write = () => {
101
+ this.response.statusCode = 200;
102
+ this.response.setHeader("content-type", "text/event-stream; charset=utf-8");
103
+ this.response.setHeader("cache-control", "no-cache, no-transform");
104
+ this.response.setHeader("connection", "keep-alive");
105
+ writeSseEvent(this.response, "error", {
106
+ type: "error",
107
+ error: {
108
+ code: "invalid_request_error",
109
+ message: this.message
110
+ }
111
+ });
112
+ this.response.end();
113
+ };
114
+ };
115
+ function buildBridgeResponsePayload(params) {
116
+ const { model, response, responseId } = params;
117
+ const outputItems = buildOpenResponsesOutputItems(response, responseId);
118
+ const usage = buildUsage(response);
119
+ return {
120
+ outputItems,
121
+ usage,
122
+ responseResource: buildResponseResource({
123
+ responseId,
124
+ model,
125
+ outputItems,
126
+ usage
127
+ })
128
+ };
129
+ }
130
+ //#endregion
131
+ export { buildBridgeResponsePayload, buildResponseResource, buildUsage, writeStreamError };
@@ -0,0 +1,10 @@
1
+ import { CodexOpenAiResponsesBridgeConfig, CodexOpenAiResponsesBridgeResult } from "../codex-openai-responses-bridge-shared.utils.js";
2
+ import { CodexOpenAiResponsesOutputObserver } from "./codex-openai-responses-stream-writer.utils.js";
3
+
4
+ //#region src/utils/codex-openai-responses-bridge.utils.d.ts
5
+ type CodexOpenAiResponsesBridgeRuntimeConfig = CodexOpenAiResponsesBridgeConfig & {
6
+ outputObserver?: CodexOpenAiResponsesOutputObserver;
7
+ };
8
+ declare function ensureCodexOpenAiResponsesBridge(config: CodexOpenAiResponsesBridgeRuntimeConfig): Promise<CodexOpenAiResponsesBridgeResult>;
9
+ //#endregion
10
+ export { CodexOpenAiResponsesBridgeRuntimeConfig, ensureCodexOpenAiResponsesBridge };
@@ -0,0 +1,138 @@
1
+ import { readBoolean, readRecord } from "../codex-openai-responses-bridge-shared.utils.js";
2
+ import { buildOpenAiCompatibleUpstreamRequest, callOpenAiCompatibleUpstream } from "./codex-openai-responses-bridge-request.utils.js";
3
+ import { buildBridgeResponsePayload, writeStreamError } from "./codex-openai-responses-bridge-stream.utils.js";
4
+ import { writeResponsesUpstreamStream } from "./codex-openai-responses-stream-writer.utils.js";
5
+ import { randomUUID } from "node:crypto";
6
+ import { createServer } from "node:http";
7
+ //#region src/utils/codex-openai-responses-bridge.utils.ts
8
+ const bridgeCache = /* @__PURE__ */ new Map();
9
+ function toBridgeCacheKey(config) {
10
+ return JSON.stringify({
11
+ upstreamApiBase: config.upstreamApiBase,
12
+ upstreamApiKey: config.upstreamApiKey ?? "",
13
+ upstreamExtraHeaders: config.upstreamExtraHeaders ?? {},
14
+ defaultModel: config.defaultModel ?? "",
15
+ modelPrefixes: (config.modelPrefixes ?? []).map((prefix) => prefix.trim().toLowerCase()),
16
+ upstreamReasoningSplit: config.upstreamReasoningSplit === true
17
+ });
18
+ }
19
+ async function readJsonBody(request) {
20
+ const chunks = [];
21
+ for await (const chunk of request) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
22
+ const rawText = Buffer.concat(chunks).toString("utf8").trim();
23
+ if (!rawText) return {};
24
+ try {
25
+ return readRecord(JSON.parse(rawText)) ?? null;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+ async function handleResponsesRequest(request, response, config) {
31
+ await new CodexResponsesBridgeRequestHandler(request, response, config).handle();
32
+ }
33
+ var JsonResponseWriter = class {
34
+ constructor(response) {
35
+ this.response = response;
36
+ }
37
+ write = (statusCode, body) => {
38
+ this.response.statusCode = statusCode;
39
+ this.response.setHeader("content-type", "application/json");
40
+ this.response.end(JSON.stringify(body));
41
+ };
42
+ };
43
+ var CodexResponsesBridgeRequestHandler = class {
44
+ jsonWriter;
45
+ constructor(request, response, config) {
46
+ this.request = request;
47
+ this.response = response;
48
+ this.config = config;
49
+ this.jsonWriter = new JsonResponseWriter(response);
50
+ }
51
+ handle = async () => {
52
+ const body = await readJsonBody(this.request);
53
+ if (!body) {
54
+ this.writeErrorJson(400, "Invalid JSON payload.");
55
+ return;
56
+ }
57
+ try {
58
+ if (readBoolean(body.stream) !== false) {
59
+ await this.writeStreamingResponse(body);
60
+ return;
61
+ }
62
+ await this.writeJsonResponse(body);
63
+ } catch (error) {
64
+ const message = error instanceof Error ? error.message : "Codex OpenAI bridge request failed.";
65
+ if (readBoolean(body.stream) !== false) {
66
+ writeStreamError(this.response, message);
67
+ return;
68
+ }
69
+ this.writeErrorJson(400, message);
70
+ }
71
+ };
72
+ writeStreamingResponse = async (body) => {
73
+ const responseId = randomUUID();
74
+ const upstream = buildOpenAiCompatibleUpstreamRequest({
75
+ config: this.config,
76
+ body,
77
+ stream: true
78
+ });
79
+ const upstreamResponse = await fetch(upstream.request.url, upstream.request.init);
80
+ if (!upstreamResponse.ok) throw new Error((await upstreamResponse.text()).slice(0, 240));
81
+ await writeResponsesUpstreamStream({
82
+ response: this.response,
83
+ responseId,
84
+ model: upstream.model,
85
+ outputObserver: this.config.outputObserver,
86
+ upstreamResponse
87
+ });
88
+ };
89
+ writeJsonResponse = async (body) => {
90
+ const responseId = randomUUID();
91
+ const upstream = await callOpenAiCompatibleUpstream({
92
+ config: this.config,
93
+ body
94
+ });
95
+ const { responseResource } = buildBridgeResponsePayload({
96
+ responseId,
97
+ model: upstream.model,
98
+ response: upstream.response
99
+ });
100
+ this.jsonWriter.write(200, responseResource);
101
+ };
102
+ writeErrorJson = (statusCode, message) => {
103
+ this.jsonWriter.write(statusCode, { error: { message } });
104
+ };
105
+ };
106
+ async function createCodexOpenAiResponsesBridge(config) {
107
+ const server = createServer((request, response) => {
108
+ const pathname = request.url ? new URL(request.url, "http://127.0.0.1").pathname : "/";
109
+ if (request.method === "POST" && (pathname === "/responses" || pathname === "/v1/responses")) {
110
+ handleResponsesRequest(request, response, config);
111
+ return;
112
+ }
113
+ new JsonResponseWriter(response).write(404, { error: { message: `Unsupported Codex bridge path: ${pathname}` } });
114
+ });
115
+ await new Promise((resolve, reject) => {
116
+ server.once("error", reject);
117
+ server.listen(0, "127.0.0.1", () => resolve());
118
+ });
119
+ const address = server.address();
120
+ if (!address || typeof address === "string") throw new Error("Codex bridge failed to bind a loopback port.");
121
+ return { baseUrl: `http://127.0.0.1:${address.port}` };
122
+ }
123
+ async function ensureCodexOpenAiResponsesBridge(config) {
124
+ if (config.outputObserver) return await createCodexOpenAiResponsesBridge(config);
125
+ const key = toBridgeCacheKey(config);
126
+ const existing = bridgeCache.get(key);
127
+ if (existing) return await existing.promise;
128
+ const promise = createCodexOpenAiResponsesBridge(config);
129
+ bridgeCache.set(key, { promise });
130
+ try {
131
+ return await promise;
132
+ } catch (error) {
133
+ bridgeCache.delete(key);
134
+ throw error;
135
+ }
136
+ }
137
+ //#endregion
138
+ export { ensureCodexOpenAiResponsesBridge };
@@ -0,0 +1,10 @@
1
+ //#region src/utils/codex-openai-responses-stream-writer.utils.d.ts
2
+ type CodexOpenAiResponsesOutputObserver = {
3
+ onDone: () => void;
4
+ onReasoningDelta: (delta: string) => void;
5
+ onReasoningDone: () => void;
6
+ onTextDelta: (delta: string) => void;
7
+ onTextDone: () => void;
8
+ };
9
+ //#endregion
10
+ export { CodexOpenAiResponsesOutputObserver };
@@ -0,0 +1,324 @@
1
+ import { nextSequenceNumber, readArray, readNumber, readRecord, readString, writeSseEvent } from "../codex-openai-responses-bridge-shared.utils.js";
2
+ import { buildResponseResource, buildUsage } from "./codex-openai-responses-bridge-stream.utils.js";
3
+ import { extractContentText, extractReasoningText, readOpenAiSseChunks } from "./codex-openai-sse-chunks.utils.js";
4
+ //#region src/utils/codex-openai-responses-stream-writer.utils.ts
5
+ async function writeResponsesUpstreamStream(params) {
6
+ await new CodexResponsesOpenAiStreamWriter(params).write();
7
+ }
8
+ var CodexResponsesOpenAiStreamWriter = class {
9
+ sequenceState = { value: 0 };
10
+ outputItems = [];
11
+ toolCalls = /* @__PURE__ */ new Map();
12
+ outputCount = 0;
13
+ textState = null;
14
+ reasoningState = null;
15
+ usage = {};
16
+ constructor(params) {
17
+ this.params = params;
18
+ }
19
+ write = async () => {
20
+ this.writeHeaders();
21
+ this.writeCreatedEvent();
22
+ for await (const chunk of readOpenAiSseChunks(this.params.upstreamResponse)) this.handleChunk(chunk);
23
+ this.finishReasoning();
24
+ this.finishText();
25
+ this.finishToolCalls();
26
+ this.writeCompletedEvent();
27
+ this.params.outputObserver?.onDone();
28
+ this.params.response.end();
29
+ };
30
+ nextOutputIndex = () => {
31
+ const value = this.outputCount;
32
+ this.outputCount += 1;
33
+ return value;
34
+ };
35
+ writeEvent = (eventType, payload) => {
36
+ writeSseEvent(this.params.response, eventType, {
37
+ ...payload,
38
+ sequence_number: nextSequenceNumber(this.sequenceState)
39
+ });
40
+ };
41
+ writeHeaders = () => {
42
+ this.params.response.statusCode = 200;
43
+ this.params.response.setHeader("content-type", "text/event-stream; charset=utf-8");
44
+ this.params.response.setHeader("cache-control", "no-cache, no-transform");
45
+ this.params.response.setHeader("connection", "keep-alive");
46
+ };
47
+ writeCreatedEvent = () => {
48
+ this.writeEvent("response.created", {
49
+ type: "response.created",
50
+ response: buildResponseResource({
51
+ responseId: this.params.responseId,
52
+ model: this.params.model,
53
+ outputItems: [],
54
+ usage: buildUsage({ usage: {} }),
55
+ status: "in_progress"
56
+ })
57
+ });
58
+ };
59
+ ensureTextState = () => {
60
+ if (this.textState) return this.textState;
61
+ this.textState = {
62
+ outputIndex: this.nextOutputIndex(),
63
+ itemId: `${this.params.responseId}:message:${this.outputCount}`,
64
+ text: ""
65
+ };
66
+ this.writeEvent("response.output_item.added", {
67
+ type: "response.output_item.added",
68
+ output_index: this.textState.outputIndex,
69
+ item: {
70
+ type: "message",
71
+ id: this.textState.itemId,
72
+ role: "assistant",
73
+ status: "in_progress",
74
+ content: []
75
+ }
76
+ });
77
+ this.writeEvent("response.content_part.added", {
78
+ type: "response.content_part.added",
79
+ output_index: this.textState.outputIndex,
80
+ item_id: this.textState.itemId,
81
+ content_index: 0,
82
+ part: {
83
+ type: "output_text",
84
+ text: "",
85
+ annotations: []
86
+ }
87
+ });
88
+ return this.textState;
89
+ };
90
+ ensureReasoningState = () => {
91
+ if (this.reasoningState) return this.reasoningState;
92
+ this.reasoningState = {
93
+ outputIndex: this.nextOutputIndex(),
94
+ itemId: `${this.params.responseId}:reasoning:${this.outputCount}`,
95
+ text: ""
96
+ };
97
+ this.writeEvent("response.output_item.added", {
98
+ type: "response.output_item.added",
99
+ output_index: this.reasoningState.outputIndex,
100
+ item: {
101
+ type: "reasoning",
102
+ id: this.reasoningState.itemId,
103
+ status: "in_progress",
104
+ content: [],
105
+ summary: []
106
+ }
107
+ });
108
+ this.writeEvent("response.reasoning_summary_part.added", {
109
+ type: "response.reasoning_summary_part.added",
110
+ output_index: this.reasoningState.outputIndex,
111
+ item_id: this.reasoningState.itemId,
112
+ summary_index: 0,
113
+ part: {
114
+ type: "summary_text",
115
+ text: ""
116
+ }
117
+ });
118
+ return this.reasoningState;
119
+ };
120
+ ensureToolCallState = (index, delta) => {
121
+ const existing = this.toolCalls.get(index);
122
+ if (existing) return existing;
123
+ const fn = readRecord(delta.function);
124
+ const state = {
125
+ outputIndex: this.nextOutputIndex(),
126
+ itemId: `${this.params.responseId}:function:${this.outputCount}`,
127
+ callId: readString(delta.id) ?? `${this.params.responseId}:call:${index}`,
128
+ name: readString(fn?.name) ?? "tool",
129
+ argumentsText: ""
130
+ };
131
+ this.toolCalls.set(index, state);
132
+ this.writeEvent("response.output_item.added", {
133
+ type: "response.output_item.added",
134
+ output_index: state.outputIndex,
135
+ item: {
136
+ type: "function_call",
137
+ id: state.itemId,
138
+ call_id: state.callId,
139
+ name: state.name,
140
+ arguments: "",
141
+ status: "in_progress"
142
+ }
143
+ });
144
+ return state;
145
+ };
146
+ handleChunk = (chunk) => {
147
+ this.usage = chunk.usage ?? this.usage;
148
+ const delta = chunk.choices?.[0]?.delta;
149
+ this.writeReasoningDelta(extractReasoningText(delta));
150
+ this.writeTextDelta(extractContentText(delta?.content));
151
+ for (const rawToolCall of readArray(delta?.tool_calls)) this.writeToolCallDelta(readRecord(rawToolCall));
152
+ };
153
+ writeReasoningDelta = (reasoningDelta) => {
154
+ if (!reasoningDelta) return;
155
+ const state = this.ensureReasoningState();
156
+ state.text += reasoningDelta;
157
+ this.params.outputObserver?.onReasoningDelta(reasoningDelta);
158
+ this.writeEvent("response.reasoning_summary_text.delta", {
159
+ type: "response.reasoning_summary_text.delta",
160
+ output_index: state.outputIndex,
161
+ item_id: state.itemId,
162
+ summary_index: 0,
163
+ delta: reasoningDelta
164
+ });
165
+ this.writeEvent("response.reasoning_text.delta", {
166
+ type: "response.reasoning_text.delta",
167
+ output_index: state.outputIndex,
168
+ item_id: state.itemId,
169
+ content_index: 0,
170
+ delta: reasoningDelta
171
+ });
172
+ };
173
+ writeTextDelta = (textDelta) => {
174
+ if (!textDelta) return;
175
+ const state = this.ensureTextState();
176
+ state.text += textDelta;
177
+ this.params.outputObserver?.onTextDelta(textDelta);
178
+ this.writeEvent("response.output_text.delta", {
179
+ type: "response.output_text.delta",
180
+ output_index: state.outputIndex,
181
+ item_id: state.itemId,
182
+ content_index: 0,
183
+ delta: textDelta
184
+ });
185
+ };
186
+ writeToolCallDelta = (toolCall) => {
187
+ const toolIndex = Math.trunc(readNumber(toolCall?.index) ?? this.toolCalls.size);
188
+ const fn = readRecord(toolCall?.function);
189
+ const state = this.ensureToolCallState(toolIndex, toolCall ?? {});
190
+ state.name = readString(fn?.name) ?? state.name;
191
+ const argumentsDelta = readString(fn?.arguments) ?? "";
192
+ state.argumentsText += argumentsDelta;
193
+ if (!argumentsDelta) return;
194
+ this.writeEvent("response.function_call_arguments.delta", {
195
+ type: "response.function_call_arguments.delta",
196
+ output_index: state.outputIndex,
197
+ item_id: state.itemId,
198
+ delta: argumentsDelta
199
+ });
200
+ };
201
+ finishReasoning = () => {
202
+ if (!this.reasoningState) return;
203
+ const item = {
204
+ type: "reasoning",
205
+ id: this.reasoningState.itemId,
206
+ summary: [{
207
+ type: "summary_text",
208
+ text: this.reasoningState.text
209
+ }],
210
+ content: [{
211
+ type: "reasoning_text",
212
+ text: this.reasoningState.text
213
+ }],
214
+ status: "completed"
215
+ };
216
+ this.outputItems[this.reasoningState.outputIndex] = item;
217
+ this.writeEvent("response.reasoning_summary_text.done", {
218
+ type: "response.reasoning_summary_text.done",
219
+ output_index: this.reasoningState.outputIndex,
220
+ item_id: this.reasoningState.itemId,
221
+ summary_index: 0,
222
+ text: this.reasoningState.text
223
+ });
224
+ this.writeEvent("response.reasoning_summary_part.done", {
225
+ type: "response.reasoning_summary_part.done",
226
+ output_index: this.reasoningState.outputIndex,
227
+ item_id: this.reasoningState.itemId,
228
+ summary_index: 0,
229
+ part: {
230
+ type: "summary_text",
231
+ text: this.reasoningState.text
232
+ }
233
+ });
234
+ this.writeEvent("response.reasoning_text.done", {
235
+ type: "response.reasoning_text.done",
236
+ output_index: this.reasoningState.outputIndex,
237
+ item_id: this.reasoningState.itemId,
238
+ content_index: 0,
239
+ text: this.reasoningState.text
240
+ });
241
+ this.writeEvent("response.output_item.done", {
242
+ type: "response.output_item.done",
243
+ output_index: this.reasoningState.outputIndex,
244
+ item
245
+ });
246
+ this.params.outputObserver?.onReasoningDone();
247
+ };
248
+ finishText = () => {
249
+ if (!this.textState) return;
250
+ const item = {
251
+ type: "message",
252
+ id: this.textState.itemId,
253
+ role: "assistant",
254
+ status: "completed",
255
+ content: [{
256
+ type: "output_text",
257
+ text: this.textState.text,
258
+ annotations: []
259
+ }]
260
+ };
261
+ this.outputItems[this.textState.outputIndex] = item;
262
+ this.writeEvent("response.output_text.done", {
263
+ type: "response.output_text.done",
264
+ output_index: this.textState.outputIndex,
265
+ item_id: this.textState.itemId,
266
+ content_index: 0,
267
+ text: this.textState.text
268
+ });
269
+ this.writeEvent("response.content_part.done", {
270
+ type: "response.content_part.done",
271
+ output_index: this.textState.outputIndex,
272
+ item_id: this.textState.itemId,
273
+ content_index: 0,
274
+ part: {
275
+ type: "output_text",
276
+ text: this.textState.text,
277
+ annotations: []
278
+ }
279
+ });
280
+ this.writeEvent("response.output_item.done", {
281
+ type: "response.output_item.done",
282
+ output_index: this.textState.outputIndex,
283
+ item
284
+ });
285
+ this.params.outputObserver?.onTextDone();
286
+ };
287
+ finishToolCalls = () => {
288
+ for (const state of [...this.toolCalls.values()].sort((a, b) => a.outputIndex - b.outputIndex)) {
289
+ const item = {
290
+ type: "function_call",
291
+ id: state.itemId,
292
+ call_id: state.callId,
293
+ name: state.name,
294
+ arguments: state.argumentsText,
295
+ status: "completed"
296
+ };
297
+ this.outputItems[state.outputIndex] = item;
298
+ this.writeEvent("response.function_call_arguments.done", {
299
+ type: "response.function_call_arguments.done",
300
+ output_index: state.outputIndex,
301
+ item_id: state.itemId,
302
+ arguments: state.argumentsText
303
+ });
304
+ this.writeEvent("response.output_item.done", {
305
+ type: "response.output_item.done",
306
+ output_index: state.outputIndex,
307
+ item
308
+ });
309
+ }
310
+ };
311
+ writeCompletedEvent = () => {
312
+ this.writeEvent("response.completed", {
313
+ type: "response.completed",
314
+ response: buildResponseResource({
315
+ responseId: this.params.responseId,
316
+ model: this.params.model,
317
+ outputItems: this.outputItems.filter(Boolean),
318
+ usage: buildUsage({ usage: this.usage })
319
+ })
320
+ });
321
+ };
322
+ };
323
+ //#endregion
324
+ export { writeResponsesUpstreamStream };
@@ -0,0 +1,30 @@
1
+ import { readArray, readRawString, readRecord } from "../codex-openai-responses-bridge-shared.utils.js";
2
+ //#region src/utils/codex-openai-sse-chunks.utils.ts
3
+ function extractContentText(content) {
4
+ if (typeof content === "string") return content;
5
+ return readArray(content).map((entry) => {
6
+ const record = readRecord(entry);
7
+ return readRawString(record?.text) ?? readRawString(record?.content) ?? "";
8
+ }).join("");
9
+ }
10
+ function extractReasoningText(delta) {
11
+ return readRawString(delta?.reasoning_content) ?? readRawString(delta?.reasoning) ?? readRawString(delta?.thinking) ?? "";
12
+ }
13
+ async function* readOpenAiSseChunks(upstreamResponse) {
14
+ const stream = upstreamResponse.body;
15
+ if (!stream) return;
16
+ const decoder = new TextDecoder();
17
+ let buffer = "";
18
+ for await (const rawChunk of stream) {
19
+ buffer += decoder.decode(rawChunk, { stream: true }).replaceAll("\r\n", "\n");
20
+ const blocks = buffer.split("\n\n");
21
+ buffer = blocks.pop() ?? "";
22
+ for (const block of blocks) {
23
+ const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n").trim();
24
+ if (!data || data === "[DONE]") continue;
25
+ yield JSON.parse(data);
26
+ }
27
+ }
28
+ }
29
+ //#endregion
30
+ export { extractContentText, extractReasoningText, readOpenAiSseChunks };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/nextclaw-ncp-runtime-codex-sdk",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "private": false,
5
5
  "description": "Optional NCP runtime adapter backed by OpenAI Codex SDK.",
6
6
  "type": "module",
@@ -17,7 +17,7 @@
17
17
  ],
18
18
  "dependencies": {
19
19
  "@openai/codex-sdk": "^0.130.0",
20
- "@nextclaw/ncp": "0.5.12"
20
+ "@nextclaw/ncp": "0.5.14"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/node": "^20.17.6",
@@ -27,7 +27,7 @@
27
27
  "scripts": {
28
28
  "build": "tsdown src/index.ts --dts --clean --target es2022 --no-fixedExtension --unbundle",
29
29
  "lint": "eslint .",
30
- "test": "vitest run src/utils/codex-sdk-ncp-event-mapper.utils.test.ts src/services/codex-live-output-stream.service.test.ts",
30
+ "test": "vitest run src/utils/codex-model-provider.utils.test.ts src/utils/codex-sdk-ncp-event-mapper.utils.test.ts src/services/codex-live-output-stream.service.test.ts src/utils/codex-openai-sse-chunks.utils.test.ts",
31
31
  "tsc": "tsc -p tsconfig.json"
32
32
  }
33
33
  }