diff --git a/open-sse/transformer/responsesTransformer.ts b/open-sse/transformer/responsesTransformer.ts index 1ef35e4aa6..f22f38f9ed 100644 --- a/open-sse/transformer/responsesTransformer.ts +++ b/open-sse/transformer/responsesTransformer.ts @@ -7,6 +7,15 @@ import { } from "../utils/reasoningPlaceholder.ts"; import * as fs from "fs"; import * as path from "path"; + +// #10223: threshold for detecting corrupted request_id fields. Normal +// request IDs are <100 chars. DeepSeek's SSE encoder bug produces 200+ +// char values with response-ID fragments. The 100-char gap between normal +// (<100) and threshold (200) provides safety margin for providers that +// use moderately longer IDs. The transformer never reads request_id, so +// stripping it has no functional impact on the output. +const CORRUPTED_REQUEST_ID_THRESHOLD = 200; + /** * Responses API Transformer * Converts OpenAI Chat Completions SSE to Codex Responses API SSE format @@ -605,6 +614,21 @@ export function createResponsesApiTransformStream( continue; } + // #10223: strip request_id when it looks corrupted (suspiciously + // long — normal request IDs are <100 chars). Some providers + // (DeepSeek) have SSE encoder bugs that leak response-ID fragments + // into this field, producing 200+ char values. Well-behaved + // providers' request_id is preserved. + if ( + typeof parsed.request_id === "string" && + parsed.request_id.length >= CORRUPTED_REQUEST_ID_THRESHOLD + ) { + logger?.logInput( + `[ResponsesTransformer] stripped corrupted request_id (${parsed.request_id.length} chars)` + ); + delete parsed.request_id; + } + if (parsed.usage) { state.usage = normalizeResponsesUsage(state.usage, parsed.usage); } diff --git a/tests/unit/responses-transformer-corrupted-request-id.test.ts b/tests/unit/responses-transformer-corrupted-request-id.test.ts new file mode 100644 index 0000000000..c952527b6d --- /dev/null +++ b/tests/unit/responses-transformer-corrupted-request-id.test.ts @@ -0,0 +1,83 @@ +// Regression guard for #10223: DeepSeek's SSE encoder bug leaks response-ID +// fragments into `request_id`, producing suspiciously long (200+ char) values. +// The transformer never reads `request_id` for its own output, but it must +// strip a corrupted one (logging that it did) and must NOT touch a normal, +// well-behaved provider's request_id. +import test from "node:test"; +import assert from "node:assert/strict"; + +const { createResponsesApiTransformStream } = await import( + "../../open-sse/transformer/responsesTransformer.ts" +); + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +async function runTransformStream(chunks, logger = null) { + const stream = createResponsesApiTransformStream(logger, 3000, {}); + const writer = stream.writable.getWriter(); + const reader = stream.readable.getReader(); + + const output = []; + const readerTask = (async () => { + while (true) { + const { value, done } = await reader.read(); + if (done) break; + output.push(decoder.decode(value)); + } + })(); + + for (const chunk of chunks) { + await writer.write(encoder.encode(chunk)); + } + await writer.close(); + await readerTask; + + return output.join(""); +} + +function makeMockLogger() { + const inputs = []; + return { + inputs, + logInput: (event) => inputs.push(event), + logOutput: () => {}, + flush: () => {}, + }; +} + +test("BUG #10223: a corrupted (>=200 char) request_id is stripped and logged", async () => { + const corruptedId = "r".repeat(250); + const logger = makeMockLogger(); + + await runTransformStream( + [ + `data: {"id":"chatcmpl_1","request_id":"${corruptedId}","choices":[{"index":0,"delta":{"content":"Hi"}}]}\n\n`, + 'data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}\n\n', + ], + logger + ); + + const stripLog = logger.inputs.find( + (entry) => typeof entry === "string" && entry.includes("stripped corrupted request_id") + ); + assert.ok(stripLog, "logger.logInput should be called noting the corrupted request_id was stripped"); + assert.match(stripLog, /\(250 chars\)/); +}); + +test("a normal (<200 char) request_id is left untouched — no strip logged", async () => { + const logger = makeMockLogger(); + + await runTransformStream( + [ + 'data: {"id":"chatcmpl_1","request_id":"req_normal_12345","choices":[{"index":0,"delta":{"content":"Hi"}}]}\n\n', + 'data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}\n\n', + ], + logger + ); + + const stripLog = logger.inputs.find( + (entry) => typeof entry === "string" && entry.includes("stripped corrupted request_id") + ); + assert.equal(stripLog, undefined, "a normal-length request_id must never be stripped"); +});