UNPKG

ai-sdk-provider-codex-cli

Version:

AI SDK v5 provider for OpenAI Codex CLI with native JSON Schema support

1,108 lines (1,101 loc) 41.6 kB
import { NoSuchModelError, LoadAPIKeyError, APICallError } from '@ai-sdk/provider'; import { spawn } from 'child_process'; import { randomUUID } from 'crypto'; import { createRequire } from 'module'; import { mkdtempSync, writeFileSync, rmSync, readFileSync } from 'fs'; import { tmpdir } from 'os'; import { join, dirname } from 'path'; import { z } from 'zod'; import { parseProviderOptions, generateId } from '@ai-sdk/provider-utils'; // src/codex-cli-provider.ts // src/logger.ts var defaultLogger = { debug: (message) => console.debug(`[DEBUG] ${message}`), info: (message) => console.info(`[INFO] ${message}`), warn: (message) => console.warn(`[WARN] ${message}`), error: (message) => console.error(`[ERROR] ${message}`) }; var noopLogger = { debug: () => { }, info: () => { }, warn: () => { }, error: () => { } }; function getLogger(logger) { if (logger === false) { return noopLogger; } if (logger === void 0) { return defaultLogger; } return logger; } function createVerboseLogger(logger, verbose = false) { if (verbose) { return logger; } return { debug: () => { }, // No-op when not verbose info: () => { }, // No-op when not verbose warn: logger.warn.bind(logger), error: logger.error.bind(logger) }; } var loggerFunctionSchema = z.object({ debug: z.any().refine((val) => typeof val === "function", { message: "debug must be a function" }), info: z.any().refine((val) => typeof val === "function", { message: "info must be a function" }), warn: z.any().refine((val) => typeof val === "function", { message: "warn must be a function" }), error: z.any().refine((val) => typeof val === "function", { message: "error must be a function" }) }); var settingsSchema = z.object({ codexPath: z.string().optional(), cwd: z.string().optional(), approvalMode: z.enum(["untrusted", "on-failure", "on-request", "never"]).optional(), sandboxMode: z.enum(["read-only", "workspace-write", "danger-full-access"]).optional(), fullAuto: z.boolean().optional(), dangerouslyBypassApprovalsAndSandbox: z.boolean().optional(), skipGitRepoCheck: z.boolean().optional(), color: z.enum(["always", "never", "auto"]).optional(), allowNpx: z.boolean().optional(), env: z.record(z.string(), z.string()).optional(), verbose: z.boolean().optional(), logger: z.union([z.literal(false), loggerFunctionSchema]).optional(), // NEW: Reasoning & Verbosity reasoningEffort: z.enum(["minimal", "low", "medium", "high"]).optional(), // Note: API rejects 'concise' and 'none' despite error messages claiming they're valid reasoningSummary: z.enum(["auto", "detailed"]).optional(), reasoningSummaryFormat: z.enum(["none", "experimental"]).optional(), modelVerbosity: z.enum(["low", "medium", "high"]).optional(), // NEW: Advanced features includePlanTool: z.boolean().optional(), profile: z.string().optional(), oss: z.boolean().optional(), webSearch: z.boolean().optional(), // NEW: Generic overrides configOverrides: z.record( z.string(), z.union([ z.string(), z.number(), z.boolean(), z.object({}).passthrough(), z.array(z.any()) ]) ).optional() }).strict(); function validateSettings(settings) { const warnings = []; const errors = []; const parsed = settingsSchema.safeParse(settings); if (!parsed.success) { const raw = parsed.error; let issues = []; if (raw && typeof raw === "object") { const v4 = raw.issues; const v3 = raw.errors; if (Array.isArray(v4)) issues = v4; else if (Array.isArray(v3)) issues = v3; } for (const i of issues) { const path = Array.isArray(i?.path) ? i.path.join(".") : ""; const message = i?.message || "Invalid value"; errors.push(`${path ? path + ": " : ""}${message}`); } return { valid: false, warnings, errors }; } const s = parsed.data; if (s.fullAuto && s.dangerouslyBypassApprovalsAndSandbox) { warnings.push( "Both fullAuto and dangerouslyBypassApprovalsAndSandbox specified; fullAuto takes precedence." ); } return { valid: true, warnings, errors }; } function validateModelId(modelId) { if (!modelId || modelId.trim() === "") return "Model ID cannot be empty"; return void 0; } // src/message-mapper.ts function isTextPart(p) { return typeof p === "object" && p !== null && "type" in p && p.type === "text" && "text" in p && typeof p.text === "string"; } function isImagePart(p) { return typeof p === "object" && p !== null && "type" in p && p.type === "image"; } function isToolItem(p) { if (typeof p !== "object" || p === null) return false; const obj = p; if (typeof obj.toolName !== "string") return false; const out = obj.output; if (!out || out.type !== "text" && out.type !== "json") return false; if (out.type === "text" && typeof out.value !== "string") return false; return true; } function mapMessagesToPrompt(prompt) { const warnings = []; const parts = []; let systemText; for (const msg of prompt) { if (msg.role === "system") { systemText = typeof msg.content === "string" ? msg.content : String(msg.content); continue; } if (msg.role === "user") { if (typeof msg.content === "string") { parts.push(`Human: ${msg.content}`); } else if (Array.isArray(msg.content)) { const text = msg.content.filter(isTextPart).map((p) => p.text).join("\n"); if (text) parts.push(`Human: ${text}`); const images = msg.content.filter(isImagePart); if (images.length) warnings.push("Image inputs ignored by Codex CLI integration."); } continue; } if (msg.role === "assistant") { if (typeof msg.content === "string") { parts.push(`Assistant: ${msg.content}`); } else if (Array.isArray(msg.content)) { const text = msg.content.filter(isTextPart).map((p) => p.text).join("\n"); if (text) parts.push(`Assistant: ${text}`); } continue; } if (msg.role === "tool") { if (Array.isArray(msg.content)) { for (const maybeTool of msg.content) { if (!isToolItem(maybeTool)) continue; const value = maybeTool.output.type === "text" ? maybeTool.output.value : JSON.stringify(maybeTool.output.value); parts.push(`Tool Result (${maybeTool.toolName}): ${value}`); } } continue; } } let promptText = ""; if (systemText) promptText += systemText + "\n\n"; promptText += parts.join("\n\n"); return { promptText, ...warnings.length ? { warnings } : {} }; } function createAPICallError({ message, code, exitCode, stderr, promptExcerpt, isRetryable = false }) { const data = { code, exitCode, stderr, promptExcerpt }; return new APICallError({ message, isRetryable, url: "codex-cli://exec", requestBodyValues: promptExcerpt ? { prompt: promptExcerpt } : void 0, data }); } function createAuthenticationError(message) { return new LoadAPIKeyError({ message: message || "Authentication failed. Ensure Codex CLI is logged in (codex login)." }); } function isAuthenticationError(err) { if (err instanceof LoadAPIKeyError) return true; if (err instanceof APICallError) { const data = err.data; if (data?.exitCode === 401) return true; } return false; } // src/codex-cli-language-model.ts var codexCliProviderOptionsSchema = z.object({ reasoningEffort: z.enum(["minimal", "low", "medium", "high"]).optional(), reasoningSummary: z.enum(["auto", "detailed"]).optional(), reasoningSummaryFormat: z.enum(["none", "experimental"]).optional(), textVerbosity: z.enum(["low", "medium", "high"]).optional(), configOverrides: z.record( z.string(), z.union([ z.string(), z.number(), z.boolean(), z.object({}).passthrough(), z.array(z.any()) ]) ).optional() }).strict(); function resolveCodexPath(explicitPath, allowNpx) { if (explicitPath) return { cmd: "node", args: [explicitPath] }; try { const req = createRequire(import.meta.url); const pkgPath = req.resolve("@openai/codex/package.json"); const root = pkgPath.replace(/package\.json$/, ""); return { cmd: "node", args: [root + "bin/codex.js"] }; } catch { if (allowNpx) return { cmd: "npx", args: ["-y", "@openai/codex"] }; return { cmd: "codex", args: [] }; } } var CodexCliLanguageModel = class { specificationVersion = "v2"; provider = "codex-cli"; defaultObjectGenerationMode = "json"; supportsImageUrls = false; supportedUrls = {}; supportsStructuredOutputs = true; modelId; settings; logger; sessionId; constructor(options) { this.modelId = options.id; this.settings = options.settings ?? {}; const baseLogger = getLogger(this.settings.logger); this.logger = createVerboseLogger(baseLogger, this.settings.verbose ?? false); if (!this.modelId || this.modelId.trim() === "") { throw new NoSuchModelError({ modelId: this.modelId, modelType: "languageModel" }); } const warn = validateModelId(this.modelId); if (warn) this.logger.warn(`Codex CLI model: ${warn}`); } mergeSettings(providerOptions) { if (!providerOptions) return this.settings; const mergedConfigOverrides = providerOptions.configOverrides || this.settings.configOverrides ? { ...this.settings.configOverrides ?? {}, ...providerOptions.configOverrides ?? {} } : void 0; return { ...this.settings, reasoningEffort: providerOptions.reasoningEffort ?? this.settings.reasoningEffort, reasoningSummary: providerOptions.reasoningSummary ?? this.settings.reasoningSummary, reasoningSummaryFormat: providerOptions.reasoningSummaryFormat ?? this.settings.reasoningSummaryFormat, modelVerbosity: providerOptions.textVerbosity ?? this.settings.modelVerbosity, configOverrides: mergedConfigOverrides }; } // Codex JSONL items use `type` for the item discriminator, but some // earlier fixtures (and defensive parsing) might still surface `item_type`. // This helper returns whichever is present. getItemType(item) { if (!item) return void 0; const data = item; const legacy = typeof data.item_type === "string" ? data.item_type : void 0; const current = typeof data.type === "string" ? data.type : void 0; return legacy ?? current; } buildArgs(promptText, responseFormat, settings = this.settings) { const base = resolveCodexPath(settings.codexPath, settings.allowNpx); const args = [...base.args, "exec", "--experimental-json"]; if (settings.fullAuto) { args.push("--full-auto"); } else if (settings.dangerouslyBypassApprovalsAndSandbox) { args.push("--dangerously-bypass-approvals-and-sandbox"); } else { const approval = settings.approvalMode ?? "on-failure"; args.push("-c", `approval_policy=${approval}`); const sandbox = settings.sandboxMode ?? "workspace-write"; args.push("-c", `sandbox_mode=${sandbox}`); } if (settings.skipGitRepoCheck !== false) { args.push("--skip-git-repo-check"); } if (settings.reasoningEffort) { args.push("-c", `model_reasoning_effort=${settings.reasoningEffort}`); } if (settings.reasoningSummary) { args.push("-c", `model_reasoning_summary=${settings.reasoningSummary}`); } if (settings.reasoningSummaryFormat) { args.push("-c", `model_reasoning_summary_format=${settings.reasoningSummaryFormat}`); } if (settings.modelVerbosity) { args.push("-c", `model_verbosity=${settings.modelVerbosity}`); } if (settings.includePlanTool) { args.push("--include-plan-tool"); } if (settings.profile) { args.push("--profile", settings.profile); } if (settings.oss) { args.push("--oss"); } if (settings.webSearch) { args.push("-c", "tools.web_search=true"); } if (settings.color) { args.push("--color", settings.color); } if (this.modelId) { args.push("-m", this.modelId); } if (settings.configOverrides) { for (const [key, value] of Object.entries(settings.configOverrides)) { this.addConfigOverride(args, key, value); } } let schemaPath; if (responseFormat?.type === "json" && responseFormat.schema) { const schema = typeof responseFormat.schema === "object" ? responseFormat.schema : {}; const sanitizedSchema = this.sanitizeJsonSchema(schema); const hasProperties = Object.keys(sanitizedSchema).length > 0; if (hasProperties) { const dir = mkdtempSync(join(tmpdir(), "codex-schema-")); schemaPath = join(dir, "schema.json"); const schemaWithAdditional = { ...sanitizedSchema, additionalProperties: false }; writeFileSync(schemaPath, JSON.stringify(schemaWithAdditional, null, 2)); args.push("--output-schema", schemaPath); } } args.push(promptText); const env = { ...process.env, ...settings.env || {}, RUST_LOG: process.env.RUST_LOG || "error" }; let lastMessagePath = settings.outputLastMessageFile; if (!lastMessagePath) { const dir = mkdtempSync(join(tmpdir(), "codex-cli-")); lastMessagePath = join(dir, "last-message.txt"); } args.push("--output-last-message", lastMessagePath); return { cmd: base.cmd, args, env, cwd: settings.cwd, lastMessagePath, schemaPath }; } addConfigOverride(args, key, value) { if (this.isPlainObject(value)) { for (const [childKey, childValue] of Object.entries(value)) { this.addConfigOverride( args, `${key}.${childKey}`, childValue ); } return; } const serialized = this.serializeConfigValue(value); args.push("-c", `${key}=${serialized}`); } /** * Serialize a config override value into a CLI-safe string. */ serializeConfigValue(value) { if (typeof value === "string") return value; if (typeof value === "number" || typeof value === "boolean") return String(value); if (Array.isArray(value)) { try { return JSON.stringify(value); } catch { return String(value); } } if (value && typeof value === "object") { try { return JSON.stringify(value); } catch { return String(value); } } return String(value); } isPlainObject(value) { return typeof value === "object" && value !== null && !Array.isArray(value) && Object.prototype.toString.call(value) === "[object Object]"; } sanitizeJsonSchema(value) { if (typeof value !== "object" || value === null) { return value; } if (Array.isArray(value)) { return value.map((item) => this.sanitizeJsonSchema(item)); } const obj = value; const result = {}; for (const [key, val] of Object.entries(obj)) { if (key === "properties" && typeof val === "object" && val !== null && !Array.isArray(val)) { const props = val; const sanitizedProps = {}; for (const [propName, propSchema] of Object.entries(props)) { sanitizedProps[propName] = this.sanitizeJsonSchema(propSchema); } result[key] = sanitizedProps; continue; } if (key === "$schema" || key === "$id" || key === "$ref" || key === "$defs" || key === "definitions" || key === "title" || key === "examples" || key === "default" || key === "format" || // OpenAI strict mode doesn't support format key === "pattern") { continue; } result[key] = this.sanitizeJsonSchema(val); } return result; } mapWarnings(options) { const unsupported = []; const add = (setting, name) => { if (setting !== void 0) unsupported.push({ type: "unsupported-setting", setting: name, details: `Codex CLI does not support ${name}; it will be ignored.` }); }; add(options.temperature, "temperature"); add(options.topP, "topP"); add(options.topK, "topK"); add(options.presencePenalty, "presencePenalty"); add(options.frequencyPenalty, "frequencyPenalty"); add(options.stopSequences?.length ? options.stopSequences : void 0, "stopSequences"); add(options.seed, "seed"); return unsupported; } parseExperimentalJsonEvent(line) { try { return JSON.parse(line); } catch { return void 0; } } extractUsage(evt) { const reported = evt.usage; if (!reported) return void 0; const inputTokens = reported.input_tokens ?? 0; const outputTokens = reported.output_tokens ?? 0; const cachedInputTokens = reported.cached_input_tokens ?? 0; return { inputTokens, outputTokens, // totalTokens should not double-count cached tokens; track cached separately totalTokens: inputTokens + outputTokens, cachedInputTokens }; } getToolName(item) { if (!item) return void 0; const itemType = this.getItemType(item); switch (itemType) { case "command_execution": return "exec"; case "file_change": return "patch"; case "mcp_tool_call": { const tool = item.tool; if (typeof tool === "string" && tool.length > 0) return tool; return "mcp_tool"; } case "web_search": return "web_search"; default: return void 0; } } buildToolInputPayload(item) { if (!item) return void 0; const data = item; switch (this.getItemType(item)) { case "command_execution": { const payload = {}; if (typeof data.command === "string") payload.command = data.command; if (typeof data.status === "string") payload.status = data.status; if (typeof data.cwd === "string") payload.cwd = data.cwd; return Object.keys(payload).length ? payload : void 0; } case "file_change": { const payload = {}; if (Array.isArray(data.changes)) payload.changes = data.changes; if (typeof data.status === "string") payload.status = data.status; return Object.keys(payload).length ? payload : void 0; } case "mcp_tool_call": { const payload = {}; if (typeof data.server === "string") payload.server = data.server; if (typeof data.tool === "string") payload.tool = data.tool; if (typeof data.status === "string") payload.status = data.status; if (data.arguments !== void 0) payload.arguments = data.arguments; return Object.keys(payload).length ? payload : void 0; } case "web_search": { const payload = {}; if (typeof data.query === "string") payload.query = data.query; return Object.keys(payload).length ? payload : void 0; } default: return void 0; } } buildToolResultPayload(item) { if (!item) return { result: {} }; const data = item; const metadata = {}; const itemType = this.getItemType(item); if (typeof itemType === "string") metadata.itemType = itemType; if (typeof item.id === "string") metadata.itemId = item.id; if (typeof data.status === "string") metadata.status = data.status; const buildResult = (result) => ({ result, metadata: Object.keys(metadata).length ? metadata : void 0 }); switch (itemType) { case "command_execution": { const result = {}; if (typeof data.command === "string") result.command = data.command; if (typeof data.aggregated_output === "string") result.aggregatedOutput = data.aggregated_output; if (typeof data.exit_code === "number") result.exitCode = data.exit_code; if (typeof data.status === "string") result.status = data.status; return buildResult(result); } case "file_change": { const result = {}; if (Array.isArray(data.changes)) result.changes = data.changes; if (typeof data.status === "string") result.status = data.status; return buildResult(result); } case "mcp_tool_call": { const result = {}; if (typeof data.server === "string") { result.server = data.server; metadata.server = data.server; } if (typeof data.tool === "string") result.tool = data.tool; if (typeof data.status === "string") result.status = data.status; if (data.result !== void 0) result.result = data.result; if (data.error !== void 0) result.error = data.error; return buildResult(result); } case "web_search": { const result = {}; if (typeof data.query === "string") result.query = data.query; if (typeof data.status === "string") result.status = data.status; return buildResult(result); } default: { const result = { ...data }; return buildResult(result); } } } safeStringify(value) { if (value === void 0) return ""; if (typeof value === "string") return value; try { return JSON.stringify(value); } catch { return ""; } } emitToolInvocation(controller, toolCallId, toolName, inputPayload) { const inputString = this.safeStringify(inputPayload); controller.enqueue({ type: "tool-input-start", id: toolCallId, toolName }); if (inputString) { controller.enqueue({ type: "tool-input-delta", id: toolCallId, delta: inputString }); } controller.enqueue({ type: "tool-input-end", id: toolCallId }); controller.enqueue({ type: "tool-call", toolCallId, toolName, input: inputString, providerExecuted: true }); } emitToolResult(controller, toolCallId, toolName, item, resultPayload, metadata) { const providerMetadataEntries = { ...metadata ?? {} }; const itemType = this.getItemType(item); if (itemType && providerMetadataEntries.itemType === void 0) { providerMetadataEntries.itemType = itemType; } if (item.id && providerMetadataEntries.itemId === void 0) { providerMetadataEntries.itemId = item.id; } let isError; if (itemType === "command_execution") { const data = item; const exitCode = typeof data.exit_code === "number" ? data.exit_code : void 0; const status = typeof data.status === "string" ? data.status : void 0; if (exitCode !== void 0 && exitCode !== 0 || status === "failed") { isError = true; } } controller.enqueue({ type: "tool-result", toolCallId, toolName, result: resultPayload ?? {}, ...isError ? { isError: true } : {}, ...Object.keys(providerMetadataEntries).length ? { providerMetadata: { "codex-cli": providerMetadataEntries } } : {} }); } handleSpawnError(err, promptExcerpt) { const e = err && typeof err === "object" ? err : void 0; const message = String((e?.message ?? err) || "Failed to run Codex CLI"); if (/login|auth|unauthorized|not\s+logged/i.test(message)) { throw createAuthenticationError(message); } throw createAPICallError({ message, code: typeof e?.code === "string" ? e.code : void 0, exitCode: typeof e?.exitCode === "number" ? e.exitCode : void 0, stderr: typeof e?.stderr === "string" ? e.stderr : void 0, promptExcerpt }); } async doGenerate(options) { this.logger.debug(`[codex-cli] Starting doGenerate request with model: ${this.modelId}`); const { promptText, warnings: mappingWarnings } = mapMessagesToPrompt(options.prompt); const promptExcerpt = promptText.slice(0, 200); const warnings = [ ...this.mapWarnings(options), ...mappingWarnings?.map((m) => ({ type: "other", message: m })) || [] ]; this.logger.debug( `[codex-cli] Converted ${options.prompt.length} messages, response format: ${options.responseFormat?.type ?? "none"}` ); const providerOptions = await parseProviderOptions({ provider: this.provider, providerOptions: options.providerOptions, schema: codexCliProviderOptionsSchema }); const effectiveSettings = this.mergeSettings(providerOptions); const responseFormat = options.responseFormat?.type === "json" ? { type: "json", schema: options.responseFormat.schema } : void 0; const { cmd, args, env, cwd, lastMessagePath, schemaPath } = this.buildArgs( promptText, responseFormat, effectiveSettings ); this.logger.debug( `[codex-cli] Executing Codex CLI: ${cmd} with ${args.length} arguments, cwd: ${cwd ?? "default"}` ); let text = ""; const usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 }; const finishReason = "stop"; const startTime = Date.now(); const child = spawn(cmd, args, { env, cwd, stdio: ["ignore", "pipe", "pipe"] }); let onAbort; if (options.abortSignal) { if (options.abortSignal.aborted) { child.kill("SIGTERM"); throw options.abortSignal.reason ?? new Error("Request aborted"); } onAbort = () => child.kill("SIGTERM"); options.abortSignal.addEventListener("abort", onAbort, { once: true }); } try { await new Promise((resolve, reject) => { let stderr = ""; let turnFailureMessage; child.stderr.on("data", (d) => stderr += String(d)); child.stdout.setEncoding("utf8"); child.stdout.on("data", (chunk) => { const lines = chunk.split(/\r?\n/).filter(Boolean); for (const line of lines) { const event = this.parseExperimentalJsonEvent(line); if (!event) continue; this.logger.debug(`[codex-cli] Received event type: ${event.type ?? "unknown"}`); if (event.type === "thread.started" && typeof event.thread_id === "string") { this.sessionId = event.thread_id; this.logger.debug(`[codex-cli] Session started: ${this.sessionId}`); } if (event.type === "session.created" && typeof event.session_id === "string") { this.sessionId = event.session_id; this.logger.debug(`[codex-cli] Session created: ${this.sessionId}`); } if (event.type === "turn.completed") { const usageEvent = this.extractUsage(event); if (usageEvent) { usage.inputTokens = usageEvent.inputTokens; usage.outputTokens = usageEvent.outputTokens; usage.totalTokens = usageEvent.totalTokens; } } if (event.type === "item.completed" && this.getItemType(event.item) === "assistant_message" && typeof event.item?.text === "string") { text = event.item.text; } if (event.type === "turn.failed") { const errorText = event.error && typeof event.error.message === "string" && event.error.message || (typeof event.message === "string" ? event.message : void 0); turnFailureMessage = errorText ?? turnFailureMessage ?? "Codex turn failed"; this.logger.error(`[codex-cli] Turn failed: ${turnFailureMessage}`); } if (event.type === "error") { const errorText = typeof event.message === "string" ? event.message : void 0; turnFailureMessage = errorText ?? turnFailureMessage ?? "Codex error"; this.logger.error(`[codex-cli] Error event: ${turnFailureMessage}`); } } }); child.on("error", (e) => { this.logger.error(`[codex-cli] Spawn error: ${String(e)}`); reject(this.handleSpawnError(e, promptExcerpt)); }); child.on("close", (code) => { const duration = Date.now() - startTime; if (code === 0) { if (turnFailureMessage) { reject( createAPICallError({ message: turnFailureMessage, stderr, promptExcerpt }) ); return; } this.logger.info( `[codex-cli] Request completed - Session: ${this.sessionId ?? "N/A"}, Duration: ${duration}ms, Tokens: ${usage.totalTokens}` ); this.logger.debug( `[codex-cli] Token usage - Input: ${usage.inputTokens}, Output: ${usage.outputTokens}, Total: ${usage.totalTokens}` ); resolve(); } else { this.logger.error(`[codex-cli] Process exited with code ${code} after ${duration}ms`); reject( createAPICallError({ message: `Codex CLI exited with code ${code}`, exitCode: code ?? void 0, stderr, promptExcerpt }) ); } }); }); } finally { if (options.abortSignal && onAbort) options.abortSignal.removeEventListener("abort", onAbort); if (schemaPath) { try { const schemaDir = dirname(schemaPath); rmSync(schemaDir, { recursive: true, force: true }); } catch { } } } if (!text && lastMessagePath) { try { const fileText = readFileSync(lastMessagePath, "utf8"); if (fileText && typeof fileText === "string") { text = fileText.trim(); } } catch { } try { rmSync(lastMessagePath, { force: true }); } catch { } } const content = [{ type: "text", text }]; return { content, usage, finishReason, warnings, response: { id: generateId(), timestamp: /* @__PURE__ */ new Date(), modelId: this.modelId }, request: { body: promptText }, providerMetadata: { "codex-cli": { ...this.sessionId ? { sessionId: this.sessionId } : {} } } }; } async doStream(options) { this.logger.debug(`[codex-cli] Starting doStream request with model: ${this.modelId}`); const { promptText, warnings: mappingWarnings } = mapMessagesToPrompt(options.prompt); const promptExcerpt = promptText.slice(0, 200); const warnings = [ ...this.mapWarnings(options), ...mappingWarnings?.map((m) => ({ type: "other", message: m })) || [] ]; this.logger.debug( `[codex-cli] Converted ${options.prompt.length} messages for streaming, response format: ${options.responseFormat?.type ?? "none"}` ); const providerOptions = await parseProviderOptions({ provider: this.provider, providerOptions: options.providerOptions, schema: codexCliProviderOptionsSchema }); const effectiveSettings = this.mergeSettings(providerOptions); const responseFormat = options.responseFormat?.type === "json" ? { type: "json", schema: options.responseFormat.schema } : void 0; const { cmd, args, env, cwd, lastMessagePath, schemaPath } = this.buildArgs( promptText, responseFormat, effectiveSettings ); this.logger.debug( `[codex-cli] Executing Codex CLI for streaming: ${cmd} with ${args.length} arguments` ); const stream = new ReadableStream({ start: (controller) => { const startTime = Date.now(); const child = spawn(cmd, args, { env, cwd, stdio: ["ignore", "pipe", "pipe"] }); controller.enqueue({ type: "stream-start", warnings }); let stderr = ""; let accumulatedText = ""; const activeTools = /* @__PURE__ */ new Map(); let responseMetadataSent = false; let lastUsage; let turnFailureMessage; const sendMetadata = (meta = {}) => { controller.enqueue({ type: "response-metadata", id: randomUUID(), timestamp: /* @__PURE__ */ new Date(), modelId: this.modelId, ...Object.keys(meta).length ? { providerMetadata: { "codex-cli": meta } } : {} }); }; const handleItemEvent = (event) => { const item = event.item; if (!item) return; if (event.type === "item.completed" && this.getItemType(item) === "assistant_message" && typeof item.text === "string") { accumulatedText = item.text; this.logger.debug( `[codex-cli] Received assistant message, length: ${item.text.length}` ); return; } const toolName = this.getToolName(item); if (!toolName) { return; } this.logger.debug( `[codex-cli] Tool detected: ${toolName}, item type: ${this.getItemType(item)}` ); const mapKey = typeof item.id === "string" && item.id.length > 0 ? item.id : randomUUID(); let toolState = activeTools.get(mapKey); const latestInput = this.buildToolInputPayload(item); if (!toolState) { toolState = { toolCallId: mapKey, toolName, inputPayload: latestInput, hasEmittedCall: false }; activeTools.set(mapKey, toolState); } else { toolState.toolName = toolName; if (latestInput !== void 0) { toolState.inputPayload = latestInput; } } if (!toolState.hasEmittedCall) { this.logger.debug(`[codex-cli] Emitting tool invocation: ${toolState.toolName}`); this.emitToolInvocation( controller, toolState.toolCallId, toolState.toolName, toolState.inputPayload ); toolState.hasEmittedCall = true; } if (event.type === "item.completed") { const { result, metadata } = this.buildToolResultPayload(item); this.logger.debug(`[codex-cli] Tool completed: ${toolState.toolName}`); this.emitToolResult( controller, toolState.toolCallId, toolState.toolName, item, result, metadata ); activeTools.delete(mapKey); } }; const onAbort = () => { child.kill("SIGTERM"); }; if (options.abortSignal) { if (options.abortSignal.aborted) { child.kill("SIGTERM"); controller.error(options.abortSignal.reason ?? new Error("Request aborted")); return; } options.abortSignal.addEventListener("abort", onAbort, { once: true }); } const finishStream = (code) => { const duration = Date.now() - startTime; if (code !== 0) { this.logger.error( `[codex-cli] Stream process exited with code ${code} after ${duration}ms` ); controller.error( createAPICallError({ message: `Codex CLI exited with code ${code}`, exitCode: code ?? void 0, stderr, promptExcerpt }) ); return; } if (turnFailureMessage) { this.logger.error(`[codex-cli] Stream failed: ${turnFailureMessage}`); controller.error( createAPICallError({ message: turnFailureMessage, stderr, promptExcerpt }) ); return; } let finalText = accumulatedText; if (!finalText && lastMessagePath) { try { const fileText = readFileSync(lastMessagePath, "utf8"); if (fileText) finalText = fileText.trim(); } catch { } try { rmSync(lastMessagePath, { force: true }); } catch { } } if (finalText) { const textId = randomUUID(); controller.enqueue({ type: "text-start", id: textId }); controller.enqueue({ type: "text-delta", id: textId, delta: finalText }); controller.enqueue({ type: "text-end", id: textId }); } const usageSummary = lastUsage ?? { inputTokens: 0, outputTokens: 0, totalTokens: 0 }; this.logger.info( `[codex-cli] Stream completed - Session: ${this.sessionId ?? "N/A"}, Duration: ${duration}ms, Tokens: ${usageSummary.totalTokens}` ); this.logger.debug( `[codex-cli] Token usage - Input: ${usageSummary.inputTokens}, Output: ${usageSummary.outputTokens}, Total: ${usageSummary.totalTokens}` ); controller.enqueue({ type: "finish", finishReason: "stop", usage: usageSummary }); controller.close(); }; child.stderr.on("data", (d) => stderr += String(d)); child.stdout.setEncoding("utf8"); child.stdout.on("data", (chunk) => { const lines = chunk.split(/\r?\n/).filter(Boolean); for (const line of lines) { const event = this.parseExperimentalJsonEvent(line); if (!event) continue; this.logger.debug(`[codex-cli] Stream event: ${event.type ?? "unknown"}`); if (event.type === "thread.started" && typeof event.thread_id === "string") { this.sessionId = event.thread_id; this.logger.debug(`[codex-cli] Stream session started: ${this.sessionId}`); if (!responseMetadataSent) { responseMetadataSent = true; sendMetadata(); } continue; } if (event.type === "session.created" && typeof event.session_id === "string") { this.sessionId = event.session_id; this.logger.debug(`[codex-cli] Stream session created: ${this.sessionId}`); if (!responseMetadataSent) { responseMetadataSent = true; sendMetadata(); } continue; } if (event.type === "turn.completed") { const usageEvent = this.extractUsage(event); if (usageEvent) { lastUsage = usageEvent; } continue; } if (event.type === "turn.failed") { const errorText = event.error && typeof event.error.message === "string" && event.error.message || (typeof event.message === "string" ? event.message : void 0); turnFailureMessage = errorText ?? turnFailureMessage ?? "Codex turn failed"; this.logger.error(`[codex-cli] Stream turn failed: ${turnFailureMessage}`); sendMetadata({ error: turnFailureMessage }); continue; } if (event.type === "error") { const errorText = typeof event.message === "string" ? event.message : void 0; const effective = errorText ?? "Codex error"; turnFailureMessage = turnFailureMessage ?? effective; this.logger.error(`[codex-cli] Stream error event: ${effective}`); sendMetadata({ error: effective }); continue; } if (event.type && event.type.startsWith("item.")) { handleItemEvent(event); } } }); const cleanupSchema = () => { if (!schemaPath) return; try { const schemaDir = dirname(schemaPath); rmSync(schemaDir, { recursive: true, force: true }); } catch { } }; child.on("error", (e) => { this.logger.error(`[codex-cli] Stream spawn error: ${String(e)}`); if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort); cleanupSchema(); controller.error(this.handleSpawnError(e, promptExcerpt)); }); child.on("close", (code) => { if (options.abortSignal) options.abortSignal.removeEventListener("abort", onAbort); cleanupSchema(); setImmediate(() => finishStream(code)); }); }, cancel: () => { } }); return { stream, request: { body: promptText } }; } }; // src/codex-cli-provider.ts function createCodexCli(options = {}) { const logger = getLogger(options.defaultSettings?.logger); if (options.defaultSettings) { const v = validateSettings(options.defaultSettings); if (!v.valid) { throw new Error(`Invalid default settings: ${v.errors.join(", ")}`); } for (const w of v.warnings) logger.warn(`Codex CLI Provider: ${w}`); } const createModel = (modelId, settings = {}) => { const merged = { ...options.defaultSettings, ...settings }; const v = validateSettings(merged); if (!v.valid) throw new Error(`Invalid settings: ${v.errors.join(", ")}`); for (const w of v.warnings) logger.warn(`Codex CLI: ${w}`); return new CodexCliLanguageModel({ id: modelId, settings: merged }); }; const provider = function(modelId, settings) { if (new.target) throw new Error("The Codex CLI provider function cannot be called with new."); return createModel(modelId, settings); }; provider.languageModel = createModel; provider.chat = createModel; provider.textEmbeddingModel = ((modelId) => { throw new NoSuchModelError({ modelId, modelType: "textEmbeddingModel" }); }); provider.imageModel = ((modelId) => { throw new NoSuchModelError({ modelId, modelType: "imageModel" }); }); return provider; } var codexCli = createCodexCli(); export { CodexCliLanguageModel, codexCli, createCodexCli, isAuthenticationError };