ai-sdk-provider-codex-cli 0.2.0 → 0.3.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
@@ -15,6 +15,7 @@ A community provider for Vercel AI SDK v5 that uses OpenAI’s Codex CLI (non‑
15
15
  - Works with `generateText`, `streamText`, and `generateObject` (native JSON Schema support via `--output-schema`)
16
16
  - Uses ChatGPT OAuth from `codex login` (tokens in `~/.codex/auth.json`) or `OPENAI_API_KEY`
17
17
  - Node-only (spawns a local process); supports CI and local dev
18
+ - **v0.3.0**: Adds comprehensive tool streaming support for monitoring autonomous tool execution
18
19
  - **v0.2.0 Breaking Changes**: Switched to `--experimental-json` and native schema enforcement (see [CHANGELOG](CHANGELOG.md))
19
20
 
20
21
  ## Installation
@@ -26,7 +27,7 @@ npm i -g @openai/codex
26
27
  codex login # or set OPENAI_API_KEY
27
28
  ```
28
29
 
29
- > **⚠️ Version Requirement**: Requires Codex CLI **>= 0.42.0** for `--experimental-json` and `--output-schema` support. Check your version with `codex --version` and upgrade if needed:
30
+ > **⚠️ Version Requirement**: Requires Codex CLI **>= 0.42.0** for `--experimental-json` and `--output-schema` support. **>= 0.44.0 recommended** for full usage tracking and tool streaming support. Check your version with `codex --version` and upgrade if needed:
30
31
  >
31
32
  > ```bash
32
33
  > npm i -g @openai/codex@latest
@@ -95,19 +96,53 @@ console.log(object);
95
96
 
96
97
  - AI SDK v5 compatible (LanguageModelV2)
97
98
  - Streaming and non‑streaming
99
+ - **Tool streaming support** (v0.3.0+) - Monitor autonomous tool execution in real-time
98
100
  - **Native JSON Schema support** via `--output-schema` (API-enforced with `strict: true`)
99
101
  - JSON object generation with Zod schemas (100-200 fewer tokens per request vs prompt engineering)
100
102
  - Safe defaults for non‑interactive automation (`on-failure`, `workspace-write`, `--skip-git-repo-check`)
101
103
  - Fallback to `npx @openai/codex` when not on PATH (`allowNpx`)
102
104
  - Usage tracking from experimental JSON event format
103
105
 
104
- ### Streaming behavior
106
+ ### Tool Streaming (v0.3.0+)
107
+
108
+ The provider supports comprehensive tool streaming, enabling real-time monitoring of Codex CLI's autonomous tool execution:
109
+
110
+ ```js
111
+ import { streamText } from 'ai';
112
+ import { codexCli } from 'ai-sdk-provider-codex-cli';
113
+
114
+ const result = await streamText({
115
+ model: codexCli('gpt-5-codex', { allowNpx: true, skipGitRepoCheck: true }),
116
+ prompt: 'List files and count lines in the largest one',
117
+ });
118
+
119
+ for await (const part of result.fullStream) {
120
+ if (part.type === 'tool-call') {
121
+ console.log('🔧 Tool:', part.toolName);
122
+ }
123
+ if (part.type === 'tool-result') {
124
+ console.log('✅ Result:', part.result);
125
+ }
126
+ }
127
+ ```
128
+
129
+ **What you get:**
130
+
131
+ - Tool invocation events when Codex starts executing tools (exec, patch, web_search, mcp_tool_call)
132
+ - Tool input tracking with full parameter visibility
133
+ - Tool result events with complete output payloads
134
+ - `providerExecuted: true` on all tool calls (Codex executes autonomously, app doesn't need to)
135
+
136
+ **Limitation:** Real-time output streaming (`output-delta` events) not yet available. Tool outputs delivered in final `tool-result` event. See `examples/streaming-tool-calls.mjs` and `examples/streaming-multiple-tools.mjs` for usage patterns.
137
+
138
+ ### Text Streaming behavior
105
139
 
106
140
  **Status:** Incremental streaming not currently supported with `--experimental-json` format (expected in future Codex CLI releases)
107
141
 
108
142
  The `--experimental-json` output format (introduced Sept 25, 2025) currently only emits `item.completed` events with full text content. Incremental streaming via `item.updated` or delta events is not yet implemented by OpenAI.
109
143
 
110
144
  **What this means:**
145
+
111
146
  - `streamText()` works functionally but delivers the entire response in a single chunk after generation completes
112
147
  - No incremental text deltas—you wait for the full response, then receive it all at once
113
148
  - The AI SDK's streaming interface is supported, but actual incremental streaming is not available
package/dist/index.cjs CHANGED
@@ -202,6 +202,16 @@ var CodexCliLanguageModel = class {
202
202
  const warn = validateModelId(this.modelId);
203
203
  if (warn) this.logger.warn(`Codex CLI model: ${warn}`);
204
204
  }
205
+ // Codex JSONL items use `type` for the item discriminator, but some
206
+ // earlier fixtures (and defensive parsing) might still surface `item_type`.
207
+ // This helper returns whichever is present.
208
+ getItemType(item) {
209
+ if (!item) return void 0;
210
+ const data = item;
211
+ const legacy = typeof data.item_type === "string" ? data.item_type : void 0;
212
+ const current = typeof data.type === "string" ? data.type : void 0;
213
+ return legacy ?? current;
214
+ }
205
215
  buildArgs(promptText, responseFormat) {
206
216
  const base = resolveCodexPath(this.settings.codexPath, this.settings.allowNpx);
207
217
  const args = [...base.args, "exec", "--experimental-json"];
@@ -226,17 +236,19 @@ var CodexCliLanguageModel = class {
226
236
  }
227
237
  let schemaPath;
228
238
  if (responseFormat?.type === "json" && responseFormat.schema) {
229
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "codex-schema-"));
230
- schemaPath = path.join(dir, "schema.json");
231
239
  const schema = typeof responseFormat.schema === "object" ? responseFormat.schema : {};
232
240
  const sanitizedSchema = this.sanitizeJsonSchema(schema);
233
- const schemaWithAdditional = {
234
- ...sanitizedSchema,
235
- // OpenAI strict mode requires additionalProperties=false even if user requested otherwise.
236
- additionalProperties: false
237
- };
238
- fs.writeFileSync(schemaPath, JSON.stringify(schemaWithAdditional, null, 2));
239
- args.push("--output-schema", schemaPath);
241
+ const hasProperties = Object.keys(sanitizedSchema).length > 0;
242
+ if (hasProperties) {
243
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "codex-schema-"));
244
+ schemaPath = path.join(dir, "schema.json");
245
+ const schemaWithAdditional = {
246
+ ...sanitizedSchema,
247
+ additionalProperties: false
248
+ };
249
+ fs.writeFileSync(schemaPath, JSON.stringify(schemaWithAdditional, null, 2));
250
+ args.push("--output-schema", schemaPath);
251
+ }
240
252
  }
