@nextclaw/nextclaw-ncp-runtime-plugin-codex-sdk 0.1.20 → 0.1.21
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-input-builder.d.ts +5 -0
- package/dist/codex-input-builder.js +82 -0
- package/dist/codex-model-provider.d.ts +14 -0
- package/dist/codex-model-provider.js +46 -0
- package/dist/codex-openai-responses-bridge-request.d.ts +11 -0
- package/dist/codex-openai-responses-bridge-request.js +334 -0
- package/dist/codex-openai-responses-bridge-shared.d.ts +47 -0
- package/dist/codex-openai-responses-bridge-shared.js +50 -0
- package/dist/codex-openai-responses-bridge-stream.d.ts +22 -0
- package/dist/codex-openai-responses-bridge-stream.js +312 -0
- package/dist/codex-openai-responses-bridge.d.ts +5 -0
- package/dist/codex-openai-responses-bridge.js +140 -0
- package/dist/codex-responses-capability.d.ts +12 -0
- package/dist/codex-responses-capability.js +109 -0
- package/dist/codex-session-type.d.ts +20 -0
- package/dist/codex-session-type.js +57 -0
- package/dist/index.d.ts +1 -13
- package/dist/index.js +39 -1123
- package/openclaw.plugin.json +12 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// src/index.ts
|
|
2
1
|
import {
|
|
3
2
|
findProviderByModel,
|
|
4
3
|
findProviderByName,
|
|
@@ -8,1102 +7,20 @@ import {
|
|
|
8
7
|
import {
|
|
9
8
|
CodexSdkNcpAgentRuntime
|
|
10
9
|
} from "@nextclaw/nextclaw-ncp-runtime-codex-sdk";
|
|
11
|
-
|
|
12
|
-
// src/codex-model-provider.ts
|
|
13
|
-
function readOptionalString(value) {
|
|
14
|
-
if (typeof value !== "string") {
|
|
15
|
-
return void 0;
|
|
16
|
-
}
|
|
17
|
-
const trimmed = value.trim();
|
|
18
|
-
return trimmed || void 0;
|
|
19
|
-
}
|
|
20
|
-
function isValidExternalModelProvider(value) {
|
|
21
|
-
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value);
|
|
22
|
-
}
|
|
23
|
-
function resolveExternalModelProvider(params) {
|
|
24
|
-
const explicitModelProvider = readOptionalString(params.explicitModelProvider);
|
|
25
|
-
if (explicitModelProvider) {
|
|
26
|
-
return explicitModelProvider;
|
|
27
|
-
}
|
|
28
|
-
const providerName = readOptionalString(params.providerName);
|
|
29
|
-
if (providerName && !providerName.startsWith("custom-")) {
|
|
30
|
-
return providerName;
|
|
31
|
-
}
|
|
32
|
-
const providerDisplayName = readOptionalString(params.providerDisplayName);
|
|
33
|
-
if (providerDisplayName && isValidExternalModelProvider(providerDisplayName)) {
|
|
34
|
-
return providerDisplayName;
|
|
35
|
-
}
|
|
36
|
-
throw new Error(
|
|
37
|
-
`[codex] custom provider "${providerName ?? "unknown"}" requires an external model provider id. Set plugins.entries.${params.pluginId}.config.modelProvider or use a provider display name with only letters, numbers, ".", "_" or "-".`
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
function buildUserFacingModelRoute(params) {
|
|
41
|
-
const providerLocalModel = params.providerLocalModel.trim();
|
|
42
|
-
if (!providerLocalModel) {
|
|
43
|
-
return params.resolvedModel.trim();
|
|
44
|
-
}
|
|
45
|
-
return `${params.externalModelProvider}/${providerLocalModel}`;
|
|
46
|
-
}
|
|
47
|
-
function buildCodexBridgeModelProviderId(externalModelProvider) {
|
|
48
|
-
const normalized = externalModelProvider.trim();
|
|
49
|
-
if (!normalized) {
|
|
50
|
-
return "nextclaw-codex-bridge";
|
|
51
|
-
}
|
|
52
|
-
return `nextclaw-codex-bridge-${normalized}`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// src/codex-access-mode.ts
|
|
56
|
-
var DEFAULT_CODEX_ACCESS_MODE = "full-access";
|
|
57
|
-
var CODEX_ACCESS_MODES = ["read-only", "workspace-write", "full-access"];
|
|
58
|
-
var LEGACY_CODEX_SANDBOX_MODES = ["read-only", "workspace-write", "danger-full-access"];
|
|
59
|
-
var CODEX_APPROVAL_POLICY = "never";
|
|
60
|
-
function readEnumString(value, allowedValues) {
|
|
61
|
-
if (typeof value !== "string") {
|
|
62
|
-
return void 0;
|
|
63
|
-
}
|
|
64
|
-
const normalized = value.trim();
|
|
65
|
-
return allowedValues.includes(normalized) ? normalized : void 0;
|
|
66
|
-
}
|
|
67
|
-
function mapLegacySandboxModeToAccessMode(sandboxMode) {
|
|
68
|
-
switch (sandboxMode) {
|
|
69
|
-
case "read-only":
|
|
70
|
-
return "read-only";
|
|
71
|
-
case "workspace-write":
|
|
72
|
-
return "workspace-write";
|
|
73
|
-
case "danger-full-access":
|
|
74
|
-
return "full-access";
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
function mapAccessModeToSandboxMode(accessMode) {
|
|
78
|
-
switch (accessMode) {
|
|
79
|
-
case "read-only":
|
|
80
|
-
return "read-only";
|
|
81
|
-
case "workspace-write":
|
|
82
|
-
return "workspace-write";
|
|
83
|
-
case "full-access":
|
|
84
|
-
return "danger-full-access";
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
function resolveCodexAccessMode(pluginConfig) {
|
|
88
|
-
const explicitAccessMode = readEnumString(pluginConfig.accessMode, CODEX_ACCESS_MODES);
|
|
89
|
-
if (explicitAccessMode) {
|
|
90
|
-
return explicitAccessMode;
|
|
91
|
-
}
|
|
92
|
-
const legacySandboxMode = readEnumString(pluginConfig.sandboxMode, LEGACY_CODEX_SANDBOX_MODES);
|
|
93
|
-
if (legacySandboxMode) {
|
|
94
|
-
return mapLegacySandboxModeToAccessMode(legacySandboxMode);
|
|
95
|
-
}
|
|
96
|
-
return DEFAULT_CODEX_ACCESS_MODE;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// src/codex-input-builder.ts
|
|
100
10
|
import {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return [];
|
|
115
|
-
}
|
|
116
|
-
return raw.map((entry) => readString(entry)).filter((entry) => Boolean(entry)).slice(0, 8);
|
|
117
|
-
}
|
|
118
|
-
function readUserText(input) {
|
|
119
|
-
for (let index = input.messages.length - 1; index >= 0; index -= 1) {
|
|
120
|
-
const message = input.messages[index];
|
|
121
|
-
if (message?.role !== "user") {
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
const text = message.parts.filter((part) => part.type === "text").map((part) => part.text).join("").trim();
|
|
125
|
-
if (text) {
|
|
126
|
-
return text;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return "";
|
|
130
|
-
}
|
|
131
|
-
function buildCodexInputBuilder(workspace) {
|
|
132
|
-
const skillsLoader = new SkillsLoader(workspace);
|
|
133
|
-
return async (input) => {
|
|
134
|
-
const userText = readUserText(input);
|
|
135
|
-
const metadata = input.metadata && typeof input.metadata === "object" && !Array.isArray(input.metadata) ? input.metadata : {};
|
|
136
|
-
const requestedSkills = readRequestedSkills(metadata);
|
|
137
|
-
return buildRequestedSkillsUserPrompt(skillsLoader, requestedSkills, userText);
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// src/codex-openai-responses-bridge.ts
|
|
142
|
-
import { randomUUID } from "crypto";
|
|
143
|
-
import { createServer } from "http";
|
|
144
|
-
|
|
145
|
-
// src/codex-openai-responses-bridge-shared.ts
|
|
146
|
-
function readString2(value) {
|
|
147
|
-
if (typeof value !== "string") {
|
|
148
|
-
return void 0;
|
|
149
|
-
}
|
|
150
|
-
const trimmed = value.trim();
|
|
151
|
-
return trimmed || void 0;
|
|
152
|
-
}
|
|
153
|
-
function readBoolean(value) {
|
|
154
|
-
return typeof value === "boolean" ? value : void 0;
|
|
155
|
-
}
|
|
156
|
-
function readNumber(value) {
|
|
157
|
-
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
158
|
-
return void 0;
|
|
159
|
-
}
|
|
160
|
-
return value;
|
|
161
|
-
}
|
|
162
|
-
function readRecord(value) {
|
|
163
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
164
|
-
return void 0;
|
|
165
|
-
}
|
|
166
|
-
return value;
|
|
167
|
-
}
|
|
168
|
-
function readArray(value) {
|
|
169
|
-
return Array.isArray(value) ? value : [];
|
|
170
|
-
}
|
|
171
|
-
function withTrailingSlash(value) {
|
|
172
|
-
return value.endsWith("/") ? value : `${value}/`;
|
|
173
|
-
}
|
|
174
|
-
function writeSseEvent(response, eventType, payload) {
|
|
175
|
-
response.write(`event: ${eventType}
|
|
176
|
-
`);
|
|
177
|
-
response.write(`data: ${JSON.stringify(payload)}
|
|
178
|
-
|
|
179
|
-
`);
|
|
180
|
-
}
|
|
181
|
-
function nextSequenceNumber(state) {
|
|
182
|
-
const nextValue = state.value;
|
|
183
|
-
state.value += 1;
|
|
184
|
-
return nextValue;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// src/codex-openai-responses-bridge-request.ts
|
|
188
|
-
function stripModelPrefix(model, prefixes) {
|
|
189
|
-
const normalizedModel = model.trim();
|
|
190
|
-
for (const prefix of prefixes) {
|
|
191
|
-
const normalizedPrefix = prefix.trim().toLowerCase();
|
|
192
|
-
if (!normalizedPrefix) {
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
const candidatePrefix = `${normalizedPrefix}/`;
|
|
196
|
-
if (normalizedModel.toLowerCase().startsWith(candidatePrefix)) {
|
|
197
|
-
return normalizedModel.slice(candidatePrefix.length);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return normalizedModel;
|
|
201
|
-
}
|
|
202
|
-
function resolveUpstreamModel(requestedModel, config) {
|
|
203
|
-
const prefixes = (config.modelPrefixes ?? []).filter((value) => value.trim().length > 0);
|
|
204
|
-
const model = stripModelPrefix(readString2(requestedModel) ?? "", prefixes) || stripModelPrefix(config.defaultModel ?? "", prefixes);
|
|
205
|
-
if (!model) {
|
|
206
|
-
throw new Error("Codex bridge could not resolve an upstream model.");
|
|
207
|
-
}
|
|
208
|
-
return model;
|
|
209
|
-
}
|
|
210
|
-
function normalizeTextPart(value) {
|
|
211
|
-
const record = readRecord(value);
|
|
212
|
-
if (!record) {
|
|
213
|
-
return "";
|
|
214
|
-
}
|
|
215
|
-
const type = readString2(record.type);
|
|
216
|
-
if (type !== "input_text" && type !== "output_text") {
|
|
217
|
-
return "";
|
|
218
|
-
}
|
|
219
|
-
return readString2(record.text) ?? "";
|
|
220
|
-
}
|
|
221
|
-
function normalizeImageUrl(value) {
|
|
222
|
-
const record = readRecord(value);
|
|
223
|
-
if (!record || readString2(record.type) !== "input_image") {
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
const source = readRecord(record.source);
|
|
227
|
-
if (!source) {
|
|
228
|
-
return null;
|
|
229
|
-
}
|
|
230
|
-
if (readString2(source.type) === "url") {
|
|
231
|
-
return readString2(source.url) ?? null;
|
|
232
|
-
}
|
|
233
|
-
if (readString2(source.type) === "base64") {
|
|
234
|
-
const mediaType = readString2(source.media_type) ?? "application/octet-stream";
|
|
235
|
-
const data = readString2(source.data);
|
|
236
|
-
if (!data) {
|
|
237
|
-
return null;
|
|
238
|
-
}
|
|
239
|
-
return `data:${mediaType};base64,${data}`;
|
|
240
|
-
}
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
|
-
function normalizeToolOutput(value) {
|
|
244
|
-
if (typeof value === "string") {
|
|
245
|
-
return value;
|
|
246
|
-
}
|
|
247
|
-
if (Array.isArray(value)) {
|
|
248
|
-
const text = value.map((entry) => normalizeTextPart(entry)).filter(Boolean).join("");
|
|
249
|
-
if (text) {
|
|
250
|
-
return text;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
try {
|
|
254
|
-
return JSON.stringify(value ?? "");
|
|
255
|
-
} catch {
|
|
256
|
-
return String(value ?? "");
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
function buildChatContent(content) {
|
|
260
|
-
if (typeof content === "string") {
|
|
261
|
-
return content;
|
|
262
|
-
}
|
|
263
|
-
if (!Array.isArray(content)) {
|
|
264
|
-
return null;
|
|
265
|
-
}
|
|
266
|
-
const chatContent = [];
|
|
267
|
-
for (const entry of content) {
|
|
268
|
-
const text = normalizeTextPart(entry);
|
|
269
|
-
if (text) {
|
|
270
|
-
chatContent.push({
|
|
271
|
-
type: "text",
|
|
272
|
-
text
|
|
273
|
-
});
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
const imageUrl = normalizeImageUrl(entry);
|
|
277
|
-
if (imageUrl) {
|
|
278
|
-
chatContent.push({
|
|
279
|
-
type: "image_url",
|
|
280
|
-
image_url: {
|
|
281
|
-
url: imageUrl
|
|
282
|
-
}
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
if (chatContent.length === 0) {
|
|
287
|
-
return null;
|
|
288
|
-
}
|
|
289
|
-
const textOnly = chatContent.every((entry) => entry.type === "text");
|
|
290
|
-
if (textOnly) {
|
|
291
|
-
return chatContent.map((entry) => readString2(entry.text) ?? "").join("\n");
|
|
292
|
-
}
|
|
293
|
-
return chatContent;
|
|
294
|
-
}
|
|
295
|
-
function readAssistantMessageText(content) {
|
|
296
|
-
if (typeof content === "string") {
|
|
297
|
-
return content;
|
|
298
|
-
}
|
|
299
|
-
if (!Array.isArray(content)) {
|
|
300
|
-
return "";
|
|
301
|
-
}
|
|
302
|
-
return content.filter((entry) => entry.type === "text").map((entry) => readString2(entry.text) ?? "").join("\n");
|
|
303
|
-
}
|
|
304
|
-
function appendMessageInputItem(params) {
|
|
305
|
-
const role = readString2(params.item.role);
|
|
306
|
-
const content = buildChatContent(params.item.content);
|
|
307
|
-
if (role === "assistant") {
|
|
308
|
-
const text = readAssistantMessageText(content);
|
|
309
|
-
if (text.trim()) {
|
|
310
|
-
params.assistantTextParts.push(text);
|
|
311
|
-
}
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
params.flushAssistant();
|
|
315
|
-
const normalizedRole = role === "developer" ? "system" : role;
|
|
316
|
-
if ((normalizedRole === "system" || normalizedRole === "user") && content !== null) {
|
|
317
|
-
params.messages.push({
|
|
318
|
-
role: normalizedRole,
|
|
319
|
-
content
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
function appendFunctionCallItem(params) {
|
|
324
|
-
const name = readString2(params.item.name);
|
|
325
|
-
const argumentsText = readString2(params.item.arguments) ?? "{}";
|
|
326
|
-
if (!name) {
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
const callId = readString2(params.item.call_id) ?? readString2(params.item.id) ?? `call_${params.assistantToolCalls.length}`;
|
|
330
|
-
params.assistantToolCalls.push({
|
|
331
|
-
id: callId,
|
|
332
|
-
type: "function",
|
|
333
|
-
function: {
|
|
334
|
-
name,
|
|
335
|
-
arguments: argumentsText
|
|
336
|
-
}
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
function appendFunctionCallOutputItem(params) {
|
|
340
|
-
params.flushAssistant();
|
|
341
|
-
const callId = readString2(params.item.call_id);
|
|
342
|
-
if (!callId) {
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
params.messages.push({
|
|
346
|
-
role: "tool",
|
|
347
|
-
tool_call_id: callId,
|
|
348
|
-
content: normalizeToolOutput(params.item.output)
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
function buildOpenAiMessages(input, instructions) {
|
|
352
|
-
const messages = [];
|
|
353
|
-
const instructionText = readString2(instructions);
|
|
354
|
-
if (instructionText) {
|
|
355
|
-
messages.push({
|
|
356
|
-
role: "system",
|
|
357
|
-
content: instructionText
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
if (typeof input === "string") {
|
|
361
|
-
return [
|
|
362
|
-
...messages,
|
|
363
|
-
{
|
|
364
|
-
role: "user",
|
|
365
|
-
content: input
|
|
366
|
-
}
|
|
367
|
-
];
|
|
368
|
-
}
|
|
369
|
-
const assistantTextParts = [];
|
|
370
|
-
const assistantToolCalls = [];
|
|
371
|
-
const flushAssistant = () => {
|
|
372
|
-
if (assistantTextParts.length === 0 && assistantToolCalls.length === 0) {
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
messages.push({
|
|
376
|
-
role: "assistant",
|
|
377
|
-
content: assistantTextParts.join("\n").trim() || null,
|
|
378
|
-
...assistantToolCalls.length > 0 ? {
|
|
379
|
-
tool_calls: structuredClone(assistantToolCalls)
|
|
380
|
-
} : {}
|
|
381
|
-
});
|
|
382
|
-
assistantTextParts.length = 0;
|
|
383
|
-
assistantToolCalls.length = 0;
|
|
384
|
-
};
|
|
385
|
-
for (const rawItem of readArray(input)) {
|
|
386
|
-
const item = readRecord(rawItem);
|
|
387
|
-
if (!item) {
|
|
388
|
-
continue;
|
|
389
|
-
}
|
|
390
|
-
const type = readString2(item.type);
|
|
391
|
-
if (type === "message") {
|
|
392
|
-
appendMessageInputItem({
|
|
393
|
-
messages,
|
|
394
|
-
assistantTextParts,
|
|
395
|
-
assistantToolCalls,
|
|
396
|
-
item,
|
|
397
|
-
flushAssistant
|
|
398
|
-
});
|
|
399
|
-
continue;
|
|
400
|
-
}
|
|
401
|
-
if (type === "function_call") {
|
|
402
|
-
appendFunctionCallItem({
|
|
403
|
-
assistantToolCalls,
|
|
404
|
-
item
|
|
405
|
-
});
|
|
406
|
-
continue;
|
|
407
|
-
}
|
|
408
|
-
if (type === "function_call_output") {
|
|
409
|
-
appendFunctionCallOutputItem({
|
|
410
|
-
messages,
|
|
411
|
-
item,
|
|
412
|
-
flushAssistant
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
flushAssistant();
|
|
417
|
-
return messages;
|
|
418
|
-
}
|
|
419
|
-
function toOpenAiTools(value) {
|
|
420
|
-
const tools = [];
|
|
421
|
-
for (const entry of readArray(value)) {
|
|
422
|
-
const tool = readRecord(entry);
|
|
423
|
-
const type = readString2(tool?.type);
|
|
424
|
-
const fn = readRecord(tool?.function);
|
|
425
|
-
const name = readString2(fn?.name) ?? readString2(tool?.name);
|
|
426
|
-
if (type !== "function" || !name) {
|
|
427
|
-
continue;
|
|
428
|
-
}
|
|
429
|
-
const description = (fn ? readString2(fn.description) : void 0) ?? readString2(tool?.description);
|
|
430
|
-
const parameters = (fn ? readRecord(fn.parameters) : void 0) ?? readRecord(tool?.parameters);
|
|
431
|
-
const strict = (fn ? readBoolean(fn.strict) : void 0) ?? readBoolean(tool?.strict);
|
|
432
|
-
tools.push({
|
|
433
|
-
type: "function",
|
|
434
|
-
function: {
|
|
435
|
-
name,
|
|
436
|
-
...description ? { description } : {},
|
|
437
|
-
parameters: parameters ?? {
|
|
438
|
-
type: "object",
|
|
439
|
-
properties: {}
|
|
440
|
-
},
|
|
441
|
-
...strict !== void 0 ? { strict } : {}
|
|
442
|
-
}
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
return tools.length > 0 ? tools : void 0;
|
|
446
|
-
}
|
|
447
|
-
function toOpenAiToolChoice(value) {
|
|
448
|
-
if (value === "auto" || value === "none" || value === "required") {
|
|
449
|
-
return value;
|
|
450
|
-
}
|
|
451
|
-
const record = readRecord(value);
|
|
452
|
-
const fn = readRecord(record?.function);
|
|
453
|
-
const name = readString2(fn?.name) ?? readString2(record?.name);
|
|
454
|
-
if (readString2(record?.type) === "function" && name) {
|
|
455
|
-
return {
|
|
456
|
-
type: "function",
|
|
457
|
-
function: {
|
|
458
|
-
name
|
|
459
|
-
}
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
return void 0;
|
|
463
|
-
}
|
|
464
|
-
async function callOpenAiCompatibleUpstream(params) {
|
|
465
|
-
const model = resolveUpstreamModel(params.body.model, params.config);
|
|
466
|
-
const upstreamUrl = new URL(
|
|
467
|
-
"chat/completions",
|
|
468
|
-
withTrailingSlash(params.config.upstreamApiBase)
|
|
469
|
-
);
|
|
470
|
-
const tools = toOpenAiTools(params.body.tools);
|
|
471
|
-
const toolChoice = toOpenAiToolChoice(params.body.tool_choice);
|
|
472
|
-
const upstreamResponse = await fetch(upstreamUrl.toString(), {
|
|
473
|
-
method: "POST",
|
|
474
|
-
headers: {
|
|
475
|
-
"Content-Type": "application/json",
|
|
476
|
-
...params.config.upstreamApiKey ? {
|
|
477
|
-
Authorization: `Bearer ${params.config.upstreamApiKey}`
|
|
478
|
-
} : {},
|
|
479
|
-
...params.config.upstreamExtraHeaders ?? {}
|
|
480
|
-
},
|
|
481
|
-
body: JSON.stringify({
|
|
482
|
-
model,
|
|
483
|
-
messages: buildOpenAiMessages(params.body.input, params.body.instructions),
|
|
484
|
-
...tools ? { tools } : {},
|
|
485
|
-
...toolChoice ? { tool_choice: toolChoice } : {},
|
|
486
|
-
...typeof params.body.max_output_tokens === "number" ? {
|
|
487
|
-
max_tokens: Math.max(
|
|
488
|
-
1,
|
|
489
|
-
Math.trunc(readNumber(params.body.max_output_tokens) ?? 1)
|
|
490
|
-
)
|
|
491
|
-
} : {}
|
|
492
|
-
})
|
|
493
|
-
});
|
|
494
|
-
const rawText = await upstreamResponse.text();
|
|
495
|
-
let parsed;
|
|
496
|
-
try {
|
|
497
|
-
parsed = JSON.parse(rawText);
|
|
498
|
-
} catch {
|
|
499
|
-
throw new Error(`Bridge upstream returned invalid JSON: ${rawText.slice(0, 240)}`);
|
|
500
|
-
}
|
|
501
|
-
if (!upstreamResponse.ok) {
|
|
502
|
-
throw new Error(
|
|
503
|
-
readString2(parsed.error?.message) ?? rawText.slice(0, 240) ?? `HTTP ${upstreamResponse.status}`
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
|
-
return {
|
|
507
|
-
model,
|
|
508
|
-
response: parsed
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// src/codex-openai-responses-bridge-stream.ts
|
|
513
|
-
function extractAssistantText(content) {
|
|
514
|
-
if (typeof content === "string") {
|
|
515
|
-
return content;
|
|
516
|
-
}
|
|
517
|
-
if (!Array.isArray(content)) {
|
|
518
|
-
return "";
|
|
519
|
-
}
|
|
520
|
-
return content.map((entry) => {
|
|
521
|
-
const record = readRecord(entry);
|
|
522
|
-
if (!record) {
|
|
523
|
-
return "";
|
|
524
|
-
}
|
|
525
|
-
const type = readString2(record.type);
|
|
526
|
-
if (type === "text" || type === "output_text") {
|
|
527
|
-
return readString2(record.text) ?? "";
|
|
528
|
-
}
|
|
529
|
-
return "";
|
|
530
|
-
}).filter(Boolean).join("");
|
|
531
|
-
}
|
|
532
|
-
function buildOpenResponsesOutputItems(response, responseId) {
|
|
533
|
-
const message = response.choices?.[0]?.message;
|
|
534
|
-
if (!message) {
|
|
535
|
-
return [];
|
|
536
|
-
}
|
|
537
|
-
const outputItems = [];
|
|
538
|
-
const text = extractAssistantText(message.content).trim();
|
|
539
|
-
if (text) {
|
|
540
|
-
outputItems.push({
|
|
541
|
-
type: "message",
|
|
542
|
-
id: `${responseId}:message:0`,
|
|
543
|
-
role: "assistant",
|
|
544
|
-
status: "completed",
|
|
545
|
-
content: [
|
|
546
|
-
{
|
|
547
|
-
type: "output_text",
|
|
548
|
-
text,
|
|
549
|
-
annotations: []
|
|
550
|
-
}
|
|
551
|
-
]
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
const toolCalls = readArray(message.tool_calls);
|
|
555
|
-
toolCalls.forEach((entry, index) => {
|
|
556
|
-
const toolCall = readRecord(entry);
|
|
557
|
-
const fn = readRecord(toolCall?.function);
|
|
558
|
-
const name = readString2(fn?.name);
|
|
559
|
-
const argumentsText = readString2(fn?.arguments) ?? "{}";
|
|
560
|
-
if (!name) {
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
|
-
const callId = readString2(toolCall?.id) ?? `${responseId}:call:${index}`;
|
|
564
|
-
outputItems.push({
|
|
565
|
-
type: "function_call",
|
|
566
|
-
id: `${responseId}:function:${index}`,
|
|
567
|
-
call_id: callId,
|
|
568
|
-
name,
|
|
569
|
-
arguments: argumentsText,
|
|
570
|
-
status: "completed"
|
|
571
|
-
});
|
|
572
|
-
});
|
|
573
|
-
return outputItems;
|
|
574
|
-
}
|
|
575
|
-
function buildUsage(response) {
|
|
576
|
-
const promptTokens = Math.max(0, Math.trunc(readNumber(response.usage?.prompt_tokens) ?? 0));
|
|
577
|
-
const completionTokens = Math.max(
|
|
578
|
-
0,
|
|
579
|
-
Math.trunc(readNumber(response.usage?.completion_tokens) ?? 0)
|
|
580
|
-
);
|
|
581
|
-
const totalTokens = Math.max(
|
|
582
|
-
0,
|
|
583
|
-
Math.trunc(readNumber(response.usage?.total_tokens) ?? promptTokens + completionTokens)
|
|
584
|
-
);
|
|
585
|
-
return {
|
|
586
|
-
input_tokens: promptTokens,
|
|
587
|
-
input_tokens_details: null,
|
|
588
|
-
output_tokens: completionTokens,
|
|
589
|
-
output_tokens_details: null,
|
|
590
|
-
total_tokens: totalTokens
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
function buildResponseResource(params) {
|
|
594
|
-
return {
|
|
595
|
-
id: params.responseId,
|
|
596
|
-
object: "response",
|
|
597
|
-
created_at: Math.floor(Date.now() / 1e3),
|
|
598
|
-
status: params.status ?? "completed",
|
|
599
|
-
model: params.model,
|
|
600
|
-
output: params.outputItems,
|
|
601
|
-
usage: params.usage,
|
|
602
|
-
error: null
|
|
603
|
-
};
|
|
604
|
-
}
|
|
605
|
-
function cloneRecord(value) {
|
|
606
|
-
return structuredClone(value);
|
|
607
|
-
}
|
|
608
|
-
function buildInProgressOutputItem(item) {
|
|
609
|
-
const type = readString2(item.type);
|
|
610
|
-
if (type === "message") {
|
|
611
|
-
return {
|
|
612
|
-
...cloneRecord(item),
|
|
613
|
-
status: "in_progress",
|
|
614
|
-
content: []
|
|
615
|
-
};
|
|
616
|
-
}
|
|
617
|
-
if (type === "function_call") {
|
|
618
|
-
return {
|
|
619
|
-
...cloneRecord(item),
|
|
620
|
-
status: "in_progress",
|
|
621
|
-
arguments: ""
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
return cloneRecord(item);
|
|
625
|
-
}
|
|
626
|
-
function writeMessageOutputItemEvents(params) {
|
|
627
|
-
const itemId = readString2(params.item.id);
|
|
628
|
-
const content = readArray(params.item.content);
|
|
629
|
-
const textPart = content.find((entry) => readString2(readRecord(entry)?.type) === "output_text");
|
|
630
|
-
const text = readString2(readRecord(textPart)?.text) ?? "";
|
|
631
|
-
writeSseEvent(params.response, "response.output_item.added", {
|
|
632
|
-
type: "response.output_item.added",
|
|
633
|
-
sequence_number: nextSequenceNumber(params.sequenceState),
|
|
634
|
-
output_index: params.outputIndex,
|
|
635
|
-
item: buildInProgressOutputItem(params.item)
|
|
636
|
-
});
|
|
637
|
-
if (itemId) {
|
|
638
|
-
writeSseEvent(params.response, "response.content_part.added", {
|
|
639
|
-
type: "response.content_part.added",
|
|
640
|
-
sequence_number: nextSequenceNumber(params.sequenceState),
|
|
641
|
-
output_index: params.outputIndex,
|
|
642
|
-
item_id: itemId,
|
|
643
|
-
content_index: 0,
|
|
644
|
-
part: {
|
|
645
|
-
type: "output_text",
|
|
646
|
-
text: "",
|
|
647
|
-
annotations: []
|
|
648
|
-
}
|
|
649
|
-
});
|
|
650
|
-
if (text) {
|
|
651
|
-
writeSseEvent(params.response, "response.output_text.delta", {
|
|
652
|
-
type: "response.output_text.delta",
|
|
653
|
-
sequence_number: nextSequenceNumber(params.sequenceState),
|
|
654
|
-
output_index: params.outputIndex,
|
|
655
|
-
item_id: itemId,
|
|
656
|
-
content_index: 0,
|
|
657
|
-
delta: text
|
|
658
|
-
});
|
|
659
|
-
}
|
|
660
|
-
writeSseEvent(params.response, "response.output_text.done", {
|
|
661
|
-
type: "response.output_text.done",
|
|
662
|
-
sequence_number: nextSequenceNumber(params.sequenceState),
|
|
663
|
-
output_index: params.outputIndex,
|
|
664
|
-
item_id: itemId,
|
|
665
|
-
content_index: 0,
|
|
666
|
-
text
|
|
667
|
-
});
|
|
668
|
-
writeSseEvent(params.response, "response.content_part.done", {
|
|
669
|
-
type: "response.content_part.done",
|
|
670
|
-
sequence_number: nextSequenceNumber(params.sequenceState),
|
|
671
|
-
output_index: params.outputIndex,
|
|
672
|
-
item_id: itemId,
|
|
673
|
-
content_index: 0,
|
|
674
|
-
part: {
|
|
675
|
-
type: "output_text",
|
|
676
|
-
text,
|
|
677
|
-
annotations: []
|
|
678
|
-
}
|
|
679
|
-
});
|
|
680
|
-
}
|
|
681
|
-
writeSseEvent(params.response, "response.output_item.done", {
|
|
682
|
-
type: "response.output_item.done",
|
|
683
|
-
sequence_number: nextSequenceNumber(params.sequenceState),
|
|
684
|
-
output_index: params.outputIndex,
|
|
685
|
-
item: params.item
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
function writeFunctionCallOutputItemEvents(params) {
|
|
689
|
-
const itemId = readString2(params.item.id);
|
|
690
|
-
const argumentsText = readString2(params.item.arguments) ?? "";
|
|
691
|
-
writeSseEvent(params.response, "response.output_item.added", {
|
|
692
|
-
type: "response.output_item.added",
|
|
693
|
-
sequence_number: nextSequenceNumber(params.sequenceState),
|
|
694
|
-
output_index: params.outputIndex,
|
|
695
|
-
item: buildInProgressOutputItem(params.item)
|
|
696
|
-
});
|
|
697
|
-
if (itemId && argumentsText) {
|
|
698
|
-
writeSseEvent(params.response, "response.function_call_arguments.delta", {
|
|
699
|
-
type: "response.function_call_arguments.delta",
|
|
700
|
-
sequence_number: nextSequenceNumber(params.sequenceState),
|
|
701
|
-
output_index: params.outputIndex,
|
|
702
|
-
item_id: itemId,
|
|
703
|
-
delta: argumentsText
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
if (itemId) {
|
|
707
|
-
writeSseEvent(params.response, "response.function_call_arguments.done", {
|
|
708
|
-
type: "response.function_call_arguments.done",
|
|
709
|
-
sequence_number: nextSequenceNumber(params.sequenceState),
|
|
710
|
-
output_index: params.outputIndex,
|
|
711
|
-
item_id: itemId,
|
|
712
|
-
arguments: argumentsText
|
|
713
|
-
});
|
|
714
|
-
}
|
|
715
|
-
writeSseEvent(params.response, "response.output_item.done", {
|
|
716
|
-
type: "response.output_item.done",
|
|
717
|
-
sequence_number: nextSequenceNumber(params.sequenceState),
|
|
718
|
-
output_index: params.outputIndex,
|
|
719
|
-
item: params.item
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
function writeResponseOutputItemEvents(params) {
|
|
723
|
-
params.outputItems.forEach((item, outputIndex) => {
|
|
724
|
-
const type = readString2(item.type);
|
|
725
|
-
if (type === "message") {
|
|
726
|
-
writeMessageOutputItemEvents({
|
|
727
|
-
response: params.response,
|
|
728
|
-
item,
|
|
729
|
-
outputIndex,
|
|
730
|
-
sequenceState: params.sequenceState
|
|
731
|
-
});
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
|
-
if (type === "function_call") {
|
|
735
|
-
writeFunctionCallOutputItemEvents({
|
|
736
|
-
response: params.response,
|
|
737
|
-
item,
|
|
738
|
-
outputIndex,
|
|
739
|
-
sequenceState: params.sequenceState
|
|
740
|
-
});
|
|
741
|
-
return;
|
|
742
|
-
}
|
|
743
|
-
writeSseEvent(params.response, "response.output_item.done", {
|
|
744
|
-
type: "response.output_item.done",
|
|
745
|
-
sequence_number: nextSequenceNumber(params.sequenceState),
|
|
746
|
-
output_index: outputIndex,
|
|
747
|
-
item
|
|
748
|
-
});
|
|
749
|
-
});
|
|
750
|
-
}
|
|
751
|
-
function writeStreamError(response, message) {
|
|
752
|
-
response.statusCode = 200;
|
|
753
|
-
response.setHeader("content-type", "text/event-stream; charset=utf-8");
|
|
754
|
-
response.setHeader("cache-control", "no-cache, no-transform");
|
|
755
|
-
response.setHeader("connection", "keep-alive");
|
|
756
|
-
writeSseEvent(response, "error", {
|
|
757
|
-
type: "error",
|
|
758
|
-
error: {
|
|
759
|
-
code: "invalid_request_error",
|
|
760
|
-
message
|
|
761
|
-
}
|
|
762
|
-
});
|
|
763
|
-
response.end();
|
|
764
|
-
}
|
|
765
|
-
function buildBridgeResponsePayload(params) {
|
|
766
|
-
const outputItems = buildOpenResponsesOutputItems(params.response, params.responseId);
|
|
767
|
-
const usage = buildUsage(params.response);
|
|
768
|
-
return {
|
|
769
|
-
outputItems,
|
|
770
|
-
usage,
|
|
771
|
-
responseResource: buildResponseResource({
|
|
772
|
-
responseId: params.responseId,
|
|
773
|
-
model: params.model,
|
|
774
|
-
outputItems,
|
|
775
|
-
usage
|
|
776
|
-
})
|
|
777
|
-
};
|
|
778
|
-
}
|
|
779
|
-
function writeResponsesStream(params) {
|
|
780
|
-
const sequenceState = {
|
|
781
|
-
value: 0
|
|
782
|
-
};
|
|
783
|
-
params.response.statusCode = 200;
|
|
784
|
-
params.response.setHeader("content-type", "text/event-stream; charset=utf-8");
|
|
785
|
-
params.response.setHeader("cache-control", "no-cache, no-transform");
|
|
786
|
-
params.response.setHeader("connection", "keep-alive");
|
|
787
|
-
writeSseEvent(params.response, "response.created", {
|
|
788
|
-
type: "response.created",
|
|
789
|
-
sequence_number: nextSequenceNumber(sequenceState),
|
|
790
|
-
response: buildResponseResource({
|
|
791
|
-
responseId: params.responseId,
|
|
792
|
-
model: params.model,
|
|
793
|
-
outputItems: [],
|
|
794
|
-
usage: buildUsage({
|
|
795
|
-
usage: {}
|
|
796
|
-
}),
|
|
797
|
-
status: "in_progress"
|
|
798
|
-
})
|
|
799
|
-
});
|
|
800
|
-
writeResponseOutputItemEvents({
|
|
801
|
-
response: params.response,
|
|
802
|
-
outputItems: params.outputItems,
|
|
803
|
-
sequenceState
|
|
804
|
-
});
|
|
805
|
-
writeSseEvent(params.response, "response.completed", {
|
|
806
|
-
type: "response.completed",
|
|
807
|
-
sequence_number: nextSequenceNumber(sequenceState),
|
|
808
|
-
response: params.responseResource
|
|
809
|
-
});
|
|
810
|
-
params.response.end();
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
// src/codex-openai-responses-bridge.ts
|
|
814
|
-
var bridgeCache = /* @__PURE__ */ new Map();
|
|
815
|
-
function toBridgeCacheKey(config) {
|
|
816
|
-
return JSON.stringify({
|
|
817
|
-
upstreamApiBase: config.upstreamApiBase,
|
|
818
|
-
upstreamApiKey: config.upstreamApiKey ?? "",
|
|
819
|
-
upstreamExtraHeaders: config.upstreamExtraHeaders ?? {},
|
|
820
|
-
defaultModel: config.defaultModel ?? "",
|
|
821
|
-
modelPrefixes: (config.modelPrefixes ?? []).map((prefix) => prefix.trim().toLowerCase())
|
|
822
|
-
});
|
|
823
|
-
}
|
|
824
|
-
async function readJsonBody(request) {
|
|
825
|
-
const chunks = [];
|
|
826
|
-
for await (const chunk of request) {
|
|
827
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
828
|
-
}
|
|
829
|
-
const rawText = Buffer.concat(chunks).toString("utf8").trim();
|
|
830
|
-
if (!rawText) {
|
|
831
|
-
return {};
|
|
832
|
-
}
|
|
833
|
-
try {
|
|
834
|
-
return readRecord(JSON.parse(rawText)) ?? null;
|
|
835
|
-
} catch {
|
|
836
|
-
return null;
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
async function handleResponsesRequest(request, response, config) {
|
|
840
|
-
const body = await readJsonBody(request);
|
|
841
|
-
if (!body) {
|
|
842
|
-
response.statusCode = 400;
|
|
843
|
-
response.setHeader("content-type", "application/json");
|
|
844
|
-
response.end(
|
|
845
|
-
JSON.stringify({
|
|
846
|
-
error: {
|
|
847
|
-
message: "Invalid JSON payload."
|
|
848
|
-
}
|
|
849
|
-
})
|
|
850
|
-
);
|
|
851
|
-
return;
|
|
852
|
-
}
|
|
853
|
-
try {
|
|
854
|
-
const upstream = await callOpenAiCompatibleUpstream({
|
|
855
|
-
config,
|
|
856
|
-
body
|
|
857
|
-
});
|
|
858
|
-
const responseId = randomUUID();
|
|
859
|
-
const { outputItems, responseResource } = buildBridgeResponsePayload({
|
|
860
|
-
responseId,
|
|
861
|
-
model: upstream.model,
|
|
862
|
-
response: upstream.response
|
|
863
|
-
});
|
|
864
|
-
const wantsStream = readBoolean(body.stream) !== false;
|
|
865
|
-
if (wantsStream) {
|
|
866
|
-
writeResponsesStream({
|
|
867
|
-
response,
|
|
868
|
-
responseId,
|
|
869
|
-
model: upstream.model,
|
|
870
|
-
outputItems,
|
|
871
|
-
responseResource
|
|
872
|
-
});
|
|
873
|
-
return;
|
|
874
|
-
}
|
|
875
|
-
response.statusCode = 200;
|
|
876
|
-
response.setHeader("content-type", "application/json");
|
|
877
|
-
response.end(JSON.stringify(responseResource));
|
|
878
|
-
} catch (error) {
|
|
879
|
-
const message = error instanceof Error ? error.message : "Codex OpenAI bridge request failed.";
|
|
880
|
-
if (readBoolean(body.stream) !== false) {
|
|
881
|
-
writeStreamError(response, message);
|
|
882
|
-
return;
|
|
883
|
-
}
|
|
884
|
-
response.statusCode = 400;
|
|
885
|
-
response.setHeader("content-type", "application/json");
|
|
886
|
-
response.end(
|
|
887
|
-
JSON.stringify({
|
|
888
|
-
error: {
|
|
889
|
-
message
|
|
890
|
-
}
|
|
891
|
-
})
|
|
892
|
-
);
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
async function createCodexOpenAiResponsesBridge(config) {
|
|
896
|
-
const server = createServer((request, response) => {
|
|
897
|
-
const pathname = request.url ? new URL(request.url, "http://127.0.0.1").pathname : "/";
|
|
898
|
-
if (request.method === "POST" && (pathname === "/responses" || pathname === "/v1/responses")) {
|
|
899
|
-
void handleResponsesRequest(request, response, config);
|
|
900
|
-
return;
|
|
901
|
-
}
|
|
902
|
-
response.statusCode = 404;
|
|
903
|
-
response.setHeader("content-type", "application/json");
|
|
904
|
-
response.end(
|
|
905
|
-
JSON.stringify({
|
|
906
|
-
error: {
|
|
907
|
-
message: `Unsupported Codex bridge path: ${pathname}`
|
|
908
|
-
}
|
|
909
|
-
})
|
|
910
|
-
);
|
|
911
|
-
});
|
|
912
|
-
await new Promise((resolve, reject) => {
|
|
913
|
-
server.once("error", reject);
|
|
914
|
-
server.listen(0, "127.0.0.1", () => resolve());
|
|
915
|
-
});
|
|
916
|
-
const address = server.address();
|
|
917
|
-
if (!address || typeof address === "string") {
|
|
918
|
-
throw new Error("Codex bridge failed to bind a loopback port.");
|
|
919
|
-
}
|
|
920
|
-
return {
|
|
921
|
-
baseUrl: `http://127.0.0.1:${address.port}`
|
|
922
|
-
};
|
|
923
|
-
}
|
|
924
|
-
async function ensureCodexOpenAiResponsesBridge(config) {
|
|
925
|
-
const key = toBridgeCacheKey(config);
|
|
926
|
-
const existing = bridgeCache.get(key);
|
|
927
|
-
if (existing) {
|
|
928
|
-
return await existing.promise;
|
|
929
|
-
}
|
|
930
|
-
const promise = createCodexOpenAiResponsesBridge(config);
|
|
931
|
-
bridgeCache.set(key, { promise });
|
|
932
|
-
try {
|
|
933
|
-
return await promise;
|
|
934
|
-
} catch (error) {
|
|
935
|
-
bridgeCache.delete(key);
|
|
936
|
-
throw error;
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
// src/codex-responses-capability.ts
|
|
941
|
-
var codexResponsesProbeCache = /* @__PURE__ */ new Map();
|
|
942
|
-
function normalizeApiBase(value) {
|
|
943
|
-
return value.endsWith("/") ? value : `${value}/`;
|
|
944
|
-
}
|
|
945
|
-
function readErrorMessage(value) {
|
|
946
|
-
if (typeof value === "string") {
|
|
947
|
-
return value.trim();
|
|
948
|
-
}
|
|
949
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
950
|
-
return "";
|
|
951
|
-
}
|
|
952
|
-
const record = value;
|
|
953
|
-
return readString2(record.message) ?? readString2(record.error) ?? "";
|
|
954
|
-
}
|
|
955
|
-
function shouldTreatResponsesProbeFailureAsUnsupported(params) {
|
|
956
|
-
const normalizedMessage = params.message.trim().toLowerCase();
|
|
957
|
-
if (params.status === 404 || params.status === 405 || params.status === 501) {
|
|
958
|
-
return true;
|
|
959
|
-
}
|
|
960
|
-
if (params.status >= 500) {
|
|
961
|
-
return false;
|
|
962
|
-
}
|
|
963
|
-
return [
|
|
964
|
-
"unsupported model",
|
|
965
|
-
"not support",
|
|
966
|
-
"not supported",
|
|
967
|
-
"unsupported endpoint",
|
|
968
|
-
"unknown url",
|
|
969
|
-
"no route matched",
|
|
970
|
-
"responses api"
|
|
971
|
-
].some((token) => normalizedMessage.includes(token));
|
|
972
|
-
}
|
|
973
|
-
async function probeCodexResponsesApiSupport(params) {
|
|
974
|
-
const cacheKey = JSON.stringify({
|
|
975
|
-
apiBase: params.apiBase,
|
|
976
|
-
apiKey: params.apiKey,
|
|
977
|
-
extraHeaders: params.extraHeaders ?? {},
|
|
978
|
-
model: params.model
|
|
979
|
-
});
|
|
980
|
-
const existing = codexResponsesProbeCache.get(cacheKey);
|
|
981
|
-
if (existing) {
|
|
982
|
-
return await existing;
|
|
983
|
-
}
|
|
984
|
-
const probePromise = (async () => {
|
|
985
|
-
const response = await fetch(new URL("responses", normalizeApiBase(params.apiBase)), {
|
|
986
|
-
method: "POST",
|
|
987
|
-
headers: {
|
|
988
|
-
"Content-Type": "application/json",
|
|
989
|
-
Authorization: `Bearer ${params.apiKey}`,
|
|
990
|
-
...params.extraHeaders ?? {}
|
|
991
|
-
},
|
|
992
|
-
body: JSON.stringify({
|
|
993
|
-
model: params.model,
|
|
994
|
-
input: "ping",
|
|
995
|
-
max_output_tokens: 1,
|
|
996
|
-
stream: false
|
|
997
|
-
})
|
|
998
|
-
});
|
|
999
|
-
const rawText = await response.text();
|
|
1000
|
-
let parsed = null;
|
|
1001
|
-
try {
|
|
1002
|
-
parsed = JSON.parse(rawText);
|
|
1003
|
-
} catch {
|
|
1004
|
-
parsed = null;
|
|
1005
|
-
}
|
|
1006
|
-
const parsedRecord = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
1007
|
-
const message = readErrorMessage(parsedRecord?.error) || readErrorMessage(parsed) || rawText.slice(0, 240);
|
|
1008
|
-
const responseFailed = readString2(parsedRecord?.status)?.toLowerCase() === "failed" || Boolean(parsedRecord?.error);
|
|
1009
|
-
if (response.ok && !responseFailed) {
|
|
1010
|
-
return true;
|
|
1011
|
-
}
|
|
1012
|
-
return !shouldTreatResponsesProbeFailureAsUnsupported({
|
|
1013
|
-
status: response.status,
|
|
1014
|
-
message
|
|
1015
|
-
});
|
|
1016
|
-
})();
|
|
1017
|
-
codexResponsesProbeCache.set(cacheKey, probePromise);
|
|
1018
|
-
try {
|
|
1019
|
-
return await probePromise;
|
|
1020
|
-
} catch (error) {
|
|
1021
|
-
codexResponsesProbeCache.delete(cacheKey);
|
|
1022
|
-
throw error;
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
async function resolveCodexResponsesApiSupport(params) {
|
|
1026
|
-
if (params.capabilitySpec?.supportsResponsesApi === true) {
|
|
1027
|
-
return true;
|
|
1028
|
-
}
|
|
1029
|
-
if (params.capabilitySpec?.supportsResponsesApi === false) {
|
|
1030
|
-
return false;
|
|
1031
|
-
}
|
|
1032
|
-
const explicitWireApi = readString2(params.wireApi)?.toLowerCase();
|
|
1033
|
-
if (explicitWireApi === "chat") {
|
|
1034
|
-
return false;
|
|
1035
|
-
}
|
|
1036
|
-
if (explicitWireApi === "responses") {
|
|
1037
|
-
return true;
|
|
1038
|
-
}
|
|
1039
|
-
return await probeCodexResponsesApiSupport({
|
|
1040
|
-
apiBase: params.apiBase,
|
|
1041
|
-
apiKey: params.apiKey,
|
|
1042
|
-
extraHeaders: params.extraHeaders,
|
|
1043
|
-
model: params.model
|
|
1044
|
-
});
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
// src/codex-session-type.ts
|
|
1048
|
-
function readString3(value) {
|
|
1049
|
-
if (typeof value !== "string") {
|
|
1050
|
-
return void 0;
|
|
1051
|
-
}
|
|
1052
|
-
const trimmed = value.trim();
|
|
1053
|
-
return trimmed || void 0;
|
|
1054
|
-
}
|
|
1055
|
-
function readStringArray(value) {
|
|
1056
|
-
if (!Array.isArray(value)) {
|
|
1057
|
-
return void 0;
|
|
1058
|
-
}
|
|
1059
|
-
const values = value.map((entry) => readString3(entry)).filter((entry) => Boolean(entry));
|
|
1060
|
-
return values.length > 0 ? values : void 0;
|
|
1061
|
-
}
|
|
1062
|
-
function dedupeStrings(values) {
|
|
1063
|
-
return Array.from(new Set(values));
|
|
1064
|
-
}
|
|
1065
|
-
function resolveConfiguredCodexModels(params) {
|
|
1066
|
-
const explicitSupportedModels = readStringArray(params.pluginConfig.supportedModels);
|
|
1067
|
-
if (explicitSupportedModels) {
|
|
1068
|
-
return dedupeStrings(explicitSupportedModels);
|
|
1069
|
-
}
|
|
1070
|
-
const configuredProviders = params.config.providers && typeof params.config.providers === "object" && !Array.isArray(params.config.providers) ? params.config.providers : {};
|
|
1071
|
-
const configuredModels = Object.entries(configuredProviders).flatMap(
|
|
1072
|
-
([providerName, provider]) => (provider.models ?? []).map((modelName) => readString3(modelName)).filter((modelName) => Boolean(modelName)).map((modelName) => `${providerName}/${modelName}`)
|
|
1073
|
-
);
|
|
1074
|
-
const fallbackModel = readString3(params.pluginConfig.model) ?? params.config.agents.defaults.model;
|
|
1075
|
-
const fallbackModels = fallbackModel ? [fallbackModel] : [];
|
|
1076
|
-
return dedupeStrings(configuredModels.length > 0 ? configuredModels : fallbackModels);
|
|
1077
|
-
}
|
|
1078
|
-
function resolveRecommendedCodexModel(params) {
|
|
1079
|
-
const configuredModel = readString3(params.pluginConfig.model) ?? params.config.agents.defaults.model;
|
|
1080
|
-
if (params.supportedModels.includes(configuredModel)) {
|
|
1081
|
-
return configuredModel;
|
|
1082
|
-
}
|
|
1083
|
-
return params.supportedModels[0] ?? configuredModel ?? null;
|
|
1084
|
-
}
|
|
1085
|
-
function createDescribeCodexSessionType(params) {
|
|
1086
|
-
return () => {
|
|
1087
|
-
const supportedModels = resolveConfiguredCodexModels(params);
|
|
1088
|
-
return {
|
|
1089
|
-
ready: true,
|
|
1090
|
-
reason: null,
|
|
1091
|
-
reasonMessage: null,
|
|
1092
|
-
supportedModels,
|
|
1093
|
-
recommendedModel: resolveRecommendedCodexModel({
|
|
1094
|
-
config: params.config,
|
|
1095
|
-
pluginConfig: params.pluginConfig,
|
|
1096
|
-
supportedModels
|
|
1097
|
-
}),
|
|
1098
|
-
cta: null
|
|
1099
|
-
};
|
|
1100
|
-
};
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
// src/index.ts
|
|
1104
|
-
var PLUGIN_ID = "nextclaw-ncp-runtime-plugin-codex-sdk";
|
|
1105
|
-
var CODEX_RUNTIME_KIND = "codex";
|
|
1106
|
-
var DeferredCodexSdkNcpAgentRuntime = class {
|
|
11
|
+
buildUserFacingModelRoute,
|
|
12
|
+
buildCodexBridgeModelProviderId,
|
|
13
|
+
resolveExternalModelProvider
|
|
14
|
+
} from "./codex-model-provider.js";
|
|
15
|
+
import { buildCodexInputBuilder } from "./codex-input-builder.js";
|
|
16
|
+
import { ensureCodexOpenAiResponsesBridge } from "./codex-openai-responses-bridge.js";
|
|
17
|
+
import { resolveCodexResponsesApiSupport } from "./codex-responses-capability.js";
|
|
18
|
+
import {
|
|
19
|
+
createDescribeCodexSessionType
|
|
20
|
+
} from "./codex-session-type.js";
|
|
21
|
+
const PLUGIN_ID = "nextclaw-ncp-runtime-plugin-codex-sdk";
|
|
22
|
+
const CODEX_RUNTIME_KIND = "codex";
|
|
23
|
+
class DeferredCodexSdkNcpAgentRuntime {
|
|
1107
24
|
constructor(createRuntime) {
|
|
1108
25
|
this.createRuntime = createRuntime;
|
|
1109
26
|
}
|
|
@@ -1121,28 +38,28 @@ var DeferredCodexSdkNcpAgentRuntime = class {
|
|
|
1121
38
|
}.bind(this);
|
|
1122
39
|
return stream();
|
|
1123
40
|
}
|
|
1124
|
-
}
|
|
1125
|
-
function
|
|
41
|
+
}
|
|
42
|
+
function readString(value) {
|
|
1126
43
|
if (typeof value !== "string") {
|
|
1127
44
|
return void 0;
|
|
1128
45
|
}
|
|
1129
46
|
const trimmed = value.trim();
|
|
1130
47
|
return trimmed || void 0;
|
|
1131
48
|
}
|
|
1132
|
-
function
|
|
49
|
+
function readBoolean(value) {
|
|
1133
50
|
return typeof value === "boolean" ? value : void 0;
|
|
1134
51
|
}
|
|
1135
|
-
function
|
|
52
|
+
function readRecord(value) {
|
|
1136
53
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1137
54
|
return void 0;
|
|
1138
55
|
}
|
|
1139
56
|
return value;
|
|
1140
57
|
}
|
|
1141
|
-
function
|
|
58
|
+
function readStringArray(value) {
|
|
1142
59
|
if (!Array.isArray(value)) {
|
|
1143
60
|
return void 0;
|
|
1144
61
|
}
|
|
1145
|
-
const values = value.map((entry) =>
|
|
62
|
+
const values = value.map((entry) => readString(entry)).filter((entry) => Boolean(entry));
|
|
1146
63
|
return values.length > 0 ? values : void 0;
|
|
1147
64
|
}
|
|
1148
65
|
function resolveCodexCapabilitySpec(params) {
|
|
@@ -1178,25 +95,25 @@ function readThinkingLevel(value) {
|
|
|
1178
95
|
return void 0;
|
|
1179
96
|
}
|
|
1180
97
|
function resolveCodexExecutionOptions(params) {
|
|
1181
|
-
const configuredWorkingDirectory =
|
|
98
|
+
const configuredWorkingDirectory = readString(params.pluginConfig.workingDirectory);
|
|
1182
99
|
const workspace = getWorkspacePath(
|
|
1183
100
|
configuredWorkingDirectory ?? params.config.agents.defaults.workspace
|
|
1184
101
|
);
|
|
1185
102
|
return {
|
|
1186
103
|
workingDirectory: workspace,
|
|
1187
|
-
skipGitRepoCheck:
|
|
104
|
+
skipGitRepoCheck: readBoolean(params.pluginConfig.skipGitRepoCheck) ?? true
|
|
1188
105
|
};
|
|
1189
106
|
}
|
|
1190
107
|
function resolveCodexCliConfig(params) {
|
|
1191
|
-
const explicitConfig =
|
|
1192
|
-
const modelProvider =
|
|
108
|
+
const explicitConfig = readRecord(params.pluginConfig.config);
|
|
109
|
+
const modelProvider = readString(params.modelProviderOverride) ?? resolveExternalModelProvider({
|
|
1193
110
|
explicitModelProvider: params.pluginConfig.modelProvider,
|
|
1194
111
|
providerName: params.providerName,
|
|
1195
112
|
providerDisplayName: params.providerDisplayName,
|
|
1196
113
|
pluginId: PLUGIN_ID
|
|
1197
114
|
});
|
|
1198
|
-
const preferredAuthMethod =
|
|
1199
|
-
const apiBase =
|
|
115
|
+
const preferredAuthMethod = readString(params.pluginConfig.preferredAuthMethod) ?? "apikey";
|
|
116
|
+
const apiBase = readString(params.pluginConfig.apiBase) ?? readString(params.apiBase);
|
|
1200
117
|
const config = {
|
|
1201
118
|
model_provider: modelProvider,
|
|
1202
119
|
preferred_auth_method: preferredAuthMethod
|
|
@@ -1217,9 +134,9 @@ function resolveCodexCliConfig(params) {
|
|
|
1217
134
|
};
|
|
1218
135
|
}
|
|
1219
136
|
function resolveCodexModel(params) {
|
|
1220
|
-
return
|
|
137
|
+
return readString(params.sessionMetadata.preferred_model) ?? readString(params.sessionMetadata.model) ?? readString(params.pluginConfig.model) ?? params.config.agents.defaults.model;
|
|
1221
138
|
}
|
|
1222
|
-
|
|
139
|
+
const plugin = {
|
|
1223
140
|
id: PLUGIN_ID,
|
|
1224
141
|
name: "NextClaw Codex NCP Runtime",
|
|
1225
142
|
description: "Registers NCP session type `codex` backed by OpenAI Codex SDK.",
|
|
@@ -1229,7 +146,7 @@ var plugin = {
|
|
|
1229
146
|
properties: {}
|
|
1230
147
|
},
|
|
1231
148
|
register(api) {
|
|
1232
|
-
const pluginConfig =
|
|
149
|
+
const pluginConfig = readRecord(api.pluginConfig) ?? {};
|
|
1233
150
|
const describeCodexSessionType = createDescribeCodexSessionType({
|
|
1234
151
|
config: api.config,
|
|
1235
152
|
pluginConfig
|
|
@@ -1263,8 +180,8 @@ var plugin = {
|
|
|
1263
180
|
providerLocalModel: resolvedProviderRuntime.providerLocalModel,
|
|
1264
181
|
resolvedModel: resolvedProviderRuntime.resolvedModel
|
|
1265
182
|
});
|
|
1266
|
-
const upstreamApiBase =
|
|
1267
|
-
const apiKey =
|
|
183
|
+
const upstreamApiBase = readString(pluginConfig.apiBase) ?? resolvedProviderRuntime.apiBase ?? void 0;
|
|
184
|
+
const apiKey = readString(pluginConfig.apiKey) ?? resolvedProviderRuntime.apiKey ?? void 0;
|
|
1268
185
|
if (!apiKey) {
|
|
1269
186
|
throw new Error(
|
|
1270
187
|
`[codex] missing apiKey. Set plugins.entries.${PLUGIN_ID}.config.apiKey or providers.*.apiKey for model "${userFacingModelRoute}".`
|
|
@@ -1279,7 +196,7 @@ var plugin = {
|
|
|
1279
196
|
let codexModelProviderOverride;
|
|
1280
197
|
const supportsResponsesApi = await resolveCodexResponsesApiSupport({
|
|
1281
198
|
capabilitySpec,
|
|
1282
|
-
wireApi:
|
|
199
|
+
wireApi: readString(resolvedProviderRuntime.provider?.wireApi),
|
|
1283
200
|
apiBase: upstreamApiBase,
|
|
1284
201
|
apiKey,
|
|
1285
202
|
extraHeaders: resolvedProviderRuntime.provider?.extraHeaders ?? null,
|
|
@@ -1306,15 +223,14 @@ var plugin = {
|
|
|
1306
223
|
config: nextConfig,
|
|
1307
224
|
pluginConfig
|
|
1308
225
|
});
|
|
1309
|
-
const accessMode = resolveCodexAccessMode(pluginConfig);
|
|
1310
226
|
const thinkingLevel = readThinkingLevel(runtimeParams.sessionMetadata.preferred_thinking) ?? readThinkingLevel(runtimeParams.sessionMetadata.thinking) ?? void 0;
|
|
1311
227
|
return new CodexSdkNcpAgentRuntime({
|
|
1312
228
|
sessionId: runtimeParams.sessionId,
|
|
1313
229
|
apiKey,
|
|
1314
230
|
apiBase: codexApiBase,
|
|
1315
231
|
model: resolvedProviderRuntime.providerLocalModel,
|
|
1316
|
-
threadId:
|
|
1317
|
-
codexPathOverride:
|
|
232
|
+
threadId: readString(runtimeParams.sessionMetadata.codex_thread_id) ?? null,
|
|
233
|
+
codexPathOverride: readString(pluginConfig.codexPathOverride),
|
|
1318
234
|
env: readStringRecord(pluginConfig.env),
|
|
1319
235
|
cliConfig: resolveCodexCliConfig({
|
|
1320
236
|
pluginConfig,
|
|
@@ -1329,15 +245,15 @@ var plugin = {
|
|
|
1329
245
|
inputBuilder: buildCodexInputBuilder(executionOptions.workingDirectory),
|
|
1330
246
|
threadOptions: {
|
|
1331
247
|
model,
|
|
1332
|
-
sandboxMode:
|
|
248
|
+
sandboxMode: readString(pluginConfig.sandboxMode),
|
|
1333
249
|
workingDirectory: executionOptions.workingDirectory,
|
|
1334
250
|
skipGitRepoCheck: executionOptions.skipGitRepoCheck,
|
|
1335
251
|
modelReasoningEffort: thinkingLevel,
|
|
1336
|
-
networkAccessEnabled:
|
|
1337
|
-
webSearchMode:
|
|
1338
|
-
webSearchEnabled:
|
|
1339
|
-
approvalPolicy:
|
|
1340
|
-
additionalDirectories:
|
|
252
|
+
networkAccessEnabled: readBoolean(pluginConfig.networkAccessEnabled),
|
|
253
|
+
webSearchMode: readString(pluginConfig.webSearchMode),
|
|
254
|
+
webSearchEnabled: readBoolean(pluginConfig.webSearchEnabled),
|
|
255
|
+
approvalPolicy: readString(pluginConfig.approvalPolicy),
|
|
256
|
+
additionalDirectories: readStringArray(pluginConfig.additionalDirectories)
|
|
1341
257
|
}
|
|
1342
258
|
});
|
|
1343
259
|
});
|