mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 05:02:15 +03:00
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:
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user