From 64dc979175a2bb7fa4cd88d6f02a394ece98dafb Mon Sep 17 00:00:00 2001 From: "@taltas" <6816042+taltas@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:32:36 +0000 Subject: [PATCH] feat(api): support official GLM-5.3 Z.ai release --- packages/types/src/providers/zai.ts | 54 +++++++------- src/api/providers/__tests__/zai.spec.ts | 36 +++++++-- src/api/providers/zai.ts | 73 +++++++++++++------ .../__tests__/providerModelConfig.spec.ts | 4 +- .../hooks/__tests__/useSelectedModel.spec.ts | 25 +++++-- 5 files changed, 126 insertions(+), 66 deletions(-) diff --git a/packages/types/src/providers/zai.ts b/packages/types/src/providers/zai.ts index a14edfe9e3..79af21d8dc 100644 --- a/packages/types/src/providers/zai.ts +++ b/packages/types/src/providers/zai.ts @@ -11,6 +11,21 @@ import { ZaiApiLine } from "../provider-settings.js" // https://docs.z.ai/guides/overview/pricing // https://bigmodel.cn/pricing +const glm53ModelInfo = { + maxTokens: 131_072, + contextWindow: 1_000_000, + supportsImages: false, + supportsPromptCache: true, + supportsMaxTokens: true, + supportsReasoningEffort: ["low", "high", "max"], + requiredReasoningEffort: true, + reasoningEffort: "max", + preserveReasoning: true, + defaultTemperature: 1, + description: + "GLM-5.3 is Zhipu's flagship coding and agent model with a 1M context window, 128k max output, and always-on reasoning with configurable effort (Low/High/Max).", +} as const satisfies ModelInfo + export type InternationalZAiModelId = keyof typeof internationalZAiModels export const internationalZAiDefaultModelId: InternationalZAiModelId = "glm-4.7" export const internationalZAiModels = { @@ -171,6 +186,13 @@ export const internationalZAiModels = { description: "GLM-5.2 is Zhipu's flagship model with a 1M context window, 128k max output, and dual thinking-effort modes (High/Max). It delivers top-tier long-context reasoning, coding, and agentic performance for extended engineering sessions.", }, + "glm-5.3": { + ...glm53ModelInfo, + inputPrice: 1.4, + outputPrice: 4.4, + cacheWritesPrice: 0, + cacheReadsPrice: 0.26, + }, "glm-5-turbo": { maxTokens: 131_072, contextWindow: 202_752, @@ -474,34 +496,9 @@ export const mainlandZAiModels = { }, } as const satisfies Record -const glm53CodingPlanModelInfo = { - maxTokens: 131_072, - contextWindow: 1_000_000, - supportsImages: false, - supportsPromptCache: true, - supportsMaxTokens: true, - supportsReasoningEffort: ["low", "high", "max"], - requiredReasoningEffort: true, - reasoningEffort: "max", - preserveReasoning: true, - description: - "GLM-5.3 is Zhipu's flagship coding and agent model with a 1M context window, 128k max output, and always-on reasoning with configurable effort (Low/High/Max). Available to GLM Coding Plan users.", -} as const satisfies ModelInfo - -export const internationalZAiCodingPlanOnlyModels = { - "glm-5.3": { - ...glm53CodingPlanModelInfo, - // GLM-5.3 API pricing is not published yet; use GLM-5.2 pricing provisionally. - inputPrice: 1.4, - outputPrice: 4.4, - cacheWritesPrice: 0, - cacheReadsPrice: 0.26, - }, -} as const satisfies Record - export const mainlandZAiCodingPlanOnlyModels = { "glm-5.3": { - ...glm53CodingPlanModelInfo, + ...glm53ModelInfo, // GLM-5.3 API pricing is not published yet; use GLM-5.2 pricing provisionally. inputPrice: 0.68, outputPrice: 2.28, @@ -538,6 +535,7 @@ export const zaiApiLineConfigs = { export function getZAiModels(apiLine: ZaiApiLine = "international_coding"): Record { const isChina = zaiApiLineConfigs[apiLine].isChina const regionalModels = isChina ? mainlandZAiModels : internationalZAiModels - const codingPlanOnlyModels = isChina ? mainlandZAiCodingPlanOnlyModels : internationalZAiCodingPlanOnlyModels - return apiLine.endsWith("_coding") ? { ...regionalModels, ...codingPlanOnlyModels } : regionalModels + return isChina && apiLine.endsWith("_coding") + ? { ...regionalModels, ...mainlandZAiCodingPlanOnlyModels } + : regionalModels } diff --git a/src/api/providers/__tests__/zai.spec.ts b/src/api/providers/__tests__/zai.spec.ts index 440bbc2328..0230b679a9 100644 --- a/src/api/providers/__tests__/zai.spec.ts +++ b/src/api/providers/__tests__/zai.spec.ts @@ -142,7 +142,7 @@ describe("ZAiHandler", () => { expect(model.info.cacheReadsPrice).toBe(0.26) }) - it("should expose GLM-5.3 for the international Coding Plan with provisional GLM-5.2 pricing", () => { + it("should expose GLM-5.3 for the international Coding Plan with official pricing", () => { const handlerWithModel = new ZAiHandler({ apiModelId: "glm-5.3", zaiApiKey: "test-zai-api-key", @@ -160,6 +160,7 @@ describe("ZAiHandler", () => { requiredReasoningEffort: true, reasoningEffort: "max", preserveReasoning: true, + defaultTemperature: 1, }) expect(model.info.inputPrice).toBe(1.4) expect(model.info.outputPrice).toBe(4.4) @@ -390,14 +391,14 @@ describe("ZAiHandler", () => { expect(model.info).toEqual(internationalZAiModels[testModelId]) }) - it("should not expose Coding Plan-only models", () => { - expect(getZAiModels("international_api")).not.toHaveProperty("glm-5.3") + it("should expose GLM-5.3 on the international API", () => { + expect(getZAiModels("international_api")).toHaveProperty("glm-5.3") const handlerWithModel = new ZAiHandler({ apiModelId: "glm-5.3", zaiApiKey: "test-zai-api-key", zaiApiLine: "international_api", }) - expect(handlerWithModel.getModel().id).toBe(internationalZAiDefaultModelId) + expect(handlerWithModel.getModel().id).toBe("glm-5.3") }) }) @@ -668,7 +669,7 @@ describe("ZAiHandler", () => { ) }) - it("should keep GLM-5.3 reasoning enabled when a persisted setting requests disable", async () => { + it("should use the official GLM-5.3 thinking and sampling defaults", async () => { const handlerWithModel = new ZAiHandler({ apiModelId: "glm-5.3", zaiApiKey: "test-zai-api-key", @@ -684,8 +685,9 @@ describe("ZAiHandler", () => { expect(mockCreate).toHaveBeenCalledWith( expect.objectContaining({ model: "glm-5.3", - thinking: { type: "enabled" }, + thinking: { type: "enabled", clear_thinking: false }, reasoning_effort: "max", + temperature: 1, }), ) }) @@ -706,12 +708,32 @@ describe("ZAiHandler", () => { expect(mockCreate).toHaveBeenCalledWith( expect.objectContaining({ model: "glm-5.3", - thinking: { type: "enabled" }, + thinking: { type: "enabled", clear_thinking: false }, reasoning_effort: "max", }), ) }) + it("should use the official GLM-5.3 parameters for completePrompt", async () => { + const handlerWithModel = new ZAiHandler({ + apiModelId: "glm-5.3", + zaiApiKey: "test-zai-api-key", + zaiApiLine: "international_api", + reasoningEffort: "low", + }) + + mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: "response" } }] }) + + await expect(handlerWithModel.completePrompt("prompt")).resolves.toBe("response") + expect(mockCreate).toHaveBeenCalledWith({ + model: "glm-5.3", + messages: [{ role: "user", content: "prompt" }], + temperature: 1, + thinking: { type: "enabled", clear_thinking: false }, + reasoning_effort: "low", + }) + }) + it("should omit reasoning_effort for GLM-5.2 when reasoningEffort is set to disable", async () => { const handlerWithModel = new ZAiHandler({ apiModelId: "glm-5.2", diff --git a/src/api/providers/zai.ts b/src/api/providers/zai.ts index 65a6bf458c..5ebc0e19a3 100644 --- a/src/api/providers/zai.ts +++ b/src/api/providers/zai.ts @@ -13,7 +13,7 @@ import { import { type ApiHandlerOptions, getModelMaxOutputTokens } from "../../shared/api" import { convertToZAiFormat } from "../transform/zai-format" -import type { ApiHandlerCreateMessageMetadata } from "../index" +import type { ApiHandlerCreateMessageMetadata, CompletePromptOptions } from "../index" import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider" import { handleOpenAIError } from "./utils/error-handler" @@ -21,8 +21,8 @@ import { handleOpenAIError } from "./utils/error-handler" // Z.ai accepts the standard `reasoning_effort` ladder (none/minimal/low/medium/high/xhigh/max) // alongside the GLM-specific `thinking` toggle. Omit the OpenAI-typed `reasoning_effort` so we // can widen it to include provider-specific values such as "max". -type ZAiChatCompletionParams = Omit & { - thinking?: { type: "enabled" | "disabled" } +type ZAiChatCompletionParams = Omit & { + thinking?: { type: "enabled" | "disabled"; clear_thinking?: boolean } reasoning_effort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" } @@ -78,22 +78,7 @@ export class ZAiHandler extends BaseOpenAiCompatibleProvider { metadata?: ApiHandlerCreateMessageMetadata, ) { const { id: model, info } = this.getModel() - - // Fall back to the model default when the resolved effort isn't supported by the model. - const supported = info.supportsReasoningEffort - const raw = - this.options.enableReasoningEffort === false - ? undefined - : (this.options.reasoningEffort ?? info.reasoningEffort) - const requiresReasoning = info.requiredReasoningEffort === true - const effort = - requiresReasoning && (!raw || raw === "disable") - ? info.reasoningEffort - : raw && Array.isArray(supported) && !supported.includes(raw) - ? info.reasoningEffort - : raw - const reasoningEffort = effort && effort !== "disable" ? effort : undefined - const useReasoning = requiresReasoning || reasoningEffort !== undefined + const { reasoningEffort, useReasoning } = this.getReasoningSettings(info) const max_tokens = this.options.modelMaxTokens || @@ -105,7 +90,7 @@ export class ZAiHandler extends BaseOpenAiCompatibleProvider { }) ?? undefined) - const temperature = this.options.modelTemperature ?? this.defaultTemperature + const temperature = this.options.modelTemperature ?? info.defaultTemperature ?? this.defaultTemperature // Use Z.ai format to preserve reasoning_content and merge post-tool text into tool messages const convertedMessages = convertToZAiFormat(messages, { mergeToolResultText: true }) @@ -118,7 +103,9 @@ export class ZAiHandler extends BaseOpenAiCompatibleProvider { stream: true, stream_options: { include_usage: true }, // Models with required reasoning stay enabled even when an old setting requests disable. - thinking: useReasoning ? { type: "enabled" } : { type: "disabled" }, + thinking: useReasoning + ? { type: "enabled", ...(model === "glm-5.3" && { clear_thinking: false }) } + : { type: "disabled" }, reasoning_effort: reasoningEffort, tools: this.convertToolsForOpenAI(metadata?.tools), tool_choice: metadata?.tool_choice, @@ -133,4 +120,48 @@ export class ZAiHandler extends BaseOpenAiCompatibleProvider { throw handleOpenAIError(error, this.providerName) } } + + private getReasoningSettings(info: ModelInfo) { + // Fall back to the model default when the resolved effort isn't supported by the model. + const supported = info.supportsReasoningEffort + const raw = + this.options.enableReasoningEffort === false + ? undefined + : (this.options.reasoningEffort ?? info.reasoningEffort) + const requiresReasoning = info.requiredReasoningEffort === true + const effort = + requiresReasoning && (!raw || raw === "disable") + ? info.reasoningEffort + : raw && Array.isArray(supported) && !supported.includes(raw) + ? info.reasoningEffort + : raw + const reasoningEffort = effort && effort !== "disable" ? effort : undefined + + return { reasoningEffort, useReasoning: requiresReasoning || reasoningEffort !== undefined } + } + + override async completePrompt(prompt: string, options?: CompletePromptOptions): Promise { + const { id: model, info } = this.getModel() + if (model !== "glm-5.3") { + return super.completePrompt(prompt, options) + } + + const { reasoningEffort } = this.getReasoningSettings(info) + const params: ZAiChatCompletionParams = { + model, + messages: [{ role: "user", content: prompt }], + temperature: this.options.modelTemperature ?? info.defaultTemperature ?? this.defaultTemperature, + thinking: { type: "enabled", clear_thinking: false }, + reasoning_effort: reasoningEffort, + } + + try { + const response = await this.client.chat.completions.create( + params as OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming, + ) + return response.choices?.[0]?.message.content || "" + } catch (error) { + throw handleOpenAIError(error, this.providerName) + } + } } diff --git a/webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts b/webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts index e19df545e5..2bd91f19aa 100644 --- a/webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts +++ b/webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts @@ -206,7 +206,7 @@ describe("providerModelConfig", () => { expect(Object.keys(models).length).toBe(0) }) - it("shows GLM-5.3 only for Z.ai Coding Plan entrypoints", () => { + it("shows GLM-5.3 for international Z.ai API and Coding Plan entrypoints", () => { const internationalCoding = getStaticModelsForProvider("zai", undefined, { apiProvider: "zai", zaiApiLine: "international_coding", @@ -226,7 +226,7 @@ describe("providerModelConfig", () => { expect(internationalCoding).toHaveProperty("glm-5.3") expect(chinaCoding).toHaveProperty("glm-5.3") - expect(internationalApi).not.toHaveProperty("glm-5.3") + expect(internationalApi).toHaveProperty("glm-5.3") expect(chinaApi).not.toHaveProperty("glm-5.3") }) }) diff --git a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts index dbc52e390b..3558d47e38 100644 --- a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts +++ b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts @@ -33,7 +33,6 @@ import { lMStudioDefaultModelInfo, opencodeGoDefaultModelInfo, getZAiModels, - internationalZAiDefaultModelId, mainlandZAiDefaultModelId, providerIdentifiers, retiredProviderIdentifiers, @@ -1355,20 +1354,30 @@ describe("useSelectedModel", () => { expect(result.current.info?.inputPrice).toBe(0.68) }) - it.each([ - ["international_api", internationalZAiDefaultModelId], - ["china_api", mainlandZAiDefaultModelId], - ] as const)("falls back when GLM-5.3 is unavailable on %s", (zaiApiLine, expectedModelId) => { + it("uses the International API catalog for GLM-5.3", () => { const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.zai, apiModelId: "glm-5.3", - zaiApiLine, + zaiApiLine: "international_api", } const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper: createWrapper() }) - expect(result.current.id).toBe(expectedModelId) - expect(result.current.info).toEqual(getZAiModels(zaiApiLine)[expectedModelId]) + expect(result.current.id).toBe("glm-5.3") + expect(result.current.info).toEqual(getZAiModels("international_api")["glm-5.3"]) + }) + + it("falls back when GLM-5.3 is unavailable on the China API", () => { + const apiConfiguration: ProviderSettings = { + apiProvider: providerIdentifiers.zai, + apiModelId: "glm-5.3", + zaiApiLine: "china_api", + } + + const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper: createWrapper() }) + + expect(result.current.id).toBe(mainlandZAiDefaultModelId) + expect(result.current.info).toEqual(getZAiModels("china_api")[mainlandZAiDefaultModelId]) }) })