diff --git a/.changeset/zai-glm5-default-lines.md b/.changeset/zai-glm5-default-lines.md new file mode 100644 index 00000000000..4516f8c6c3b --- /dev/null +++ b/.changeset/zai-glm5-default-lines.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Add support for GLM 5 and set Z.ai default to `glm-5` and align Z.ai API line model selection in VS Code and webview settings diff --git a/packages/types/src/providers/zai.ts b/packages/types/src/providers/zai.ts index 8f6eb8774c7..79e961bdea7 100644 --- a/packages/types/src/providers/zai.ts +++ b/packages/types/src/providers/zai.ts @@ -6,11 +6,12 @@ import { ZaiApiLine } from "../provider-settings.js" // https://docs.z.ai/guides/llm/glm-4.5 // https://docs.z.ai/guides/llm/glm-4.6 // https://docs.z.ai/guides/llm/glm-4.7 +// https://docs.z.ai/guides/llm/glm-5 // kilocode_change // https://docs.z.ai/guides/overview/pricing // https://bigmodel.cn/pricing export type InternationalZAiModelId = keyof typeof internationalZAiModels -export const internationalZAiDefaultModelId: InternationalZAiModelId = "glm-4.7" +export const internationalZAiDefaultModelId: InternationalZAiModelId = "glm-5" // kilocode_change export const internationalZAiModels = { "glm-4.5": { maxTokens: 16_384, @@ -157,6 +158,24 @@ export const internationalZAiModels = { preferredIndex: 1, }, // kilocode_change start + "glm-5": { + maxTokens: 131_072, + contextWindow: 200_000, + supportsImages: false, + supportsPromptCache: true, + supportsNativeTools: true, + defaultToolProtocol: "native", + supportsReasoningEffort: ["disable", "medium"], + reasoningEffort: "medium", + preserveReasoning: true, + inputPrice: 1, + outputPrice: 3.2, + cacheWritesPrice: 0, + cacheReadsPrice: 0.2, + description: + "GLM-5 is Z.AI's flagship text model with 200K context, 128K max output, thinking mode, function calling, and context caching.", + preferredIndex: 0, + }, "glm-4.7-flash": { maxTokens: 16_384, contextWindow: 200_000, @@ -189,7 +208,7 @@ export const internationalZAiModels = { } as const satisfies Record export type MainlandZAiModelId = keyof typeof mainlandZAiModels -export const mainlandZAiDefaultModelId: MainlandZAiModelId = "glm-4.7" +export const mainlandZAiDefaultModelId: MainlandZAiModelId = "glm-5" // kilocode_change export const mainlandZAiModels = { "glm-4.5": { maxTokens: 16_384, @@ -306,6 +325,24 @@ export const mainlandZAiModels = { preferredIndex: 1, }, // kilocode_change start + "glm-5": { + maxTokens: 131_072, + contextWindow: 200_000, + supportsImages: false, + supportsPromptCache: true, + supportsNativeTools: true, + defaultToolProtocol: "native", + supportsReasoningEffort: ["disable", "medium"], + reasoningEffort: "medium", + preserveReasoning: true, + inputPrice: 0.57, + outputPrice: 2.57, + cacheWritesPrice: 0, + cacheReadsPrice: 0.14, + description: + "GLM-5 is Z.AI's flagship text model with 200K context, 128K max output, thinking mode, function calling, and context caching.", + preferredIndex: 0, + }, "glm-4.7-flash": { maxTokens: 16_384, contextWindow: 204_800, diff --git a/src/api/providers/__tests__/zai.spec.ts b/src/api/providers/__tests__/zai.spec.ts index 60b8ba82cf5..369673c981c 100644 --- a/src/api/providers/__tests__/zai.spec.ts +++ b/src/api/providers/__tests__/zai.spec.ts @@ -108,6 +108,25 @@ describe("ZAiHandler", () => { expect(model.info.preserveReasoning).toBe(true) }) + // kilocode_change start + it("should return GLM-5 international model with documented limits", () => { + const testModelId: InternationalZAiModelId = "glm-5" + const handlerWithModel = new ZAiHandler({ + apiModelId: testModelId, + zaiApiKey: "test-zai-api-key", + zaiApiLine: "international_coding", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual(internationalZAiModels[testModelId]) + expect(model.info.contextWindow).toBe(200_000) + expect(model.info.maxTokens).toBe(131_072) + expect(model.info.supportsReasoningEffort).toEqual(["disable", "medium"]) + expect(model.info.reasoningEffort).toBe("medium") + expect(model.info.preserveReasoning).toBe(true) + }) + // kilocode_change end + it("should return GLM-4.5v international model with vision support", () => { const testModelId: InternationalZAiModelId = "glm-4.5v" const handlerWithModel = new ZAiHandler({ @@ -203,6 +222,25 @@ describe("ZAiHandler", () => { expect(model.info.reasoningEffort).toBe("medium") expect(model.info.preserveReasoning).toBe(true) }) + + // kilocode_change start + it("should return GLM-5 China model with documented limits", () => { + const testModelId: MainlandZAiModelId = "glm-5" + const handlerWithModel = new ZAiHandler({ + apiModelId: testModelId, + zaiApiKey: "test-zai-api-key", + zaiApiLine: "china_coding", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual(mainlandZAiModels[testModelId]) + expect(model.info.contextWindow).toBe(200_000) + expect(model.info.maxTokens).toBe(131_072) + expect(model.info.supportsReasoningEffort).toEqual(["disable", "medium"]) + expect(model.info.reasoningEffort).toBe("medium") + expect(model.info.preserveReasoning).toBe(true) + }) + // kilocode_change end }) describe("International API", () => { @@ -242,6 +280,23 @@ describe("ZAiHandler", () => { expect(model.id).toBe(testModelId) expect(model.info).toEqual(internationalZAiModels[testModelId]) }) + + // kilocode_change start + it("should return GLM-5 international API model with documented limits", () => { + const testModelId: InternationalZAiModelId = "glm-5" + const handlerWithModel = new ZAiHandler({ + apiModelId: testModelId, + zaiApiKey: "test-zai-api-key", + zaiApiLine: "international_api", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual(internationalZAiModels[testModelId]) + expect(model.info.contextWindow).toBe(200_000) + expect(model.info.maxTokens).toBe(131_072) + expect(model.info.supportsReasoningEffort).toEqual(["disable", "medium"]) + }) + // kilocode_change end }) describe("China API", () => { @@ -281,6 +336,23 @@ describe("ZAiHandler", () => { expect(model.id).toBe(testModelId) expect(model.info).toEqual(mainlandZAiModels[testModelId]) }) + + // kilocode_change start + it("should return GLM-5 China API model with documented limits", () => { + const testModelId: MainlandZAiModelId = "glm-5" + const handlerWithModel = new ZAiHandler({ + apiModelId: testModelId, + zaiApiKey: "test-zai-api-key", + zaiApiLine: "china_api", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual(mainlandZAiModels[testModelId]) + expect(model.info.contextWindow).toBe(200_000) + expect(model.info.maxTokens).toBe(131_072) + expect(model.info.supportsReasoningEffort).toEqual(["disable", "medium"]) + }) + // kilocode_change end }) describe("Default behavior", () => { @@ -414,7 +486,8 @@ describe("ZAiHandler", () => { }) }) - describe("GLM-4.7 Thinking Mode", () => { + // kilocode_change start + describe("Z.ai Thinking Mode", () => { it("should enable thinking by default for GLM-4.7 (default reasoningEffort is medium)", async () => { const handlerWithModel = new ZAiHandler({ apiModelId: "glm-4.7", @@ -507,6 +580,64 @@ describe("ZAiHandler", () => { ) }) + it("should enable thinking by default for GLM-5 (default reasoningEffort is medium)", async () => { + const handlerWithModel = new ZAiHandler({ + apiModelId: "glm-5", + zaiApiKey: "test-zai-api-key", + zaiApiLine: "international_coding", + }) + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + } + }) + + const messageGenerator = handlerWithModel.createMessage("system prompt", []) + await messageGenerator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "glm-5", + thinking: { type: "enabled" }, + }), + ) + }) + + it("should disable thinking for GLM-5 when reasoningEffort is set to disable", async () => { + const handlerWithModel = new ZAiHandler({ + apiModelId: "glm-5", + zaiApiKey: "test-zai-api-key", + zaiApiLine: "international_coding", + enableReasoningEffort: true, + reasoningEffort: "disable", + }) + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + } + }) + + const messageGenerator = handlerWithModel.createMessage("system prompt", []) + await messageGenerator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "glm-5", + thinking: { type: "disabled" }, + }), + ) + }) + it("should NOT add thinking parameter for non-thinking models like GLM-4.6", async () => { const handlerWithModel = new ZAiHandler({ apiModelId: "glm-4.6", @@ -532,4 +663,5 @@ describe("ZAiHandler", () => { expect(callArgs.thinking).toBeUndefined() }) }) + // kilocode_change end }) diff --git a/src/api/providers/zai.ts b/src/api/providers/zai.ts index c7bf6d635e8..1f60457e17f 100644 --- a/src/api/providers/zai.ts +++ b/src/api/providers/zai.ts @@ -39,10 +39,11 @@ export class ZAiHandler extends BaseOpenAiCompatibleProvider { }) } + // kilocode_change start /** - * Override createStream to handle GLM-4.7's thinking mode. - * GLM-4.7 has thinking enabled by default in the API, so we need to - * explicitly send { type: "disabled" } when the user turns off reasoning. + * Override createStream to handle Z.ai models with thinking mode. + * Thinking-capable models have reasoning enabled by default in the API, + * so we explicitly send { type: "disabled" } when users turn reasoning off. */ protected override createStream( systemPrompt: string, @@ -50,13 +51,13 @@ export class ZAiHandler extends BaseOpenAiCompatibleProvider { metadata?: ApiHandlerCreateMessageMetadata, requestOptions?: OpenAI.RequestOptions, ) { - const { id: modelId, info } = this.getModel() + const { info } = this.getModel() - // Check if this is a GLM-4.7 model with thinking support - const isThinkingModel = modelId === "glm-4.7" && Array.isArray(info.supportsReasoningEffort) + // Thinking models advertise explicit reasoning effort support. + const isThinkingModel = Array.isArray(info.supportsReasoningEffort) if (isThinkingModel) { - // For GLM-4.7, thinking is ON by default in the API. + // For thinking-enabled models, thinking is ON by default in the API. // We need to explicitly disable it when reasoning is off. const useReasoning = shouldUseReasoningEffort({ model: info, settings: this.options }) @@ -67,9 +68,11 @@ export class ZAiHandler extends BaseOpenAiCompatibleProvider { // For non-thinking models, use the default behavior return super.createStream(systemPrompt, messages, metadata, requestOptions) } + // kilocode_change end + // kilocode_change start /** - * Creates a stream with explicit thinking control for GLM-4.7 + * Creates a stream with explicit thinking control for Z.ai thinking models. */ private createStreamWithThinking( systemPrompt: string, @@ -99,7 +102,7 @@ export class ZAiHandler extends BaseOpenAiCompatibleProvider { messages: [{ role: "system", content: systemPrompt }, ...convertedMessages], stream: true, stream_options: { include_usage: true }, - // For GLM-4.7: thinking is ON by default, so we explicitly disable when needed + // Thinking is ON by default, so we explicitly disable when needed. thinking: useReasoning ? { type: "enabled" } : { type: "disabled" }, ...(metadata?.tools && { tools: this.convertToolsForOpenAI(metadata.tools) }), ...(metadata?.tool_choice && { tool_choice: metadata.tool_choice }), @@ -110,4 +113,5 @@ export class ZAiHandler extends BaseOpenAiCompatibleProvider { return this.client.chat.completions.create(params) } + // kilocode_change end } diff --git a/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts b/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts index 02d6c16e87c..604d9f773d3 100644 --- a/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts +++ b/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts @@ -117,4 +117,11 @@ describe("getOptionsForProvider", () => { const result = getOptionsForProvider("zai", { zaiApiLine: "china_coding" }) expect(result).toEqual({ isChina: true }) }) + + // kilocode_change start + it("returns isChina: true for zai provider with china_api apiConfiguration", () => { + const result = getOptionsForProvider("zai", { zaiApiLine: "china_api" }) + expect(result).toEqual({ isChina: true }) + }) + // kilocode_change end }) diff --git a/webview-ui/src/components/kilocode/hooks/useProviderModels.ts b/webview-ui/src/components/kilocode/hooks/useProviderModels.ts index b89ccd5e1ca..6160472d4bd 100644 --- a/webview-ui/src/components/kilocode/hooks/useProviderModels.ts +++ b/webview-ui/src/components/kilocode/hooks/useProviderModels.ts @@ -357,7 +357,12 @@ export const getOptionsForProvider = (provider: ProviderName, apiConfiguration?: switch (provider) { case "zai": // Determine which Z.AI model set to use based on the API line configuration - return { isChina: apiConfiguration?.zaiApiLine === "china_coding" } + // kilocode_change start + return { + isChina: + apiConfiguration?.zaiApiLine === "china_coding" || apiConfiguration?.zaiApiLine === "china_api", + } + // kilocode_change end default: return {} } diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 7506f556363..c82cc991d95 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -460,7 +460,8 @@ const ApiOptions = ({ zai: { field: "apiModelId", default: - apiConfiguration.zaiApiLine === "china_coding" + // kilocode_change - china_api uses mainland model catalog too. + apiConfiguration.zaiApiLine === "china_coding" || apiConfiguration.zaiApiLine === "china_api" ? mainlandZAiDefaultModelId : internationalZAiDefaultModelId, }, diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx index 1f42dda9d6e..e975a3356dd 100644 --- a/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx @@ -3,7 +3,12 @@ import { render, screen, fireEvent } from "@/utils/test-utils" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" -import { type ModelInfo, type ProviderSettings, openAiModelInfoSaneDefaults } from "@roo-code/types" +import { + type ModelInfo, + type ProviderSettings, + mainlandZAiDefaultModelId, + openAiModelInfoSaneDefaults, +} from "@roo-code/types" import { openAiCodexDefaultModelId } from "@roo-code/types" import * as ExtensionStateContext from "@src/context/ExtensionStateContext" @@ -339,6 +344,30 @@ describe("ApiOptions", () => { expect(mockSetApiConfigurationField).toHaveBeenCalledWith("apiModelId", openAiCodexDefaultModelId, false) }) + // kilocode_change start + it("resets model to mainland Z.ai default when switching to Z.ai with china_api line", () => { + const mockSetApiConfigurationField = vi.fn() + + renderApiOptions({ + apiConfiguration: { + apiProvider: "anthropic", + apiModelId: "claude-3-5-sonnet-20241022", + zaiApiLine: "china_api", + }, + setApiConfigurationField: mockSetApiConfigurationField, + }) + + const providerSelectContainer = screen.getByTestId("provider-select") + const providerSelect = providerSelectContainer.querySelector("select") as HTMLSelectElement + expect(providerSelect).toBeInTheDocument() + + fireEvent.change(providerSelect, { target: { value: "zai" } }) + + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("apiProvider", "zai") + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("apiModelId", mainlandZAiDefaultModelId, false) + }) + // kilocode_change end + it("hides kimi-for-coding from model options when Moonshot endpoint is not coding", () => { renderApiOptions({ apiConfiguration: { diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 2f159b0d18d..f8225340aa2 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -336,7 +336,8 @@ function getSelectedModel({ return { id, info } } case "zai": { - const isChina = apiConfiguration.zaiApiLine === "china_coding" + // kilocode_change - china_api uses mainland model catalog too. + const isChina = apiConfiguration.zaiApiLine === "china_coding" || apiConfiguration.zaiApiLine === "china_api" const models = isChina ? mainlandZAiModels : internationalZAiModels const defaultModelId = getProviderDefaultModelId(provider, { isChina }) const id = apiConfiguration.apiModelId ?? defaultModelId