Skip to main content

codex_convert_proxy/providers/
glm.rs

1//! GLM provider implementation.
2
3use crate::providers::trait_::Provider;
4use crate::types::chat_api::{ChatRequest, ChatResponse, ChatStreamChunk};
5
6/// GLM (Zhipu AI) provider.
7///
8/// GLM has some specific requirements:
9/// - Does not support function calling tools
10/// - Messages should be flattened to simple text format
11/// - API path is /chat/completions (not /v1/chat/completions)
12pub struct GLMProvider;
13
14impl Default for GLMProvider {
15    fn default() -> Self {
16        Self
17    }
18}
19
20impl GLMProvider {
21    pub fn new() -> Self {
22        Self
23    }
24}
25
26impl Provider for GLMProvider {
27    fn name(&self) -> &'static str {
28        "glm"
29    }
30
31    fn chat_completions_path(&self) -> String {
32        // GLM base_path already includes version prefix (/api/paas/v4),
33        // so we only need the endpoint suffix.
34        "/chat/completions".to_string()
35    }
36
37    fn transform_request(&self, request: &mut ChatRequest) {
38        // GLM doesn't support tools - remove them
39        request.tools = None;
40        request.tool_choice = None;
41
42        // Flatten message content to simple strings
43        for message in &mut request.messages {
44            // GLM doesn't support developer role - convert to user
45            if message.role == crate::types::chat_api::MessageRole::Developer {
46                message.role = crate::types::chat_api::MessageRole::User;
47            }
48            let text = message.content.as_text();
49            message.content = crate::types::chat_api::Content::String(text);
50        }
51    }
52
53    fn transform_response(&self, response: &mut ChatResponse) {
54        // Ensure content is string format
55        for choice in &mut response.choices {
56            let text = choice.message.content.as_text();
57            choice.message.content = crate::types::chat_api::Content::String(text);
58        }
59    }
60
61    fn transform_stream_chunk(&self, chunk: &mut ChatStreamChunk) {
62        // Ensure delta content is string format
63        for choice in &mut chunk.choices {
64            if let Some(delta) = &mut choice.delta
65                && let Some(content) = &delta.content {
66                    let text = content.as_text();
67                    if !text.is_empty() {
68                        delta.content = Some(crate::types::chat_api::Content::String(text));
69                    }
70                }
71        }
72    }
73
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use crate::types::chat_api::{ChatMessage, Content, MessageRole};
80
81    #[test]
82    fn test_glm_removes_tools() {
83        let mut request = ChatRequest {
84            model: "glm-4".to_string(),
85            messages: vec![ChatMessage {
86                role: MessageRole::User,
87                content: Content::String("Hello".to_string()),
88                name: None,
89                annotations: None,
90                tool_calls: None,
91                tool_call_id: None,
92                function_call: None,
93                refusal: None,
94            }],
95            tools: Some(vec![]),
96            tool_choice: None,
97            stream: Some(false),
98            temperature: None,
99            max_tokens: None,
100            top_p: None,
101            user: None,
102            stream_options: None,
103            frequency_penalty: None,
104            presence_penalty: None,
105            logit_bias: None,
106            logprobs: None,
107            top_logprobs: None,
108            n: None,
109            stop: None,
110            response_format: None,
111            reasoning_effort: None,
112            parallel_tool_calls: None,
113            seed: None,
114            service_tier: None,
115            web_search_options: None,
116            modalities: None,
117            prediction: None,
118            audio: None,
119        };
120
121        let provider = GLMProvider;
122        provider.transform_request(&mut request);
123
124        assert!(request.tools.is_none());
125        assert!(request.tool_choice.is_none());
126    }
127
128    #[test]
129    fn test_glm_flattens_content() {
130        let mut request = ChatRequest {
131            model: "glm-4".to_string(),
132            messages: vec![ChatMessage {
133                role: MessageRole::User,
134                content: Content::Array(vec![crate::types::chat_api::ContentBlock {
135                    block_type: "text".to_string(),
136                    text: Some("Hello".to_string()),
137                    image_url: None,
138                    input_audio: None,
139                    file: None,
140                    refusal: None,
141                }]),
142                name: None,
143                annotations: None,
144                tool_calls: None,
145                tool_call_id: None,
146                function_call: None,
147                refusal: None,
148            }],
149            tools: None,
150            tool_choice: None,
151            stream: Some(false),
152            temperature: None,
153            max_tokens: None,
154            top_p: None,
155            user: None,
156            stream_options: None,
157            frequency_penalty: None,
158            presence_penalty: None,
159            logit_bias: None,
160            logprobs: None,
161            top_logprobs: None,
162            n: None,
163            stop: None,
164            response_format: None,
165            reasoning_effort: None,
166            parallel_tool_calls: None,
167            seed: None,
168            service_tier: None,
169            web_search_options: None,
170            modalities: None,
171            prediction: None,
172            audio: None,
173        };
174
175        let provider = GLMProvider;
176        provider.transform_request(&mut request);
177
178        let msg = request.messages.first().unwrap();
179        assert!(matches!(msg.content, Content::String(_)));
180        assert_eq!(msg.content.as_text(), "Hello");
181    }
182}