diff --git a/CHANGELOG.md b/CHANGELOG.md index 863e083d6b..ddb3135b06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/open-sse/services/roleNormalizer.ts b/open-sse/services/roleNormalizer.ts index 7f63d2dd2f..b7190a67a6 100644 --- a/open-sse/services/roleNormalizer.ts +++ b/open-sse/services/roleNormalizer.ts @@ -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-." 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 = { // 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; } diff --git a/tests/unit/role-normalizer.test.ts b/tests/unit/role-normalizer.test.ts index 038ab20d45..5d5eaceabf 100644 --- a/tests/unit/role-normalizer.test.ts +++ b/tests/unit/role-normalizer.test.ts @@ -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" } }] },