feat(sse): add GLM-5.3 models and effort tiers

GLM-5.3 (released 2026-08-14) ships one upstream model id with effort as
a reasoning_effort request param (low|high|max, default max); thinking
can no longer be disabled. Adds glm-5.3/glm-5.3-high/glm-5.3-low to
GLM_SHARED_MODELS (glm/glm-cn/glmt) and glm-5.3 base to the zai catalog.

- GlmExecutor: parseGlm52Effort generalized to parseGlmEffortTier; 5.3
  tiers stay on the OpenAI coding transport and inject reasoning_effort
  + thinking.type=enabled; 5.2 tiers keep the Anthropic effort beta path
- modelSpecs + GLM_PRICING entries for the three ids
- default context window (1M) and pricing mirrored from GLM-5.2 (same
  base model) until Z.ai publishes official 5.3 specs

Extracted from #10358 (unrelated .planning/codebase/ scaffolding dump dropped).
This commit is contained in:
phuongddx
2026-08-14 14:52:00 +07:00
committed by Markus Hartung
parent 25ba4f2a34
commit b3017e4b8e
7 changed files with 326 additions and 22 deletions

View File

@@ -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",

View File

@@ -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" },

View File

@@ -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(

View File

@@ -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,

View File

@@ -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,

View 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");
});

View File

@@ -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.