Skip to content

Commit 4cbb4bb

Browse files
committed
feat(providers): enable GLM 5.3 for Z AI coding plans
1 parent fe05015 commit 4cbb4bb

9 files changed

Lines changed: 238 additions & 26 deletions

File tree

packages/types/src/providers/zai.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ZaiApiLine } from "../provider-settings.js"
55
// https://docs.z.ai/guides/llm/glm-4-32b-0414-128k
66
// https://docs.z.ai/guides/llm/glm-4.5
77
// https://docs.z.ai/guides/llm/glm-4.6
8+
// https://docs.z.ai/guides/llm/glm-5.3
89
// https://docs.z.ai/guides/llm/glm-5.1
910
// https://docs.z.ai/guides/llm/glm-5-turbo
1011
// https://docs.z.ai/guides/overview/pricing
@@ -473,6 +474,42 @@ export const mainlandZAiModels = {
473474
},
474475
} as const satisfies Record<string, ModelInfo>
475476

477+
const glm53CodingPlanModelInfo = {
478+
maxTokens: 131_072,
479+
contextWindow: 1_000_000,
480+
supportsImages: false,
481+
supportsPromptCache: true,
482+
supportsMaxTokens: true,
483+
supportsReasoningEffort: ["low", "high", "max"],
484+
requiredReasoningEffort: true,
485+
reasoningEffort: "max",
486+
preserveReasoning: true,
487+
description:
488+
"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.",
489+
} as const satisfies ModelInfo
490+
491+
export const internationalZAiCodingPlanOnlyModels = {
492+
"glm-5.3": {
493+
...glm53CodingPlanModelInfo,
494+
// GLM-5.3 API pricing is not published yet; use GLM-5.2 pricing provisionally.
495+
inputPrice: 1.4,
496+
outputPrice: 4.4,
497+
cacheWritesPrice: 0,
498+
cacheReadsPrice: 0.26,
499+
},
500+
} as const satisfies Record<string, ModelInfo>
501+
502+
export const mainlandZAiCodingPlanOnlyModels = {
503+
"glm-5.3": {
504+
...glm53CodingPlanModelInfo,
505+
// GLM-5.3 API pricing is not published yet; use GLM-5.2 pricing provisionally.
506+
inputPrice: 0.68,
507+
outputPrice: 2.28,
508+
cacheWritesPrice: 0,
509+
cacheReadsPrice: 0.13,
510+
},
511+
} as const satisfies Record<string, ModelInfo>
512+
476513
export const ZAI_DEFAULT_TEMPERATURE = 0.6
477514

478515
export const zaiApiLineConfigs = {
@@ -497,3 +534,10 @@ export const zaiApiLineConfigs = {
497534
isChina: true,
498535
},
499536
} satisfies Record<ZaiApiLine, { name: string; baseUrl: string; isChina: boolean }>
537+
538+
export function getZAiModels(apiLine: ZaiApiLine = "international_coding"): Record<string, ModelInfo> {
539+
const isChina = zaiApiLineConfigs[apiLine].isChina
540+
const regionalModels = isChina ? mainlandZAiModels : internationalZAiModels
541+
const codingPlanOnlyModels = isChina ? mainlandZAiCodingPlanOnlyModels : internationalZAiCodingPlanOnlyModels
542+
return apiLine.endsWith("_coding") ? { ...regionalModels, ...codingPlanOnlyModels } : regionalModels
543+
}

src/api/providers/__tests__/zai.spec.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
internationalZAiModels,
1212
mainlandZAiModels,
1313
ZAI_DEFAULT_TEMPERATURE,
14+
getZAiModels,
1415
} from "@roo-code/types"
1516

1617
import { ZAiHandler } from "../zai"
@@ -141,6 +142,30 @@ describe("ZAiHandler", () => {
141142
expect(model.info.cacheReadsPrice).toBe(0.26)
142143
})
143144

