diff --git a/open-sse/rtk/headroom.js b/open-sse/rtk/headroom.js index 1dd7abea..8f3b1f33 100644 --- a/open-sse/rtk/headroom.js +++ b/open-sse/rtk/headroom.js @@ -73,6 +73,14 @@ function maskEndpoint(endpoint) { } } +function hasUnsafeResponsesInputForCompression(body) { + if (!Array.isArray(body?.input)) return false; + return body.input.some((item) => { + if (!item || typeof item !== "object" || Array.isArray(item)) return false; + return typeof item.type === "string" && item.type !== "message"; + }); +} + // POST messages to Headroom /v1/compress; returns compressed messages + stats or null. async function callCompress(url, messages, model, timeoutMs, compressUserMessages, diagnostics) { const endpoint = buildCompressEndpoint(url); @@ -143,6 +151,10 @@ export async function compressWithHeadroom(body, { enabled, url, model, format, // messages. Translate input -> OpenAI -> compress -> translate back to input so // body.input keeps the Responses contract (the proxy only understands OpenAI). (#1998) if (format === "openai-responses") { + if (hasUnsafeResponsesInputForCompression(body)) { + setDiagnostic(diagnostics, "skipped: openai-responses tool/reasoning input is not safe to compress"); + return null; + } const oai = openaiResponsesToOpenAIRequest(model, body, false); if (!Array.isArray(oai?.messages)) return null; const data = await callCompress(url, oai.messages, model, timeoutMs, compressUserMessages, diagnostics || {}); diff --git a/tests/unit/headroom-responses-format.test.js b/tests/unit/headroom-responses-format.test.js index 0050a731..a796e16b 100644 --- a/tests/unit/headroom-responses-format.test.js +++ b/tests/unit/headroom-responses-format.test.js @@ -47,4 +47,61 @@ describe("compressWithHeadroom openai-responses format (#1998)", () => { expect(Array.isArray(body.input[0].content)).toBe(true); expect(typeof body.input[0].content).not.toBe("string"); }); + + it("skips Responses tool/reasoning history instead of collapsing it into a message (#2132)", async () => { + global.fetch = vi.fn(async () => ({ + ok: true, + json: async () => ({ + messages: [{ role: "user", content: "compressed tool history" }], + tokens_saved: 10, + }), + })); + + const input = [ + { + type: "message", + role: "user", + content: [{ type: "input_text", text: "investigate bug" }], + }, + { + type: "function_call", + call_id: "call_apply_patch_123", + name: "apply_patch", + arguments: "*** Begin Patch\n*** End Patch", + }, + { + type: "function_call_output", + call_id: "call_apply_patch_123", + output: "ok", + }, + { + type: "reasoning", + summary: [{ type: "summary_text", text: "Need a plan" }], + }, + ]; + const body = { + input: structuredClone(input), + tools: [ + { + type: "custom", + name: "apply_patch", + format: { type: "grammar", syntax: "lark", definition: "start: /.+/" }, + }, + ], + }; + const diagnostics = {}; + + const data = await compressWithHeadroom(body, { + enabled: true, + url: "http://headroom.test", + model: "gpt-5", + format: "openai-responses", + diagnostics, + }); + + expect(data).toBeNull(); + expect(global.fetch).not.toHaveBeenCalled(); + expect(body.input).toEqual(input); + expect(diagnostics.reason).toBe("skipped: openai-responses tool/reasoning input is not safe to compress"); + }); });