fix(open-sse): cover nested translator shapes + system fields in dedup hash (#10438)

computeRequestHash() only read top-level body.messages ?? body.contents ??
body.input, but several translated request shapes nest their prompt
content: the Antigravity Cloud Code envelope under request.contents, and
Kiro under conversationState.currentMessage.userInputMessage.content (plus
conversationState.history). Two different concurrent prompts to those
targets could hash identically and share/leak a response between callers.

Adds extractPromptContent()/extractSystemContent() helpers covering every
prompt-bearing shape produced by open-sse/translator/request/*.ts
(OpenAI/Cursor messages, Claude messages+system, Gemini contents+
systemInstruction, Responses input+instructions, Antigravity and Kiro
nesting), and folds system/instructions/systemInstruction into the
canonical hash so two requests with the same user message but a different
system prompt no longer collide either.
This commit is contained in:
adevwithpurpose
2026-08-17 22:43:28 -03:00
parent fb9984760c
commit d84a62393f
2 changed files with 211 additions and 8 deletions

View File

@@ -32,23 +32,110 @@ export interface DedupResult<T> {
const inflight = new Map<string, Promise<unknown>>();
function asRecord(value: unknown): Record<string, unknown> | null {
return value !== null && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
/**
* Extract the prompt-bearing content from a (possibly translated) request body.
*
* The prompt content lives under different keys depending on the target
* provider format the body has already been translated to:
* - OpenAI-style bodies (`open-sse/translator/request/*-to-openai.ts`,
* `openai-to-cursor.ts`): `messages`
* - Gemini-translated bodies (`openai-to-gemini.ts`,
* `claude-to-gemini.ts`): `contents`
* - Responses-API-translated bodies (`openai-responses/toResponses.ts`):
* `input`
* - Antigravity-translated bodies (`openai-to-gemini.ts`
* `openaiToAntigravityRequest` / `wrapInCloudCodeEnvelope`): nested under
* `request.contents` (a Cloud Code envelope wrapper)
* - Kiro-translated bodies (`openai-to-kiro.ts` `buildKiroPayload`): nested
* under `conversationState.currentMessage.userInputMessage.content` (the
* current turn) plus `conversationState.history` (prior turns)
*
* Falling back to only `messages` made every non-OpenAI-format body hash the
* prompt as `null`, colliding different prompts onto the same dedup hash
* (#10249). The Antigravity/Kiro nesting was still missed by the flat
* `messages ?? contents ?? input` fallback chain, so different prompts
* targeting those two providers still collided (#10438).
*/
function extractPromptContent(body: Record<string, unknown>): unknown {
if (body.messages !== undefined) return body.messages;
if (body.contents !== undefined) return body.contents;
if (body.input !== undefined) return body.input;
// Antigravity Cloud Code envelope: { request: { contents, ... } }
const request = asRecord(body.request);
if (request && request.contents !== undefined) {
return request.contents;
}
// Kiro conversationState envelope:
// { conversationState: { currentMessage: { userInputMessage: { content } }, history } }
const conversationState = asRecord(body.conversationState);
if (conversationState) {
const currentMessage = asRecord(conversationState.currentMessage);
const userInputMessage = asRecord(currentMessage?.userInputMessage);
if (userInputMessage || conversationState.history !== undefined) {
return {
content: userInputMessage?.content ?? null,
history: conversationState.history ?? null,
};
}
}
return null;
}
/**
* Extract the system/instruction content that shapes generation but is not
* carried in the message list itself. Two requests with the same user
* message but a different system prompt must hash differently — omitting
* this field let them collide.
*
* - Claude-translated bodies (`openai-to-claude.ts`): `system`
* - Responses-API-translated bodies (`openai-responses/toResponses.ts`):
* `instructions`
* - Gemini-translated bodies (`openai-to-gemini.ts`, `claude-to-gemini.ts`):
* `systemInstruction`
* - Antigravity-translated bodies: nested under `request.systemInstruction`
* (note: the client system prompt is folded into `request.contents[0]`
* instead per #9030, so this is usually the constant Antigravity
* default — it is still included for completeness/future-proofing)
*/
function extractSystemContent(body: Record<string, unknown>): unknown {
if (body.system !== undefined) return body.system;
if (body.instructions !== undefined) return body.instructions;
if (body.systemInstruction !== undefined) return body.systemInstruction;
const request = asRecord(body.request);
if (request && request.systemInstruction !== undefined) {
return request.systemInstruction;
}
return null;
}
/**
* Compute a deterministic hash for a request body.
* Includes: model, messages, temperature, tools, tool_choice, max_tokens, response_format
* Includes: model, messages/prompt content, system/instructions, temperature,
* tools, tool_choice, max_tokens, response_format
* Excludes: stream, user, metadata (don't affect LLM output)
*
* The prompt content can live under different keys depending on the target
* provider format the body has already been translated to: OpenAI-style
* bodies use `messages`, Gemini-translated bodies use `contents`, and
* Responses-API-translated bodies use `input`. Falling back to only
* `messages` made every non-OpenAI-format body hash the prompt as `null`,
* colliding different prompts onto the same dedup hash (#10249).
* `computeRequestHash` is called post-translation (`chatCore.ts`, on
* `translatedBody`), so the body shape here is whatever the target provider
* format produced — see `extractPromptContent`/`extractSystemContent` for the
* full list of shapes this must cover (#10249, #10438).
*/
export function computeRequestHash(requestBody: unknown): string {
const body = requestBody as Record<string, unknown>;
const canonical = {
model: body.model ?? null,
messages: body.messages ?? body.contents ?? body.input ?? null,
messages: extractPromptContent(body),
system: extractSystemContent(body),
temperature: typeof body.temperature === "number" ? body.temperature : 1.0,
tools: body.tools ?? null,
tool_choice: body.tool_choice ?? null,

View File

@@ -63,6 +63,122 @@ test("Sanity: OpenAI-format bodies with different prompts DO get distinct hashes
assert.notEqual(hashA, hashB);
});
// Regression tests for #10438: the flat `messages ?? contents ?? input`
// fallback chain from #10249 still missed the NESTED prompt shapes that
// `openai-to-gemini.ts::wrapInCloudCodeEnvelope` (Antigravity) and
// `openai-to-kiro.ts::buildKiroPayload` (Kiro) actually produce, and never
// looked at the system/instruction fields (`system` for Claude, `instructions`
// for the Responses API, `systemInstruction` for Gemini) at all — two
// requests with the same user message but a different system prompt hashed
// identically.
test("Antigravity Cloud Code envelope bodies with different prompts must NOT collide on dedup hash", async () => {
clearInflight();
const buildEnvelope = (text: string) => ({
project: "proj-123",
requestId: "req-abc",
request: {
sessionId: "sess-1",
contents: [{ role: "user", parts: [{ text }] }],
systemInstruction: { role: "system", parts: [{ text: "You are Antigravity." }] },
generationConfig: { maxOutputTokens: 8192 },
},
model: "gemini-3-pro",
userAgent: "antigravity/1.0",
requestType: "agent",
});
const bodyA = buildEnvelope("Summarize the Q3 financial report attached.");
const bodyB = buildEnvelope("Extract every invoice number from the attached PDF.");
const hashA = computeRequestHash({ ...bodyA, model: "antigravity/gemini-3-pro", stream: false });
const hashB = computeRequestHash({ ...bodyB, model: "antigravity/gemini-3-pro", stream: false });
assert.notEqual(hashA, hashB, "Different prompts must have different dedup hashes");
const [resA, resB] = await Promise.all([
deduplicate(hashA, async () => "RESPONSE_A"),
deduplicate(hashB, async () => "RESPONSE_B"),
]);
assert.equal(resA.result, "RESPONSE_A");
assert.equal(resB.result, "RESPONSE_B");
assert.equal(resB.wasDeduplicated, false);
});
test("Kiro conversationState bodies with different prompts must NOT collide on dedup hash", async () => {
clearInflight();
const buildPayload = (content: string) => ({
conversationState: {
chatTriggerType: "MANUAL",
conversationId: "conv-1",
currentMessage: {
userInputMessage: {
content,
modelId: "kiro-claude-sonnet",
origin: "AI_EDITOR",
},
},
history: [],
},
});
const bodyA = buildPayload("[Context: Current time is 2026-08-17]\n\nWhat is the capital of France?");
const bodyB = buildPayload("[Context: Current time is 2026-08-17]\n\nExplain quantum entanglement.");
const hashA = computeRequestHash({ ...bodyA, model: "kiro/claude-sonnet-4.5", stream: false });
const hashB = computeRequestHash({ ...bodyB, model: "kiro/claude-sonnet-4.5", stream: false });
assert.notEqual(hashA, hashB, "Different prompts must have different dedup hashes");
const [resA, resB] = await Promise.all([
deduplicate(hashA, async () => "RESPONSE_A"),
deduplicate(hashB, async () => "RESPONSE_B"),
]);
assert.equal(resA.result, "RESPONSE_A");
assert.equal(resB.result, "RESPONSE_B");
assert.equal(resB.wasDeduplicated, false);
});
test("Claude-translated bodies with the same messages but different `system` prompts must NOT collide", () => {
const bodyA = {
messages: [{ role: "user", content: [{ type: "text", text: "Hello" }] }],
system: [{ type: "text", text: "You are a pirate. Speak like one." }],
temperature: 0,
};
const bodyB = {
messages: [{ role: "user", content: [{ type: "text", text: "Hello" }] }],
system: [{ type: "text", text: "You are a formal legal assistant." }],
temperature: 0,
};
const hashA = computeRequestHash({ ...bodyA, model: "anthropic/claude-sonnet-4.5", stream: false });
const hashB = computeRequestHash({ ...bodyB, model: "anthropic/claude-sonnet-4.5", stream: false });
assert.notEqual(hashA, hashB, "Same messages with a different system prompt must hash differently");
});
test("Responses-API-translated bodies with the same input but different `instructions` must NOT collide", () => {
const bodyA = {
input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "Hello" }] }],
instructions: "You are a pirate. Speak like one.",
};
const bodyB = {
input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "Hello" }] }],
instructions: "You are a formal legal assistant.",
};
const hashA = computeRequestHash({ ...bodyA, model: "openai/gpt-5", stream: false });
const hashB = computeRequestHash({ ...bodyB, model: "openai/gpt-5", stream: false });
assert.notEqual(hashA, hashB, "Same input with different instructions must hash differently");
});
test("Gemini-translated bodies with the same contents but different `systemInstruction` must NOT collide", () => {
const bodyA = {
contents: [{ role: "user", parts: [{ text: "Hello" }] }],
systemInstruction: { role: "system", parts: [{ text: "You are a pirate. Speak like one." }] },
temperature: 0,
};
const bodyB = {
contents: [{ role: "user", parts: [{ text: "Hello" }] }],
systemInstruction: { role: "system", parts: [{ text: "You are a formal legal assistant." }] },
temperature: 0,
};
const hashA = computeRequestHash({ ...bodyA, model: "gemini/gemini-2.5-flash", stream: false });
const hashB = computeRequestHash({ ...bodyB, model: "gemini/gemini-2.5-flash", stream: false });
assert.notEqual(hashA, hashB, "Same contents with different systemInstruction must hash differently");
});
test("Genuinely identical requests still hash identically and get deduplicated (perf feature preserved)", async () => {
clearInflight();
const body = {