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