Skip to main content

rig_core/providers/
zai.rs

1//! Z.AI API clients and Rig integrations.
2//!
3//! Z.AI exposes OpenAI-compatible APIs for both its general platform and
4//! coding-focused platform, plus an Anthropic-compatible endpoint for tools
5//! like Claude Code.
6//!
7//! # OpenAI-compatible example
8//! ```no_run
9//! use rig_core::client::CompletionClient;
10//! use rig_core::providers::zai;
11//!
12//! let client = zai::Client::new("YOUR_API_KEY").expect("Failed to build client");
13//! let glm_4_6 = client.completion_model(zai::GLM_4_6);
14//! ```
15//!
16//! # Anthropic-compatible example
17//! ```no_run
18//! use rig_core::client::CompletionClient;
19//! use rig_core::providers::zai;
20//!
21//! let client = zai::AnthropicClient::new("YOUR_API_KEY").expect("Failed to build client");
22//! let glm_4_6 = client.completion_model(zai::GLM_4_6);
23//! ```
24
25use crate::client::{
26    self, BearerAuth, Capabilities, Capable, DebugExt, Nothing, Provider, ProviderBuilder,
27    ProviderClient,
28};
29use crate::http_client::{self, HttpClientExt};
30use crate::providers::anthropic::client::{
31    AnthropicBuilder as AnthropicCompatBuilder, AnthropicKey, finish_anthropic_builder,
32};
33
34/// General-purpose OpenAI-compatible base URL.
35pub const GENERAL_API_BASE_URL: &str = "https://api.z.ai/api/paas/v4";
36/// Coding-focused OpenAI-compatible base URL.
37pub const CODING_API_BASE_URL: &str = "https://api.z.ai/api/coding/paas/v4";
38/// Anthropic-compatible base URL.
39pub const ANTHROPIC_API_BASE_URL: &str = "https://api.z.ai/api/anthropic";
40
41/// `glm-4.6`
42pub const GLM_4_6: &str = "glm-4.6";
43/// `glm-4.6-air`
44pub const GLM_4_6_AIR: &str = "glm-4.6-air";
45/// `glm-4.6-x`
46pub const GLM_4_6_X: &str = "glm-4.6-x";
47/// `glm-4.5`
48pub const GLM_4_5: &str = "glm-4.5";
49/// `glm-4.5-air`
50pub const GLM_4_5_AIR: &str = "glm-4.5-air";
51/// `glm-4.5v`
52pub const GLM_4_5V: &str = "glm-4.5v";
53/// `glm-4.5-airx`
54pub const GLM_4_5_AIRX: &str = "glm-4.5-airx";
55
56#[derive(Debug, Default, Clone, Copy)]
57pub struct ZAiExt;
58
59#[derive(Debug, Default, Clone, Copy)]
60pub struct ZAiBuilder;
61
62#[derive(Debug, Default, Clone)]
63pub struct ZAiAnthropicBuilder {
64    anthropic: AnthropicCompatBuilder,
65}
66
67#[derive(Debug, Default, Clone, Copy)]
68pub struct ZAiAnthropicExt;
69
70type ZAiApiKey = BearerAuth;
71
72pub type Client<H = reqwest::Client> = client::Client<ZAiExt, H>;
73pub type ClientBuilder<H = crate::markers::Missing> =
74    client::ClientBuilder<ZAiBuilder, ZAiApiKey, H>;
75
76pub type AnthropicClient<H = reqwest::Client> = client::Client<ZAiAnthropicExt, H>;
77pub type AnthropicClientBuilder<H = crate::markers::Missing> =
78    client::ClientBuilder<ZAiAnthropicBuilder, AnthropicKey, H>;
79
80impl Provider for ZAiExt {
81    type Builder = ZAiBuilder;
82
83    const VERIFY_PATH: &'static str = "/models";
84}
85
86impl Provider for ZAiAnthropicExt {
87    type Builder = ZAiAnthropicBuilder;
88
89    const VERIFY_PATH: &'static str = "/v1/models";
90}
91
92impl<H> Capabilities<H> for ZAiExt {
93    type Completion = Capable<super::openai::completion::GenericCompletionModel<ZAiExt, H>>;
94    type Embeddings = Nothing;
95    type Transcription = Nothing;
96    type ModelListing = Nothing;
97    #[cfg(feature = "image")]
98    type ImageGeneration = Nothing;
99    #[cfg(feature = "audio")]
100    type AudioGeneration = Nothing;
101    type Rerank = Nothing;
102}
103
104impl<H> Capabilities<H> for ZAiAnthropicExt {
105    type Completion =
106        Capable<super::anthropic::completion::GenericCompletionModel<ZAiAnthropicExt, H>>;
107    type Embeddings = Nothing;
108    type Transcription = Nothing;
109    type ModelListing = Nothing;
110    #[cfg(feature = "image")]
111    type ImageGeneration = Nothing;
112    #[cfg(feature = "audio")]
113    type AudioGeneration = Nothing;
114    type Rerank = Nothing;
115}
116
117impl DebugExt for ZAiExt {}
118impl DebugExt for ZAiAnthropicExt {}
119
120impl super::openai::completion::OpenAICompatibleProvider for ZAiExt {
121    const PROVIDER_NAME: &'static str = "zai";
122
123    type StreamingUsage = super::openai::Usage;
124
125    type Response = super::openai::CompletionResponse;
126}
127
128impl ProviderBuilder for ZAiBuilder {
129    type Extension<H>
130        = ZAiExt
131    where
132        H: HttpClientExt;
133    type ApiKey = ZAiApiKey;
134
135    const BASE_URL: &'static str = GENERAL_API_BASE_URL;
136
137    fn build<H>(
138        _builder: &client::ClientBuilder<Self, Self::ApiKey, H>,
139    ) -> http_client::Result<Self::Extension<H>>
140    where
141        H: HttpClientExt,
142    {
143        Ok(ZAiExt)
144    }
145}
146
147impl ProviderBuilder for ZAiAnthropicBuilder {
148    type Extension<H>
149        = ZAiAnthropicExt
150    where
151        H: HttpClientExt;
152    type ApiKey = AnthropicKey;
153
154    const BASE_URL: &'static str = ANTHROPIC_API_BASE_URL;
155
156    fn build<H>(
157        _builder: &client::ClientBuilder<Self, Self::ApiKey, H>,
158    ) -> http_client::Result<Self::Extension<H>>
159    where
160        H: HttpClientExt,
161    {
162        Ok(ZAiAnthropicExt)
163    }
164
165    fn finish<H>(
166        &self,
167        builder: client::ClientBuilder<Self, AnthropicKey, H>,
168    ) -> http_client::Result<client::ClientBuilder<Self, AnthropicKey, H>> {
169        finish_anthropic_builder(&self.anthropic, builder)
170    }
171}
172
173impl super::anthropic::completion::AnthropicCompatibleProvider for ZAiAnthropicExt {
174    const PROVIDER_NAME: &'static str = "z.ai";
175
176    fn default_max_tokens(_model: &str) -> Option<u64> {
177        Some(4096)
178    }
179}
180
181impl ProviderClient for Client {
182    type Input = ZAiApiKey;
183    type Error = crate::client::ProviderClientError;
184
185    fn from_env() -> Result<Self, Self::Error> {
186        let api_key = crate::client::required_env_var("ZAI_API_KEY")?;
187        let mut builder = Self::builder().api_key(api_key);
188
189        if let Some(base_url) = crate::client::optional_env_var("ZAI_API_BASE")? {
190            builder = builder.base_url(base_url);
191        }
192
193        builder.build().map_err(Into::into)
194    }
195
196    fn from_val(input: Self::Input) -> Result<Self, Self::Error> {
197        Self::new(input).map_err(Into::into)
198    }
199}
200
201impl ProviderClient for AnthropicClient {
202    type Input = String;
203    type Error = crate::client::ProviderClientError;
204
205    fn from_env() -> Result<Self, Self::Error> {
206        let api_key = crate::client::required_env_var("ZAI_API_KEY")?;
207        let mut builder = Self::builder().api_key(api_key);
208
209        if let Some(base_url) = anthropic_base_override("ZAI_ANTHROPIC_API_BASE", "ZAI_API_BASE")? {
210            builder = builder.base_url(base_url);
211        }
212
213        builder.build().map_err(Into::into)
214    }
215
216    fn from_val(input: Self::Input) -> Result<Self, Self::Error> {
217        Self::builder().api_key(input).build().map_err(Into::into)
218    }
219}
220
221fn anthropic_base_override(
222    primary_env: &'static str,
223    fallback_env: &'static str,
224) -> crate::client::ProviderClientResult<Option<String>> {
225    let primary = crate::client::optional_env_var(primary_env)?;
226    let fallback = crate::client::optional_env_var(fallback_env)?;
227
228    Ok(resolve_anthropic_base_override(
229        primary.as_deref(),
230        fallback.as_deref(),
231    ))
232}
233
234fn resolve_anthropic_base_override(
235    primary: Option<&str>,
236    fallback: Option<&str>,
237) -> Option<String> {
238    primary
239        .map(str::to_owned)
240        .or_else(|| fallback.and_then(normalize_anthropic_base_url))
241}
242
243fn normalize_anthropic_base_url(base_url: &str) -> Option<String> {
244    if base_url.contains("/anthropic") {
245        return Some(base_url.to_owned());
246    }
247
248    match base_url.trim_end_matches('/') {
249        GENERAL_API_BASE_URL | CODING_API_BASE_URL => Some(ANTHROPIC_API_BASE_URL.to_owned()),
250        _ => {
251            let mut url = url::Url::parse(base_url).ok()?;
252            if !matches!(
253                url.path(),
254                "/api/paas/v4" | "/api/paas/v4/" | "/api/coding/paas/v4" | "/api/coding/paas/v4/"
255            ) {
256                return None;
257            }
258            url.set_path("/api/anthropic");
259            Some(url.to_string())
260        }
261    }
262}
263
264impl<H> ClientBuilder<H> {
265    pub fn general(self) -> Self {
266        self.base_url(GENERAL_API_BASE_URL)
267    }
268
269    pub fn coding(self) -> Self {
270        self.base_url(CODING_API_BASE_URL)
271    }
272}
273
274impl<H> AnthropicClientBuilder<H> {
275    pub fn general(self) -> Self {
276        self.base_url(ANTHROPIC_API_BASE_URL)
277    }
278
279    pub fn anthropic_version(self, anthropic_version: &str) -> Self {
280        self.over_ext(|mut ext| {
281            ext.anthropic.anthropic_version = anthropic_version.into();
282            ext
283        })
284    }
285
286    pub fn anthropic_betas(self, anthropic_betas: &[&str]) -> Self {
287        self.over_ext(|mut ext| {
288            ext.anthropic
289                .anthropic_betas
290                .extend(anthropic_betas.iter().copied().map(String::from));
291            ext
292        })
293    }
294
295    pub fn anthropic_beta(self, anthropic_beta: &str) -> Self {
296        self.over_ext(|mut ext| {
297            ext.anthropic.anthropic_betas.push(anthropic_beta.into());
298            ext
299        })
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::{
306        ANTHROPIC_API_BASE_URL, CODING_API_BASE_URL, GENERAL_API_BASE_URL,
307        normalize_anthropic_base_url, resolve_anthropic_base_override,
308    };
309
310    #[test]
311    fn test_client_initialization() {
312        let _client = crate::providers::zai::Client::new("dummy-key").expect("Client::new()");
313        let _client_from_builder = crate::providers::zai::Client::builder()
314            .api_key("dummy-key")
315            .build()
316            .expect("Client::builder()");
317        let _anthropic_client = crate::providers::zai::AnthropicClient::new("dummy-key")
318            .expect("AnthropicClient::new()");
319        let _anthropic_client_from_builder = crate::providers::zai::AnthropicClient::builder()
320            .api_key("dummy-key")
321            .build()
322            .expect("AnthropicClient::builder()");
323    }
324
325    #[test]
326    fn normalize_openai_style_bases_to_anthropic_base() {
327        assert_eq!(
328            normalize_anthropic_base_url(GENERAL_API_BASE_URL).as_deref(),
329            Some(ANTHROPIC_API_BASE_URL)
330        );
331        assert_eq!(
332            normalize_anthropic_base_url(CODING_API_BASE_URL).as_deref(),
333            Some(ANTHROPIC_API_BASE_URL)
334        );
335        assert_eq!(
336            normalize_anthropic_base_url("https://proxy.example.com/api/paas/v4").as_deref(),
337            Some("https://proxy.example.com/api/anthropic")
338        );
339        assert_eq!(
340            normalize_anthropic_base_url("https://proxy.example.com/api/coding/paas/v4").as_deref(),
341            Some("https://proxy.example.com/api/anthropic")
342        );
343    }
344
345    #[test]
346    fn normalize_preserves_existing_anthropic_base() {
347        assert_eq!(
348            normalize_anthropic_base_url("https://proxy.example.com/api/anthropic").as_deref(),
349            Some("https://proxy.example.com/api/anthropic")
350        );
351    }
352
353    #[test]
354    fn anthropic_primary_override_wins() {
355        let override_url = resolve_anthropic_base_override(
356            Some("https://primary.example.com/api/anthropic"),
357            Some(GENERAL_API_BASE_URL),
358        );
359
360        assert_eq!(
361            override_url.as_deref(),
362            Some("https://primary.example.com/api/anthropic")
363        );
364    }
365}