ai-sdk-provider-codex-cli 0.1.0-ai-sdk-v4
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/LICENSE +22 -0
- package/README.md +210 -0
- package/dist/index.cjs +547 -0
- package/dist/index.d.cts +63 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +541 -0
- package/package.json +88 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import { NoSuchModelError, LoadAPIKeyError, APICallError } from '@ai-sdk/provider';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
4
|
+
import { createRequire } from 'module';
|
|
5
|
+
import { mkdtempSync, readFileSync, rmSync } from 'fs';
|
|
6
|
+
import { tmpdir } from 'os';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { generateId } from '@ai-sdk/provider-utils';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
|
|
11
|
+
// src/codex-cli-provider.ts
|
|
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
|
+
// src/logger.ts
|
|
30
|
+
var defaultLogger = {
|
|
31
|
+
warn: (m) => console.warn(m),
|
|
32
|
+
error: (m) => console.error(m)
|
|
33
|
+
};
|
|
34
|
+
var noopLogger = {
|
|
35
|
+
warn: () => {
|
|
36
|
+
},
|
|
37
|
+
error: () => {
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
function getLogger(logger) {
|
|
41
|
+
if (logger === false) return noopLogger;
|
|
42
|
+
if (!logger) return defaultLogger;
|
|
43
|
+
return logger;
|
|
44
|
+
}
|
|
45
|
+
var settingsSchema = z.object({
|
|
46
|
+
codexPath: z.string().optional(),
|
|
47
|
+
cwd: z.string().optional(),
|
|
48
|
+
approvalMode: z.enum(["untrusted", "on-failure", "on-request", "never"]).optional(),
|
|
49
|
+
sandboxMode: z.enum(["read-only", "workspace-write", "danger-full-access"]).optional(),
|
|
50
|
+
fullAuto: z.boolean().optional(),
|
|
51
|
+
dangerouslyBypassApprovalsAndSandbox: z.boolean().optional(),
|
|
52
|
+
skipGitRepoCheck: z.boolean().optional(),
|
|
53
|
+
color: z.enum(["always", "never", "auto"]).optional(),
|
|
54
|
+
allowNpx: z.boolean().optional(),
|
|
55
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
56
|
+
verbose: z.boolean().optional(),
|
|
57
|
+
logger: z.any().optional()
|
|
58
|
+
}).strict();
|
|
59
|
+
function validateSettings(settings) {
|
|
60
|
+
const warnings = [];
|
|
61
|
+
const errors = [];
|
|
62
|
+
const parsed = settingsSchema.safeParse(settings);
|
|
63
|
+
if (!parsed.success) {
|
|
64
|
+
const raw = parsed.error;
|
|
65
|
+
let issues = [];
|
|
66
|
+
if (raw && typeof raw === "object") {
|
|
67
|
+
const v4 = raw.issues;
|
|
68
|
+
const v3 = raw.errors;
|
|
69
|
+
if (Array.isArray(v4)) issues = v4;
|
|
70
|
+
else if (Array.isArray(v3)) issues = v3;
|
|
71
|
+
}
|
|
72
|
+
for (const i of issues) {
|
|
73
|
+
const path = Array.isArray(i?.path) ? i.path.join(".") : "";
|
|
74
|
+
const message = i?.message || "Invalid value";
|
|
75
|
+
errors.push(`${path ? path + ": " : ""}${message}`);
|
|
76
|
+
}
|
|
77
|
+
return { valid: false, warnings, errors };
|
|
78
|
+
}
|
|
79
|
+
const s = parsed.data;
|
|
80
|
+
if (s.fullAuto && s.dangerouslyBypassApprovalsAndSandbox) {
|
|
81
|
+
warnings.push(
|
|
82
|
+
"Both fullAuto and dangerouslyBypassApprovalsAndSandbox specified; fullAuto takes precedence."
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return { valid: true, warnings, errors };
|
|
86
|
+
}
|
|
87
|
+
function validateModelId(modelId) {
|
|
88
|
+
if (!modelId || modelId.trim() === "") return "Model ID cannot be empty";
|
|
89
|
+
return void 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/message-mapper.ts
|
|
93
|
+
function isTextPart(p) {
|
|
94
|
+
return typeof p === "object" && p !== null && "type" in p && p.type === "text" && "text" in p && typeof p.text === "string";
|
|
95
|
+
}
|
|
96
|
+
function isImagePart(p) {
|
|
97
|
+
return typeof p === "object" && p !== null && "type" in p && p.type === "image";
|
|
98
|
+
}
|
|
99
|
+
function isToolItem(p) {
|
|
100
|
+
if (typeof p !== "object" || p === null) return false;
|
|
101
|
+
const obj = p;
|
|
102
|
+
if (typeof obj.toolName !== "string") return false;
|
|
103
|
+
const out = obj.output;
|
|
104
|
+
if (!out || out.type !== "text" && out.type !== "json") return false;
|
|
105
|
+
if (out.type === "text" && typeof out.value !== "string") return false;
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
function mapMessagesToPrompt(prompt, mode = { type: "regular" }, jsonSchema) {
|
|
109
|
+
const warnings = [];
|
|
110
|
+
const parts = [];
|
|
111
|
+
let systemText;
|
|
112
|
+
for (const msg of prompt) {
|
|
113
|
+
if (msg.role === "system") {
|
|
114
|
+
systemText = typeof msg.content === "string" ? msg.content : String(msg.content);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (msg.role === "user") {
|
|
118
|
+
if (typeof msg.content === "string") {
|
|
119
|
+
parts.push(`Human: ${msg.content}`);
|
|
120
|
+
} else if (Array.isArray(msg.content)) {
|
|
121
|
+
const text = msg.content.filter(isTextPart).map((p) => p.text).join("\n");
|
|
122
|
+
if (text) parts.push(`Human: ${text}`);
|
|
123
|
+
const images = msg.content.filter(isImagePart);
|
|
124
|
+
if (images.length) warnings.push("Image inputs ignored by Codex CLI integration.");
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (msg.role === "assistant") {
|
|
129
|
+
if (typeof msg.content === "string") {
|
|
130
|
+
parts.push(`Assistant: ${msg.content}`);
|
|
131
|
+
} else if (Array.isArray(msg.content)) {
|
|
132
|
+
const text = msg.content.filter(isTextPart).map((p) => p.text).join("\n");
|
|
133
|
+
if (text) parts.push(`Assistant: ${text}`);
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (msg.role === "tool") {
|
|
138
|
+
if (Array.isArray(msg.content)) {
|
|
139
|
+
for (const maybeTool of msg.content) {
|
|
140
|
+
if (!isToolItem(maybeTool)) continue;
|
|
141
|
+
const value = maybeTool.output.type === "text" ? maybeTool.output.value : JSON.stringify(maybeTool.output.value);
|
|
142
|
+
parts.push(`Tool Result (${maybeTool.toolName}): ${value}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
let promptText = "";
|
|
149
|
+
if (systemText) promptText += systemText + "\n\n";
|
|
150
|
+
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
|
+
return { promptText, ...warnings.length ? { warnings } : {} };
|
|
163
|
+
}
|
|
164
|
+
function createAPICallError({
|
|
165
|
+
message,
|
|
166
|
+
code,
|
|
167
|
+
exitCode,
|
|
168
|
+
stderr,
|
|
169
|
+
promptExcerpt,
|
|
170
|
+
isRetryable = false
|
|
171
|
+
}) {
|
|
172
|
+
const data = { code, exitCode, stderr, promptExcerpt };
|
|
173
|
+
return new APICallError({
|
|
174
|
+
message,
|
|
175
|
+
isRetryable,
|
|
176
|
+
url: "codex-cli://exec",
|
|
177
|
+
requestBodyValues: promptExcerpt ? { prompt: promptExcerpt } : void 0,
|
|
178
|
+
data
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function createAuthenticationError(message) {
|
|
182
|
+
return new LoadAPIKeyError({
|
|
183
|
+
message: message || "Authentication failed. Ensure Codex CLI is logged in (codex login)."
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function isAuthenticationError(err) {
|
|
187
|
+
if (err instanceof LoadAPIKeyError) return true;
|
|
188
|
+
if (err instanceof APICallError) {
|
|
189
|
+
const data = err.data;
|
|
190
|
+
if (data?.exitCode === 401) return true;
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/codex-cli-language-model.ts
|
|
196
|
+
function resolveCodexPath(explicitPath, allowNpx) {
|
|
197
|
+
if (explicitPath) return { cmd: "node", args: [explicitPath] };
|
|
198
|
+
try {
|
|
199
|
+
const req = createRequire(import.meta.url);
|
|
200
|
+
const pkgPath = req.resolve("@openai/codex/package.json");
|
|
201
|
+
const root = pkgPath.replace(/package\.json$/, "");
|
|
202
|
+
return { cmd: "node", args: [root + "bin/codex.js"] };
|
|
203
|
+
} catch {
|
|
204
|
+
if (allowNpx) return { cmd: "npx", args: ["-y", "@openai/codex"] };
|
|
205
|
+
return { cmd: "codex", args: [] };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
var CodexCliLanguageModel = class {
|
|
209
|
+
specificationVersion = "v1";
|
|
210
|
+
provider = "codex-cli";
|
|
211
|
+
defaultObjectGenerationMode = "json";
|
|
212
|
+
supportsImageUrls = false;
|
|
213
|
+
supportsStructuredOutputs = false;
|
|
214
|
+
modelId;
|
|
215
|
+
settings;
|
|
216
|
+
logger;
|
|
217
|
+
sessionId;
|
|
218
|
+
constructor(options) {
|
|
219
|
+
this.modelId = options.id;
|
|
220
|
+
this.settings = options.settings ?? {};
|
|
221
|
+
this.logger = getLogger(this.settings.logger);
|
|
222
|
+
if (!this.modelId || this.modelId.trim() === "") {
|
|
223
|
+
throw new NoSuchModelError({ modelId: this.modelId, modelType: "languageModel" });
|
|
224
|
+
}
|
|
225
|
+
const warn = validateModelId(this.modelId);
|
|
226
|
+
if (warn) this.logger.warn(`Codex CLI model: ${warn}`);
|
|
227
|
+
}
|
|
228
|
+
buildArgs(promptText) {
|
|
229
|
+
const base = resolveCodexPath(this.settings.codexPath, this.settings.allowNpx);
|
|
230
|
+
const args = [...base.args, "exec", "--json"];
|
|
231
|
+
if (this.settings.fullAuto) {
|
|
232
|
+
args.push("--full-auto");
|
|
233
|
+
} else if (this.settings.dangerouslyBypassApprovalsAndSandbox) {
|
|
234
|
+
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
235
|
+
} else {
|
|
236
|
+
const approval = this.settings.approvalMode ?? "on-failure";
|
|
237
|
+
args.push("-c", `approval_policy=${approval}`);
|
|
238
|
+
const sandbox = this.settings.sandboxMode ?? "workspace-write";
|
|
239
|
+
args.push("-c", `sandbox_mode=${sandbox}`);
|
|
240
|
+
}
|
|
241
|
+
if (this.settings.skipGitRepoCheck !== false) {
|
|
242
|
+
args.push("--skip-git-repo-check");
|
|
243
|
+
}
|
|
244
|
+
if (this.settings.color) {
|
|
245
|
+
args.push("--color", this.settings.color);
|
|
246
|
+
}
|
|
247
|
+
if (this.modelId) {
|
|
248
|
+
args.push("-m", this.modelId);
|
|
249
|
+
}
|
|
250
|
+
args.push(promptText);
|
|
251
|
+
const env = {
|
|
252
|
+
...process.env,
|
|
253
|
+
...this.settings.env || {},
|
|
254
|
+
RUST_LOG: process.env.RUST_LOG || "error"
|
|
255
|
+
};
|
|
256
|
+
let lastMessagePath = this.settings.outputLastMessageFile;
|
|
257
|
+
if (!lastMessagePath) {
|
|
258
|
+
const dir = mkdtempSync(join(tmpdir(), "codex-cli-"));
|
|
259
|
+
lastMessagePath = join(dir, "last-message.txt");
|
|
260
|
+
}
|
|
261
|
+
args.push("--output-last-message", lastMessagePath);
|
|
262
|
+
return { cmd: base.cmd, args, env, cwd: this.settings.cwd, lastMessagePath };
|
|
263
|
+
}
|
|
264
|
+
mapWarnings(options) {
|
|
265
|
+
const unsupported = [];
|
|
266
|
+
const add = (setting, name) => {
|
|
267
|
+
if (setting !== void 0)
|
|
268
|
+
unsupported.push({
|
|
269
|
+
type: "unsupported-setting",
|
|
270
|
+
setting: name,
|
|
271
|
+
details: `Codex CLI does not support ${name}; it will be ignored.`
|
|
272
|
+
});
|
|
273
|
+
};
|
|
274
|
+
add(options.temperature, "temperature");
|
|
275
|
+
add(options.maxTokens, "maxTokens");
|
|
276
|
+
add(options.topP, "topP");
|
|
277
|
+
add(options.topK, "topK");
|
|
278
|
+
add(options.presencePenalty, "presencePenalty");
|
|
279
|
+
add(options.frequencyPenalty, "frequencyPenalty");
|
|
280
|
+
add(options.stopSequences?.length ? options.stopSequences : void 0, "stopSequences");
|
|
281
|
+
add(options.seed, "seed");
|
|
282
|
+
return unsupported;
|
|
283
|
+
}
|
|
284
|
+
parseJsonLine(line) {
|
|
285
|
+
try {
|
|
286
|
+
return JSON.parse(line);
|
|
287
|
+
} catch {
|
|
288
|
+
return void 0;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
handleSpawnError(err, promptExcerpt) {
|
|
292
|
+
const e = err && typeof err === "object" ? err : void 0;
|
|
293
|
+
const message = String((e?.message ?? err) || "Failed to run Codex CLI");
|
|
294
|
+
if (/login|auth|unauthorized|not\s+logged/i.test(message)) {
|
|
295
|
+
throw createAuthenticationError(message);
|
|
296
|
+
}
|
|
297
|
+
throw createAPICallError({
|
|
298
|
+
message,
|
|
299
|
+
code: typeof e?.code === "string" ? e.code : void 0,
|
|
300
|
+
exitCode: typeof e?.exitCode === "number" ? e.exitCode : void 0,
|
|
301
|
+
stderr: typeof e?.stderr === "string" ? e.stderr : void 0,
|
|
302
|
+
promptExcerpt
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
async doGenerate(options) {
|
|
306
|
+
const mode = options.mode?.type === "object-json" ? { type: "object-json" } : { type: "regular" };
|
|
307
|
+
const { promptText, warnings: mappingWarnings } = mapMessagesToPrompt(
|
|
308
|
+
options.prompt,
|
|
309
|
+
mode,
|
|
310
|
+
options.mode?.type === "object-json" ? options.mode.schema : void 0
|
|
311
|
+
);
|
|
312
|
+
const promptExcerpt = promptText.slice(0, 200);
|
|
313
|
+
const warnings = [
|
|
314
|
+
...this.mapWarnings(options),
|
|
315
|
+
...mappingWarnings?.map((m) => ({ type: "other", message: m })) || []
|
|
316
|
+
];
|
|
317
|
+
const { cmd, args, env, cwd, lastMessagePath } = this.buildArgs(promptText);
|
|
318
|
+
let text = "";
|
|
319
|
+
const usage = { promptTokens: 0, completionTokens: 0 };
|
|
320
|
+
const finishReason = "stop";
|
|
321
|
+
const child = spawn(cmd, args, { env, cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
322
|
+
let onAbort;
|
|
323
|
+
if (options.abortSignal) {
|
|
324
|
+
if (options.abortSignal.aborted) {
|
|
325
|
+
child.kill("SIGTERM");
|
|
326
|
+
throw options.abortSignal.reason ?? new Error("Request aborted");
|
|
327
|
+
}
|
|
328
|
+
onAbort = () => child.kill("SIGTERM");
|
|
329
|
+
options.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
await new Promise((resolve, reject) => {
|
|
333
|
+
let stderr = "";
|
|
334
|
+
child.stderr.on("data", (d) => stderr += String(d));
|
|
335
|
+
child.stdout.setEncoding("utf8");
|
|
336
|
+
child.stdout.on("data", (chunk) => {
|
|
337
|
+
const lines = chunk.split(/\r?\n/).filter(Boolean);
|
|
338
|
+
for (const line of lines) {
|
|
339
|
+
const evt = this.parseJsonLine(line);
|
|
340
|
+
if (!evt) continue;
|
|
341
|
+
const msg = evt.msg;
|
|
342
|
+
const type = msg?.type;
|
|
343
|
+
if (type === "session_configured" && msg) {
|
|
344
|
+
this.sessionId = msg.session_id;
|
|
345
|
+
} else if (type === "task_complete" && msg) {
|
|
346
|
+
const last = msg.last_agent_message;
|
|
347
|
+
if (typeof last === "string") text = last;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
child.on("error", (e) => reject(this.handleSpawnError(e, promptExcerpt)));
|
|
352
|
+
child.on("close", (code) => {
|
|
353
|
+
if (code === 0) resolve();
|
|
354
|
+
else
|
|
355
|
+
reject(
|
|
356
|
+
createAPICallError({
|
|
357
|
+
message: `Codex CLI exited with code ${code}`,
|
|
358
|
+
exitCode: code ?? void 0,
|
|
359
|
+
stderr,
|
|
360
|
+
promptExcerpt
|
|
361
|
+
})
|
|
362
|
+
);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
} finally {
|
|
366
|
+
if (options.abortSignal && onAbort) options.abortSignal.removeEventListener("abort", onAbort);
|
|
367
|
+
}
|
|
368
|
+
if (!text && lastMessagePath) {
|
|
369
|
+
try {
|
|
370
|
+
const fileText = readFileSync(lastMessagePath, "utf8");
|
|
371
|
+
if (fileText && typeof fileText === "string") {
|
|
372
|
+
text = fileText.trim();
|
|
373
|
+
}
|
|
374
|
+
} catch {
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
rmSync(lastMessagePath, { force: true });
|
|
378
|
+
} catch {
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (options.mode?.type === "object-json" && text) {
|
|
382
|
+
text = extractJson(text);
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
text: text || void 0,
|
|
386
|
+
usage,
|
|
387
|
+
finishReason,
|
|
388
|
+
warnings: warnings.length ? warnings : void 0,
|
|
389
|
+
response: { id: generateId(), timestamp: /* @__PURE__ */ new Date(), modelId: this.modelId },
|
|
390
|
+
request: { body: promptText },
|
|
391
|
+
providerMetadata: {
|
|
392
|
+
"codex-cli": { ...this.sessionId ? { sessionId: this.sessionId } : {} }
|
|
393
|
+
},
|
|
394
|
+
rawCall: { rawPrompt: promptText, rawSettings: { model: this.modelId, settings: this.settings } }
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
async doStream(options) {
|
|
398
|
+
const mode = options.mode?.type === "object-json" ? { type: "object-json" } : { type: "regular" };
|
|
399
|
+
const { promptText, warnings: mappingWarnings } = mapMessagesToPrompt(
|
|
400
|
+
options.prompt,
|
|
401
|
+
mode,
|
|
402
|
+
options.mode?.type === "object-json" ? options.mode.schema : void 0
|
|
403
|
+
);
|
|
404
|
+
const promptExcerpt = promptText.slice(0, 200);
|
|
405
|
+
const warnings = [
|
|
406
|
+
...this.mapWarnings(options),
|
|
407
|
+
...mappingWarnings?.map((m) => ({ type: "other", message: m })) || []
|
|
408
|
+
];
|
|
409
|
+
const { cmd, args, env, cwd, lastMessagePath } = this.buildArgs(promptText);
|
|
410
|
+
const stream = new ReadableStream({
|
|
411
|
+
start: (controller) => {
|
|
412
|
+
const child = spawn(cmd, args, { env, cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
413
|
+
let stderr = "";
|
|
414
|
+
let accumulatedText = "";
|
|
415
|
+
const onAbort = () => {
|
|
416
|
+
child.kill("SIGTERM");
|
|
417
|
+
};
|
|
418
|
+
if (options.abortSignal) {
|
|
419
|
+
if (options.abortSignal.aborted) {
|
|
420
|
+
child.kill("SIGTERM");
|
|
421
|
+
controller.error(options.abortSignal.reason ?? new Error("Request aborted"));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
options.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
425
|
+
}
|
|
426
|
+
child.stderr.on("data", (d) => stderr += String(d));
|
|
427
|
+
child.stdout.setEncoding("utf8");
|
|
428
|
+
child.stdout.on("data", (chunk) => {
|
|
429
|
+
const lines = chunk.split(/\r?\n/).filter(Boolean);
|
|
430
|
+
for (const line of lines) {
|
|
431
|
+
const evt = this.parseJsonLine(line);
|
|
432
|
+
if (!evt) continue;
|
|
433
|
+
const msg = evt.msg;
|
|
434
|
+
const type = msg?.type;
|
|
435
|
+
if (type === "session_configured" && msg) {
|
|
436
|
+
this.sessionId = msg.session_id;
|
|
437
|
+
controller.enqueue({
|
|
438
|
+
type: "response-metadata",
|
|
439
|
+
id: randomUUID(),
|
|
440
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
441
|
+
modelId: this.modelId
|
|
442
|
+
});
|
|
443
|
+
} else if (type === "task_complete" && msg) {
|
|
444
|
+
const last = msg.last_agent_message;
|
|
445
|
+
if (typeof last === "string") {
|
|
446
|
+
accumulatedText = last;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
child.on("error", (e) => {
|
|
452
|
+
if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort);
|
|
453
|
+
controller.error(this.handleSpawnError(e, promptExcerpt));
|
|
454
|
+
});
|
|
455
|
+
child.on("close", (code) => {
|
|
456
|
+
if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort);
|
|
457
|
+
if (code !== 0) {
|
|
458
|
+
controller.error(
|
|
459
|
+
createAPICallError({
|
|
460
|
+
message: `Codex CLI exited with code ${code}`,
|
|
461
|
+
exitCode: code ?? void 0,
|
|
462
|
+
stderr,
|
|
463
|
+
promptExcerpt
|
|
464
|
+
})
|
|
465
|
+
);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
let finalText = accumulatedText;
|
|
469
|
+
if (!finalText && lastMessagePath) {
|
|
470
|
+
try {
|
|
471
|
+
const fileText = readFileSync(lastMessagePath, "utf8");
|
|
472
|
+
if (fileText) finalText = fileText.trim();
|
|
473
|
+
} catch {
|
|
474
|
+
}
|
|
475
|
+
try {
|
|
476
|
+
rmSync(lastMessagePath, { force: true });
|
|
477
|
+
} catch {
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (finalText) {
|
|
481
|
+
if (options.mode?.type === "object-json") {
|
|
482
|
+
finalText = extractJson(finalText);
|
|
483
|
+
}
|
|
484
|
+
controller.enqueue({ type: "text-delta", textDelta: finalText });
|
|
485
|
+
}
|
|
486
|
+
controller.enqueue({
|
|
487
|
+
type: "finish",
|
|
488
|
+
finishReason: "stop",
|
|
489
|
+
usage: { promptTokens: 0, completionTokens: 0 },
|
|
490
|
+
providerMetadata: {
|
|
491
|
+
"codex-cli": { ...this.sessionId ? { sessionId: this.sessionId } : {} }
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
controller.close();
|
|
495
|
+
});
|
|
496
|
+
},
|
|
497
|
+
cancel: () => {
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
return {
|
|
501
|
+
stream,
|
|
502
|
+
rawCall: { rawPrompt: promptText, rawSettings: { model: this.modelId, settings: this.settings } },
|
|
503
|
+
warnings: warnings.length ? warnings : void 0
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
// src/codex-cli-provider.ts
|
|
509
|
+
function createCodexCli(options = {}) {
|
|
510
|
+
const logger = getLogger(options.defaultSettings?.logger);
|
|
511
|
+
if (options.defaultSettings) {
|
|
512
|
+
const v = validateSettings(options.defaultSettings);
|
|
513
|
+
if (!v.valid) {
|
|
514
|
+
throw new Error(`Invalid default settings: ${v.errors.join(", ")}`);
|
|
515
|
+
}
|
|
516
|
+
for (const w of v.warnings) logger.warn(`Codex CLI Provider: ${w}`);
|
|
517
|
+
}
|
|
518
|
+
const createModel = (modelId, settings = {}) => {
|
|
519
|
+
const merged = { ...options.defaultSettings, ...settings };
|
|
520
|
+
const v = validateSettings(merged);
|
|
521
|
+
if (!v.valid) throw new Error(`Invalid settings: ${v.errors.join(", ")}`);
|
|
522
|
+
for (const w of v.warnings) logger.warn(`Codex CLI: ${w}`);
|
|
523
|
+
return new CodexCliLanguageModel({ id: modelId, settings: merged });
|
|
524
|
+
};
|
|
525
|
+
const provider = function(modelId, settings) {
|
|
526
|
+
if (new.target) throw new Error("The Codex CLI provider function cannot be called with new.");
|
|
527
|
+
return createModel(modelId, settings);
|
|
528
|
+
};
|
|
529
|
+
provider.languageModel = createModel;
|
|
530
|
+
provider.chat = createModel;
|
|
531
|
+
provider.textEmbeddingModel = ((modelId) => {
|
|
532
|
+
throw new NoSuchModelError({ modelId, modelType: "textEmbeddingModel" });
|
|
533
|
+
});
|
|
534
|
+
provider.imageModel = ((modelId) => {
|
|
535
|
+
throw new NoSuchModelError({ modelId, modelType: "imageModel" });
|
|
536
|
+
});
|
|
537
|
+
return provider;
|
|
538
|
+
}
|
|
539
|
+
var codexCli = createCodexCli();
|
|
540
|
+
|
|
541
|
+
export { CodexCliLanguageModel, codexCli, createCodexCli, isAuthenticationError };
|
package/package.json
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-sdk-provider-codex-cli",
|
|
3
|
+
"version": "0.1.0-ai-sdk-v4",
|
|
4
|
+
"description": "AI SDK v4 provider for spawning OpenAI Codex CLI (gpt-5 via ChatGPT OAuth)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai-sdk",
|
|
7
|
+
"codex",
|
|
8
|
+
"openai",
|
|
9
|
+
"cli",
|
|
10
|
+
"language-model",
|
|
11
|
+
"gpt-5",
|
|
12
|
+
"provider"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/ben-vargas/ai-sdk-provider-codex-cli",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/ben-vargas/ai-sdk-provider-codex-cli.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/ben-vargas/ai-sdk-provider-codex-cli/issues"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": "Ben Vargas",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"main": "./dist/index.cjs",
|
|
27
|
+
"module": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
"./package.json": "./package.json",
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"import": "./dist/index.js",
|
|
34
|
+
"require": "./dist/index.cjs"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist/**/*",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsup",
|
|
44
|
+
"clean": "rm -rf dist",
|
|
45
|
+
"dev": "tsup --watch",
|
|
46
|
+
"prepare": "npm run build",
|
|
47
|
+
"prepublishOnly": "npm run clean && npm run build",
|
|
48
|
+
"typecheck": "tsc --noEmit",
|
|
49
|
+
"format": "prettier --check .",
|
|
50
|
+
"format:fix": "prettier --write .",
|
|
51
|
+
"lint": "eslint .",
|
|
52
|
+
"lint:fix": "eslint . --fix",
|
|
53
|
+
"validate": "npm run build && npm run typecheck && npm run format && npm run lint && npm run test",
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"test:watch": "vitest",
|
|
56
|
+
"test:coverage": "vitest run --coverage"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@ai-sdk/provider": "1.1.3",
|
|
60
|
+
"@ai-sdk/provider-utils": "2.2.8",
|
|
61
|
+
"jsonc-parser": "^3.3.1"
|
|
62
|
+
},
|
|
63
|
+
"optionalDependencies": {
|
|
64
|
+
"@openai/codex": "*"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@eslint/js": "^9.14.0",
|
|
68
|
+
"@types/node": "20.17.24",
|
|
69
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
70
|
+
"ai": "4.3.16",
|
|
71
|
+
"eslint": "^9.14.0",
|
|
72
|
+
"prettier": "^3.3.3",
|
|
73
|
+
"tsup": "8.5.0",
|
|
74
|
+
"typescript": "5.6.3",
|
|
75
|
+
"vitest": "^3.2.4",
|
|
76
|
+
"typescript-eslint": "^8.6.0",
|
|
77
|
+
"zod": "3.24.1"
|
|
78
|
+
},
|
|
79
|
+
"peerDependencies": {
|
|
80
|
+
"zod": "^3.0.0"
|
|
81
|
+
},
|
|
82
|
+
"engines": {
|
|
83
|
+
"node": ">=18"
|
|
84
|
+
},
|
|
85
|
+
"publishConfig": {
|
|
86
|
+
"access": "public"
|
|
87
|
+
}
|
|
88
|
+
}
|