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 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 images = msg.content.filter(isImagePart);
172
- if (images.length) warnings.push("Image inputs ignored by Codex CLI integration.");
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 { cmd: base.cmd, args, env, cwd: settings.cwd, lastMessagePath, schemaPath };
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
- for (const [childKey, childValue] of Object.entries(value)) {
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
- try {
806
- fs.rmSync(lastMessagePath, { force: true });
807
- } catch {
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
- try {
973
- fs.rmSync(lastMessagePath, { force: true });
974
- } catch {
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
- cleanupSchema();
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
- cleanupSchema();
1350
+ cleanupTempFiles();
1067
1351
  setImmediate(() => finishStream(code));
1068
1352
  });
1069
1353
  },