Skip to content

Commit dc08f53

Browse files
daniel-lxsPrasangAPrajapati
authored andcommitted
feat: Add supportsReasoning property for Z.ai GLM binary thinking mode (RooCodeInc#8872)
* 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 RooCodeInc#8465 * 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
1 parent d7ae715 commit dc08f53

File tree

6 files changed

+251
-0
lines changed

6 files changed

+251
-0
lines changed

packages/types/src/model.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ export const modelInfoSchema = z.object({
6161
// Capability flag to indicate whether the model supports an output verbosity parameter
6262
supportsVerbosity: z.boolean().optional(),
6363
supportsReasoningBudget: z.boolean().optional(),
64+
// Capability flag to indicate whether the model supports simple on/off binary reasoning
65+
supportsReasoningBinary: z.boolean().optional(),
6466
// Capability flag to indicate whether the model supports temperature parameter
6567
supportsTemperature: z.boolean().optional(),
6668
requiredReasoningBudget: z.boolean().optional(),

packages/types/src/providers/zai.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const internationalZAiModels = {
1616
contextWindow: 131_072,
1717
supportsImages: false,
1818
supportsPromptCache: true,
19+
supportsReasoningBinary: true,
1920
inputPrice: 0.6,
2021
outputPrice: 2.2,
2122
cacheWritesPrice: 0,
@@ -86,6 +87,7 @@ export const internationalZAiModels = {
8687
contextWindow: 200_000,
8788
supportsImages: false,
8889
supportsPromptCache: true,
90+
supportsReasoningBinary: true,
8991
inputPrice: 0.6,
9092
outputPrice: 2.2,
9193
cacheWritesPrice: 0,
@@ -114,6 +116,7 @@ export const mainlandZAiModels = {
114116
contextWindow: 131_072,
115117
supportsImages: false,
116118
supportsPromptCache: true,
119+
supportsReasoningBinary: true,
117120
inputPrice: 0.29,
118121
outputPrice: 1.14,
119122
cacheWritesPrice: 0,
@@ -184,6 +187,7 @@ export const mainlandZAiModels = {
184187
contextWindow: 204_800,
185188
supportsImages: false,
186189
supportsPromptCache: true,
190+
supportsReasoningBinary: true,
187191
inputPrice: 0.29,
188192
outputPrice: 1.14,
189193
cacheWritesPrice: 0,

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

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,5 +295,143 @@ describe("ZAiHandler", () => {
295295
undefined,
296296
)
297297
})
298+
299+
describe("Reasoning functionality", () => {
300+
it("should include thinking parameter when enableReasoningEffort is true and model supports reasoning in createMessage", async () => {
301+
const handlerWithReasoning = new ZAiHandler({
302+
apiModelId: "glm-4.6", // GLM-4.6 has supportsReasoningBinary: true
303+
zaiApiKey: "test-zai-api-key",
304+
zaiApiLine: "international_coding",
305+
enableReasoningEffort: true,
306+
})
307+
308+
mockCreate.mockImplementationOnce(() => {
309+
return {
310+
[Symbol.asyncIterator]: () => ({
311+
async next() {
312+
return { done: true }
313+
},
314+
}),
315+
}
316+
})
317+
318+
const systemPrompt = "Test system prompt"
319+
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message" }]
320+
321+
const messageGenerator = handlerWithReasoning.createMessage(systemPrompt, messages)
322+
await messageGenerator.next()
323+
324+
expect(mockCreate).toHaveBeenCalledWith(
325+
expect.objectContaining({
326+
thinking: { type: "enabled" },
327+
}),
328+
undefined,
329+
)
330+
})
331+
332+
it("should not include thinking parameter when enableReasoningEffort is false in createMessage", async () => {
333+
const handlerWithoutReasoning = new ZAiHandler({
334+
apiModelId: "glm-4.6", // GLM-4.6 has supportsReasoningBinary: true
335+
zaiApiKey: "test-zai-api-key",
336+
zaiApiLine: "international_coding",
337+
enableReasoningEffort: false,
338+
})
339+
340+
mockCreate.mockImplementationOnce(() => {
341+
return {
342+
[Symbol.asyncIterator]: () => ({
343+
async next() {
344+
return { done: true }
345+
},
346+
}),
347+
}
348+
})
349+
350+
const systemPrompt = "Test system prompt"
351+
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message" }]
352+
353+
const messageGenerator = handlerWithoutReasoning.createMessage(systemPrompt, messages)
354+
await messageGenerator.next()
355+
356+
expect(mockCreate).toHaveBeenCalledWith(
357+
expect.not.objectContaining({
358+
thinking: expect.anything(),
359+
}),
360+
undefined,
361+
)
362+
})
363+
364+
it("should not include thinking parameter when model does not support reasoning in createMessage", async () => {
365+
const handlerWithNonReasoningModel = new ZAiHandler({
366+
apiModelId: "glm-4-32b-0414-128k", // This model doesn't have supportsReasoningBinary: true
367+
zaiApiKey: "test-zai-api-key",
368+
zaiApiLine: "international_coding",
369+
enableReasoningEffort: true,
370+
})
371+
372+
mockCreate.mockImplementationOnce(() => {
373+
return {
374+
[Symbol.asyncIterator]: () => ({
375+
async next() {
376+
return { done: true }
377+
},
378+
}),
379+
}
380+
})
381+
382+
const systemPrompt = "Test system prompt"
383+
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message" }]
384+
385+
const messageGenerator = handlerWithNonReasoningModel.createMessage(systemPrompt, messages)
386+
await messageGenerator.next()
387+
388+
expect(mockCreate).toHaveBeenCalledWith(
389+
expect.not.objectContaining({
390+
thinking: expect.anything(),
391+
}),
392+
undefined,
393+
)
394+
})
395+
396+
it("should include thinking parameter when enableReasoningEffort is true and model supports reasoning in completePrompt", async () => {
397+
const handlerWithReasoning = new ZAiHandler({
398+
apiModelId: "glm-4.5", // GLM-4.5 has supportsReasoningBinary: true
399+
zaiApiKey: "test-zai-api-key",
400+
zaiApiLine: "international_coding",
401+
enableReasoningEffort: true,
402+
})
403+
404+
const expectedResponse = "This is a test response"
405+
mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] })
406+
407+
await handlerWithReasoning.completePrompt("test prompt")
408+
409+
expect(mockCreate).toHaveBeenCalledWith(
410+
expect.objectContaining({
411+
thinking: { type: "enabled" },
412+
}),
413+
)
414+
})
415+
416+
it("should not include thinking parameter when enableReasoningEffort is false in completePrompt", async () => {
417+
const handlerWithoutReasoning = new ZAiHandler({
418+
apiModelId: "glm-4.5", // GLM-4.5 has supportsReasoningBinary: true
419+
zaiApiKey: "test-zai-api-key",
420+
zaiApiLine: "international_coding",
421+
enableReasoningEffort: false,
422+
})
423+
424+
const expectedResponse = "This is a test response"
425+
mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] })
426+
427+
await handlerWithoutReasoning.completePrompt("test prompt")
428+
429+
expect(mockCreate).toHaveBeenCalledWith(
430+
expect.not.objectContaining({
431+
thinking: expect.anything(),
432+
}),
433+
)
434+
})
435+
})
298436
})
299437
})

