@@ -15,6 +15,7 @@ import type { ApiHandlerOptions } from "../../shared/api"
1515import { TagMatcher } from "../../utils/tag-matcher"
1616
1717import { convertToOpenAiMessages } from "../transform/openai-format"
18+ import { convertToZAiFormat } from "../transform/zai-format"
1819import { convertToR1Format } from "../transform/r1-format"
1920import { ApiStream , ApiStreamUsageChunk } from "../transform/stream"
2021import { getModelParams } from "../transform/model-params"
@@ -24,14 +25,16 @@ import { BaseProvider } from "./base-provider"
2425import type { SingleCompletionHandler , ApiHandlerCreateMessageMetadata } from "../index"
2526import { getApiRequestTimeout } from "./utils/timeout-config"
2627import { handleOpenAIError } from "./utils/openai-error-handler"
28+ import { detectGlmModel , logGlmDetection , type GlmModelConfig } from "./utils/glm-model-detection"
2729
2830// TODO: Rename this to OpenAICompatibleHandler. Also, I think the
2931// `OpenAINativeHandler` can subclass from this, since it's obviously
3032// compatible with the OpenAI API. We can also rename it to `OpenAIHandler`.
3133export class OpenAiHandler extends BaseProvider implements SingleCompletionHandler {
3234 protected options : ApiHandlerOptions
3335 protected client : OpenAI
34- private readonly providerName = "OpenAI"
36+ private readonly providerName = "OpenAI Compatible"
37+ private glmConfig : GlmModelConfig | null = null
3538
3639 constructor ( options : ApiHandlerOptions ) {
3740 super ( )
@@ -77,6 +80,13 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
7780 timeout,
7881 } )
7982 }
83+
84+ // Detect GLM model on construction if model ID is available
85+ const modelId = this . options . openAiModelId || ""
86+ if ( modelId ) {
87+ this . glmConfig = detectGlmModel ( modelId )
88+ logGlmDetection ( this . providerName , modelId , this . glmConfig )
89+ }
8090 }
8191
8292 override async * createMessage (
@@ -91,6 +101,12 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
91101 const isAzureAiInference = this . _isAzureAiInference ( modelUrl )
92102 const deepseekReasoner = modelId . includes ( "deepseek-reasoner" ) || enabledR1Format
93103
104+ // Re-detect GLM model if not already done or if model ID changed
105+ if ( ! this . glmConfig || this . glmConfig . originalModelId !== modelId ) {
106+ this . glmConfig = detectGlmModel ( modelId )
107+ logGlmDetection ( this . providerName , modelId , this . glmConfig )
108+ }
109+
94110 if ( modelId . includes ( "o1" ) || modelId . includes ( "o3" ) || modelId . includes ( "o4" ) ) {
95111 yield * this . handleO3FamilyMessage ( modelId , systemPrompt , messages , metadata )
96112 return
@@ -106,6 +122,12 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
106122
107123 if ( deepseekReasoner ) {
108124 convertedMessages = convertToR1Format ( [ { role : "user" , content : systemPrompt } , ...messages ] )
125+ } else if ( this . glmConfig . isGlmModel ) {
126+ // GLM models benefit from mergeToolResultText to prevent reasoning_content loss
127+ const glmConvertedMessages = convertToZAiFormat ( messages , {
128+ mergeToolResultText : this . glmConfig . mergeToolResultText ,
129+ } )
130+ convertedMessages = [ systemMessage , ...glmConvertedMessages ]
109131 } else {
110132 if ( modelInfo . supportsPromptCache ) {
111133 systemMessage = {
@@ -152,6 +174,16 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
152174
153175 const isGrokXAI = this . _isGrokXAI ( this . options . openAiBaseUrl )
154176
177+ // Determine parallel_tool_calls setting
178+ // Disable for GLM models as they may not support it properly
179+ let parallelToolCalls : boolean
180+ if ( this . glmConfig . isGlmModel && this . glmConfig . disableParallelToolCalls ) {
181+ parallelToolCalls = false
182+ console . log ( `[${ this . providerName } ] parallel_tool_calls disabled for GLM model` )
183+ } else {
184+ parallelToolCalls = metadata ?. parallelToolCalls ?? true
185+ }
186+
155187 const requestOptions : OpenAI . Chat . Completions . ChatCompletionCreateParamsStreaming = {
156188 model : modelId ,
157189 temperature : this . options . modelTemperature ?? ( deepseekReasoner ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0 ) ,
@@ -161,12 +193,19 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
161193 ...( reasoning && reasoning ) ,
162194 tools : this . convertToolsForOpenAI ( metadata ?. tools ) ,
163195 tool_choice : metadata ?. tool_choice ,
164- parallel_tool_calls : metadata ?. parallelToolCalls ?? true ,
196+ parallel_tool_calls : parallelToolCalls ,
165197 }
166198
167199 // Add max_tokens if needed
168200 this . addMaxTokensIfNeeded ( requestOptions , modelInfo )
169201
202+ // For GLM-4.7 models with thinking support, add thinking parameter
203+ if ( this . glmConfig . isGlmModel && this . glmConfig . supportsThinking ) {
204+ const useReasoning = this . options . enableReasoningEffort !== false // Default to enabled for GLM-4.7
205+ ; ( requestOptions as any ) . thinking = useReasoning ? { type : "enabled" } : { type : "disabled" }
206+ console . log ( `[${ this . providerName } ] GLM-4.7 thinking mode: ${ useReasoning ? "enabled" : "disabled" } ` )
207+ }
208+
170209 let stream
171210 try {
172211 stream = await this . client . chat . completions . create (
@@ -221,20 +260,46 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
221260 yield this . processUsageMetrics ( lastUsage , modelInfo )
222261 }
223262 } else {
263+ // Determine message conversion based on model type
264+ let nonStreamingMessages
265+ if ( deepseekReasoner ) {
266+ nonStreamingMessages = convertToR1Format ( [ { role : "user" , content : systemPrompt } , ...messages ] )
267+ } else if ( this . glmConfig . isGlmModel ) {
268+ // GLM models benefit from mergeToolResultText to prevent reasoning_content loss
269+ const glmConvertedMessages = convertToZAiFormat ( messages , {
270+ mergeToolResultText : this . glmConfig . mergeToolResultText ,
271+ } )
272+ nonStreamingMessages = [ systemMessage , ...glmConvertedMessages ]
273+ } else {
274+ nonStreamingMessages = [ systemMessage , ...convertToOpenAiMessages ( messages ) ]
275+ }
276+
277+ // Determine parallel_tool_calls setting for non-streaming
278+ let nonStreamingParallelToolCalls : boolean
279+ if ( this . glmConfig . isGlmModel && this . glmConfig . disableParallelToolCalls ) {
280+ nonStreamingParallelToolCalls = false
281+ } else {
282+ nonStreamingParallelToolCalls = metadata ?. parallelToolCalls ?? true
283+ }
284+
224285 const requestOptions : OpenAI . Chat . Completions . ChatCompletionCreateParamsNonStreaming = {
225286 model : modelId ,
226- messages : deepseekReasoner
227- ? convertToR1Format ( [ { role : "user" , content : systemPrompt } , ...messages ] )
228- : [ systemMessage , ...convertToOpenAiMessages ( messages ) ] ,
287+ messages : nonStreamingMessages ,
229288 // Tools are always present (minimum ALWAYS_AVAILABLE_TOOLS)
230289 tools : this . convertToolsForOpenAI ( metadata ?. tools ) ,
231290 tool_choice : metadata ?. tool_choice ,
232- parallel_tool_calls : metadata ?. parallelToolCalls ?? true ,
291+ parallel_tool_calls : nonStreamingParallelToolCalls ,
233292 }
234293
235294 // Add max_tokens if needed
236295 this . addMaxTokensIfNeeded ( requestOptions , modelInfo )
237296
297+ // For GLM-4.7 models with thinking support, add thinking parameter
298+ if ( this . glmConfig . isGlmModel && this . glmConfig . supportsThinking ) {
299+ const useReasoning = this . options . enableReasoningEffort !== false
300+ ; ( requestOptions as any ) . thinking = useReasoning ? { type : "enabled" } : { type : "disabled" }
301+ }
302+
238303 let response
239304 try {
240305 response = await this . client . chat . completions . create (
0 commit comments