From 5cd22739b4256b6fe8f9a89ef20e71df362080e5 Mon Sep 17 00:00:00 2001 From: Tyagiquamar Date: Sun, 13 Sep 2026 14:38:50 +0530 Subject: [PATCH 1/4] fix(js): support /v1/embeddings endpoint in OpenAI batch instrumentation (#2425) --- .../openai-batch-instrumentation.test.ts | 29 +++++++++++++++++++ .../plugins/openai-batch-instrumentation.ts | 6 +++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/js/src/instrumentation/plugins/openai-batch-instrumentation.test.ts b/js/src/instrumentation/plugins/openai-batch-instrumentation.test.ts index 96a2809ef..b09093c95 100644 --- a/js/src/instrumentation/plugins/openai-batch-instrumentation.test.ts +++ b/js/src/instrumentation/plugins/openai-batch-instrumentation.test.ts @@ -394,4 +394,33 @@ describe("OpenAI Batch instrumentation", () => { expect(received).toBe(content); expect(await backgroundLogger.drain()).toEqual([]); }); + + it("supports /v1/embeddings batch requests", async () => { + const embeddingsInput = [ + { + custom_id: "emb1", + method: "POST", + url: "/v1/embeddings", + body: { + model: "text-embedding-3-small", + input: "hello world", + }, + }, + ]; + + let received = ""; + const result = await openaiFilesCreateTraced({ + create(params: { file: Blob; purpose: string }) { + return asyncAPIPromise( + Promise.resolve({ id: "file_emb" }).then(async (file) => { + received = await params.file.text(); + return file; + }), + ); + }, + })({ file: new Blob([jsonl(embeddingsInput)]), purpose: "batch" }); + + expect(result).toEqual({ id: "file_emb" }); + expect(received).toBe(jsonl(embeddingsInput)); + }); }); diff --git a/js/src/instrumentation/plugins/openai-batch-instrumentation.ts b/js/src/instrumentation/plugins/openai-batch-instrumentation.ts index 560ac51ca..cb2e2aa31 100644 --- a/js/src/instrumentation/plugins/openai-batch-instrumentation.ts +++ b/js/src/instrumentation/plugins/openai-batch-instrumentation.ts @@ -26,7 +26,11 @@ import { processImagesInOutput, } from "./openai-span-data"; -const SUPPORTED_ENDPOINTS = new Set(["/v1/chat/completions", "/v1/responses"]); +const SUPPORTED_ENDPOINTS = new Set([ + "/v1/chat/completions", + "/v1/responses", + "/v1/embeddings", +]); const TERMINAL_STATUSES = new Set([ "completed", "failed", From 1fc466863706d736141d8182246c02999d28d06b Mon Sep 17 00:00:00 2001 From: Tyagiquamar Date: Sun, 13 Sep 2026 21:54:42 +0530 Subject: [PATCH 2/4] fix(js): support Uint8Array and Buffer JSONL sources in OpenAI batch instrumentation --- .../plugins/openai-batch-instrumentation.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/js/src/instrumentation/plugins/openai-batch-instrumentation.ts b/js/src/instrumentation/plugins/openai-batch-instrumentation.ts index cb2e2aa31..01e8d4b54 100644 --- a/js/src/instrumentation/plugins/openai-batch-instrumentation.ts +++ b/js/src/instrumentation/plugins/openai-batch-instrumentation.ts @@ -241,8 +241,17 @@ async function* jsonlRecords( onIssue: (error: Error) => void = () => {}, ): AsyncGenerator { const resolvedFile = await file; - if (typeof resolvedFile === "string") { - for (const line of resolvedFile.split("\n")) { + if ( + typeof resolvedFile === "string" || + resolvedFile instanceof Uint8Array || + resolvedFile instanceof ArrayBuffer || + (typeof Buffer !== "undefined" && Buffer.isBuffer(resolvedFile)) + ) { + const text = + typeof resolvedFile === "string" + ? resolvedFile + : new TextDecoder().decode(resolvedFile); + for (const line of text.split("\n")) { if (!line.trim()) { continue; } From d5c225bdb7e76e393840d64a738b24199ca1248d Mon Sep 17 00:00:00 2001 From: lforst <8118419+lforst@users.noreply.github.com> Date: Thu, 17 Sep 2026 10:32:33 +0000 Subject: [PATCH 3/4] Update PR #2479 --- js/src/openai-batch-types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/js/src/openai-batch-types.ts b/js/src/openai-batch-types.ts index b6052d575..eebd92789 100644 --- a/js/src/openai-batch-types.ts +++ b/js/src/openai-batch-types.ts @@ -1,5 +1,6 @@ export type OpenAIBatchJSONL = | string + | ArrayBuffer | Iterable | AsyncIterable; From 25e63e7eec28afead55cff7e6891a3d9abf7627e Mon Sep 17 00:00:00 2001 From: lforst <8118419+lforst@users.noreply.github.com> Date: Thu, 17 Sep 2026 11:56:07 +0000 Subject: [PATCH 4/4] Update PR #2479 --- .changeset/fix-openai-batch-binary-jsonl.md | 5 ++ .../openai-instrumentation/assertions.ts | 37 +++++++++ .../openai-instrumentation/scenario.impl.mjs | 78 +++++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 .changeset/fix-openai-batch-binary-jsonl.md diff --git a/.changeset/fix-openai-batch-binary-jsonl.md b/.changeset/fix-openai-batch-binary-jsonl.md new file mode 100644 index 000000000..4e0fc6c9e --- /dev/null +++ b/.changeset/fix-openai-batch-binary-jsonl.md @@ -0,0 +1,5 @@ +--- +"braintrust": patch +--- + +fix(js): Support Uint8Array and Buffer JSONL sources in OpenAI batch instrumentation diff --git a/e2e/scenarios/openai-instrumentation/assertions.ts b/e2e/scenarios/openai-instrumentation/assertions.ts index af9f516fe..601f37fca 100644 --- a/e2e/scenarios/openai-instrumentation/assertions.ts +++ b/e2e/scenarios/openai-instrumentation/assertions.ts @@ -864,6 +864,43 @@ export function defineOpenAIInstrumentationAssertions(options: { } }); + test( + "accepts binary JSONL sources for OpenAI Batch traces", + testConfig, + () => { + const root = findLatestSpan(events, ROOT_NAME); + const operation = findLatestSpan( + events, + "openai-batch-binary-jsonl-operation", + ); + const task = findOpenAISpan(events, operation?.span.id, [ + "openai.batch", + ]); + const children = findOpenAISpans(events, task?.span.id, [ + "Chat Completion", + ]); + + expect(operation).toBeDefined(); + expect(operation?.row.metadata).toMatchObject({ + operation: "batch-binary-jsonl", + }); + expect(operation?.span.parentIds).toEqual([root?.span.id ?? ""]); + expect(task?.row.metadata).toMatchObject({ + endpoint: "/v1/chat/completions", + input_file_id: "file_binary_batch_e2e_fixture", + provider: "openai", + }); + expect(task?.span.parentIds).toEqual([operation?.span.id ?? ""]); + expect(spanInstrumentationName(task)).toBe("openai"); + expect(children).toHaveLength(EXPECTED_BATCH_OUTPUTS.size); + for (const child of children) { + expect(child.span.parentIds).toEqual([task?.span.id ?? ""]); + expect(spanInstrumentationName(child)).toBe("openai"); + } + validateChatBatchSpans(children); + }, + ); + const scenarioDir = path.dirname(fileURLToPath(options.testFileUrl)); const cassetteMode = process.env.BRAINTRUST_E2E_CASSETTE_MODE; const cassetteEngaged = diff --git a/e2e/scenarios/openai-instrumentation/scenario.impl.mjs b/e2e/scenarios/openai-instrumentation/scenario.impl.mjs index 634d50a06..7c0e2e14c 100644 --- a/e2e/scenarios/openai-instrumentation/scenario.impl.mjs +++ b/e2e/scenarios/openai-instrumentation/scenario.impl.mjs @@ -819,6 +819,84 @@ export async function runOpenAIInstrumentationScenario(options) { } }); + await runOperation( + "openai-batch-binary-jsonl-operation", + "batch-binary-jsonl", + async () => { + const batchItems = [ + { + customId: "batch_binary_alpha", + prompt: "Reply with exactly ALPHA.", + response: "ALPHA", + }, + { + customId: "batch_binary_bravo", + prompt: "Reply with exactly BRAVO.", + response: "BRAVO", + }, + { + customId: "batch_binary_charlie", + prompt: "Reply with exactly CHARLIE.", + response: "CHARLIE", + }, + ]; + const input = batchItems + .map((item) => + JSON.stringify({ + custom_id: item.customId, + method: "POST", + url: "/v1/chat/completions", + body: { + model: OPENAI_MODEL, + messages: [{ role: "user", content: item.prompt }], + }, + }), + ) + .join("\n"); + const output = [batchItems[1], batchItems[0]] + .map((item, index) => + JSON.stringify({ + custom_id: item.customId, + response: { + status_code: 200, + body: { + choices: [ + { + index: 0, + finish_reason: "stop", + message: { + role: "assistant", + content: item.response, + }, + }, + ], + usage: { + prompt_tokens: 8 + index, + completion_tokens: 1, + total_tokens: 9 + index, + }, + }, + }, + }), + ) + .join("\n"); + const error = JSON.stringify({ + custom_id: batchItems[2].customId, + error: { + code: "fixture_error", + message: "Batch fixture request failed", + }, + }); + + await completeOpenAIBatchTrace({ + inputFileId: "file_binary_batch_e2e_fixture", + inputFileContent: new TextEncoder().encode(input), + outputFileContent: Promise.resolve(Buffer.from(output)), + errorFileContent: await new Blob([error]).arrayBuffer(), + }); + }, + ); + await runOperation( "openai-embedding-batch-operation", "embedding-batch",