ai-sdk-provider-codex-cli 0.5.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -0
- package/dist/index.cjs +322 -38
- package/dist/index.d.cts +79 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +322 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -104,6 +104,48 @@ console.log(object);
|
|
|
104
104
|
- Safe defaults for non‑interactive automation (`on-failure`, `workspace-write`, `--skip-git-repo-check`)
|
|
105
105
|
- Fallback to `npx @openai/codex` when not on PATH (`allowNpx`)
|
|
106
106
|
- Usage tracking from experimental JSON event format
|
|
107
|
+
- **Image support** - Pass images to vision-capable models via `--image` flag
|
|
108
|
+
|
|
109
|
+
### Image Support
|
|
110
|
+
|
|
111
|
+
The provider supports multimodal (image) inputs for vision-capable models:
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
import { generateText } from 'ai';
|
|
115
|
+
import { codexCli } from 'ai-sdk-provider-codex-cli';
|
|
116
|
+
import { readFileSync } from 'fs';
|
|
117
|
+
|
|
118
|
+
const model = codexCli('gpt-5.1-codex', { allowNpx: true, skipGitRepoCheck: true });
|
|
119
|
+
const imageBuffer = readFileSync('./screenshot.png');
|
|
120
|
+
|
|
121
|
+
const { text } = await generateText({
|
|
122
|
+
model,
|
|
123
|
+
messages: [
|
|
124
|
+
{
|
|
125
|
+
role: 'user',
|
|
126
|
+
content: [
|
|
127
|
+
{ type: 'text', text: 'What do you see in this image?' },
|
|
128
|
+
{ type: 'image', image: imageBuffer, mimeType: 'image/png' },
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
});
|
|
133
|
+
console.log(text);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Supported image formats:**
|
|
137
|
+
|
|
138
|
+
- Base64 data URL (`data:image/png;base64,...`)
|
|
139
|
+
- Base64 string (without data URL prefix)
|
|
140
|
+
- `Buffer` / `Uint8Array` / `ArrayBuffer`
|
|
141
|
+
|
|
142
|
+
**Not supported:**
|
|
143
|
+
|
|
144
|
+
- HTTP/HTTPS URLs (images must be provided as binary data)
|
|
145
|
+
|
|
146
|
+
Images are written to temporary files and passed to Codex CLI via the `--image` flag. Temp files are automatically cleaned up after the request completes.
|
|
147
|
+
|
|
148
|
+
See [examples/image-support.mjs](examples/image-support.mjs) for a complete working example.
|
|
107
149
|
|
|
108
150
|
### Tool Streaming (v0.3.0+)
|
|
109
151
|
|
|
@@ -223,6 +265,7 @@ When OpenAI adds streaming support, this provider will be updated to handle thos
|
|
|
223
265
|
|
|
224
266
|
- `allowNpx`: If true, falls back to `npx -y @openai/codex` when Codex is not on PATH
|
|
225
267
|
- `cwd`: Working directory for Codex
|
|
268
|
+
- `addDirs`: Extra directories Codex may read/write (repeats `--add-dir`)
|
|
226
269
|
- Autonomy/sandbox:
|
|
227
270
|
- `fullAuto` (equivalent to `--full-auto`)
|
|
228
271
|
- `dangerouslyBypassApprovalsAndSandbox` (bypass approvals and sandbox; dangerous)
|
|
@@ -246,6 +289,7 @@ import { codexCli } from 'ai-sdk-provider-codex-cli';
|
|
|
246
289
|
const model = codexCli('gpt-5.1-codex', {
|
|
247
290
|
allowNpx: true,
|
|
248
291
|
skipGitRepoCheck: true,
|
|
292
|
+
addDirs: ['../shared'],
|
|
249
293
|
|
|
250
294
|
// Reasoning & verbosity
|
|
251
295
|
reasoningEffort: 'medium', // minimal | low | medium | high | xhigh (xhigh only on gpt-5.1-codex-max)
|
|
@@ -259,6 +303,23 @@ const model = codexCli('gpt-5.1-codex', {
|
|
|
259
303
|
oss: false, // adds --oss when true
|
|
260
304
|
webSearch: true, // maps to -c tools.web_search=true
|
|
261
305
|
|
|
306
|
+
// MCP servers (stdio + HTTP/RMCP)
|
|
307
|
+
rmcpClient: true, // enables HTTP-based MCP clients (features.rmcp_client=true)
|
|
308
|
+
mcpServers: {
|
|
309
|
+
local: {
|
|
310
|
+
transport: 'stdio',
|
|
311
|
+
command: 'node',
|
|
312
|
+
args: ['tools/mcp.js'],
|
|
313
|
+
env: { API_KEY: process.env.MCP_API_KEY ?? '' },
|
|
314
|
+
},
|
|
315
|
+
docs: {
|
|
316
|
+
transport: 'http',
|
|
317
|
+
url: 'https://mcp.my-org.com',
|
|
318
|
+
bearerTokenEnvVar: 'MCP_BEARER',
|
|
319
|
+
httpHeaders: { 'x-tenant': 'acme' },
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
|
|
262
323
|
// Generic overrides (maps to -c key=value)
|
|
263
324
|
configOverrides: {
|
|
264
325
|
experimental_resume: '/tmp/session.jsonl',
|
|
@@ -269,6 +330,7 @@ const model = codexCli('gpt-5.1-codex', {
|
|
|
269
330
|
|
|
270
331
|
Nested override objects are flattened to dotted keys (e.g., the example above emits
|
|
271
332
|
`-c sandbox_workspace_write.network_access=true`). Arrays are serialized to JSON strings.
|
|
333
|
+
MCP server env/header objects flatten the same way (e.g., `mcp_servers.docs.http_headers.x-tenant=acme`).
|
|
272
334
|
|
|
273
335
|
### Per-call overrides via `providerOptions` (v0.4.0+)
|
|
274
336
|
|
|
@@ -293,6 +355,14 @@ const response = await generateText({
|
|
|
293
355
|
reasoningEffort: 'high',
|
|
294
356
|
reasoningSummary: 'detailed',
|
|
295
357
|
textVerbosity: 'high', // AI SDK naming; maps to model_verbosity
|
|
358
|
+
rmcpClient: true,
|
|
359
|
+
mcpServers: {
|
|
360
|
+
scratch: {
|
|
361
|
+
transport: 'stdio',
|
|
362
|
+
command: 'pnpm',
|
|
363
|
+
args: ['mcp', 'serve'],
|
|
364
|
+
},
|
|
365
|
+
},
|
|
296
366
|
configOverrides: {
|
|
297
367
|
experimental_resume: '/tmp/resume.jsonl',
|
|
298
368
|
},
|
package/dist/index.cjs
CHANGED
|
@@ -68,9 +68,37 @@ var loggerFunctionSchema = zod.z.object({
|
|
|
68
68
|
message: "error must be a function"
|
|
69
69
|
})
|
|
70
70
|
});
|
|
71
|
+
var mcpServerBaseSchema = zod.z.object({
|
|
72
|
+
enabled: zod.z.boolean().optional(),
|
|
73
|
+
startupTimeoutSec: zod.z.number().int().positive().optional(),
|
|
74
|
+
toolTimeoutSec: zod.z.number().int().positive().optional(),
|
|
75
|
+
enabledTools: zod.z.array(zod.z.string()).optional(),
|
|
76
|
+
disabledTools: zod.z.array(zod.z.string()).optional()
|
|
77
|
+
});
|
|
78
|
+
var mcpServerStdioSchema = mcpServerBaseSchema.extend({
|
|
79
|
+
transport: zod.z.literal("stdio"),
|
|
80
|
+
command: zod.z.string().min(1),
|
|
81
|
+
args: zod.z.array(zod.z.string()).optional(),
|
|
82
|
+
env: zod.z.record(zod.z.string(), zod.z.string()).optional(),
|
|
83
|
+
cwd: zod.z.string().optional()
|
|
84
|
+
});
|
|
85
|
+
var mcpServerHttpSchema = mcpServerBaseSchema.extend({
|
|
86
|
+
transport: zod.z.literal("http"),
|
|
87
|
+
url: zod.z.string().min(1),
|
|
88
|
+
bearerToken: zod.z.string().optional(),
|
|
89
|
+
bearerTokenEnvVar: zod.z.string().optional(),
|
|
90
|
+
httpHeaders: zod.z.record(zod.z.string(), zod.z.string()).optional(),
|
|
91
|
+
envHttpHeaders: zod.z.record(zod.z.string(), zod.z.string()).optional()
|
|
92
|
+
});
|
|
93
|
+
var mcpServerSchema = zod.z.discriminatedUnion("transport", [
|
|
94
|
+
mcpServerStdioSchema,
|
|
95
|
+
mcpServerHttpSchema
|
|
96
|
+
]);
|
|
97
|
+
var mcpServersSchema = zod.z.record(zod.z.string(), mcpServerSchema);
|
|
71
98
|
var settingsSchema = zod.z.object({
|
|
72
99
|
codexPath: zod.z.string().optional(),
|
|
73
100
|
cwd: zod.z.string().optional(),
|
|
101
|
+
addDirs: zod.z.array(zod.z.string().min(1)).optional(),
|
|
74
102
|
approvalMode: zod.z.enum(["untrusted", "on-failure", "on-request", "never"]).optional(),
|
|
75
103
|
sandboxMode: zod.z.enum(["read-only", "workspace-write", "danger-full-access"]).optional(),
|
|
76
104
|
fullAuto: zod.z.boolean().optional(),
|
|
@@ -78,6 +106,7 @@ var settingsSchema = zod.z.object({
|
|
|
78
106
|
skipGitRepoCheck: zod.z.boolean().optional(),
|
|
79
107
|
color: zod.z.enum(["always", "never", "auto"]).optional(),
|
|
80
108
|
allowNpx: zod.z.boolean().optional(),
|
|
109
|
+
outputLastMessageFile: zod.z.string().optional(),
|
|
81
110
|
env: zod.z.record(zod.z.string(), zod.z.string()).optional(),
|
|
82
111
|
verbose: zod.z.boolean().optional(),
|
|
83
112
|
logger: zod.z.union([zod.z.literal(false), loggerFunctionSchema]).optional(),
|
|
@@ -92,6 +121,8 @@ var settingsSchema = zod.z.object({
|
|
|
92
121
|
profile: zod.z.string().optional(),
|
|
93
122
|
oss: zod.z.boolean().optional(),
|
|
94
123
|
webSearch: zod.z.boolean().optional(),
|
|
124
|
+
mcpServers: mcpServersSchema.optional(),
|
|
125
|
+
rmcpClient: zod.z.boolean().optional(),
|
|
95
126
|
// NEW: Generic overrides
|
|
96
127
|
configOverrides: zod.z.record(
|
|
97
128
|
zod.z.string(),
|
|
@@ -136,6 +167,94 @@ function validateModelId(modelId) {
|
|
|
136
167
|
if (!modelId || modelId.trim() === "") return "Model ID cannot be empty";
|
|
137
168
|
return void 0;
|
|
138
169
|
}
|
|
170
|
+
function extractImageData(part) {
|
|
171
|
+
if (typeof part !== "object" || part === null) return null;
|
|
172
|
+
const p = part;
|
|
173
|
+
const mimeType = p.mimeType || "image/png";
|
|
174
|
+
if (typeof p.image === "string") {
|
|
175
|
+
return extractFromString(p.image, mimeType);
|
|
176
|
+
}
|
|
177
|
+
if (p.image instanceof URL) {
|
|
178
|
+
if (p.image.protocol === "data:") {
|
|
179
|
+
const dataUrlStr = p.image.toString();
|
|
180
|
+
const match = dataUrlStr.match(/^data:([^;,]+)/);
|
|
181
|
+
const extractedMimeType = match?.[1] || mimeType;
|
|
182
|
+
return { data: dataUrlStr, mimeType: extractedMimeType };
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
if (Buffer.isBuffer(p.image)) {
|
|
187
|
+
const base64 = p.image.toString("base64");
|
|
188
|
+
return { data: `data:${mimeType};base64,${base64}`, mimeType };
|
|
189
|
+
}
|
|
190
|
+
if (p.image instanceof ArrayBuffer || p.image instanceof Uint8Array) {
|
|
191
|
+
const buffer = Buffer.from(p.image);
|
|
192
|
+
const base64 = buffer.toString("base64");
|
|
193
|
+
return { data: `data:${mimeType};base64,${base64}`, mimeType };
|
|
194
|
+
}
|
|
195
|
+
if (typeof p.data === "string") {
|
|
196
|
+
return extractFromString(p.data, mimeType);
|
|
197
|
+
}
|
|
198
|
+
if (typeof p.url === "string") {
|
|
199
|
+
return extractFromString(p.url, mimeType);
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
function extractFromString(value, fallbackMimeType) {
|
|
204
|
+
const trimmed = value.trim();
|
|
205
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
if (trimmed.startsWith("data:")) {
|
|
209
|
+
if (!trimmed.includes(";base64,")) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
const match = trimmed.match(/^data:([^;,]+)/);
|
|
213
|
+
const mimeType = match?.[1] || fallbackMimeType;
|
|
214
|
+
return { data: trimmed, mimeType };
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
data: `data:${fallbackMimeType};base64,${trimmed}`,
|
|
218
|
+
mimeType: fallbackMimeType
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function writeImageToTempFile(imageData) {
|
|
222
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "codex-img-"));
|
|
223
|
+
const ext = getExtensionFromMimeType(imageData.mimeType);
|
|
224
|
+
const filePath = path.join(dir, `image.${ext}`);
|
|
225
|
+
const base64Match = imageData.data.match(/^data:[^;]+;base64,(.+)$/);
|
|
226
|
+
if (!base64Match) {
|
|
227
|
+
throw new Error("Invalid data URL format: expected data:[type];base64,[data]");
|
|
228
|
+
}
|
|
229
|
+
const buffer = Buffer.from(base64Match[1], "base64");
|
|
230
|
+
fs.writeFileSync(filePath, buffer);
|
|
231
|
+
return filePath;
|
|
232
|
+
}
|
|
233
|
+
function cleanupTempImages(paths) {
|
|
234
|
+
for (const filePath of paths) {
|
|
235
|
+
try {
|
|
236
|
+
fs.rmSync(filePath, { force: true });
|
|
237
|
+
const dir = filePath.replace(/[/\\][^/\\]+$/, "");
|
|
238
|
+
if (dir.includes("codex-img-")) {
|
|
239
|
+
fs.rmSync(dir, { force: true, recursive: true });
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function getExtensionFromMimeType(mimeType) {
|
|
246
|
+
if (!mimeType) return "png";
|
|
247
|
+
const mapping = {
|
|
248
|
+
"image/png": "png",
|
|
249
|
+
"image/jpeg": "jpg",
|
|
250
|
+
"image/jpg": "jpg",
|
|
251
|
+
"image/gif": "gif",
|
|
252
|
+
"image/webp": "webp",
|
|
253
|
+
"image/bmp": "bmp",
|
|
254
|
+
"image/svg+xml": "svg"
|
|
255
|
+
};
|
|
256
|
+
return mapping[mimeType.toLowerCase()] || mimeType.split("/")[1] || "png";
|
|
257
|
+
}
|
|
139
258
|
|
|
140
259
|
// src/message-mapper.ts
|
|
141
260
|
function isTextPart(p) {
|
|
@@ -156,6 +275,7 @@ function isToolItem(p) {
|
|
|
156
275
|
function mapMessagesToPrompt(prompt) {
|
|
157
276
|
const warnings = [];
|
|
158
277
|
const parts = [];
|
|
278
|
+
const images = [];
|
|
159
279
|
let systemText;
|
|
160
280
|
for (const msg of prompt) {
|
|
161
281
|
if (msg.role === "system") {
|
|
@@ -168,8 +288,14 @@ function mapMessagesToPrompt(prompt) {
|
|
|
168
288
|
} else if (Array.isArray(msg.content)) {
|
|
169
289
|
const text = msg.content.filter(isTextPart).map((p) => p.text).join("\n");
|
|
170
290
|
if (text) parts.push(`Human: ${text}`);
|
|
171
|
-
const
|
|
172
|
-
|
|
291
|
+
for (const part of msg.content.filter(isImagePart)) {
|
|
292
|
+
const imageData = extractImageData(part);
|
|
293
|
+
if (imageData) {
|
|
294
|
+
images.push(imageData);
|
|
295
|
+
} else {
|
|
296
|
+
warnings.push("Unsupported image format in message (HTTP URLs not supported)");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
173
299
|
}
|
|
174
300
|
continue;
|
|
175
301
|
}
|
|
@@ -196,7 +322,7 @@ function mapMessagesToPrompt(prompt) {
|
|
|
196
322
|
let promptText = "";
|
|
197
323
|
if (systemText) promptText += systemText + "\n\n";
|
|
198
324
|
promptText += parts.join("\n\n");
|
|
199
|
-
return { promptText, ...warnings.length ? { warnings } : {} };
|
|
325
|
+
return { promptText, images, ...warnings.length ? { warnings } : {} };
|
|
200
326
|
}
|
|
201
327
|
function createAPICallError({
|
|
202
328
|
message,
|
|
@@ -235,6 +361,7 @@ var codexCliProviderOptionsSchema = zod.z.object({
|
|
|
235
361
|
reasoningSummary: zod.z.enum(["auto", "detailed"]).optional(),
|
|
236
362
|
reasoningSummaryFormat: zod.z.enum(["none", "experimental"]).optional(),
|
|
237
363
|
textVerbosity: zod.z.enum(["low", "medium", "high"]).optional(),
|
|
364
|
+
addDirs: zod.z.array(zod.z.string().min(1)).optional(),
|
|
238
365
|
configOverrides: zod.z.record(
|
|
239
366
|
zod.z.string(),
|
|
240
367
|
zod.z.union([
|
|
@@ -244,7 +371,9 @@ var codexCliProviderOptionsSchema = zod.z.object({
|
|
|
244
371
|
zod.z.object({}).passthrough(),
|
|
245
372
|
zod.z.array(zod.z.any())
|
|
246
373
|
])
|
|
247
|
-
).optional()
|
|
374
|
+
).optional(),
|
|
375
|
+
mcpServers: mcpServersSchema.optional(),
|
|
376
|
+
rmcpClient: zod.z.boolean().optional()
|
|
248
377
|
}).strict();
|
|
249
378
|
function resolveCodexPath(explicitPath, allowNpx) {
|
|
250
379
|
if (explicitPath) return { cmd: "node", args: [explicitPath] };
|
|
@@ -286,14 +415,79 @@ var CodexCliLanguageModel = class {
|
|
|
286
415
|
...this.settings.configOverrides ?? {},
|
|
287
416
|
...providerOptions.configOverrides ?? {}
|
|
288
417
|
} : void 0;
|
|
418
|
+
const mergedAddDirs = providerOptions.addDirs || this.settings.addDirs ? [...this.settings.addDirs ?? [], ...providerOptions.addDirs ?? []] : void 0;
|
|
419
|
+
const mergedMcpServers = this.mergeMcpServers(
|
|
420
|
+
this.settings.mcpServers,
|
|
421
|
+
providerOptions.mcpServers
|
|
422
|
+
);
|
|
289
423
|
return {
|
|
290
424
|
...this.settings,
|
|
291
425
|
reasoningEffort: providerOptions.reasoningEffort ?? this.settings.reasoningEffort,
|
|
292
426
|
reasoningSummary: providerOptions.reasoningSummary ?? this.settings.reasoningSummary,
|
|
293
427
|
reasoningSummaryFormat: providerOptions.reasoningSummaryFormat ?? this.settings.reasoningSummaryFormat,
|
|
294
428
|
modelVerbosity: providerOptions.textVerbosity ?? this.settings.modelVerbosity,
|
|
295
|
-
configOverrides: mergedConfigOverrides
|
|
429
|
+
configOverrides: mergedConfigOverrides,
|
|
430
|
+
addDirs: mergedAddDirs,
|
|
431
|
+
mcpServers: mergedMcpServers,
|
|
432
|
+
rmcpClient: providerOptions.rmcpClient ?? this.settings.rmcpClient
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
mergeMcpServers(base, override) {
|
|
436
|
+
if (!base) return override;
|
|
437
|
+
if (!override) return base;
|
|
438
|
+
const merged = { ...base };
|
|
439
|
+
for (const [name, incoming] of Object.entries(override)) {
|
|
440
|
+
const existing = base[name];
|
|
441
|
+
merged[name] = this.mergeSingleMcpServer(existing, incoming);
|
|
442
|
+
}
|
|
443
|
+
return merged;
|
|
444
|
+
}
|
|
445
|
+
mergeSingleMcpServer(existing, incoming) {
|
|
446
|
+
if (!existing || existing.transport !== incoming.transport) {
|
|
447
|
+
return { ...incoming };
|
|
448
|
+
}
|
|
449
|
+
if (incoming.transport === "stdio") {
|
|
450
|
+
const baseStdio = existing;
|
|
451
|
+
const result2 = {
|
|
452
|
+
transport: "stdio",
|
|
453
|
+
command: incoming.command,
|
|
454
|
+
args: incoming.args ?? baseStdio.args,
|
|
455
|
+
env: this.mergeStringRecord(baseStdio.env, incoming.env),
|
|
456
|
+
cwd: incoming.cwd ?? baseStdio.cwd,
|
|
457
|
+
enabled: incoming.enabled ?? existing.enabled,
|
|
458
|
+
startupTimeoutSec: incoming.startupTimeoutSec ?? existing.startupTimeoutSec,
|
|
459
|
+
toolTimeoutSec: incoming.toolTimeoutSec ?? existing.toolTimeoutSec,
|
|
460
|
+
enabledTools: incoming.enabledTools ?? existing.enabledTools,
|
|
461
|
+
disabledTools: incoming.disabledTools ?? existing.disabledTools
|
|
462
|
+
};
|
|
463
|
+
return result2;
|
|
464
|
+
}
|
|
465
|
+
const baseHttp = existing;
|
|
466
|
+
const hasIncomingAuth = incoming.bearerToken !== void 0 || incoming.bearerTokenEnvVar !== void 0;
|
|
467
|
+
const bearerToken = hasIncomingAuth ? incoming.bearerToken : baseHttp.bearerToken;
|
|
468
|
+
const bearerTokenEnvVar = hasIncomingAuth ? incoming.bearerTokenEnvVar : baseHttp.bearerTokenEnvVar;
|
|
469
|
+
const result = {
|
|
470
|
+
transport: "http",
|
|
471
|
+
url: incoming.url,
|
|
472
|
+
bearerToken,
|
|
473
|
+
bearerTokenEnvVar,
|
|
474
|
+
httpHeaders: this.mergeStringRecord(baseHttp.httpHeaders, incoming.httpHeaders),
|
|
475
|
+
envHttpHeaders: this.mergeStringRecord(baseHttp.envHttpHeaders, incoming.envHttpHeaders),
|
|
476
|
+
enabled: incoming.enabled ?? existing.enabled,
|
|
477
|
+
startupTimeoutSec: incoming.startupTimeoutSec ?? existing.startupTimeoutSec,
|
|
478
|
+
toolTimeoutSec: incoming.toolTimeoutSec ?? existing.toolTimeoutSec,
|
|
479
|
+
enabledTools: incoming.enabledTools ?? existing.enabledTools,
|
|
480
|
+
disabledTools: incoming.disabledTools ?? existing.disabledTools
|
|
296
481
|
};
|
|
482
|
+
return result;
|
|
483
|
+
}
|
|
484
|
+
mergeStringRecord(base, override) {
|
|
485
|
+
if (override !== void 0) {
|
|
486
|
+
if (Object.keys(override).length === 0) return {};
|
|
487
|
+
return { ...base ?? {}, ...override };
|
|
488
|
+
}
|
|
489
|
+
if (base) return { ...base };
|
|
490
|
+
return void 0;
|
|
297
491
|
}
|
|
298
492
|
// Codex JSONL items use `type` for the item discriminator, but some
|
|
299
493
|
// earlier fixtures (and defensive parsing) might still surface `item_type`.
|
|
@@ -305,7 +499,7 @@ var CodexCliLanguageModel = class {
|
|
|
305
499
|
const current = typeof data.type === "string" ? data.type : void 0;
|
|
306
500
|
return legacy ?? current;
|
|
307
501
|
}
|
|
308
|
-
buildArgs(promptText, responseFormat, settings = this.settings) {
|
|
502
|
+
buildArgs(promptText, images = [], responseFormat, settings = this.settings) {
|
|
309
503
|
const base = resolveCodexPath(settings.codexPath, settings.allowNpx);
|
|
310
504
|
const args = [...base.args, "exec", "--experimental-json"];
|
|
311
505
|
if (settings.fullAuto) {
|
|
@@ -345,12 +539,20 @@ var CodexCliLanguageModel = class {
|
|
|
345
539
|
if (settings.webSearch) {
|
|
346
540
|
args.push("-c", "tools.web_search=true");
|
|
347
541
|
}
|
|
542
|
+
this.applyMcpSettings(args, settings);
|
|
348
543
|
if (settings.color) {
|
|
349
544
|
args.push("--color", settings.color);
|
|
350
545
|
}
|
|
351
546
|
if (this.modelId) {
|
|
352
547
|
args.push("-m", this.modelId);
|
|
353
548
|
}
|
|
549
|
+
if (settings.addDirs?.length) {
|
|
550
|
+
for (const dir of settings.addDirs) {
|
|
551
|
+
if (typeof dir === "string" && dir.trim().length > 0) {
|
|
552
|
+
args.push("--add-dir", dir);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
354
556
|
if (settings.configOverrides) {
|
|
355
557
|
for (const [key, value] of Object.entries(settings.configOverrides)) {
|
|
356
558
|
this.addConfigOverride(args, key, value);
|
|
@@ -372,6 +574,16 @@ var CodexCliLanguageModel = class {
|
|
|
372
574
|
args.push("--output-schema", schemaPath);
|
|
373
575
|
}
|
|
374
576
|
}
|
|
577
|
+
const tempImagePaths = [];
|
|
578
|
+
for (const img of images) {
|
|
579
|
+
try {
|
|
580
|
+
const tempPath = writeImageToTempFile(img);
|
|
581
|
+
tempImagePaths.push(tempPath);
|
|
582
|
+
args.push("--image", tempPath);
|
|
583
|
+
} catch (e) {
|
|
584
|
+
this.logger.warn(`[codex-cli] Failed to write image to temp file: ${String(e)}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
375
587
|
args.push(promptText);
|
|
376
588
|
const env = {
|
|
377
589
|
...process.env,
|
|
@@ -379,16 +591,74 @@ var CodexCliLanguageModel = class {
|
|
|
379
591
|
RUST_LOG: process.env.RUST_LOG || "error"
|
|
380
592
|
};
|
|
381
593
|
let lastMessagePath = settings.outputLastMessageFile;
|
|
594
|
+
let lastMessageIsTemp = false;
|
|
382
595
|
if (!lastMessagePath) {
|
|
383
596
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "codex-cli-"));
|
|
384
597
|
lastMessagePath = path.join(dir, "last-message.txt");
|
|
598
|
+
lastMessageIsTemp = true;
|
|
385
599
|
}
|
|
386
600
|
args.push("--output-last-message", lastMessagePath);
|
|
387
|
-
return {
|
|
601
|
+
return {
|
|
602
|
+
cmd: base.cmd,
|
|
603
|
+
args,
|
|
604
|
+
env,
|
|
605
|
+
cwd: settings.cwd,
|
|
606
|
+
lastMessagePath,
|
|
607
|
+
lastMessageIsTemp,
|
|
608
|
+
schemaPath,
|
|
609
|
+
tempImagePaths: tempImagePaths.length > 0 ? tempImagePaths : void 0
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
applyMcpSettings(args, settings) {
|
|
613
|
+
if (settings.rmcpClient) {
|
|
614
|
+
this.addConfigOverride(args, "features.rmcp_client", true);
|
|
615
|
+
}
|
|
616
|
+
if (!settings.mcpServers) return;
|
|
617
|
+
for (const [rawName, server] of Object.entries(settings.mcpServers)) {
|
|
618
|
+
const name = rawName.trim();
|
|
619
|
+
if (!name) continue;
|
|
620
|
+
const prefix = `mcp_servers.${name}`;
|
|
621
|
+
if (server.enabled !== void 0) {
|
|
622
|
+
this.addConfigOverride(args, `${prefix}.enabled`, server.enabled);
|
|
623
|
+
}
|
|
624
|
+
if (server.startupTimeoutSec !== void 0) {
|
|
625
|
+
this.addConfigOverride(args, `${prefix}.startup_timeout_sec`, server.startupTimeoutSec);
|
|
626
|
+
}
|
|
627
|
+
if (server.toolTimeoutSec !== void 0) {
|
|
628
|
+
this.addConfigOverride(args, `${prefix}.tool_timeout_sec`, server.toolTimeoutSec);
|
|
629
|
+
}
|
|
630
|
+
if (server.enabledTools !== void 0) {
|
|
631
|
+
this.addConfigOverride(args, `${prefix}.enabled_tools`, server.enabledTools);
|
|
632
|
+
}
|
|
633
|
+
if (server.disabledTools !== void 0) {
|
|
634
|
+
this.addConfigOverride(args, `${prefix}.disabled_tools`, server.disabledTools);
|
|
635
|
+
}
|
|
636
|
+
if (server.transport === "stdio") {
|
|
637
|
+
this.addConfigOverride(args, `${prefix}.command`, server.command);
|
|
638
|
+
if (server.args !== void 0) this.addConfigOverride(args, `${prefix}.args`, server.args);
|
|
639
|
+
if (server.env !== void 0) this.addConfigOverride(args, `${prefix}.env`, server.env);
|
|
640
|
+
if (server.cwd) this.addConfigOverride(args, `${prefix}.cwd`, server.cwd);
|
|
641
|
+
} else {
|
|
642
|
+
this.addConfigOverride(args, `${prefix}.url`, server.url);
|
|
643
|
+
if (server.bearerToken !== void 0)
|
|
644
|
+
this.addConfigOverride(args, `${prefix}.bearer_token`, server.bearerToken);
|
|
645
|
+
if (server.bearerTokenEnvVar)
|
|
646
|
+
this.addConfigOverride(args, `${prefix}.bearer_token_env_var`, server.bearerTokenEnvVar);
|
|
647
|
+
if (server.httpHeaders !== void 0)
|
|
648
|
+
this.addConfigOverride(args, `${prefix}.http_headers`, server.httpHeaders);
|
|
649
|
+
if (server.envHttpHeaders !== void 0)
|
|
650
|
+
this.addConfigOverride(args, `${prefix}.env_http_headers`, server.envHttpHeaders);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
388
653
|
}
|
|
389
654
|
addConfigOverride(args, key, value) {
|
|
390
655
|
if (this.isPlainObject(value)) {
|
|
391
|
-
|
|
656
|
+
const entries = Object.entries(value);
|
|
657
|
+
if (entries.length === 0) {
|
|
658
|
+
args.push("-c", `${key}={}`);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
for (const [childKey, childValue] of entries) {
|
|
392
662
|
this.addConfigOverride(
|
|
393
663
|
args,
|
|
394
664
|
`${key}.${childKey}`,
|
|
@@ -666,14 +936,14 @@ var CodexCliLanguageModel = class {
|
|
|
666
936
|
}
|
|
667
937
|
async doGenerate(options) {
|
|
668
938
|
this.logger.debug(`[codex-cli] Starting doGenerate request with model: ${this.modelId}`);
|
|
669
|
-
const { promptText, warnings: mappingWarnings } = mapMessagesToPrompt(options.prompt);
|
|
939
|
+
const { promptText, images, warnings: mappingWarnings } = mapMessagesToPrompt(options.prompt);
|
|
670
940
|
const promptExcerpt = promptText.slice(0, 200);
|
|
671
941
|
const warnings = [
|
|
672
942
|
...this.mapWarnings(options),
|
|
673
943
|
...mappingWarnings?.map((m) => ({ type: "other", message: m })) || []
|
|
674
944
|
];
|
|
675
945
|
this.logger.debug(
|
|
676
|
-
`[codex-cli] Converted ${options.prompt.length} messages, response format: ${options.responseFormat?.type ?? "none"}`
|
|
946
|
+
`[codex-cli] Converted ${options.prompt.length} messages (${images.length} images), response format: ${options.responseFormat?.type ?? "none"}`
|
|
677
947
|
);
|
|
678
948
|
const providerOptions = await providerUtils.parseProviderOptions({
|
|
679
949
|
provider: this.provider,
|
|
@@ -682,11 +952,7 @@ var CodexCliLanguageModel = class {
|
|
|
682
952
|
});
|
|
683
953
|
const effectiveSettings = this.mergeSettings(providerOptions);
|
|
684
954
|
const responseFormat = options.responseFormat?.type === "json" ? { type: "json", schema: options.responseFormat.schema } : void 0;
|
|
685
|
-
const { cmd, args, env, cwd, lastMessagePath, schemaPath } = this.buildArgs(
|
|
686
|
-
promptText,
|
|
687
|
-
responseFormat,
|
|
688
|
-
effectiveSettings
|
|
689
|
-
);
|
|
955
|
+
const { cmd, args, env, cwd, lastMessagePath, lastMessageIsTemp, schemaPath, tempImagePaths } = this.buildArgs(promptText, images, responseFormat, effectiveSettings);
|
|
690
956
|
this.logger.debug(
|
|
691
957
|
`[codex-cli] Executing Codex CLI: ${cmd} with ${args.length} arguments, cwd: ${cwd ?? "default"}`
|
|
692
958
|
);
|
|
@@ -699,6 +965,16 @@ var CodexCliLanguageModel = class {
|
|
|
699
965
|
if (options.abortSignal) {
|
|
700
966
|
if (options.abortSignal.aborted) {
|
|
701
967
|
child.kill("SIGTERM");
|
|
968
|
+
if (schemaPath) {
|
|
969
|
+
try {
|
|
970
|
+
const schemaDir = path.dirname(schemaPath);
|
|
971
|
+
fs.rmSync(schemaDir, { recursive: true, force: true });
|
|
972
|
+
} catch {
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
if (tempImagePaths?.length) {
|
|
976
|
+
cleanupTempImages(tempImagePaths);
|
|
977
|
+
}
|
|
702
978
|
throw options.abortSignal.reason ?? new Error("Request aborted");
|
|
703
979
|
}
|
|
704
980
|
onAbort = () => child.kill("SIGTERM");
|
|
@@ -793,6 +1069,9 @@ var CodexCliLanguageModel = class {
|
|
|
793
1069
|
} catch {
|
|
794
1070
|
}
|
|
795
1071
|
}
|
|
1072
|
+
if (tempImagePaths?.length) {
|
|
1073
|
+
cleanupTempImages(tempImagePaths);
|
|
1074
|
+
}
|
|
796
1075
|
}
|
|
797
1076
|
if (!text && lastMessagePath) {
|
|
798
1077
|
try {
|
|
@@ -802,9 +1081,11 @@ var CodexCliLanguageModel = class {
|
|
|
802
1081
|
}
|
|
803
1082
|
} catch {
|
|
804
1083
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1084
|
+
if (lastMessageIsTemp) {
|
|
1085
|
+
try {
|
|
1086
|
+
fs.rmSync(lastMessagePath, { force: true });
|
|
1087
|
+
} catch {
|
|
1088
|
+
}
|
|
808
1089
|
}
|
|
809
1090
|
}
|
|
810
1091
|
const content = [{ type: "text", text }];
|
|
@@ -822,14 +1103,14 @@ var CodexCliLanguageModel = class {
|
|
|
822
1103
|
}
|
|
823
1104
|
async doStream(options) {
|
|
824
1105
|
this.logger.debug(`[codex-cli] Starting doStream request with model: ${this.modelId}`);
|
|
825
|
-
const { promptText, warnings: mappingWarnings } = mapMessagesToPrompt(options.prompt);
|
|
1106
|
+
const { promptText, images, warnings: mappingWarnings } = mapMessagesToPrompt(options.prompt);
|
|
826
1107
|
const promptExcerpt = promptText.slice(0, 200);
|
|
827
1108
|
const warnings = [
|
|
828
1109
|
...this.mapWarnings(options),
|
|
829
1110
|
...mappingWarnings?.map((m) => ({ type: "other", message: m })) || []
|
|
830
1111
|
];
|
|
831
1112
|
this.logger.debug(
|
|
832
|
-
`[codex-cli] Converted ${options.prompt.length} messages for streaming, response format: ${options.responseFormat?.type ?? "none"}`
|
|
1113
|
+
`[codex-cli] Converted ${options.prompt.length} messages (${images.length} images) for streaming, response format: ${options.responseFormat?.type ?? "none"}`
|
|
833
1114
|
);
|
|
834
1115
|
const providerOptions = await providerUtils.parseProviderOptions({
|
|
835
1116
|
provider: this.provider,
|
|
@@ -838,11 +1119,7 @@ var CodexCliLanguageModel = class {
|
|
|
838
1119
|
});
|
|
839
1120
|
const effectiveSettings = this.mergeSettings(providerOptions);
|
|
840
1121
|
const responseFormat = options.responseFormat?.type === "json" ? { type: "json", schema: options.responseFormat.schema } : void 0;
|
|
841
|
-
const { cmd, args, env, cwd, lastMessagePath, schemaPath } = this.buildArgs(
|
|
842
|
-
promptText,
|
|
843
|
-
responseFormat,
|
|
844
|
-
effectiveSettings
|
|
845
|
-
);
|
|
1122
|
+
const { cmd, args, env, cwd, lastMessagePath, lastMessageIsTemp, schemaPath, tempImagePaths } = this.buildArgs(promptText, images, responseFormat, effectiveSettings);
|
|
846
1123
|
this.logger.debug(
|
|
847
1124
|
`[codex-cli] Executing Codex CLI for streaming: ${cmd} with ${args.length} arguments`
|
|
848
1125
|
);
|
|
@@ -857,6 +1134,18 @@ var CodexCliLanguageModel = class {
|
|
|
857
1134
|
let responseMetadataSent = false;
|
|
858
1135
|
let lastUsage;
|
|
859
1136
|
let turnFailureMessage;
|
|
1137
|
+
const cleanupTempFiles = () => {
|
|
1138
|
+
if (schemaPath) {
|
|
1139
|
+
try {
|
|
1140
|
+
const schemaDir = path.dirname(schemaPath);
|
|
1141
|
+
fs.rmSync(schemaDir, { recursive: true, force: true });
|
|
1142
|
+
} catch {
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
if (tempImagePaths?.length) {
|
|
1146
|
+
cleanupTempImages(tempImagePaths);
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
860
1149
|
const sendMetadata = (meta = {}) => {
|
|
861
1150
|
controller.enqueue({
|
|
862
1151
|
type: "response-metadata",
|
|
@@ -930,6 +1219,7 @@ var CodexCliLanguageModel = class {
|
|
|
930
1219
|
if (options.abortSignal) {
|
|
931
1220
|
if (options.abortSignal.aborted) {
|
|
932
1221
|
child.kill("SIGTERM");
|
|
1222
|
+
cleanupTempFiles();
|
|
933
1223
|
controller.error(options.abortSignal.reason ?? new Error("Request aborted"));
|
|
934
1224
|
return;
|
|
935
1225
|
}
|
|
@@ -969,9 +1259,11 @@ var CodexCliLanguageModel = class {
|
|
|
969
1259
|
if (fileText) finalText = fileText.trim();
|
|
970
1260
|
} catch {
|
|
971
1261
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1262
|
+
if (lastMessageIsTemp) {
|
|
1263
|
+
try {
|
|
1264
|
+
fs.rmSync(lastMessagePath, { force: true });
|
|
1265
|
+
} catch {
|
|
1266
|
+
}
|
|
975
1267
|
}
|
|
976
1268
|
}
|
|
977
1269
|
if (finalText) {
|
|
@@ -1047,23 +1339,15 @@ var CodexCliLanguageModel = class {
|
|
|
1047
1339
|
}
|
|
1048
1340
|
}
|
|
1049
1341
|
});
|
|
1050
|
-
const cleanupSchema = () => {
|
|
1051
|
-
if (!schemaPath) return;
|
|
1052
|
-
try {
|
|
1053
|
-
const schemaDir = path.dirname(schemaPath);
|
|
1054
|
-
fs.rmSync(schemaDir, { recursive: true, force: true });
|
|
1055
|
-
} catch {
|
|
1056
|
-
}
|
|
1057
|
-
};
|
|
1058
1342
|
child.on("error", (e) => {
|
|
1059
1343
|
this.logger.error(`[codex-cli] Stream spawn error: ${String(e)}`);
|
|
1060
1344
|
if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort);
|
|
1061
|
-
|
|
1345
|
+
cleanupTempFiles();
|
|
1062
1346
|
controller.error(this.handleSpawnError(e, promptExcerpt));
|
|
1063
1347
|
});
|
|
1064
1348
|
child.on("close", (code) => {
|
|
1065
1349
|
if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort);
|
|
1066
|
-
|
|
1350
|
+
cleanupTempFiles();
|
|
1067
1351
|
setImmediate(() => finishStream(code));
|
|
1068
1352
|
});
|
|
1069
1353
|
},
|