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

Commit 1379ba4

Browse files
committed
feat: add GLM model detection for LM Studio and OpenAI-compatible providers
This adds automatic GLM model detection for third-party providers, enabling the same optimizations that Z.ai uses for GLM models: 1. Created isGlmModel() utility function that detects GLM model IDs 2. Created getGlmModelOptions() to get model-specific configuration 3. Modified LM Studio provider to detect GLM models and apply: - mergeToolResultText option to prevent dropping reasoning_content - disabled parallel_tool_calls by default for GLM models 4. Modified BaseOpenAiCompatibleProvider with the same GLM handling This addresses issue #11071 questions about GLM model detection and ensuring Z.ai improvements are available to LM Studio and OpenAI-compatible endpoints running GLM models.
1 parent 67e568f commit 1379ba4

4 files changed

Lines changed: 228 additions & 5 deletions

File tree

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

Lines changed: 20 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/model-detection"
1718

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

79+
// Get model-specific options for GLM models (applies Z.ai optimizations)
80+
// This allows third-party GLM models via OpenAI-compatible endpoints to benefit
81+
// from the same optimizations used by Z.ai
82+
const glmOptions = getGlmModelOptions(model)
83+
7884
// Centralized cap: clamp to 20% of the context window (unless provider-specific exceptions apply)
7985
const max_tokens =
8086
getModelMaxOutputTokens({
@@ -86,16 +92,28 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
8692

8793
const temperature = this.options.modelTemperature ?? info.defaultTemperature ?? this.defaultTemperature
8894

95+
// For GLM models, disable parallel_tool_calls by default as they may not support it
96+
// Users can still explicitly enable it via metadata if their model supports it
97+
const parallelToolCalls = glmOptions.disableParallelToolCalls
98+
? (metadata?.parallelToolCalls ?? false)
99+
: (metadata?.parallelToolCalls ?? true)
100+
101+
// Convert messages with GLM-specific handling when applicable
102+
// mergeToolResultText prevents GLM models from dropping reasoning_content
103+
const convertedMessages = convertToOpenAiMessages(messages, {
104+
mergeToolResultText: glmOptions.mergeToolResultText,
105+
})
106+
89107
const params: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
90108
model,
91109
max_tokens,
92110
temperature,
93-
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
111+
messages: [{ role: "system", content: systemPrompt }, ...convertedMessages],
94112
stream: true,
95113
stream_options: { include_usage: true },
96114
tools: this.convertToolsForOpenAI(metadata?.tools),
97115
tool_choice: metadata?.tool_choice,
98-
parallel_tool_calls: metadata?.parallelToolCalls ?? true,
116+
parallel_tool_calls: parallelToolCalls,
99117
}
100118

101119
// 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/model-detection"
2021

2122
export class LmStudioHandler extends BaseProvider implements SingleCompletionHandler {
2223
protected options: ApiHandlerOptions
@@ -42,9 +43,15 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan
4243
messages: Anthropic.Messages.MessageParam[],
4344
metadata?: ApiHandlerCreateMessageMetadata,
4445
): ApiStream {
46+
// Get model-specific options for GLM models (applies Z.ai optimizations)
47+
const modelId = this.getModel().id
48+
const glmOptions = getGlmModelOptions(modelId)
49+
50+
// Convert messages with GLM-specific handling when applicable
51+
// mergeToolResultText prevents GLM models from dropping reasoning_content
4552
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
4653
{ role: "system", content: systemPrompt },
47-
...convertToOpenAiMessages(messages),
54+
...convertToOpenAiMessages(messages, { mergeToolResultText: glmOptions.mergeToolResultText }),
4855
]
4956

5057
// -------------------------
@@ -83,14 +90,20 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan
8390
let assistantText = ""
8491

8592
try {
93+
// For GLM models, disable parallel_tool_calls by default as they may not support it
94+
// Users can still explicitly enable it via metadata if their model supports it
95+
const parallelToolCalls = glmOptions.disableParallelToolCalls
96+
? (metadata?.parallelToolCalls ?? 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: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { isGlmModel, getGlmModelOptions, GlmModelOptions } from "../model-detection"
2+
3+
describe("isGlmModel", () => {
4+
describe("GLM model detection", () => {
5+
it("should detect official GLM model names with dash", () => {
6+
expect(isGlmModel("glm-4")).toBe(true)
7+
expect(isGlmModel("glm-4.5")).toBe(true)
8+
expect(isGlmModel("glm-4.7")).toBe(true)
9+
expect(isGlmModel("glm-4-plus")).toBe(true)
10+
})
11+
12+
it("should detect GLM models with uppercase", () => {
13+
expect(isGlmModel("GLM-4")).toBe(true)
14+
expect(isGlmModel("GLM-4.5")).toBe(true)
15+
expect(isGlmModel("GLM-4.7")).toBe(true)
16+
})
17+
18+
it("should detect compact GLM model names without dash", () => {
19+
expect(isGlmModel("glm4")).toBe(true)
20+
expect(isGlmModel("GLM4")).toBe(true)
21+
expect(isGlmModel("glm4-9b")).toBe(true)
22+
})
23+
24+
it("should detect LM Studio GGUF model names", () => {
25+
expect(isGlmModel("GLM4-9B-Chat-GGUF")).toBe(true)
26+
expect(isGlmModel("glm4-9b-chat-gguf")).toBe(true)
27+
})
28+
29+
it("should detect ChatGLM models", () => {
30+
expect(isGlmModel("chatglm")).toBe(true)
31+
expect(isGlmModel("ChatGLM")).toBe(true)
32+
expect(isGlmModel("chatglm-6b")).toBe(true)
33+
expect(isGlmModel("chatglm3-6b")).toBe(true)
34+
})
35+
})
36+
37+
describe("non-GLM model detection", () => {
38+
it("should not detect OpenAI models as GLM", () => {
39+
expect(isGlmModel("gpt-4")).toBe(false)
40+
expect(isGlmModel("gpt-4-turbo")).toBe(false)
41+
expect(isGlmModel("gpt-3.5-turbo")).toBe(false)
42+
expect(isGlmModel("o1-preview")).toBe(false)
43+
})
44+
45+
it("should not detect Anthropic models as GLM", () => {
46+
expect(isGlmModel("claude-3")).toBe(false)
47+
expect(isGlmModel("claude-3-sonnet")).toBe(false)
48+
expect(isGlmModel("claude-3-opus")).toBe(false)
49+
})
50+
51+
it("should not detect DeepSeek models as GLM", () => {
52+
expect(isGlmModel("deepseek-coder")).toBe(false)
53+
expect(isGlmModel("deepseek-reasoner")).toBe(false)
54+
})
55+
56+
it("should not detect Gemini models as GLM", () => {
57+
expect(isGlmModel("gemini-pro")).toBe(false)
58+
expect(isGlmModel("gemini-2-flash")).toBe(false)
59+
})
60+
61+
it("should not detect Qwen models as GLM", () => {
62+
expect(isGlmModel("qwen-7b")).toBe(false)
63+
expect(isGlmModel("qwen2-7b")).toBe(false)
64+
})
65+
66+
it("should not detect Llama models as GLM", () => {
67+
expect(isGlmModel("llama-2-7b")).toBe(false)
68+
expect(isGlmModel("llama-3-8b")).toBe(false)
69+
expect(isGlmModel("codellama")).toBe(false)
70+
})
71+
})
72+
73+
describe("edge cases", () => {
74+
it("should handle empty string", () => {
75+
expect(isGlmModel("")).toBe(false)
76+
})
77+
78+
it("should handle undefined-like values", () => {
79+
expect(isGlmModel(null as unknown as string)).toBe(false)
80+
expect(isGlmModel(undefined as unknown as string)).toBe(false)
81+
})
82+
83+
it("should not match 'glm' in the middle of unrelated model names", () => {
84+
// This tests that we're not accidentally matching "glm" as a substring
85+
// in unrelated contexts
86+
expect(isGlmModel("myglmodel")).toBe(false)
87+
expect(isGlmModel("some-glm-inspired-model")).toBe(false)
88+
})
89+
})
90+
})
91+
92+
describe("getGlmModelOptions", () => {
93+
it("should return GLM-optimized options for GLM models", () => {
94+
const options = getGlmModelOptions("glm-4.5")
95+
96+
expect(options.mergeToolResultText).toBe(true)
97+
expect(options.disableParallelToolCalls).toBe(true)
98+
})
99+
100+
it("should return default options for non-GLM models", () => {
101+
const options = getGlmModelOptions("gpt-4")
102+
103+
expect(options.mergeToolResultText).toBe(false)
104+
expect(options.disableParallelToolCalls).toBe(false)
105+
})
106+
107+
it("should return the correct type", () => {
108+
const options: GlmModelOptions = getGlmModelOptions("glm-4")
109+
110+
expect(options).toHaveProperty("mergeToolResultText")
111+
expect(options).toHaveProperty("disableParallelToolCalls")
112+
})
113+
})
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* Utility functions for detecting model types based on model ID patterns.
3+
* These functions help providers apply model-specific handling for third-party
4+
* models running on LM Studio, OpenAI-compatible endpoints, etc.
5+
*/
6+
7+
/**
8+
* Detects if a model ID represents a GLM (General Language Model) from Zhipu AI.
9+
*
10+
* GLM models (like GLM-4, GLM-4.5, GLM-4.7) have specific requirements:
11+
* - They benefit from `mergeToolResultText: true` to avoid dropping reasoning_content
12+
* - They may not support `parallel_tool_calls` parameter
13+
*
14+
* This detection allows LM Studio and OpenAI-compatible providers to apply
15+
* the same optimizations that Z.ai uses for GLM models.
16+
*
17+
* @param modelId - The model identifier (e.g., "glm-4.5", "GLM4-9B-Chat-GGUF")
18+
* @returns true if the model is a GLM model, false otherwise
19+
*
20+
* @example
21+
* ```typescript
22+
* isGlmModel("glm-4.5") // true
23+
* isGlmModel("GLM4-9B-Chat-GGUF") // true
24+
* isGlmModel("glm-4.7") // true
25+
* isGlmModel("gpt-4") // false
26+
* isGlmModel("claude-3") // false
27+
* ```
28+
*/
29+
export function isGlmModel(modelId: string): boolean {
30+
if (!modelId) {
31+
return false
32+
}
33+
34+
// Case-insensitive check for "glm" prefix or pattern
35+
// Matches: glm-4, glm-4.5, glm-4.7, GLM4-9B-Chat, glm4, etc.
36+
const lowerModelId = modelId.toLowerCase()
37+
38+
// Check for common GLM model patterns:
39+
// - "glm-" prefix (official naming: glm-4, glm-4.5, glm-4.7)
40+
// - "glm4" (compact naming without dash)
41+
// - "chatglm" (older ChatGLM models)
42+
return lowerModelId.startsWith("glm-") || lowerModelId.startsWith("glm4") || lowerModelId.includes("chatglm")
43+
}
44+
45+
/**
46+
* Configuration options for GLM model-specific handling.
47+
* These options are derived from Z.ai's optimizations for GLM models.
48+
*/
49+
export interface GlmModelOptions {
50+
/**
51+
* Whether to merge text content after tool_results into the last tool message.
52+
* This prevents GLM models from dropping reasoning_content when they see
53+
* a user message after tool results.
54+
*/
55+
mergeToolResultText: boolean
56+
57+
/**
58+
* Whether to disable parallel_tool_calls for this model.
59+
* GLM models may not support this parameter and can behave unexpectedly
60+
* when it's enabled.
61+
*/
62+
disableParallelToolCalls: boolean
63+
}
64+
65+
/**
66+
* Returns the recommended configuration options for a GLM model.
67+
* Non-GLM models will receive default options that maintain existing behavior.
68+
*
69+
* @param modelId - The model identifier
70+
* @returns Configuration options for the model
71+
*/
72+
export function getGlmModelOptions(modelId: string): GlmModelOptions {
73+
const isGlm = isGlmModel(modelId)
74+
75+
return {
76+
mergeToolResultText: isGlm,
77+
disableParallelToolCalls: isGlm,
78+
}
79+
}

0 commit comments

Comments
 (0)