ai-sdk-provider-codex-cli 0.1.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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ben Vargas
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # AI SDK Provider for Codex CLI
2
+
3
+ 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 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.
4
+
5
+ - Works with `generateText`, `streamText`, and `generateObject` (JSON schemas via prompt engineering)
6
+ - Uses ChatGPT OAuth from `codex login` (tokens in `~/.codex/auth.json`) or `OPENAI_API_KEY`
7
+ - Node-only (spawns a local process); supports CI and local dev
8
+
9
+ ## Installation
10
+
11
+ 1. Install and authenticate Codex CLI
12
+
13
+ ```bash
14
+ npm i -g @openai/codex
15
+ codex login # or set OPENAI_API_KEY
16
+ ```
17
+
18
+ 2. Install provider and AI SDK
19
+
20
+ ```bash
21
+ npm i ai ai-sdk-provider-codex-cli
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ Text generation
27
+
28
+ ```js
29
+ import { generateText } from 'ai';
30
+ import { codexCli } from 'ai-sdk-provider-codex-cli';
31
+
32
+ const model = codexCli('gpt-5', {
33
+ allowNpx: true,
34
+ skipGitRepoCheck: true,
35
+ approvalMode: 'on-failure',
36
+ sandboxMode: 'workspace-write',
37
+ });
38
+
39
+ const { text } = await generateText({
40
+ model,
41
+ prompt: 'Reply with a single word: hello.',
42
+ });
43
+ console.log(text);
44
+ ```
45
+
46
+ Streaming
47
+
48
+ ```js
49
+ import { streamText } from 'ai';
50
+ import { codexCli } from 'ai-sdk-provider-codex-cli';
51
+
52
+ const { textStream } = await streamText({
53
+ model: codexCli('gpt-5', { allowNpx: true, skipGitRepoCheck: true }),
54
+ prompt: 'Write two short lines of encouragement.',
55
+ });
56
+ for await (const chunk of textStream) process.stdout.write(chunk);
57
+ ```
58
+
59
+ Object generation (Zod)
60
+
61
+ ```js
62
+ import { generateObject } from 'ai';
63
+ import { z } from 'zod';
64
+ import { codexCli } from 'ai-sdk-provider-codex-cli';
65
+
66
+ const schema = z.object({ name: z.string(), age: z.number().int() });
67
+ const { object } = await generateObject({
68
+ model: codexCli('gpt-5', { allowNpx: true, skipGitRepoCheck: true }),
69
+ schema,
70
+ prompt: 'Generate a small user profile.',
71
+ });
72
+ console.log(object);
73
+ ```
74
+
75
+ ## Features
76
+
77
+ - AI SDK v5 compatible (LanguageModelV2)
78
+ - Streaming and non‑streaming
79
+ - JSON object generation with Zod schemas (prompt‑engineered)
80
+ - Safe defaults for non‑interactive automation (`on-failure`, `workspace-write`, `--skip-git-repo-check`)
81
+ - Fallback to `npx @openai/codex` when not on PATH (`allowNpx`)
82
+
83
+ ### Streaming behavior
84
+
85
+ When using `codex exec --json`, the Codex CLI intentionally suppresses token/assistant deltas in its JSON event stream. Instead, it writes the final assistant message to the file you pass via `--output-last-message` and then signals completion. This provider:
86
+
87
+ - emits `response-metadata` early (as soon as the session is configured), and
88
+ - returns the final text as a single `text-delta` right before `finish`.
89
+
90
+ This is expected behavior for JSON mode in Codex exec, so streaming typically “feels” like a final chunk rather than a gradual trickle.
91
+
92
+ ## Documentation
93
+
94
+ - Getting started, configuration, and troubleshooting live in `docs/`:
95
+ - [docs/ai-sdk-v5/guide.md](docs/ai-sdk-v5/guide.md) – full usage guide and examples
96
+ - [docs/ai-sdk-v5/configuration.md](docs/ai-sdk-v5/configuration.md) – all settings and how they map to CLI flags
97
+ - [docs/ai-sdk-v5/troubleshooting.md](docs/ai-sdk-v5/troubleshooting.md) – common issues and fixes
98
+ - [docs/ai-sdk-v5/limitations.md](docs/ai-sdk-v5/limitations.md) – known constraints and behavior differences
99
+ - See [examples/](examples/) for runnable scripts covering core usage, streaming, permissions/sandboxing, and object generation.
100
+
101
+ ## Authentication
102
+
103
+ - Preferred: ChatGPT OAuth via `codex login` (stores tokens at `~/.codex/auth.json`)
104
+ - Alternative: export `OPENAI_API_KEY` in the provider’s `env` settings (forwarded to the spawned process)
105
+
106
+ ## Configuration (high level)
107
+
108
+ - `allowNpx`: If true, falls back to `npx -y @openai/codex` when Codex is not on PATH
109
+ - `cwd`: Working directory for Codex
110
+ - Autonomy/sandbox:
111
+ - `fullAuto` (equivalent to `--full-auto`)
112
+ - `dangerouslyBypassApprovalsAndSandbox` (bypass approvals and sandbox; dangerous)
113
+ - Otherwise the provider writes `-c approval_policy=...` and `-c sandbox_mode=...` for you; defaults to `on-failure` and `workspace-write`
114
+ - `skipGitRepoCheck`: enable by default for CI/non‑repo contexts
115
+ - `color`: `always` | `never` | `auto`
116
+ - `outputLastMessageFile`: by default the provider sets a temp path and reads it to capture final text reliably
117
+
118
+ See [docs/ai-sdk-v5/configuration.md](docs/ai-sdk-v5/configuration.md) for the full list and examples.
119
+
120
+ ## Zod Compatibility
121
+
122
+ - Peer supports `zod@^3 || ^4`
123
+ - Validation logic normalizes v3/v4 error shapes
124
+
125
+ ## Limitations
126
+
127
+ - Node ≥ 18, local process only (no Edge)
128
+ - Codex JSON mode (`codex exec --json`) suppresses mid‑response deltas; streaming typically yields a final chunk. The CLI writes the final assistant text via `--output-last-message`, which this provider reads and emits at the end.
129
+ - Some AI SDK parameters are unsupported by Codex CLI (e.g., temperature/topP/penalties); the provider surfaces warnings and ignores them
130
+
131
+ ## Disclaimer
132
+
133
+ 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.
134
+
135
+ ## License
136
+
137
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,541 @@
1
+ 'use strict';
2
+
3
+ var provider = require('@ai-sdk/provider');
4
+ var child_process = require('child_process');
5
+ var crypto = require('crypto');
6
+ var module$1 = require('module');
7
+ var fs = require('fs');
8
+ var os = require('os');
9
+ var path = require('path');
10
+ var providerUtils = require('@ai-sdk/provider-utils');
11
+ var zod = require('zod');
12
+
13
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
14
+ // src/codex-cli-provider.ts
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
+ // src/logger.ts
33
+ var defaultLogger = {
34
+ warn: (m) => console.warn(m),
35
+ error: (m) => console.error(m)
36
+ };
37
+ var noopLogger = {
38
+ warn: () => {
39
+ },
40
+ error: () => {
41
+ }
42
+ };
43
+ function getLogger(logger) {
44
+ if (logger === false) return noopLogger;
45
+ if (!logger) return defaultLogger;
46
+ return logger;
47
+ }
48
+ var settingsSchema = zod.z.object({
49
+ codexPath: zod.z.string().optional(),
50
+ cwd: zod.z.string().optional(),
51
+ approvalMode: zod.z.enum(["untrusted", "on-failure", "on-request", "never"]).optional(),
52
+ sandboxMode: zod.z.enum(["read-only", "workspace-write", "danger-full-access"]).optional(),
53
+ fullAuto: zod.z.boolean().optional(),
54
+ dangerouslyBypassApprovalsAndSandbox: zod.z.boolean().optional(),
55
+ skipGitRepoCheck: zod.z.boolean().optional(),
56
+ color: zod.z.enum(["always", "never", "auto"]).optional(),
57
+ allowNpx: zod.z.boolean().optional(),
58
+ env: zod.z.record(zod.z.string(), zod.z.string()).optional(),
59
+ verbose: zod.z.boolean().optional(),
60
+ logger: zod.z.any().optional()
61
+ }).strict();
62
+ function validateSettings(settings) {
63
+ const warnings = [];
64
+ const errors = [];
65
+ const parsed = settingsSchema.safeParse(settings);
66
+ if (!parsed.success) {
67
+ const raw = parsed.error;
68
+ let issues = [];
69
+ if (raw && typeof raw === "object") {
70
+ const v4 = raw.issues;
71
+ const v3 = raw.errors;
72
+ if (Array.isArray(v4)) issues = v4;
73
+ else if (Array.isArray(v3)) issues = v3;
74
+ }
75
+ for (const i of issues) {
76
+ const path = Array.isArray(i?.path) ? i.path.join(".") : "";
77
+ const message = i?.message || "Invalid value";
78
+ errors.push(`${path ? path + ": " : ""}${message}`);
79
+ }
80
+ return { valid: false, warnings, errors };
81
+ }
82
+ const s = parsed.data;
83
+ if (s.fullAuto && s.dangerouslyBypassApprovalsAndSandbox) {
84
+ warnings.push(
85
+ "Both fullAuto and dangerouslyBypassApprovalsAndSandbox specified; fullAuto takes precedence."
86
+ );
87
+ }
88
+ return { valid: true, warnings, errors };
89
+ }
90
+ function validateModelId(modelId) {
91
+ if (!modelId || modelId.trim() === "") return "Model ID cannot be empty";
92
+ return void 0;
93
+ }
94
+
95
+ // src/message-mapper.ts
96
+ function isTextPart(p) {
97
+ return typeof p === "object" && p !== null && "type" in p && p.type === "text" && "text" in p && typeof p.text === "string";
98
+ }
99
+ function isImagePart(p) {
100
+ return typeof p === "object" && p !== null && "type" in p && p.type === "image";
101
+ }
102
+ function isToolItem(p) {
103
+ if (typeof p !== "object" || p === null) return false;
104
+ const obj = p;
105
+ if (typeof obj.toolName !== "string") return false;
106
+ const out = obj.output;
107
+ if (!out || out.type !== "text" && out.type !== "json") return false;
108
+ if (out.type === "text" && typeof out.value !== "string") return false;
109
+ return true;
110
+ }
111
+ function mapMessagesToPrompt(prompt, mode = { type: "regular" }, jsonSchema) {
112
+ const warnings = [];
113
+ const parts = [];
114
+ let systemText;
115
+ for (const msg of prompt) {
116
+ if (msg.role === "system") {
117
+ systemText = typeof msg.content === "string" ? msg.content : String(msg.content);
118
+ continue;
119
+ }
120
+ if (msg.role === "user") {
121
+ if (typeof msg.content === "string") {
122
+ parts.push(`Human: ${msg.content}`);
123
+ } else if (Array.isArray(msg.content)) {
124
+ const text = msg.content.filter(isTextPart).map((p) => p.text).join("\n");
125
+ if (text) parts.push(`Human: ${text}`);
126
+ const images = msg.content.filter(isImagePart);
127
+ if (images.length) warnings.push("Image inputs ignored by Codex CLI integration.");
128
+ }
129
+ continue;
130
+ }
131
+ if (msg.role === "assistant") {
132
+ if (typeof msg.content === "string") {
133
+ parts.push(`Assistant: ${msg.content}`);
134
+ } else if (Array.isArray(msg.content)) {
135
+ const text = msg.content.filter(isTextPart).map((p) => p.text).join("\n");
136
+ if (text) parts.push(`Assistant: ${text}`);
137
+ }
138
+ continue;
139
+ }
140
+ if (msg.role === "tool") {
141
+ if (Array.isArray(msg.content)) {
142
+ for (const maybeTool of msg.content) {
143
+ if (!isToolItem(maybeTool)) continue;
144
+ const value = maybeTool.output.type === "text" ? maybeTool.output.value : JSON.stringify(maybeTool.output.value);
145
+ parts.push(`Tool Result (${maybeTool.toolName}): ${value}`);
146
+ }
147
+ }
148
+ continue;
149
+ }
150
+ }
151
+ let promptText = "";
152
+ if (systemText) promptText += systemText + "\n\n";
153
+ 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
+ return { promptText, ...warnings.length ? { warnings } : {} };
166
+ }
167
+ function createAPICallError({
168
+ message,
169
+ code,
170
+ exitCode,
171
+ stderr,
172
+ promptExcerpt,
173
+ isRetryable = false
174
+ }) {
175
+ const data = { code, exitCode, stderr, promptExcerpt };
176
+ return new provider.APICallError({
177
+ message,
178
+ isRetryable,
179
+ url: "codex-cli://exec",
180
+ requestBodyValues: promptExcerpt ? { prompt: promptExcerpt } : void 0,
181
+ data
182
+ });
183
+ }
184
+ function createAuthenticationError(message) {
185
+ return new provider.LoadAPIKeyError({
186
+ message: message || "Authentication failed. Ensure Codex CLI is logged in (codex login)."
187
+ });
188
+ }
189
+ function isAuthenticationError(err) {
190
+ if (err instanceof provider.LoadAPIKeyError) return true;
191
+ if (err instanceof provider.APICallError) {
192
+ const data = err.data;
193
+ if (data?.exitCode === 401) return true;
194
+ }
195
+ return false;
196
+ }
197
+
198
+ // src/codex-cli-language-model.ts
199
+ function resolveCodexPath(explicitPath, allowNpx) {
200
+ if (explicitPath) return { cmd: "node", args: [explicitPath] };
201
+ try {
202
+ const req = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
203
+ const pkgPath = req.resolve("@openai/codex/package.json");
204
+ const root = pkgPath.replace(/package\.json$/, "");
205
+ return { cmd: "node", args: [root + "bin/codex.js"] };
206
+ } catch {
207
+ if (allowNpx) return { cmd: "npx", args: ["-y", "@openai/codex"] };
208
+ return { cmd: "codex", args: [] };
209
+ }
210
+ }
211
+ var CodexCliLanguageModel = class {
212
+ specificationVersion = "v2";
213
+ provider = "codex-cli";
214
+ defaultObjectGenerationMode = "json";
215
+ supportsImageUrls = false;
216
+ supportedUrls = {};
217
+ supportsStructuredOutputs = false;
218
+ modelId;
219
+ settings;
220
+ logger;
221
+ sessionId;
222
+ constructor(options) {
223
+ this.modelId = options.id;
224
+ this.settings = options.settings ?? {};
225
+ this.logger = getLogger(this.settings.logger);
226
+ if (!this.modelId || this.modelId.trim() === "") {
227
+ throw new provider.NoSuchModelError({ modelId: this.modelId, modelType: "languageModel" });
228
+ }
229
+ const warn = validateModelId(this.modelId);
230
+ if (warn) this.logger.warn(`Codex CLI model: ${warn}`);
231
+ }
232
+ buildArgs(promptText) {
233
+ const base = resolveCodexPath(this.settings.codexPath, this.settings.allowNpx);
234
+ const args = [...base.args, "exec", "--json"];
235
+ if (this.settings.fullAuto) {
236
+ args.push("--full-auto");
237
+ } else if (this.settings.dangerouslyBypassApprovalsAndSandbox) {
238
+ args.push("--dangerously-bypass-approvals-and-sandbox");
239
+ } else {
240
+ const approval = this.settings.approvalMode ?? "on-failure";
241
+ args.push("-c", `approval_policy=${approval}`);
242
+ const sandbox = this.settings.sandboxMode ?? "workspace-write";
243
+ args.push("-c", `sandbox_mode=${sandbox}`);
244
+ }
245
+ if (this.settings.skipGitRepoCheck !== false) {
246
+ args.push("--skip-git-repo-check");
247
+ }
248
+ if (this.settings.color) {
249
+ args.push("--color", this.settings.color);
250
+ }
251
+ if (this.modelId) {
252
+ args.push("-m", this.modelId);
253
+ }
254
+ args.push(promptText);
255
+ const env = {
256
+ ...process.env,
257
+ ...this.settings.env || {},
258
+ RUST_LOG: process.env.RUST_LOG || "error"
259
+ };
260
+ let lastMessagePath = this.settings.outputLastMessageFile;
261
+ if (!lastMessagePath) {
262
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "codex-cli-"));
263
+ lastMessagePath = path.join(dir, "last-message.txt");
264
+ }
265
+ args.push("--output-last-message", lastMessagePath);
266
+ return { cmd: base.cmd, args, env, cwd: this.settings.cwd, lastMessagePath };
267
+ }
268
+ mapWarnings(options) {
269
+ const unsupported = [];
270
+ const add = (setting, name) => {
271
+ if (setting !== void 0)
272
+ unsupported.push({
273
+ type: "unsupported-setting",
274
+ setting: name,
275
+ details: `Codex CLI does not support ${name}; it will be ignored.`
276
+ });
277
+ };
278
+ add(options.temperature, "temperature");
279
+ add(options.topP, "topP");
280
+ add(options.topK, "topK");
281
+ add(options.presencePenalty, "presencePenalty");
282
+ add(options.frequencyPenalty, "frequencyPenalty");
283
+ add(options.stopSequences?.length ? options.stopSequences : void 0, "stopSequences");
284
+ add(options.seed, "seed");
285
+ return unsupported;
286
+ }
287
+ parseJsonLine(line) {
288
+ try {
289
+ return JSON.parse(line);
290
+ } catch {
291
+ return void 0;
292
+ }
293
+ }
294
+ handleSpawnError(err, promptExcerpt) {
295
+ const e = err && typeof err === "object" ? err : void 0;
296
+ const message = String((e?.message ?? err) || "Failed to run Codex CLI");
297
+ if (/login|auth|unauthorized|not\s+logged/i.test(message)) {
298
+ throw createAuthenticationError(message);
299
+ }
300
+ throw createAPICallError({
301
+ message,
302
+ code: typeof e?.code === "string" ? e.code : void 0,
303
+ exitCode: typeof e?.exitCode === "number" ? e.exitCode : void 0,
304
+ stderr: typeof e?.stderr === "string" ? e.stderr : void 0,
305
+ promptExcerpt
306
+ });
307
+ }
308
+ async doGenerate(options) {
309
+ const mode = options.responseFormat?.type === "json" ? { type: "object-json" } : { type: "regular" };
310
+ const { promptText, warnings: mappingWarnings } = mapMessagesToPrompt(
311
+ options.prompt,
312
+ mode,
313
+ options.responseFormat?.type === "json" ? options.responseFormat.schema : void 0
314
+ );
315
+ const promptExcerpt = promptText.slice(0, 200);
316
+ const warnings = [
317
+ ...this.mapWarnings(options),
318
+ ...mappingWarnings?.map((m) => ({ type: "other", message: m })) || []
319
+ ];
320
+ const { cmd, args, env, cwd, lastMessagePath } = this.buildArgs(promptText);
321
+ let text = "";
322
+ const usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
323
+ const finishReason = "stop";
324
+ const child = child_process.spawn(cmd, args, { env, cwd, stdio: ["ignore", "pipe", "pipe"] });
325
+ let onAbort;
326
+ if (options.abortSignal) {
327
+ if (options.abortSignal.aborted) {
328
+ child.kill("SIGTERM");
329
+ throw options.abortSignal.reason ?? new Error("Request aborted");
330
+ }
331
+ onAbort = () => child.kill("SIGTERM");
332
+ options.abortSignal.addEventListener("abort", onAbort, { once: true });
333
+ }
334
+ try {
335
+ await new Promise((resolve, reject) => {
336
+ let stderr = "";
337
+ child.stderr.on("data", (d) => stderr += String(d));
338
+ child.stdout.setEncoding("utf8");
339
+ child.stdout.on("data", (chunk) => {
340
+ const lines = chunk.split(/\r?\n/).filter(Boolean);
341
+ for (const line of lines) {
342
+ const evt = this.parseJsonLine(line);
343
+ if (!evt) continue;
344
+ const msg = evt.msg;
345
+ const type = msg?.type;
346
+ if (type === "session_configured" && msg) {
347
+ this.sessionId = msg.session_id;
348
+ } else if (type === "task_complete" && msg) {
349
+ const last = msg.last_agent_message;
350
+ if (typeof last === "string") text = last;
351
+ }
352
+ }
353
+ });
354
+ child.on("error", (e) => reject(this.handleSpawnError(e, promptExcerpt)));
355
+ child.on("close", (code) => {
356
+ if (code === 0) resolve();
357
+ else
358
+ reject(
359
+ createAPICallError({
360
+ message: `Codex CLI exited with code ${code}`,
361
+ exitCode: code ?? void 0,
362
+ stderr,
363
+ promptExcerpt
364
+ })
365
+ );
366
+ });
367
+ });
368
+ } finally {
369
+ if (options.abortSignal && onAbort) options.abortSignal.removeEventListener("abort", onAbort);
370
+ }
371
+ if (!text && lastMessagePath) {
372
+ try {
373
+ const fileText = fs.readFileSync(lastMessagePath, "utf8");
374
+ if (fileText && typeof fileText === "string") {
375
+ text = fileText.trim();
376
+ }
377
+ } catch {
378
+ }
379
+ try {
380
+ fs.rmSync(lastMessagePath, { force: true });
381
+ } catch {
382
+ }
383
+ }
384
+ if (options.responseFormat?.type === "json" && text) {
385
+ text = extractJson(text);
386
+ }
387
+ const content = [{ type: "text", text }];
388
+ return {
389
+ content,
390
+ usage,
391
+ finishReason,
392
+ warnings,
393
+ response: { id: providerUtils.generateId(), timestamp: /* @__PURE__ */ new Date(), modelId: this.modelId },
394
+ request: { body: promptText },
395
+ providerMetadata: {
396
+ "codex-cli": { ...this.sessionId ? { sessionId: this.sessionId } : {} }
397
+ }
398
+ };
399
+ }
400
+ async doStream(options) {
401
+ const mode = options.responseFormat?.type === "json" ? { type: "object-json" } : { type: "regular" };
402
+ const { promptText, warnings: mappingWarnings } = mapMessagesToPrompt(
403
+ options.prompt,
404
+ mode,
405
+ options.responseFormat?.type === "json" ? options.responseFormat.schema : void 0
406
+ );
407
+ const promptExcerpt = promptText.slice(0, 200);
408
+ const warnings = [
409
+ ...this.mapWarnings(options),
410
+ ...mappingWarnings?.map((m) => ({ type: "other", message: m })) || []
411
+ ];
412
+ const { cmd, args, env, cwd, lastMessagePath } = this.buildArgs(promptText);
413
+ const stream = new ReadableStream({
414
+ start: (controller) => {
415
+ const child = child_process.spawn(cmd, args, { env, cwd, stdio: ["ignore", "pipe", "pipe"] });
416
+ controller.enqueue({ type: "stream-start", warnings });
417
+ let stderr = "";
418
+ let accumulatedText = "";
419
+ const onAbort = () => {
420
+ child.kill("SIGTERM");
421
+ };
422
+ if (options.abortSignal) {
423
+ if (options.abortSignal.aborted) {
424
+ child.kill("SIGTERM");
425
+ controller.error(options.abortSignal.reason ?? new Error("Request aborted"));
426
+ return;
427
+ }
428
+ options.abortSignal.addEventListener("abort", onAbort, { once: true });
429
+ }
430
+ child.stderr.on("data", (d) => stderr += String(d));
431
+ child.stdout.setEncoding("utf8");
432
+ child.stdout.on("data", (chunk) => {
433
+ const lines = chunk.split(/\r?\n/).filter(Boolean);
434
+ for (const line of lines) {
435
+ const evt = this.parseJsonLine(line);
436
+ if (!evt) continue;
437
+ const msg = evt.msg;
438
+ const type = msg?.type;
439
+ if (type === "session_configured" && msg) {
440
+ this.sessionId = msg.session_id;
441
+ controller.enqueue({
442
+ type: "response-metadata",
443
+ id: crypto.randomUUID(),
444
+ timestamp: /* @__PURE__ */ new Date(),
445
+ modelId: this.modelId
446
+ });
447
+ } else if (type === "task_complete" && msg) {
448
+ const last = msg.last_agent_message;
449
+ if (typeof last === "string") {
450
+ accumulatedText = last;
451
+ }
452
+ }
453
+ }
454
+ });
455
+ child.on("error", (e) => {
456
+ if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort);
457
+ controller.error(this.handleSpawnError(e, promptExcerpt));
458
+ });
459
+ child.on("close", (code) => {
460
+ if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort);
461
+ if (code !== 0) {
462
+ controller.error(
463
+ createAPICallError({
464
+ message: `Codex CLI exited with code ${code}`,
465
+ exitCode: code ?? void 0,
466
+ stderr,
467
+ promptExcerpt
468
+ })
469
+ );
470
+ return;
471
+ }
472
+ let finalText = accumulatedText;
473
+ if (!finalText && lastMessagePath) {
474
+ try {
475
+ const fileText = fs.readFileSync(lastMessagePath, "utf8");
476
+ if (fileText) finalText = fileText.trim();
477
+ } catch {
478
+ }
479
+ try {
480
+ fs.rmSync(lastMessagePath, { force: true });
481
+ } catch {
482
+ }
483
+ }
484
+ if (finalText) {
485
+ if (options.responseFormat?.type === "json") {
486
+ finalText = extractJson(finalText);
487
+ }
488
+ controller.enqueue({ type: "text-delta", id: crypto.randomUUID(), delta: finalText });
489
+ }
490
+ controller.enqueue({
491
+ type: "finish",
492
+ finishReason: "stop",
493
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
494
+ });
495
+ controller.close();
496
+ });
497
+ },
498
+ cancel: () => {
499
+ }
500
+ });
501
+ return { stream, request: { body: promptText } };
502
+ }
503
+ };
504
+
505
+ // src/codex-cli-provider.ts
506
+ function createCodexCli(options = {}) {
507
+ const logger = getLogger(options.defaultSettings?.logger);
508
+ if (options.defaultSettings) {
509
+ const v = validateSettings(options.defaultSettings);
510
+ if (!v.valid) {
511
+ throw new Error(`Invalid default settings: ${v.errors.join(", ")}`);
512
+ }
513
+ for (const w of v.warnings) logger.warn(`Codex CLI Provider: ${w}`);
514
+ }
515
+ const createModel = (modelId, settings = {}) => {
516
+ const merged = { ...options.defaultSettings, ...settings };
517
+ const v = validateSettings(merged);
518
+ if (!v.valid) throw new Error(`Invalid settings: ${v.errors.join(", ")}`);
519
+ for (const w of v.warnings) logger.warn(`Codex CLI: ${w}`);
520
+ return new CodexCliLanguageModel({ id: modelId, settings: merged });
521
+ };
522
+ const provider$1 = function(modelId, settings) {
523
+ if (new.target) throw new Error("The Codex CLI provider function cannot be called with new.");
524
+ return createModel(modelId, settings);
525
+ };
526
+ provider$1.languageModel = createModel;
527
+ provider$1.chat = createModel;
528
+ provider$1.textEmbeddingModel = ((modelId) => {
529
+ throw new provider.NoSuchModelError({ modelId, modelType: "textEmbeddingModel" });
530
+ });
531
+ provider$1.imageModel = ((modelId) => {
532
+ throw new provider.NoSuchModelError({ modelId, modelType: "imageModel" });
533
+ });
534
+ return provider$1;
535
+ }
536
+ var codexCli = createCodexCli();
537
+
538
+ exports.CodexCliLanguageModel = CodexCliLanguageModel;
539
+ exports.codexCli = codexCli;
540
+ exports.createCodexCli = createCodexCli;
541
+ exports.isAuthenticationError = isAuthenticationError;