fix: preserve system role for GLM 5.1/5.2 (#5610) (#5663)

This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-30 12:28:05 -03:00
committed by GitHub
parent 806d1f650b
commit 9be5528548
3 changed files with 61 additions and 3 deletions

View File

@@ -10,6 +10,8 @@
### 🔧 Bug Fixes
- **providers (GLM):** GLM **5.1 / 5.2** now keep the `system` role instead of having the system prompt folded into the first user turn. `roleNormalizer.ts` matched every `glm*` id with a blanket `startsWith("glm")` / `startsWith("glm-")` prefix, so the next-generation models — which z.ai documents as supporting the `system` role (GLM > 5.0) — were normalized as if they rejected it, degrading instruction-following. The matcher is now version-aware: it strips the system role only for bare `glm`, the 4.x family, and the 5.0 generation, and preserves it for `glm-5.1`/`glm-5.2` (and the Fireworks `glm-5p1` point alias). The ZenMux vendor-prefixed `z-ai/glm-*` compressed-history rule and the ERNIE rule are unchanged. Regression guards in `tests/unit/role-normalizer.test.ts`. ([#5610](https://github.com/diegosouzapw/OmniRoute/issues/5610))
- **Security hardening follow-ups (v3.8.15):** the `auth_token` cookie now sets an explicit 30-day `maxAge` so sessions persist as intended (Seg3); the management bootstrap warns at boot when `INITIAL_PASSWORD` is left at the insecure `CHANGEME` default (Seg2); VS Code path-token endpoints (`/api/v1/vscode/raw/[token]`) emit a once-per-process security warning since the API key travels in the URL and can leak via logs/proxies (Seg4); the system version route resolves the real global install path via `npm root -g` instead of a hardcoded `/app` (Bug3); and auto-update mode detection segment-matches `node_modules` instead of substring-matching, eliminating false "global install" positives (Bug1).
### 📝 Maintenance

View File

@@ -49,14 +49,30 @@ function defaultPreserveDeveloperForProvider(provider: string): boolean {
/**
* Models that are known to reject the `system` role regardless of provider.
* Uses prefix matching (e.g., "glm-" matches "glm-4.7", "glm-4.5", etc.)
* Uses prefix matching (e.g., "ernie-" matches "ernie-4.0").
*/
const MODELS_WITHOUT_SYSTEM_ROLE = [
"glm-", // ZhipuAI GLM models (prefix: glm-5.1, glm-4.7, etc.)
"glm", // Exact match for model id "glm" (e.g., Pollinations)
"ernie-", // Baidu ERNIE models
];
/**
* ZhipuAI GLM rejects the `system` role EXCEPT generation > 5.0: per z.ai docs,
* GLM 5.1 / 5.2 (and newer) accept it, so their system prompt must NOT be folded
* into the first user turn (#5610). Everything else GLM — bare "glm" (Pollinations),
* the 4.x family, and the 5.0 generation — still needs the fold. The version is read
* from "glm-<major>.<minor>" or the Fireworks "glm-5p1" point alias.
*/
function isGlmWithoutSystemRole(modelLower: string): boolean {
if (!modelLower.startsWith("glm")) return false;
const match = modelLower.match(/glm-?(\d+)(?:[.p](\d+))?/);
if (match) {
const major = Number(match[1]);
const minor = match[2] ? Number(match[2]) : 0;
if (major > 5 || (major === 5 && minor >= 1)) return false;
}
return true;
}
const PROVIDER_SCOPED_MODELS_WITHOUT_SYSTEM_ROLE: Record<string, RegExp[]> = {
// ZenMux exposes Z.AI GLM through OpenAI-compatible model ids such as
// "z-ai/glm-5.2". Z.AI rejects compressed histories that start with a
@@ -106,6 +122,8 @@ function supportsSystemRole(provider: string, model: string): boolean {
if (pattern.test(modelLower)) return false;
}
if (isGlmWithoutSystemRole(modelLower)) return false;
for (const prefix of MODELS_WITHOUT_SYSTEM_ROLE) {
if (modelLower.startsWith(prefix)) return false;
}

View File

@@ -84,6 +84,44 @@ test("normalizeSystemRole merges system and developer content into the first use
]);
});
test("normalizeSystemRole preserves the system role for GLM > 5.0 (glm-5.1/5.2 support it, #5610)", () => {
const messages = [
{ role: "system", content: "be helpful" },
{ role: "user", content: "hello" },
];
for (const model of [
"glm-5.1",
"glm-5.2",
"glm-5.1-precision",
"glm-5.2-high",
"glm-5.2-max",
"glm-5p1",
]) {
assert.deepEqual(
normalizeSystemRole(messages, "glm", model),
messages,
`expected system role preserved for ${model}`
);
}
});
test("normalizeSystemRole still strips the system role for pre-5.1 GLM and bare glm (#5610 guard)", () => {
const messages = [
{ role: "system", content: "policy" },
{ role: "user", content: "ok" },
];
const merged = [
{ role: "user", content: "[System Instructions]\npolicy\n\n[User Message]\nok" },
];
for (const model of ["glm", "glm-4.7", "glm-5", "glm-5-turbo", "glm-5.0", "glm-5.0-turbo"]) {
assert.deepEqual(
normalizeSystemRole(messages, "openai", model),
merged,
`expected system role stripped for ${model}`
);
}
});
test("normalizeSystemRole inserts a user message when no user exists and drops empty system payloads", () => {
const messages = [
{ role: "system", content: [{ type: "image_url", image_url: { url: "ignored" } }] },