ai-sdk-provider-codex-cli 0.1.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/dist/index.js CHANGED
@@ -2,30 +2,14 @@ import { NoSuchModelError, LoadAPIKeyError, APICallError } from '@ai-sdk/provide
2
2
  import { spawn } from 'child_process';
3
3
  import { randomUUID } from 'crypto';
4
4
  import { createRequire } from 'module';
5
- import { mkdtempSync, readFileSync, rmSync } from 'fs';
5
+ import { mkdtempSync, writeFileSync, rmSync, readFileSync } from 'fs';
6
6
  import { tmpdir } from 'os';
7
- import { join } from 'path';
7
+ import { join, dirname } from 'path';
8
8
  import { generateId } from '@ai-sdk/provider-utils';
9
9
  import { z } from 'zod';
10
10
 
11
11
  // src/codex-cli-provider.ts
12
12
 
13
- // src/extract-json.ts
14
- function extractJson(text) {
15
- const start = text.indexOf("{");
16
- if (start === -1) return text;
17
- let depth = 0;
18
- for (let i = start; i < text.length; i++) {
19
- const ch = text[i];
20
- if (ch === "{") depth++;
21
- else if (ch === "}") {
22
- depth--;
23
- if (depth === 0) return text.slice(start, i + 1);
24
- }
25
- }
26
- return text;
27
- }
28
-
29
13
  // src/logger.ts
