Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sdks/code-interpreter/javascript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"packageManager": "pnpm@9.15.0",
"scripts": {
"build": "tsup",
"test": "pnpm run build && node --test tests/*.test.mjs",
"lint": "eslint src --max-warnings 0",
"clean": "rm -rf dist"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ import type { Codes } from "../services/codes.js";
export interface CreateCodesStackOptions {
sandbox: Sandbox;
execdBaseUrl: string;
endpointHeaders?: Record<string, string>;
}

/**
* Factory abstraction for Code Interpreter SDK to decouple from concrete adapters/clients.
*/
export interface AdapterFactory {
createCodes(opts: CreateCodesStackOptions): Codes;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,19 @@ import type { Codes } from "../services/codes.js";

export class DefaultAdapterFactory implements AdapterFactory {
createCodes(opts: CreateCodesStackOptions): Codes {
const headers: Record<string, string> = {
...(opts.sandbox.connectionConfig.headers ?? {}),
...(opts.endpointHeaders ?? {}),
};
const client = createExecdClient({
baseUrl: opts.execdBaseUrl,
headers: opts.sandbox.connectionConfig.headers,
headers,
fetch: opts.sandbox.connectionConfig.fetch,
});

return new CodesAdapter(client, {
baseUrl: opts.execdBaseUrl,
headers: opts.sandbox.connectionConfig.headers,
headers,
// Streaming calls (SSE) use a dedicated fetch, aligned with Kotlin/Python SDKs.
fetch: opts.sandbox.connectionConfig.sseFetch,
});
Expand All @@ -36,4 +40,4 @@ export class DefaultAdapterFactory implements AdapterFactory {

export function createDefaultAdapterFactory(): AdapterFactory {
return new DefaultAdapterFactory();
}
}
11 changes: 8 additions & 3 deletions sdks/code-interpreter/javascript/src/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,14 @@ export class CodeInterpreter {
) {}

static async create(sandbox: Sandbox, opts: CodeInterpreterCreateOptions = {}): Promise<CodeInterpreter> {
const execdBaseUrl = await sandbox.getEndpointUrl(DEFAULT_EXECD_PORT);
const endpoint = await sandbox.getEndpoint(DEFAULT_EXECD_PORT);
const execdBaseUrl = `${sandbox.connectionConfig.protocol}://${endpoint.endpoint}`;
const adapterFactory = opts.adapterFactory ?? createDefaultAdapterFactory();
const codes = adapterFactory.createCodes({ sandbox, execdBaseUrl });
const codes = adapterFactory.createCodes({
sandbox,
execdBaseUrl,
endpointHeaders: endpoint.headers,
});

return new CodeInterpreter(sandbox, codes);
}
Expand All @@ -61,4 +66,4 @@ export class CodeInterpreter {
get metrics() {
return this.sandbox.metrics;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import assert from "node:assert/strict";
import test from "node:test";

import { DefaultAdapterFactory } from "../dist/index.js";

test("DefaultAdapterFactory merges sandbox and endpoint headers for code requests", async () => {
const recorded = [];
const fetchImpl = async (input, init = {}) => {
const request = input instanceof Request ? input : new Request(input, init);
const url = new URL(request.url);
const headers = Object.fromEntries(request.headers.entries());
recorded.push({
url: request.url,
method: request.method,
headers,
});

if (url.pathname === "/code/context") {
return new Response(JSON.stringify({ id: "ctx-1", language: "python" }), {
status: 200,
headers: { "content-type": "application/json" },
});
}

return new Response(
[
JSON.stringify({ type: "stdout", text: "hello", timestamp: 1 }),
JSON.stringify({ type: "execution_complete", execution_time: 2, timestamp: 2 }),
].join("\n"),
{
status: 200,
headers: { "content-type": "text/event-stream" },
}
);
};

const sandbox = {
connectionConfig: {
headers: { "x-global": "global" },
fetch: fetchImpl,
sseFetch: fetchImpl,
},
};

const factory = new DefaultAdapterFactory();
const codes = factory.createCodes({
sandbox,
execdBaseUrl: "http://sandbox.internal:3456",
endpointHeaders: { "x-endpoint": "endpoint" },
});

const context = await codes.createContext("python");
assert.equal(context.id, "ctx-1");

const execution = await codes.run("print('hello')");
assert.equal(execution.logs.stdout[0]?.text, "hello");

assert.equal(recorded.length, 2);
assert.equal(recorded[0].url, "http://sandbox.internal:3456/code/context");
assert.equal(recorded[0].headers["x-global"], "global");
assert.equal(recorded[0].headers["x-endpoint"], "endpoint");
assert.equal(recorded[1].url, "http://sandbox.internal:3456/code");
assert.equal(recorded[1].headers["x-global"], "global");
assert.equal(recorded[1].headers["x-endpoint"], "endpoint");
assert.equal(recorded[1].headers.accept, "text/event-stream");
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import assert from "node:assert/strict";
import test from "node:test";

import { CodeInterpreter } from "../dist/index.js";
import { DEFAULT_EXECD_PORT } from "../../../sandbox/javascript/dist/index.js";

test("CodeInterpreter.create forwards endpoint headers to adapter factory", async () => {
const calls = [];
const sandbox = {
connectionConfig: {
protocol: "https",
headers: { "x-global": "global" },
},
async getEndpoint(port) {
assert.equal(port, DEFAULT_EXECD_PORT);
return {
endpoint: "sandbox.internal:3456",
headers: { "x-endpoint": "endpoint" },
};
},
};
const codes = { kind: "codes" };
const adapterFactory = {
createCodes(opts) {
calls.push(opts);
return codes;
},
};

const interpreter = await CodeInterpreter.create(sandbox, { adapterFactory });

assert.equal(interpreter.codes, codes);
assert.equal(calls.length, 1);
assert.equal(calls[0].execdBaseUrl, "https://sandbox.internal:3456");
assert.deepEqual(calls[0].endpointHeaders, { "x-endpoint": "endpoint" });
});
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import com.alibaba.opensandbox.sandbox.infrastructure.adapters.converter.Executi
import com.alibaba.opensandbox.sandbox.infrastructure.adapters.converter.jsonParser
import com.alibaba.opensandbox.sandbox.infrastructure.adapters.converter.parseSandboxError
import com.alibaba.opensandbox.sandbox.infrastructure.adapters.converter.toSandboxException
import okhttp3.Headers.Companion.toHeaders
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
Expand All @@ -49,8 +50,19 @@ class CodesAdapter(
}

private val logger = LoggerFactory.getLogger(CodesAdapter::class.java)
private val baseUrl = "${httpClientProvider.config.protocol}://${execdEndpoint.endpoint}"
private val apiClient =
httpClientProvider.httpClient.newBuilder()
.addInterceptor { chain ->
val requestBuilder = chain.request().newBuilder()
execdEndpoint.headers.forEach { (key, value) ->
requestBuilder.header(key, value)
}
chain.proceed(requestBuilder.build())
}
.build()
private val api =
CodeInterpretingApi("${httpClientProvider.config.protocol}://${execdEndpoint.endpoint}", httpClientProvider.httpClient)
CodeInterpretingApi(baseUrl, apiClient)

override fun getContext(id: String): CodeContext {
try {
Expand Down Expand Up @@ -109,10 +121,11 @@ class CodesAdapter(
val apiRequest = request.toApiRunCodeRequest()
val httpRequest =
Request.Builder()
.url("${httpClientProvider.config.protocol}://${execdEndpoint.endpoint}$RUN_CODE_PATH")
.url("$baseUrl$RUN_CODE_PATH")
.post(
jsonParser.encodeToString(apiRequest).toRequestBody("application/json".toMediaType()),
)
.headers(execdEndpoint.headers.toHeaders())
.build()

val execution = Execution()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,32 @@ class CodesAdapterTest {
assertEquals("/code/context", request.path)
}

@Test
fun `createContext should include endpoint headers`() {
mockWebServer.enqueue(
MockResponse()
.setResponseCode(200)
.setBody("""{"id":"ctx-123", "language":"python"}"""),
)

val host = mockWebServer.hostName
val port = mockWebServer.port
val config =
ConnectionConfig.builder()
.domain("$host:$port")
.protocol("http")
.build()
val endpoint = SandboxEndpoint("$host:$port", mapOf("X-Endpoint" to "endpoint"))

HttpClientProvider(config).use { provider ->
val adapter = CodesAdapter(endpoint, provider)
adapter.createContext("python")
}

val request = mockWebServer.takeRequest()
assertEquals("endpoint", request.getHeader("X-Endpoint"))
}

@Test
fun `run should stream events correctly`() {
// SSE format
Expand Down Expand Up @@ -124,6 +150,41 @@ class CodesAdapterTest {
assertEquals("POST", recordedRequest.method)
}

@Test
fun `run should include endpoint headers`() {
val event1 = """{"type":"stdout","text":"Hello World","timestamp":1672531200000}"""
val event2 = """{"type":"execution_complete","execution_time":100,"timestamp":1672531201000}"""

mockWebServer.enqueue(
MockResponse()
.setResponseCode(200)
.setBody("$event1\n$event2\n"),
)

val host = mockWebServer.hostName
val port = mockWebServer.port
val config =
ConnectionConfig.builder()
.domain("$host:$port")
.protocol("http")
.build()
val endpoint = SandboxEndpoint("$host:$port", mapOf("X-Endpoint" to "endpoint"))

HttpClientProvider(config).use { provider ->
val adapter = CodesAdapter(endpoint, provider)
val request =
RunCodeRequest.builder()
.code("print('Hello World')")
.handlers(ExecutionHandlers.builder().build())
.build()

adapter.run(request)
}

val recordedRequest = mockWebServer.takeRequest()
assertEquals("endpoint", recordedRequest.getHeader("X-Endpoint"))
}

@Test
fun `interrupt should send correct request`() {
mockWebServer.enqueue(MockResponse().setResponseCode(204))
Expand Down
Loading