145+
it("should expose GLM-5.3 for the international Coding Plan with provisional GLM-5.2 pricing", () => {
146+
const handlerWithModel = new ZAiHandler({
147+
apiModelId: "glm-5.3",
148+
zaiApiKey: "test-zai-api-key",
149+
zaiApiLine: "international_coding",
150+
})
151+
const model = handlerWithModel.getModel()
152+
expect(model.id).toBe("glm-5.3")
153+
expect(model.info).toMatchObject({
154+
contextWindow: 1_000_000,
155+
maxTokens: 131_072,
156+
supportsImages: false,
157+
supportsPromptCache: true,
158+
supportsMaxTokens: true,
159+
supportsReasoningEffort: ["low", "high", "max"],
160+
requiredReasoningEffort: true,
161+
reasoningEffort: "max",
162+
preserveReasoning: true,
163+
})
164+
expect(model.info.inputPrice).toBe(1.4)
165+
expect(model.info.outputPrice).toBe(4.4)
166+
expect(model.info.cacheReadsPrice).toBe(0.26)
167+
})
168+
144169
it("should return GLM-5-Turbo international model with thinking support", () => {
145170
const testModelId: InternationalZAiModelId = "glm-5-turbo"
146171
const handlerWithModel = new ZAiHandler({
@@ -277,6 +302,22 @@ describe("ZAiHandler", () => {
277302
expect(model.info.cacheReadsPrice).toBe(0.13)
278303
})
279304

305+
it("should expose GLM-5.3 for the China Coding Plan", () => {
306+
const handlerWithModel = new ZAiHandler({
307+
apiModelId: "glm-5.3",
308+
zaiApiKey: "test-zai-api-key",
309+
zaiApiLine: "china_coding",
310+
})
311+
const model = handlerWithModel.getModel()
312+
expect(model.id).toBe("glm-5.3")
313+
expect(model.info.supportsReasoningEffort).toEqual(["low", "high", "max"])
314+
expect(model.info.requiredReasoningEffort).toBe(true)
315+
expect(model.info.reasoningEffort).toBe("max")
316+
expect(model.info.inputPrice).toBe(0.68)
317+
expect(model.info.outputPrice).toBe(2.28)
318+
expect(model.info.cacheReadsPrice).toBe(0.13)
319+
})
320+
280321
it("should return GLM-4.7 China model with thinking support", () => {
281322
const testModelId: MainlandZAiModelId = "glm-4.7"
282323
const handlerWithModel = new ZAiHandler({
@@ -348,6 +389,16 @@ describe("ZAiHandler", () => {
348389
expect(model.id).toBe(testModelId)
349390
expect(model.info).toEqual(internationalZAiModels[testModelId])
350391
})
392+
393+
it("should not expose Coding Plan-only models", () => {
394+
expect(getZAiModels("international_api")).not.toHaveProperty("glm-5.3")
395+
const handlerWithModel = new ZAiHandler({
396+
apiModelId: "glm-5.3",
397+
zaiApiKey: "test-zai-api-key",
398+
zaiApiLine: "international_api",
399+
})
400+
expect(handlerWithModel.getModel().id).toBe(internationalZAiDefaultModelId)
401+
})
351402
})
352403

353404
describe("China API", () => {
@@ -387,6 +438,10 @@ describe("ZAiHandler", () => {
387438
expect(model.id).toBe(testModelId)
388439
expect(model.info).toEqual(mainlandZAiModels[testModelId])
389440
})
441+
442+
it("should not expose Coding Plan-only models", () => {
443+
expect(getZAiModels("china_api")).not.toHaveProperty("glm-5.3")
444+
})
390445
})
391446

392447
describe("Default behavior", () => {
@@ -613,6 +668,50 @@ describe("ZAiHandler", () => {
613668
)
614669
})
615670

671+
it("should keep GLM-5.3 reasoning enabled when a persisted setting requests disable", async () => {
672+
const handlerWithModel = new ZAiHandler({
673+
apiModelId: "glm-5.3",
674+
zaiApiKey: "test-zai-api-key",
675+
zaiApiLine: "international_coding",
676+
reasoningEffort: "disable",
677+
})
678+
679+
mockCreate.mockImplementationOnce(() => asyncStreamFrom([]))
680+
681+
const messageGenerator = handlerWithModel.createMessage("system prompt", [])
682+
await messageGenerator.next()
683+
684+
expect(mockCreate).toHaveBeenCalledWith(
685+
expect.objectContaining({
686+
model: "glm-5.3",
687+
thinking: { type: "enabled" },
688+
reasoning_effort: "max",
689+
}),
690+
)
691+
})
692+
693+
it("should keep GLM-5.3 reasoning enabled when the master reasoning setting is disabled", async () => {
694+
const handlerWithModel = new ZAiHandler({
695+
apiModelId: "glm-5.3",
696+
zaiApiKey: "test-zai-api-key",
697+
zaiApiLine: "international_coding",
698+
enableReasoningEffort: false,
699+
})
700+
701+
mockCreate.mockImplementationOnce(() => asyncStreamFrom([]))
702+
703+
const messageGenerator = handlerWithModel.createMessage("system prompt", [])
704+
await messageGenerator.next()
705+
706+
expect(mockCreate).toHaveBeenCalledWith(
707+
expect.objectContaining({
708+
model: "glm-5.3",
709+
thinking: { type: "enabled" },
710+
reasoning_effort: "max",
711+
}),
712+
)
713+
})
714+
616715
it("should omit reasoning_effort for GLM-5.2 when reasoningEffort is set to disable", async () => {
617716
const handlerWithModel = new ZAiHandler({
618717
apiModelId: "glm-5.2",

src/api/providers/zai.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@ import { Anthropic } from "@anthropic-ai/sdk"
22
import OpenAI from "openai"
33

44
import {
5-
internationalZAiModels,
6-
mainlandZAiModels,
75
internationalZAiDefaultModelId,
86
mainlandZAiDefaultModelId,
97
type ModelInfo,
108
ZAI_DEFAULT_TEMPERATURE,
119
zaiApiLineConfigs,
10+
getZAiModels,
1211
} from "@roo-code/types"
1312

1413
import { type ApiHandlerOptions, getModelMaxOutputTokens } from "../../shared/api"
@@ -29,14 +28,15 @@ type ZAiChatCompletionParams = Omit<OpenAI.Chat.ChatCompletionCreateParamsStream
2928

3029
export class ZAiHandler extends BaseOpenAiCompatibleProvider<string> {
3130
constructor(options: ApiHandlerOptions) {
32-
const isChina = zaiApiLineConfigs[options.zaiApiLine ?? "international_coding"].isChina
33-
const models = (isChina ? mainlandZAiModels : internationalZAiModels) as unknown as Record<string, ModelInfo>
31+
const apiLine = options.zaiApiLine ?? "international_coding"
32+
const isChina = zaiApiLineConfigs[apiLine].isChina
33+
const models = getZAiModels(apiLine)
3434
const defaultModelId = (isChina ? mainlandZAiDefaultModelId : internationalZAiDefaultModelId) as string
3535

3636
super({
3737
...options,
3838
providerName: "Z.ai",
39-
baseURL: zaiApiLineConfigs[options.zaiApiLine ?? "international_coding"].baseUrl,
39+
baseURL: zaiApiLineConfigs[apiLine].baseUrl,
4040
apiKey: options.zaiApiKey ?? "not-provided",
4141
defaultProviderModelId: defaultModelId,
4242
providerModels: models,
@@ -85,12 +85,15 @@ export class ZAiHandler extends BaseOpenAiCompatibleProvider<string> {
8585
this.options.enableReasoningEffort === false
8686
? undefined
8787
: (this.options.reasoningEffort ?? info.reasoningEffort)
88+
const requiresReasoning = info.requiredReasoningEffort === true
8889
const effort =
89-
raw && raw !== "disable" && Array.isArray(supported) && !supported.includes(raw)
90+
requiresReasoning && (!raw || raw === "disable")
9091
? info.reasoningEffort
91-
: raw
92+
: raw && Array.isArray(supported) && !supported.includes(raw)
93+
? info.reasoningEffort
94+
: raw
9295
const reasoningEffort = effort && effort !== "disable" ? effort : undefined
93-
const useReasoning = reasoningEffort !== undefined
96+
const useReasoning = requiresReasoning || reasoningEffort !== undefined
9497

9598
const max_tokens =
9699
this.options.modelMaxTokens ||
@@ -114,7 +117,7 @@ export class ZAiHandler extends BaseOpenAiCompatibleProvider<string> {
114117
messages: [{ role: "system", content: systemPrompt }, ...convertedMessages],
115118
stream: true,
116119
stream_options: { include_usage: true },
117-
// Thinking is ON by default for these models, so explicitly disable it when needed.
120+
// Models with required reasoning stay enabled even when an old setting requests disable.
118121
thinking: useReasoning ? { type: "enabled" } : { type: "disabled" },
119122
reasoning_effort: reasoningEffort,
120123
tools: this.convertToolsForOpenAI(metadata?.tools),

webview-ui/src/components/settings/ApiOptions.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,7 @@ const ApiOptions = ({
722722
models={getStaticModelsForProvider(
723723
activeSelectedProvider,
724724
t("settings:labels.useCustomArn"),
725+
apiConfiguration,
725726
)}
726727
modelIdKey="apiModelId"
727728
serviceName={getProviderServiceConfig(activeSelectedProvider).serviceName}

webview-ui/src/components/settings/ThinkingBudget.tsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,22 +109,27 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod
109109
// Clamp to availableOptions so the Select trigger always renders a valid option.
110110
const storedReasoningEffort = apiConfiguration.reasoningEffort as ReasoningEffortOption | undefined
111111
const rawReasoningEffort: ReasoningEffortOption = storedReasoningEffort || defaultReasoningEffort
112+
const fallbackReasoningEffort = availableOptions.includes(defaultReasoningEffort)
113+
? defaultReasoningEffort
114+
: (availableOptions[0] ?? rawReasoningEffort)
112115
const currentReasoningEffort: ReasoningEffortOption = availableOptions.includes(rawReasoningEffort)
113116
? rawReasoningEffort
114-
: (availableOptions[0] ?? rawReasoningEffort)
117+
: fallbackReasoningEffort
115118

116119
// Set default reasoning effort when model supports it and no value is set
117120
useEffect(() => {
118-
if (isReasoningEffortSupported && !apiConfiguration.reasoningEffort) {
119-
// Only set a default if reasoning is required, otherwise leave as undefined (which maps to "disable")
120-
if (modelInfo?.requiredReasoningEffort && defaultReasoningEffort !== "disable") {
121-
setApiConfigurationField("reasoningEffort", defaultReasoningEffort as ReasoningEffortExtended, false)
122-
}
121+
if (
122+
isReasoningEffortSupported &&
123+
modelInfo?.requiredReasoningEffort &&
124+
storedReasoningEffort !== currentReasoningEffort &&
125+
currentReasoningEffort !== "disable"
126+
) {
127+
setApiConfigurationField("reasoningEffort", currentReasoningEffort as ReasoningEffortExtended, false)
123128
}
124129
}, [
125130
isReasoningEffortSupported,
126-
apiConfiguration.reasoningEffort,
127-
defaultReasoningEffort,
131+
storedReasoningEffort,
132+
currentReasoningEffort,
128133
modelInfo?.requiredReasoningEffort,
129134
setApiConfigurationField,
130135
])

webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,26 @@ describe("ThinkingBudget", () => {
289289
expect(screen.getByTestId("select")).toHaveAttribute("data-value", "low")
290290
})
291291

292+
it("should normalize an invalid disabled value to the default for required reasoning", () => {
293+
const setApiConfigurationField = vi.fn()
294+
render(
295+
<ThinkingBudget
296+
{...defaultProps}
297+
apiConfiguration={{ reasoningEffort: "disable" }}
298+
setApiConfigurationField={setApiConfigurationField}
299+
modelInfo={{
300+
...reasoningEffortModelInfo,
301+
supportsReasoningEffort: ["low", "high", "max"],
302+
requiredReasoningEffort: true,
303+
reasoningEffort: "max",
304+
}}
305+
/>,
306+
)
307+
308+
expect(screen.getByTestId("select")).toHaveAttribute("data-value", "max")
309+
expect(setApiConfigurationField).toHaveBeenCalledWith("reasoningEffort", "max", false)
310+
})
311+
292312
it("should fall back to rawReasoningEffort when availableOptions is empty", () => {
293313
// Covers the ?? rawReasoningEffort branch when availableOptions[0] is undefined
294314
render(

webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,15 @@ describe("providerModelConfig", () => {
110110
expect(defaultId.length).toBeGreaterThan(0)
111111
})
112112

113+
it("returns mainland default for Z.ai with china_api entrypoint", () => {
114+
expect(
115+
getDefaultModelIdForProvider("zai", {
116+
apiProvider: "zai",
117+
zaiApiLine: "china_api",
118+
}),
119+
).toBe(mainlandZAiDefaultModelId)
120+
})
121+
113122
it("returns international default for Z.ai with international_coding entrypoint", () => {
114123
const defaultId = getDefaultModelIdForProvider("zai", {
115124
apiProvider: "zai",
@@ -177,6 +186,30 @@ describe("providerModelConfig", () => {
177186
const models = getStaticModelsForProvider("openrouter")
178187
expect(Object.keys(models).length).toBe(0)
179188
})
189+
190+
it("shows GLM-5.3 only for Z.ai Coding Plan entrypoints", () => {
191+
const internationalCoding = getStaticModelsForProvider("zai", undefined, {
192+
apiProvider: "zai",
193+
zaiApiLine: "international_coding",
194+
})
195+
const chinaCoding = getStaticModelsForProvider("zai", undefined, {
196+
apiProvider: "zai",
197+
zaiApiLine: "china_coding",
198+
})
199+
const internationalApi = getStaticModelsForProvider("zai", undefined, {
200+
apiProvider: "zai",
201+
zaiApiLine: "international_api",
202+
})
203+
const chinaApi = getStaticModelsForProvider("zai", undefined, {
204+
apiProvider: "zai",
205+
zaiApiLine: "china_api",
206+
})
207+
208+
expect(internationalCoding).toHaveProperty("glm-5.3")
209+
expect(chinaCoding).toHaveProperty("glm-5.3")
210+
expect(internationalApi).not.toHaveProperty("glm-5.3")
211+
expect(chinaApi).not.toHaveProperty("glm-5.3")
212+
})
180213
})
181214

182215
describe("isStaticModelProvider", () => {

0 commit comments

Comments
 (0)