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