Skip to main content

rig/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::client::CompletionClient;
10//! use rig::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::client::CompletionClient;
19//! use rig::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 = reqwest::Client> = client::ClientBuilder<ZAiBuilder, ZAiApiKey, H>;
74
75pub type AnthropicClient<H = reqwest::Client> = client::Client<ZAiAnthropicExt, H>;
76pub type AnthropicClientBuilder<H = reqwest::Client> =
77    client::ClientBuilder<ZAiAnthropicBuilder, AnthropicKey, H>;
78
79impl Provider for ZAiExt {
80    type Builder = ZAiBuilder;
81
82    const VERIFY_PATH: &'static str = "/models";
83}
84
85impl Provider for ZAiAnthropicExt {
86    type Builder = ZAiAnthropicBuilder;
87
88    const VERIFY_PATH: &'static str = "/v1/models";
89}
90
91impl<H> Capabilities<H> for ZAiExt {
92    type Completion = Capable<super::openai::completion::GenericCompletionModel<ZAiExt, H>>;
93    type Embeddings = Nothing;
94    type Transcription = Nothing;
95    type ModelListing = Nothing;
96    #[cfg(feature = "image")]
97    type ImageGeneration = Nothing;
98    #[cfg(feature = "audio")]
99    type AudioGeneration = Nothing;
100}
101
102impl<H> Capabilities<H> for ZAiAnthropicExt {
103    type Completion =
104        Capable<super::anthropic::completion::GenericCompletionModel<ZAiAnthropicExt, H>>;
105    type Embeddings = Nothing;
106    type Transcription = Nothing;
107    type ModelListing = Nothing;
108    #[cfg(feature = "image")]
109    type ImageGeneration = Nothing;
110    #[cfg(feature = "audio")]
111    type AudioGeneration = Nothing;
112}
113
114impl DebugExt for ZAiExt {}
115impl DebugExt for ZAiAnthropicExt {}
116
117impl ProviderBuilder for ZAiBuilder {
118    type Extension<H>
119        = ZAiExt
120    where
121        H: HttpClientExt;
122    type ApiKey = ZAiApiKey;
123
124    const BASE_URL: &'static str = GENERAL_API_BASE_URL;
125
126    fn build<H>(
127        _builder: &client::ClientBuilder<Self, Self::ApiKey, H>,
128    ) -> http_client::Result<Self::Extension<H>>
129    where
130        H: HttpClientExt,
131    {
132        Ok(ZAiExt)
133    }
134}
135
136impl ProviderBuilder for ZAiAnthropicBuilder {
137    type Extension<H>
138        = ZAiAnthropicExt
139    where
140        H: HttpClientExt;
141    type ApiKey = AnthropicKey;
142
143    const BASE_URL: &'static str = ANTHROPIC_API_BASE_URL;
144
145    fn build<H>(
146        _builder: &client::ClientBuilder<Self, Self::ApiKey, H>,
147    ) -> http_client::Result<Self::Extension<H>>
148    where
149        H: HttpClientExt,
150    {
151        Ok(ZAiAnthropicExt)
152    }
153
154    fn finish<H>(
155        &self,
156        builder: client::ClientBuilder<Self, AnthropicKey, H>,
157    ) -> http_client::Result<client::ClientBuilder<Self, AnthropicKey, H>> {
158        finish_anthropic_builder(&self.anthropic, builder)
159    }
160}
161
162impl super::anthropic::completion::AnthropicCompatibleProvider for ZAiAnthropicExt {
163    const PROVIDER_NAME: &'static str = "z.ai";
164
165    fn default_max_tokens(_model: &str) -> Option<u64> {
166        Some(4096)
167    }
168}
169
170impl ProviderClient for Client {
171    type Input = ZAiApiKey;
172    type Error = crate::client::ProviderClientError;
173
174    fn from_env() -> Result<Self, Self::Error> {
175        let api_key = crate::client::required_env_var("ZAI_API_KEY")?;
176        let mut builder = Self::builder().api_key(api_key);
177
178        if let Some(base_url) = crate::client::optional_env_var("ZAI_API_BASE")? {
179            builder = builder.base_url(base_url);
180        }
181
182        builder.build().map_err(Into::into)
183    }
184
185    fn from_val(input: Self::Input) -> Result<Self, Self::Error> {
186        Self::new(input).map_err(Into::into)
187    }
188}
189
190impl ProviderClient for AnthropicClient {
191    type Input = String;
192    type Error = crate::client::ProviderClientError;
193
194    fn from_env() -> Result<Self, Self::Error> {
195        let api_key = crate::client::required_env_var("ZAI_API_KEY")?;
196        let mut builder = Self::builder().api_key(api_key);
197
198        if let Some(base_url) = anthropic_base_override("ZAI_ANTHROPIC_API_BASE", "ZAI_API_BASE")? {
199            builder = builder.base_url(base_url);
200        }
201
202        builder.build().map_err(Into::into)
203    }
204
205    fn from_val(input: Self::Input) -> Result<Self, Self::Error> {
206        Self::builder().api_key(input).build().map_err(Into::into)
207    }
208}
209
210fn anthropic_base_override(
211    primary_env: &'static str,
212    fallback_env: &'static str,
213) -> crate::client::ProviderClientResult<Option<String>> {
214    let primary = crate::client::optional_env_var(primary_env)?;
215    let fallback = crate::client::optional_env_var(fallback_env)?;
216
217    Ok(resolve_anthropic_base_override(
218        primary.as_deref(),
219        fallback.as_deref(),
220    ))
221}
222
223fn resolve_anthropic_base_override(
224    primary: Option<&str>,
225    fallback: Option<&str>,
226) -> Option<String> {
227    primary
228        .map(str::to_owned)
229        .or_else(|| fallback.and_then(normalize_anthropic_base_url))
230}
231
232fn normalize_anthropic_base_url(base_url: &str) -> Option<String> {
233    if base_url.contains("/anthropic") {
234        return Some(base_url.to_owned());
235    }
236
237    match base_url.trim_end_matches('/') {
238        GENERAL_API_BASE_URL | CODING_API_BASE_URL => Some(ANTHROPIC_API_BASE_URL.to_owned()),
239        _ => {
240            let mut url = url::Url::parse(base_url).ok()?;
241            if !matches!(
242                url.path(),
243                "/api/paas/v4" | "/api/paas/v4/" | "/api/coding/paas/v4" | "/api/coding/paas/v4/"
244            ) {
245                return None;
246            }
247            url.set_path("/api/anthropic");
248            Some(url.to_string())
249        }
250    }
251}
252
253impl<H> ClientBuilder<H> {
254    pub fn general(self) -> Self {
255        self.base_url(GENERAL_API_BASE_URL)
256    }
257
258    pub fn coding(self) -> Self {
259        self.base_url(CODING_API_BASE_URL)
260    }
261}
262
263impl<H> AnthropicClientBuilder<H> {
264    pub fn general(self) -> Self {
265        self.base_url(ANTHROPIC_API_BASE_URL)
266    }
267
268    pub fn anthropic_version(self, anthropic_version: &str) -> Self {
269        self.over_ext(|mut ext| {
270            ext.anthropic.anthropic_version = anthropic_version.into();
271            ext
272        })
273    }
274
275    pub fn anthropic_betas(self, anthropic_betas: &[&str]) -> Self {
276        self.over_ext(|mut ext| {
277            ext.anthropic
278                .anthropic_betas
279                .extend(anthropic_betas.iter().copied().map(String::from));
280            ext
281        })
282    }
283
284    pub fn anthropic_beta(self, anthropic_beta: &str) -> Self {
285        self.over_ext(|mut ext| {
286            ext.anthropic.anthropic_betas.push(anthropic_beta.into());
287            ext
288        })
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::{
295        ANTHROPIC_API_BASE_URL, CODING_API_BASE_URL, GENERAL_API_BASE_URL,
296        normalize_anthropic_base_url, resolve_anthropic_base_override,
297    };
298
299    #[test]
300    fn test_client_initialization() {
301        let _client = crate::providers::zai::Client::new("dummy-key").expect("Client::new()");
302        let _client_from_builder = crate::providers::zai::Client::builder()
303            .api_key("dummy-key")
304            .build()
305            .expect("Client::builder()");
306        let _anthropic_client = crate::providers::zai::AnthropicClient::new("dummy-key")
307            .expect("AnthropicClient::new()");
308        let _anthropic_client_from_builder = crate::providers::zai::AnthropicClient::builder()
309            .api_key("dummy-key")
310            .build()
311            .expect("AnthropicClient::builder()");
312    }
313
314    #[test]
315    fn normalize_openai_style_bases_to_anthropic_base() {
316        assert_eq!(
317            normalize_anthropic_base_url(GENERAL_API_BASE_URL).as_deref(),
318            Some(ANTHROPIC_API_BASE_URL)
319        );
320        assert_eq!(
321            normalize_anthropic_base_url(CODING_API_BASE_URL).as_deref(),
322            Some(ANTHROPIC_API_BASE_URL)
323        );
324        assert_eq!(
325            normalize_anthropic_base_url("https://proxy.example.com/api/paas/v4").as_deref(),
326            Some("https://proxy.example.com/api/anthropic")
327        );
328        assert_eq!(
329            normalize_anthropic_base_url("https://proxy.example.com/api/coding/paas/v4").as_deref(),
330            Some("https://proxy.example.com/api/anthropic")
331        );
332    }
333
334    #[test]
335    fn normalize_preserves_existing_anthropic_base() {
336        assert_eq!(
337            normalize_anthropic_base_url("https://proxy.example.com/api/anthropic").as_deref(),
338            Some("https://proxy.example.com/api/anthropic")
339        );
340    }
341
342    #[test]
343    fn anthropic_primary_override_wins() {
344        let override_url = resolve_anthropic_base_override(
345            Some("https://primary.example.com/api/anthropic"),
346            Some(GENERAL_API_BASE_URL),
347        );
348
349        assert_eq!(
350            override_url.as_deref(),
351            Some("https://primary.example.com/api/anthropic")
352        );
353    }
354}