Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/api/providers/base-openai-compatible-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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({
Expand All @@ -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
Expand Down
19 changes: 16 additions & 3 deletions src/api/providers/lm-studio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
}),
]

// -------------------------
Expand Down Expand Up @@ -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) {
Expand Down
115 changes: 115 additions & 0 deletions src/api/providers/utils/__tests__/glm-model-detection.spec.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
})
80 changes: 80 additions & 0 deletions src/api/providers/utils/glm-model-detection.ts
Original file line number Diff line number Diff line change
@@ -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,
}
}
Loading