mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 23:32:12 +03:00
fix(translator): wrap Kiro system prompt in <system-reminder> (port from 9router#2306) (#6053)
Kiro/CodeWhisperer has no system role, so system messages were normalized to a user turn with no wrapper — the full Claude Code system prompt then appeared as raw user text, polluting the model context. Wrap system-origin content in <system-reminder> tags before merging it into the Kiro user message. Real user turns are unaffected. Existing history-merge tests aligned to the wrapped value. Reported-by: VitzS7 (https://github.com/decolua/9router/issues/2306)
This commit is contained in:
committed by
GitHub
parent
65602b5477
commit
90aca8b85e
@@ -24,6 +24,8 @@
|
||||
|
||||
- **dashboard ("Update now" → Internal Server Error):** clicking **Update now** on the dashboard home could crash the page with a blank "Internal Server Error" screen (`Minified React error #31`). The handler POSTs the loopback-only `/api/system/version` auto-update endpoint and, on a non-OK JSON response (e.g. a `403` when the dashboard is reached through a reverse proxy / non-loopback origin), passed the raw error envelope object `{ error: { code, message, correlation_id } }` straight to `notify.error()`, which rendered the object as a React child and threw #31. The update-error path now funnels the body through `extractApiErrorMessage()` (the same safe extractor added in #5340), so a readable string always reaches the toast. Regression guard: `tests/unit/ui/home-update-error-render-5991.test.ts`. ([#5991](https://github.com/diegosouzapw/OmniRoute/issues/5991))
|
||||
|
||||
- **kiro (system prompt leaked as raw user text):** when Claude Code routed through the Kiro/CodeWhisperer backend, the `system` message was normalized to a `user` turn with no wrapper, so the entire system prompt (environment info, tool definitions, memory instructions, etc.) appeared as if the user had typed it — polluting the model context. System-origin content is now wrapped in `<system-reminder>` tags before being merged into the Kiro user message, so the model can distinguish it from real user input. Real user turns are untouched. Regression guard: `tests/unit/kiro-system-reminder-2306.test.ts`. (thanks @VitzS7)
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **test (deflake `setup-claude`):** `tests/unit/cli/setup-claude.test.ts` failed ~50% of runs with `Unable to deserialize cloned data due to invalid or unsupported version` at file teardown (all subtests passed), randomly reddening `Unit Tests fast-path (2/2)` / `Fast Quality Gates` across the PR→release queue. Root cause: `node --test` streams each file's report to the parent as V8-serialized frames on fd 1 (stdout), and the CLI helper under test (`syncClaudeProfilesFromModels`) prints progress via `console.log` — that stdout output interleaved with the serialized frames and corrupted the stream. The test now silences the stdout-writing `console` methods for the file's duration (no assertion inspects stdout), making it deterministic (15/15 green locally). ([#5959](https://github.com/diegosouzapw/OmniRoute/issues/5959))
|
||||
|
||||
@@ -32,6 +32,15 @@ export function hasUnsupportedKiroContextSuffix(model: unknown): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap system-prompt content in <system-reminder> tags before it is merged into
|
||||
* a Kiro user message. Kiro/CodeWhisperer has no `system` role, so without this
|
||||
* the system prompt would appear as raw user text (issue #2306).
|
||||
*/
|
||||
function wrapSystemReminder(text: string): string {
|
||||
return `<system-reminder>\n${text}\n</system-reminder>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert OpenAI messages to Kiro format
|
||||
* Rules: system/tool/user -> user role, merge consecutive same roles
|
||||
@@ -238,7 +247,11 @@ function convertMessages(messages, tools, model) {
|
||||
content: [{ text: toolContent }],
|
||||
});
|
||||
} else if (content) {
|
||||
pendingUserContent.push(content);
|
||||
// #2306: Kiro/CodeWhisperer has no `system` role, so system messages are
|
||||
// normalized to `user`. Wrap their content in <system-reminder> tags so
|
||||
// the model can tell the system prompt apart from real user input instead
|
||||
// of treating the full Claude Code prompt as something the user typed.
|
||||
pendingUserContent.push(msg.role === "system" ? wrapSystemReminder(content) : content);
|
||||
}
|
||||
} else if (role === "assistant") {
|
||||
// Extract text content and tool uses
|
||||
|
||||
35
tests/unit/kiro-system-reminder-2306.test.ts
Normal file
35
tests/unit/kiro-system-reminder-2306.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* #2306 — When Claude Code routes through the Kiro/CodeWhisperer backend, the
|
||||
* `system` message was normalized to `role: user` WITHOUT any wrapper, so the
|
||||
* full system prompt (env info, tool defs, memory instructions, etc.) appeared
|
||||
* as raw user text — indistinguishable from real user input, polluting context.
|
||||
*
|
||||
* Fix: wrap system-origin content in `<system-reminder>...</system-reminder>`
|
||||
* before it is merged into the Kiro user message. Real user turns stay raw.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { buildKiroPayload } from "../../open-sse/translator/request/openai-to-kiro.ts";
|
||||
|
||||
test("#2306 system prompt is wrapped in <system-reminder> for Kiro, not raw user text", () => {
|
||||
const body = {
|
||||
messages: [
|
||||
{ role: "system", content: "You are Claude Code. ENV: cwd=/tmp. Secret: do not reveal." },
|
||||
{ role: "user", content: "hello there" },
|
||||
],
|
||||
};
|
||||
|
||||
const payload = JSON.stringify(buildKiroPayload("claude-sonnet-4-5", body, false, {}));
|
||||
|
||||
assert.ok(payload.includes("<system-reminder>"), "system content must be wrapped");
|
||||
assert.ok(payload.includes("You are Claude Code"), "system text must still be present");
|
||||
// The real user turn must NOT be wrapped.
|
||||
assert.ok(payload.includes("hello there"), "user text preserved");
|
||||
});
|
||||
|
||||
test("#2306 a plain user-only request is never wrapped in <system-reminder>", () => {
|
||||
const body = { messages: [{ role: "user", content: "just a normal question" }] };
|
||||
const payload = JSON.stringify(buildKiroPayload("claude-sonnet-4-5", body, false, {}));
|
||||
assert.ok(!payload.includes("<system-reminder>"), "no system → no wrapper");
|
||||
});
|
||||
@@ -81,7 +81,9 @@ test("OpenAI -> Kiro preserves prior history, tool uses and accumulated tool res
|
||||
assert.equal(result.conversationState.history.length, 2);
|
||||
assert.deepEqual(result.conversationState.history[0], {
|
||||
userInputMessage: {
|
||||
content: "Rules\n\nHello",
|
||||
// #2306: the system prompt ("Rules") is wrapped in <system-reminder> before
|
||||
// being merged into the Kiro user turn, instead of leaking as raw user text.
|
||||
content: "<system-reminder>\nRules\n</system-reminder>\n\nHello",
|
||||
modelId: "claude-sonnet-4",
|
||||
origin: "AI_EDITOR",
|
||||
},
|
||||
@@ -233,11 +235,11 @@ test("OpenAI -> Kiro derives a stable conversationId for the same first history
|
||||
|
||||
assert.equal(
|
||||
(first.conversationState as any).history[0].userInputMessage.content,
|
||||
"Rules\n\nHello"
|
||||
"<system-reminder>\nRules\n</system-reminder>\n\nHello"
|
||||
);
|
||||
assert.equal(
|
||||
(second as any).conversationState.history[0].userInputMessage.content,
|
||||
"Rules\n\nHello"
|
||||
"<system-reminder>\nRules\n</system-reminder>\n\nHello"
|
||||
);
|
||||
assert.equal(first.conversationState.conversationId, second.conversationState.conversationId);
|
||||
});
|
||||
@@ -291,7 +293,10 @@ test("OpenAI -> Kiro merges adjacent user history turns after role normalization
|
||||
|
||||
const firstUser = history[0].userInputMessage;
|
||||
assert.ok(firstUser, "first history turn should be a user turn");
|
||||
assert.equal(firstUser.content, "System rules\n\nFirst question");
|
||||
assert.equal(
|
||||
firstUser.content,
|
||||
"<system-reminder>\nSystem rules\n</system-reminder>\n\nFirst question"
|
||||
);
|
||||
assert.equal(history[1].assistantResponseMessage?.content, "Answer 1");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user