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)
This commit is contained in:
Roo Code 2026-01-29 17:55:36 +00:00
parent 67e568f6bb
commit 0016aa033d
4 changed files with 225 additions and 5 deletions

View file

@ -14,6 +14,7 @@ import { BaseProvider } from "./base-provider"
import { handleOpenAIError } from "./utils/openai-error-handler"
import { calculateApiCostOpenAI } from "../../shared/cost"
import { getApiRequestTimeout } from "./utils/timeout-config"
import { getGlmModelOptions } from "./utils/glm-model-detection"
type BaseOpenAiCompatibleProviderOptions<ModelName extends string> = ApiHandlerOptions & {
providerName: string
@ -75,6 +76,9 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
) {
const { id: model, info } = this.getModel()
// Check if this is a GLM model and get recommended options
const glmOptions = getGlmModelOptions(model)
// Centralized cap: clamp to 20% of the context window (unless provider-specific exceptions apply)
const max_tokens =
getModelMaxOutputTokens({
@ -86,16 +90,24 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
const temperature = this.options.modelTemperature ?? info.defaultTemperature ?? this.defaultTemperature
// For GLM models, disable parallel_tool_calls as they may not support it
const parallelToolCalls = glmOptions?.disableParallelToolCalls ? false : (metadata?.parallelToolCalls ?? true)
const params: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
model,
max_tokens,
temperature,
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
messages: [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages, {
mergeToolResultText: glmOptions?.mergeToolResultText ?? false,
}),
],
stream: true,
stream_options: { include_usage: true },
tools: this.convertToolsForOpenAI(metadata?.tools),
tool_choice: metadata?.tool_choice,
parallel_tool_calls: metadata?.parallelToolCalls ?? true,
parallel_tool_calls: parallelToolCalls,
}
// Add thinking parameter if reasoning is enabled and model supports it

View file

@ -17,6 +17,7 @@ import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from ".
import { getModelsFromCache } from "./fetchers/modelCache"
import { getApiRequestTimeout } from "./utils/timeout-config"
import { handleOpenAIError } from "./utils/openai-error-handler"
import { getGlmModelOptions } from "./utils/glm-model-detection"
export class LmStudioHandler extends BaseProvider implements SingleCompletionHandler {
protected options: ApiHandlerOptions
@ -42,9 +43,16 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan
messages: Anthropic.Messages.MessageParam[],
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
const modelId = this.getModel().id
// Check if this is a GLM model and get recommended options
const glmOptions = getGlmModelOptions(modelId)
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
...convertToOpenAiMessages(messages, {
mergeToolResultText: glmOptions?.mergeToolResultText ?? false,
}),
]
// -------------------------
@ -83,14 +91,19 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan
let assistantText = ""
try {
// For GLM models, disable parallel_tool_calls as they may not support it
const parallelToolCalls = glmOptions?.disableParallelToolCalls
? false
: (metadata?.parallelToolCalls ?? true)
const params: OpenAI.Chat.ChatCompletionCreateParamsStreaming & { draft_model?: string } = {
model: this.getModel().id,
model: modelId,
messages: openAiMessages,
temperature: this.options.modelTemperature ?? LMSTUDIO_DEFAULT_TEMPERATURE,
stream: true,
tools: this.convertToolsForOpenAI(metadata?.tools),
tool_choice: metadata?.tool_choice,
parallel_tool_calls: metadata?.parallelToolCalls ?? true,
parallel_tool_calls: parallelToolCalls,
}
if (this.options.lmStudioSpeculativeDecodingEnabled && this.options.lmStudioDraftModelId) {

View file

@ -0,0 +1,115 @@
import { isGlmModel, getGlmModelOptions } from "../glm-model-detection"
describe("GLM Model Detection", () => {
describe("isGlmModel", () => {
describe("should detect GLM models", () => {
const validGlmModels = [
// Standard Z.ai format
"glm-4.5",
"glm-4.6",
"glm-4.7",
"glm-4.5-air",
"glm-4.5v",
// MLX format (from user's report)
"mlx-community/GLM-4.5-4bit",
"mlx-community/GLM-4.5-8bit",
// GGUF format (from user's report)
"GLM-4.5-UD-Q8_K_XL-00001-of-00008.gguf",
"GLM-4.5-UD-Q4_K_M.gguf",
// HuggingFace format
"THUDM/glm-4-9b-chat",
"THUDM/glm-4v-9b",
// ChatGLM variants
"chatglm-6b",
"chatglm2-6b",
"chatglm3-6b",
"ChatGLM-6B",
// Without hyphen
"glm4",
"GLM4",
// Mixed case
"GLM-4.5",
"Glm-4.5",
]
test.each(validGlmModels)('should detect "%s" as a GLM model', (modelId) => {
expect(isGlmModel(modelId)).toBe(true)
})
})
describe("should NOT detect non-GLM models", () => {
const nonGlmModels = [
// OpenAI models
"gpt-4",
"gpt-4-turbo",
"gpt-3.5-turbo",
"o1-preview",
// Anthropic models
"claude-3-opus",
"claude-3.5-sonnet",
// Llama models
"llama-3.1-70b",
"meta-llama/Llama-3.1-8B-Instruct",
// Mistral models
"mistral-7b",
"mixtral-8x7b",
// DeepSeek models
"deepseek-coder",
"deepseek-reasoner",
// Qwen models
"qwen-2.5-72b",
"qwen-coder",
// Empty/undefined
"",
]
test.each(nonGlmModels)('should NOT detect "%s" as a GLM model', (modelId) => {
expect(isGlmModel(modelId)).toBe(false)
})
})
it("should return false for undefined modelId", () => {
expect(isGlmModel(undefined)).toBe(false)
})
})
describe("getGlmModelOptions", () => {
it("should return options for GLM models", () => {
const options = getGlmModelOptions("glm-4.5")
expect(options).toEqual({
mergeToolResultText: true,
disableParallelToolCalls: true,
})
})
it("should return options for MLX GLM models", () => {
const options = getGlmModelOptions("mlx-community/GLM-4.5-4bit")
expect(options).toEqual({
mergeToolResultText: true,
disableParallelToolCalls: true,
})
})
it("should return options for GGUF GLM models", () => {
const options = getGlmModelOptions("GLM-4.5-UD-Q8_K_XL-00001-of-00008.gguf")
expect(options).toEqual({
mergeToolResultText: true,
disableParallelToolCalls: true,
})
})
it("should return undefined for non-GLM models", () => {
expect(getGlmModelOptions("gpt-4")).toBeUndefined()
expect(getGlmModelOptions("llama-3.1")).toBeUndefined()
expect(getGlmModelOptions("claude-3")).toBeUndefined()
})
it("should return undefined for undefined modelId", () => {
expect(getGlmModelOptions(undefined)).toBeUndefined()
})
it("should return undefined for empty string", () => {
expect(getGlmModelOptions("")).toBeUndefined()
})
})
})

View file

@ -0,0 +1,80 @@
/**
* Utility functions for detecting GLM (General Language Model) models.
*
* GLM models from Z.ai/THUDM may require special handling:
* - mergeToolResultText: true - prevents conversation flow disruption
* - parallel_tool_calls: false - some GLM models do not support this parameter
*/
/**
* Pattern to detect GLM models in model IDs.
*
* This regex matches "glm" anywhere in the model ID (case-insensitive),
* including common variations like:
* - "glm-4.5" (standard Z.ai format)
* - "glm4" (without hyphen)
* - "chatglm" (ChatGLM variants)
* - "mlx-community/GLM-4.5-4bit" (MLX format with prefix)
* - "GLM-4.5-UD-Q8_K_XL-00001-of-00008.gguf" (GGUF format)
* - "THUDM/glm-4-9b-chat" (HuggingFace format)
*/
const GLM_MODEL_PATTERN = /glm/i
/**
* Detects if a model ID represents a GLM (General Language Model) model.
*
* @param modelId - The model ID to check (e.g., "glm-4.5", "mlx-community/GLM-4.5-4bit")
* @returns true if the model ID indicates a GLM model, false otherwise
*
* @example
* ```typescript
* isGlmModel("glm-4.5") // true
* isGlmModel("mlx-community/GLM-4.5-4bit") // true
* isGlmModel("GLM-4.5-UD-Q8_K_XL.gguf") // true
* isGlmModel("chatglm-6b") // true
* isGlmModel("gpt-4") // false
* isGlmModel("llama-3.1") // false
* ```
*/
export function isGlmModel(modelId: string | undefined): boolean {
if (!modelId) {
return false
}
return GLM_MODEL_PATTERN.test(modelId)
}
/**
* Configuration options for GLM models when used via LM Studio
* or OpenAI-compatible endpoints.
*/
export interface GlmModelOptions {
/**
* If true, merge text content after tool_results into the last tool message
* instead of creating a separate user message. This prevents GLM models from
* losing context or reasoning_content after tool results.
*/
mergeToolResultText: boolean
/**
* If true, disable parallel_tool_calls parameter for GLM models
* since they may not support it.
*/
disableParallelToolCalls: boolean
}
/**
* Returns the recommended configuration options for a GLM model.
*
* @param modelId - The model ID to check
* @returns GlmModelOptions if GLM model detected, undefined otherwise
*/
export function getGlmModelOptions(modelId: string | undefined): GlmModelOptions | undefined {
if (!isGlmModel(modelId)) {
return undefined
}
return {
mergeToolResultText: true,
disableParallelToolCalls: true,
}
}