mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Merge pull request #1672 from diegosouzapw/release/v3.7.2
Release v3.7.2
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.7.2] — 2026-04-27
|
||||
## [3.7.2] — 2026-04-28
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
- **fix(stream):** fail zombie SSE streams before accepting response — returns 504 instead of hanging indefinitely, enables combo fallback (#1693)
|
||||
- **fix(combo):** complete context truncation hotfix — cache getCombos() with 10s TTL, pass allCombosData to resolveComboTargets() for nested combo resolution, consolidate duplicated context overflow regex patterns (#1685)
|
||||
- **fix(codex):** raise default quota threshold from 90% to 99% to avoid premature account blocking when usable quota remains (#1697)
|
||||
- **fix(memory):** use `user` role for GLM/ZAI/Qianfan providers — providers with strict role constraints (no `system` role) now correctly receive memory context as a `user` message instead of a `system` message, preventing 422 validation errors (#1701)
|
||||
- **fix(oauth):** target specific connection by ID on re-auth token exchange — prevents duplicate account creation when re-authenticating an existing OAuth connection (#1702 — thanks @namhhitvn)
|
||||
- **feat(email-privacy):** integrate email visibility toggle in RequestLoggerV2 — log detail modal now respects global email privacy state, hiding email addresses by default (#1700 — thanks @namhhitvn)
|
||||
- **fix(combo):** trigger fallback on Anthropic `Invalid signature in thinking block` errors instead of returning 400 directly (#1696)
|
||||
- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681)
|
||||
- **fix(search):** support optional bearer auth for SearXNG (#1683)
|
||||
|
||||
@@ -135,8 +135,6 @@ export function googApiClientHeader(): string {
|
||||
|
||||
export {
|
||||
ANTIGRAVITY_VERSION,
|
||||
GEMINI_CLI_VERSION,
|
||||
GEMINI_SDK_VERSION,
|
||||
LOAD_CODE_ASSIST_USER_AGENT as ANTIGRAVITY_LOAD_CODE_ASSIST_USER_AGENT,
|
||||
LOAD_CODE_ASSIST_API_CLIENT as ANTIGRAVITY_LOAD_CODE_ASSIST_API_CLIENT,
|
||||
};
|
||||
|
||||
@@ -77,18 +77,18 @@ const COMBO_BAD_REQUEST_FALLBACK_PATTERNS = [
|
||||
/unsupported model/i,
|
||||
/model.*has no provider/i,
|
||||
// Function calling format error — model doesn't support this capability
|
||||
/function\.?arguments.*(must be|should be|必须).*(json|JSON)/i,
|
||||
/function\.?arguments.*(must be|should be|\u5fc5\u987b).*(json|JSON)/i,
|
||||
/tool.*arguments.*invalid/i,
|
||||
/function.*parameter.*(invalid|format)/i,
|
||||
// Input length range error — model-specific context limit
|
||||
/range of input length/i,
|
||||
/input length should be/i,
|
||||
// Transient 400 errors from upstream — should fallback to next combo target
|
||||
/服务遇到了一点小状况/i, // ModelScope/Qwen transient error
|
||||
/抱歉.*?敏感内容.*?请检查/i, // ModelScope/Qwen content moderation with context
|
||||
/内容.*?敏感.*?(?:无法|过滤)/i, // Content sensitivity block
|
||||
/无法响应.*?请求/i, // "unable to respond to request"
|
||||
/稍后重试/i, // "retry later" in Chinese
|
||||
/\u670d\u52a1\u9047\u5230\u4e86\u4e00\u70b9\u5c0f\u72b6\u51b5/i, // ModelScope/Qwen transient error
|
||||
/\u62b1\u6b49.*?\u654f\u611f\u5185\u5bb9.*?\u8bf7\u68c0\u67e5/i, // ModelScope/Qwen content moderation with context
|
||||
/\u5185\u5bb9.*?\u654f\u611f.*?(?:\u65e0\u6cd5|\u8fc7\u6ee4)/i, // Content sensitivity block
|
||||
/\u65e0\u6cd5\u54cd\u5e94.*?\u8bf7\u6c42/i, // "unable to respond to request"
|
||||
/\u7a0d\u540e\u91cd\u8bd5/i, // "retry later" in Chinese
|
||||
/temporary.*error/i,
|
||||
/transient.*error/i,
|
||||
/service.*unavailable/i,
|
||||
@@ -96,7 +96,7 @@ const COMBO_BAD_REQUEST_FALLBACK_PATTERNS = [
|
||||
// Rate limit errors — some providers return 400 instead of 429
|
||||
/\brate.?-?limit.?(?:exceeded|reached|hit)/i,
|
||||
/too many requests/i,
|
||||
/请求过于频繁/i, // Chinese rate limit message
|
||||
/\u8bf7\u6c42\u8fc7\u4e8e\u9891\u7e41/i, // Chinese rate limit message
|
||||
// Tool call function name errors — model-specific, try next combo target
|
||||
/\bfunction'?s? name (?:can't|can not|is|has) (?:blank|empty|missing)/i,
|
||||
/function.*name.*(?:blank|empty|missing)/i,
|
||||
|
||||
@@ -30,7 +30,8 @@ const PROVIDERS_WITHOUT_SYSTEM_ROLE = new Set([
|
||||
* Uses prefix matching (e.g., "glm-" matches "glm-4.7", "glm-4.5", etc.)
|
||||
*/
|
||||
const MODELS_WITHOUT_SYSTEM_ROLE = [
|
||||
"glm-", // ZhipuAI GLM models
|
||||
"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
|
||||
];
|
||||
|
||||
|
||||
@@ -969,7 +969,8 @@ export default function ProviderDetailPage() {
|
||||
const [connections, setConnections] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [providerNode, setProviderNode] = useState(null);
|
||||
const [showOAuthModal, setShowOAuthModal] = useState(false);
|
||||
const [showOAuthModal, _setShowOAuthModal] = useState(false);
|
||||
const [reauthConnection, setReauthConnection] = useState<ConnectionRowConnection | null>(null);
|
||||
const [showAddApiKeyModal, setShowAddApiKeyModal] = useState(false);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [showEditNodeModal, setShowEditNodeModal] = useState(false);
|
||||
@@ -1021,6 +1022,11 @@ export default function ProviderDetailPage() {
|
||||
const isCompatible = isOpenAICompatible || isAnthropicCompatible || isCcCompatible;
|
||||
const isAnthropicProtocolCompatible = isAnthropicCompatible || isCcCompatible;
|
||||
|
||||
const setShowOAuthModal = (show: boolean, connectionRow?: ConnectionRowConnection) => {
|
||||
_setShowOAuthModal(show);
|
||||
setReauthConnection(show && connectionRow ? connectionRow : null);
|
||||
};
|
||||
|
||||
const providerInfo = resolveDashboardProviderInfo(providerId, {
|
||||
providerNode,
|
||||
compatibleLabels: {
|
||||
@@ -2928,7 +2934,9 @@ export default function ProviderDetailPage() {
|
||||
}}
|
||||
onDelete={() => handleDelete(conn.id)}
|
||||
onReauth={
|
||||
conn.authType === "oauth" ? () => setShowOAuthModal(true) : undefined
|
||||
conn.authType === "oauth"
|
||||
? () => setShowOAuthModal(true, conn)
|
||||
: undefined
|
||||
}
|
||||
onRefreshToken={
|
||||
conn.authType === "oauth" ? () => handleRefreshToken(conn.id) : undefined
|
||||
@@ -3048,7 +3056,7 @@ export default function ProviderDetailPage() {
|
||||
onDelete={() => handleDelete(conn.id)}
|
||||
onReauth={
|
||||
conn.authType === "oauth"
|
||||
? () => setShowOAuthModal(true)
|
||||
? () => setShowOAuthModal(true, conn)
|
||||
: undefined
|
||||
}
|
||||
onRefreshToken={
|
||||
@@ -3183,6 +3191,7 @@ export default function ProviderDetailPage() {
|
||||
(providerId === "kiro" || providerId === "amazon-q" ? (
|
||||
<KiroOAuthWrapper
|
||||
isOpen={showOAuthModal}
|
||||
reauthConnection={reauthConnection}
|
||||
providerInfo={{ ...providerInfo, id: providerId }}
|
||||
onSuccess={handleOAuthSuccess}
|
||||
onClose={() => {
|
||||
@@ -3192,6 +3201,7 @@ export default function ProviderDetailPage() {
|
||||
) : providerId === "cursor" ? (
|
||||
<CursorAuthModal
|
||||
isOpen={showOAuthModal}
|
||||
reauthConnection={reauthConnection}
|
||||
onSuccess={handleOAuthSuccess}
|
||||
onClose={() => {
|
||||
setShowOAuthModal(false);
|
||||
@@ -3200,6 +3210,7 @@ export default function ProviderDetailPage() {
|
||||
) : (
|
||||
<OAuthModal
|
||||
isOpen={showOAuthModal}
|
||||
reauthConnection={reauthConnection}
|
||||
provider={providerId}
|
||||
providerInfo={providerInfo}
|
||||
onSuccess={handleOAuthSuccess}
|
||||
|
||||
@@ -234,7 +234,7 @@ export async function POST(
|
||||
}
|
||||
|
||||
if (action === "exchange") {
|
||||
const { code, redirectUri, codeVerifier, state } = body;
|
||||
const { code, redirectUri, connectionId, codeVerifier, state } = body;
|
||||
const normalizedState = typeof state === "string" && state.length > 0 ? state : undefined;
|
||||
const providerData = getProvider(provider);
|
||||
|
||||
@@ -278,6 +278,7 @@ export async function POST(
|
||||
if (tokenData.email) {
|
||||
const existing = await getProviderConnections({ provider });
|
||||
const match = existing.find((c: any) => {
|
||||
if (c.id && safeEqual(connectionId, c.id)) return true;
|
||||
// safeEqual: constant-time comparison to prevent timing attacks (CWE-208, finding #258-6/7)
|
||||
if (!safeEqual(c.email, tokenData.email) || c.authType !== "oauth") return false;
|
||||
// For Codex, also check workspaceId to avoid overwriting different workspace connections
|
||||
@@ -322,7 +323,7 @@ export async function POST(
|
||||
}
|
||||
|
||||
if (action === "poll") {
|
||||
const { deviceCode, codeVerifier, extraData } = body;
|
||||
const { deviceCode, connectionId, codeVerifier, extraData } = body;
|
||||
|
||||
// Resolve proxy for this provider (provider-level → global → direct)
|
||||
const proxy = await resolveProxyForProvider(provider);
|
||||
@@ -364,6 +365,7 @@ export async function POST(
|
||||
if (result.tokens.email) {
|
||||
const existing = await getProviderConnections({ provider });
|
||||
const match = existing.find((c: any) => {
|
||||
if (c.id && safeEqual(connectionId, c.id)) return true;
|
||||
// safeEqual: constant-time comparison to prevent timing attacks (CWE-208, finding #258-8/9)
|
||||
if (!safeEqual(c.email, result.tokens.email) || c.authType !== "oauth") return false;
|
||||
// For Codex, also check workspaceId to avoid overwriting different workspace connections
|
||||
@@ -418,6 +420,8 @@ export async function POST(
|
||||
}
|
||||
|
||||
if (action === "poll-callback") {
|
||||
const { connectionId } = body;
|
||||
|
||||
// Poll for Codex callback server result
|
||||
if (provider !== "codex") {
|
||||
return NextResponse.json(
|
||||
@@ -489,6 +493,7 @@ export async function POST(
|
||||
if (tokenData.email) {
|
||||
const existing = await getProviderConnections({ provider });
|
||||
const match = existing.find((c: any) => {
|
||||
if (c.id && safeEqual(connectionId, c.id)) return true;
|
||||
// safeEqual: constant-time comparison to prevent timing attacks (CWE-208, finding #258-6/7)
|
||||
if (!safeEqual(c.email, tokenData.email) || c.authType !== "oauth") return false;
|
||||
// For Codex, also check workspaceId to avoid overwriting different workspace connections
|
||||
|
||||
@@ -35,7 +35,16 @@ export interface ChatRequest {
|
||||
* Providers known NOT to support a top-level system-role message.
|
||||
* These receive memories injected as the first user message instead.
|
||||
*/
|
||||
const PROVIDERS_WITHOUT_SYSTEM_MESSAGE = new Set(["o1", "o1-mini", "o1-preview"]);
|
||||
const PROVIDERS_WITHOUT_SYSTEM_MESSAGE = new Set([
|
||||
"o1",
|
||||
"o1-mini",
|
||||
"o1-preview",
|
||||
"glm", // GLM/ZhipuAI rejects system role (#1701)
|
||||
"glmt", // GLM Thinking variant
|
||||
"glm-cn", // GLM China variant
|
||||
"zai", // Z.AI uses same GLM backend
|
||||
"qianfan", // Baidu ERNIE rejects system role
|
||||
]);
|
||||
|
||||
/**
|
||||
* Returns true when the given provider accepts a system-role message.
|
||||
|
||||
@@ -11,7 +11,7 @@ import Input from "./Input";
|
||||
* Cursor Auth Modal
|
||||
* Auto-detect and import token from Cursor IDE's local SQLite database
|
||||
*/
|
||||
export default function CursorAuthModal({ isOpen, onSuccess, onClose }) {
|
||||
export default function CursorAuthModal({ isOpen, onSuccess, onClose, reauthConnection: _ }) {
|
||||
const t = useTranslations("cursorAuthModal");
|
||||
const [accessToken, setAccessToken] = useState("");
|
||||
const [machineId, setMachineId] = useState("");
|
||||
|
||||
@@ -10,7 +10,13 @@ import KiroSocialOAuthModal from "./KiroSocialOAuthModal";
|
||||
* Kiro OAuth Wrapper
|
||||
* Orchestrates between method selection, device code flow, and social login flow
|
||||
*/
|
||||
export default function KiroOAuthWrapper({ isOpen, providerInfo, onSuccess, onClose }) {
|
||||
export default function KiroOAuthWrapper({
|
||||
isOpen,
|
||||
providerInfo,
|
||||
onSuccess,
|
||||
onClose,
|
||||
reauthConnection,
|
||||
}) {
|
||||
const [authMethod, setAuthMethod] = useState(null); // null | "builder-id" | "idc" | "social" | "import"
|
||||
const [socialProvider, setSocialProvider] = useState(null); // "google" | "github"
|
||||
const [idcConfig, setIdcConfig] = useState(null);
|
||||
@@ -78,6 +84,7 @@ export default function KiroOAuthWrapper({ isOpen, providerInfo, onSuccess, onCl
|
||||
provider={oauthProviderId}
|
||||
providerInfo={providerInfo}
|
||||
onSuccess={handleDeviceSuccess}
|
||||
reauthConnection={reauthConnection}
|
||||
onClose={handleBack}
|
||||
idcConfig={idcConfig}
|
||||
/>
|
||||
|
||||
@@ -17,6 +17,7 @@ type OAuthModalProps = {
|
||||
onSuccess?: () => void;
|
||||
onClose: () => void;
|
||||
idcConfig?: unknown;
|
||||
reauthConnection?: null | { id?: string };
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -31,6 +32,7 @@ export default function OAuthModal({
|
||||
onSuccess,
|
||||
onClose,
|
||||
idcConfig,
|
||||
reauthConnection,
|
||||
}: OAuthModalProps) {
|
||||
const t = useTranslations("oauthModal");
|
||||
const [step, setStep] = useState("waiting"); // waiting | input | success | error
|
||||
@@ -93,6 +95,7 @@ export default function OAuthModal({
|
||||
body: JSON.stringify({
|
||||
code,
|
||||
redirectUri: authData.redirectUri,
|
||||
connectionId: reauthConnection?.id,
|
||||
codeVerifier: authData.codeVerifier,
|
||||
...(normalizedState ? { state: normalizedState } : {}),
|
||||
}),
|
||||
@@ -141,7 +144,7 @@ export default function OAuthModal({
|
||||
setStep("error");
|
||||
}
|
||||
},
|
||||
[authData, provider, onSuccess]
|
||||
[authData, provider, onSuccess, reauthConnection]
|
||||
);
|
||||
|
||||
// Poll for device code token
|
||||
@@ -157,7 +160,12 @@ export default function OAuthModal({
|
||||
const res = await fetch(`/api/oauth/${provider}/poll`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ deviceCode, codeVerifier, extraData }),
|
||||
body: JSON.stringify({
|
||||
deviceCode,
|
||||
connectionId: reauthConnection?.id,
|
||||
codeVerifier,
|
||||
extraData,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
@@ -188,7 +196,7 @@ export default function OAuthModal({
|
||||
setStep("error");
|
||||
setPolling(false);
|
||||
},
|
||||
[provider, onSuccess]
|
||||
[provider, onSuccess, reauthConnection]
|
||||
);
|
||||
|
||||
// Start OAuth flow
|
||||
@@ -271,7 +279,7 @@ export default function OAuthModal({
|
||||
const pollRes = await fetch(`/api/oauth/codex/poll-callback`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
body: JSON.stringify({ connectionId: reauthConnection?.id }),
|
||||
});
|
||||
const pollData = await pollRes.json();
|
||||
|
||||
@@ -371,7 +379,7 @@ export default function OAuthModal({
|
||||
setError(err.message);
|
||||
setStep("error");
|
||||
}
|
||||
}, [provider, isLocalhost, isTrueLocalhost, startPolling, onSuccess]);
|
||||
}, [provider, isLocalhost, isTrueLocalhost, startPolling, onSuccess, reauthConnection]);
|
||||
|
||||
// Reset guard when modal closes
|
||||
useEffect(() => {
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
maskAccount,
|
||||
formatApiKeyLabel,
|
||||
} from "@/shared/utils/formatting";
|
||||
import useEmailPrivacyStore from "@/store/emailPrivacyStore";
|
||||
|
||||
// Quick filter categories - status-based only (providers are dynamic from data)
|
||||
const STATUS_FILTERS = [
|
||||
@@ -114,6 +115,7 @@ function getCacheSourceMeta(cacheSource: unknown) {
|
||||
|
||||
export default function RequestLoggerV2() {
|
||||
const t = useTranslations("requestLogger");
|
||||
const { emailsVisible } = useEmailPrivacyStore();
|
||||
|
||||
// Get translated status filters
|
||||
const statusFilters = useMemo(
|
||||
@@ -795,7 +797,7 @@ export default function RequestLoggerV2() {
|
||||
className="px-3 py-2 text-text-muted truncate max-w-[120px]"
|
||||
title={log.account}
|
||||
>
|
||||
{maskAccount(log.account)}
|
||||
{maskAccount(log.account, emailsVisible)}
|
||||
</td>
|
||||
)}
|
||||
{visibleColumns.apiKey && (
|
||||
|
||||
@@ -65,12 +65,14 @@ export function maskSegment(value, start = 2, end = 2) {
|
||||
/**
|
||||
* Mask an email or account string for display.
|
||||
* @param {string} account - Account identifier (email or username)
|
||||
* @param {boolean} emailsVisible - Whether to show full email (true) or mask it (false)
|
||||
* @returns {string}
|
||||
*/
|
||||
export function maskAccount(account) {
|
||||
export function maskAccount(account, emailsVisible) {
|
||||
if (!account || account === "-") return "-";
|
||||
const atIdx = account.indexOf("@");
|
||||
if (atIdx > 3) {
|
||||
if (emailsVisible) return account;
|
||||
return account.slice(0, 3) + "***" + account.slice(atIdx);
|
||||
}
|
||||
if (account.length > 8) {
|
||||
|
||||
176
tests/unit/memory-glm-injection.test.ts
Normal file
176
tests/unit/memory-glm-injection.test.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* Tests for #1701: GLM providers rejecting system role from memory injection.
|
||||
*
|
||||
* Validates that injectMemory() uses the user role fallback for GLM/ZAI/Qianfan
|
||||
* providers that do not support the system role, while still using system role
|
||||
* for standard providers like OpenAI.
|
||||
*/
|
||||
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { injectMemory, providerSupportsSystemMessage } from "../../src/lib/memory/injection.ts";
|
||||
import type { ChatRequest } from "../../src/lib/memory/injection.ts";
|
||||
import type { Memory } from "../../src/lib/memory/types.ts";
|
||||
import { normalizeSystemRole } from "../../open-sse/services/roleNormalizer.ts";
|
||||
|
||||
// ── Fixtures ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const baseRequest: ChatRequest = {
|
||||
model: "glm-5.1",
|
||||
messages: [{ role: "user", content: "Hello" }],
|
||||
};
|
||||
|
||||
const testMemories: Memory[] = [
|
||||
{
|
||||
id: "mem-1",
|
||||
content: "User prefers dark mode",
|
||||
type: "factual" as any,
|
||||
apiKeyId: "test-key",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
importance: 0.5,
|
||||
},
|
||||
];
|
||||
|
||||
// ── providerSupportsSystemMessage ──────────────────────────────────────────────
|
||||
|
||||
describe("providerSupportsSystemMessage — GLM providers (#1701)", () => {
|
||||
it("should return false for glm", () => {
|
||||
assert.equal(providerSupportsSystemMessage("glm"), false);
|
||||
});
|
||||
|
||||
it("should return false for glmt", () => {
|
||||
assert.equal(providerSupportsSystemMessage("glmt"), false);
|
||||
});
|
||||
|
||||
it("should return false for glm-cn", () => {
|
||||
assert.equal(providerSupportsSystemMessage("glm-cn"), false);
|
||||
});
|
||||
|
||||
it("should return false for zai", () => {
|
||||
assert.equal(providerSupportsSystemMessage("zai"), false);
|
||||
});
|
||||
|
||||
it("should return false for qianfan", () => {
|
||||
assert.equal(providerSupportsSystemMessage("qianfan"), false);
|
||||
});
|
||||
|
||||
it("should return false for o1 (existing behavior)", () => {
|
||||
assert.equal(providerSupportsSystemMessage("o1"), false);
|
||||
});
|
||||
|
||||
it("should return true for openai (regression check)", () => {
|
||||
assert.equal(providerSupportsSystemMessage("openai"), true);
|
||||
});
|
||||
|
||||
it("should return true for anthropic (regression check)", () => {
|
||||
assert.equal(providerSupportsSystemMessage("anthropic"), true);
|
||||
});
|
||||
|
||||
it("should return true for null/undefined (safe default)", () => {
|
||||
assert.equal(providerSupportsSystemMessage(null), true);
|
||||
assert.equal(providerSupportsSystemMessage(undefined), true);
|
||||
});
|
||||
});
|
||||
|
||||
// ── injectMemory with GLM providers ────────────────────────────────────────────
|
||||
|
||||
describe("injectMemory — GLM providers use user role (#1701)", () => {
|
||||
it("should inject as user role for provider=glm", () => {
|
||||
const result = injectMemory(baseRequest, testMemories, "glm");
|
||||
assert.equal(result.messages[0].role, "user");
|
||||
assert.ok(result.messages[0].content.includes("Memory context:"));
|
||||
});
|
||||
|
||||
it("should inject as user role for provider=glmt", () => {
|
||||
const result = injectMemory(baseRequest, testMemories, "glmt");
|
||||
assert.equal(result.messages[0].role, "user");
|
||||
});
|
||||
|
||||
it("should inject as user role for provider=glm-cn", () => {
|
||||
const result = injectMemory(baseRequest, testMemories, "glm-cn");
|
||||
assert.equal(result.messages[0].role, "user");
|
||||
});
|
||||
|
||||
it("should inject as user role for provider=zai", () => {
|
||||
const result = injectMemory(baseRequest, testMemories, "zai");
|
||||
assert.equal(result.messages[0].role, "user");
|
||||
});
|
||||
|
||||
it("should inject as user role for provider=qianfan", () => {
|
||||
const result = injectMemory(baseRequest, testMemories, "qianfan");
|
||||
assert.equal(result.messages[0].role, "user");
|
||||
});
|
||||
|
||||
it("should inject as system role for provider=openai (regression)", () => {
|
||||
const result = injectMemory(baseRequest, testMemories, "openai");
|
||||
assert.equal(result.messages[0].role, "system");
|
||||
});
|
||||
|
||||
it("should inject as system role for provider=anthropic (regression)", () => {
|
||||
const result = injectMemory(baseRequest, testMemories, "anthropic");
|
||||
assert.equal(result.messages[0].role, "system");
|
||||
});
|
||||
|
||||
it("should not modify original request", () => {
|
||||
const original = { ...baseRequest, messages: [...baseRequest.messages] };
|
||||
injectMemory(baseRequest, testMemories, "glm");
|
||||
assert.equal(baseRequest.messages.length, original.messages.length);
|
||||
});
|
||||
});
|
||||
|
||||
// ── normalizeSystemRole — GLM model names ──────────────────────────────────────
|
||||
|
||||
describe("normalizeSystemRole — GLM model names (#1701)", () => {
|
||||
it("should convert system→user for glm-5.1", () => {
|
||||
const messages = [
|
||||
{ role: "system", content: "Memory context: test" },
|
||||
{ role: "user", content: "Hello" },
|
||||
];
|
||||
const result = normalizeSystemRole(messages, "glm", "glm-5.1");
|
||||
assert.ok(Array.isArray(result));
|
||||
const roles = (result as { role: string }[]).map((m) => m.role);
|
||||
assert.ok(!roles.includes("system"), "system role should be converted");
|
||||
assert.ok(roles.includes("user"), "should have user role");
|
||||
});
|
||||
|
||||
it("should convert system→user for glm-4.7", () => {
|
||||
const messages = [
|
||||
{ role: "system", content: "Instructions" },
|
||||
{ role: "user", content: "Hello" },
|
||||
];
|
||||
const result = normalizeSystemRole(messages, "glm", "glm-4.7");
|
||||
const roles = (result as { role: string }[]).map((m) => m.role);
|
||||
assert.ok(!roles.includes("system"));
|
||||
});
|
||||
|
||||
it("should convert system→user for exact model id 'glm'", () => {
|
||||
const messages = [
|
||||
{ role: "system", content: "Memory" },
|
||||
{ role: "user", content: "Hello" },
|
||||
];
|
||||
const result = normalizeSystemRole(messages, "pollinations", "glm");
|
||||
const roles = (result as { role: string }[]).map((m) => m.role);
|
||||
assert.ok(!roles.includes("system"));
|
||||
});
|
||||
|
||||
it("should convert system→user for ernie models", () => {
|
||||
const messages = [
|
||||
{ role: "system", content: "Memory" },
|
||||
{ role: "user", content: "Hello" },
|
||||
];
|
||||
const result = normalizeSystemRole(messages, "qianfan", "ernie-4.5-turbo-128k");
|
||||
const roles = (result as { role: string }[]).map((m) => m.role);
|
||||
assert.ok(!roles.includes("system"));
|
||||
});
|
||||
|
||||
it("should preserve system role for openai models (regression)", () => {
|
||||
const messages = [
|
||||
{ role: "system", content: "Instructions" },
|
||||
{ role: "user", content: "Hello" },
|
||||
];
|
||||
const result = normalizeSystemRole(messages, "openai", "gpt-5.4");
|
||||
const roles = (result as { role: string }[]).map((m) => m.role);
|
||||
assert.ok(roles.includes("system"), "system role should be preserved");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user