@nextclaw/nextclaw-ncp-runtime-codex-sdk 0.1.33 → 0.1.34
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/dist/codex-model-provider.d.ts +4 -0
- package/dist/codex-model-provider.js +8 -0
- package/dist/codex-openai-responses-bridge-assistant-output.utils.js +106 -0
- package/dist/codex-openai-responses-bridge-message-content.utils.js +79 -0
- package/dist/codex-openai-responses-bridge-shared.utils.d.ts +14 -0
- package/dist/codex-openai-responses-bridge-shared.utils.js +36 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -2
- package/dist/utils/codex-openai-responses-bridge-request.utils.js +218 -0
- package/dist/utils/codex-openai-responses-bridge-stream.utils.js +131 -0
- package/dist/utils/codex-openai-responses-bridge.utils.d.ts +10 -0
- package/dist/utils/codex-openai-responses-bridge.utils.js +138 -0
- package/dist/utils/codex-openai-responses-stream-writer.utils.d.ts +10 -0
- package/dist/utils/codex-openai-responses-stream-writer.utils.js +324 -0
- package/dist/utils/codex-openai-sse-chunks.utils.js +30 -0
- package/package.json +3 -3
|
@@ -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.
|
|
3
|
+
"version": "0.1.34",
|
|
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.
|
|
20
|
+
"@nextclaw/ncp": "0.5.13"
|
|
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
|
}
|