Skip to content

Commit 516acc0

Browse files
authored
Support model and sandbox mode in the sdk (#4503)
1 parent 5b03813 commit 516acc0

File tree

6 files changed

+116
-5
lines changed

6 files changed

+116
-5
lines changed

sdk/typescript/src/exec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import { spawn } from "child_process";
22
import readline from "node:readline";
33

4+
import { SandboxMode } from "./turnOptions";
5+
46
export type CodexExecArgs = {
57
input: string;
68

79
baseUrl?: string;
810
apiKey?: string;
911
threadId?: string | null;
12+
model?: string;
13+
sandboxMode?: SandboxMode;
1014
};
1115

1216
export class CodexExec {
@@ -17,6 +21,15 @@ export class CodexExec {
1721

1822
async *run(args: CodexExecArgs): AsyncGenerator<string> {
1923
const commandArgs: string[] = ["exec", "--experimental-json"];
24+
25+
if (args.model) {
26+
commandArgs.push("--model", args.model);
27+
}
28+
29+
if (args.sandboxMode) {
30+
commandArgs.push("--sandbox", args.sandboxMode);
31+
}
32+
2033
if (args.threadId) {
2134
commandArgs.push("resume", args.threadId, args.input);
2235
} else {

sdk/typescript/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,5 @@ export type { Thread, RunResult, RunStreamedResult, Input } from "./thread";
2727
export type { Codex } from "./codex";
2828

2929
export type { CodexOptions } from "./codexOptions";
30+
31+
export type { TurnOptions, ApprovalMode, SandboxMode } from "./turnOptions";

sdk/typescript/src/thread.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { CodexOptions } from "./codexOptions";
22
import { ThreadEvent } from "./events";
33
import { CodexExec } from "./exec";
44
import { ThreadItem } from "./items";
5+
import { TurnOptions } from "./turnOptions";
56

67
export type RunResult = {
78
items: ThreadItem[];
@@ -25,16 +26,21 @@ export class Thread {
2526
this.id = id;
2627
}
2728

28-
async runStreamed(input: string): Promise<RunStreamedResult> {
29-
return { events: this.runStreamedInternal(input) };
29+
async runStreamed(input: string, options?: TurnOptions): Promise<RunStreamedResult> {
30+
return { events: this.runStreamedInternal(input, options) };
3031
}
3132

32-
private async *runStreamedInternal(input: string): AsyncGenerator<ThreadEvent> {
33+
private async *runStreamedInternal(
34+
input: string,
35+
options?: TurnOptions,
36+
): AsyncGenerator<ThreadEvent> {
3337
const generator = this.exec.run({
3438
input,
3539
baseUrl: this.options.baseUrl,
3640
apiKey: this.options.apiKey,
3741
threadId: this.id,
42+
model: options?.model,
43+
sandboxMode: options?.sandboxMode,
3844
});
3945
for await (const item of generator) {
4046
const parsed = JSON.parse(item) as ThreadEvent;
@@ -45,8 +51,8 @@ export class Thread {
4551
}
4652
}
4753

48-
async run(input: string): Promise<RunResult> {
49-
const generator = this.runStreamedInternal(input);
54+
async run(input: string, options?: TurnOptions): Promise<RunResult> {
55+
const generator = this.runStreamedInternal(input, options);
5056
const items: ThreadItem[] = [];
5157
let finalResponse: string = "";
5258
for await (const event of generator) {

sdk/typescript/src/turnOptions.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export type ApprovalMode = "never" | "on-request" | "on-failure" | "untrusted";
2+
3+
export type SandboxMode = "read-only" | "workspace-write" | "danger-full-access";
4+
5+
export type TurnOptions = {
6+
model?: string;
7+
sandboxMode?: SandboxMode;
8+
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as child_process from "child_process";
2+
3+
jest.mock("child_process", () => {
4+
const actual = jest.requireActual<typeof import("child_process")>("child_process");
5+
return { ...actual, spawn: jest.fn(actual.spawn) };
6+
});
7+
8+
const actualChildProcess = jest.requireActual<typeof import("child_process")>("child_process");
9+
const spawnMock = child_process.spawn as jest.MockedFunction<typeof actualChildProcess.spawn>;
10+
11+
export function codexExecSpy(): { args: string[][]; restore: () => void } {
12+
const previousImplementation =
13+
spawnMock.getMockImplementation() ?? actualChildProcess.spawn;
14+
const args: string[][] = [];
15+
16+
spawnMock.mockImplementation(((...spawnArgs: Parameters<typeof child_process.spawn>) => {
17+
const commandArgs = spawnArgs[1];
18+
args.push(Array.isArray(commandArgs) ? [...commandArgs] : []);
19+
return previousImplementation(...spawnArgs);
20+
}) as typeof actualChildProcess.spawn);
21+
22+
return {
23+
args,
24+
restore: () => {
25+
spawnMock.mockClear();
26+
spawnMock.mockImplementation(previousImplementation);
27+
},
28+
};
29+
}

sdk/typescript/tests/run.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import path from "path";
22

3+
import { codexExecSpy } from "./codexExecSpy";
34
import { describe, expect, it } from "@jest/globals";
45

56
import { Codex } from "../src/codex";
@@ -130,4 +131,56 @@ describe("Codex", () => {
130131
await close();
131132
}
132133
});
134+
135+
it("passes turn options to exec", async () => {
136+
const { url, close, requests } = await startResponsesTestProxy({
137+
statusCode: 200,
138+
responseBodies: [
139+
sse(
140+
responseStarted("response_1"),
141+
assistantMessage("Turn options applied", "item_1"),
142+
responseCompleted("response_1"),
143+
),
144+
],
145+
});
146+
147+
const { args: spawnArgs, restore } = codexExecSpy();
148+
149+
try {
150+
const client = new Codex({ executablePath: codexExecPath, baseUrl: url, apiKey: "test" });
151+
152+
const thread = client.startThread();
153+
await thread.run("apply options", {
154+
model: "gpt-test-1",
155+
sandboxMode: "workspace-write",
156+
});
157+
158+
const payload = requests[0];
159+
expect(payload).toBeDefined();
160+
const json = payload!.json as { model?: string } | undefined;
161+
162+
expect(json?.model).toBe("gpt-test-1");
163+
expect(spawnArgs.length).toBeGreaterThan(0);
164+
const commandArgs = spawnArgs[0];
165+
166+
expectPair(commandArgs, ["--sandbox", "workspace-write"]);
167+
expectPair(commandArgs, ["--model", "gpt-test-1"]);
168+
169+
} finally {
170+
restore();
171+
await close();
172+
}
173+
});
133174
});
175+
176+
177+
function expectPair(args: string[] | undefined, pair: [string, string]) {
178+
if (!args) {
179+
throw new Error("Args is undefined");
180+
}
181+
const index = args.indexOf(pair[0]);
182+
if (index === -1) {
183+
throw new Error(`Pair ${pair[0]} not found in args`);
184+
}
185+
expect(args[index + 1]).toBe(pair[1]);
186+
}

0 commit comments

Comments
 (0)