Skip to content

Commit 90fe5e4

Browse files
authored
Add structured-output support (#4793)
Add samples and docs.
1 parent a90a58f commit 90fe5e4

15 files changed

+291
-26
lines changed

pnpm-lock.yaml

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/typescript/README.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,41 @@ for await (const event of events) {
5050
}
5151
```
5252

53+
### Structured output
54+
55+
Provide a JSON schema per turn to have Codex respond with structured JSON. Pass schemas as
56+
plain JavaScript objects.
57+
58+
59+
```typescript
60+
const schema = {
61+
type: "object",
62+
properties: {
63+
summary: { type: "string" },
64+
status: { type: "string", enum: ["ok", "action_required"] },
65+
},
66+
required: ["summary", "status"],
67+
additionalProperties: false,
68+
} as const;
69+
70+
const turn = await thread.run("Summarize repository status", { outputSchema: schema });
71+
console.log(turn.finalResponse);
72+
```
73+
74+
You can also create JSON schemas for Zod types using the `zod-to-json-schema` package and setting the `target` to `"openAi"`.
75+
76+
```typescript
77+
const schema = z.object({
78+
summary: z.string(),
79+
status: z.enum(["ok", "action_required"]),
80+
});
81+
82+
const turn = await thread.run("Summarize repository status", {
83+
outputSchema: zodToJsonSchema(schema, { target: "openAi" }),
84+
});
85+
console.log(turn.finalResponse);
86+
```
87+
5388
### Resuming an existing thread
5489

5590
Threads are persisted in `~/.codex/sessions`. If you lose the in-memory `Thread` object, reconstruct it with `resumeThread()` and keep going.
@@ -70,4 +105,3 @@ const thread = codex.startThread({
70105
skipGitRepoCheck: true,
71106
});
72107
```
73-

sdk/typescript/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
"ts-node": "^10.9.2",
5959
"tsup": "^8.5.0",
6060
"typescript": "^5.9.2",
61-
"typescript-eslint": "^8.45.0"
61+
"typescript-eslint": "^8.45.0",
62+
"zod": "^3.24.2",
63+
"zod-to-json-schema": "^3.24.6"
6264
}
6365
}

sdk/typescript/samples/basic_streaming.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,9 @@ import { stdin as input, stdout as output } from "node:process";
55

66
import { Codex } from "@openai/codex-sdk";
77
import type { ThreadEvent, ThreadItem } from "@openai/codex-sdk";
8-
import path from "node:path";
8+
import { codexPathOverride } from "./helpers.ts";
99

10-
const codexPathOverride =
11-
process.env.CODEX_EXECUTABLE ??
12-
path.join(process.cwd(), "..", "..", "codex-rs", "target", "debug", "codex");
13-
14-
const codex = new Codex({ codexPathOverride });
10+
const codex = new Codex({ codexPathOverride: codexPathOverride() });
1511
const thread = codex.startThread();
1612
const rl = createInterface({ input, output });
1713

sdk/typescript/samples/helpers.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import path from "node:path";
2+
3+
export function codexPathOverride() {
4+
return process.env.CODEX_EXECUTABLE ??
5+
path.join(process.cwd(), "..", "..", "codex-rs", "target", "debug", "codex");
6+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/usr/bin/env -S NODE_NO_WARNINGS=1 pnpm ts-node-esm --files
2+
3+
import { Codex } from "@openai/codex-sdk";
4+
5+
import { codexPathOverride } from "./helpers.ts";
6+
7+
const codex = new Codex({ codexPathOverride: codexPathOverride() });
8+
9+
const thread = codex.startThread();
10+
11+
const schema = {
12+
type: "object",
13+
properties: {
14+
summary: { type: "string" },
15+
status: { type: "string", enum: ["ok", "action_required"] },
16+
},
17+
required: ["summary", "status"],
18+
additionalProperties: false,
19+
} as const;
20+
21+
const turn = await thread.run("Summarize repository status", { outputSchema: schema });
22+
console.log(turn.finalResponse);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/env -S NODE_NO_WARNINGS=1 pnpm ts-node-esm --files
2+
3+
import { Codex } from "@openai/codex-sdk";
4+
import { codexPathOverride } from "./helpers.ts";
5+
import z from "zod";
6+
import zodToJsonSchema from "zod-to-json-schema";
7+
8+
const codex = new Codex({ codexPathOverride: codexPathOverride() });
9+
const thread = codex.startThread();
10+
11+
const schema = z.object({
12+
summary: z.string(),
13+
status: z.enum(["ok", "action_required"]),
14+
});
15+
16+
const turn = await thread.run("Summarize repository status", {
17+
outputSchema: zodToJsonSchema(schema, { target: "openAi" }),
18+
});
19+
console.log(turn.finalResponse);

sdk/typescript/src/exec.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { spawn } from "node:child_process";
2-
2+
import path from "node:path";
33
import readline from "node:readline";
4+
import { fileURLToPath } from "node:url";
45

56
import { SandboxMode } from "./threadOptions";
6-
import path from "node:path";
7-
import { fileURLToPath } from "node:url";
87

98
export type CodexExecArgs = {
109
input: string;
@@ -20,6 +19,8 @@ export type CodexExecArgs = {
2019
workingDirectory?: string;
2120
// --skip-git-repo-check
2221
skipGitRepoCheck?: boolean;
22+
// --output-schema
23+
outputSchemaFile?: string;
2324
};
2425

2526
export class CodexExec {
@@ -47,6 +48,10 @@ export class CodexExec {
4748
commandArgs.push("--skip-git-repo-check");
4849
}
4950

51+
if (args.outputSchemaFile) {
52+
commandArgs.push("--output-schema", args.outputSchemaFile);
53+
}
54+
5055
if (args.threadId) {
5156
commandArgs.push("resume", args.threadId);
5257
}

sdk/typescript/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ export { Codex } from "./codex";
3131
export type { CodexOptions } from "./codexOptions";
3232

3333
export type { ThreadOptions, ApprovalMode, SandboxMode } from "./threadOptions";
34+
export type { TurnOptions } from "./turnOptions";
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { promises as fs } from "node:fs";
2+
import os from "node:os";
3+
import path from "node:path";
4+
5+
export type OutputSchemaFile = {
6+
schemaPath?: string;
7+
cleanup: () => Promise<void>;
8+
};
9+
10+
export async function createOutputSchemaFile(schema: unknown): Promise<OutputSchemaFile> {
11+
if (schema === undefined) {
12+
return { cleanup: async () => {} };
13+
}
14+
15+
if (!isJsonObject(schema)) {
16+
throw new Error("outputSchema must be a plain JSON object");
17+
}
18+
19+
const schemaDir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-output-schema-"));
20+
const schemaPath = path.join(schemaDir, "schema.json");
21+
const cleanup = async () => {
22+
try {
23+
await fs.rm(schemaDir, { recursive: true, force: true });
24+
}
25+
catch {
26+
// suppress
27+
}
28+
};
29+
30+
try {
31+
await fs.writeFile(schemaPath, JSON.stringify(schema), "utf8");
32+
return { schemaPath, cleanup };
33+
} catch (error) {
34+
await cleanup();
35+
throw error;
36+
}
37+
}
38+
39+
function isJsonObject(value: unknown): value is Record<string, unknown> {
40+
return typeof value === "object" && value !== null && !Array.isArray(value);
41+
}

0 commit comments

Comments
 (0)