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

Commit 0016aa0

Browse files
committed
fix: improve GLM model detection patterns for LM Studio and OpenAI-compatible endpoints
Addresses Issue #11071 where GLM4.5 models via LM Studio get stuck repeating file reads due to undetected model names. Changes: - Add isGlmModel() utility that uses a flexible regex pattern to detect GLM models anywhere in the model ID (case-insensitive) - Handle model name formats like: - mlx-community/GLM-4.5-4bit - GLM-4.5-UD-Q8_K_XL-00001-of-00008.gguf - glm-4.5 (standard format) - chatglm variants - Update LmStudioHandler to apply GLM-specific optimizations when detected - Update BaseOpenAiCompatibleProvider similarly - Add comprehensive tests for GLM model detection When GLM model is detected: - mergeToolResultText: true (prevents conversation flow disruption) - parallel_tool_calls: false (GLM may not support this parameter)
1 parent 67e568f commit 0016aa0

4 files changed

Lines changed: 225 additions & 5 deletions

File tree

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { BaseProvider } from "./base-provider"
1414
import { handleOpenAIError } from "./utils/openai-error-handler"
1515
import { calculateApiCostOpenAI } from "../../shared/cost"
1616
import { getApiRequestTimeout } from "./utils/timeout-config"
17+
import { getGlmModelOptions } from "./utils/glm-model-detection"
1718

