Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 26 additions & 28 deletions packages/types/src/providers/zai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -474,34 +496,9 @@ export const mainlandZAiModels = {
},
} as const satisfies Record<string, ModelInfo>

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<string, ModelInfo>

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,
Expand Down Expand Up @@ -538,6 +535,7 @@ export const zaiApiLineConfigs = {
export function getZAiModels(apiLine: ZaiApiLine = "international_coding"): Record<string, ModelInfo> {
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
}
36 changes: 29 additions & 7 deletions src/api/providers/__tests__/zai.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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)
Expand Down Expand Up @@ -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")
})
})

Expand Down Expand Up @@ -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",
Expand All @@ -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,
}),
)
})
Expand All @@ -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",
Expand Down
73 changes: 52 additions & 21 deletions src/api/providers/zai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ 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"

// Custom interface for Z.ai params to support thinking mode and reasoning effort tiers.
// 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<OpenAI.Chat.ChatCompletionCreateParamsStreaming, "reasoning_effort"> & {
thinking?: { type: "enabled" | "disabled" }
type ZAiChatCompletionParams = Omit<OpenAI.Chat.ChatCompletionCreateParams, "reasoning_effort"> & {
thinking?: { type: "enabled" | "disabled"; clear_thinking?: boolean }
reasoning_effort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"
}

Expand Down Expand Up @@ -78,22 +78,7 @@ export class ZAiHandler extends BaseOpenAiCompatibleProvider<string> {
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 ||
Expand All @@ -105,7 +90,7 @@ export class ZAiHandler extends BaseOpenAiCompatibleProvider<string> {
}) ??
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 })
Expand All @@ -118,7 +103,9 @@ export class ZAiHandler extends BaseOpenAiCompatibleProvider<string> {
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,
Expand All @@ -133,4 +120,48 @@ export class ZAiHandler extends BaseOpenAiCompatibleProvider<string> {
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<string> {
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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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")
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import {
lMStudioDefaultModelInfo,
opencodeGoDefaultModelInfo,
getZAiModels,
internationalZAiDefaultModelId,
mainlandZAiDefaultModelId,
providerIdentifiers,
retiredProviderIdentifiers,
Expand Down Expand Up @@ -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])
})
})

Expand Down
Loading