From 585dd317077f195057772633fb1cc4343f4603ef Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Mon, 27 Oct 2025 18:10:32 -0500 Subject: [PATCH 1/2] feat: Add supportsReasoning property for Z.ai GLM binary thinking mode - Add supportsReasoning to ModelInfo schema for binary reasoning models - Update GLM-4.5 and GLM-4.6 models to use supportsReasoning: true - Implement thinking parameter support in ZAiHandler for Deep Thinking API - Update ThinkingBudget component to show simple toggle for supportsReasoning models - Add comprehensive tests for binary reasoning functionality Closes #8465 --- packages/types/src/model.ts | 2 + packages/types/src/providers/zai.ts | 4 + src/api/providers/__tests__/zai.spec.ts | 138 ++++++++++++++++++ src/api/providers/zai.ts | 70 +++++++++ .../components/settings/ThinkingBudget.tsx | 16 ++ .../__tests__/ThinkingBudget.spec.tsx | 21 +++ 6 files changed, 251 insertions(+) diff --git a/packages/types/src/model.ts b/packages/types/src/model.ts index a97519af1f1..7691dd2a8fc 100644 --- a/packages/types/src/model.ts +++ b/packages/types/src/model.ts @@ -61,6 +61,8 @@ export const modelInfoSchema = z.object({ // Capability flag to indicate whether the model supports an output verbosity parameter supportsVerbosity: z.boolean().optional(), supportsReasoningBudget: z.boolean().optional(), + // Capability flag to indicate whether the model supports simple on/off reasoning + supportsReasoning: z.boolean().optional(), // Capability flag to indicate whether the model supports temperature parameter supportsTemperature: z.boolean().optional(), requiredReasoningBudget: z.boolean().optional(), diff --git a/packages/types/src/providers/zai.ts b/packages/types/src/providers/zai.ts index 2b156cfb51f..8e4fe5e5c7a 100644 --- a/packages/types/src/providers/zai.ts +++ b/packages/types/src/providers/zai.ts @@ -16,6 +16,7 @@ export const internationalZAiModels = { contextWindow: 131_072, supportsImages: false, supportsPromptCache: true, + supportsReasoning: true, inputPrice: 0.6, outputPrice: 2.2, cacheWritesPrice: 0, @@ -86,6 +87,7 @@ export const internationalZAiModels = { contextWindow: 200_000, supportsImages: false, supportsPromptCache: true, + supportsReasoning: true, inputPrice: 0.6, outputPrice: 2.2, cacheWritesPrice: 0, @@ -114,6 +116,7 @@ export const mainlandZAiModels = { contextWindow: 131_072, supportsImages: false, supportsPromptCache: true, + supportsReasoning: true, inputPrice: 0.29, outputPrice: 1.14, cacheWritesPrice: 0, @@ -184,6 +187,7 @@ export const mainlandZAiModels = { contextWindow: 204_800, supportsImages: false, supportsPromptCache: true, + supportsReasoning: true, inputPrice: 0.29, outputPrice: 1.14, cacheWritesPrice: 0, diff --git a/src/api/providers/__tests__/zai.spec.ts b/src/api/providers/__tests__/zai.spec.ts index ec3c9dbe0e4..3d6bb8f4ae1 100644 --- a/src/api/providers/__tests__/zai.spec.ts +++ b/src/api/providers/__tests__/zai.spec.ts @@ -295,5 +295,143 @@ describe("ZAiHandler", () => { undefined, ) }) + + describe("Reasoning functionality", () => { + it("should include thinking parameter when enableReasoningEffort is true and model supports reasoning in createMessage", async () => { + const handlerWithReasoning = new ZAiHandler({ + apiModelId: "glm-4.6", // GLM-4.6 has supportsReasoning: true + zaiApiKey: "test-zai-api-key", + zaiApiLine: "international_coding", + enableReasoningEffort: true, + }) + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + } + }) + + const systemPrompt = "Test system prompt" + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message" }] + + const messageGenerator = handlerWithReasoning.createMessage(systemPrompt, messages) + await messageGenerator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + thinking: { type: "enabled" }, + }), + undefined, + ) + }) + + it("should not include thinking parameter when enableReasoningEffort is false in createMessage", async () => { + const handlerWithoutReasoning = new ZAiHandler({ + apiModelId: "glm-4.6", // GLM-4.6 has supportsReasoning: true + zaiApiKey: "test-zai-api-key", + zaiApiLine: "international_coding", + enableReasoningEffort: false, + }) + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + } + }) + + const systemPrompt = "Test system prompt" + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message" }] + + const messageGenerator = handlerWithoutReasoning.createMessage(systemPrompt, messages) + await messageGenerator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.not.objectContaining({ + thinking: expect.anything(), + }), + undefined, + ) + }) + + it("should not include thinking parameter when model does not support reasoning in createMessage", async () => { + const handlerWithNonReasoningModel = new ZAiHandler({ + apiModelId: "glm-4-32b-0414-128k", // This model doesn't have supportsReasoning: true + zaiApiKey: "test-zai-api-key", + zaiApiLine: "international_coding", + enableReasoningEffort: true, + }) + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + } + }) + + const systemPrompt = "Test system prompt" + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message" }] + + const messageGenerator = handlerWithNonReasoningModel.createMessage(systemPrompt, messages) + await messageGenerator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.not.objectContaining({ + thinking: expect.anything(), + }), + undefined, + ) + }) + + it("should include thinking parameter when enableReasoningEffort is true and model supports reasoning in completePrompt", async () => { + const handlerWithReasoning = new ZAiHandler({ + apiModelId: "glm-4.5", // GLM-4.5 has supportsReasoning: true + zaiApiKey: "test-zai-api-key", + zaiApiLine: "international_coding", + enableReasoningEffort: true, + }) + + const expectedResponse = "This is a test response" + mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] }) + + await handlerWithReasoning.completePrompt("test prompt") + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + thinking: { type: "enabled" }, + }), + ) + }) + + it("should not include thinking parameter when enableReasoningEffort is false in completePrompt", async () => { + const handlerWithoutReasoning = new ZAiHandler({ + apiModelId: "glm-4.5", // GLM-4.5 has supportsReasoning: true + zaiApiKey: "test-zai-api-key", + zaiApiLine: "international_coding", + enableReasoningEffort: false, + }) + + const expectedResponse = "This is a test response" + mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] }) + + await handlerWithoutReasoning.completePrompt("test prompt") + + expect(mockCreate).toHaveBeenCalledWith( + expect.not.objectContaining({ + thinking: expect.anything(), + }), + ) + }) + }) }) }) diff --git a/src/api/providers/zai.ts b/src/api/providers/zai.ts index a72be571d4f..4b5fcd9fe61 100644 --- a/src/api/providers/zai.ts +++ b/src/api/providers/zai.ts @@ -10,7 +10,14 @@ import { zaiApiLineConfigs, } from "@roo-code/types" +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" + import type { ApiHandlerOptions } from "../../shared/api" +import { getModelMaxOutputTokens } from "../../shared/api" +import { convertToOpenAiMessages } from "../transform/openai-format" +import type { ApiHandlerCreateMessageMetadata } from "../index" +import { handleOpenAIError } from "./utils/openai-error-handler" import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider" @@ -30,4 +37,67 @@ export class ZAiHandler extends BaseOpenAiCompatibleProvider { defaultTemperature: ZAI_DEFAULT_TEMPERATURE, }) } + + protected override createStream( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + metadata?: ApiHandlerCreateMessageMetadata, + requestOptions?: OpenAI.RequestOptions, + ) { + const { id: model, info } = this.getModel() + + // Centralized cap: clamp to 20% of the context window (unless provider-specific exceptions apply) + const max_tokens = + getModelMaxOutputTokens({ + modelId: model, + model: info, + settings: this.options, + format: "openai", + }) ?? undefined + + const temperature = this.options.modelTemperature ?? this.defaultTemperature + + const params: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { + model, + max_tokens, + temperature, + messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)], + stream: true, + stream_options: { include_usage: true }, + } + + // Add thinking parameter if reasoning is enabled and model supports it + const { id: modelId, info: modelInfo } = this.getModel() + if (this.options.enableReasoningEffort && modelInfo.supportsReasoning) { + ;(params as any).thinking = { type: "enabled" } + } + + try { + return this.client.chat.completions.create(params, requestOptions) + } catch (error) { + throw handleOpenAIError(error, this.providerName) + } + } + + override async completePrompt(prompt: string): Promise { + const { id: modelId } = this.getModel() + + const params: OpenAI.Chat.Completions.ChatCompletionCreateParams = { + model: modelId, + messages: [{ role: "user", content: prompt }], + } + + // Add thinking parameter if reasoning is enabled and model supports it + const { info: modelInfo } = this.getModel() + if (this.options.enableReasoningEffort && modelInfo.supportsReasoning) { + ;(params as any).thinking = { type: "enabled" } + } + + try { + const response = await this.client.chat.completions.create(params) + return response.choices[0]?.message.content || "" + } catch (error) { + throw handleOpenAIError(error, this.providerName) + } + } } diff --git a/webview-ui/src/components/settings/ThinkingBudget.tsx b/webview-ui/src/components/settings/ThinkingBudget.tsx index 7a85e61e7a5..245fc241f34 100644 --- a/webview-ui/src/components/settings/ThinkingBudget.tsx +++ b/webview-ui/src/components/settings/ThinkingBudget.tsx @@ -48,6 +48,7 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod const minThinkingTokens = isGemini25Pro ? GEMINI_25_PRO_MIN_THINKING_TOKENS : 1024 // Check model capabilities + const isReasoningSupported = !!modelInfo && modelInfo.supportsReasoning const isReasoningBudgetSupported = !!modelInfo && modelInfo.supportsReasoningBudget const isReasoningBudgetRequired = !!modelInfo && modelInfo.requiredReasoningBudget const isReasoningEffortSupported = !!modelInfo && modelInfo.supportsReasoningEffort @@ -103,6 +104,21 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod return null } + // Models with supportsReasoning (binary reasoning) show a simple on/off toggle + if (isReasoningSupported) { + return ( +
+ + setApiConfigurationField("enableReasoningEffort", checked === true) + }> + {t("settings:providers.useReasoning")} + +
+ ) + } + return isReasoningBudgetSupported && !!modelInfo.maxTokens ? ( <> {!isReasoningBudgetRequired && ( diff --git a/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx b/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx index 9bc235e5f00..edd106a9614 100644 --- a/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx @@ -77,6 +77,27 @@ describe("ThinkingBudget", () => { expect(container.firstChild).toBeNull() }) + it("should render simple reasoning toggle when model has supportsReasoning (binary reasoning)", () => { + render( + , + ) + + // Should show the reasoning checkbox (translation key) + expect(screen.getByText("settings:providers.useReasoning")).toBeInTheDocument() + + // Should NOT show sliders or other complex reasoning controls + expect(screen.queryByTestId("reasoning-budget")).not.toBeInTheDocument() + expect(screen.queryByTestId("reasoning-effort")).not.toBeInTheDocument() + }) + it("should render sliders when model supports thinking", () => { render() From 8342ce9daebac1f58e78c0dc984894ca1f9ebbd6 Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Mon, 27 Oct 2025 20:48:11 -0500 Subject: [PATCH 2/2] refactor: rename supportsReasoning to supportsReasoningBinary for clarity - Rename supportsReasoning -> supportsReasoningBinary in model schema - Update Z.AI GLM model configurations to use supportsReasoningBinary - Update Z.AI provider logic in createStream and completePrompt methods - Update ThinkingBudget UI component and tests - Update all test comments and expectations This change improves naming clarity by distinguishing between: - supportsReasoningBinary: Simple on/off reasoning toggle - supportsReasoningBudget: Advanced reasoning with token budget controls - supportsReasoningEffort: Advanced reasoning with effort levels --- packages/types/src/model.ts | 4 ++-- packages/types/src/providers/zai.ts | 8 ++++---- src/api/providers/__tests__/zai.spec.ts | 10 +++++----- src/api/providers/zai.ts | 4 ++-- webview-ui/src/components/settings/ThinkingBudget.tsx | 4 ++-- .../settings/__tests__/ThinkingBudget.spec.tsx | 4 ++-- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/types/src/model.ts b/packages/types/src/model.ts index 7691dd2a8fc..b52b3b7f13f 100644 --- a/packages/types/src/model.ts +++ b/packages/types/src/model.ts @@ -61,8 +61,8 @@ export const modelInfoSchema = z.object({ // Capability flag to indicate whether the model supports an output verbosity parameter supportsVerbosity: z.boolean().optional(), supportsReasoningBudget: z.boolean().optional(), - // Capability flag to indicate whether the model supports simple on/off reasoning - supportsReasoning: z.boolean().optional(), + // Capability flag to indicate whether the model supports simple on/off binary reasoning + supportsReasoningBinary: z.boolean().optional(), // Capability flag to indicate whether the model supports temperature parameter supportsTemperature: z.boolean().optional(), requiredReasoningBudget: z.boolean().optional(), diff --git a/packages/types/src/providers/zai.ts b/packages/types/src/providers/zai.ts index 8e4fe5e5c7a..2db77322301 100644 --- a/packages/types/src/providers/zai.ts +++ b/packages/types/src/providers/zai.ts @@ -16,7 +16,7 @@ export const internationalZAiModels = { contextWindow: 131_072, supportsImages: false, supportsPromptCache: true, - supportsReasoning: true, + supportsReasoningBinary: true, inputPrice: 0.6, outputPrice: 2.2, cacheWritesPrice: 0, @@ -87,7 +87,7 @@ export const internationalZAiModels = { contextWindow: 200_000, supportsImages: false, supportsPromptCache: true, - supportsReasoning: true, + supportsReasoningBinary: true, inputPrice: 0.6, outputPrice: 2.2, cacheWritesPrice: 0, @@ -116,7 +116,7 @@ export const mainlandZAiModels = { contextWindow: 131_072, supportsImages: false, supportsPromptCache: true, - supportsReasoning: true, + supportsReasoningBinary: true, inputPrice: 0.29, outputPrice: 1.14, cacheWritesPrice: 0, @@ -187,7 +187,7 @@ export const mainlandZAiModels = { contextWindow: 204_800, supportsImages: false, supportsPromptCache: true, - supportsReasoning: true, + supportsReasoningBinary: true, inputPrice: 0.29, outputPrice: 1.14, cacheWritesPrice: 0, diff --git a/src/api/providers/__tests__/zai.spec.ts b/src/api/providers/__tests__/zai.spec.ts index 3d6bb8f4ae1..9db5350080e 100644 --- a/src/api/providers/__tests__/zai.spec.ts +++ b/src/api/providers/__tests__/zai.spec.ts @@ -299,7 +299,7 @@ describe("ZAiHandler", () => { describe("Reasoning functionality", () => { it("should include thinking parameter when enableReasoningEffort is true and model supports reasoning in createMessage", async () => { const handlerWithReasoning = new ZAiHandler({ - apiModelId: "glm-4.6", // GLM-4.6 has supportsReasoning: true + apiModelId: "glm-4.6", // GLM-4.6 has supportsReasoningBinary: true zaiApiKey: "test-zai-api-key", zaiApiLine: "international_coding", enableReasoningEffort: true, @@ -331,7 +331,7 @@ describe("ZAiHandler", () => { it("should not include thinking parameter when enableReasoningEffort is false in createMessage", async () => { const handlerWithoutReasoning = new ZAiHandler({ - apiModelId: "glm-4.6", // GLM-4.6 has supportsReasoning: true + apiModelId: "glm-4.6", // GLM-4.6 has supportsReasoningBinary: true zaiApiKey: "test-zai-api-key", zaiApiLine: "international_coding", enableReasoningEffort: false, @@ -363,7 +363,7 @@ describe("ZAiHandler", () => { it("should not include thinking parameter when model does not support reasoning in createMessage", async () => { const handlerWithNonReasoningModel = new ZAiHandler({ - apiModelId: "glm-4-32b-0414-128k", // This model doesn't have supportsReasoning: true + apiModelId: "glm-4-32b-0414-128k", // This model doesn't have supportsReasoningBinary: true zaiApiKey: "test-zai-api-key", zaiApiLine: "international_coding", enableReasoningEffort: true, @@ -395,7 +395,7 @@ describe("ZAiHandler", () => { it("should include thinking parameter when enableReasoningEffort is true and model supports reasoning in completePrompt", async () => { const handlerWithReasoning = new ZAiHandler({ - apiModelId: "glm-4.5", // GLM-4.5 has supportsReasoning: true + apiModelId: "glm-4.5", // GLM-4.5 has supportsReasoningBinary: true zaiApiKey: "test-zai-api-key", zaiApiLine: "international_coding", enableReasoningEffort: true, @@ -415,7 +415,7 @@ describe("ZAiHandler", () => { it("should not include thinking parameter when enableReasoningEffort is false in completePrompt", async () => { const handlerWithoutReasoning = new ZAiHandler({ - apiModelId: "glm-4.5", // GLM-4.5 has supportsReasoning: true + apiModelId: "glm-4.5", // GLM-4.5 has supportsReasoningBinary: true zaiApiKey: "test-zai-api-key", zaiApiLine: "international_coding", enableReasoningEffort: false, diff --git a/src/api/providers/zai.ts b/src/api/providers/zai.ts index 4b5fcd9fe61..cc83945e48a 100644 --- a/src/api/providers/zai.ts +++ b/src/api/providers/zai.ts @@ -68,7 +68,7 @@ export class ZAiHandler extends BaseOpenAiCompatibleProvider { // Add thinking parameter if reasoning is enabled and model supports it const { id: modelId, info: modelInfo } = this.getModel() - if (this.options.enableReasoningEffort && modelInfo.supportsReasoning) { + if (this.options.enableReasoningEffort && modelInfo.supportsReasoningBinary) { ;(params as any).thinking = { type: "enabled" } } @@ -89,7 +89,7 @@ export class ZAiHandler extends BaseOpenAiCompatibleProvider { // Add thinking parameter if reasoning is enabled and model supports it const { info: modelInfo } = this.getModel() - if (this.options.enableReasoningEffort && modelInfo.supportsReasoning) { + if (this.options.enableReasoningEffort && modelInfo.supportsReasoningBinary) { ;(params as any).thinking = { type: "enabled" } } diff --git a/webview-ui/src/components/settings/ThinkingBudget.tsx b/webview-ui/src/components/settings/ThinkingBudget.tsx index 245fc241f34..2716d422206 100644 --- a/webview-ui/src/components/settings/ThinkingBudget.tsx +++ b/webview-ui/src/components/settings/ThinkingBudget.tsx @@ -48,7 +48,7 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod const minThinkingTokens = isGemini25Pro ? GEMINI_25_PRO_MIN_THINKING_TOKENS : 1024 // Check model capabilities - const isReasoningSupported = !!modelInfo && modelInfo.supportsReasoning + const isReasoningSupported = !!modelInfo && modelInfo.supportsReasoningBinary const isReasoningBudgetSupported = !!modelInfo && modelInfo.supportsReasoningBudget const isReasoningBudgetRequired = !!modelInfo && modelInfo.requiredReasoningBudget const isReasoningEffortSupported = !!modelInfo && modelInfo.supportsReasoningEffort @@ -104,7 +104,7 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod return null } - // Models with supportsReasoning (binary reasoning) show a simple on/off toggle + // Models with supportsReasoningBinary (binary reasoning) show a simple on/off toggle if (isReasoningSupported) { return (
diff --git a/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx b/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx index edd106a9614..f7d978e8818 100644 --- a/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx @@ -77,13 +77,13 @@ describe("ThinkingBudget", () => { expect(container.firstChild).toBeNull() }) - it("should render simple reasoning toggle when model has supportsReasoning (binary reasoning)", () => { + it("should render simple reasoning toggle when model has supportsReasoningBinary (binary reasoning)", () => { render(