Skip to content

Commit 8be22aa

Browse files
koki-developclaude
andcommitted
feat: Update SDK to match OpenAPI v1.9.0 spec
- Rename `language` to `runtime` with new SandboxRuntime type - Add `base64Encoded` optional field to files with camelCase-to-snake_case transform - Flatten error response structure (remove nested `error` wrapper) - Add new error codes (INVALID_REQUEST_BODY, EXECUTION_TIMEOUT, etc.) - Change exitCode from nullable to non-nullable - Update SandboxStageStatus values (SUCCESS→OK, remove RUNTIME_ERROR/ERROR_LIMIT_EXCEEDED) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 1e586a8 commit 8be22aa

File tree

6 files changed

+148
-84
lines changed

6 files changed

+148
-84
lines changed

typescript/CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ bun test # run all tests via vitest
4747

4848
Three source files:
4949

50-
- **`src/client.ts`**`CodizeClient` class. Takes `{ apiKey, fetchFn? }`. Makes `POST https://codize.dev/api/v1/sandbox/execute` with Bearer auth. Returns `SandboxExecuteResponse` containing `data.compile` (nullable) and `data.run` (nullable) stage results plus `headers`.
51-
- **`src/error.ts`**`CodizeApiError` class. Thrown on non-2xx responses. Parses error body using valibot's `apiErrorResponseSchema`. Exposes `.code`, `.status`, `.headers`, `.errors`.
50+
- **`src/client.ts`**`CodizeClient` class. Takes `{ apiKey, fetchFn? }`. Makes `POST https://codize.dev/api/v1/sandbox/execute` with Bearer auth. Transforms request (camelCase `base64Encoded` → snake_case `base64_encoded`). Returns `SandboxExecuteResponse` containing `data.compile` (nullable) and `data.run` (nullable) stage results plus `headers`.
51+
- **`src/error.ts`**`CodizeApiError` class. Thrown on non-2xx responses with a flat `{ code, message, errors? }` JSON body. Parses error body using valibot's `apiErrorResponseSchema`. Exposes `.code`, `.status`, `.headers`, `.errors`.
5252
- **`src/index.ts`** — Barrel file re-exporting all public types and classes from `client.ts` and `error.ts`.
5353

5454
## Key Dependencies

typescript/src/client.spec.ts

Lines changed: 69 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,16 @@ const makeTextResponse = (body: string, status: number): Response =>
2020
new Response(body, { status });
2121

2222
const sampleRunResult = {
23-
stdout: "Hello\n",
23+
stdout: "SGVsbG8K",
2424
stderr: "",
25-
output: "Hello\n",
25+
output: "SGVsbG8K",
2626
exit_code: 0,
2727
signal: null,
28-
status: "SUCCESS",
28+
status: "OK",
2929
};
3030

3131
const sampleRequest = {
32-
language: "python" as const,
32+
runtime: "python" as const,
3333
files: [{ name: "main.py", content: 'print("Hello")' }],
3434
};
3535

@@ -108,7 +108,48 @@ describe("CodizeClient", () => {
108108
await client.sandbox.execute(sampleRequest);
109109

110110
const init = fetchFn.mock.calls[0]![1] as RequestInit;
111-
expect(JSON.parse(init.body as string)).toEqual(sampleRequest);
111+
expect(JSON.parse(init.body as string)).toEqual({
112+
runtime: "python",
113+
files: [{ name: "main.py", content: 'print("Hello")', base64_encoded: false }],
114+
});
115+
});
116+
117+
it("sends base64_encoded flag when specified", async () => {
118+
const fetchFn = vi.fn().mockResolvedValue(
119+
makeJsonResponse({ compile: null, run: sampleRunResult }),
120+
);
121+
const client = new CodizeClient({ apiKey: "key", fetchFn });
122+
await client.sandbox.execute({
123+
runtime: "python",
124+
files: [{ name: "data.bin", content: "aGVsbG8=", base64Encoded: true }],
125+
});
126+
127+
const init = fetchFn.mock.calls[0]![1] as RequestInit;
128+
const body = JSON.parse(init.body as string);
129+
expect(body.files[0].base64_encoded).toBe(true);
130+
});
131+
132+
it("handles mixed base64Encoded values across multiple files", async () => {
133+
const fetchFn = vi.fn().mockResolvedValue(
134+
makeJsonResponse({ compile: null, run: sampleRunResult }),
135+
);
136+
const client = new CodizeClient({ apiKey: "key", fetchFn });
137+
await client.sandbox.execute({
138+
runtime: "python",
139+
files: [
140+
{ name: "main.py", content: 'print("Hello")' },
141+
{ name: "data.bin", content: "aGVsbG8=", base64Encoded: true },
142+
{ name: "config.json", content: '{}', base64Encoded: false },
143+
],
144+
});
145+
146+
const init = fetchFn.mock.calls[0]![1] as RequestInit;
147+
const body = JSON.parse(init.body as string);
148+
expect(body.files).toEqual([
149+
{ name: "main.py", content: 'print("Hello")', base64_encoded: false },
150+
{ name: "data.bin", content: "aGVsbG8=", base64_encoded: true },
151+
{ name: "config.json", content: '{}', base64_encoded: false },
152+
]);
112153
});
113154
});
114155

