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 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}