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 uses /chat/completions not /v1/chat/completions
33        "/chat/completions".to_string()
34    }
35
36    fn transform_request(&self, request: &mut ChatRequest) {
37        // GLM doesn't support tools - remove them
38        request.tools = None;
39        request.tool_choice = None;
40
41        // Flatten message content to simple strings
42        for message in &mut request.messages {
43            // GLM doesn't support developer role - convert to user
44            if message.role == crate::types::chat_api::MessageRole::Developer {
45                message.role = crate::types::chat_api::MessageRole::User;
46            }
47            let text = message.content.as_text();
48            message.content = crate::types::chat_api::Content::String(text);
49        }
50    }
51
52    fn transform_response(&self, response: &mut ChatResponse) {
53        // Ensure content is string format
54        for choice in &mut response.choices {
55            let text = choice.message.content.as_text();
56            choice.message.content = crate::types::chat_api::Content::String(text);
57        }
58    }
59
60    fn transform_stream_chunk(&self, chunk: &mut ChatStreamChunk) {
61        // Ensure delta content is string format
62        for choice in &mut chunk.choices {
63            if let Some(delta) = &mut choice.delta
64                && let Some(content) = &delta.content {
65                    let text = content.as_text();
66                    if !text.is_empty() {
67                        delta.content = Some(crate::types::chat_api::Content::String(text));
68                    }
69                }
70        }
71    }
72
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use crate::types::chat_api::{ChatMessage, Content, MessageRole};
79
80    #[test]
81    fn test_glm_removes_tools() {
82        let mut request = ChatRequest {
83            model: "glm-4".to_string(),
84            messages: vec![ChatMessage {
85                role: MessageRole::User,
86                content: Content::String("Hello".to_string()),
87                name: None,
88                annotations: None,
89                tool_calls: None,
90                tool_call_id: None,
91                function_call: None,
92                refusal: None,
93            }],
94            tools: Some(vec![]),
95            tool_choice: None,
96            stream: Some(false),
97            temperature: None,
98            max_tokens: None,
99            top_p: None,
100            user: None,
101            stream_options: None,
102            frequency_penalty: None,
103            presence_penalty: None,
104            logit_bias: None,
105            logprobs: None,
106            top_logprobs: None,
107            n: None,
108            stop: None,
109            response_format: None,
110            reasoning_effort: None,
111            parallel_tool_calls: None,
112            seed: None,
113            service_tier: None,
114            web_search_options: None,
115            modalities: None,
116            prediction: None,
117            audio: None,
118        };
119
120        let provider = GLMProvider;
121        provider.transform_request(&mut request);
122
123        assert!(request.tools.is_none());
124        assert!(request.tool_choice.is_none());
125    }
126
127    #[test]
128    fn test_glm_flattens_content() {
129        let mut request = ChatRequest {
130            model: "glm-4".to_string(),
131            messages: vec![ChatMessage {
132                role: MessageRole::User,
133                content: Content::Array(vec![crate::types::chat_api::ContentBlock {
134                    block_type: "text".to_string(),
135                    text: Some("Hello".to_string()),
136                    image_url: None,
137                    input_audio: None,
138                    file: None,
139                    refusal: None,
140                }]),
141                name: None,
142                annotations: None,
143                tool_calls: None,
144                tool_call_id: None,
145                function_call: None,
146                refusal: None,
147            }],
148            tools: None,
149            tool_choice: None,
150            stream: Some(false),
151            temperature: None,
152            max_tokens: None,
153            top_p: None,
154            user: None,
155            stream_options: None,
156            frequency_penalty: None,
157            presence_penalty: None,
158            logit_bias: None,
159            logprobs: None,
160            top_logprobs: None,
161            n: None,
162            stop: None,
163            response_format: None,
164            reasoning_effort: None,
165            parallel_tool_calls: None,
166            seed: None,
167            service_tier: None,
168            web_search_options: None,
169            modalities: None,
170            prediction: None,
171            audio: None,
172        };
173
174        let provider = GLMProvider;
175        provider.transform_request(&mut request);
176
177        let msg = request.messages.first().unwrap();
178        assert!(matches!(msg.content, Content::String(_)));
179        assert_eq!(msg.content.as_text(), "Hello");
180    }
181}