src/api/providers/zai.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@ import {
1010
zaiApiLineConfigs,
1111
} from "@roo-code/types"
1212

13+
import { Anthropic } from "@anthropic-ai/sdk"
14+
import OpenAI from "openai"
15+
1316
import type { ApiHandlerOptions } from "../../shared/api"
17+
import { getModelMaxOutputTokens } from "../../shared/api"
18+
import { convertToOpenAiMessages } from "../transform/openai-format"
19+
import type { ApiHandlerCreateMessageMetadata } from "../index"
20+
import { handleOpenAIError } from "./utils/openai-error-handler"
1421

1522
import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider"
1623

@@ -30,4 +37,67 @@ export class ZAiHandler extends BaseOpenAiCompatibleProvider<string> {
3037
defaultTemperature: ZAI_DEFAULT_TEMPERATURE,
3138
})
3239
}
40+
41+
protected override createStream(
42+
systemPrompt: string,
43+
messages: Anthropic.Messages.MessageParam[],
44+
metadata?: ApiHandlerCreateMessageMetadata,
45+
requestOptions?: OpenAI.RequestOptions,
46+
) {
47+
const { id: model, info } = this.getModel()
48+
49+
// Centralized cap: clamp to 20% of the context window (unless provider-specific exceptions apply)
50+
const max_tokens =
51+
getModelMaxOutputTokens({
52+
modelId: model,
53+
model: info,
54+
settings: this.options,
55+
format: "openai",
56+
}) ?? undefined
57+
58+
const temperature = this.options.modelTemperature ?? this.defaultTemperature
59+
60+
const params: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
61+
model,
62+
max_tokens,
63+
temperature,
64+
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
65+
stream: true,
66+
stream_options: { include_usage: true },
67+
}
68+
69+
// Add thinking parameter if reasoning is enabled and model supports it
70+
const { id: modelId, info: modelInfo } = this.getModel()
71+
if (this.options.enableReasoningEffort && modelInfo.supportsReasoningBinary) {
72+
;(params as any).thinking = { type: "enabled" }
73+
}
74+
75+
try {
76+
return this.client.chat.completions.create(params, requestOptions)
77+
} catch (error) {
78+
throw handleOpenAIError(error, this.providerName)
79+
}
80+
}
81+
82+
override async completePrompt(prompt: string): Promise<string> {
83+
const { id: modelId } = this.getModel()
84+
85+
const params: OpenAI.Chat.Completions.ChatCompletionCreateParams = {
86+
model: modelId,
87+
messages: [{ role: "user", content: prompt }],
88+
}
89+
90+
// Add thinking parameter if reasoning is enabled and model supports it
91+
const { info: modelInfo } = this.getModel()
92+
if (this.options.enableReasoningEffort && modelInfo.supportsReasoningBinary) {
93+
;(params as any).thinking = { type: "enabled" }
94+
}
95+
96+
try {
97+
const response = await this.client.chat.completions.create(params)
98+
return response.choices[0]?.message.content || ""
99+
} catch (error) {
100+
throw handleOpenAIError(error, this.providerName)
101+
}
102+
}
33103
}

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod
4848
const minThinkingTokens = isGemini25Pro ? GEMINI_25_PRO_MIN_THINKING_TOKENS : 1024
4949

