mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-21 14:42:20 +03:00
Compare commits
3 Commits
fix/codeql
...
fix/10358-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf6d30b1bb | ||
|
|
3e1761329b | ||
|
|
b3017e4b8e |
1
changelog.d/features/10896-glm-5.3.md
Normal file
1
changelog.d/features/10896-glm-5.3.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(sse):** add GLM-5.3 support (`glm-5.3`, `glm-5.3-high`, `glm-5.3-low`) across the z.ai first-party providers, mapping the upstream `reasoning_effort` request parameter to the existing 5.2 tier UX ([#10896](https://github.com/diegosouzapw/OmniRoute/pull/10896)) — thanks @phuongddx
|
||||
@@ -18,6 +18,35 @@ export const GLM_ANTHROPIC_DEFAULT_BASE_URLS = Object.freeze({
|
||||
});
|
||||
|
||||
export const GLM_SHARED_MODELS = Object.freeze([
|
||||
{
|
||||
// GLM-5.3 (2026-08-14): one upstream id; effort is the reasoning_effort
|
||||
// param (low|high|max, default max) — the -high/-low entries below are
|
||||
// OmniRoute aliases resolved by GlmExecutor::parseGlmEffortTier.
|
||||
// Default context window not yet published by Z.ai; 1M mirrored from
|
||||
// GLM-5.2 (same base model). https://z.ai/blog/glm-5.3
|
||||
id: "glm-5.3",
|
||||
name: "GLM 5.3",
|
||||
contextLength: 1000000,
|
||||
maxOutputTokens: 131072,
|
||||
toolCalling: true,
|
||||
supportsReasoning: true,
|
||||
},
|
||||
{
|
||||
id: "glm-5.3-high",
|
||||
name: "GLM 5.3 High",
|
||||
contextLength: 1000000,
|
||||
maxOutputTokens: 131072,
|
||||
toolCalling: true,
|
||||
supportsReasoning: true,
|
||||
},
|
||||
{
|
||||
id: "glm-5.3-low",
|
||||
name: "GLM 5.3 Low",
|
||||
contextLength: 1000000,
|
||||
maxOutputTokens: 131072,
|
||||
toolCalling: true,
|
||||
supportsReasoning: true,
|
||||
},
|
||||
{
|
||||
id: "glm-5.2",
|
||||
name: "GLM 5.2",
|
||||
|
||||
@@ -11,13 +11,15 @@ export const zaiProvider: RegistryEntry = {
|
||||
authType: "apikey",
|
||||
authHeader: "x-api-key",
|
||||
headers: getAnthropicCompatHeaders(),
|
||||
// Real upstream model IDs only. The effort tiers (glm-5.2-high / glm-5.2-max)
|
||||
// are intentionally NOT listed here: they are OmniRoute aliases resolved by the
|
||||
// GlmExecutor (parseGlm52Effort → base "glm-5.2" + effort field). This provider
|
||||
// uses the DefaultExecutor, which sends the model ID verbatim, so the aliases
|
||||
// would reach z.ai's Anthropic endpoint as unknown IDs. Use the `glm` provider
|
||||
// for effort tiers. Vision models are likewise omitted (handled elsewhere).
|
||||
// Real upstream model IDs only. The effort tiers (glm-5.2-high/-max,
|
||||
// glm-5.3-high/-low) are intentionally NOT listed here: they are OmniRoute
|
||||
// aliases resolved by the GlmExecutor (parseGlmEffortTier → base model +
|
||||
// effort selector). This provider uses the DefaultExecutor, which sends the
|
||||
// model ID verbatim, so the aliases would reach z.ai's Anthropic endpoint as
|
||||
// unknown IDs. Use the `glm` provider for effort tiers. Vision models are
|
||||
// likewise omitted (handled elsewhere).
|
||||
models: [
|
||||
{ id: "glm-5.3", name: "GLM 5.3" },
|
||||
{ id: "glm-5.2", name: "GLM 5.2" },
|
||||
{ id: "glm-5.1", name: "GLM 5.1" },
|
||||
{ id: "glm-5", name: "GLM 5" },
|
||||
|
||||
@@ -52,17 +52,41 @@ function getEffectiveKey(credentials: ProviderCredentials): string {
|
||||
return credentials.apiKey || credentials.accessToken || "";
|
||||
}
|
||||
|
||||
export type GlmEffortLevel = "low" | "high" | "max";
|
||||
|
||||
type GlmEffortTier = {
|
||||
baseModel: string;
|
||||
effort: GlmEffortLevel;
|
||||
/** Transport where the upstream honors the effort selector for this family. */
|
||||
transport: GlmTransport;
|
||||
};
|
||||
|
||||
/**
|
||||
* GLM-5.2 effort tiers route exclusively through the Anthropic transport,
|
||||
* where Zhipu maps Claude Code effort selectors (high/max) to reasoning
|
||||
* intensity. The base model ID sent upstream is always "glm-5.2".
|
||||
* GLM-5.2 effort tiers (glm-5.2-high/-max) route exclusively through the
|
||||
* Anthropic transport, where Zhipu maps Claude Code effort selectors (high/max)
|
||||
* to reasoning intensity. The base model ID sent upstream is always "glm-5.2".
|
||||
*
|
||||
* GLM-5.3 replaced tier endpoints with a documented `reasoning_effort` request
|
||||
* parameter (low|high|max, default max) on the coding chat/completions endpoint,
|
||||
* so its tiers stay on the OpenAI transport and inject `reasoning_effort` +
|
||||
* `thinking.type=enabled` (5.3 no longer accepts thinking disabled).
|
||||
*
|
||||
* https://docs.z.ai/devpack/latest-model
|
||||
* https://z.ai/blog/glm-5.3
|
||||
*/
|
||||
function parseGlm52Effort(model: string): { baseModel: string; effort: "high" | "max" } | null {
|
||||
if (model === "glm-5.2-high") return { baseModel: "glm-5.2", effort: "high" };
|
||||
if (model === "glm-5.2-max") return { baseModel: "glm-5.2", effort: "max" };
|
||||
return null;
|
||||
function parseGlmEffortTier(model: string): GlmEffortTier | null {
|
||||
switch (model) {
|
||||
case "glm-5.2-high":
|
||||
return { baseModel: "glm-5.2", effort: "high", transport: "anthropic" };
|
||||
case "glm-5.2-max":
|
||||
return { baseModel: "glm-5.2", effort: "max", transport: "anthropic" };
|
||||
case "glm-5.3-high":
|
||||
return { baseModel: "glm-5.3", effort: "high", transport: "openai" };
|
||||
case "glm-5.3-low":
|
||||
return { baseModel: "glm-5.3", effort: "low", transport: "openai" };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -278,7 +302,7 @@ export class GlmExecutor extends DefaultExecutor {
|
||||
credentials: ProviderCredentials,
|
||||
transport: GlmTransport
|
||||
) {
|
||||
const effortTier = parseGlm52Effort(model);
|
||||
const effortTier = parseGlmEffortTier(model);
|
||||
const effectiveModel = effortTier ? effortTier.baseModel : model;
|
||||
|
||||
const transformed = this.transformRequest(effectiveModel, body, stream, credentials);
|
||||
@@ -313,6 +337,14 @@ export class GlmExecutor extends DefaultExecutor {
|
||||
}
|
||||
|
||||
if (transport === "openai") {
|
||||
// GLM-5.3 effort tiers: inject the documented `reasoning_effort` param and
|
||||
// force thinking on — 5.3 rejects thinking.type "disabled", and an effort
|
||||
// tier without thinking would silently drop the selector upstream.
|
||||
if (record && effortTier && effortTier.transport === "openai") {
|
||||
const existingThinking = asRecord(record.thinking);
|
||||
record.thinking = { ...existingThinking, type: "enabled" };
|
||||
record.reasoning_effort = effortTier.effort;
|
||||
}
|
||||
if (record && stream && hasTools(record) && record.tool_stream === undefined) {
|
||||
return { ...record, tool_stream: true };
|
||||
}
|
||||
@@ -446,7 +478,12 @@ export class GlmExecutor extends DefaultExecutor {
|
||||
*/
|
||||
private async finalizeAnthropicTransportResult(
|
||||
input: ExecuteInput,
|
||||
result: { response: Response; url: string; headers: Record<string, string>; transformedBody: unknown }
|
||||
result: {
|
||||
response: Response;
|
||||
url: string;
|
||||
headers: Record<string, string>;
|
||||
transformedBody: unknown;
|
||||
}
|
||||
): Promise<GlmExecuteResult> {
|
||||
const { response: rawResponse, url, headers, transformedBody } = result;
|
||||
const clientHeaders = input.clientHeaders ?? {};
|
||||
@@ -475,13 +512,14 @@ export class GlmExecutor extends DefaultExecutor {
|
||||
}
|
||||
|
||||
async execute(input: ExecuteInput): Promise<GlmExecuteResult> {
|
||||
const effortTier = parseGlm52Effort(input.model);
|
||||
const effortTier = parseGlmEffortTier(input.model);
|
||||
|
||||
// GLM-5.2 effort tiers route directly through Anthropic transport (no fallback).
|
||||
// Zhipu only graduates effort on the Anthropic endpoint via the
|
||||
// effort-2025-11-24 beta header included in GLM_ANTHROPIC_BETA.
|
||||
// Effort tiers route directly through their family's transport (no fallback):
|
||||
// GLM-5.2 → Anthropic (Zhipu only graduates effort there, via the
|
||||
// effort-2025-11-24 beta header in GLM_ANTHROPIC_BETA); GLM-5.3 → OpenAI
|
||||
// coding endpoint (`reasoning_effort` param). See parseGlmEffortTier.
|
||||
if (effortTier) {
|
||||
return this.executeTransport(input, "anthropic");
|
||||
return this.executeTransport(input, effortTier.transport);
|
||||
}
|
||||
|
||||
const primaryTransport = getGlmTransport(
|
||||
|
||||
@@ -66,7 +66,14 @@ const BEDROCK_CLAUDE_ALIASES = (...modelIds: string[]) => [
|
||||
// Provider discovery/sync sources can under-report GLM-5.2 IDs as 128K.
|
||||
// Keep native/bare Z.AI GLM-5.2 context authoritative, but do not blindly apply
|
||||
// it to every provider-wrapped alias: hosted providers can and do cap lower.
|
||||
const AUTHORITATIVE_CONTEXT_WINDOW_MODEL_IDS = new Set(["glm-5.2", "glm-5.2-high", "glm-5.2-max"]);
|
||||
const AUTHORITATIVE_CONTEXT_WINDOW_MODEL_IDS = new Set([
|
||||
"glm-5.3",
|
||||
"glm-5.3-high",
|
||||
"glm-5.3-low",
|
||||
"glm-5.2",
|
||||
"glm-5.2-high",
|
||||
"glm-5.2-max",
|
||||
]);
|
||||
const AUTHORITATIVE_PROVIDER_CONTEXT_WINDOWS = new Map<string, number>([
|
||||
["cloudflare-ai/@cf/zai-org/glm-5.2", 262144],
|
||||
// Hugging Face Router has 1M-capable backends, but bare routing can select
|
||||
@@ -567,6 +574,30 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
|
||||
supportsTools: true,
|
||||
},
|
||||
|
||||
// ── Z.AI GLM-5.3 (1M context mirrored from 5.2 — same base model; 128K max
|
||||
// output; effort via reasoning_effort param, tiers are OmniRoute aliases) ──
|
||||
"glm-5.3": {
|
||||
maxOutputTokens: 131072,
|
||||
contextWindow: 1000000,
|
||||
thinkingBudgetCap: 38912,
|
||||
supportsThinking: true,
|
||||
supportsTools: true,
|
||||
},
|
||||
"glm-5.3-high": {
|
||||
maxOutputTokens: 131072,
|
||||
contextWindow: 1000000,
|
||||
thinkingBudgetCap: 38912,
|
||||
supportsThinking: true,
|
||||
supportsTools: true,
|
||||
},
|
||||
"glm-5.3-low": {
|
||||
maxOutputTokens: 131072,
|
||||
contextWindow: 1000000,
|
||||
thinkingBudgetCap: 38912,
|
||||
supportsThinking: true,
|
||||
supportsTools: true,
|
||||
},
|
||||
|
||||
// ── Z.AI GLM-5.2 (1M context, 128K max output, effort tiers) ────
|
||||
"glm-5.2": {
|
||||
maxOutputTokens: 131072,
|
||||
|
||||
@@ -111,6 +111,30 @@ export const CLAUDE_SONNET_5_PRICING = {
|
||||
};
|
||||
|
||||
export const GLM_PRICING = {
|
||||
// GLM-5.3 (2026-08-14): Z.ai hasn't published 5.3 rates yet — mirrored from
|
||||
// GLM-5.2 (same base model; 5.1 and 5.2 also share identical rates).
|
||||
// Correct when https://docs.z.ai/guides/overview/pricing lists glm-5.3.
|
||||
"glm-5.3": {
|
||||
input: 1.2,
|
||||
output: 5,
|
||||
cached: 0.3,
|
||||
reasoning: 5,
|
||||
cache_creation: 1.2,
|
||||
},
|
||||
"glm-5.3-high": {
|
||||
input: 1.2,
|
||||
output: 5,
|
||||
cached: 0.3,
|
||||
reasoning: 5,
|
||||
cache_creation: 1.2,
|
||||
},
|
||||
"glm-5.3-low": {
|
||||
input: 1.2,
|
||||
output: 5,
|
||||
cached: 0.3,
|
||||
reasoning: 5,
|
||||
cache_creation: 1.2,
|
||||
},
|
||||
"glm-5.2": {
|
||||
input: 1.2,
|
||||
output: 5,
|
||||
|
||||
180
tests/unit/glm-5.3-catalog-and-effort-tiers.test.ts
Normal file
180
tests/unit/glm-5.3-catalog-and-effort-tiers.test.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
// GLM-5.3 support (released 2026-08-14, https://z.ai/blog/glm-5.3).
|
||||
//
|
||||
// Upstream ships ONE model id (`glm-5.3`) — effort is a request parameter
|
||||
// (`reasoning_effort`: low|high|max, default max) on the coding chat/completions
|
||||
// endpoint, and `thinking.type: "disabled"` is rejected (converted to low by the
|
||||
// coding endpoint). OmniRoute keeps the GLM-5.2 tier UX: `glm-5.3-high` /
|
||||
// `glm-5.3-low` pseudo-ids resolved by the GlmExecutor only. Base `glm-5.3` uses
|
||||
// the upstream default (max). Unlike the 5.2 tiers (Anthropic-transport effort
|
||||
// beta header), the 5.3 tiers use the documented `reasoning_effort` param on the
|
||||
// OpenAI coding transport.
|
||||
//
|
||||
// Spec caveat: Z.ai has not yet published the default context window — 1M is
|
||||
// mirrored from GLM-5.2 (same base model) per operator decision; correct when
|
||||
// the official spec lands.
|
||||
|
||||
const { getRegistryEntry } = await import("../../open-sse/config/providerRegistry.ts");
|
||||
const { GlmExecutor } = await import("../../open-sse/executors/glm.ts");
|
||||
const { MODEL_SPECS } = await import("../../src/shared/constants/modelSpecs.ts");
|
||||
const { GLM_PRICING } = await import("../../src/shared/constants/pricing/shared-tiers.ts");
|
||||
|
||||
const GLM_5_3_IDS = ["glm-5.3", "glm-5.3-high", "glm-5.3-low"] as const;
|
||||
|
||||
// transformForTransport returns an opaque body; surface only the fields asserted below.
|
||||
type TransformedRequest = {
|
||||
model?: string;
|
||||
reasoning_effort?: string;
|
||||
thinking?: { type?: string } | null;
|
||||
max_tokens?: number;
|
||||
effort?: string;
|
||||
};
|
||||
|
||||
function modelIds(provider: string): string[] {
|
||||
const entry = getRegistryEntry(provider);
|
||||
assert.ok(entry, `provider "${provider}" should be registered`);
|
||||
return (entry.models ?? []).map((m) => m.id);
|
||||
}
|
||||
|
||||
for (const provider of ["glm", "glm-cn", "glmt"]) {
|
||||
test(`${provider} advertises the GLM-5.3 base model and effort tiers (GLM_SHARED_MODELS)`, () => {
|
||||
const ids = modelIds(provider);
|
||||
for (const id of GLM_5_3_IDS) {
|
||||
assert.ok(ids.includes(id), `${provider} should expose ${id}; got ${ids.join(", ")}`);
|
||||
}
|
||||
});
|
||||
|
||||
test(`${provider} GLM-5.3 entries mirror the GLM-5.2 shape (1M ctx, 128K out)`, () => {
|
||||
const models = getRegistryEntry(provider)!.models ?? [];
|
||||
const base = models.find((m) => m.id === "glm-5.3");
|
||||
assert.ok(base, "glm-5.3 entry missing");
|
||||
assert.equal(base.contextLength, 1_000_000);
|
||||
assert.equal(base.maxOutputTokens, 131_072);
|
||||
assert.equal(base.toolCalling, true);
|
||||
assert.equal(base.supportsReasoning, true);
|
||||
});
|
||||
}
|
||||
|
||||
test("zai advertises the GLM-5.3 base model only (DefaultExecutor sends ids verbatim)", () => {
|
||||
const ids = modelIds("zai");
|
||||
assert.ok(ids.includes("glm-5.3"), `zai should advertise glm-5.3; got ${ids.join(", ")}`);
|
||||
for (const alias of ["glm-5.3-high", "glm-5.3-low"]) {
|
||||
assert.ok(
|
||||
!ids.includes(alias),
|
||||
`zai must not list ${alias}: GlmExecutor-only alias, unknown upstream on the Anthropic endpoint`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("modelSpecs carries 1M/128K specs for all GLM-5.3 ids", () => {
|
||||
for (const id of GLM_5_3_IDS) {
|
||||
const spec = MODEL_SPECS[id];
|
||||
assert.ok(spec, `MODEL_SPECS should include ${id}`);
|
||||
assert.equal(spec.contextWindow, 1_000_000);
|
||||
assert.equal(spec.maxOutputTokens, 131_072);
|
||||
assert.equal(spec.supportsThinking, true);
|
||||
}
|
||||
});
|
||||
|
||||
test("GLM_PRICING covers the GLM-5.3 ids with GLM-5.2-parity rates", () => {
|
||||
const reference = GLM_PRICING["glm-5.2"];
|
||||
assert.ok(reference, "glm-5.2 pricing reference missing");
|
||||
for (const id of GLM_5_3_IDS) {
|
||||
const pricing = GLM_PRICING[id];
|
||||
assert.ok(pricing, `GLM_PRICING should include ${id}`);
|
||||
assert.deepEqual(pricing, reference);
|
||||
}
|
||||
});
|
||||
|
||||
test("GlmExecutor resolves glm-5.3-high to reasoning_effort=high on the OpenAI coding transport", () => {
|
||||
const executor = new GlmExecutor("glm");
|
||||
const transformed = executor.transformForTransport(
|
||||
"glm-5.3-high",
|
||||
{ messages: [{ role: "user", content: "hi" }] },
|
||||
false,
|
||||
{ apiKey: "glm-key" },
|
||||
"openai"
|
||||
) as TransformedRequest;
|
||||
|
||||
assert.equal(transformed.model, "glm-5.3", "upstream must receive the base model id");
|
||||
assert.equal(transformed.reasoning_effort, "high");
|
||||
assert.equal(transformed.thinking?.type, "enabled");
|
||||
});
|
||||
|
||||
test("GlmExecutor resolves glm-5.3-low to reasoning_effort=low with thinking enabled", () => {
|
||||
const executor = new GlmExecutor("glm");
|
||||
const transformed = executor.transformForTransport(
|
||||
"glm-5.3-low",
|
||||
{ messages: [{ role: "user", content: "hi" }] },
|
||||
false,
|
||||
{ apiKey: "glm-key" },
|
||||
"openai"
|
||||
) as TransformedRequest;
|
||||
|
||||
assert.equal(transformed.model, "glm-5.3");
|
||||
assert.equal(transformed.reasoning_effort, "low");
|
||||
assert.equal(transformed.thinking?.type, "enabled");
|
||||
});
|
||||
|
||||
test("GlmExecutor leaves base glm-5.3 without an injected reasoning_effort (upstream default = max)", () => {
|
||||
const executor = new GlmExecutor("glm");
|
||||
const transformed = executor.transformForTransport(
|
||||
"glm-5.3",
|
||||
{ model: "glm-5.3", messages: [{ role: "user", content: "hi" }] },
|
||||
false,
|
||||
{ apiKey: "glm-key" },
|
||||
"openai"
|
||||
) as TransformedRequest;
|
||||
|
||||
assert.equal(transformed.model, "glm-5.3");
|
||||
assert.equal(transformed.reasoning_effort, undefined);
|
||||
// Thinking-model max_tokens default applies to 5.3 (GLM_THINKING_MODEL_PATTERN)
|
||||
assert.equal(transformed.max_tokens, 131_072);
|
||||
});
|
||||
|
||||
test("GLM-5.3 effort tiers execute on the OpenAI coding transport (no Anthropic-only pinning)", async () => {
|
||||
const executor = new GlmExecutor("glm");
|
||||
const originalFetch = globalThis.fetch;
|
||||
const calls: string[] = [];
|
||||
|
||||
globalThis.fetch = async (url) => {
|
||||
calls.push(String(url));
|
||||
return new Response(
|
||||
'data: {"id":"chatcmpl-glm53","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","content":"ok"}}]}\n\ndata: [DONE]\n\n',
|
||||
{ headers: { "Content-Type": "text/event-stream" } }
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
await executor.execute({
|
||||
model: "glm-5.3-high",
|
||||
body: { messages: [{ role: "user", content: "hello" }] },
|
||||
stream: true,
|
||||
credentials: {
|
||||
apiKey: "glm-key",
|
||||
providerSpecificData: { baseUrl: "https://api.z.ai/api/coding/paas/v4" },
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(calls, ["https://api.z.ai/api/coding/paas/v4/chat/completions"]);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("GLM-5.2 effort tiers still pin the Anthropic transport (effort beta header) — regression guard", () => {
|
||||
const executor = new GlmExecutor("glm");
|
||||
const transformed = executor.transformForTransport(
|
||||
"glm-5.2-max",
|
||||
{ messages: [{ role: "user", content: "hi" }] },
|
||||
false,
|
||||
{ apiKey: "glm-key" },
|
||||
"anthropic"
|
||||
) as TransformedRequest;
|
||||
|
||||
assert.equal(transformed.model, "glm-5.2");
|
||||
assert.equal(transformed.effort, "max");
|
||||
assert.equal(transformed.thinking?.type, "enabled");
|
||||
});
|
||||
@@ -10,7 +10,7 @@ import assert from "node:assert/strict";
|
||||
//
|
||||
// The `zai` provider uses the DefaultExecutor, which sends the requested model ID
|
||||
// verbatim. The effort tiers `glm-5.2-high` / `glm-5.2-max` are OmniRoute aliases
|
||||
// that only the GlmExecutor knows how to resolve (parseGlm52Effort → base model
|
||||
// that only the GlmExecutor knows how to resolve (parseGlmEffortTier → base model
|
||||
// "glm-5.2" + `effort` field + effort-2025-11-24 beta header). Listing them under
|
||||
// `zai` would send unknown model IDs to z.ai's Anthropic endpoint, so they belong
|
||||
// to the `glm` provider only.
|
||||
|
||||
Reference in New Issue
Block a user