fix(headroom): skip unsafe responses tool history (#2132)
Guard openai-responses compression: skip Headroom when body.input contains non-message items (function_call, function_call_output, reasoning) to preserve the Responses contract instead of collapsing them into chat messages. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
decolua
parent
749c2e3f9c
commit
373850ee36
@@ -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 || {});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user