mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
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
co-authored by
Cursor
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.
|
// POST messages to Headroom /v1/compress; returns compressed messages + stats or null.
|
||||||
async function callCompress(url, messages, model, timeoutMs, compressUserMessages, diagnostics) {
|
async function callCompress(url, messages, model, timeoutMs, compressUserMessages, diagnostics) {
|
||||||
const endpoint = buildCompressEndpoint(url);
|
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
|
// messages. Translate input -> OpenAI -> compress -> translate back to input so
|
||||||
// body.input keeps the Responses contract (the proxy only understands OpenAI). (#1998)
|
// body.input keeps the Responses contract (the proxy only understands OpenAI). (#1998)
|
||||||
if (format === "openai-responses") {
|
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);
|
const oai = openaiResponsesToOpenAIRequest(model, body, false);
|
||||||
if (!Array.isArray(oai?.messages)) return null;
|
if (!Array.isArray(oai?.messages)) return null;
|
||||||
const data = await callCompress(url, oai.messages, model, timeoutMs, compressUserMessages, diagnostics || {});
|
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(Array.isArray(body.input[0].content)).toBe(true);
|
||||||
expect(typeof body.input[0].content).not.toBe("string");
|
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