5050
// Check model capabilities
51+
const isReasoningSupported = !!modelInfo && modelInfo.supportsReasoningBinary
5152
const isReasoningBudgetSupported = !!modelInfo && modelInfo.supportsReasoningBudget
5253
const isReasoningBudgetRequired = !!modelInfo && modelInfo.requiredReasoningBudget
5354
const isReasoningEffortSupported = !!modelInfo && modelInfo.supportsReasoningEffort
@@ -103,6 +104,21 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod
103104
return null
104105
}
105106

107+
// Models with supportsReasoningBinary (binary reasoning) show a simple on/off toggle
108+
if (isReasoningSupported) {
109+
return (
110+
<div className="flex flex-col gap-1">
111+
<Checkbox
112+
checked={enableReasoningEffort}
113+
onChange={(checked: boolean) =>
114+
setApiConfigurationField("enableReasoningEffort", checked === true)
115+
}>
116+
{t("settings:providers.useReasoning")}
117+
</Checkbox>
118+
</div>
119+
)
120+
}
121+
106122
return isReasoningBudgetSupported && !!modelInfo.maxTokens ? (
107123
<>
108124
{!isReasoningBudgetRequired && (

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,27 @@ describe("ThinkingBudget", () => {
7777
expect(container.firstChild).toBeNull()
7878
})
7979

80+
it("should render simple reasoning toggle when model has supportsReasoningBinary (binary reasoning)", () => {
81+
render(
82+
<ThinkingBudget
83+
{...defaultProps}
84+
modelInfo={{
85+
...mockModelInfo,
86+
supportsReasoningBinary: true,
87+
supportsReasoningBudget: false,
88+
supportsReasoningEffort: false,
89+
}}
90+
/>,
91+
)
92+
93+
// Should show the reasoning checkbox (translation key)
94+
expect(screen.getByText("settings:providers.useReasoning")).toBeInTheDocument()
95+
96+
// Should NOT show sliders or other complex reasoning controls
97+
expect(screen.queryByTestId("reasoning-budget")).not.toBeInTheDocument()
98+
expect(screen.queryByTestId("reasoning-effort")).not.toBeInTheDocument()
99+
})
100+
80101
it("should render sliders when model supports thinking", () => {
81102
render(<ThinkingBudget {...defaultProps} />)
82103

0 commit comments

Comments
 (0)