mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-21 22:32:22 +03:00
* fix(compression): place output-style instruction in top-level system, not messages[0] Anthropic-shaped bodies reject a synthetic system-role entry unshifted at messages[0] (the claude passthrough forwards messages unchanged, and the upstream API requires system content in the top-level system parameter). Output styles and the caveman output mode now land their instruction in the top-level system field when it exists (string or content-block array) and only fall back to a trailing system message for OpenAI-shaped bodies, which the claude system-role extraction hoists. Custom endpoint system prompts skip the unshift whenever a top-level system field is present. All compression options stay enabled; placement-only fix. Fixes #12584 * test(compression): cover system-instruction placement branches 4 and 6 Adds direct unit cases for the placement ladder branches that had no coverage, asserting where the instruction lands: - branch 4: merge into a system message at index >= 1, leaving messages[0] untouched (plus the skip-over-a-block-content system message variant). - branch 6: trailing append when the body has neither a `system` field nor a string-content system message. - block-array `system`: appends one block and is idempotent on a second pass (first coverage of the array path of the marker check). Also normalizes a malformed non-string/non-array top-level `system` (e.g. `null`) to "" before the format branch in injectSystemPrompt and injectCustomSystemPrompt. Previously such a body entered the `system` branch, matched neither format, and silently dropped the prompt without falling back to the messages path. Both new guards fail without this change. * chore(compression): add changelog fragment for top-level system placement
173 lines
7.3 KiB
TypeScript
173 lines
7.3 KiB
TypeScript
/**
|
|
* TDD test for per-endpoint custom system prompt injection (port of upstream #2063).
|
|
*
|
|
* Validates that injectCustomSystemPrompt():
|
|
* - Appends the custom prompt to an existing system message (string content)
|
|
* - Appends the custom prompt to an existing system message (array content)
|
|
* - Creates a new system message when none exists
|
|
* - Injects into Claude-style `system` field (string)
|
|
* - Injects into Claude-style `system` field (array)
|
|
* - Does NOT inject when prompt is empty string
|
|
* - Does NOT inject when body has _skipSystemPrompt flag
|
|
* - Leaves body unchanged when prompt is falsy
|
|
*
|
|
* Also validates settings defaults include customSystemPromptEnabled / customSystemPrompt.
|
|
*/
|
|
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const { injectCustomSystemPrompt } = await import("../../open-sse/services/systemPrompt.ts");
|
|
|
|
// ─── injectCustomSystemPrompt ────────────────────────────────────────────────
|
|
|
|
test("injectCustomSystemPrompt: appends to existing string system message", () => {
|
|
const body = {
|
|
messages: [
|
|
{ role: "system", content: "You are a helpful assistant." },
|
|
{ role: "user", content: "Hello" },
|
|
],
|
|
};
|
|
const result = injectCustomSystemPrompt(body, "Always respond formally.");
|
|
const sysMsg = result.messages.find((m) => m.role === "system");
|
|
assert.ok(sysMsg, "system message must exist");
|
|
assert.ok(
|
|
typeof sysMsg.content === "string" && sysMsg.content.includes("You are a helpful assistant."),
|
|
"original content preserved"
|
|
);
|
|
assert.ok(
|
|
typeof sysMsg.content === "string" && sysMsg.content.includes("Always respond formally."),
|
|
"custom prompt appended"
|
|
);
|
|
});
|
|
|
|
test("injectCustomSystemPrompt: appends to existing array-content system message", () => {
|
|
const body = {
|
|
messages: [
|
|
{ role: "system", content: [{ type: "text", text: "Base instructions." }] },
|
|
{ role: "user", content: "Hi" },
|
|
],
|
|
};
|
|
const result = injectCustomSystemPrompt(body, "Speak like a pirate.");
|
|
const sysMsg = result.messages.find((m) => m.role === "system");
|
|
assert.ok(sysMsg, "system message must exist");
|
|
assert.ok(Array.isArray(sysMsg.content), "content must remain array");
|
|
const texts = (sysMsg.content as Array<{ type: string; text: string }>).map((c) => c.text);
|
|
assert.ok(texts.includes("Base instructions."), "original block preserved");
|
|
assert.ok(texts.includes("Speak like a pirate."), "custom prompt block appended");
|
|
});
|
|
|
|
test("injectCustomSystemPrompt: creates system message when none exists", () => {
|
|
const body = {
|
|
messages: [{ role: "user", content: "What is 2+2?" }],
|
|
};
|
|
const result = injectCustomSystemPrompt(body, "Think step by step.");
|
|
assert.equal(result.messages[0].role, "system", "system message prepended");
|
|
assert.equal(result.messages[0].content, "Think step by step.", "prompt is the content");
|
|
assert.equal(result.messages[1].role, "user", "user message preserved after system");
|
|
});
|
|
|
|
test("injectCustomSystemPrompt: injects into Claude-style string system field", () => {
|
|
const body = {
|
|
system: "You are Claude.",
|
|
messages: [{ role: "user", content: "Hello" }],
|
|
};
|
|
const result = injectCustomSystemPrompt(body, "Be concise.");
|
|
assert.ok(typeof result.system === "string", "system field still a string");
|
|
assert.ok(result.system.includes("You are Claude."), "original system preserved");
|
|
assert.ok(result.system.includes("Be concise."), "custom prompt appended to system");
|
|
});
|
|
|
|
test("injectCustomSystemPrompt: injects into Claude-style array system field", () => {
|
|
const body = {
|
|
system: [{ type: "text", text: "You are Claude." }],
|
|
messages: [{ role: "user", content: "Hello" }],
|
|
};
|
|
const result = injectCustomSystemPrompt(body, "Be concise.");
|
|
assert.ok(Array.isArray(result.system), "system field still an array");
|
|
const texts = (result.system as Array<{ type: string; text: string }>).map((c) => c.text);
|
|
assert.ok(texts.includes("You are Claude."), "original system block preserved");
|
|
assert.ok(texts.includes("Be concise."), "custom prompt block appended");
|
|
});
|
|
|
|
test("injectCustomSystemPrompt: no-op when prompt is empty string", () => {
|
|
const body = {
|
|
messages: [{ role: "user", content: "Hello" }],
|
|
};
|
|
const result = injectCustomSystemPrompt(body, "");
|
|
assert.deepEqual(result, body, "body unchanged when prompt is empty");
|
|
});
|
|
|
|
test("injectCustomSystemPrompt: no-op when body has _skipSystemPrompt flag", () => {
|
|
const body = {
|
|
_skipSystemPrompt: true,
|
|
messages: [{ role: "user", content: "Hello" }],
|
|
};
|
|
const result = injectCustomSystemPrompt(body as Record<string, unknown>, "Be formal.");
|
|
assert.deepEqual(result, body, "body unchanged when _skipSystemPrompt is set");
|
|
});
|
|
|
|
test("injectCustomSystemPrompt: does not mutate input body (immutable)", () => {
|
|
const body = {
|
|
messages: [
|
|
{ role: "system", content: "Original." },
|
|
{ role: "user", content: "Hi" },
|
|
],
|
|
};
|
|
const original = JSON.stringify(body);
|
|
injectCustomSystemPrompt(body, "New instruction.");
|
|
assert.equal(JSON.stringify(body), original, "input body not mutated");
|
|
});
|
|
|
|
// ─── Settings defaults ───────────────────────────────────────────────────────
|
|
// Verify that getSettings() returns the expected defaults for the new keys.
|
|
// Uses an in-memory DB (DATA_DIR set to a temp path so no production DB is touched).
|
|
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { mkdirSync } from "node:fs";
|
|
|
|
const tmpDir = join(tmpdir(), `omniroute-test-${Date.now()}`);
|
|
mkdirSync(tmpDir, { recursive: true });
|
|
process.env["DATA_DIR"] = tmpDir;
|
|
|
|
test("settings defaults include customSystemPromptEnabled=false and customSystemPrompt=''", async (t) => {
|
|
// Dynamic import after setting DATA_DIR to avoid polluting other tests
|
|
const { resetDbInstance } = await import("../../src/lib/db/core.ts");
|
|
const { getSettings } = await import("../../src/lib/db/settings.ts");
|
|
|
|
const settings = await getSettings();
|
|
assert.equal(
|
|
settings.customSystemPromptEnabled,
|
|
false,
|
|
"customSystemPromptEnabled default is false"
|
|
);
|
|
assert.equal(settings.customSystemPrompt, "", "customSystemPrompt default is empty string");
|
|
|
|
t.after(() => {
|
|
resetDbInstance();
|
|
});
|
|
});
|
|
|
|
test("injectCustomSystemPrompt: claude-shaped body keeps prompt out of messages[0] (#12584)", () => {
|
|
const body = {
|
|
system: "You are Claude Code.",
|
|
messages: [{ role: "user", content: "Hello" }],
|
|
};
|
|
const result = injectCustomSystemPrompt(body, "Always respond formally.");
|
|
assert.equal(result.messages.length, 1);
|
|
assert.equal(result.messages[0].role, "user");
|
|
assert.ok(String(result.system).includes("Always respond formally."));
|
|
});
|
|
|
|
test("injectCustomSystemPrompt: malformed null system still receives the prompt (#12584)", () => {
|
|
const body = {
|
|
system: null,
|
|
messages: [{ role: "user", content: "Hello" }],
|
|
};
|
|
const result = injectCustomSystemPrompt(body, "Always respond formally.");
|
|
assert.equal(result.system, "Always respond formally.", "prompt is not silently dropped");
|
|
assert.equal(result.messages.length, 1, "prompt does not leak into messages");
|
|
assert.equal(result.messages[0].role, "user");
|
|
});
|