From f82bd3129e9cf76f3ff985666550cad6f2d813aa Mon Sep 17 00:00:00 2001 From: Arthur Bodera Date: Sun, 28 Jun 2026 01:09:01 +1000 Subject: [PATCH] fix(zenmux): normalize vendor-prefixed GLM system roles (#5158) Integrated into release/v3.8.39. ZenMux vendor-prefixed GLM system-role normalization; 12/12 role-normalizer tests pass on merge result. CI reds base-stale. --- open-sse/services/roleNormalizer.ts | 17 ++++++++++++++++- tests/unit/role-normalizer.test.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/open-sse/services/roleNormalizer.ts b/open-sse/services/roleNormalizer.ts index ebf1bab32a..7f63d2dd2f 100644 --- a/open-sse/services/roleNormalizer.ts +++ b/open-sse/services/roleNormalizer.ts @@ -57,6 +57,15 @@ const MODELS_WITHOUT_SYSTEM_ROLE = [ "ernie-", // Baidu ERNIE models ]; +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 + // system summary followed by an assistant/tool bundle, while OpenRouter + // tolerates the same shape. Treat these vendor-prefixed GLM ids like native + // GLM so normalizeSystemRole moves system/developer content into a user turn. + zenmux: [/(?:^|\/)glm(?:-|$)/i], +}; + interface MessageContentPart { type?: string; text?: string; @@ -88,9 +97,15 @@ function extractTextFromContent(content: unknown): string { * Check if a provider+model combo supports the system role. */ function supportsSystemRole(provider: string, model: string): boolean { - if (PROVIDERS_WITHOUT_SYSTEM_ROLE.has(provider)) return false; + const providerLower = (provider || "").trim().toLowerCase(); + if (PROVIDERS_WITHOUT_SYSTEM_ROLE.has(providerLower)) return false; const modelLower = (model || "").toLowerCase(); + + for (const pattern of PROVIDER_SCOPED_MODELS_WITHOUT_SYSTEM_ROLE[providerLower] ?? []) { + if (pattern.test(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 b45780ca3f..038ab20d45 100644 --- a/tests/unit/role-normalizer.test.ts +++ b/tests/unit/role-normalizer.test.ts @@ -99,6 +99,35 @@ test("normalizeSystemRole inserts a user message when no user exists and drops e ]); }); +test("normalizeSystemRole treats ZenMux z-ai/glm models as GLM even with vendor prefix", () => { + const messages = [ + { role: "system", content: "[Context compressed: earlier messages removed]" }, + { + role: "assistant", + content: null, + tool_calls: [ + { + id: "call_1", + type: "function", + function: { name: "read", arguments: "{}" }, + }, + ], + }, + { role: "tool", tool_call_id: "call_1", content: "ok" }, + ]; + + const result = normalizeSystemRole(messages, "zenmux", "z-ai/glm-5.2"); + + assert.deepEqual(result, [ + { + role: "user", + content: "[System Instructions]\n[Context compressed: earlier messages removed]", + }, + messages[1], + messages[2], + ]); +}); + test("normalizeRoles composes model, developer and system normalization in order", () => { const messages = [ { role: "model", content: "first answer" },