Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 96b6845

Browse files
committed
fix: combine tool call sync fix with GLM model detection for issue #11071
This PR combines: 1. PR #11093 fix: NativeToolCallParser processFinishReason hasStarted check 2. GLM model detection utility for LM Studio and OpenAI-compatible providers 3. mergeToolResultText optimization for GLM models 4. Disable parallel_tool_calls for GLM models 5. GLM-4.7 thinking parameter support 6. Diagnostic logging for GLM detection Closes #11071
1 parent cc86049 commit 96b6845

6 files changed

Lines changed: 677 additions & 9 deletions

File tree

src/api/providers/base-openai-compatible-provider.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ import { type ApiHandlerOptions, getModelMaxOutputTokens } from "../../shared/ap
77
import { TagMatcher } from "../../utils/tag-matcher"
88
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
99
import { convertToOpenAiMessages } from "../transform/openai-format"
10+
import { convertToZAiFormat } from "../transform/zai-format"
1011

1112
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
1213
import { DEFAULT_HEADERS } from "./constants"
1314
import { BaseProvider } from "./base-provider"
1415
import { handleOpenAIError } from "./utils/openai-error-handler"
1516
import { calculateApiCostOpenAI } from "../../shared/cost"
1617
import { getApiRequestTimeout } from "./utils/timeout-config"
18+
import { detectGlmModel, logGlmDetection, type GlmModelConfig } from "./utils/glm-model-detection"
1719

1820
type BaseOpenAiCompatibleProviderOptions<ModelName extends string> = ApiHandlerOptions & {
1921
providerName: string
@@ -36,6 +38,7 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
3638
protected readonly options: ApiHandlerOptions
3739

3840
protected client: OpenAI
41+
protected glmConfig: GlmModelConfig | null = null
3942

4043
constructor({
4144
providerName,
@@ -65,6 +68,13 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
6568
defaultHeaders: DEFAULT_HEADERS,
6669
timeout: getApiRequestTimeout(),
6770
})
71+
72+
// Detect GLM model on construction if model ID is available
73+
const modelId = this.options.apiModelId || ""
74+
if (modelId) {
75+
this.glmConfig = detectGlmModel(modelId)
76+
logGlmDetection(this.providerName, modelId, this.glmConfig)
77+
}
6878
}
6979