30
14
  var defaultLogger = {
31
15
  warn: (m) => console.warn(m),
@@ -105,7 +89,7 @@ function isToolItem(p) {
105
89
  if (out.type === "text" && typeof out.value !== "string") return false;
106
90
  return true;
107
91
  }
108
- function mapMessagesToPrompt(prompt, mode = { type: "regular" }, jsonSchema) {
92
+ function mapMessagesToPrompt(prompt) {
109
93
  const warnings = [];
110
94
  const parts = [];
111
95
  let systemText;
@@ -148,17 +132,6 @@ function mapMessagesToPrompt(prompt, mode = { type: "regular" }, jsonSchema) {
148
132
  let promptText = "";
149
133
  if (systemText) promptText += systemText + "\n\n";
150
134
  promptText += parts.join("\n\n");
151
- if (mode.type === "object-json" && jsonSchema) {
152
- const schemaStr = JSON.stringify(jsonSchema, null, 2);
153
- promptText = `CRITICAL: You MUST respond with ONLY a JSON object. NO other text.
154
- Your response MUST start with { and end with }
155
- The JSON MUST match this EXACT schema:
156
- ${schemaStr}
157
-
158
- Now, based on the following conversation, generate ONLY the JSON object:
159
-
160
- ${promptText}`;
161
- }
162
135
  return { promptText, ...warnings.length ? { warnings } : {} };
163
136
  }
164
137
  function createAPICallError({
@@ -211,7 +184,7 @@ var CodexCliLanguageModel = class {
211
184
  defaultObjectGenerationMode = "json";
212
185
  supportsImageUrls = false;
213
186
  supportedUrls = {};
214
- supportsStructuredOutputs = false;
187
+ supportsStructuredOutputs = true;
215
188
  modelId;
216
189
  settings;
217
190
  logger;
@@ -226,9 +199,19 @@ var CodexCliLanguageModel = class {
226
199
  const warn = validateModelId(this.modelId);
227
200
  if (warn) this.logger.warn(`Codex CLI model: ${warn}`);
228
201
  }
229
- buildArgs(promptText) {
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
+ }
212
+ buildArgs(promptText, responseFormat) {
230
213
  const base = resolveCodexPath(this.settings.codexPath, this.settings.allowNpx);
231
- const args = [...base.args, "exec", "--json"];
214
+ const args = [...base.args, "exec", "--experimental-json"];
232
215
  if (this.settings.fullAuto) {
233
216
  args.push("--full-auto");
234
217
  } else if (this.settings.dangerouslyBypassApprovalsAndSandbox) {
@@ -248,6 +231,22 @@ var CodexCliLanguageModel = class {
248
231
  if (this.modelId) {
249
232
  args.push("-m", this.modelId);
250
233
  }
234
+ let schemaPath;
235
+ if (responseFormat?.type === "json" && responseFormat.schema) {
236
+ const schema = typeof responseFormat.schema === "object" ? responseFormat.schema : {};
237
+ const sanitizedSchema = this.sanitizeJsonSchema(schema);
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
+ }
249
+ }
251
250
  args.push(promptText);
252
251
  const env = {
253
252
  ...process.env,
@@ -260,7 +259,34 @@ var CodexCliLanguageModel = class {
260
259
  lastMessagePath = join(dir, "last-message.txt");
261
260
  }
262
261
  args.push("--output-last-message", lastMessagePath);
263
- return { cmd: base.cmd, args, env, cwd: this.settings.cwd, lastMessagePath };
262
+ return { cmd: base.cmd, args, env, cwd: this.settings.cwd, lastMessagePath, schemaPath };
263
+ }
264
+ sanitizeJsonSchema(value) {
265
+ if (typeof value !== "object" || value === null) {
266
+ return value;
267
+ }
268
+ if (Array.isArray(value)) {
269
+ return value.map((item) => this.sanitizeJsonSchema(item));
270
+ }
271
+ const obj = value;
272
+ const result = {};
273
+ for (const [key, val] of Object.entries(obj)) {
274
+ if (key === "properties" && typeof val === "object" && val !== null && !Array.isArray(val)) {
275
+ const props = val;
276
+ const sanitizedProps = {};
277
+ for (const [propName, propSchema] of Object.entries(props)) {
278
+ sanitizedProps[propName] = this.sanitizeJsonSchema(propSchema);
279
+ }
280
+ result[key] = sanitizedProps;
281
+ continue;
282
+ }
283
+ if (key === "$schema" || key === "$id" || key === "$ref" || key === "$defs" || key === "definitions" || key === "title" || key === "examples" || key === "default" || key === "format" || // OpenAI strict mode doesn't support format
284
+ key === "pattern") {
285
+ continue;
286
+ }
287
+ result[key] = this.sanitizeJsonSchema(val);
288
+ }
289
+ return result;
264
290
  }
265
291
  mapWarnings(options) {
266
292
  const unsupported = [];
@@ -281,13 +307,185 @@ var CodexCliLanguageModel = class {
281
307
  add(options.seed, "seed");
282
308
  return unsupported;
283
309
  }
284
- parseJsonLine(line) {
310
+ parseExperimentalJsonEvent(line) {
285
311
  try {
286
312
  return JSON.parse(line);
287
313
  } catch {
288
314
  return void 0;
289
315
  }
290
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;
366
+ }
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);
441
+ } catch {
442
+ return "";
443
+ }
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
+ }
291
489
  handleSpawnError(err, promptExcerpt) {
292
490
  const e = err && typeof err === "object" ? err : void 0;
293
491
  const message = String((e?.message ?? err) || "Failed to run Codex CLI");
@@ -303,18 +501,17 @@ var CodexCliLanguageModel = class {
303
501
  });
304
502
  }
305
503
  async doGenerate(options) {
306
- const mode = options.responseFormat?.type === "json" ? { type: "object-json" } : { type: "regular" };
307
- const { promptText, warnings: mappingWarnings } = mapMessagesToPrompt(
308
- options.prompt,
309
- mode,
310
- options.responseFormat?.type === "json" ? options.responseFormat.schema : void 0
311
- );
504
+ const { promptText, warnings: mappingWarnings } = mapMessagesToPrompt(options.prompt);
312
505
  const promptExcerpt = promptText.slice(0, 200);
313
506
  const warnings = [
314
507
  ...this.mapWarnings(options),
315
508
  ...mappingWarnings?.map((m) => ({ type: "other", message: m })) || []
316
509
  ];
317
- const { cmd, args, env, cwd, lastMessagePath } = this.buildArgs(promptText);
510
+ const responseFormat = options.responseFormat?.type === "json" ? { type: "json", schema: options.responseFormat.schema } : void 0;
511
+ const { cmd, args, env, cwd, lastMessagePath, schemaPath } = this.buildArgs(
512
+ promptText,
513
+ responseFormat
514
+ );
318
515
  let text = "";
319
516
  const usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
320
517
  const finishReason = "stop";
@@ -331,27 +528,56 @@ var CodexCliLanguageModel = class {
331
528
  try {
332
529
  await new Promise((resolve, reject) => {
333
530
  let stderr = "";
531
+ let turnFailureMessage;
334
532
  child.stderr.on("data", (d) => stderr += String(d));
335
533
  child.stdout.setEncoding("utf8");
336
534
  child.stdout.on("data", (chunk) => {
337
535
  const lines = chunk.split(/\r?\n/).filter(Boolean);
338
536
  for (const line of lines) {
339
- const evt = this.parseJsonLine(line);
340
- if (!evt) continue;
341
- const msg = evt.msg;
342
- const type = msg?.type;
343
- if (type === "session_configured" && msg) {
344
- this.sessionId = msg.session_id;
345
- } else if (type === "task_complete" && msg) {
346
- const last = msg.last_agent_message;
347
- if (typeof last === "string") text = last;
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";
348
563
  }
349
564
  }
350
565
  });
351
566
  child.on("error", (e) => reject(this.handleSpawnError(e, promptExcerpt)));
352
567
  child.on("close", (code) => {
353
- if (code === 0) resolve();
354
- 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 {
355
581
  reject(
356
582
  createAPICallError({
357
583
  message: `Codex CLI exited with code ${code}`,
@@ -360,10 +586,18 @@ var CodexCliLanguageModel = class {
360
586
  promptExcerpt
361
587
  })
362
588
  );
589
+ }
363
590
  });
364
591
  });
365
592
  } finally {
366
593
  if (options.abortSignal && onAbort) options.abortSignal.removeEventListener("abort", onAbort);
594
+ if (schemaPath) {
595
+ try {
596
+ const schemaDir = dirname(schemaPath);
597
+ rmSync(schemaDir, { recursive: true, force: true });
598
+ } catch {
599
+ }
600
+ }
367
601
  }
368
602
  if (!text && lastMessagePath) {
369
603
  try {
@@ -378,9 +612,6 @@ var CodexCliLanguageModel = class {
378
612
  } catch {
379
613
  }
380
614
  }
381
- if (options.responseFormat?.type === "json" && text) {
382
- text = extractJson(text);
383
- }
384
615
  const content = [{ type: "text", text }];
385
616
  return {
386
617
  content,
@@ -395,24 +626,86 @@ var CodexCliLanguageModel = class {
395
626
  };
396
627
  }
397
628
  async doStream(options) {
398
- const mode = options.responseFormat?.type === "json" ? { type: "object-json" } : { type: "regular" };
399
- const { promptText, warnings: mappingWarnings } = mapMessagesToPrompt(
400
- options.prompt,
401
- mode,
402
- options.responseFormat?.type === "json" ? options.responseFormat.schema : void 0
403
- );
629
+ const { promptText, warnings: mappingWarnings } = mapMessagesToPrompt(options.prompt);
404
630
  const promptExcerpt = promptText.slice(0, 200);
405
631
  const warnings = [
406
632
  ...this.mapWarnings(options),
407
633
  ...mappingWarnings?.map((m) => ({ type: "other", message: m })) || []
408
634
  ];
409
- const { cmd, args, env, cwd, lastMessagePath } = this.buildArgs(promptText);
635
+ const responseFormat = options.responseFormat?.type === "json" ? { type: "json", schema: options.responseFormat.schema } : void 0;
636
+ const { cmd, args, env, cwd, lastMessagePath, schemaPath } = this.buildArgs(
637
+ promptText,
638
+ responseFormat
639
+ );
410
640
  const stream = new ReadableStream({
411
641
  start: (controller) => {
412
642
  const child = spawn(cmd, args, { env, cwd, stdio: ["ignore", "pipe", "pipe"] });
413
643
  controller.enqueue({ type: "stream-start", warnings });
414
644
  let stderr = "";
415
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
+ };
416
709
  const onAbort = () => {
417
710
  child.kill("SIGTERM");
418
711
  };
@@ -424,37 +717,7 @@ var CodexCliLanguageModel = class {
424
717
  }
425
718
  options.abortSignal.addEventListener("abort", onAbort, { once: true });
426
719
  }
427
- child.stderr.on("data", (d) => stderr += String(d));
428
- child.stdout.setEncoding("utf8");
429
- child.stdout.on("data", (chunk) => {
430
- const lines = chunk.split(/\r?\n/).filter(Boolean);
431
- for (const line of lines) {
432
- const evt = this.parseJsonLine(line);
433
- if (!evt) continue;
434
- const msg = evt.msg;
435
- const type = msg?.type;
436
- if (type === "session_configured" && msg) {
437
- this.sessionId = msg.session_id;
438
- controller.enqueue({
439
- type: "response-metadata",
440
- id: randomUUID(),
441
- timestamp: /* @__PURE__ */ new Date(),
442
- modelId: this.modelId
443
- });
444
- } else if (type === "task_complete" && msg) {
445
- const last = msg.last_agent_message;
446
- if (typeof last === "string") {
447
- accumulatedText = last;
448
- }
449
- }
450
- }
451
- });
452
- child.on("error", (e) => {
453
- if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort);
454
- controller.error(this.handleSpawnError(e, promptExcerpt));
455
- });
456
- child.on("close", (code) => {
457
- if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort);
720
+ const finishStream = (code) => {
458
721
  if (code !== 0) {
459
722
  controller.error(
460
723
  createAPICallError({
@@ -466,6 +729,16 @@ var CodexCliLanguageModel = class {
466
729
  );
467
730
  return;
468
731
  }
732
+ if (turnFailureMessage) {
733
+ controller.error(
734
+ createAPICallError({
735
+ message: turnFailureMessage,
736
+ stderr,
737
+ promptExcerpt
738
+ })
739
+ );
740
+ return;
741
+ }
469
742
  let finalText = accumulatedText;
470
743
  if (!finalText && lastMessagePath) {
471
744
  try {
@@ -479,17 +752,84 @@ var CodexCliLanguageModel = class {
479
752
  }
480
753
  }
481
754
  if (finalText) {
482
- if (options.responseFormat?.type === "json") {
483
- finalText = extractJson(finalText);
484
- }
485
- 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 });
486
759
  }
760
+ const usageSummary = lastUsage ?? { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
487
761
  controller.enqueue({
488
762
  type: "finish",
489
763
  finishReason: "stop",
490
- usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
764
+ usage: usageSummary
491
765
  });
492
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));
493
833
  });
494
834
  },
495
835
  cancel: () => {