@@ -123,9 +164,9 @@ describe("CodizeClient", () => {
123164
output: "",
124165
exit_code: 0,
125166
signal: null,
126-
status: "SUCCESS",
167+
status: "OK",
127168
},
128-
run: { stdout: "ok\n", stderr: "", output: "ok\n", exit_code: 0, signal: null, status: "SUCCESS" },
169+
run: { stdout: "b2sK", stderr: "", output: "b2sK", exit_code: 0, signal: null, status: "OK" },
129170
};
130171
const fetchFn = vi.fn().mockResolvedValue(makeJsonResponse(body));
131172
const client = new CodizeClient({ apiKey: "key", fetchFn });
@@ -137,22 +178,22 @@ describe("CodizeClient", () => {
137178
output: "",
138179
exitCode: 0,
139180
signal: null,
140-
status: "SUCCESS",
181+
status: "OK",
141182
});
142183
expect(result.data.run).toEqual({
143-
stdout: "ok\n",
184+
stdout: "b2sK",
144185
stderr: "",
145-
output: "ok\n",
186+
output: "b2sK",
146187
exitCode: 0,
147188
signal: null,
148-
status: "SUCCESS",
189+
status: "OK",
149190
});
150191
});
151192

152193
it("returns compile as null when the API returns null", async () => {
153194
const body = {
154195
compile: null,
155-
run: { stdout: "", stderr: "", output: "", exit_code: 0, signal: null, status: "SUCCESS" },
196+
run: { stdout: "", stderr: "", output: "", exit_code: 0, signal: null, status: "OK" },
156197
};
157198
const fetchFn = vi.fn().mockResolvedValue(makeJsonResponse(body));
158199
const client = new CodizeClient({ apiKey: "key", fetchFn });
@@ -193,7 +234,8 @@ describe("CodizeClient", () => {
193234
describe("structured API error", () => {
194235
it("throws CodizeApiError for a structured error response", async () => {
195236
const errorBody = {
196-
error: { code: "UNAUTHORIZED", message: "Invalid API key" },
237+
code: "UNAUTHORIZED",
238+
message: "Invalid API key",
197239
};
198240
const fetchFn = vi.fn().mockResolvedValue(
199241
makeJsonResponse(errorBody, 401),
@@ -207,7 +249,8 @@ describe("CodizeClient", () => {
207249

208250
it("sets the correct status on CodizeApiError", async () => {
209251
const errorBody = {
210-
error: { code: "RATE_LIMITED", message: "Too many requests" },
252+
code: "RATE_LIMITED",
253+
message: "Too many requests",
211254
};
212255
const fetchFn = vi.fn().mockResolvedValue(
213256
makeJsonResponse(errorBody, 429),
@@ -223,7 +266,8 @@ describe("CodizeClient", () => {
223266

224267
it("sets the correct code on CodizeApiError", async () => {
225268
const errorBody = {
226-
error: { code: "RATE_LIMITED", message: "Too many requests" },
269+
code: "RATE_LIMITED",
270+
message: "Too many requests",
227271
};
228272
const fetchFn = vi.fn().mockResolvedValue(
229273
makeJsonResponse(errorBody, 429),
@@ -238,7 +282,8 @@ describe("CodizeClient", () => {
238282

239283
it("sets the correct message on CodizeApiError", async () => {
240284
const errorBody = {
241-
error: { code: "UNAUTHORIZED", message: "Invalid API key" },
285+
code: "UNAUTHORIZED",
286+
message: "Invalid API key",
242287
};
243288
const fetchFn = vi.fn().mockResolvedValue(
244289
makeJsonResponse(errorBody, 401),
@@ -253,13 +298,11 @@ describe("CodizeClient", () => {
253298

254299
it("includes validation errors array when present", async () => {
255300
const errorBody = {
256-
error: {
257-
code: "VALIDATION_ERROR",
258-
message: "Validation failed",
259-
errors: [
260-
{ message: "'files' is required", path: ["files"] },
261-
],
262-
},
301+
code: "VALIDATION_ERROR",
302+
message: "Validation failed",
303+
errors: [
304+
{ message: "'files' is required", path: ["files"] },
305+
],
263306
};
264307
const fetchFn = vi.fn().mockResolvedValue(
265308
makeJsonResponse(errorBody, 422),
@@ -270,13 +313,14 @@ describe("CodizeClient", () => {
270313
.catch((e: unknown) => e);
271314

272315
expect((error as CodizeApiError).errors).toEqual(
273-
errorBody.error.errors,
316+
errorBody.errors,
274317
);
275318
});
276319

277320
it("attaches response headers to CodizeApiError", async () => {
278321
const errorBody = {
279-
error: { code: "UNAUTHORIZED", message: "Invalid API key" },
322+
code: "UNAUTHORIZED",
323+
message: "Invalid API key",
280324
};
281325
const fetchFn = vi.fn().mockResolvedValue(
282326
makeJsonResponse(errorBody, 401, {

typescript/src/client.ts

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,23 @@ export type CodizeClientOptions = {
2626
};
2727

2828
/**
29-
* Request payload for `sandbox.execute`.
29+
* Runtime identifier for sandbox execution.
3030
*/
31+
export type SandboxRuntime =
32+
| "bash"
33+
| "go"
34+
| "node"
35+
| "node-typescript"
36+
| "python"
37+
| "ruby"
38+
| "rust"
39+
| (string & {});
40+
3141
export type SandboxExecuteRequest = {
3242
/**
33-
* Language name used for execution.
43+
* Runtime used for execution.
3444
*/
35-
language:
36-
| "go"
37-
| "javascript"
38-
| "python"
39-
| "ruby"
40-
| "rust"
41-
| "typescript"
42-
| (string & {});
45+
runtime: SandboxRuntime;
4346
/**
4447
* Source files to execute, with their content.
4548
*/
@@ -49,9 +52,13 @@ export type SandboxExecuteRequest = {
4952
*/
5053
name: string;
5154
/**
52-
* Full text content of the file.
55+
* File content. Plain text by default, or Base64-encoded data when `base64Encoded` is `true`.
5356
*/
5457
content: string;
58+
/**
59+
* Whether the content is Base64 encoded.
60+
*/
61+
base64Encoded?: boolean;
5562
}[];
5663
};
5764

@@ -79,12 +86,10 @@ export type SandboxExecuteResponse = {
7986
* Execution status of a sandbox stage.
8087
*/
8188
export type SandboxStageStatus =
82-
| "SUCCESS"
83-
| "RUNTIME_ERROR"
89+
| "OK"
8490
| "SIGNAL"
8591
| "TIMEOUT"
8692
| "OUTPUT_LIMIT_EXCEEDED"
87-
| "ERROR_LIMIT_EXCEEDED"
8893
| (string & {});
8994

9095
/**
@@ -106,7 +111,7 @@ export type SandboxStageResult = {
106111
/**
107112
* Process exit code.
108113
*/
109-
exitCode: number | null;
114+
exitCode: number;
110115
/**
111116
* Signal name that terminated the process, or null if not terminated by a signal.
112117
*/
@@ -124,7 +129,7 @@ type RawStageResult = {
124129
stdout: string;
125130
stderr: string;
126131
output: string;
127-
exit_code: number | null;
132+
exit_code: number;
128133
signal: string | null;
129134
status: SandboxStageStatus;
130135
};
@@ -188,6 +193,15 @@ export class CodizeClient {
188193
private async _sandboxExecute(
189194
request: SandboxExecuteRequest,
190195
): Promise<SandboxExecuteResponse> {
196+
const body = {
197+
runtime: request.runtime,
198+
files: request.files.map((f) => ({
199+
name: f.name,
200+
content: f.content,
201+
base64_encoded: f.base64Encoded ?? false,
202+
})),
203+
};
204+
191205
const response = await this._fetchFn(
192206
new URL("/api/v1/sandbox/execute", this._baseUrl),
193207
{
@@ -196,7 +210,7 @@ export class CodizeClient {
196210
"Content-Type": "application/json",
197211
Authorization: `Bearer ${this._apiKey}`,
198212
},
199-
body: JSON.stringify(request),
213+
body: JSON.stringify(body),
200214
},
201215
);
202216

typescript/src/error.spec.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,41 @@ describe("CodizeApiError", () => {
77

88
it("sets name to 'CodizeApiError'", () => {
99
const error = new CodizeApiError(401, makeHeaders(), {
10-
error: { code: "UNAUTHORIZED", message: "Unauthorized" },
10+
code: "UNAUTHORIZED",
11+
message: "Unauthorized",
1112
});
1213
expect(error.name).toBe("CodizeApiError");
1314
});
1415

15-
it("sets message from response.error.message", () => {
16+
it("sets message from response.message", () => {
1617
const error = new CodizeApiError(401, makeHeaders(), {
17-
error: { code: "UNAUTHORIZED", message: "Invalid API key" },
18+
code: "UNAUTHORIZED",
19+
message: "Invalid API key",
1820
});
1921
expect(error.message).toBe("Invalid API key");
2022
});
2123

22-
it("sets code from response.error.code", () => {
24+
it("sets code from response.code", () => {
2325
const error = new CodizeApiError(401, makeHeaders(), {
24-
error: { code: "UNAUTHORIZED", message: "Unauthorized" },
26+
code: "UNAUTHORIZED",
27+
message: "Unauthorized",
2528
});
2629
expect(error.code).toBe("UNAUTHORIZED");
2730
});
2831

2932
it("sets status from the first argument", () => {
3033
const error = new CodizeApiError(429, makeHeaders(), {
31-
error: { code: "RATE_LIMITED", message: "Too many requests" },
34+
code: "RATE_LIMITED",
35+
message: "Too many requests",
3236
});
3337
expect(error.status).toBe(429);
3438
});
3539

3640
it("sets headers from the second argument", () => {
3741
const headers = makeHeaders({ "x-request-id": "abc123" });
3842
const error = new CodizeApiError(500, headers, {
39-
error: { code: "INTERNAL_ERROR", message: "Server error" },
43+
code: "INTERNAL_ERROR",
44+
message: "Server error",
4045
});
4146
expect(error.headers).toBe(headers);
4247
expect(error.headers.get("x-request-id")).toBe("abc123");
@@ -45,25 +50,25 @@ describe("CodizeApiError", () => {
4550
it("sets errors when provided", () => {
4651
const errors = [{ message: "field required", path: ["files", 0] }];
4752
const error = new CodizeApiError(422, makeHeaders(), {
48-
error: {
49-
code: "VALIDATION_ERROR",
50-
message: "Validation failed",
51-
errors,
52-
},
53+
code: "VALIDATION_ERROR",
54+
message: "Validation failed",
55+
errors,
5356
});
5457
expect(error.errors).toEqual(errors);
5558
});
5659

5760
it("leaves errors undefined when not provided", () => {
5861
const error = new CodizeApiError(401, makeHeaders(), {
59-
error: { code: "UNAUTHORIZED", message: "Unauthorized" },
62+
code: "UNAUTHORIZED",
63+
message: "Unauthorized",
6064
});
6165
expect(error.errors).toBeUndefined();
6266
});
6367

6468
it("is an instance of Error", () => {
6569
const error = new CodizeApiError(500, makeHeaders(), {
66-
error: { code: "INTERNAL_ERROR", message: "Oops" },
70+
code: "INTERNAL_ERROR",
71+
message: "Oops",
6772
});
6873
expect(error).toBeInstanceOf(Error);
6974
});

0 commit comments

Comments
 (0)