Skip to main content

zai_rs/client/
endpoints.rs

1//! Endpoint registry for Zhipu AI / BigModel API families.
2//!
3//! Keep endpoint bases and paths centralized so product modules describe
4//! behavior instead of re-encoding transport URLs.
5
6/// Official default base for general PAAS v4 APIs.
7pub const PAAS_V4_BASE: &str = "https://open.bigmodel.cn/api/paas/v4";
8/// Official default base for Coding Plan PAAS v4 APIs.
9pub const CODING_PAAS_V4_BASE: &str = "https://open.bigmodel.cn/api/coding/paas/v4";
10/// Official default base for knowledge-base APIs.
11pub const LLM_APPLICATION_BASE: &str = "https://open.bigmodel.cn/api/llm-application/open";
12/// Official default base for realtime APIs.
13///
14/// Verified against the official GLM-Realtime protocol
15/// (<https://github.com/MetaGLM/glm-realtime-sdk/blob/main/GLM-Realtime-doc-for-llm.md>
16/// and <https://docs.bigmodel.cn/cn/asyncapi/realtime>): the realtime endpoint
17/// lives under `/api/paas/v4/realtime`, same v4 family as the REST APIs.
18pub const REALTIME_BASE: &str = "wss://open.bigmodel.cn/api/paas/v4/realtime";
19
20/// Official default base for the monitor / usage-statistics APIs.
21///
22/// The GLM Coding Plan exposes a usage/quota query endpoint under
23/// `/api/monitor/usage/quota/limit` (verified via the official
24/// `glm-plan-usage` plugin at
25/// <https://docs.bigmodel.cn/cn/coding-plan/extension/usage-query-plugin> and
26/// the community CLI <https://github.com/JinHanAI/coding-plan-monitor>). It
27/// returns the per-5-hour and weekly quota consumption/remaining amounts.
28pub const MONITOR_BASE: &str = "https://open.bigmodel.cn/api/monitor";
29
30/// API family selector used by [`EndpointConfig`].
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum ApiBase {
33    PaasV4,
34    CodingPaasV4,
35    LlmApplication,
36    Realtime,
37    /// Monitor / usage-statistics endpoints (Coding Plan quota query).
38    Monitor,
39    Custom(String),
40}
41
42/// Runtime-configurable API bases.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct EndpointConfig {
45    pub paas_v4_base: String,
46    pub coding_paas_v4_base: String,
47    pub llm_application_base: String,
48    pub realtime_base: String,
49    /// Monitor / usage-statistics base URL.
50    pub monitor_base: String,
51}
52
53impl Default for EndpointConfig {
54    fn default() -> Self {
55        Self {
56            paas_v4_base: PAAS_V4_BASE.to_string(),
57            coding_paas_v4_base: CODING_PAAS_V4_BASE.to_string(),
58            llm_application_base: LLM_APPLICATION_BASE.to_string(),
59            realtime_base: REALTIME_BASE.to_string(),
60            monitor_base: MONITOR_BASE.to_string(),
61        }
62    }
63}
64
65impl EndpointConfig {
66    pub fn with_paas_v4_base(mut self, base: impl Into<String>) -> Self {
67        self.paas_v4_base = base.into();
68        self
69    }
70
71    pub fn with_coding_paas_v4_base(mut self, base: impl Into<String>) -> Self {
72        self.coding_paas_v4_base = base.into();
73        self
74    }
75
76    pub fn with_llm_application_base(mut self, base: impl Into<String>) -> Self {
77        self.llm_application_base = base.into();
78        self
79    }
80
81    pub fn with_realtime_base(mut self, base: impl Into<String>) -> Self {
82        self.realtime_base = base.into();
83        self
84    }
85
86    /// Override the monitor / usage-statistics base URL.
87    pub fn with_monitor_base(mut self, base: impl Into<String>) -> Self {
88        self.monitor_base = base.into();
89        self
90    }
91
92    pub fn base<'a>(&'a self, api_base: &'a ApiBase) -> &'a str {
93        match api_base {
94            ApiBase::PaasV4 => &self.paas_v4_base,
95            ApiBase::CodingPaasV4 => &self.coding_paas_v4_base,
96            ApiBase::LlmApplication => &self.llm_application_base,
97            ApiBase::Realtime => &self.realtime_base,
98            ApiBase::Monitor => &self.monitor_base,
99            ApiBase::Custom(base) => base,
100        }
101    }
102
103    pub fn url(&self, api_base: &ApiBase, path: &str) -> String {
104        join_url(self.base(api_base), path)
105    }
106}
107
108/// Join a base URL and an endpoint path without duplicating slashes.
109pub fn join_url(base: &str, path: &str) -> String {
110    let base = base.trim_end_matches('/');
111    let path = path.trim_start_matches('/');
112    if path.is_empty() {
113        base.to_string()
114    } else {
115        format!("{}/{}", base, path)
116    }
117}
118
119/// Build a URL by appending query parameters to a base.
120///
121/// Centralizes query-string construction across list-style endpoints so every
122/// module uses identical (and panic-free) percent-encoding via `url::Url`. The
123/// iterator is generic so callers may pass `Vec<(&str, String)>`, arrays, etc.
124///
125/// If `base_url` is not a parseable absolute URL (a malformed user-supplied
126/// `ApiBase::Custom`), it falls back to naive string concatenation rather than
127/// panicking — a genuinely broken base will surface as a network error at
128/// request time. This keeps the fluent `with_*` builder API infallible.
129pub fn build_query<K, V, I>(base_url: &str, params: I) -> String
130where
131    K: AsRef<str>,
132    V: AsRef<str>,
133    I: IntoIterator<Item = (K, V)>,
134{
135    let collected: Vec<(K, V)> = params.into_iter().collect();
136    if let Ok(mut url) = url::Url::parse(base_url) {
137        if !collected.is_empty() {
138            url.query_pairs_mut()
139                .extend_pairs(collected.iter().map(|(k, v)| (k.as_ref(), v.as_ref())));
140        }
141        return url.to_string();
142    }
143
144    // Fallback for non-parseable bases (malformed custom URL): best-effort join.
145    if collected.is_empty() {
146        return base_url.to_string();
147    }
148    let query = collected
149        .iter()
150        .map(|(k, v)| format!("{}={}", k.as_ref(), v.as_ref()))
151        .collect::<Vec<_>>()
152        .join("&");
153    format!("{base_url}?{query}")
154}
155
156pub mod paths {
157    pub const CHAT_COMPLETIONS: &str = "chat/completions";
158    pub const ASYNC_CHAT_COMPLETIONS: &str = "async/chat/completions";
159    pub const ASYNC_RESULT: &str = "async-result";
160    pub const EMBEDDINGS: &str = "embeddings";
161    pub const RERANK: &str = "rerank";
162    pub const TOKENIZER: &str = "tokenizer";
163    pub const MODERATIONS: &str = "moderations";
164    pub const IMAGES_GENERATIONS: &str = "images/generations";
165    pub const VIDEOS_GENERATIONS: &str = "videos/generations";
166    pub const AUDIO_TRANSCRIPTIONS: &str = "audio/transcriptions";
167    pub const AUDIO_SPEECH: &str = "audio/speech";
168    pub const VOICE_CLONE: &str = "voice/clone";
169    pub const VOICE_LIST: &str = "voice/list";
170    pub const VOICE_DELETE: &str = "voice/delete";
171    pub const FILES: &str = "files";
172    pub const FILES_OCR: &str = "files/ocr";
173    pub const FILE_PARSER_CREATE: &str = "files/parser/create";
174    pub const FILE_PARSER_RESULT: &str = "files/parser/result";
175    pub const WEB_SEARCH: &str = "web_search";
176    pub const BATCHES: &str = "batches";
177    pub const AGENTS: &str = "agents";
178
179    pub const KNOWLEDGE: &str = "knowledge";
180    pub const KNOWLEDGE_CAPACITY: &str = "knowledge/capacity";
181    pub const DOCUMENT: &str = "document";
182    pub const DOCUMENT_UPLOAD_URL: &str = "document/upload_url";
183    pub const DOCUMENT_UPLOAD_DOCUMENT: &str = "document/upload_document";
184    pub const DOCUMENT_EMBEDDING: &str = "document/embedding";
185    pub const DOCUMENT_SLICE_IMAGE_LIST: &str = "document/slice/image_list";
186
187    /// GLM Coding Plan quota/usage query (GET, under the monitor base).
188    /// Returns per-5-hour and weekly quota consumption + remaining amounts.
189    pub const MONITOR_USAGE_QUOTA_LIMIT: &str = "usage/quota/limit";
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn joins_base_and_path_without_double_slashes() {
198        assert_eq!(
199            join_url("https://open.bigmodel.cn/api/paas/v4/", "/chat/completions"),
200            "https://open.bigmodel.cn/api/paas/v4/chat/completions"
201        );
202    }
203
204    #[test]
205    fn default_config_uses_official_bases() {
206        let config = EndpointConfig::default();
207        assert_eq!(config.paas_v4_base, PAAS_V4_BASE);
208        assert_eq!(config.coding_paas_v4_base, CODING_PAAS_V4_BASE);
209        assert_eq!(config.llm_application_base, LLM_APPLICATION_BASE);
210    }
211
212    #[test]
213    fn realtime_base_matches_official_endpoint() {
214        // Verified: https://github.com/MetaGLM/glm-realtime-sdk and
215        // https://docs.bigmodel.cn/cn/asyncapi/realtime
216        assert_eq!(REALTIME_BASE, "wss://open.bigmodel.cn/api/paas/v4/realtime");
217    }
218
219    #[test]
220    fn realtime_url_is_buildable_from_endpoint_config() {
221        let url = EndpointConfig::default().url(&ApiBase::Realtime, "");
222        assert_eq!(url, REALTIME_BASE);
223        // join_url must not mangle the wss:// scheme.
224        assert!(url.starts_with("wss://"));
225    }
226
227    #[test]
228    fn build_query_appends_percent_encoded_pairs() {
229        let url = build_query(
230            "https://open.bigmodel.cn/api/paas/v4/files",
231            [("purpose", "batch".to_string()), ("limit", "2".to_string())],
232        );
233        assert_eq!(
234            url,
235            "https://open.bigmodel.cn/api/paas/v4/files?purpose=batch&limit=2"
236        );
237    }
238
239    #[test]
240    fn build_query_without_params_returns_base() {
241        let url = build_query(
242            "https://open.bigmodel.cn/api/paas/v4/files",
243            Vec::<(String, String)>::new(),
244        );
245        assert_eq!(url, "https://open.bigmodel.cn/api/paas/v4/files");
246    }
247
248    #[test]
249    fn build_query_falls_back_for_malformed_base() {
250        // A non-parseable base must not panic.
251        let url = build_query("not a url", [("limit", "5".to_string())]);
252        assert_eq!(url, "not a url?limit=5");
253    }
254}