7080
protected createStream(
@@ -75,6 +85,12 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
7585
) {
7686
const { id: model, info } = this.getModel()
7787

88+
// Re-detect GLM model if not already done or if model ID changed
89+
if (!this.glmConfig || this.glmConfig.originalModelId !== model) {
90+
this.glmConfig = detectGlmModel(model)
91+
logGlmDetection(this.providerName, model, this.glmConfig)
92+
}
93+
7894
// Centralized cap: clamp to 20% of the context window (unless provider-specific exceptions apply)
7995
const max_tokens =
8096
getModelMaxOutputTokens({
@@ -86,23 +102,46 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
86102

87103
const temperature = this.options.modelTemperature ?? info.defaultTemperature ?? this.defaultTemperature
88104

105+
// Convert messages based on whether this is a GLM model
106+
// GLM models benefit from mergeToolResultText to prevent reasoning_content loss
107+
const convertedMessages = this.glmConfig.isGlmModel
108+
? convertToZAiFormat(messages, { mergeToolResultText: this.glmConfig.mergeToolResultText })
109+
: convertToOpenAiMessages(messages)
110+
111+
// Determine parallel_tool_calls setting
112+
// Disable for GLM models as they may not support it properly
113+
let parallelToolCalls: boolean
114+
if (this.glmConfig.isGlmModel && this.glmConfig.disableParallelToolCalls) {
115+
parallelToolCalls = false
116+
console.log(`[${this.providerName}] parallel_tool_calls disabled for GLM model`)
117+
} else {
118+
parallelToolCalls = metadata?.parallelToolCalls ?? true
119+
}
120+
89121
const params: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
90122
model,
91123
max_tokens,
92124
temperature,
93-
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
125+
messages: [{ role: "system", content: systemPrompt }, ...convertedMessages],
94126
stream: true,
95127
stream_options: { include_usage: true },
96128
tools: this.convertToolsForOpenAI(metadata?.tools),
97129
tool_choice: metadata?.tool_choice,
98-
parallel_tool_calls: metadata?.parallelToolCalls ?? true,
130+
parallel_tool_calls: parallelToolCalls,
99131
}
100132

101133
// Add thinking parameter if reasoning is enabled and model supports it
102134
if (this.options.enableReasoningEffort && info.supportsReasoningBinary) {
103135
;(params as any).thinking = { type: "enabled" }
104136
}
105137

138+
// For GLM-4.7 models with thinking support, add thinking parameter
139+
if (this.glmConfig.isGlmModel && this.glmConfig.supportsThinking) {
140+
const useReasoning = this.options.enableReasoningEffort !== false // Default to enabled for GLM-4.7
141+
;(params as any).thinking = useReasoning ? { type: "enabled" } : { type: "disabled" }
142+
console.log(`[${this.providerName}] GLM-4.7 thinking mode: ${useReasoning ? "enabled" : "disabled"}`)
143+
}
144+
106145
try {
107146
return this.client.chat.completions.create(params, requestOptions)
108147
} catch (error) {

src/api/providers/lm-studio.ts

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,21 @@ import { NativeToolCallParser } from "../../core/assistant-message/NativeToolCal
1010
import { TagMatcher } from "../../utils/tag-matcher"
1111

1212
import { convertToOpenAiMessages } from "../transform/openai-format"
13+
import { convertToZAiFormat } from "../transform/zai-format"
1314
import { ApiStream } from "../transform/stream"
1415

1516
import { BaseProvider } from "./base-provider"
1617
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
1718
import { getModelsFromCache } from "./fetchers/modelCache"
1819
import { getApiRequestTimeout } from "./utils/timeout-config"
1920
import { handleOpenAIError } from "./utils/openai-error-handler"
21+
import { detectGlmModel, logGlmDetection, type GlmModelConfig } from "./utils/glm-model-detection"
2022

2123
export class LmStudioHandler extends BaseProvider implements SingleCompletionHandler {
2224
protected options: ApiHandlerOptions
2325
private client: OpenAI
2426
private readonly providerName = "LM Studio"
27+
private glmConfig: GlmModelConfig | null = null
2528

2629
constructor(options: ApiHandlerOptions) {
2730
super()
@@ -35,16 +38,37 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan
3538
apiKey: apiKey,
3639
timeout: getApiRequestTimeout(),
3740
})
41+
42+
// Detect GLM model on construction if model ID is available
43+
const modelId = this.options.lmStudioModelId || ""
44+
if (modelId) {
45+
this.glmConfig = detectGlmModel(modelId)
46+
logGlmDetection(this.providerName, modelId, this.glmConfig)
47+
}
3848
}
3949

4050
override async *createMessage(
4151
systemPrompt: string,
4252
messages: Anthropic.Messages.MessageParam[],
4353
metadata?: ApiHandlerCreateMessageMetadata,
4454
): ApiStream {
55+
const model = this.getModel()
56+
57+
// Re-detect GLM model if not already done or if model ID changed
58+
if (!this.glmConfig || this.glmConfig.originalModelId !== model.id) {
59+
this.glmConfig = detectGlmModel(model.id)
60+
logGlmDetection(this.providerName, model.id, this.glmConfig)
61+
}
62+
63+
// Convert messages based on whether this is a GLM model
64+
// GLM models benefit from mergeToolResultText to prevent reasoning_content loss
65+
const convertedMessages = this.glmConfig.isGlmModel
66+
? convertToZAiFormat(messages, { mergeToolResultText: this.glmConfig.mergeToolResultText })
67+
: convertToOpenAiMessages(messages)
68+
4569
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
4670
{ role: "system", content: systemPrompt },
47-
...convertToOpenAiMessages(messages),
71+
...convertedMessages,
4872
]
4973

5074
// -------------------------
@@ -83,20 +107,37 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan
83107
let assistantText = ""
84108

85109
try {
110+
// Determine parallel_tool_calls setting
111+
// Disable for GLM models as they may not support it properly
112+
let parallelToolCalls: boolean
113+
if (this.glmConfig.isGlmModel && this.glmConfig.disableParallelToolCalls) {
114+
parallelToolCalls = false
115+
console.log(`[${this.providerName}] parallel_tool_calls disabled for GLM model`)
116+
} else {
117+
parallelToolCalls = metadata?.parallelToolCalls ?? true
118+
}
119+
86120
const params: OpenAI.Chat.ChatCompletionCreateParamsStreaming & { draft_model?: string } = {
87-
model: this.getModel().id,
121+
model: model.id,
88122
messages: openAiMessages,
89123
temperature: this.options.modelTemperature ?? LMSTUDIO_DEFAULT_TEMPERATURE,
90124
stream: true,
91125
tools: this.convertToolsForOpenAI(metadata?.tools),
92126
tool_choice: metadata?.tool_choice,
93-
parallel_tool_calls: metadata?.parallelToolCalls ?? true,
127+
parallel_tool_calls: parallelToolCalls,
94128
}
95129

96130
if (this.options.lmStudioSpeculativeDecodingEnabled && this.options.lmStudioDraftModelId) {
97131
params.draft_model = this.options.lmStudioDraftModelId
98132
}
99133

134+
// For GLM-4.7 models with thinking support, add thinking parameter
135+
if (this.glmConfig.isGlmModel && this.glmConfig.supportsThinking) {
136+
const useReasoning = this.options.enableReasoningEffort !== false // Default to enabled for GLM-4.7
137+
;(params as any).thinking = useReasoning ? { type: "enabled" } : { type: "disabled" }
138+
console.log(`[${this.providerName}] GLM-4.7 thinking mode: ${useReasoning ? "enabled" : "disabled"}`)
139+
}
140+
100141
let results
101142
try {
102143
results = await this.client.chat.completions.create(params)
@@ -124,6 +165,19 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan
124165
}
125166
}
126167

168+
// Handle reasoning_content for GLM models (similar to Z.ai)
169+
if (delta) {
170+
for (const key of ["reasoning_content", "reasoning"] as const) {
171+
if (key in delta) {
172+
const reasoning_content = ((delta as any)[key] as string | undefined) || ""
173+
if (reasoning_content?.trim()) {
174+
yield { type: "reasoning", text: reasoning_content }
175+
}
176+
break
177+
}
178+
}
179+
}
180+
127181
// Handle tool calls in stream - emit partial chunks for NativeToolCallParser
128182
if (delta?.tool_calls) {
129183
for (const toolCall of delta.tool_calls) {

0 commit comments

Comments
 (0)