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

Commit d010015

Browse files
committed
feat: add strategic GLM model family detection for LM Studio and OpenAI-compatible providers
This PR addresses Issue #11071 by implementing a comprehensive GLM model detection system: 1. Created glm-model-detection.ts utility that: - Detects GLM family models (GLM-4.5, 4.6, 4.7 and variants) - Supports various model ID formats (standard, MLX, GGUF, ChatGLM) - Identifies version (4.5, 4.6, 4.7) and variant (base, air, flash, v, etc.) - Returns appropriate configuration for each model 2. Updated LmStudioHandler to: - Detect GLM models and log detection results to console - Use convertToZAiFormat with mergeToolResultText for GLM models - Disable parallel_tool_calls for GLM models - Handle reasoning_content for GLM-4.7 models 3. Updated BaseOpenAiCompatibleProvider similarly 4. Added 33 comprehensive tests for the detection utility The detection uses flexible regex patterns to match model IDs like: - mlx-community/GLM-4.5-4bit - GLM-4.5-UD-Q8_K_XL-00001-of-00008.gguf - glm-4.5, glm-4.7-flash, etc.
1 parent cc86049 commit d010015

4 files changed

Lines changed: 566 additions & 6 deletions

File tree

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

Lines changed: 55 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,48 @@ 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(
143+
`[${this.providerName}] GLM thinking mode: ${useReasoning ? "enabled" : "disabled"} for ${this.glmConfig.displayName}`,
144+
)
145+
}
146+
106147
try {
107148
return this.client.chat.completions.create(params, requestOptions)
108149
} catch (error) {
@@ -222,6 +263,12 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
222263
async completePrompt(prompt: string): Promise<string> {
223264
const { id: modelId, info: modelInfo } = this.getModel()
224265

266+
// Re-detect GLM model if not already done or if model ID changed
267+
if (!this.glmConfig || this.glmConfig.originalModelId !== modelId) {
268+
this.glmConfig = detectGlmModel(modelId)
269+
logGlmDetection(this.providerName, modelId, this.glmConfig)
270+
}
271+
225272
const params: OpenAI.Chat.Completions.ChatCompletionCreateParams = {
226273
model: modelId,
227274
messages: [{ role: "user", content: prompt }],
@@ -232,6 +279,12 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
232279
;(params as any).thinking = { type: "enabled" }
233280
}
234281

282+
// For GLM-4.7 models with thinking support, add thinking parameter
283+
if (this.glmConfig.isGlmModel && this.glmConfig.supportsThinking) {
284+
const useReasoning = this.options.enableReasoningEffort !== false
285+
;(params as any).thinking = useReasoning ? { type: "enabled" } : { type: "disabled" }
286+
}
287+
235288
try {
236289
const response = await this.client.chat.completions.create(params)
237290

src/api/providers/lm-studio.ts

Lines changed: 58 additions & 4 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 modelId = this.getModel().id
56+
57+
// Re-detect GLM model if not already done or if model ID changed
58+
if (!this.glmConfig || this.glmConfig.originalModelId !== modelId) {
59+
this.glmConfig = detectGlmModel(modelId)
60+
logGlmDetection(this.providerName, modelId, 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,14 +107,24 @@ 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: modelId,
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) {
@@ -124,6 +158,14 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan
124158
}
125159
}
126160

161+
// Handle reasoning_content for GLM models with thinking support
162+
if (delta && this.glmConfig?.supportsThinking) {
163+
const deltaAny = delta as any
164+
if (deltaAny.reasoning_content) {
165+
yield { type: "reasoning", text: deltaAny.reasoning_content }
166+
}
167+
}
168+
127169
// Handle tool calls in stream - emit partial chunks for NativeToolCallParser
128170
if (delta?.tool_calls) {
129171
for (const toolCall of delta.tool_calls) {
@@ -186,10 +228,22 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan
186228
}
187229

188230
async completePrompt(prompt: string): Promise<string> {
231+
const modelId = this.getModel().id
232+
233+
// Re-detect GLM model if not already done or if model ID changed
234+
if (!this.glmConfig || this.glmConfig.originalModelId !== modelId) {
235+
this.glmConfig = detectGlmModel(modelId)
236+
logGlmDetection(this.providerName, modelId, this.glmConfig)
237+
}
238+
189239
try {
240+
// Determine parallel_tool_calls setting for GLM models
241+
const parallelToolCalls =
242+
this.glmConfig?.isGlmModel && this.glmConfig.disableParallelToolCalls ? false : true
243+
190244
// Create params object with optional draft model
191245
const params: any = {
192-
model: this.getModel().id,
246+
model: modelId,
193247
messages: [{ role: "user", content: prompt }],
194248
temperature: this.options.modelTemperature ?? LMSTUDIO_DEFAULT_TEMPERATURE,
195249
stream: false,

0 commit comments

Comments
 (0)