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 +37 -2
- package/dist/index.cjs +381 -80
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +381 -80
- package/package.json +2 -2
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
|
|
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
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
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
|
|
379
|
-
if (
|
|
380
|
-
if (
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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)
|
|
391
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
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
|
|
376
|
-
if (
|
|
377
|
-
if (
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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)
|
|
388
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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",
|