1use 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 = 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}