fix(catalog): declare GLM reasoning effort tiers (#10963)

Merged after conflict resolution in modelMetadataRegistry.ts: the tip's effortTiers chain (declared efforts → declared tiers → undefined-if-thinking-declared → codex extension) now carries this PR's GLM guard as the final-fallback override — GLM-family models without a provider-declared contract get the authoritative empty tier list instead of generic OpenAI tiers. GLM/ZCode suites 40/40 on the resolved branch. Closes #10962. Thank you @xz-dev!
This commit is contained in:
Xiangzhe
2026-08-23 09:54:49 +08:00
committed by GitHub
parent e52d2db449
commit c018bb41a7
11 changed files with 196 additions and 31 deletions

View File

@@ -0,0 +1 @@
fix(catalog): expose only provider-routable GLM reasoning-effort tiers and remove unroutable ZCode aliases

View File

@@ -19,17 +19,16 @@ 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
// GLM-5.3 exposes low|high|max reasoning_effort (default max); -high/-low
// are OmniRoute aliases resolved by GlmExecutor::parseGlmEffortTier.
// https://docs.z.ai/guides/llm/glm-5.3
id: "glm-5.3",
name: "GLM 5.3",
contextLength: 1000000,
maxOutputTokens: 131072,
toolCalling: true,
supportsReasoning: true,
supportedThinkingEfforts: ["low", "high", "max"],
},
{
id: "glm-5.3-high",
@@ -38,6 +37,7 @@ export const GLM_SHARED_MODELS = Object.freeze([
maxOutputTokens: 131072,
toolCalling: true,
supportsReasoning: true,
supportedThinkingEfforts: ["high"],
},
{
id: "glm-5.3-low",
@@ -46,14 +46,19 @@ export const GLM_SHARED_MODELS = Object.freeze([
maxOutputTokens: 131072,
toolCalling: true,
supportsReasoning: true,
supportedThinkingEfforts: ["low"],
},
{
// GLM-5.2 has two positive effective tiers: low/medium map to high and xhigh
// maps to max; disabling thinking remains the separate thinking toggle.
// https://docs.z.ai/guides/capabilities/thinking
id: "glm-5.2",
name: "GLM 5.2",
contextLength: 1000000,
maxOutputTokens: 131072,
toolCalling: true,
supportsReasoning: true,
supportedThinkingEfforts: ["high", "max"],
},
{
id: "glm-5.2-high",
@@ -62,6 +67,7 @@ export const GLM_SHARED_MODELS = Object.freeze([
maxOutputTokens: 131072,
toolCalling: true,
supportsReasoning: true,
supportedThinkingEfforts: ["high"],
},
{
id: "glm-5.2-max",
@@ -70,14 +76,18 @@ export const GLM_SHARED_MODELS = Object.freeze([
maxOutputTokens: 131072,
toolCalling: true,
supportsReasoning: true,
supportedThinkingEfforts: ["max"],
},
{
// Earlier GLM families support the thinking toggle, not reasoning_effort.
// An explicit empty list prevents generic catalog tiers from being inferred.
id: "glm-5.1",
name: "GLM 5.1",
contextLength: 204800,
maxOutputTokens: 131072,
toolCalling: true,
supportsReasoning: true,
supportedThinkingEfforts: [],
},
{
id: "glm-5",
@@ -86,6 +96,7 @@ export const GLM_SHARED_MODELS = Object.freeze([
maxOutputTokens: 131072,
toolCalling: true,
supportsReasoning: true,
supportedThinkingEfforts: [],
},
{
id: "glm-5-turbo",
@@ -94,6 +105,7 @@ export const GLM_SHARED_MODELS = Object.freeze([
maxOutputTokens: 131072,
toolCalling: true,
supportsReasoning: true,
supportedThinkingEfforts: [],
},
{
id: "glm-4.7-flash",
@@ -102,6 +114,7 @@ export const GLM_SHARED_MODELS = Object.freeze([
maxOutputTokens: 131072,
toolCalling: true,
supportsReasoning: true,
supportedThinkingEfforts: [],
},
{
id: "glm-4.7",
@@ -110,6 +123,7 @@ export const GLM_SHARED_MODELS = Object.freeze([
maxOutputTokens: 131072,
toolCalling: true,
supportsReasoning: true,
supportedThinkingEfforts: [],
},
{
id: "glm-4.6v",
@@ -118,6 +132,7 @@ export const GLM_SHARED_MODELS = Object.freeze([
maxOutputTokens: 32768,
toolCalling: true,
supportsReasoning: true,
supportedThinkingEfforts: [],
supportsVision: true,
},
{
@@ -127,6 +142,7 @@ export const GLM_SHARED_MODELS = Object.freeze([
maxOutputTokens: 32768,
toolCalling: true,
supportsReasoning: true,
supportedThinkingEfforts: [],
},
{
id: "glm-4.5v",
@@ -135,6 +151,7 @@ export const GLM_SHARED_MODELS = Object.freeze([
maxOutputTokens: 32768,
toolCalling: true,
supportsReasoning: true,
supportedThinkingEfforts: [],
supportsVision: true,
},
{
@@ -144,6 +161,7 @@ export const GLM_SHARED_MODELS = Object.freeze([
maxOutputTokens: 32768,
toolCalling: true,
supportsReasoning: true,
supportedThinkingEfforts: [],
},
{
id: "glm-4.5-air",
@@ -152,6 +170,7 @@ export const GLM_SHARED_MODELS = Object.freeze([
maxOutputTokens: 32768,
toolCalling: true,
supportsReasoning: true,
supportedThinkingEfforts: [],
},
]);

View File

@@ -1,6 +1,17 @@
import type { RegistryEntry } from "../../shared.ts";
import { GLM_SHARED_MODELS } from "../../../glmProvider.ts";
const GLM_EXECUTOR_EFFORT_ALIASES = new Set([
"glm-5.3-high",
"glm-5.3-low",
"glm-5.2-high",
"glm-5.2-max",
]);
export const ZCODE_MODELS = GLM_SHARED_MODELS.filter(
(model) => !GLM_EXECUTOR_EFFORT_ALIASES.has(model.id)
).map((model) => ({ ...model, supportedThinkingEfforts: [] }));
/**
* Local ZCode app-server backend. Authentication remains in the user's local
* ZCode profile (`builtin:zai-coding-plan`); OmniRoute does not receive or
@@ -14,5 +25,7 @@ export const zcodeProvider: RegistryEntry = {
baseUrl: "zcode://app-server/stdio",
authType: "none",
authHeader: "none",
models: [...GLM_SHARED_MODELS],
// ZCode's app-server transport does not consume reasoning_effort; keep thinking
// capability metadata without advertising aliases or tiers that it would ignore.
models: ZCODE_MODELS,
};

View File

@@ -73,7 +73,7 @@ type GlmEffortTier = {
* `thinking.type=enabled` (5.3 no longer accepts thinking disabled).
*
* https://docs.z.ai/devpack/latest-model
* https://z.ai/blog/glm-5.3
* https://docs.z.ai/guides/llm/glm-5.3
*/
function parseGlmEffortTier(model: string): GlmEffortTier | null {
switch (model) {

View File

@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
import { existsSync } from "node:fs";
import { homedir } from "node:os";
import { join, resolve } from "node:path";
import { GLM_SHARED_MODELS } from "../config/glmProvider.ts";
import { ZCODE_MODELS } from "../config/providers/registry/zcode/index.ts";
import { BaseExecutor, type ExecuteInput, type ExecutorExecuteResult, type ProviderCredentials } from "./base.ts";
import { ZcodeAppServerClient, type ZcodeClientLike } from "./zcodeProtocol.ts";
import { buildErrorBody, errorResponse, sanitizeErrorMessage } from "../utils/error.ts";
@@ -12,8 +12,8 @@ const DEFAULT_PROVIDER_ID = "builtin:zai-coding-plan";
const DEFAULT_TURN_TIMEOUT_MS = 120_000;
const DEFAULT_POLL_INTERVAL_MS = 250;
const TERMINAL_STATUSES = new Set(["completed", "idle", "paused", "error"]);
const ZCODE_MODEL_ALLOWLIST = new Set(GLM_SHARED_MODELS.map((model) => model.id));
const DEFAULT_ZCODE_MODEL = GLM_SHARED_MODELS[0]?.id || "glm-5.2";
const ZCODE_MODEL_ALLOWLIST = new Set(ZCODE_MODELS.map((model) => model.id));
const DEFAULT_ZCODE_MODEL = ZCODE_MODELS[0]?.id || "glm-5.2";
type JsonRecord = Record<string, unknown>;
type OpenAIMsg = { role?: string; content?: unknown };

View File

@@ -106,6 +106,29 @@ describe("GLM Coding provider registry surfaces", () => {
]);
});
it("declares exact GLM reasoning-effort tiers across every shared GLM provider", () => {
const routedTiers = new Map<string, readonly string[]>([
["glm-5.3", ["low", "high", "max"]],
["glm-5.3-high", ["high"]],
["glm-5.3-low", ["low"]],
["glm-5.2", ["high", "max"]],
["glm-5.2-high", ["high"]],
["glm-5.2-max", ["max"]],
]);
for (const provider of ["glm", "glm-cn", "glmt"]) {
for (const model of getModelsByProviderId(provider)) {
expect(model.supportedThinkingEfforts, `${provider}/${model.id} effort tiers`).toEqual(
routedTiers.get(model.id) ?? []
);
}
}
for (const model of getModelsByProviderId("zcode")) {
expect(model.supportedThinkingEfforts, `zcode/${model.id} effort tiers`).toEqual([]);
}
});
it("registers GLM-5.2 with correct specs and effort tier aliases", () => {
const models = getModelsByProviderId("glm");
const get = (id: string) => models.find((m) => m.id === id);

View File

@@ -19,17 +19,17 @@
* only when the base model's own `supportedThinkingEfforts` actually declares that tier —
* never a blind string match.
*
* Skipped entirely for `codex` and `kimi`-owned models: both already own a conflicting
* native `-{effort}` suffix mechanism (`splitCodexReasoningSuffix` /
* `getKimiCodeStaticThinkingPolicy`), so double-registering here would collide with their
* own alias resolution. Also skipped for any model whose id already ends in a token that
* matches a canonical effort value, to avoid colliding with a model that legitimately ends
* in an effort-like token (e.g. a model literally named "...-high").
* Skipped entirely for `codex`, `kimi`-owned, and GLM (`glm`, `glm-cn`, `glmt`) models:
* they already own conflicting `-{effort}` aliases (`splitCodexReasoningSuffix`,
* `getKimiCodeStaticThinkingPolicy`, or `GlmExecutor::parseGlmEffortTier`), so generating
* another layer here would create invalid nested ids. Also skipped for any model whose id
* already ends in a token that matches a canonical effort value, to avoid colliding with a
* model that legitimately ends in an effort-like token (e.g. a model named "...-high").
*/
import { CANONICAL_EFFORT_VALUES } from "@/shared/reasoning/effortStandardization.ts";
/** Provider ids that already own a native `-{effort}` suffix mechanism — never double-register. */
export const SYNCED_EFFORT_SKIP_PROVIDERS = new Set(["codex"]);
/** Provider ids with dedicated `-{effort}` aliases — never synthesize another suffix layer. */
export const SYNCED_EFFORT_SKIP_PROVIDERS = new Set(["codex", "glm", "glm-cn", "glmt"]);
/** Provider-id prefixes covering that mechanism's multiple connection variants (kimi-coding, kimi-coding-apikey). */
const SYNCED_EFFORT_SKIP_PROVIDER_PREFIXES = ["kimi"];

View File

@@ -130,6 +130,11 @@ function uniqueStrings(values: Array<string | null | undefined>) {
];
}
export function isGlmFamilyModel(modelId: string, displayName = ""): boolean {
const glmFamilyPattern = /(?:^|[/@:_. -])glm(?=$|[-._ /@:](?:z)?\d|\d)/i;
return glmFamilyPattern.test(modelId) || glmFamilyPattern.test(displayName);
}
function toQualifiedId(
providerAlias: string | null,
provider: string | null,
@@ -477,11 +482,16 @@ export function enrichCatalogModelEntry<T extends JsonRecord>(
? declaredEffortTiers
: sourceDeclaresThinking
? undefined
: extendCodexGpt56EffortValues(
metadata.provider,
metadata.model,
CANONICAL_EFFORT_VALUES
);
: // #10963: GLM-family models never inherit generic OpenAI tiers — an
// explicit empty list is authoritative unless a provider-declared
// contract exists (handled by declaredEffortTiers above).
isGlmFamilyModel(metadata.model, metadata.displayName)
? []
: extendCodexGpt56EffortValues(
metadata.provider,
metadata.model,
CANONICAL_EFFORT_VALUES
);
const capabilityFields = {
...(typeof metadata.capabilities.vision === "boolean"
? { vision: metadata.capabilities.vision }
@@ -502,7 +512,9 @@ export function enrichCatalogModelEntry<T extends JsonRecord>(
// #6241: surface thinking support + the canonical effort tiers so the frontend can
// render the effort/thinking toggles. `thinking` is kept for back-compat; `supportsThinking`
// is the explicit flag and `effort_tiers` lists the selectable reasoning levels
// (only when the model actually supports thinking).
// (only when the model actually supports thinking). An explicit empty registry list
// is authoritative; GLM models also require a provider-declared contract instead of
// inheriting generic OpenAI effort tiers.
...(typeof metadata.capabilities.supportsThinking === "boolean"
? {
thinking: metadata.capabilities.supportsThinking,

View File

@@ -1,7 +1,7 @@
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).
// GLM-5.3 support (released 2026-08-14, https://docs.z.ai/guides/llm/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
@@ -12,14 +12,15 @@ import assert from "node:assert/strict";
// 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.
// Z.AI documents a 1M context window and 128K maximum output.
const { getRegistryEntry } = await import("../../open-sse/config/providerRegistry.ts");
const { getRegistryEntry, REGISTRY } = 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 metadataRegistry = await import("../../src/lib/modelMetadataRegistry.ts");
const { shouldExposeSyncedEffortVariants, SYNCED_EFFORT_SKIP_PROVIDERS } =
await import("../../open-sse/utils/syncedEffortVariants.ts");
const GLM_5_3_IDS = ["glm-5.3", "glm-5.3-high", "glm-5.3-low"] as const;
@@ -38,6 +39,87 @@ function modelIds(provider: string): string[] {
return (entry.models ?? []).map((m) => m.id);
}
test("shared GLM providers keep their dedicated aliases instead of synthesizing another layer", () => {
for (const provider of ["glm", "glm-cn", "glmt"]) {
assert.ok(SYNCED_EFFORT_SKIP_PROVIDERS.has(provider), provider);
assert.equal(
shouldExposeSyncedEffortVariants({
id: `${provider}/glm-5.3`,
owned_by: provider,
capabilities: { effort_tiers: ["low", "high", "max"] },
}),
false,
provider
);
}
assert.equal(SYNCED_EFFORT_SKIP_PROVIDERS.has("zcode"), false);
});
test("GLM family detection covers numeric, Z1, and bare provider model ids", () => {
for (const modelId of [
"hf:zai-org/GLM-5.2",
"THUDM/GLM-Z1-32B-0414",
"THUDM/GLM-Z1-9B-0414",
"glm",
]) {
assert.equal(metadataRegistry.isGlmFamilyModel(modelId), true, modelId);
}
assert.equal(metadataRegistry.isGlmFamilyModel("llama-3.3"), false);
});
test("catalog suppresses inferred tiers for every GLM registry entry without a provider contract", () => {
let audited = 0;
for (const [provider, entry] of Object.entries(REGISTRY)) {
for (const model of entry.models ?? []) {
if (!metadataRegistry.isGlmFamilyModel(model.id, model.name)) continue;
audited += 1;
const enriched = metadataRegistry.enrichCatalogModelEntry({
id: `${provider}/${model.id}`,
object: "model",
owned_by: provider,
root: model.id,
}) as Record<string, unknown>;
const capabilities = enriched.capabilities as Record<string, unknown>;
if (capabilities.supportsThinking === true) {
assert.deepEqual(
capabilities.effort_tiers,
model.supportedThinkingEfforts ?? [],
`${provider}/${model.id}`
);
} else {
assert.equal("effort_tiers" in capabilities, false, `${provider}/${model.id}`);
}
}
}
assert.ok(audited > 0);
});
test("catalog exposes only GLM effort tiers that each provider can route", () => {
const routedTiers = new Map<string, string[]>([
["glm-5.3", ["low", "high", "max"]],
["glm-5.3-high", ["high"]],
["glm-5.3-low", ["low"]],
["glm-5.2", ["high", "max"]],
["glm-5.2-high", ["high"]],
["glm-5.2-max", ["max"]],
]);
for (const provider of ["glm", "glm-cn", "glmt", "zcode"]) {
for (const model of getRegistryEntry(provider)!.models ?? []) {
const enriched = metadataRegistry.enrichCatalogModelEntry({
id: `${provider}/${model.id}`,
object: "model",
owned_by: provider,
root: model.id,
}) as Record<string, unknown>;
const capabilities = enriched.capabilities as Record<string, unknown>;
const expected = provider === "zcode" ? [] : (routedTiers.get(model.id) ?? []);
assert.equal(capabilities.supportsThinking, true, `${provider}/${model.id}`);
assert.deepEqual(capabilities.effort_tiers, expected, `${provider}/${model.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);

View File

@@ -26,6 +26,8 @@ function requestBody() {
test("ZCode accepts GLM Coding Plan models and rejects unsafe/unknown ids", async () => {
const { resolveZcodeModel } = await loadZcodeExecutor();
assert.deepEqual(resolveZcodeModel("glm-5.2"), { ok: true, model: "glm-5.2" });
assert.equal(resolveZcodeModel("glm-5.2-high").ok, false);
assert.equal(resolveZcodeModel("glm-5.3-low").ok, false);
assert.equal(resolveZcodeModel("-unexpected").ok, false);
assert.equal(resolveZcodeModel("unknown-model").ok, false);
});
@@ -70,7 +72,7 @@ test("ZCode buffers the completed turn into OpenAI SSE when stream=true", async
});
const result = await executor.execute({
model: "glm-5.2-high",
model: "glm-5.2",
body: requestBody(),
stream: true,
credentials: {},

View File

@@ -10,5 +10,18 @@ test("ZCode provider registry exposes a local no-auth GLM Coding Plan backend",
assert.equal(zcodeProvider.baseUrl, "zcode://app-server/stdio");
assert.equal(zcodeProvider.authType, "none");
assert.equal(zcodeProvider.authHeader, "none");
assert.equal(zcodeProvider.models.some((model) => model.id === "glm-5.2"), true);
assert.equal(
zcodeProvider.models.some((model) => model.id === "glm-5.2"),
true
);
for (const alias of ["glm-5.3-high", "glm-5.3-low", "glm-5.2-high", "glm-5.2-max"]) {
assert.equal(
zcodeProvider.models.some((model) => model.id === alias),
false,
alias
);
}
for (const model of zcodeProvider.models) {
assert.deepEqual(model.supportedThinkingEfforts, [], model.id);
}
});