1819
type BaseOpenAiCompatibleProviderOptions<ModelName extends string> = ApiHandlerOptions & {
1920
providerName: string
@@ -75,6 +76,9 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
7576
) {
7677
const { id: model, info } = this.getModel()
7778

79+
// Check if this is a GLM model and get recommended options
80+
const glmOptions = getGlmModelOptions(model)
81+
7882
// Centralized cap: clamp to 20% of the context window (unless provider-specific exceptions apply)
7983
const max_tokens =
8084
getModelMaxOutputTokens({
@@ -86,16 +90,24 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
8690

8791
const temperature = this.options.modelTemperature ?? info.defaultTemperature ?? this.defaultTemperature
8892

93+
// For GLM models, disable parallel_tool_calls as they may not support it
94+
const parallelToolCalls = glmOptions?.disableParallelToolCalls ? false : (metadata?.parallelToolCalls ?? true)
95+
8996
const params: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
9097
model,
9198
max_tokens,
9299
temperature,
93-
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
100+
messages: [
101+
{ role: "system", content: systemPrompt },
102+
...convertToOpenAiMessages(messages, {
103+
mergeToolResultText: glmOptions?.mergeToolResultText ?? false,
104+
}),
105+
],
94106
stream: true,
95107
stream_options: { include_usage: true },
96108
tools: this.convertToolsForOpenAI(metadata?.tools),
97109
tool_choice: metadata?.tool_choice,
98-
parallel_tool_calls: metadata?.parallelToolCalls ?? true,
110+
parallel_tool_calls: parallelToolCalls,
99111
}
100112

101113
// Add thinking parameter if reasoning is enabled and model supports it

src/api/providers/lm-studio.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from ".
1717
import { getModelsFromCache } from "./fetchers/modelCache"
1818
import { getApiRequestTimeout } from "./utils/timeout-config"
1919
import { handleOpenAIError } from "./utils/openai-error-handler"
20+
import { getGlmModelOptions } from "./utils/glm-model-detection"
2021

2122
export class LmStudioHandler extends BaseProvider implements SingleCompletionHandler {
2223
protected options: ApiHandlerOptions
@@ -42,9 +43,16 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan
4243
messages: Anthropic.Messages.MessageParam[],
4344
metadata?: ApiHandlerCreateMessageMetadata,
4445
): ApiStream {
46+
const modelId = this.getModel().id
47+
48+
// Check if this is a GLM model and get recommended options
49+
const glmOptions = getGlmModelOptions(modelId)
50+
4551
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
4652
{ role: "system", content: systemPrompt },
47-
...convertToOpenAiMessages(messages),
53+
...convertToOpenAiMessages(messages, {
54+
mergeToolResultText: glmOptions?.mergeToolResultText ?? false,
55+
}),
4856
]
4957

5058
// -------------------------
@@ -83,14 +91,19 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan
8391
let assistantText = ""
8492

8593
try {
94+
// For GLM models, disable parallel_tool_calls as they may not support it
95+
const parallelToolCalls = glmOptions?.disableParallelToolCalls
96+
? false
97+
: (metadata?.parallelToolCalls ?? true)
98+
8699
const params: OpenAI.Chat.ChatCompletionCreateParamsStreaming & { draft_model?: string } = {
87-
model: this.getModel().id,
100+
model: modelId,
88101
messages: openAiMessages,
89102
temperature: this.options.modelTemperature ?? LMSTUDIO_DEFAULT_TEMPERATURE,
90103
stream: true,
91104
tools: this.convertToolsForOpenAI(metadata?.tools),
92105
tool_choice: metadata?.tool_choice,
93-
parallel_tool_calls: metadata?.parallelToolCalls ?? true,
106+
parallel_tool_calls: parallelToolCalls,
94107
}
95108

96109
if (this.options.lmStudioSpeculativeDecodingEnabled && this.options.lmStudioDraftModelId) {
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { isGlmModel, getGlmModelOptions } from "../glm-model-detection"
2+
3+
describe("GLM Model Detection", () => {
4+
describe("isGlmModel", () => {
5+
describe("should detect GLM models", () => {
6+
const validGlmModels = [
7+
// Standard Z.ai format
8+
"glm-4.5",
9+
"glm-4.6",
10+
"glm-4.7",
11+
"glm-4.5-air",
12+
"glm-4.5v",
13+
// MLX format (from user's report)
14+
"mlx-community/GLM-4.5-4bit",
15+
"mlx-community/GLM-4.5-8bit",
16+
// GGUF format (from user's report)
17+
"GLM-4.5-UD-Q8_K_XL-00001-of-00008.gguf",
18+
"GLM-4.5-UD-Q4_K_M.gguf",
19+
// HuggingFace format
20+
"THUDM/glm-4-9b-chat",
21+
"THUDM/glm-4v-9b",
22+
// ChatGLM variants
23+
"chatglm-6b",
24+
"chatglm2-6b",
25+
"chatglm3-6b",
26+
"ChatGLM-6B",
27+
// Without hyphen
28+
"glm4",
29+
"GLM4",
30+
// Mixed case
31+
"GLM-4.5",
32+
"Glm-4.5",
33+
]
34+
35+
test.each(validGlmModels)('should detect "%s" as a GLM model', (modelId) => {
36+
expect(isGlmModel(modelId)).toBe(true)
37+
})
38+
})
39+
40+
describe("should NOT detect non-GLM models", () => {
41+
const nonGlmModels = [
42+
// OpenAI models
43+
"gpt-4",
44+
"gpt-4-turbo",
45+
"gpt-3.5-turbo",
46+
"o1-preview",
47+
// Anthropic models
48+
"claude-3-opus",
49+
"claude-3.5-sonnet",
50+
// Llama models
51+
"llama-3.1-70b",
52+
"meta-llama/Llama-3.1-8B-Instruct",
53+
// Mistral models
54+
"mistral-7b",
55+
"mixtral-8x7b",
56+
// DeepSeek models
57+
"deepseek-coder",
58+
"deepseek-reasoner",
59+
// Qwen models
60+
"qwen-2.5-72b",
61+
"qwen-coder",
62+
// Empty/undefined
63+
"",
64+
]
65+
66+
test.each(nonGlmModels)('should NOT detect "%s" as a GLM model', (modelId) => {
67+
expect(isGlmModel(modelId)).toBe(false)
68+
})
69+
})
70+
71+
it("should return false for undefined modelId", () => {
72+
expect(isGlmModel(undefined)).toBe(false)
73+
})
74+
})
75+
76+
describe("getGlmModelOptions", () => {
77+
it("should return options for GLM models", () => {
78+
const options = getGlmModelOptions("glm-4.5")
79+
expect(options).toEqual({
80+
mergeToolResultText: true,
81+
disableParallelToolCalls: true,
82+
})
83+
})
84+
85+
it("should return options for MLX GLM models", () => {
86+
const options = getGlmModelOptions("mlx-community/GLM-4.5-4bit")
87+
expect(options).toEqual({
88+
mergeToolResultText: true,
89+
disableParallelToolCalls: true,
90+
})
91+
})
92+
93+
it("should return options for GGUF GLM models", () => {
94+
const options = getGlmModelOptions("GLM-4.5-UD-Q8_K_XL-00001-of-00008.gguf")
95+
expect(options).toEqual({
96+
mergeToolResultText: true,
97+
disableParallelToolCalls: true,
98+
})
99+
})
100+
101+
it("should return undefined for non-GLM models", () => {
102+
expect(getGlmModelOptions("gpt-4")).toBeUndefined()
103+
expect(getGlmModelOptions("llama-3.1")).toBeUndefined()
104+
expect(getGlmModelOptions("claude-3")).toBeUndefined()
105+
})
106+
107+
it("should return undefined for undefined modelId", () => {
108+
expect(getGlmModelOptions(undefined)).toBeUndefined()
109+
})
110+
111+
it("should return undefined for empty string", () => {
112+
expect(getGlmModelOptions("")).toBeUndefined()
113+
})
114+
})
115+
})
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* Utility functions for detecting GLM (General Language Model) models.
3+
*
4+
* GLM models from Z.ai/THUDM may require special handling:
5+
* - mergeToolResultText: true - prevents conversation flow disruption
6+
* - parallel_tool_calls: false - some GLM models do not support this parameter
7+
*/
8+
9+
/**
10+
* Pattern to detect GLM models in model IDs.
11+
*
12+
* This regex matches "glm" anywhere in the model ID (case-insensitive),
13+
* including common variations like:
14+
* - "glm-4.5" (standard Z.ai format)
15+
* - "glm4" (without hyphen)
16+
* - "chatglm" (ChatGLM variants)
17+
* - "mlx-community/GLM-4.5-4bit" (MLX format with prefix)
18+
* - "GLM-4.5-UD-Q8_K_XL-00001-of-00008.gguf" (GGUF format)
19+
* - "THUDM/glm-4-9b-chat" (HuggingFace format)
20+
*/
21+
const GLM_MODEL_PATTERN = /glm/i
22+
23+
/**
24+
* Detects if a model ID represents a GLM (General Language Model) model.
25+
*
26+
* @param modelId - The model ID to check (e.g., "glm-4.5", "mlx-community/GLM-4.5-4bit")
27+
* @returns true if the model ID indicates a GLM model, false otherwise
28+
*
29+
* @example
30+
* ```typescript
31+
* isGlmModel("glm-4.5") // true
32+
* isGlmModel("mlx-community/GLM-4.5-4bit") // true
33+
* isGlmModel("GLM-4.5-UD-Q8_K_XL.gguf") // true
34+
* isGlmModel("chatglm-6b") // true
35+
* isGlmModel("gpt-4") // false
36+
* isGlmModel("llama-3.1") // false
37+
* ```
38+
*/
39+
export function isGlmModel(modelId: string | undefined): boolean {
40+
if (!modelId) {
41+
return false
42+
}
43+
return GLM_MODEL_PATTERN.test(modelId)
44+
}
45+
46+
/**
47+
* Configuration options for GLM models when used via LM Studio
48+
* or OpenAI-compatible endpoints.
49+
*/
50+
export interface GlmModelOptions {
51+
/**
52+
* If true, merge text content after tool_results into the last tool message
53+
* instead of creating a separate user message. This prevents GLM models from
54+
* losing context or reasoning_content after tool results.
55+
*/
56+
mergeToolResultText: boolean
57+
58+
/**
59+
* If true, disable parallel_tool_calls parameter for GLM models
60+
* since they may not support it.
61+
*/
62+
disableParallelToolCalls: boolean
63+
}
64+
65+
/**
66+
* Returns the recommended configuration options for a GLM model.
67+
*
68+
* @param modelId - The model ID to check
69+
* @returns GlmModelOptions if GLM model detected, undefined otherwise
70+
*/
71+
export function getGlmModelOptions(modelId: string | undefined): GlmModelOptions | undefined {
72+
if (!isGlmModel(modelId)) {
73+
return undefined
74+
}
75+
76+
return {
77+
mergeToolResultText: true,
78+
disableParallelToolCalls: true,
79+
}
80+
}

0 commit comments

Comments
 (0)