ai-sdk-provider-codex-cli 0.6.0 → 1.0.0-beta.1
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 +69 -10
- package/dist/index.cjs +178 -50
- package/dist/index.d.cts +10 -10
- package/dist/index.d.ts +10 -10
- package/dist/index.js +178 -50
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -4,23 +4,34 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/ai-sdk-provider-codex-cli)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|

|
|
7
|
-

|
|
8
8
|

|
|
9
9
|

|
|
10
10
|
[](https://github.com/ben-vargas/ai-sdk-provider-codex-cli/issues)
|
|
11
11
|
[](https://github.com/ben-vargas/ai-sdk-provider-codex-cli/releases/latest)
|
|
12
12
|
|
|
13
|
-
A community provider for Vercel AI SDK
|
|
13
|
+
A community provider for Vercel AI SDK v6 (beta) that uses OpenAI's Codex CLI (non‑interactive `codex exec`) to talk to GPT‑5.1 class models (`gpt-5.1`, the Codex-specific `gpt-5.1-codex`, the flagship `gpt-5.1-codex-max`, and the lightweight `gpt-5.1-codex-mini` slugs) with your ChatGPT Plus/Pro subscription. The provider spawns the Codex CLI process, parses its JSONL output, and adapts it to the AI SDK LanguageModelV3 interface. Legacy GPT-5 / GPT-5-codex slugs remain compatible for existing workflows.
|
|
14
|
+
|
|
15
|
+
> **⚠️ AI SDK v6 Beta**: This version (`1.0.0-beta.x`) requires AI SDK v6 beta packages. For AI SDK v5, use the `0.x` releases from the main branch.
|
|
14
16
|
|
|
15
17
|
- Works with `generateText`, `streamText`, and `generateObject` (native JSON Schema support via `--output-schema`)
|
|
16
18
|
- Uses ChatGPT OAuth from `codex login` (tokens in `~/.codex/auth.json`) or `OPENAI_API_KEY`
|
|
17
19
|
- Node-only (spawns a local process); supports CI and local dev
|
|
20
|
+
- **v1.0.0-beta.1**: AI SDK v6 migration with LanguageModelV3 interface
|
|
18
21
|
- **v0.5.0**: Adds comprehensive logging system with verbose mode and custom logger support
|
|
19
22
|
- **v0.3.0**: Adds comprehensive tool streaming support for monitoring autonomous tool execution
|
|
20
|
-
|
|
23
|
+
|
|
24
|
+
## Version Compatibility
|
|
25
|
+
|
|
26
|
+
| Provider Version | AI SDK Version | NPM Tag | Status | Branch |
|
|
27
|
+
| ---------------- | -------------- | --------- | ------ | ------------ |
|
|
28
|
+
| 1.x.x-beta | v6 (beta) | `beta` | Beta | `ai-sdk-v6` |
|
|
29
|
+
| 0.x.x | v5 | `latest` | Stable | `main` |
|
|
21
30
|
|
|
22
31
|
## Installation
|
|
23
32
|
|
|
33
|
+
### For AI SDK v6 (beta)
|
|
34
|
+
|
|
24
35
|
1. Install and authenticate Codex CLI
|
|
25
36
|
|
|
26
37
|
```bash
|
|
@@ -28,18 +39,24 @@ npm i -g @openai/codex
|
|
|
28
39
|
codex login # or set OPENAI_API_KEY
|
|
29
40
|
```
|
|
30
41
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
2. Install provider and AI SDK v6 beta
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm i ai@beta ai-sdk-provider-codex-cli@beta
|
|
46
|
+
```
|
|
36
47
|
|
|
37
|
-
|
|
48
|
+
### For AI SDK v5 (stable)
|
|
38
49
|
|
|
39
50
|
```bash
|
|
40
51
|
npm i ai ai-sdk-provider-codex-cli
|
|
41
52
|
```
|
|
42
53
|
|
|
54
|
+
> **⚠️ Codex CLI Version**: Requires Codex CLI **>= 0.42.0** for `--experimental-json` and `--output-schema` support. **>= 0.60.0 recommended** for `gpt-5.1-codex-max` and `xhigh` reasoning effort. If you supply your own Codex CLI (global install or custom `codexPath`/`allowNpx`), check it with `codex --version` and upgrade if needed. The optional dependency `@openai/codex` in this package pulls a compatible version automatically.
|
|
55
|
+
>
|
|
56
|
+
> ```bash
|
|
57
|
+
> npm i -g @openai/codex@latest
|
|
58
|
+
> ```
|
|
59
|
+
|
|
43
60
|
## Quick Start
|
|
44
61
|
|
|
45
62
|
Text generation
|
|
@@ -95,7 +112,7 @@ console.log(object);
|
|
|
95
112
|
|
|
96
113
|
## Features
|
|
97
114
|
|
|
98
|
-
- AI SDK
|
|
115
|
+
- AI SDK v6 compatible (LanguageModelV3)
|
|
99
116
|
- Streaming and non‑streaming
|
|
100
117
|
- **Configurable logging** (v0.5.0+) - Verbose mode, custom loggers, or silent operation
|
|
101
118
|
- **Tool streaming support** (v0.3.0+) - Monitor autonomous tool execution in real-time
|
|
@@ -104,6 +121,48 @@ console.log(object);
|
|
|
104
121
|
- Safe defaults for non‑interactive automation (`on-failure`, `workspace-write`, `--skip-git-repo-check`)
|
|
105
122
|
- Fallback to `npx @openai/codex` when not on PATH (`allowNpx`)
|
|
106
123
|
- Usage tracking from experimental JSON event format
|
|
124
|
+
- **Image support** - Pass images to vision-capable models via `--image` flag
|
|
125
|
+
|
|
126
|
+
### Image Support
|
|
127
|
+
|
|
128
|
+
The provider supports multimodal (image) inputs for vision-capable models:
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
import { generateText } from 'ai';
|
|
132
|
+
import { codexCli } from 'ai-sdk-provider-codex-cli';
|
|
133
|
+
import { readFileSync } from 'fs';
|
|
134
|
+
|
|
135
|
+
const model = codexCli('gpt-5.1-codex', { allowNpx: true, skipGitRepoCheck: true });
|
|
136
|
+
const imageBuffer = readFileSync('./screenshot.png');
|
|
137
|
+
|
|
138
|
+
const { text } = await generateText({
|
|
139
|
+
model,
|
|
140
|
+
messages: [
|
|
141
|
+
{
|
|
142
|
+
role: 'user',
|
|
143
|
+
content: [
|
|
144
|
+
{ type: 'text', text: 'What do you see in this image?' },
|
|
145
|
+
{ type: 'image', image: imageBuffer, mimeType: 'image/png' },
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
});
|
|
150
|
+
console.log(text);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Supported image formats:**
|
|
154
|
+
|
|
155
|
+
- Base64 data URL (`data:image/png;base64,...`)
|
|
156
|
+
- Base64 string (without data URL prefix)
|
|
157
|
+
- `Buffer` / `Uint8Array` / `ArrayBuffer`
|
|
158
|
+
|
|
159
|
+
**Not supported:**
|
|
160
|
+
|
|
161
|
+
- HTTP/HTTPS URLs (images must be provided as binary data)
|
|
162
|
+
|
|
163
|
+
Images are written to temporary files and passed to Codex CLI via the `--image` flag. Temp files are automatically cleaned up after the request completes.
|
|
164
|
+
|
|
165
|
+
See [examples/image-support.mjs](examples/image-support.mjs) for a complete working example.
|
|
107
166
|
|
|
108
167
|
### Tool Streaming (v0.3.0+)
|
|
109
168
|
|
package/dist/index.cjs
CHANGED
|
@@ -167,6 +167,94 @@ function validateModelId(modelId) {
|
|
|
167
167
|
if (!modelId || modelId.trim() === "") return "Model ID cannot be empty";
|
|
168
168
|
return void 0;
|
|
169
169
|
}
|
|
170
|
+
function extractImageData(part) {
|
|
171
|
+
if (typeof part !== "object" || part === null) return null;
|
|
172
|
+
const p = part;
|
|
173
|
+
const mimeType = p.mimeType || "image/png";
|
|
174
|
+
if (typeof p.image === "string") {
|
|
175
|
+
return extractFromString(p.image, mimeType);
|
|
176
|
+
}
|
|
177
|
+
if (p.image instanceof URL) {
|
|
178
|
+
if (p.image.protocol === "data:") {
|
|
179
|
+
const dataUrlStr = p.image.toString();
|
|
180
|
+
const match = dataUrlStr.match(/^data:([^;,]+)/);
|
|
181
|
+
const extractedMimeType = match?.[1] || mimeType;
|
|
182
|
+
return { data: dataUrlStr, mimeType: extractedMimeType };
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
if (Buffer.isBuffer(p.image)) {
|
|
187
|
+
const base64 = p.image.toString("base64");
|
|
188
|
+
return { data: `data:${mimeType};base64,${base64}`, mimeType };
|
|
189
|
+
}
|
|
190
|
+
if (p.image instanceof ArrayBuffer || p.image instanceof Uint8Array) {
|
|
191
|
+
const buffer = Buffer.from(p.image);
|
|
192
|
+
const base64 = buffer.toString("base64");
|
|
193
|
+
return { data: `data:${mimeType};base64,${base64}`, mimeType };
|
|
194
|
+
}
|
|
195
|
+
if (typeof p.data === "string") {
|
|
196
|
+
return extractFromString(p.data, mimeType);
|
|
197
|
+
}
|
|
198
|
+
if (typeof p.url === "string") {
|
|
199
|
+
return extractFromString(p.url, mimeType);
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
function extractFromString(value, fallbackMimeType) {
|
|
204
|
+
const trimmed = value.trim();
|
|
205
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
if (trimmed.startsWith("data:")) {
|
|
209
|
+
if (!trimmed.includes(";base64,")) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
const match = trimmed.match(/^data:([^;,]+)/);
|
|
213
|
+
const mimeType = match?.[1] || fallbackMimeType;
|
|
214
|
+
return { data: trimmed, mimeType };
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
data: `data:${fallbackMimeType};base64,${trimmed}`,
|
|
218
|
+
mimeType: fallbackMimeType
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function writeImageToTempFile(imageData) {
|
|
222
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "codex-img-"));
|
|
223
|
+
const ext = getExtensionFromMimeType(imageData.mimeType);
|
|
224
|
+
const filePath = path.join(dir, `image.${ext}`);
|
|
225
|
+
const base64Match = imageData.data.match(/^data:[^;]+;base64,(.+)$/);
|
|
226
|
+
if (!base64Match) {
|
|
227
|
+
throw new Error("Invalid data URL format: expected data:[type];base64,[data]");
|
|
228
|
+
}
|
|
229
|
+
const buffer = Buffer.from(base64Match[1], "base64");
|
|
230
|
+
fs.writeFileSync(filePath, buffer);
|
|
231
|
+
return filePath;
|
|
232
|
+
}
|
|
233
|
+
function cleanupTempImages(paths) {
|
|
234
|
+
for (const filePath of paths) {
|
|
235
|
+
try {
|
|
236
|
+
fs.rmSync(filePath, { force: true });
|
|
237
|
+
const dir = filePath.replace(/[/\\][^/\\]+$/, "");
|
|
238
|
+
if (dir.includes("codex-img-")) {
|
|
239
|
+
fs.rmSync(dir, { force: true, recursive: true });
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function getExtensionFromMimeType(mimeType) {
|
|
246
|
+
if (!mimeType) return "png";
|
|
247
|
+
const mapping = {
|
|
248
|
+
"image/png": "png",
|
|
249
|
+
"image/jpeg": "jpg",
|
|
250
|
+
"image/jpg": "jpg",
|
|
251
|
+
"image/gif": "gif",
|
|
252
|
+
"image/webp": "webp",
|
|
253
|
+
"image/bmp": "bmp",
|
|
254
|
+
"image/svg+xml": "svg"
|
|
255
|
+
};
|
|
256
|
+
return mapping[mimeType.toLowerCase()] || mimeType.split("/")[1] || "png";
|
|
257
|
+
}
|
|
170
258
|
|
|
171
259
|
// src/message-mapper.ts
|
|
172
260
|
function isTextPart(p) {
|
|
@@ -187,6 +275,7 @@ function isToolItem(p) {
|
|
|
187
275
|
function mapMessagesToPrompt(prompt) {
|
|
188
276
|
const warnings = [];
|
|
189
277
|
const parts = [];
|
|
278
|
+
const images = [];
|
|
190
279
|
let systemText;
|
|
191
280
|
for (const msg of prompt) {
|
|
192
281
|
if (msg.role === "system") {
|
|
@@ -199,8 +288,14 @@ function mapMessagesToPrompt(prompt) {
|
|
|
199
288
|
} else if (Array.isArray(msg.content)) {
|
|
200
289
|
const text = msg.content.filter(isTextPart).map((p) => p.text).join("\n");
|
|
201
290
|
if (text) parts.push(`Human: ${text}`);
|
|
202
|
-
const
|
|
203
|
-
|
|
291
|
+
for (const part of msg.content.filter(isImagePart)) {
|
|
292
|
+
const imageData = extractImageData(part);
|
|
293
|
+
if (imageData) {
|
|
294
|
+
images.push(imageData);
|
|
295
|
+
} else {
|
|
296
|
+
warnings.push("Unsupported image format in message (HTTP URLs not supported)");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
204
299
|
}
|
|
205
300
|
continue;
|
|
206
301
|
}
|
|
@@ -227,7 +322,7 @@ function mapMessagesToPrompt(prompt) {
|
|
|
227
322
|
let promptText = "";
|
|
228
323
|
if (systemText) promptText += systemText + "\n\n";
|
|
229
324
|
promptText += parts.join("\n\n");
|
|
230
|
-
return { promptText, ...warnings.length ? { warnings } : {} };
|
|
325
|
+
return { promptText, images, ...warnings.length ? { warnings } : {} };
|
|
231
326
|
}
|
|
232
327
|
function createAPICallError({
|
|
233
328
|
message,
|
|
@@ -293,7 +388,7 @@ function resolveCodexPath(explicitPath, allowNpx) {
|
|
|
293
388
|
}
|
|
294
389
|
}
|
|
295
390
|
var CodexCliLanguageModel = class {
|
|
296
|
-
specificationVersion = "
|
|
391
|
+
specificationVersion = "v3";
|
|
297
392
|
provider = "codex-cli";
|
|
298
393
|
defaultObjectGenerationMode = "json";
|
|
299
394
|
supportsImageUrls = false;
|
|
@@ -404,7 +499,7 @@ var CodexCliLanguageModel = class {
|
|
|
404
499
|
const current = typeof data.type === "string" ? data.type : void 0;
|
|
405
500
|
return legacy ?? current;
|
|
406
501
|
}
|
|
407
|
-
buildArgs(promptText, responseFormat, settings = this.settings) {
|
|
502
|
+
buildArgs(promptText, images = [], responseFormat, settings = this.settings) {
|
|
408
503
|
const base = resolveCodexPath(settings.codexPath, settings.allowNpx);
|
|
409
504
|
const args = [...base.args, "exec", "--experimental-json"];
|
|
410
505
|
if (settings.fullAuto) {
|
|
@@ -479,6 +574,16 @@ var CodexCliLanguageModel = class {
|
|
|
479
574
|
args.push("--output-schema", schemaPath);
|
|
480
575
|
}
|
|
481
576
|
}
|
|
577
|
+
const tempImagePaths = [];
|
|
578
|
+
for (const img of images) {
|
|
579
|
+
try {
|
|
580
|
+
const tempPath = writeImageToTempFile(img);
|
|
581
|
+
tempImagePaths.push(tempPath);
|
|
582
|
+
args.push("--image", tempPath);
|
|
583
|
+
} catch (e) {
|
|
584
|
+
this.logger.warn(`[codex-cli] Failed to write image to temp file: ${String(e)}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
482
587
|
args.push(promptText);
|
|
483
588
|
const env = {
|
|
484
589
|
...process.env,
|
|
@@ -500,7 +605,8 @@ var CodexCliLanguageModel = class {
|
|
|
500
605
|
cwd: settings.cwd,
|
|
501
606
|
lastMessagePath,
|
|
502
607
|
lastMessageIsTemp,
|
|
503
|
-
schemaPath
|
|
608
|
+
schemaPath,
|
|
609
|
+
tempImagePaths: tempImagePaths.length > 0 ? tempImagePaths : void 0
|
|
504
610
|
};
|
|
505
611
|
}
|
|
506
612
|
applyMcpSettings(args, settings) {
|
|
@@ -621,8 +727,8 @@ var CodexCliLanguageModel = class {
|
|
|
621
727
|
const add = (setting, name) => {
|
|
622
728
|
if (setting !== void 0)
|
|
623
729
|
unsupported.push({
|
|
624
|
-
type: "unsupported
|
|
625
|
-
|
|
730
|
+
type: "unsupported",
|
|
731
|
+
feature: name,
|
|
626
732
|
details: `Codex CLI does not support ${name}; it will be ignored.`
|
|
627
733
|
});
|
|
628
734
|
};
|
|
@@ -645,15 +751,21 @@ var CodexCliLanguageModel = class {
|
|
|
645
751
|
extractUsage(evt) {
|
|
646
752
|
const reported = evt.usage;
|
|
647
753
|
if (!reported) return void 0;
|
|
648
|
-
const
|
|
649
|
-
const
|
|
754
|
+
const inputTotal = reported.input_tokens ?? 0;
|
|
755
|
+
const outputTotal = reported.output_tokens ?? 0;
|
|
650
756
|
const cachedInputTokens = reported.cached_input_tokens ?? 0;
|
|
651
757
|
return {
|
|
652
|
-
inputTokens
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
758
|
+
inputTokens: {
|
|
759
|
+
total: inputTotal,
|
|
760
|
+
noCache: inputTotal - cachedInputTokens,
|
|
761
|
+
cacheRead: cachedInputTokens,
|
|
762
|
+
cacheWrite: void 0
|
|
763
|
+
},
|
|
764
|
+
outputTokens: {
|
|
765
|
+
total: outputTotal,
|
|
766
|
+
text: outputTotal,
|
|
767
|
+
reasoning: void 0
|
|
768
|
+
}
|
|
657
769
|
};
|
|
658
770
|
}
|
|
659
771
|
getToolName(item) {
|
|
@@ -830,14 +942,14 @@ var CodexCliLanguageModel = class {
|
|
|
830
942
|
}
|
|
831
943
|
async doGenerate(options) {
|
|
832
944
|
this.logger.debug(`[codex-cli] Starting doGenerate request with model: ${this.modelId}`);
|
|
833
|
-
const { promptText, warnings: mappingWarnings } = mapMessagesToPrompt(options.prompt);
|
|
945
|
+
const { promptText, images, warnings: mappingWarnings } = mapMessagesToPrompt(options.prompt);
|
|
834
946
|
const promptExcerpt = promptText.slice(0, 200);
|
|
835
947
|
const warnings = [
|
|
836
948
|
...this.mapWarnings(options),
|
|
837
949
|
...mappingWarnings?.map((m) => ({ type: "other", message: m })) || []
|
|
838
950
|
];
|
|
839
951
|
this.logger.debug(
|
|
840
|
-
`[codex-cli] Converted ${options.prompt.length} messages, response format: ${options.responseFormat?.type ?? "none"}`
|
|
952
|
+
`[codex-cli] Converted ${options.prompt.length} messages (${images.length} images), response format: ${options.responseFormat?.type ?? "none"}`
|
|
841
953
|
);
|
|
842
954
|
const providerOptions = await providerUtils.parseProviderOptions({
|
|
843
955
|
provider: this.provider,
|
|
@@ -846,16 +958,15 @@ var CodexCliLanguageModel = class {
|
|
|
846
958
|
});
|
|
847
959
|
const effectiveSettings = this.mergeSettings(providerOptions);
|
|
848
960
|
const responseFormat = options.responseFormat?.type === "json" ? { type: "json", schema: options.responseFormat.schema } : void 0;
|
|
849
|
-
const { cmd, args, env, cwd, lastMessagePath, lastMessageIsTemp, schemaPath } = this.buildArgs(
|
|
850
|
-
promptText,
|
|
851
|
-
responseFormat,
|
|
852
|
-
effectiveSettings
|
|
853
|
-
);
|
|
961
|
+
const { cmd, args, env, cwd, lastMessagePath, lastMessageIsTemp, schemaPath, tempImagePaths } = this.buildArgs(promptText, images, responseFormat, effectiveSettings);
|
|
854
962
|
this.logger.debug(
|
|
855
963
|
`[codex-cli] Executing Codex CLI: ${cmd} with ${args.length} arguments, cwd: ${cwd ?? "default"}`
|
|
856
964
|
);
|
|
857
965
|
let text = "";
|
|
858
|
-
|
|
966
|
+
let usage = {
|
|
967
|
+
inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: void 0 },
|
|
968
|
+
outputTokens: { total: 0, text: 0, reasoning: void 0 }
|
|
969
|
+
};
|
|
859
970
|
const finishReason = "stop";
|
|
860
971
|
const startTime = Date.now();
|
|
861
972
|
const child = child_process.spawn(cmd, args, { env, cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -863,6 +974,16 @@ var CodexCliLanguageModel = class {
|
|
|
863
974
|
if (options.abortSignal) {
|
|
864
975
|
if (options.abortSignal.aborted) {
|
|
865
976
|
child.kill("SIGTERM");
|
|
977
|
+
if (schemaPath) {
|
|
978
|
+
try {
|
|
979
|
+
const schemaDir = path.dirname(schemaPath);
|
|
980
|
+
fs.rmSync(schemaDir, { recursive: true, force: true });
|
|
981
|
+
} catch {
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
if (tempImagePaths?.length) {
|
|
985
|
+
cleanupTempImages(tempImagePaths);
|
|
986
|
+
}
|
|
866
987
|
throw options.abortSignal.reason ?? new Error("Request aborted");
|
|
867
988
|
}
|
|
868
989
|
onAbort = () => child.kill("SIGTERM");
|
|
@@ -891,9 +1012,7 @@ var CodexCliLanguageModel = class {
|
|
|
891
1012
|
if (event.type === "turn.completed") {
|
|
892
1013
|
const usageEvent = this.extractUsage(event);
|
|
893
1014
|
if (usageEvent) {
|
|
894
|
-
usage
|
|
895
|
-
usage.outputTokens = usageEvent.outputTokens;
|
|
896
|
-
usage.totalTokens = usageEvent.totalTokens;
|
|
1015
|
+
usage = usageEvent;
|
|
897
1016
|
}
|
|
898
1017
|
}
|
|
899
1018
|
if (event.type === "item.completed" && this.getItemType(event.item) === "assistant_message" && typeof event.item?.text === "string") {
|
|
@@ -928,11 +1047,12 @@ var CodexCliLanguageModel = class {
|
|
|
928
1047
|
);
|
|
929
1048
|
return;
|
|
930
1049
|
}
|
|
1050
|
+
const totalTokens = (usage.inputTokens.total ?? 0) + (usage.outputTokens.total ?? 0);
|
|
931
1051
|
this.logger.info(
|
|
932
|
-
`[codex-cli] Request completed - Session: ${this.sessionId ?? "N/A"}, Duration: ${duration}ms, Tokens: ${
|
|
1052
|
+
`[codex-cli] Request completed - Session: ${this.sessionId ?? "N/A"}, Duration: ${duration}ms, Tokens: ${totalTokens}`
|
|
933
1053
|
);
|
|
934
1054
|
this.logger.debug(
|
|
935
|
-
`[codex-cli] Token usage - Input: ${usage.inputTokens}, Output: ${usage.outputTokens}, Total: ${
|
|
1055
|
+
`[codex-cli] Token usage - Input: ${usage.inputTokens.total ?? 0}, Output: ${usage.outputTokens.total ?? 0}, Total: ${totalTokens}`
|
|
936
1056
|
);
|
|
937
1057
|
resolve();
|
|
938
1058
|
} else {
|
|
@@ -957,6 +1077,9 @@ var CodexCliLanguageModel = class {
|
|
|
957
1077
|
} catch {
|
|
958
1078
|
}
|
|
959
1079
|
}
|
|
1080
|
+
if (tempImagePaths?.length) {
|
|
1081
|
+
cleanupTempImages(tempImagePaths);
|
|
1082
|
+
}
|
|
960
1083
|
}
|
|
961
1084
|
if (!text && lastMessagePath) {
|
|
962
1085
|
try {
|
|
@@ -988,14 +1111,14 @@ var CodexCliLanguageModel = class {
|
|
|
988
1111
|
}
|
|
989
1112
|
async doStream(options) {
|
|
990
1113
|
this.logger.debug(`[codex-cli] Starting doStream request with model: ${this.modelId}`);
|
|
991
|
-
const { promptText, warnings: mappingWarnings } = mapMessagesToPrompt(options.prompt);
|
|
1114
|
+
const { promptText, images, warnings: mappingWarnings } = mapMessagesToPrompt(options.prompt);
|
|
992
1115
|
const promptExcerpt = promptText.slice(0, 200);
|
|
993
1116
|
const warnings = [
|
|
994
1117
|
...this.mapWarnings(options),
|
|
995
1118
|
...mappingWarnings?.map((m) => ({ type: "other", message: m })) || []
|
|
996
1119
|
];
|
|
997
1120
|
this.logger.debug(
|
|
998
|
-
`[codex-cli] Converted ${options.prompt.length} messages for streaming, response format: ${options.responseFormat?.type ?? "none"}`
|
|
1121
|
+
`[codex-cli] Converted ${options.prompt.length} messages (${images.length} images) for streaming, response format: ${options.responseFormat?.type ?? "none"}`
|
|
999
1122
|
);
|
|
1000
1123
|
const providerOptions = await providerUtils.parseProviderOptions({
|
|
1001
1124
|
provider: this.provider,
|
|
@@ -1004,11 +1127,7 @@ var CodexCliLanguageModel = class {
|
|
|
1004
1127
|
});
|
|
1005
1128
|
const effectiveSettings = this.mergeSettings(providerOptions);
|
|
1006
1129
|
const responseFormat = options.responseFormat?.type === "json" ? { type: "json", schema: options.responseFormat.schema } : void 0;
|
|
1007
|
-
const { cmd, args, env, cwd, lastMessagePath, lastMessageIsTemp, schemaPath } = this.buildArgs(
|
|
1008
|
-
promptText,
|
|
1009
|
-
responseFormat,
|
|
1010
|
-
effectiveSettings
|
|
1011
|
-
);
|
|
1130
|
+
const { cmd, args, env, cwd, lastMessagePath, lastMessageIsTemp, schemaPath, tempImagePaths } = this.buildArgs(promptText, images, responseFormat, effectiveSettings);
|
|
1012
1131
|
this.logger.debug(
|
|
1013
1132
|
`[codex-cli] Executing Codex CLI for streaming: ${cmd} with ${args.length} arguments`
|
|
1014
1133
|
);
|
|
@@ -1023,6 +1142,18 @@ var CodexCliLanguageModel = class {
|
|
|
1023
1142
|
let responseMetadataSent = false;
|
|
1024
1143
|
let lastUsage;
|
|
1025
1144
|
let turnFailureMessage;
|
|
1145
|
+
const cleanupTempFiles = () => {
|
|
1146
|
+
if (schemaPath) {
|
|
1147
|
+
try {
|
|
1148
|
+
const schemaDir = path.dirname(schemaPath);
|
|
1149
|
+
fs.rmSync(schemaDir, { recursive: true, force: true });
|
|
1150
|
+
} catch {
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
if (tempImagePaths?.length) {
|
|
1154
|
+
cleanupTempImages(tempImagePaths);
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1026
1157
|
const sendMetadata = (meta = {}) => {
|
|
1027
1158
|
controller.enqueue({
|
|
1028
1159
|
type: "response-metadata",
|
|
@@ -1096,6 +1227,7 @@ var CodexCliLanguageModel = class {
|
|
|
1096
1227
|
if (options.abortSignal) {
|
|
1097
1228
|
if (options.abortSignal.aborted) {
|
|
1098
1229
|
child.kill("SIGTERM");
|
|
1230
|
+
cleanupTempFiles();
|
|
1099
1231
|
controller.error(options.abortSignal.reason ?? new Error("Request aborted"));
|
|
1100
1232
|
return;
|
|
1101
1233
|
}
|
|
@@ -1148,12 +1280,16 @@ var CodexCliLanguageModel = class {
|
|
|
1148
1280
|
controller.enqueue({ type: "text-delta", id: textId, delta: finalText });
|
|
1149
1281
|
controller.enqueue({ type: "text-end", id: textId });
|
|
1150
1282
|
}
|
|
1151
|
-
const usageSummary = lastUsage ?? {
|
|
1283
|
+
const usageSummary = lastUsage ?? {
|
|
1284
|
+
inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: void 0 },
|
|
1285
|
+
outputTokens: { total: 0, text: 0, reasoning: void 0 }
|
|
1286
|
+
};
|
|
1287
|
+
const totalTokens = (usageSummary.inputTokens.total ?? 0) + (usageSummary.outputTokens.total ?? 0);
|
|
1152
1288
|
this.logger.info(
|
|
1153
|
-
`[codex-cli] Stream completed - Session: ${this.sessionId ?? "N/A"}, Duration: ${duration}ms, Tokens: ${
|
|
1289
|
+
`[codex-cli] Stream completed - Session: ${this.sessionId ?? "N/A"}, Duration: ${duration}ms, Tokens: ${totalTokens}`
|
|
1154
1290
|
);
|
|
1155
1291
|
this.logger.debug(
|
|
1156
|
-
`[codex-cli] Token usage - Input: ${usageSummary.inputTokens}, Output: ${usageSummary.outputTokens}, Total: ${
|
|
1292
|
+
`[codex-cli] Token usage - Input: ${usageSummary.inputTokens.total ?? 0}, Output: ${usageSummary.outputTokens.total ?? 0}, Total: ${totalTokens}`
|
|
1157
1293
|
);
|
|
1158
1294
|
controller.enqueue({
|
|
1159
1295
|
type: "finish",
|
|
@@ -1215,23 +1351,15 @@ var CodexCliLanguageModel = class {
|
|
|
1215
1351
|
}
|
|
1216
1352
|
}
|
|
1217
1353
|
});
|
|
1218
|
-
const cleanupSchema = () => {
|
|
1219
|
-
if (!schemaPath) return;
|
|
1220
|
-
try {
|
|
1221
|
-
const schemaDir = path.dirname(schemaPath);
|
|
1222
|
-
fs.rmSync(schemaDir, { recursive: true, force: true });
|
|
1223
|
-
} catch {
|
|
1224
|
-
}
|
|
1225
|
-
};
|
|
1226
1354
|
child.on("error", (e) => {
|
|
1227
1355
|
this.logger.error(`[codex-cli] Stream spawn error: ${String(e)}`);
|
|
1228
1356
|
if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort);
|
|
1229
|
-
|
|
1357
|
+
cleanupTempFiles();
|
|
1230
1358
|
controller.error(this.handleSpawnError(e, promptExcerpt));
|
|
1231
1359
|
});
|
|
1232
1360
|
child.on("close", (code) => {
|
|
1233
1361
|
if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort);
|
|
1234
|
-
|
|
1362
|
+
cleanupTempFiles();
|
|
1235
1363
|
setImmediate(() => finishStream(code));
|
|
1236
1364
|
});
|
|
1237
1365
|
},
|
|
@@ -1265,8 +1393,8 @@ function createCodexCli(options = {}) {
|
|
|
1265
1393
|
};
|
|
1266
1394
|
provider$1.languageModel = createModel;
|
|
1267
1395
|
provider$1.chat = createModel;
|
|
1268
|
-
provider$1.
|
|
1269
|
-
throw new provider.NoSuchModelError({ modelId, modelType: "
|
|
1396
|
+
provider$1.embeddingModel = ((modelId) => {
|
|
1397
|
+
throw new provider.NoSuchModelError({ modelId, modelType: "embeddingModel" });
|
|
1270
1398
|
});
|
|
1271
1399
|
provider$1.imageModel = ((modelId) => {
|
|
1272
1400
|
throw new provider.NoSuchModelError({ modelId, modelType: "imageModel" });
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ProviderV3, LanguageModelV3 } from '@ai-sdk/provider';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Logger interface for custom logging.
|
|
@@ -251,11 +251,11 @@ interface CodexCliProviderOptions {
|
|
|
251
251
|
rmcpClient?: boolean;
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
interface CodexCliProvider extends
|
|
255
|
-
(modelId: string, settings?: CodexCliSettings):
|
|
256
|
-
languageModel(modelId: string, settings?: CodexCliSettings):
|
|
257
|
-
chat(modelId: string, settings?: CodexCliSettings):
|
|
258
|
-
|
|
254
|
+
interface CodexCliProvider extends ProviderV3 {
|
|
255
|
+
(modelId: string, settings?: CodexCliSettings): LanguageModelV3;
|
|
256
|
+
languageModel(modelId: string, settings?: CodexCliSettings): LanguageModelV3;
|
|
257
|
+
chat(modelId: string, settings?: CodexCliSettings): LanguageModelV3;
|
|
258
|
+
embeddingModel(modelId: string): never;
|
|
259
259
|
imageModel(modelId: string): never;
|
|
260
260
|
}
|
|
261
261
|
declare function createCodexCli(options?: CodexCliProviderSettings): CodexCliProvider;
|
|
@@ -265,8 +265,8 @@ interface CodexLanguageModelOptions {
|
|
|
265
265
|
id: string;
|
|
266
266
|
settings?: CodexCliSettings;
|
|
267
267
|
}
|
|
268
|
-
declare class CodexCliLanguageModel implements
|
|
269
|
-
readonly specificationVersion: "
|
|
268
|
+
declare class CodexCliLanguageModel implements LanguageModelV3 {
|
|
269
|
+
readonly specificationVersion: "v3";
|
|
270
270
|
readonly provider = "codex-cli";
|
|
271
271
|
readonly defaultObjectGenerationMode: "json";
|
|
272
272
|
readonly supportsImageUrls = false;
|
|
@@ -301,8 +301,8 @@ declare class CodexCliLanguageModel implements LanguageModelV2 {
|
|
|
301
301
|
private emitToolInvocation;
|
|
302
302
|
private emitToolResult;
|
|
303
303
|
private handleSpawnError;
|
|
304
|
-
doGenerate(options: Parameters<
|
|
305
|
-
doStream(options: Parameters<
|
|
304
|
+
doGenerate(options: Parameters<LanguageModelV3['doGenerate']>[0]): Promise<Awaited<ReturnType<LanguageModelV3['doGenerate']>>>;
|
|
305
|
+
doStream(options: Parameters<LanguageModelV3['doStream']>[0]): Promise<Awaited<ReturnType<LanguageModelV3['doStream']>>>;
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
declare function isAuthenticationError(err: unknown): boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ProviderV3, LanguageModelV3 } from '@ai-sdk/provider';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Logger interface for custom logging.
|
|
@@ -251,11 +251,11 @@ interface CodexCliProviderOptions {
|
|
|
251
251
|
rmcpClient?: boolean;
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
interface CodexCliProvider extends
|
|
255
|
-
(modelId: string, settings?: CodexCliSettings):
|
|
256
|
-
languageModel(modelId: string, settings?: CodexCliSettings):
|
|
257
|
-
chat(modelId: string, settings?: CodexCliSettings):
|
|
258
|
-
|
|
254
|
+
interface CodexCliProvider extends ProviderV3 {
|
|
255
|
+
(modelId: string, settings?: CodexCliSettings): LanguageModelV3;
|
|
256
|
+
languageModel(modelId: string, settings?: CodexCliSettings): LanguageModelV3;
|
|
257
|
+
chat(modelId: string, settings?: CodexCliSettings): LanguageModelV3;
|
|
258
|
+
embeddingModel(modelId: string): never;
|
|
259
259
|
imageModel(modelId: string): never;
|
|
260
260
|
}
|
|
261
261
|
declare function createCodexCli(options?: CodexCliProviderSettings): CodexCliProvider;
|
|
@@ -265,8 +265,8 @@ interface CodexLanguageModelOptions {
|
|
|
265
265
|
id: string;
|
|
266
266
|
settings?: CodexCliSettings;
|
|
267
267
|
}
|
|
268
|
-
declare class CodexCliLanguageModel implements
|
|
269
|
-
readonly specificationVersion: "
|
|
268
|
+
declare class CodexCliLanguageModel implements LanguageModelV3 {
|
|
269
|
+
readonly specificationVersion: "v3";
|
|
270
270
|
readonly provider = "codex-cli";
|
|
271
271
|
readonly defaultObjectGenerationMode: "json";
|
|
272
272
|
readonly supportsImageUrls = false;
|
|
@@ -301,8 +301,8 @@ declare class CodexCliLanguageModel implements LanguageModelV2 {
|
|
|
301
301
|
private emitToolInvocation;
|
|
302
302
|
private emitToolResult;
|
|
303
303
|
private handleSpawnError;
|
|
304
|
-
doGenerate(options: Parameters<
|
|
305
|
-
doStream(options: Parameters<
|
|
304
|
+
doGenerate(options: Parameters<LanguageModelV3['doGenerate']>[0]): Promise<Awaited<ReturnType<LanguageModelV3['doGenerate']>>>;
|
|
305
|
+
doStream(options: Parameters<LanguageModelV3['doStream']>[0]): Promise<Awaited<ReturnType<LanguageModelV3['doStream']>>>;
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
declare function isAuthenticationError(err: unknown): boolean;
|
package/dist/index.js
CHANGED
|
@@ -164,6 +164,94 @@ function validateModelId(modelId) {
|
|
|
164
164
|
if (!modelId || modelId.trim() === "") return "Model ID cannot be empty";
|
|
165
165
|
return void 0;
|
|
166
166
|
}
|
|
167
|
+
function extractImageData(part) {
|
|
168
|
+
if (typeof part !== "object" || part === null) return null;
|
|
169
|
+
const p = part;
|
|
170
|
+
const mimeType = p.mimeType || "image/png";
|
|
171
|
+
if (typeof p.image === "string") {
|
|
172
|
+
return extractFromString(p.image, mimeType);
|
|
173
|
+
}
|
|
174
|
+
if (p.image instanceof URL) {
|
|
175
|
+
if (p.image.protocol === "data:") {
|
|
176
|
+
const dataUrlStr = p.image.toString();
|
|
177
|
+
const match = dataUrlStr.match(/^data:([^;,]+)/);
|
|
178
|
+
const extractedMimeType = match?.[1] || mimeType;
|
|
179
|
+
return { data: dataUrlStr, mimeType: extractedMimeType };
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
if (Buffer.isBuffer(p.image)) {
|
|
184
|
+
const base64 = p.image.toString("base64");
|
|
185
|
+
return { data: `data:${mimeType};base64,${base64}`, mimeType };
|
|
186
|
+
}
|
|
187
|
+
if (p.image instanceof ArrayBuffer || p.image instanceof Uint8Array) {
|
|
188
|
+
const buffer = Buffer.from(p.image);
|
|
189
|
+
const base64 = buffer.toString("base64");
|
|
190
|
+
return { data: `data:${mimeType};base64,${base64}`, mimeType };
|
|
191
|
+
}
|
|
192
|
+
if (typeof p.data === "string") {
|
|
193
|
+
return extractFromString(p.data, mimeType);
|
|
194
|
+
}
|
|
195
|
+
if (typeof p.url === "string") {
|
|
196
|
+
return extractFromString(p.url, mimeType);
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
function extractFromString(value, fallbackMimeType) {
|
|
201
|
+
const trimmed = value.trim();
|
|
202
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
if (trimmed.startsWith("data:")) {
|
|
206
|
+
if (!trimmed.includes(";base64,")) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
const match = trimmed.match(/^data:([^;,]+)/);
|
|
210
|
+
const mimeType = match?.[1] || fallbackMimeType;
|
|
211
|
+
return { data: trimmed, mimeType };
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
data: `data:${fallbackMimeType};base64,${trimmed}`,
|
|
215
|
+
mimeType: fallbackMimeType
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function writeImageToTempFile(imageData) {
|
|
219
|
+
const dir = mkdtempSync(join(tmpdir(), "codex-img-"));
|
|
220
|
+
const ext = getExtensionFromMimeType(imageData.mimeType);
|
|
221
|
+
const filePath = join(dir, `image.${ext}`);
|
|
222
|
+
const base64Match = imageData.data.match(/^data:[^;]+;base64,(.+)$/);
|
|
223
|
+
if (!base64Match) {
|
|
224
|
+
throw new Error("Invalid data URL format: expected data:[type];base64,[data]");
|
|
225
|
+
}
|
|
226
|
+
const buffer = Buffer.from(base64Match[1], "base64");
|
|
227
|
+
writeFileSync(filePath, buffer);
|
|
228
|
+
return filePath;
|
|
229
|
+
}
|
|
230
|
+
function cleanupTempImages(paths) {
|
|
231
|
+
for (const filePath of paths) {
|
|
232
|
+
try {
|
|
233
|
+
rmSync(filePath, { force: true });
|
|
234
|
+
const dir = filePath.replace(/[/\\][^/\\]+$/, "");
|
|
235
|
+
if (dir.includes("codex-img-")) {
|
|
236
|
+
rmSync(dir, { force: true, recursive: true });
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function getExtensionFromMimeType(mimeType) {
|
|
243
|
+
if (!mimeType) return "png";
|
|
244
|
+
const mapping = {
|
|
245
|
+
"image/png": "png",
|
|
246
|
+
"image/jpeg": "jpg",
|
|
247
|
+
"image/jpg": "jpg",
|
|
248
|
+
"image/gif": "gif",
|
|
249
|
+
"image/webp": "webp",
|
|
250
|
+
"image/bmp": "bmp",
|
|
251
|
+
"image/svg+xml": "svg"
|
|
252
|
+
};
|
|
253
|
+
return mapping[mimeType.toLowerCase()] || mimeType.split("/")[1] || "png";
|
|
254
|
+
}
|
|
167
255
|
|
|
168
256
|
// src/message-mapper.ts
|
|
169
257
|
function isTextPart(p) {
|
|
@@ -184,6 +272,7 @@ function isToolItem(p) {
|
|
|
184
272
|
function mapMessagesToPrompt(prompt) {
|
|
185
273
|
const warnings = [];
|
|
186
274
|
const parts = [];
|
|
275
|
+
const images = [];
|
|
187
276
|
let systemText;
|
|
188
277
|
for (const msg of prompt) {
|
|
189
278
|
if (msg.role === "system") {
|
|
@@ -196,8 +285,14 @@ function mapMessagesToPrompt(prompt) {
|
|
|
196
285
|
} else if (Array.isArray(msg.content)) {
|
|
197
286
|
const text = msg.content.filter(isTextPart).map((p) => p.text).join("\n");
|
|
198
287
|
if (text) parts.push(`Human: ${text}`);
|
|
199
|
-
const
|
|
200
|
-
|
|
288
|
+
for (const part of msg.content.filter(isImagePart)) {
|
|
289
|
+
const imageData = extractImageData(part);
|
|
290
|
+
if (imageData) {
|
|
291
|
+
images.push(imageData);
|
|
292
|
+
} else {
|
|
293
|
+
warnings.push("Unsupported image format in message (HTTP URLs not supported)");
|
|
294
|
+
}
|
|
295
|
+
}
|
|
201
296
|
}
|
|
202
297
|
continue;
|
|
203
298
|
}
|
|
@@ -224,7 +319,7 @@ function mapMessagesToPrompt(prompt) {
|
|
|
224
319
|
let promptText = "";
|
|
225
320
|
if (systemText) promptText += systemText + "\n\n";
|
|
226
321
|
promptText += parts.join("\n\n");
|
|
227
|
-
return { promptText, ...warnings.length ? { warnings } : {} };
|
|
322
|
+
return { promptText, images, ...warnings.length ? { warnings } : {} };
|
|
228
323
|
}
|
|
229
324
|
function createAPICallError({
|
|
230
325
|
message,
|
|
@@ -290,7 +385,7 @@ function resolveCodexPath(explicitPath, allowNpx) {
|
|
|
290
385
|
}
|
|
291
386
|
}
|
|
292
387
|
var CodexCliLanguageModel = class {
|
|
293
|
-
specificationVersion = "
|
|
388
|
+
specificationVersion = "v3";
|
|
294
389
|
provider = "codex-cli";
|
|
295
390
|
defaultObjectGenerationMode = "json";
|
|
296
391
|
supportsImageUrls = false;
|
|
@@ -401,7 +496,7 @@ var CodexCliLanguageModel = class {
|
|
|
401
496
|
const current = typeof data.type === "string" ? data.type : void 0;
|
|
402
497
|
return legacy ?? current;
|
|
403
498
|
}
|
|
404
|
-
buildArgs(promptText, responseFormat, settings = this.settings) {
|
|
499
|
+
buildArgs(promptText, images = [], responseFormat, settings = this.settings) {
|
|
405
500
|
const base = resolveCodexPath(settings.codexPath, settings.allowNpx);
|
|
406
501
|
const args = [...base.args, "exec", "--experimental-json"];
|
|
407
502
|
if (settings.fullAuto) {
|
|
@@ -476,6 +571,16 @@ var CodexCliLanguageModel = class {
|
|
|
476
571
|
args.push("--output-schema", schemaPath);
|
|
477
572
|
}
|
|
478
573
|
}
|
|
574
|
+
const tempImagePaths = [];
|
|
575
|
+
for (const img of images) {
|
|
576
|
+
try {
|
|
577
|
+
const tempPath = writeImageToTempFile(img);
|
|
578
|
+
tempImagePaths.push(tempPath);
|
|
579
|
+
args.push("--image", tempPath);
|
|
580
|
+
} catch (e) {
|
|
581
|
+
this.logger.warn(`[codex-cli] Failed to write image to temp file: ${String(e)}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
479
584
|
args.push(promptText);
|
|
480
585
|
const env = {
|
|
481
586
|
...process.env,
|
|
@@ -497,7 +602,8 @@ var CodexCliLanguageModel = class {
|
|
|
497
602
|
cwd: settings.cwd,
|
|
498
603
|
lastMessagePath,
|
|
499
604
|
lastMessageIsTemp,
|
|
500
|
-
schemaPath
|
|
605
|
+
schemaPath,
|
|
606
|
+
tempImagePaths: tempImagePaths.length > 0 ? tempImagePaths : void 0
|
|
501
607
|
};
|
|
502
608
|
}
|
|
503
609
|
applyMcpSettings(args, settings) {
|
|
@@ -618,8 +724,8 @@ var CodexCliLanguageModel = class {
|
|
|
618
724
|
const add = (setting, name) => {
|
|
619
725
|
if (setting !== void 0)
|
|
620
726
|
unsupported.push({
|
|
621
|
-
type: "unsupported
|
|
622
|
-
|
|
727
|
+
type: "unsupported",
|
|
728
|
+
feature: name,
|
|
623
729
|
details: `Codex CLI does not support ${name}; it will be ignored.`
|
|
624
730
|
});
|
|
625
731
|
};
|
|
@@ -642,15 +748,21 @@ var CodexCliLanguageModel = class {
|
|
|
642
748
|
extractUsage(evt) {
|
|
643
749
|
const reported = evt.usage;
|
|
644
750
|
if (!reported) return void 0;
|
|
645
|
-
const
|
|
646
|
-
const
|
|
751
|
+
const inputTotal = reported.input_tokens ?? 0;
|
|
752
|
+
const outputTotal = reported.output_tokens ?? 0;
|
|
647
753
|
const cachedInputTokens = reported.cached_input_tokens ?? 0;
|
|
648
754
|
return {
|
|
649
|
-
inputTokens
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
755
|
+
inputTokens: {
|
|
756
|
+
total: inputTotal,
|
|
757
|
+
noCache: inputTotal - cachedInputTokens,
|
|
758
|
+
cacheRead: cachedInputTokens,
|
|
759
|
+
cacheWrite: void 0
|
|
760
|
+
},
|
|
761
|
+
outputTokens: {
|
|
762
|
+
total: outputTotal,
|
|
763
|
+
text: outputTotal,
|
|
764
|
+
reasoning: void 0
|
|
765
|
+
}
|
|
654
766
|
};
|
|
655
767
|
}
|
|
656
768
|
getToolName(item) {
|
|
@@ -827,14 +939,14 @@ var CodexCliLanguageModel = class {
|
|
|
827
939
|
}
|
|
828
940
|
async doGenerate(options) {
|
|
829
941
|
this.logger.debug(`[codex-cli] Starting doGenerate request with model: ${this.modelId}`);
|
|
830
|
-
const { promptText, warnings: mappingWarnings } = mapMessagesToPrompt(options.prompt);
|
|
942
|
+
const { promptText, images, warnings: mappingWarnings } = mapMessagesToPrompt(options.prompt);
|
|
831
943
|
const promptExcerpt = promptText.slice(0, 200);
|
|
832
944
|
const warnings = [
|
|
833
945
|
...this.mapWarnings(options),
|
|
834
946
|
...mappingWarnings?.map((m) => ({ type: "other", message: m })) || []
|
|
835
947
|
];
|
|
836
948
|
this.logger.debug(
|
|
837
|
-
`[codex-cli] Converted ${options.prompt.length} messages, response format: ${options.responseFormat?.type ?? "none"}`
|
|
949
|
+
`[codex-cli] Converted ${options.prompt.length} messages (${images.length} images), response format: ${options.responseFormat?.type ?? "none"}`
|
|
838
950
|
);
|
|
839
951
|
const providerOptions = await parseProviderOptions({
|
|
840
952
|
provider: this.provider,
|
|
@@ -843,16 +955,15 @@ var CodexCliLanguageModel = class {
|
|
|
843
955
|
});
|
|
844
956
|
const effectiveSettings = this.mergeSettings(providerOptions);
|
|
845
957
|
const responseFormat = options.responseFormat?.type === "json" ? { type: "json", schema: options.responseFormat.schema } : void 0;
|
|
846
|
-
const { cmd, args, env, cwd, lastMessagePath, lastMessageIsTemp, schemaPath } = this.buildArgs(
|
|
847
|
-
promptText,
|
|
848
|
-
responseFormat,
|
|
849
|
-
effectiveSettings
|
|
850
|
-
);
|
|
958
|
+
const { cmd, args, env, cwd, lastMessagePath, lastMessageIsTemp, schemaPath, tempImagePaths } = this.buildArgs(promptText, images, responseFormat, effectiveSettings);
|
|
851
959
|
this.logger.debug(
|
|
852
960
|
`[codex-cli] Executing Codex CLI: ${cmd} with ${args.length} arguments, cwd: ${cwd ?? "default"}`
|
|
853
961
|
);
|
|
854
962
|
let text = "";
|
|
855
|
-
|
|
963
|
+
let usage = {
|
|
964
|
+
inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: void 0 },
|
|
965
|
+
outputTokens: { total: 0, text: 0, reasoning: void 0 }
|
|
966
|
+
};
|
|
856
967
|
const finishReason = "stop";
|
|
857
968
|
const startTime = Date.now();
|
|
858
969
|
const child = spawn(cmd, args, { env, cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -860,6 +971,16 @@ var CodexCliLanguageModel = class {
|
|
|
860
971
|
if (options.abortSignal) {
|
|
861
972
|
if (options.abortSignal.aborted) {
|
|
862
973
|
child.kill("SIGTERM");
|
|
974
|
+
if (schemaPath) {
|
|
975
|
+
try {
|
|
976
|
+
const schemaDir = dirname(schemaPath);
|
|
977
|
+
rmSync(schemaDir, { recursive: true, force: true });
|
|
978
|
+
} catch {
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
if (tempImagePaths?.length) {
|
|
982
|
+
cleanupTempImages(tempImagePaths);
|
|
983
|
+
}
|
|
863
984
|
throw options.abortSignal.reason ?? new Error("Request aborted");
|
|
864
985
|
}
|
|
865
986
|
onAbort = () => child.kill("SIGTERM");
|
|
@@ -888,9 +1009,7 @@ var CodexCliLanguageModel = class {
|
|
|
888
1009
|
if (event.type === "turn.completed") {
|
|
889
1010
|
const usageEvent = this.extractUsage(event);
|
|
890
1011
|
if (usageEvent) {
|
|
891
|
-
usage
|
|
892
|
-
usage.outputTokens = usageEvent.outputTokens;
|
|
893
|
-
usage.totalTokens = usageEvent.totalTokens;
|
|
1012
|
+
usage = usageEvent;
|
|
894
1013
|
}
|
|
895
1014
|
}
|
|
896
1015
|
if (event.type === "item.completed" && this.getItemType(event.item) === "assistant_message" && typeof event.item?.text === "string") {
|
|
@@ -925,11 +1044,12 @@ var CodexCliLanguageModel = class {
|
|
|
925
1044
|
);
|
|
926
1045
|
return;
|
|
927
1046
|
}
|
|
1047
|
+
const totalTokens = (usage.inputTokens.total ?? 0) + (usage.outputTokens.total ?? 0);
|
|
928
1048
|
this.logger.info(
|
|
929
|
-
`[codex-cli] Request completed - Session: ${this.sessionId ?? "N/A"}, Duration: ${duration}ms, Tokens: ${
|
|
1049
|
+
`[codex-cli] Request completed - Session: ${this.sessionId ?? "N/A"}, Duration: ${duration}ms, Tokens: ${totalTokens}`
|
|
930
1050
|
);
|
|
931
1051
|
this.logger.debug(
|
|
932
|
-
`[codex-cli] Token usage - Input: ${usage.inputTokens}, Output: ${usage.outputTokens}, Total: ${
|
|
1052
|
+
`[codex-cli] Token usage - Input: ${usage.inputTokens.total ?? 0}, Output: ${usage.outputTokens.total ?? 0}, Total: ${totalTokens}`
|
|
933
1053
|
);
|
|
934
1054
|
resolve();
|
|
935
1055
|
} else {
|
|
@@ -954,6 +1074,9 @@ var CodexCliLanguageModel = class {
|
|
|
954
1074
|
} catch {
|
|
955
1075
|
}
|
|
956
1076
|
}
|
|
1077
|
+
if (tempImagePaths?.length) {
|
|
1078
|
+
cleanupTempImages(tempImagePaths);
|
|
1079
|
+
}
|
|
957
1080
|
}
|
|
958
1081
|
if (!text && lastMessagePath) {
|
|
959
1082
|
try {
|
|
@@ -985,14 +1108,14 @@ var CodexCliLanguageModel = class {
|
|
|
985
1108
|
}
|
|
986
1109
|
async doStream(options) {
|
|
987
1110
|
this.logger.debug(`[codex-cli] Starting doStream request with model: ${this.modelId}`);
|
|
988
|
-
const { promptText, warnings: mappingWarnings } = mapMessagesToPrompt(options.prompt);
|
|
1111
|
+
const { promptText, images, warnings: mappingWarnings } = mapMessagesToPrompt(options.prompt);
|
|
989
1112
|
const promptExcerpt = promptText.slice(0, 200);
|
|
990
1113
|
const warnings = [
|
|
991
1114
|
...this.mapWarnings(options),
|
|
992
1115
|
...mappingWarnings?.map((m) => ({ type: "other", message: m })) || []
|
|
993
1116
|
];
|
|
994
1117
|
this.logger.debug(
|
|
995
|
-
`[codex-cli] Converted ${options.prompt.length} messages for streaming, response format: ${options.responseFormat?.type ?? "none"}`
|
|
1118
|
+
`[codex-cli] Converted ${options.prompt.length} messages (${images.length} images) for streaming, response format: ${options.responseFormat?.type ?? "none"}`
|
|
996
1119
|
);
|
|
997
1120
|
const providerOptions = await parseProviderOptions({
|
|
998
1121
|
provider: this.provider,
|
|
@@ -1001,11 +1124,7 @@ var CodexCliLanguageModel = class {
|
|
|
1001
1124
|
});
|
|
1002
1125
|
const effectiveSettings = this.mergeSettings(providerOptions);
|
|
1003
1126
|
const responseFormat = options.responseFormat?.type === "json" ? { type: "json", schema: options.responseFormat.schema } : void 0;
|
|
1004
|
-
const { cmd, args, env, cwd, lastMessagePath, lastMessageIsTemp, schemaPath } = this.buildArgs(
|
|
1005
|
-
promptText,
|
|
1006
|
-
responseFormat,
|
|
1007
|
-
effectiveSettings
|
|
1008
|
-
);
|
|
1127
|
+
const { cmd, args, env, cwd, lastMessagePath, lastMessageIsTemp, schemaPath, tempImagePaths } = this.buildArgs(promptText, images, responseFormat, effectiveSettings);
|
|
1009
1128
|
this.logger.debug(
|
|
1010
1129
|
`[codex-cli] Executing Codex CLI for streaming: ${cmd} with ${args.length} arguments`
|
|
1011
1130
|
);
|
|
@@ -1020,6 +1139,18 @@ var CodexCliLanguageModel = class {
|
|
|
1020
1139
|
let responseMetadataSent = false;
|
|
1021
1140
|
let lastUsage;
|
|
1022
1141
|
let turnFailureMessage;
|
|
1142
|
+
const cleanupTempFiles = () => {
|
|
1143
|
+
if (schemaPath) {
|
|
1144
|
+
try {
|
|
1145
|
+
const schemaDir = dirname(schemaPath);
|
|
1146
|
+
rmSync(schemaDir, { recursive: true, force: true });
|
|
1147
|
+
} catch {
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
if (tempImagePaths?.length) {
|
|
1151
|
+
cleanupTempImages(tempImagePaths);
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1023
1154
|
const sendMetadata = (meta = {}) => {
|
|
1024
1155
|
controller.enqueue({
|
|
1025
1156
|
type: "response-metadata",
|
|
@@ -1093,6 +1224,7 @@ var CodexCliLanguageModel = class {
|
|
|
1093
1224
|
if (options.abortSignal) {
|
|
1094
1225
|
if (options.abortSignal.aborted) {
|
|
1095
1226
|
child.kill("SIGTERM");
|
|
1227
|
+
cleanupTempFiles();
|
|
1096
1228
|
controller.error(options.abortSignal.reason ?? new Error("Request aborted"));
|
|
1097
1229
|
return;
|
|
1098
1230
|
}
|
|
@@ -1145,12 +1277,16 @@ var CodexCliLanguageModel = class {
|
|
|
1145
1277
|
controller.enqueue({ type: "text-delta", id: textId, delta: finalText });
|
|
1146
1278
|
controller.enqueue({ type: "text-end", id: textId });
|
|
1147
1279
|
}
|
|
1148
|
-
const usageSummary = lastUsage ?? {
|
|
1280
|
+
const usageSummary = lastUsage ?? {
|
|
1281
|
+
inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: void 0 },
|
|
1282
|
+
outputTokens: { total: 0, text: 0, reasoning: void 0 }
|
|
1283
|
+
};
|
|
1284
|
+
const totalTokens = (usageSummary.inputTokens.total ?? 0) + (usageSummary.outputTokens.total ?? 0);
|
|
1149
1285
|
this.logger.info(
|
|
1150
|
-
`[codex-cli] Stream completed - Session: ${this.sessionId ?? "N/A"}, Duration: ${duration}ms, Tokens: ${
|
|
1286
|
+
`[codex-cli] Stream completed - Session: ${this.sessionId ?? "N/A"}, Duration: ${duration}ms, Tokens: ${totalTokens}`
|
|
1151
1287
|
);
|
|
1152
1288
|
this.logger.debug(
|
|
1153
|
-
`[codex-cli] Token usage - Input: ${usageSummary.inputTokens}, Output: ${usageSummary.outputTokens}, Total: ${
|
|
1289
|
+
`[codex-cli] Token usage - Input: ${usageSummary.inputTokens.total ?? 0}, Output: ${usageSummary.outputTokens.total ?? 0}, Total: ${totalTokens}`
|
|
1154
1290
|
);
|
|
1155
1291
|
controller.enqueue({
|
|
1156
1292
|
type: "finish",
|
|
@@ -1212,23 +1348,15 @@ var CodexCliLanguageModel = class {
|
|
|
1212
1348
|
}
|
|
1213
1349
|
}
|
|
1214
1350
|
});
|
|
1215
|
-
const cleanupSchema = () => {
|
|
1216
|
-
if (!schemaPath) return;
|
|
1217
|
-
try {
|
|
1218
|
-
const schemaDir = dirname(schemaPath);
|
|
1219
|
-
rmSync(schemaDir, { recursive: true, force: true });
|
|
1220
|
-
} catch {
|
|
1221
|
-
}
|
|
1222
|
-
};
|
|
1223
1351
|
child.on("error", (e) => {
|
|
1224
1352
|
this.logger.error(`[codex-cli] Stream spawn error: ${String(e)}`);
|
|
1225
1353
|
if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort);
|
|
1226
|
-
|
|
1354
|
+
cleanupTempFiles();
|
|
1227
1355
|
controller.error(this.handleSpawnError(e, promptExcerpt));
|
|
1228
1356
|
});
|
|
1229
1357
|
child.on("close", (code) => {
|
|
1230
1358
|
if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort);
|
|
1231
|
-
|
|
1359
|
+
cleanupTempFiles();
|
|
1232
1360
|
setImmediate(() => finishStream(code));
|
|
1233
1361
|
});
|
|
1234
1362
|
},
|
|
@@ -1262,8 +1390,8 @@ function createCodexCli(options = {}) {
|
|
|
1262
1390
|
};
|
|
1263
1391
|
provider.languageModel = createModel;
|
|
1264
1392
|
provider.chat = createModel;
|
|
1265
|
-
provider.
|
|
1266
|
-
throw new NoSuchModelError({ modelId, modelType: "
|
|
1393
|
+
provider.embeddingModel = ((modelId) => {
|
|
1394
|
+
throw new NoSuchModelError({ modelId, modelType: "embeddingModel" });
|
|
1267
1395
|
});
|
|
1268
1396
|
provider.imageModel = ((modelId) => {
|
|
1269
1397
|
throw new NoSuchModelError({ modelId, modelType: "imageModel" });
|
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
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
|
+
"description": "AI SDK v6 provider for OpenAI Codex CLI with native JSON Schema support",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-sdk",
|
|
7
7
|
"codex",
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
"test:coverage": "vitest run --coverage"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@ai-sdk/provider": "
|
|
63
|
-
"@ai-sdk/provider-utils": "
|
|
62
|
+
"@ai-sdk/provider": "3.0.0-beta.26",
|
|
63
|
+
"@ai-sdk/provider-utils": "4.0.0-beta.51",
|
|
64
64
|
"jsonc-parser": "^3.3.1"
|
|
65
65
|
},
|
|
66
66
|
"optionalDependencies": {
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"@eslint/js": "^9.14.0",
|
|
71
71
|
"@types/node": "20.19.6",
|
|
72
72
|
"@vitest/coverage-v8": "^3.2.4",
|
|
73
|
-
"ai": "
|
|
73
|
+
"ai": "6.0.0-beta.156",
|
|
74
74
|
"eslint": "^9.14.0",
|
|
75
75
|
"prettier": "^3.3.3",
|
|
76
76
|
"tsup": "8.5.0",
|