@openai/codex-sdk
Version:
TypeScript SDK for Codex APIs.
321 lines (318 loc) • 9.01 kB
JavaScript
// src/outputSchemaFile.ts
import { promises as fs } from "fs";
import os from "os";
import path from "path";
async function createOutputSchemaFile(schema) {
if (schema === void 0) {
return { cleanup: async () => {
} };
}
if (!isJsonObject(schema)) {
throw new Error("outputSchema must be a plain JSON object");
}
const schemaDir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-output-schema-"));
const schemaPath = path.join(schemaDir, "schema.json");
const cleanup = async () => {
try {
await fs.rm(schemaDir, { recursive: true, force: true });
} catch {
}
};
try {
await fs.writeFile(schemaPath, JSON.stringify(schema), "utf8");
return { schemaPath, cleanup };
} catch (error) {
await cleanup();
throw error;
}
}
function isJsonObject(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
// src/thread.ts
var Thread = class {
_exec;
_options;
_id;
_threadOptions;
/** Returns the ID of the thread. Populated after the first turn starts. */
get id() {
return this._id;
}
/* @internal */
constructor(exec, options, threadOptions, id = null) {
this._exec = exec;
this._options = options;
this._id = id;
this._threadOptions = threadOptions;
}
/** Provides the input to the agent and streams events as they are produced during the turn. */
async runStreamed(input, turnOptions = {}) {
return { events: this.runStreamedInternal(input, turnOptions) };
}
async *runStreamedInternal(input, turnOptions = {}) {
const { schemaPath, cleanup } = await createOutputSchemaFile(turnOptions.outputSchema);
const options = this._threadOptions;
const { prompt, images } = normalizeInput(input);
const generator = this._exec.run({
input: prompt,
baseUrl: this._options.baseUrl,
apiKey: this._options.apiKey,
threadId: this._id,
images,
model: options?.model,
sandboxMode: options?.sandboxMode,
workingDirectory: options?.workingDirectory,
skipGitRepoCheck: options?.skipGitRepoCheck,
outputSchemaFile: schemaPath,
modelReasoningEffort: options?.modelReasoningEffort
});
try {
for await (const item of generator) {
let parsed;
try {
parsed = JSON.parse(item);
} catch (error) {
throw new Error(`Failed to parse item: ${item}`, { cause: error });
}
if (parsed.type === "thread.started") {
this._id = parsed.thread_id;
}
yield parsed;
}
} finally {
await cleanup();
}
}
/** Provides the input to the agent and returns the completed turn. */
async run(input, turnOptions = {}) {
const generator = this.runStreamedInternal(input, turnOptions);
const items = [];
let finalResponse = "";
let usage = null;
let turnFailure = null;
for await (const event of generator) {
if (event.type === "item.completed") {
if (event.item.type === "agent_message") {
finalResponse = event.item.text;
}
items.push(event.item);
} else if (event.type === "turn.completed") {
usage = event.usage;
} else if (event.type === "turn.failed") {
turnFailure = event.error;
break;
}
}
if (turnFailure) {
throw new Error(turnFailure.message);
}
return { items, finalResponse, usage };
}
};
function normalizeInput(input) {
if (typeof input === "string") {
return { prompt: input, images: [] };
}
const promptParts = [];
const images = [];
for (const item of input) {
if (item.type === "text") {
promptParts.push(item.text);
} else if (item.type === "local_image") {
images.push(item.path);
}
}
return { prompt: promptParts.join("\n\n"), images };
}
// src/exec.ts
import { spawn } from "child_process";
import path2 from "path";
import readline from "readline";
import { fileURLToPath } from "url";
var INTERNAL_ORIGINATOR_ENV = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE";
var TYPESCRIPT_SDK_ORIGINATOR = "codex_sdk_ts";
var CodexExec = class {
executablePath;
constructor(executablePath = null) {
this.executablePath = executablePath || findCodexPath();
}
async *run(args) {
const commandArgs = ["exec", "--experimental-json"];
if (args.model) {
commandArgs.push("--model", args.model);
}
if (args.sandboxMode) {
commandArgs.push("--sandbox", args.sandboxMode);
}
if (args.workingDirectory) {
commandArgs.push("--cd", args.workingDirectory);
}
if (args.skipGitRepoCheck) {
commandArgs.push("--skip-git-repo-check");
}
if (args.outputSchemaFile) {
commandArgs.push("--output-schema", args.outputSchemaFile);
}
if (args.modelReasoningEffort) {
commandArgs.push("--config", `model_reasoning_effort="${args.modelReasoningEffort}"`);
}
if (args.images?.length) {
for (const image of args.images) {
commandArgs.push("--image", image);
}
}
if (args.threadId) {
commandArgs.push("resume", args.threadId);
}
const env = {
...process.env
};
if (!env[INTERNAL_ORIGINATOR_ENV]) {
env[INTERNAL_ORIGINATOR_ENV] = TYPESCRIPT_SDK_ORIGINATOR;
}
if (args.baseUrl) {
env.OPENAI_BASE_URL = args.baseUrl;
}
if (args.apiKey) {
env.CODEX_API_KEY = args.apiKey;
}
const child = spawn(this.executablePath, commandArgs, {
env
});
let spawnError = null;
child.once("error", (err) => spawnError = err);
if (!child.stdin) {
child.kill();
throw new Error("Child process has no stdin");
}
child.stdin.write(args.input);
child.stdin.end();
if (!child.stdout) {
child.kill();
throw new Error("Child process has no stdout");
}
const stderrChunks = [];
if (child.stderr) {
child.stderr.on("data", (data) => {
stderrChunks.push(data);
});
}
const rl = readline.createInterface({
input: child.stdout,
crlfDelay: Infinity
});
try {
for await (const line of rl) {
yield line;
}
const exitCode = new Promise((resolve, reject) => {
child.once("exit", (code) => {
if (code === 0) {
resolve(code);
} else {
const stderrBuffer = Buffer.concat(stderrChunks);
reject(
new Error(`Codex Exec exited with code ${code}: ${stderrBuffer.toString("utf8")}`)
);
}
});
});
if (spawnError) throw spawnError;
await exitCode;
} finally {
rl.close();
child.removeAllListeners();
try {
if (!child.killed) child.kill();
} catch {
}
}
}
};
var scriptFileName = fileURLToPath(import.meta.url);
var scriptDirName = path2.dirname(scriptFileName);
function findCodexPath() {
const { platform, arch } = process;
let targetTriple = null;
switch (platform) {
case "linux":
case "android":
switch (arch) {
case "x64":
targetTriple = "x86_64-unknown-linux-musl";
break;
case "arm64":
targetTriple = "aarch64-unknown-linux-musl";
break;
default:
break;
}
break;
case "darwin":
switch (arch) {
case "x64":
targetTriple = "x86_64-apple-darwin";
break;
case "arm64":
targetTriple = "aarch64-apple-darwin";
break;
default:
break;
}
break;
case "win32":
switch (arch) {
case "x64":
targetTriple = "x86_64-pc-windows-msvc";
break;
case "arm64":
targetTriple = "aarch64-pc-windows-msvc";
break;
default:
break;
}
break;
default:
break;
}
if (!targetTriple) {
throw new Error(`Unsupported platform: ${platform} (${arch})`);
}
const vendorRoot = path2.join(scriptDirName, "..", "vendor");
const archRoot = path2.join(vendorRoot, targetTriple);
const codexBinaryName = process.platform === "win32" ? "codex.exe" : "codex";
const binaryPath = path2.join(archRoot, "codex", codexBinaryName);
return binaryPath;
}
// src/codex.ts
var Codex = class {
exec;
options;
constructor(options = {}) {
this.exec = new CodexExec(options.codexPathOverride);
this.options = options;
}
/**
* Starts a new conversation with an agent.
* @returns A new thread instance.
*/
startThread(options = {}) {
return new Thread(this.exec, this.options, options);
}
/**
* Resumes a conversation with an agent based on the thread id.
* Threads are persisted in ~/.codex/sessions.
*
* @param id The id of the thread to resume.
* @returns A new thread instance.
*/
resumeThread(id, options = {}) {
return new Thread(this.exec, this.options, options, id);
}
};
export {
Codex,
Thread
};
//# sourceMappingURL=index.js.map