241
253
  args.push(promptText);
242
254
  const env = {
@@ -300,34 +312,183 @@ var CodexCliLanguageModel = class {
300
312
  }
301
313
  parseExperimentalJsonEvent(line) {
302
314
  try {
303
- const evt = JSON.parse(line);
304
- const result = {};
305
- switch (evt.type) {
306
- case "session.created":
307
- result.sessionId = evt.session_id;
308
- break;
309
- case "turn.completed":
310
- if (evt.usage) {
311
- result.usage = {
312
- inputTokens: evt.usage.input_tokens || 0,
313
- outputTokens: evt.usage.output_tokens || 0
314
- };
315
- }
316
- break;
317
- case "item.completed":
318
- if (evt.item?.item_type === "assistant_message" || evt.item?.item_type === "reasoning") {
319
- result.text = evt.item.text;
320
- }
321
- break;
322
- case "error":
323
- result.error = evt.message;
324
- break;
315
+ return JSON.parse(line);
316
+ } catch {
317
+ return void 0;
318
+ }
319
+ }
320
+ extractUsage(evt) {
321
+ const reported = evt.usage;
322
+ if (!reported) return void 0;
323
+ const inputTokens = reported.input_tokens ?? 0;
324
+ const outputTokens = reported.output_tokens ?? 0;
325
+ const cachedInputTokens = reported.cached_input_tokens ?? 0;
326
+ return {
327
+ inputTokens,
328
+ outputTokens,
329
+ // totalTokens should not double-count cached tokens; track cached separately
330
+ totalTokens: inputTokens + outputTokens,
331
+ cachedInputTokens
332
+ };
333
+ }
334
+ getToolName(item) {
335
+ if (!item) return void 0;
336
+ const itemType = this.getItemType(item);
337
+ switch (itemType) {
338
+ case "command_execution":
339
+ return "exec";
340
+ case "file_change":
341
+ return "patch";
342
+ case "mcp_tool_call": {
343
+ const tool = item.tool;
344
+ if (typeof tool === "string" && tool.length > 0) return tool;
345
+ return "mcp_tool";
346
+ }
347
+ case "web_search":
348
+ return "web_search";
349
+ default:
350
+ return void 0;
351
+ }
352
+ }
353
+ buildToolInputPayload(item) {
354
+ if (!item) return void 0;
355
+ const data = item;
356
+ switch (this.getItemType(item)) {
357
+ case "command_execution": {
358
+ const payload = {};
359
+ if (typeof data.command === "string") payload.command = data.command;
360
+ if (typeof data.status === "string") payload.status = data.status;
361
+ if (typeof data.cwd === "string") payload.cwd = data.cwd;
362
+ return Object.keys(payload).length ? payload : void 0;
363
+ }
364
+ case "file_change": {
365
+ const payload = {};
366
+ if (Array.isArray(data.changes)) payload.changes = data.changes;
367
+ if (typeof data.status === "string") payload.status = data.status;
368
+ return Object.keys(payload).length ? payload : void 0;
325
369
  }
326
- return result;
370
+ case "mcp_tool_call": {
371
+ const payload = {};
372
+ if (typeof data.server === "string") payload.server = data.server;
373
+ if (typeof data.tool === "string") payload.tool = data.tool;
374
+ if (typeof data.status === "string") payload.status = data.status;
375
+ if (data.arguments !== void 0) payload.arguments = data.arguments;
376
+ return Object.keys(payload).length ? payload : void 0;
377
+ }
378
+ case "web_search": {
379
+ const payload = {};
380
+ if (typeof data.query === "string") payload.query = data.query;
381
+ return Object.keys(payload).length ? payload : void 0;
382
+ }
383
+ default:
384
+ return void 0;
385
+ }
386
+ }
387
+ buildToolResultPayload(item) {
388
+ if (!item) return { result: {} };
389
+ const data = item;
390
+ const metadata = {};
391
+ const itemType = this.getItemType(item);
392
+ if (typeof itemType === "string") metadata.itemType = itemType;
393
+ if (typeof item.id === "string") metadata.itemId = item.id;
394
+ if (typeof data.status === "string") metadata.status = data.status;
395
+ const buildResult = (result) => ({
396
+ result,
397
+ metadata: Object.keys(metadata).length ? metadata : void 0
398
+ });
399
+ switch (itemType) {
400
+ case "command_execution": {
401
+ const result = {};
402
+ if (typeof data.command === "string") result.command = data.command;
403
+ if (typeof data.aggregated_output === "string")
404
+ result.aggregatedOutput = data.aggregated_output;
405
+ if (typeof data.exit_code === "number") result.exitCode = data.exit_code;
406
+ if (typeof data.status === "string") result.status = data.status;
407
+ return buildResult(result);
408
+ }
409
+ case "file_change": {
410
+ const result = {};
411
+ if (Array.isArray(data.changes)) result.changes = data.changes;
412
+ if (typeof data.status === "string") result.status = data.status;
413
+ return buildResult(result);
414
+ }
415
+ case "mcp_tool_call": {
416
+ const result = {};
417
+ if (typeof data.server === "string") {
418
+ result.server = data.server;
419
+ metadata.server = data.server;
420
+ }
421
+ if (typeof data.tool === "string") result.tool = data.tool;
422
+ if (typeof data.status === "string") result.status = data.status;
423
+ if (data.result !== void 0) result.result = data.result;
424
+ if (data.error !== void 0) result.error = data.error;
425
+ return buildResult(result);
426
+ }
427
+ case "web_search": {
428
+ const result = {};
429
+ if (typeof data.query === "string") result.query = data.query;
430
+ if (typeof data.status === "string") result.status = data.status;
431
+ return buildResult(result);
432
+ }
433
+ default: {
434
+ const result = { ...data };
435
+ return buildResult(result);
436
+ }
437
+ }
438
+ }
439
+ safeStringify(value) {
440
+ if (value === void 0) return "";
441
+ if (typeof value === "string") return value;
442
+ try {
443
+ return JSON.stringify(value);
327
444
  } catch {
328
- return {};
445
+ return "";
329
446
  }
330
447
  }
448
+ emitToolInvocation(controller, toolCallId, toolName, inputPayload) {
449
+ const inputString = this.safeStringify(inputPayload);
450
+ controller.enqueue({ type: "tool-input-start", id: toolCallId, toolName });
451
+ if (inputString) {
452
+ controller.enqueue({ type: "tool-input-delta", id: toolCallId, delta: inputString });
453
+ }
454
+ controller.enqueue({ type: "tool-input-end", id: toolCallId });
455
+ controller.enqueue({
456
+ type: "tool-call",
457
+ toolCallId,
458
+ toolName,
459
+ input: inputString,
460
+ providerExecuted: true
461
+ });
462
+ }
463
+ emitToolResult(controller, toolCallId, toolName, item, resultPayload, metadata) {
464
+ const providerMetadataEntries = {
465
+ ...metadata ?? {}
466
+ };
467
+ const itemType = this.getItemType(item);
468
+ if (itemType && providerMetadataEntries.itemType === void 0) {
469
+ providerMetadataEntries.itemType = itemType;
470
+ }
471
+ if (item.id && providerMetadataEntries.itemId === void 0) {
472
+ providerMetadataEntries.itemId = item.id;
473
+ }
474
+ let isError;
475
+ if (itemType === "command_execution") {
476
+ const data = item;
477
+ const exitCode = typeof data.exit_code === "number" ? data.exit_code : void 0;
478
+ const status = typeof data.status === "string" ? data.status : void 0;
479
+ if (exitCode !== void 0 && exitCode !== 0 || status === "failed") {
480
+ isError = true;
481
+ }
482
+ }
483
+ controller.enqueue({
484
+ type: "tool-result",
485
+ toolCallId,
486
+ toolName,
487
+ result: resultPayload ?? {},
488
+ ...isError ? { isError: true } : {},
489
+ ...Object.keys(providerMetadataEntries).length ? { providerMetadata: { "codex-cli": providerMetadataEntries } } : {}
490
+ });
491
+ }
331
492
  handleSpawnError(err, promptExcerpt) {
332
493
  const e = err && typeof err === "object" ? err : void 0;
333
494
  const message = String((e?.message ?? err) || "Failed to run Codex CLI");
@@ -370,25 +531,56 @@ var CodexCliLanguageModel = class {
370
531
  try {
371
532
  await new Promise((resolve, reject) => {
372
533
  let stderr = "";
534
+ let turnFailureMessage;
373
535
  child.stderr.on("data", (d) => stderr += String(d));
374
536
  child.stdout.setEncoding("utf8");
375
537
  child.stdout.on("data", (chunk) => {
376
538
  const lines = chunk.split(/\r?\n/).filter(Boolean);
377
539
  for (const line of lines) {
378
- const parsed = this.parseExperimentalJsonEvent(line);
379
- if (parsed.sessionId) this.sessionId = parsed.sessionId;
380
- if (parsed.text) text = parsed.text;
381
- if (parsed.usage) {
382
- usage.inputTokens = parsed.usage.inputTokens;
383
- usage.outputTokens = parsed.usage.outputTokens;
384
- usage.totalTokens = usage.inputTokens + usage.outputTokens;
540
+ const event = this.parseExperimentalJsonEvent(line);
541
+ if (!event) continue;
542
+ if (event.type === "thread.started" && typeof event.thread_id === "string") {
543
+ this.sessionId = event.thread_id;
544
+ }
545
+ if (event.type === "session.created" && typeof event.session_id === "string") {
546
+ this.sessionId = event.session_id;
547
+ }
548
+ if (event.type === "turn.completed") {
549
+ const usageEvent = this.extractUsage(event);
550
+ if (usageEvent) {
551
+ usage.inputTokens = usageEvent.inputTokens;
552
+ usage.outputTokens = usageEvent.outputTokens;
553
+ usage.totalTokens = usageEvent.totalTokens;
554
+ }
555
+ }
556
+ if (event.type === "item.completed" && this.getItemType(event.item) === "assistant_message" && typeof event.item?.text === "string") {
557
+ text = event.item.text;
558
+ }
559
+ if (event.type === "turn.failed") {
560
+ const errorText = event.error && typeof event.error.message === "string" && event.error.message || (typeof event.message === "string" ? event.message : void 0);
561
+ turnFailureMessage = errorText ?? turnFailureMessage ?? "Codex turn failed";
562
+ }
563
+ if (event.type === "error") {
564
+ const errorText = typeof event.message === "string" ? event.message : void 0;
565
+ turnFailureMessage = errorText ?? turnFailureMessage ?? "Codex error";
385
566
  }
386
567
  }
387
568
  });
388
569
  child.on("error", (e) => reject(this.handleSpawnError(e, promptExcerpt)));
389
570
  child.on("close", (code) => {
390
- if (code === 0) resolve();
391
- else
571
+ if (code === 0) {
572
+ if (turnFailureMessage) {
573
+ reject(
574
+ createAPICallError({
575
+ message: turnFailureMessage,
576
+ stderr,
577
+ promptExcerpt
578
+ })
579
+ );
580
+ return;
581
+ }
582
+ resolve();
583
+ } else {
392
584
  reject(
393
585
  createAPICallError({
394
586
  message: `Codex CLI exited with code ${code}`,
@@ -397,6 +589,7 @@ var CodexCliLanguageModel = class {
397
589
  promptExcerpt
398
590
  })
399
591
  );
592
+ }
400
593
  });
401
594
  });
402
595
  } finally {
@@ -453,6 +646,69 @@ var CodexCliLanguageModel = class {
453
646
  controller.enqueue({ type: "stream-start", warnings });
454
647
  let stderr = "";
455
648
  let accumulatedText = "";
649
+ const activeTools = /* @__PURE__ */ new Map();
650
+ let responseMetadataSent = false;
651
+ let lastUsage;
652
+ let turnFailureMessage;
653
+ const sendMetadata = (meta = {}) => {
654
+ controller.enqueue({
655
+ type: "response-metadata",
656
+ id: crypto.randomUUID(),
657
+ timestamp: /* @__PURE__ */ new Date(),
658
+ modelId: this.modelId,
659
+ ...Object.keys(meta).length ? { providerMetadata: { "codex-cli": meta } } : {}
660
+ });
661
+ };
662
+ const handleItemEvent = (event) => {
663
+ const item = event.item;
664
+ if (!item) return;
665
+ if (event.type === "item.completed" && this.getItemType(item) === "assistant_message" && typeof item.text === "string") {
666
+ accumulatedText = item.text;
667
+ return;
668
+ }
669
+ const toolName = this.getToolName(item);
670
+ if (!toolName) {
671
+ return;
672
+ }
673
+ const mapKey = typeof item.id === "string" && item.id.length > 0 ? item.id : crypto.randomUUID();
674
+ let toolState = activeTools.get(mapKey);
675
+ const latestInput = this.buildToolInputPayload(item);
676
+ if (!toolState) {
677
+ toolState = {
678
+ toolCallId: mapKey,
679
+ toolName,
680
+ inputPayload: latestInput,
681
+ hasEmittedCall: false
682
+ };
683
+ activeTools.set(mapKey, toolState);
684
+ } else {
685
+ toolState.toolName = toolName;
686
+ if (latestInput !== void 0) {
687
+ toolState.inputPayload = latestInput;
688
+ }
689
+ }
690
+ if (!toolState.hasEmittedCall) {
691
+ this.emitToolInvocation(
692
+ controller,
693
+ toolState.toolCallId,
694
+ toolState.toolName,
695
+ toolState.inputPayload
696
+ );
697
+ toolState.hasEmittedCall = true;
698
+ }
699
+ if (event.type === "item.completed") {
700
+ const { result, metadata } = this.buildToolResultPayload(item);
701
+ this.emitToolResult(
702
+ controller,
703
+ toolState.toolCallId,
704
+ toolState.toolName,
705
+ item,
706
+ result,
707
+ metadata
708
+ );
709
+ activeTools.delete(mapKey);
710
+ }
711
+ };
456
712
  const onAbort = () => {
457
713
  child.kill("SIGTERM");
458
714
  };
@@ -464,42 +720,7 @@ var CodexCliLanguageModel = class {
464
720
  }
465
721
  options.abortSignal.addEventListener("abort", onAbort, { once: true });
466
722
  }
467
- child.stderr.on("data", (d) => stderr += String(d));
468
- child.stdout.setEncoding("utf8");
469
- child.stdout.on("data", (chunk) => {
470
- const lines = chunk.split(/\r?\n/).filter(Boolean);
471
- for (const line of lines) {
472
- const parsed = this.parseExperimentalJsonEvent(line);
473
- if (parsed.sessionId) {
474
- this.sessionId = parsed.sessionId;
475
- controller.enqueue({
476
- type: "response-metadata",
477
- id: crypto.randomUUID(),
478
- timestamp: /* @__PURE__ */ new Date(),
479
- modelId: this.modelId
480
- });
481
- }
482
- if (parsed.text) {
483
- accumulatedText = parsed.text;
484
- }
485
- }
486
- });
487
- const cleanupSchema = () => {
488
- if (!schemaPath) return;
489
- try {
490
- const schemaDir = path.dirname(schemaPath);
491
- fs.rmSync(schemaDir, { recursive: true, force: true });
492
- } catch {
493
- }
494
- };
495
- child.on("error", (e) => {
496
- if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort);
497
- cleanupSchema();
498
- controller.error(this.handleSpawnError(e, promptExcerpt));
499
- });
500
- child.on("close", (code) => {
501
- if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort);
502
- cleanupSchema();
723
+ const finishStream = (code) => {
503
724
  if (code !== 0) {
504
725
  controller.error(
505
726
  createAPICallError({
@@ -511,6 +732,16 @@ var CodexCliLanguageModel = class {
511
732
  );
512
733
  return;
513
734
  }
735
+ if (turnFailureMessage) {
736
+ controller.error(
737
+ createAPICallError({
738
+ message: turnFailureMessage,
739
+ stderr,
740
+ promptExcerpt
741
+ })
742
+ );
743
+ return;
744
+ }
514
745
  let finalText = accumulatedText;
515
746
  if (!finalText && lastMessagePath) {
516
747
  try {
@@ -524,14 +755,84 @@ var CodexCliLanguageModel = class {
524
755
  }
525
756
  }
526
757
  if (finalText) {
527
- controller.enqueue({ type: "text-delta", id: crypto.randomUUID(), delta: finalText });
758
+ const textId = crypto.randomUUID();
759
+ controller.enqueue({ type: "text-start", id: textId });
760
+ controller.enqueue({ type: "text-delta", id: textId, delta: finalText });
761
+ controller.enqueue({ type: "text-end", id: textId });
528
762
  }
763
+ const usageSummary = lastUsage ?? { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
529
764
  controller.enqueue({
530
765
  type: "finish",
531
766
  finishReason: "stop",
532
- usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
767
+ usage: usageSummary
533
768
  });
534
769
  controller.close();
770
+ };
771
+ child.stderr.on("data", (d) => stderr += String(d));
772
+ child.stdout.setEncoding("utf8");
773
+ child.stdout.on("data", (chunk) => {
774
+ const lines = chunk.split(/\r?\n/).filter(Boolean);
775
+ for (const line of lines) {
776
+ const event = this.parseExperimentalJsonEvent(line);
777
+ if (!event) continue;
778
+ if (event.type === "thread.started" && typeof event.thread_id === "string") {
779
+ this.sessionId = event.thread_id;
780
+ if (!responseMetadataSent) {
781
+ responseMetadataSent = true;
782
+ sendMetadata();
783
+ }
784
+ continue;
785
+ }
786
+ if (event.type === "session.created" && typeof event.session_id === "string") {
787
+ this.sessionId = event.session_id;
788
+ if (!responseMetadataSent) {
789
+ responseMetadataSent = true;
790
+ sendMetadata();
791
+ }
792
+ continue;
793
+ }
794
+ if (event.type === "turn.completed") {
795
+ const usageEvent = this.extractUsage(event);
796
+ if (usageEvent) {
797
+ lastUsage = usageEvent;
798
+ }
799
+ continue;
800
+ }
801
+ if (event.type === "turn.failed") {
802
+ const errorText = event.error && typeof event.error.message === "string" && event.error.message || (typeof event.message === "string" ? event.message : void 0);
803
+ turnFailureMessage = errorText ?? turnFailureMessage ?? "Codex turn failed";
804
+ sendMetadata({ error: turnFailureMessage });
805
+ continue;
806
+ }
807
+ if (event.type === "error") {
808
+ const errorText = typeof event.message === "string" ? event.message : void 0;
809
+ const effective = errorText ?? "Codex error";
810
+ turnFailureMessage = turnFailureMessage ?? effective;
811
+ sendMetadata({ error: effective });
812
+ continue;
813
+ }
814
+ if (event.type && event.type.startsWith("item.")) {
815
+ handleItemEvent(event);
816
+ }
817
+ }
818
+ });
819
+ const cleanupSchema = () => {
820
+ if (!schemaPath) return;
821
+ try {
822
+ const schemaDir = path.dirname(schemaPath);
823
+ fs.rmSync(schemaDir, { recursive: true, force: true });
824
+ } catch {
825
+ }
826
+ };
827
+ child.on("error", (e) => {
828
+ if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort);
829
+ cleanupSchema();
830
+ controller.error(this.handleSpawnError(e, promptExcerpt));
831
+ });
832
+ child.on("close", (code) => {
833
+ if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort);
834
+ cleanupSchema();
835
+ setImmediate(() => finishStream(code));
535
836
  });
536
837
  },
537
838
  cancel: () => {
package/dist/index.d.cts CHANGED
@@ -51,10 +51,18 @@ declare class CodexCliLanguageModel implements LanguageModelV2 {
51
51
  private logger;
52
52
  private sessionId?;
53
53
  constructor(options: CodexLanguageModelOptions);
54
+ private getItemType;
54
55
  private buildArgs;
55
56
  private sanitizeJsonSchema;
56
57
  private mapWarnings;
57
58
  private parseExperimentalJsonEvent;
59
+ private extractUsage;
60
+ private getToolName;
61
+ private buildToolInputPayload;
62
+ private buildToolResultPayload;
63
+ private safeStringify;
64
+ private emitToolInvocation;
65
+ private emitToolResult;
58
66
  private handleSpawnError;
59
67
  doGenerate(options: Parameters<LanguageModelV2['doGenerate']>[0]): Promise<Awaited<ReturnType<LanguageModelV2['doGenerate']>>>;
60
68
  doStream(options: Parameters<LanguageModelV2['doStream']>[0]): Promise<Awaited<ReturnType<LanguageModelV2['doStream']>>>;
package/dist/index.d.ts CHANGED
@@ -51,10 +51,18 @@ declare class CodexCliLanguageModel implements LanguageModelV2 {
51
51
  private logger;
52
52
  private sessionId?;
53
53
  constructor(options: CodexLanguageModelOptions);
54
+ private getItemType;
54
55
  private buildArgs;
55
56
  private sanitizeJsonSchema;
56
57
  private mapWarnings;
57
58
  private parseExperimentalJsonEvent;
59
+ private extractUsage;
60
+ private getToolName;
61
+ private buildToolInputPayload;
62
+ private buildToolResultPayload;
63
+ private safeStringify;
64
+ private emitToolInvocation;
65
+ private emitToolResult;
58
66
  private handleSpawnError;
59
67
  doGenerate(options: Parameters<LanguageModelV2['doGenerate']>[0]): Promise<Awaited<ReturnType<LanguageModelV2['doGenerate']>>>;
60
68
  doStream(options: Parameters<LanguageModelV2['doStream']>[0]): Promise<Awaited<ReturnType<LanguageModelV2['doStream']>>>;
package/dist/index.js CHANGED
@@ -199,6 +199,16 @@ var CodexCliLanguageModel = class {
199
199
  const warn = validateModelId(this.modelId);
200
200
  if (warn) this.logger.warn(`Codex CLI model: ${warn}`);
201
201
  }
202
+ // Codex JSONL items use `type` for the item discriminator, but some
203
+ // earlier fixtures (and defensive parsing) might still surface `item_type`.
204
+ // This helper returns whichever is present.
205
+ getItemType(item) {
206
+ if (!item) return void 0;
207
+ const data = item;
208
+ const legacy = typeof data.item_type === "string" ? data.item_type : void 0;
209
+ const current = typeof data.type === "string" ? data.type : void 0;
210
+ return legacy ?? current;
211
+ }
202
212
  buildArgs(promptText, responseFormat) {
203
213
  const base = resolveCodexPath(this.settings.codexPath, this.settings.allowNpx);
204
214
  const args = [...base.args, "exec", "--experimental-json"];
@@ -223,17 +233,19 @@ var CodexCliLanguageModel = class {
223
233
  }
224
234
  let schemaPath;
225
235
  if (responseFormat?.type === "json" && responseFormat.schema) {
226
- const dir = mkdtempSync(join(tmpdir(), "codex-schema-"));
227
- schemaPath = join(dir, "schema.json");
228
236
  const schema = typeof responseFormat.schema === "object" ? responseFormat.schema : {};
229
237
  const sanitizedSchema = this.sanitizeJsonSchema(schema);
230
- const schemaWithAdditional = {
231
- ...sanitizedSchema,
232
- // OpenAI strict mode requires additionalProperties=false even if user requested otherwise.
233
- additionalProperties: false
234
- };
235
- writeFileSync(schemaPath, JSON.stringify(schemaWithAdditional, null, 2));
236
- args.push("--output-schema", schemaPath);
238
+ const hasProperties = Object.keys(sanitizedSchema).length > 0;
239
+ if (hasProperties) {
240
+ const dir = mkdtempSync(join(tmpdir(), "codex-schema-"));
241
+ schemaPath = join(dir, "schema.json");
242
+ const schemaWithAdditional = {
243
+ ...sanitizedSchema,
244
+ additionalProperties: false
245
+ };
246
+ writeFileSync(schemaPath, JSON.stringify(schemaWithAdditional, null, 2));
247
+ args.push("--output-schema", schemaPath);
248
+ }
237
249
  }
238
250
  args.push(promptText);
239
251
  const env = {
@@ -297,34 +309,183 @@ var CodexCliLanguageModel = class {
297
309
  }
298
310
  parseExperimentalJsonEvent(line) {
299
311
  try {
300
- const evt = JSON.parse(line);
301
- const result = {};
302
- switch (evt.type) {
303
- case "session.created":
304
- result.sessionId = evt.session_id;
305
- break;
306
- case "turn.completed":
307
- if (evt.usage) {
308
- result.usage = {
309
- inputTokens: evt.usage.input_tokens || 0,
310
- outputTokens: evt.usage.output_tokens || 0
311
- };
312
- }
313
- break;
314
- case "item.completed":
315
- if (evt.item?.item_type === "assistant_message" || evt.item?.item_type === "reasoning") {
316
- result.text = evt.item.text;
317
- }
318
- break;
319
- case "error":
320
- result.error = evt.message;
321
- break;
312
+ return JSON.parse(line);
313
+ } catch {
314
+ return void 0;
315
+ }
316
+ }
317
+ extractUsage(evt) {
318
+ const reported = evt.usage;
319
+ if (!reported) return void 0;
320
+ const inputTokens = reported.input_tokens ?? 0;
321
+ const outputTokens = reported.output_tokens ?? 0;
322
+ const cachedInputTokens = reported.cached_input_tokens ?? 0;
323
+ return {
324
+ inputTokens,
325
+ outputTokens,
326
+ // totalTokens should not double-count cached tokens; track cached separately
327
+ totalTokens: inputTokens + outputTokens,
328
+ cachedInputTokens
329
+ };
330
+ }
331
+ getToolName(item) {
332
+ if (!item) return void 0;
333
+ const itemType = this.getItemType(item);
334
+ switch (itemType) {
335
+ case "command_execution":
336
+ return "exec";
337
+ case "file_change":
338
+ return "patch";
339
+ case "mcp_tool_call": {
340
+ const tool = item.tool;
341
+ if (typeof tool === "string" && tool.length > 0) return tool;
342
+ return "mcp_tool";
343
+ }
344
+ case "web_search":
345
+ return "web_search";
346
+ default:
347
+ return void 0;
348
+ }
349
+ }
350
+ buildToolInputPayload(item) {
351
+ if (!item) return void 0;
352
+ const data = item;
353
+ switch (this.getItemType(item)) {
354
+ case "command_execution": {
355
+ const payload = {};
356
+ if (typeof data.command === "string") payload.command = data.command;
357
+ if (typeof data.status === "string") payload.status = data.status;
358
+ if (typeof data.cwd === "string") payload.cwd = data.cwd;
359
+ return Object.keys(payload).length ? payload : void 0;
360
+ }
361
+ case "file_change": {
362
+ const payload = {};
363
+ if (Array.isArray(data.changes)) payload.changes = data.changes;
364
+ if (typeof data.status === "string") payload.status = data.status;
365
+ return Object.keys(payload).length ? payload : void 0;
322
366
  }
323
- return result;
367
+ case "mcp_tool_call": {
368
+ const payload = {};
369
+ if (typeof data.server === "string") payload.server = data.server;
370
+ if (typeof data.tool === "string") payload.tool = data.tool;
371
+ if (typeof data.status === "string") payload.status = data.status;
372
+ if (data.arguments !== void 0) payload.arguments = data.arguments;
373
+ return Object.keys(payload).length ? payload : void 0;
374
+ }
375
+ case "web_search": {
376
+ const payload = {};
377
+ if (typeof data.query === "string") payload.query = data.query;
378
+ return Object.keys(payload).length ? payload : void 0;
379
+ }
380
+ default:
381
+ return void 0;
382
+ }
383
+ }
384
+ buildToolResultPayload(item) {
385
+ if (!item) return { result: {} };
386
+ const data = item;
387
+ const metadata = {};
388
+ const itemType = this.getItemType(item);
389
+ if (typeof itemType === "string") metadata.itemType = itemType;
390
+ if (typeof item.id === "string") metadata.itemId = item.id;
391
+ if (typeof data.status === "string") metadata.status = data.status;
392
+ const buildResult = (result) => ({
393
+ result,
394
+ metadata: Object.keys(metadata).length ? metadata : void 0
395
+ });
396
+ switch (itemType) {
397
+ case "command_execution": {
398
+ const result = {};
399
+ if (typeof data.command === "string") result.command = data.command;
400
+ if (typeof data.aggregated_output === "string")
401
+ result.aggregatedOutput = data.aggregated_output;
402
+ if (typeof data.exit_code === "number") result.exitCode = data.exit_code;
403
+ if (typeof data.status === "string") result.status = data.status;
404
+ return buildResult(result);
405
+ }
406
+ case "file_change": {
407
+ const result = {};
408
+ if (Array.isArray(data.changes)) result.changes = data.changes;
409
+ if (typeof data.status === "string") result.status = data.status;
410
+ return buildResult(result);
411
+ }
412
+ case "mcp_tool_call": {
413
+ const result = {};
414
+ if (typeof data.server === "string") {
415
+ result.server = data.server;
416
+ metadata.server = data.server;
417
+ }
418
+ if (typeof data.tool === "string") result.tool = data.tool;
419
+ if (typeof data.status === "string") result.status = data.status;
420
+ if (data.result !== void 0) result.result = data.result;
421
+ if (data.error !== void 0) result.error = data.error;
422
+ return buildResult(result);
423
+ }
424
+ case "web_search": {
425
+ const result = {};
426
+ if (typeof data.query === "string") result.query = data.query;
427
+ if (typeof data.status === "string") result.status = data.status;
428
+ return buildResult(result);
429
+ }
430
+ default: {
431
+ const result = { ...data };
432
+ return buildResult(result);
433
+ }
434
+ }
435
+ }
436
+ safeStringify(value) {
437
+ if (value === void 0) return "";
438
+ if (typeof value === "string") return value;
439
+ try {
440
+ return JSON.stringify(value);
324
441
  } catch {
325
- return {};
442
+ return "";
326
443
  }
327
444
  }
445
+ emitToolInvocation(controller, toolCallId, toolName, inputPayload) {
446
+ const inputString = this.safeStringify(inputPayload);
447
+ controller.enqueue({ type: "tool-input-start", id: toolCallId, toolName });
448
+ if (inputString) {
449
+ controller.enqueue({ type: "tool-input-delta", id: toolCallId, delta: inputString });
450
+ }
451
+ controller.enqueue({ type: "tool-input-end", id: toolCallId });
452
+ controller.enqueue({
453
+ type: "tool-call",
454
+ toolCallId,
455
+ toolName,
456
+ input: inputString,
457
+ providerExecuted: true
458
+ });
459
+ }
460
+ emitToolResult(controller, toolCallId, toolName, item, resultPayload, metadata) {
461
+ const providerMetadataEntries = {
462
+ ...metadata ?? {}
463
+ };
464
+ const itemType = this.getItemType(item);
465
+ if (itemType && providerMetadataEntries.itemType === void 0) {
466
+ providerMetadataEntries.itemType = itemType;
467
+ }
468
+ if (item.id && providerMetadataEntries.itemId === void 0) {
469
+ providerMetadataEntries.itemId = item.id;
470
+ }
471
+ let isError;
472
+ if (itemType === "command_execution") {
473
+ const data = item;
474
+ const exitCode = typeof data.exit_code === "number" ? data.exit_code : void 0;
475
+ const status = typeof data.status === "string" ? data.status : void 0;
476
+ if (exitCode !== void 0 && exitCode !== 0 || status === "failed") {
477
+ isError = true;
478
+ }
479
+ }
480
+ controller.enqueue({
481
+ type: "tool-result",
482
+ toolCallId,
483
+ toolName,
484
+ result: resultPayload ?? {},
485
+ ...isError ? { isError: true } : {},
486
+ ...Object.keys(providerMetadataEntries).length ? { providerMetadata: { "codex-cli": providerMetadataEntries } } : {}
487
+ });
488
+ }
328
489
  handleSpawnError(err, promptExcerpt) {
329
490
  const e = err && typeof err === "object" ? err : void 0;
330
491
  const message = String((e?.message ?? err) || "Failed to run Codex CLI");
@@ -367,25 +528,56 @@ var CodexCliLanguageModel = class {
367
528
  try {
368
529
  await new Promise((resolve, reject) => {
369
530
  let stderr = "";
531
+ let turnFailureMessage;
370
532
  child.stderr.on("data", (d) => stderr += String(d));
371
533
  child.stdout.setEncoding("utf8");
372
534
  child.stdout.on("data", (chunk) => {
373
535
  const lines = chunk.split(/\r?\n/).filter(Boolean);
374
536
  for (const line of lines) {
375
- const parsed = this.parseExperimentalJsonEvent(line);
376
- if (parsed.sessionId) this.sessionId = parsed.sessionId;
377
- if (parsed.text) text = parsed.text;
378
- if (parsed.usage) {
379
- usage.inputTokens = parsed.usage.inputTokens;
380
- usage.outputTokens = parsed.usage.outputTokens;
381
- usage.totalTokens = usage.inputTokens + usage.outputTokens;
537
+ const event = this.parseExperimentalJsonEvent(line);
538
+ if (!event) continue;
539
+ if (event.type === "thread.started" && typeof event.thread_id === "string") {
540
+ this.sessionId = event.thread_id;
541
+ }
542
+ if (event.type === "session.created" && typeof event.session_id === "string") {
543
+ this.sessionId = event.session_id;
544
+ }
545
+ if (event.type === "turn.completed") {
546
+ const usageEvent = this.extractUsage(event);
547
+ if (usageEvent) {
548
+ usage.inputTokens = usageEvent.inputTokens;
549
+ usage.outputTokens = usageEvent.outputTokens;
550
+ usage.totalTokens = usageEvent.totalTokens;
551
+ }
552
+ }
553
+ if (event.type === "item.completed" && this.getItemType(event.item) === "assistant_message" && typeof event.item?.text === "string") {
554
+ text = event.item.text;
555
+ }
556
+ if (event.type === "turn.failed") {
557
+ const errorText = event.error && typeof event.error.message === "string" && event.error.message || (typeof event.message === "string" ? event.message : void 0);
558
+ turnFailureMessage = errorText ?? turnFailureMessage ?? "Codex turn failed";
559
+ }
560
+ if (event.type === "error") {
561
+ const errorText = typeof event.message === "string" ? event.message : void 0;
562
+ turnFailureMessage = errorText ?? turnFailureMessage ?? "Codex error";
382
563
  }
383
564
  }
384
565
  });
385
566
  child.on("error", (e) => reject(this.handleSpawnError(e, promptExcerpt)));
386
567
  child.on("close", (code) => {
387
- if (code === 0) resolve();
388
- else
568
+ if (code === 0) {
569
+ if (turnFailureMessage) {
570
+ reject(
571
+ createAPICallError({
572
+ message: turnFailureMessage,
573
+ stderr,
574
+ promptExcerpt
575
+ })
576
+ );
577
+ return;
578
+ }
579
+ resolve();
580
+ } else {
389
581
  reject(
390
582
  createAPICallError({
391
583
  message: `Codex CLI exited with code ${code}`,
@@ -394,6 +586,7 @@ var CodexCliLanguageModel = class {
394
586
  promptExcerpt
395
587
  })
396
588
  );
589
+ }
397
590
  });
398
591
  });
399
592
  } finally {
@@ -450,6 +643,69 @@ var CodexCliLanguageModel = class {
450
643
  controller.enqueue({ type: "stream-start", warnings });
451
644
  let stderr = "";
452
645
  let accumulatedText = "";
646
+ const activeTools = /* @__PURE__ */ new Map();
647
+ let responseMetadataSent = false;
648
+ let lastUsage;
649
+ let turnFailureMessage;
650
+ const sendMetadata = (meta = {}) => {
651
+ controller.enqueue({
652
+ type: "response-metadata",
653
+ id: randomUUID(),
654
+ timestamp: /* @__PURE__ */ new Date(),
655
+ modelId: this.modelId,
656
+ ...Object.keys(meta).length ? { providerMetadata: { "codex-cli": meta } } : {}
657
+ });
658
+ };
659
+ const handleItemEvent = (event) => {
660
+ const item = event.item;
661
+ if (!item) return;
662
+ if (event.type === "item.completed" && this.getItemType(item) === "assistant_message" && typeof item.text === "string") {
663
+ accumulatedText = item.text;
664
+ return;
665
+ }
666
+ const toolName = this.getToolName(item);
667
+ if (!toolName) {
668
+ return;
669
+ }
670
+ const mapKey = typeof item.id === "string" && item.id.length > 0 ? item.id : randomUUID();
671
+ let toolState = activeTools.get(mapKey);
672
+ const latestInput = this.buildToolInputPayload(item);
673
+ if (!toolState) {
674
+ toolState = {
675
+ toolCallId: mapKey,
676
+ toolName,
677
+ inputPayload: latestInput,
678
+ hasEmittedCall: false
679
+ };
680
+ activeTools.set(mapKey, toolState);
681
+ } else {
682
+ toolState.toolName = toolName;
683
+ if (latestInput !== void 0) {
684
+ toolState.inputPayload = latestInput;
685
+ }
686
+ }
687
+ if (!toolState.hasEmittedCall) {
688
+ this.emitToolInvocation(
689
+ controller,
690
+ toolState.toolCallId,
691
+ toolState.toolName,
692
+ toolState.inputPayload
693
+ );
694
+ toolState.hasEmittedCall = true;
695
+ }
696
+ if (event.type === "item.completed") {
697
+ const { result, metadata } = this.buildToolResultPayload(item);
698
+ this.emitToolResult(
699
+ controller,
700
+ toolState.toolCallId,
701
+ toolState.toolName,
702
+ item,
703
+ result,
704
+ metadata
705
+ );
706
+ activeTools.delete(mapKey);
707
+ }
708
+ };
453
709
  const onAbort = () => {
454
710
  child.kill("SIGTERM");
455
711
  };
@@ -461,42 +717,7 @@ var CodexCliLanguageModel = class {
461
717
  }
462
718
  options.abortSignal.addEventListener("abort", onAbort, { once: true });
463
719
  }
464
- child.stderr.on("data", (d) => stderr += String(d));
465
- child.stdout.setEncoding("utf8");
466
- child.stdout.on("data", (chunk) => {
467
- const lines = chunk.split(/\r?\n/).filter(Boolean);
468
- for (const line of lines) {
469
- const parsed = this.parseExperimentalJsonEvent(line);
470
- if (parsed.sessionId) {
471
- this.sessionId = parsed.sessionId;
472
- controller.enqueue({
473
- type: "response-metadata",
474
- id: randomUUID(),
475
- timestamp: /* @__PURE__ */ new Date(),
476
- modelId: this.modelId
477
- });
478
- }
479
- if (parsed.text) {
480
- accumulatedText = parsed.text;
481
- }
482
- }
483
- });
484
- const cleanupSchema = () => {
485
- if (!schemaPath) return;
486
- try {
487
- const schemaDir = dirname(schemaPath);
488
- rmSync(schemaDir, { recursive: true, force: true });
489
- } catch {
490
- }
491
- };
492
- child.on("error", (e) => {
493
- if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort);
494
- cleanupSchema();
495
- controller.error(this.handleSpawnError(e, promptExcerpt));
496
- });
497
- child.on("close", (code) => {
498
- if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort);
499
- cleanupSchema();
720
+ const finishStream = (code) => {
500
721
  if (code !== 0) {
501
722
  controller.error(
502
723
  createAPICallError({
@@ -508,6 +729,16 @@ var CodexCliLanguageModel = class {
508
729
  );
509
730
  return;
510
731
  }
732
+ if (turnFailureMessage) {
733
+ controller.error(
734
+ createAPICallError({
735
+ message: turnFailureMessage,
736
+ stderr,
737
+ promptExcerpt
738
+ })
739
+ );
740
+ return;
741
+ }
511
742
  let finalText = accumulatedText;
512
743
  if (!finalText && lastMessagePath) {
513
744
  try {
@@ -521,14 +752,84 @@ var CodexCliLanguageModel = class {
521
752
  }
522
753
  }
523
754
  if (finalText) {
524
- controller.enqueue({ type: "text-delta", id: randomUUID(), delta: finalText });
755
+ const textId = randomUUID();
756
+ controller.enqueue({ type: "text-start", id: textId });
757
+ controller.enqueue({ type: "text-delta", id: textId, delta: finalText });
758
+ controller.enqueue({ type: "text-end", id: textId });
525
759
  }
760
+ const usageSummary = lastUsage ?? { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
526
761
  controller.enqueue({
527
762
  type: "finish",
528
763
  finishReason: "stop",
529
- usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
764
+ usage: usageSummary
530
765
  });
531
766
  controller.close();
767
+ };
768
+ child.stderr.on("data", (d) => stderr += String(d));
769
+ child.stdout.setEncoding("utf8");
770
+ child.stdout.on("data", (chunk) => {
771
+ const lines = chunk.split(/\r?\n/).filter(Boolean);
772
+ for (const line of lines) {
773
+ const event = this.parseExperimentalJsonEvent(line);
774
+ if (!event) continue;
775
+ if (event.type === "thread.started" && typeof event.thread_id === "string") {
776
+ this.sessionId = event.thread_id;
777
+ if (!responseMetadataSent) {
778
+ responseMetadataSent = true;
779
+ sendMetadata();
780
+ }
781
+ continue;
782
+ }
783
+ if (event.type === "session.created" && typeof event.session_id === "string") {
784
+ this.sessionId = event.session_id;
785
+ if (!responseMetadataSent) {
786
+ responseMetadataSent = true;
787
+ sendMetadata();
788
+ }
789
+ continue;
790
+ }
791
+ if (event.type === "turn.completed") {
792
+ const usageEvent = this.extractUsage(event);
793
+ if (usageEvent) {
794
+ lastUsage = usageEvent;
795
+ }
796
+ continue;
797
+ }
798
+ if (event.type === "turn.failed") {
799
+ const errorText = event.error && typeof event.error.message === "string" && event.error.message || (typeof event.message === "string" ? event.message : void 0);
800
+ turnFailureMessage = errorText ?? turnFailureMessage ?? "Codex turn failed";
801
+ sendMetadata({ error: turnFailureMessage });
802
+ continue;
803
+ }
804
+ if (event.type === "error") {
805
+ const errorText = typeof event.message === "string" ? event.message : void 0;
806
+ const effective = errorText ?? "Codex error";
807
+ turnFailureMessage = turnFailureMessage ?? effective;
808
+ sendMetadata({ error: effective });
809
+ continue;
810
+ }
811
+ if (event.type && event.type.startsWith("item.")) {
812
+ handleItemEvent(event);
813
+ }
814
+ }
815
+ });
816
+ const cleanupSchema = () => {
817
+ if (!schemaPath) return;
818
+ try {
819
+ const schemaDir = dirname(schemaPath);
820
+ rmSync(schemaDir, { recursive: true, force: true });
821
+ } catch {
822
+ }
823
+ };
824
+ child.on("error", (e) => {
825
+ if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort);
826
+ cleanupSchema();
827
+ controller.error(this.handleSpawnError(e, promptExcerpt));
828
+ });
829
+ child.on("close", (code) => {
830
+ if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort);
831
+ cleanupSchema();
832
+ setImmediate(() => finishStream(code));
532
833
  });
533
834
  },
534
835
  cancel: () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-sdk-provider-codex-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "AI SDK v5 provider for OpenAI Codex CLI with native JSON Schema support",
5
5
  "keywords": [
6
6
  "ai-sdk",
@@ -61,7 +61,7 @@
61
61
  "jsonc-parser": "^3.3.1"
62
62
  },
63
63
  "optionalDependencies": {
64
- "@openai/codex": "*"
64
+ "@openai/codex": "^0.44.0"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@eslint/js": "^9.14.0",