ai-sdk-provider-codex-cli 0.1.0 → 0.2.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 +49 -12
- package/dist/index.cjs +114 -75
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +116 -77
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
# AI SDK Provider for Codex CLI
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/ai-sdk-provider-codex-cli)
|
|
4
|
+
[](https://www.npmjs.com/package/ai-sdk-provider-codex-cli)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+
[](https://github.com/ben-vargas/ai-sdk-provider-codex-cli/issues)
|
|
11
|
+
[](https://github.com/ben-vargas/ai-sdk-provider-codex-cli/releases/latest)
|
|
12
|
+
|
|
13
|
+
A community provider for Vercel AI SDK v5 that uses OpenAI’s Codex CLI (non‑interactive `codex exec`) to talk to GPT‑5 class models (`gpt-5` and the Codex-specific `gpt-5-codex` slug) with your ChatGPT Plus/Pro subscription. The provider spawns the Codex CLI process, parses its JSONL output, and adapts it to the AI SDK LanguageModelV2 interface.
|
|
14
|
+
|
|
15
|
+
- Works with `generateText`, `streamText`, and `generateObject` (native JSON Schema support via `--output-schema`)
|
|
6
16
|
- Uses ChatGPT OAuth from `codex login` (tokens in `~/.codex/auth.json`) or `OPENAI_API_KEY`
|
|
7
17
|
- Node-only (spawns a local process); supports CI and local dev
|
|
18
|
+
- **v0.2.0 Breaking Changes**: Switched to `--experimental-json` and native schema enforcement (see [CHANGELOG](CHANGELOG.md))
|
|
8
19
|
|
|
9
20
|
## Installation
|
|
10
21
|
|
|
@@ -15,6 +26,12 @@ npm i -g @openai/codex
|
|
|
15
26
|
codex login # or set OPENAI_API_KEY
|
|
16
27
|
```
|
|
17
28
|
|
|
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
|
+
>
|
|
31
|
+
> ```bash
|
|
32
|
+
> npm i -g @openai/codex@latest
|
|
33
|
+
> ```
|
|
34
|
+
|
|
18
35
|
2. Install provider and AI SDK
|
|
19
36
|
|
|
20
37
|
```bash
|
|
@@ -29,7 +46,7 @@ Text generation
|
|
|
29
46
|
import { generateText } from 'ai';
|
|
30
47
|
import { codexCli } from 'ai-sdk-provider-codex-cli';
|
|
31
48
|
|
|
32
|
-
const model = codexCli('gpt-5', {
|
|
49
|
+
const model = codexCli('gpt-5-codex', {
|
|
33
50
|
allowNpx: true,
|
|
34
51
|
skipGitRepoCheck: true,
|
|
35
52
|
approvalMode: 'on-failure',
|
|
@@ -49,8 +66,10 @@ Streaming
|
|
|
49
66
|
import { streamText } from 'ai';
|
|
50
67
|
import { codexCli } from 'ai-sdk-provider-codex-cli';
|
|
51
68
|
|
|
69
|
+
// The provider works with both `gpt-5` and `gpt-5-codex`; use the latter for
|
|
70
|
+
// the Codex CLI specific slug.
|
|
52
71
|
const { textStream } = await streamText({
|
|
53
|
-
model: codexCli('gpt-5', { allowNpx: true, skipGitRepoCheck: true }),
|
|
72
|
+
model: codexCli('gpt-5-codex', { allowNpx: true, skipGitRepoCheck: true }),
|
|
54
73
|
prompt: 'Write two short lines of encouragement.',
|
|
55
74
|
});
|
|
56
75
|
for await (const chunk of textStream) process.stdout.write(chunk);
|
|
@@ -65,7 +84,7 @@ import { codexCli } from 'ai-sdk-provider-codex-cli';
|
|
|
65
84
|
|
|
66
85
|
const schema = z.object({ name: z.string(), age: z.number().int() });
|
|
67
86
|
const { object } = await generateObject({
|
|
68
|
-
model: codexCli('gpt-5', { allowNpx: true, skipGitRepoCheck: true }),
|
|
87
|
+
model: codexCli('gpt-5-codex', { allowNpx: true, skipGitRepoCheck: true }),
|
|
69
88
|
schema,
|
|
70
89
|
prompt: 'Generate a small user profile.',
|
|
71
90
|
});
|
|
@@ -76,18 +95,26 @@ console.log(object);
|
|
|
76
95
|
|
|
77
96
|
- AI SDK v5 compatible (LanguageModelV2)
|
|
78
97
|
- Streaming and non‑streaming
|
|
79
|
-
- JSON
|
|
98
|
+
- **Native JSON Schema support** via `--output-schema` (API-enforced with `strict: true`)
|
|
99
|
+
- JSON object generation with Zod schemas (100-200 fewer tokens per request vs prompt engineering)
|
|
80
100
|
- Safe defaults for non‑interactive automation (`on-failure`, `workspace-write`, `--skip-git-repo-check`)
|
|
81
101
|
- Fallback to `npx @openai/codex` when not on PATH (`allowNpx`)
|
|
102
|
+
- Usage tracking from experimental JSON event format
|
|
82
103
|
|
|
83
104
|
### Streaming behavior
|
|
84
105
|
|
|
85
|
-
|
|
106
|
+
**Status:** Incremental streaming not currently supported with `--experimental-json` format (expected in future Codex CLI releases)
|
|
107
|
+
|
|
108
|
+
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
|
+
|
|
110
|
+
**What this means:**
|
|
111
|
+
- `streamText()` works functionally but delivers the entire response in a single chunk after generation completes
|
|
112
|
+
- No incremental text deltas—you wait for the full response, then receive it all at once
|
|
113
|
+
- The AI SDK's streaming interface is supported, but actual incremental streaming is not available
|
|
86
114
|
|
|
87
|
-
|
|
88
|
-
- returns the final text as a single `text-delta` right before `finish`.
|
|
115
|
+
**Future support:** The Codex CLI commit (344d4a1d) introducing experimental JSON explicitly notes: "or other item types like `item.output_delta` when we need streaming" and states "more event types and item types to come."
|
|
89
116
|
|
|
90
|
-
|
|
117
|
+
When OpenAI adds streaming support, this provider will be updated to handle those events and enable true incremental streaming.
|
|
91
118
|
|
|
92
119
|
## Documentation
|
|
93
120
|
|
|
@@ -125,9 +152,19 @@ See [docs/ai-sdk-v5/configuration.md](docs/ai-sdk-v5/configuration.md) for the f
|
|
|
125
152
|
## Limitations
|
|
126
153
|
|
|
127
154
|
- Node ≥ 18, local process only (no Edge)
|
|
128
|
-
- Codex
|
|
155
|
+
- Codex `--experimental-json` mode emits events rather than streaming deltas; streaming typically yields a final chunk. The CLI provides the final assistant text in the `item.completed` event, which this provider reads and emits at the end.
|
|
129
156
|
- Some AI SDK parameters are unsupported by Codex CLI (e.g., temperature/topP/penalties); the provider surfaces warnings and ignores them
|
|
130
157
|
|
|
158
|
+
### JSON Schema Limitations (v0.2.0+)
|
|
159
|
+
|
|
160
|
+
**⚠️ Important:** OpenAI strict mode has limitations:
|
|
161
|
+
|
|
162
|
+
- **Optional fields NOT supported**: All fields must be required (no `.optional()`)
|
|
163
|
+
- **Format validators stripped**: `.email()`, `.url()`, `.uuid()` are removed (use descriptions instead)
|
|
164
|
+
- **Pattern validators stripped**: `.regex()` is removed (use descriptions instead)
|
|
165
|
+
|
|
166
|
+
See [LIMITATIONS.md](LIMITATIONS.md) for comprehensive details and migration guidance.
|
|
167
|
+
|
|
131
168
|
## Disclaimer
|
|
132
169
|
|
|
133
170
|
This is a community provider and not an official OpenAI or Vercel product. You are responsible for complying with all applicable terms and ensuring safe usage.
|
package/dist/index.cjs
CHANGED
|
@@ -13,22 +13,6 @@ var zod = require('zod');
|
|
|
13
13
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
14
14
|
// src/codex-cli-provider.ts
|
|
15
15
|
|
|
16
|
-
// src/extract-json.ts
|
|
17
|
-
function extractJson(text) {
|
|
18
|
-
const start = text.indexOf("{");
|
|
19
|
-
if (start === -1) return text;
|
|
20
|
-
let depth = 0;
|
|
21
|
-
for (let i = start; i < text.length; i++) {
|
|
22
|
-
const ch = text[i];
|
|
23
|
-
if (ch === "{") depth++;
|
|
24
|
-
else if (ch === "}") {
|
|
25
|
-
depth--;
|
|
26
|
-
if (depth === 0) return text.slice(start, i + 1);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return text;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
16
|
// src/logger.ts
|
|
33
17
|
var defaultLogger = {
|
|
34
18
|
warn: (m) => console.warn(m),
|
|
@@ -108,7 +92,7 @@ function isToolItem(p) {
|
|
|
108
92
|
if (out.type === "text" && typeof out.value !== "string") return false;
|
|
109
93
|
return true;
|
|
110
94
|
}
|
|
111
|
-
function mapMessagesToPrompt(prompt
|
|
95
|
+
function mapMessagesToPrompt(prompt) {
|
|
112
96
|
const warnings = [];
|
|
113
97
|
const parts = [];
|
|
114
98
|
let systemText;
|
|
@@ -151,17 +135,6 @@ function mapMessagesToPrompt(prompt, mode = { type: "regular" }, jsonSchema) {
|
|
|
151
135
|
let promptText = "";
|
|
152
136
|
if (systemText) promptText += systemText + "\n\n";
|
|
153
137
|
promptText += parts.join("\n\n");
|
|
154
|
-
if (mode.type === "object-json" && jsonSchema) {
|
|
155
|
-
const schemaStr = JSON.stringify(jsonSchema, null, 2);
|
|
156
|
-
promptText = `CRITICAL: You MUST respond with ONLY a JSON object. NO other text.
|
|
157
|
-
Your response MUST start with { and end with }
|
|
158
|
-
The JSON MUST match this EXACT schema:
|
|
159
|
-
${schemaStr}
|
|
160
|
-
|
|
161
|
-
Now, based on the following conversation, generate ONLY the JSON object:
|
|
162
|
-
|
|
163
|
-
${promptText}`;
|
|
164
|
-
}
|
|
165
138
|
return { promptText, ...warnings.length ? { warnings } : {} };
|
|
166
139
|
}
|
|
167
140
|
function createAPICallError({
|
|
@@ -214,7 +187,7 @@ var CodexCliLanguageModel = class {
|
|
|
214
187
|
defaultObjectGenerationMode = "json";
|
|
215
188
|
supportsImageUrls = false;
|
|
216
189
|
supportedUrls = {};
|
|
217
|
-
supportsStructuredOutputs =
|
|
190
|
+
supportsStructuredOutputs = true;
|
|
218
191
|
modelId;
|
|
219
192
|
settings;
|
|
220
193
|
logger;
|
|
@@ -229,9 +202,9 @@ var CodexCliLanguageModel = class {
|
|
|
229
202
|
const warn = validateModelId(this.modelId);
|
|
230
203
|
if (warn) this.logger.warn(`Codex CLI model: ${warn}`);
|
|
231
204
|
}
|
|
232
|
-
buildArgs(promptText) {
|
|
205
|
+
buildArgs(promptText, responseFormat) {
|
|
233
206
|
const base = resolveCodexPath(this.settings.codexPath, this.settings.allowNpx);
|
|
234
|
-
const args = [...base.args, "exec", "--json"];
|
|
207
|
+
const args = [...base.args, "exec", "--experimental-json"];
|
|
235
208
|
if (this.settings.fullAuto) {
|
|
236
209
|
args.push("--full-auto");
|
|
237
210
|
} else if (this.settings.dangerouslyBypassApprovalsAndSandbox) {
|
|
@@ -251,6 +224,20 @@ var CodexCliLanguageModel = class {
|
|
|
251
224
|
if (this.modelId) {
|
|
252
225
|
args.push("-m", this.modelId);
|
|
253
226
|
}
|
|
227
|
+
let schemaPath;
|
|
228
|
+
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
|
+
const schema = typeof responseFormat.schema === "object" ? responseFormat.schema : {};
|
|
232
|
+
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);
|
|
240
|
+
}
|
|
254
241
|
args.push(promptText);
|
|
255
242
|
const env = {
|
|
256
243
|
...process.env,
|
|
@@ -263,7 +250,34 @@ var CodexCliLanguageModel = class {
|
|
|
263
250
|
lastMessagePath = path.join(dir, "last-message.txt");
|
|
264
251
|
}
|
|
265
252
|
args.push("--output-last-message", lastMessagePath);
|
|
266
|
-
return { cmd: base.cmd, args, env, cwd: this.settings.cwd, lastMessagePath };
|
|
253
|
+
return { cmd: base.cmd, args, env, cwd: this.settings.cwd, lastMessagePath, schemaPath };
|
|
254
|
+
}
|
|
255
|
+
sanitizeJsonSchema(value) {
|
|
256
|
+
if (typeof value !== "object" || value === null) {
|
|
257
|
+
return value;
|
|
258
|
+
}
|
|
259
|
+
if (Array.isArray(value)) {
|
|
260
|
+
return value.map((item) => this.sanitizeJsonSchema(item));
|
|
261
|
+
}
|
|
262
|
+
const obj = value;
|
|
263
|
+
const result = {};
|
|
264
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
265
|
+
if (key === "properties" && typeof val === "object" && val !== null && !Array.isArray(val)) {
|
|
266
|
+
const props = val;
|
|
267
|
+
const sanitizedProps = {};
|
|
268
|
+
for (const [propName, propSchema] of Object.entries(props)) {
|
|
269
|
+
sanitizedProps[propName] = this.sanitizeJsonSchema(propSchema);
|
|
270
|
+
}
|
|
271
|
+
result[key] = sanitizedProps;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
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
|
|
275
|
+
key === "pattern") {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
result[key] = this.sanitizeJsonSchema(val);
|
|
279
|
+
}
|
|
280
|
+
return result;
|
|
267
281
|
}
|
|
268
282
|
mapWarnings(options) {
|
|
269
283
|
const unsupported = [];
|
|
@@ -284,11 +298,34 @@ var CodexCliLanguageModel = class {
|
|
|
284
298
|
add(options.seed, "seed");
|
|
285
299
|
return unsupported;
|
|
286
300
|
}
|
|
287
|
-
|
|
301
|
+
parseExperimentalJsonEvent(line) {
|
|
288
302
|
try {
|
|
289
|
-
|
|
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;
|
|
325
|
+
}
|
|
326
|
+
return result;
|
|
290
327
|
} catch {
|
|
291
|
-
return
|
|
328
|
+
return {};
|
|
292
329
|
}
|
|
293
330
|
}
|
|
294
331
|
handleSpawnError(err, promptExcerpt) {
|
|
@@ -306,18 +343,17 @@ var CodexCliLanguageModel = class {
|
|
|
306
343
|
});
|
|
307
344
|
}
|
|
308
345
|
async doGenerate(options) {
|
|
309
|
-
const
|
|
310
|
-
const { promptText, warnings: mappingWarnings } = mapMessagesToPrompt(
|
|
311
|
-
options.prompt,
|
|
312
|
-
mode,
|
|
313
|
-
options.responseFormat?.type === "json" ? options.responseFormat.schema : void 0
|
|
314
|
-
);
|
|
346
|
+
const { promptText, warnings: mappingWarnings } = mapMessagesToPrompt(options.prompt);
|
|
315
347
|
const promptExcerpt = promptText.slice(0, 200);
|
|
316
348
|
const warnings = [
|
|
317
349
|
...this.mapWarnings(options),
|
|
318
350
|
...mappingWarnings?.map((m) => ({ type: "other", message: m })) || []
|
|
319
351
|
];
|
|
320
|
-
const {
|
|
352
|
+
const responseFormat = options.responseFormat?.type === "json" ? { type: "json", schema: options.responseFormat.schema } : void 0;
|
|
353
|
+
const { cmd, args, env, cwd, lastMessagePath, schemaPath } = this.buildArgs(
|
|
354
|
+
promptText,
|
|
355
|
+
responseFormat
|
|
356
|
+
);
|
|
321
357
|
let text = "";
|
|
322
358
|
const usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
323
359
|
const finishReason = "stop";
|
|
@@ -339,15 +375,13 @@ var CodexCliLanguageModel = class {
|
|
|
339
375
|
child.stdout.on("data", (chunk) => {
|
|
340
376
|
const lines = chunk.split(/\r?\n/).filter(Boolean);
|
|
341
377
|
for (const line of lines) {
|
|
342
|
-
const
|
|
343
|
-
if (
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const last = msg.last_agent_message;
|
|
350
|
-
if (typeof last === "string") text = last;
|
|
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;
|
|
351
385
|
}
|
|
352
386
|
}
|
|
353
387
|
});
|
|
@@ -367,6 +401,13 @@ var CodexCliLanguageModel = class {
|
|
|
367
401
|
});
|
|
368
402
|
} finally {
|
|
369
403
|
if (options.abortSignal && onAbort) options.abortSignal.removeEventListener("abort", onAbort);
|
|
404
|
+
if (schemaPath) {
|
|
405
|
+
try {
|
|
406
|
+
const schemaDir = path.dirname(schemaPath);
|
|
407
|
+
fs.rmSync(schemaDir, { recursive: true, force: true });
|
|
408
|
+
} catch {
|
|
409
|
+
}
|
|
410
|
+
}
|
|
370
411
|
}
|
|
371
412
|
if (!text && lastMessagePath) {
|
|
372
413
|
try {
|
|
@@ -381,9 +422,6 @@ var CodexCliLanguageModel = class {
|
|
|
381
422
|
} catch {
|
|
382
423
|
}
|
|
383
424
|
}
|
|
384
|
-
if (options.responseFormat?.type === "json" && text) {
|
|
385
|
-
text = extractJson(text);
|
|
386
|
-
}
|
|
387
425
|
const content = [{ type: "text", text }];
|
|
388
426
|
return {
|
|
389
427
|
content,
|
|
@@ -398,18 +436,17 @@ var CodexCliLanguageModel = class {
|
|
|
398
436
|
};
|
|
399
437
|
}
|
|
400
438
|
async doStream(options) {
|
|
401
|
-
const
|
|
402
|
-
const { promptText, warnings: mappingWarnings } = mapMessagesToPrompt(
|
|
403
|
-
options.prompt,
|
|
404
|
-
mode,
|
|
405
|
-
options.responseFormat?.type === "json" ? options.responseFormat.schema : void 0
|
|
406
|
-
);
|
|
439
|
+
const { promptText, warnings: mappingWarnings } = mapMessagesToPrompt(options.prompt);
|
|
407
440
|
const promptExcerpt = promptText.slice(0, 200);
|
|
408
441
|
const warnings = [
|
|
409
442
|
...this.mapWarnings(options),
|
|
410
443
|
...mappingWarnings?.map((m) => ({ type: "other", message: m })) || []
|
|
411
444
|
];
|
|
412
|
-
const {
|
|
445
|
+
const responseFormat = options.responseFormat?.type === "json" ? { type: "json", schema: options.responseFormat.schema } : void 0;
|
|
446
|
+
const { cmd, args, env, cwd, lastMessagePath, schemaPath } = this.buildArgs(
|
|
447
|
+
promptText,
|
|
448
|
+
responseFormat
|
|
449
|
+
);
|
|
413
450
|
const stream = new ReadableStream({
|
|
414
451
|
start: (controller) => {
|
|
415
452
|
const child = child_process.spawn(cmd, args, { env, cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -432,32 +469,37 @@ var CodexCliLanguageModel = class {
|
|
|
432
469
|
child.stdout.on("data", (chunk) => {
|
|
433
470
|
const lines = chunk.split(/\r?\n/).filter(Boolean);
|
|
434
471
|
for (const line of lines) {
|
|
435
|
-
const
|
|
436
|
-
if (
|
|
437
|
-
|
|
438
|
-
const type = msg?.type;
|
|
439
|
-
if (type === "session_configured" && msg) {
|
|
440
|
-
this.sessionId = msg.session_id;
|
|
472
|
+
const parsed = this.parseExperimentalJsonEvent(line);
|
|
473
|
+
if (parsed.sessionId) {
|
|
474
|
+
this.sessionId = parsed.sessionId;
|
|
441
475
|
controller.enqueue({
|
|
442
476
|
type: "response-metadata",
|
|
443
477
|
id: crypto.randomUUID(),
|
|
444
478
|
timestamp: /* @__PURE__ */ new Date(),
|
|
445
479
|
modelId: this.modelId
|
|
446
480
|
});
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
accumulatedText = last;
|
|
451
|
-
}
|
|
481
|
+
}
|
|
482
|
+
if (parsed.text) {
|
|
483
|
+
accumulatedText = parsed.text;
|
|
452
484
|
}
|
|
453
485
|
}
|
|
454
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
|
+
};
|
|
455
495
|
child.on("error", (e) => {
|
|
456
496
|
if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort);
|
|
497
|
+
cleanupSchema();
|
|
457
498
|
controller.error(this.handleSpawnError(e, promptExcerpt));
|
|
458
499
|
});
|
|
459
500
|
child.on("close", (code) => {
|
|
460
501
|
if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort);
|
|
502
|
+
cleanupSchema();
|
|
461
503
|
if (code !== 0) {
|
|
462
504
|
controller.error(
|
|
463
505
|
createAPICallError({
|
|
@@ -482,9 +524,6 @@ var CodexCliLanguageModel = class {
|
|
|
482
524
|
}
|
|
483
525
|
}
|
|
484
526
|
if (finalText) {
|
|
485
|
-
if (options.responseFormat?.type === "json") {
|
|
486
|
-
finalText = extractJson(finalText);
|
|
487
|
-
}
|
|
488
527
|
controller.enqueue({ type: "text-delta", id: crypto.randomUUID(), delta: finalText });
|
|
489
528
|
}
|
|
490
529
|
controller.enqueue({
|
package/dist/index.d.cts
CHANGED
|
@@ -45,15 +45,16 @@ declare class CodexCliLanguageModel implements LanguageModelV2 {
|
|
|
45
45
|
readonly defaultObjectGenerationMode: "json";
|
|
46
46
|
readonly supportsImageUrls = false;
|
|
47
47
|
readonly supportedUrls: {};
|
|
48
|
-
readonly supportsStructuredOutputs =
|
|
48
|
+
readonly supportsStructuredOutputs = true;
|
|
49
49
|
readonly modelId: string;
|
|
50
50
|
readonly settings: CodexCliSettings;
|
|
51
51
|
private logger;
|
|
52
52
|
private sessionId?;
|
|
53
53
|
constructor(options: CodexLanguageModelOptions);
|
|
54
54
|
private buildArgs;
|
|
55
|
+
private sanitizeJsonSchema;
|
|
55
56
|
private mapWarnings;
|
|
56
|
-
private
|
|
57
|
+
private parseExperimentalJsonEvent;
|
|
57
58
|
private handleSpawnError;
|
|
58
59
|
doGenerate(options: Parameters<LanguageModelV2['doGenerate']>[0]): Promise<Awaited<ReturnType<LanguageModelV2['doGenerate']>>>;
|
|
59
60
|
doStream(options: Parameters<LanguageModelV2['doStream']>[0]): Promise<Awaited<ReturnType<LanguageModelV2['doStream']>>>;
|
package/dist/index.d.ts
CHANGED
|
@@ -45,15 +45,16 @@ declare class CodexCliLanguageModel implements LanguageModelV2 {
|
|
|
45
45
|
readonly defaultObjectGenerationMode: "json";
|
|
46
46
|
readonly supportsImageUrls = false;
|
|
47
47
|
readonly supportedUrls: {};
|
|
48
|
-
readonly supportsStructuredOutputs =
|
|
48
|
+
readonly supportsStructuredOutputs = true;
|
|
49
49
|
readonly modelId: string;
|
|
50
50
|
readonly settings: CodexCliSettings;
|
|
51
51
|
private logger;
|
|
52
52
|
private sessionId?;
|
|
53
53
|
constructor(options: CodexLanguageModelOptions);
|
|
54
54
|
private buildArgs;
|
|
55
|
+
private sanitizeJsonSchema;
|
|
55
56
|
private mapWarnings;
|
|
56
|
-
private
|
|
57
|
+
private parseExperimentalJsonEvent;
|
|
57
58
|
private handleSpawnError;
|
|
58
59
|
doGenerate(options: Parameters<LanguageModelV2['doGenerate']>[0]): Promise<Awaited<ReturnType<LanguageModelV2['doGenerate']>>>;
|
|
59
60
|
doStream(options: Parameters<LanguageModelV2['doStream']>[0]): Promise<Awaited<ReturnType<LanguageModelV2['doStream']>>>;
|
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,
|
|
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
|
|
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 =
|
|
187
|
+
supportsStructuredOutputs = true;
|
|
215
188
|
modelId;
|
|
216
189
|
settings;
|
|
217
190
|
logger;
|
|
@@ -226,9 +199,9 @@ 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
|
+
buildArgs(promptText, responseFormat) {
|
|
230
203
|
const base = resolveCodexPath(this.settings.codexPath, this.settings.allowNpx);
|
|
231
|
-
const args = [...base.args, "exec", "--json"];
|
|
204
|
+
const args = [...base.args, "exec", "--experimental-json"];
|
|
232
205
|
if (this.settings.fullAuto) {
|
|
233
206
|
args.push("--full-auto");
|
|
234
207
|
} else if (this.settings.dangerouslyBypassApprovalsAndSandbox) {
|
|
@@ -248,6 +221,20 @@ var CodexCliLanguageModel = class {
|
|
|
248
221
|
if (this.modelId) {
|
|
249
222
|
args.push("-m", this.modelId);
|
|
250
223
|
}
|
|
224
|
+
let schemaPath;
|
|
225
|
+
if (responseFormat?.type === "json" && responseFormat.schema) {
|
|
226
|
+
const dir = mkdtempSync(join(tmpdir(), "codex-schema-"));
|
|
227
|
+
schemaPath = join(dir, "schema.json");
|
|
228
|
+
const schema = typeof responseFormat.schema === "object" ? responseFormat.schema : {};
|
|
229
|
+
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);
|
|
237
|
+
}
|
|
251
238
|
args.push(promptText);
|
|
252
239
|
const env = {
|
|
253
240
|
...process.env,
|
|
@@ -260,7 +247,34 @@ var CodexCliLanguageModel = class {
|
|
|
260
247
|
lastMessagePath = join(dir, "last-message.txt");
|
|
261
248
|
}
|
|
262
249
|
args.push("--output-last-message", lastMessagePath);
|
|
263
|
-
return { cmd: base.cmd, args, env, cwd: this.settings.cwd, lastMessagePath };
|
|
250
|
+
return { cmd: base.cmd, args, env, cwd: this.settings.cwd, lastMessagePath, schemaPath };
|
|
251
|
+
}
|
|
252
|
+
sanitizeJsonSchema(value) {
|
|
253
|
+
if (typeof value !== "object" || value === null) {
|
|
254
|
+
return value;
|
|
255
|
+
}
|
|
256
|
+
if (Array.isArray(value)) {
|
|
257
|
+
return value.map((item) => this.sanitizeJsonSchema(item));
|
|
258
|
+
}
|
|
259
|
+
const obj = value;
|
|
260
|
+
const result = {};
|
|
261
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
262
|
+
if (key === "properties" && typeof val === "object" && val !== null && !Array.isArray(val)) {
|
|
263
|
+
const props = val;
|
|
264
|
+
const sanitizedProps = {};
|
|
265
|
+
for (const [propName, propSchema] of Object.entries(props)) {
|
|
266
|
+
sanitizedProps[propName] = this.sanitizeJsonSchema(propSchema);
|
|
267
|
+
}
|
|
268
|
+
result[key] = sanitizedProps;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
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
|
|
272
|
+
key === "pattern") {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
result[key] = this.sanitizeJsonSchema(val);
|
|
276
|
+
}
|
|
277
|
+
return result;
|
|
264
278
|
}
|
|
265
279
|
mapWarnings(options) {
|
|
266
280
|
const unsupported = [];
|
|
@@ -281,11 +295,34 @@ var CodexCliLanguageModel = class {
|
|
|
281
295
|
add(options.seed, "seed");
|
|
282
296
|
return unsupported;
|
|
283
297
|
}
|
|
284
|
-
|
|
298
|
+
parseExperimentalJsonEvent(line) {
|
|
285
299
|
try {
|
|
286
|
-
|
|
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;
|
|
322
|
+
}
|
|
323
|
+
return result;
|
|
287
324
|
} catch {
|
|
288
|
-
return
|
|
325
|
+
return {};
|
|
289
326
|
}
|
|
290
327
|
}
|
|
291
328
|
handleSpawnError(err, promptExcerpt) {
|
|
@@ -303,18 +340,17 @@ var CodexCliLanguageModel = class {
|
|
|
303
340
|
});
|
|
304
341
|
}
|
|
305
342
|
async doGenerate(options) {
|
|
306
|
-
const
|
|
307
|
-
const { promptText, warnings: mappingWarnings } = mapMessagesToPrompt(
|
|
308
|
-
options.prompt,
|
|
309
|
-
mode,
|
|
310
|
-
options.responseFormat?.type === "json" ? options.responseFormat.schema : void 0
|
|
311
|
-
);
|
|
343
|
+
const { promptText, warnings: mappingWarnings } = mapMessagesToPrompt(options.prompt);
|
|
312
344
|
const promptExcerpt = promptText.slice(0, 200);
|
|
313
345
|
const warnings = [
|
|
314
346
|
...this.mapWarnings(options),
|
|
315
347
|
...mappingWarnings?.map((m) => ({ type: "other", message: m })) || []
|
|
316
348
|
];
|
|
317
|
-
const {
|
|
349
|
+
const responseFormat = options.responseFormat?.type === "json" ? { type: "json", schema: options.responseFormat.schema } : void 0;
|
|
350
|
+
const { cmd, args, env, cwd, lastMessagePath, schemaPath } = this.buildArgs(
|
|
351
|
+
promptText,
|
|
352
|
+
responseFormat
|
|
353
|
+
);
|
|
318
354
|
let text = "";
|
|
319
355
|
const usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
320
356
|
const finishReason = "stop";
|
|
@@ -336,15 +372,13 @@ var CodexCliLanguageModel = class {
|
|
|
336
372
|
child.stdout.on("data", (chunk) => {
|
|
337
373
|
const lines = chunk.split(/\r?\n/).filter(Boolean);
|
|
338
374
|
for (const line of lines) {
|
|
339
|
-
const
|
|
340
|
-
if (
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const last = msg.last_agent_message;
|
|
347
|
-
if (typeof last === "string") text = last;
|
|
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;
|
|
348
382
|
}
|
|
349
383
|
}
|
|
350
384
|
});
|
|
@@ -364,6 +398,13 @@ var CodexCliLanguageModel = class {
|
|
|
364
398
|
});
|
|
365
399
|
} finally {
|
|
366
400
|
if (options.abortSignal && onAbort) options.abortSignal.removeEventListener("abort", onAbort);
|
|
401
|
+
if (schemaPath) {
|
|
402
|
+
try {
|
|
403
|
+
const schemaDir = dirname(schemaPath);
|
|
404
|
+
rmSync(schemaDir, { recursive: true, force: true });
|
|
405
|
+
} catch {
|
|
406
|
+
}
|
|
407
|
+
}
|
|
367
408
|
}
|
|
368
409
|
if (!text && lastMessagePath) {
|
|
369
410
|
try {
|
|
@@ -378,9 +419,6 @@ var CodexCliLanguageModel = class {
|
|
|
378
419
|
} catch {
|
|
379
420
|
}
|
|
380
421
|
}
|
|
381
|
-
if (options.responseFormat?.type === "json" && text) {
|
|
382
|
-
text = extractJson(text);
|
|
383
|
-
}
|
|
384
422
|
const content = [{ type: "text", text }];
|
|
385
423
|
return {
|
|
386
424
|
content,
|
|
@@ -395,18 +433,17 @@ var CodexCliLanguageModel = class {
|
|
|
395
433
|
};
|
|
396
434
|
}
|
|
397
435
|
async doStream(options) {
|
|
398
|
-
const
|
|
399
|
-
const { promptText, warnings: mappingWarnings } = mapMessagesToPrompt(
|
|
400
|
-
options.prompt,
|
|
401
|
-
mode,
|
|
402
|
-
options.responseFormat?.type === "json" ? options.responseFormat.schema : void 0
|
|
403
|
-
);
|
|
436
|
+
const { promptText, warnings: mappingWarnings } = mapMessagesToPrompt(options.prompt);
|
|
404
437
|
const promptExcerpt = promptText.slice(0, 200);
|
|
405
438
|
const warnings = [
|
|
406
439
|
...this.mapWarnings(options),
|
|
407
440
|
...mappingWarnings?.map((m) => ({ type: "other", message: m })) || []
|
|
408
441
|
];
|
|
409
|
-
const {
|
|
442
|
+
const responseFormat = options.responseFormat?.type === "json" ? { type: "json", schema: options.responseFormat.schema } : void 0;
|
|
443
|
+
const { cmd, args, env, cwd, lastMessagePath, schemaPath } = this.buildArgs(
|
|
444
|
+
promptText,
|
|
445
|
+
responseFormat
|
|
446
|
+
);
|
|
410
447
|
const stream = new ReadableStream({
|
|
411
448
|
start: (controller) => {
|
|
412
449
|
const child = spawn(cmd, args, { env, cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -429,32 +466,37 @@ var CodexCliLanguageModel = class {
|
|
|
429
466
|
child.stdout.on("data", (chunk) => {
|
|
430
467
|
const lines = chunk.split(/\r?\n/).filter(Boolean);
|
|
431
468
|
for (const line of lines) {
|
|
432
|
-
const
|
|
433
|
-
if (
|
|
434
|
-
|
|
435
|
-
const type = msg?.type;
|
|
436
|
-
if (type === "session_configured" && msg) {
|
|
437
|
-
this.sessionId = msg.session_id;
|
|
469
|
+
const parsed = this.parseExperimentalJsonEvent(line);
|
|
470
|
+
if (parsed.sessionId) {
|
|
471
|
+
this.sessionId = parsed.sessionId;
|
|
438
472
|
controller.enqueue({
|
|
439
473
|
type: "response-metadata",
|
|
440
474
|
id: randomUUID(),
|
|
441
475
|
timestamp: /* @__PURE__ */ new Date(),
|
|
442
476
|
modelId: this.modelId
|
|
443
477
|
});
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
accumulatedText = last;
|
|
448
|
-
}
|
|
478
|
+
}
|
|
479
|
+
if (parsed.text) {
|
|
480
|
+
accumulatedText = parsed.text;
|
|
449
481
|
}
|
|
450
482
|
}
|
|
451
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
|
+
};
|
|
452
492
|
child.on("error", (e) => {
|
|
453
493
|
if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort);
|
|
494
|
+
cleanupSchema();
|
|
454
495
|
controller.error(this.handleSpawnError(e, promptExcerpt));
|
|
455
496
|
});
|
|
456
497
|
child.on("close", (code) => {
|
|
457
498
|
if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort);
|
|
499
|
+
cleanupSchema();
|
|
458
500
|
if (code !== 0) {
|
|
459
501
|
controller.error(
|
|
460
502
|
createAPICallError({
|
|
@@ -479,9 +521,6 @@ var CodexCliLanguageModel = class {
|
|
|
479
521
|
}
|
|
480
522
|
}
|
|
481
523
|
if (finalText) {
|
|
482
|
-
if (options.responseFormat?.type === "json") {
|
|
483
|
-
finalText = extractJson(finalText);
|
|
484
|
-
}
|
|
485
524
|
controller.enqueue({ type: "text-delta", id: randomUUID(), delta: finalText });
|
|
486
525
|
}
|
|
487
526
|
controller.enqueue({
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-sdk-provider-codex-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "AI SDK v5 provider for
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "AI SDK v5 provider for OpenAI Codex CLI with native JSON Schema support",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-sdk",
|
|
7
7
|
"codex",
|