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
JavaScript
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 };