返回 CodeWhale
zai.rs
1 //! Z.AI and Zhipu structured Web Search API contracts.
2
3 use anyhow::{Result, bail};
4 use serde_json::{Value, json};
5
6 use super::{
7 ProviderNativeSearchRequest, ProviderNativeSearchResponse, citation_from_url, push_citation,
8 };
9
10 pub(super) fn build_body(request: &ProviderNativeSearchRequest, base_url: &str) -> Result<Value> {
11 let normalized = base_url.trim().trim_end_matches('/').to_ascii_lowercase();
12 let search_engine = match normalized.as_str() {
13 "https://api.z.ai/api/paas/v4" => "search-prime",
14 "https://open.bigmodel.cn/api/paas/v4" => "search_std",
15 _ => bail!("unsupported Z.AI web-search endpoint: {base_url}"),
16 };
17 Ok(json!({
18 "search_engine": search_engine,
19 "search_query": request.query,
20 "count": request.max_results,
21 }))
22 }
23
24 pub(super) fn parse(payload: &Value) -> ProviderNativeSearchResponse {
25 let mut citations = Vec::new();
26 if let Some(results) = payload.get("search_result").and_then(Value::as_array) {
27 for result in results {
28 let Some(url) = result.get("link").and_then(Value::as_str) else {
29 continue;
30 };
31 let title = result
32 .get("title")
33 .and_then(Value::as_str)
34 .map(str::to_string);
35 let snippet = result
36 .get("content")
37 .and_then(Value::as_str)
38 .map(str::to_string);
39 let published = result
40 .get("publish_date")
41 .and_then(Value::as_str)
42 .map(str::to_string);
43 push_citation(
44 &mut citations,
45 citation_from_url(url, title, snippet, published),
46 );
47 }
48 }
49 ProviderNativeSearchResponse {
50 answer: None,
51 citations,
52 truncated: false,
53 }
54 }
55
56 #[cfg(test)]
57 mod tests {
58 use super::*;
59
60 fn request() -> ProviderNativeSearchRequest {
61 ProviderNativeSearchRequest {
62 query: "current release".to_string(),
63 max_results: 3,
64 domains: vec!["example.com".to_string()],
65 }
66 }
67
68 #[test]
69 fn request_uses_site_specific_search_engine() {
70 for (base_url, expected_engine) in [
71 ("https://api.z.ai/api/paas/v4", "search-prime"),
72 ("https://open.bigmodel.cn/api/paas/v4/", "search_std"),
73 ] {
74 let body = build_body(&request(), base_url).expect("official endpoint");
75 assert_eq!(body["search_engine"], expected_engine);
76 assert_eq!(body["search_query"], "current release");
77 assert_eq!(body["count"], 3);
78 assert!(body.get("search_domain_filter").is_none());
79 }
80 }
81
82 #[test]
83 fn request_rejects_unproven_product_surface() {
84 assert!(build_body(&request(), "https://api.z.ai/api/coding/paas/v4").is_err());
85 }
86
87 #[test]
88 fn parses_structured_search_results() {
89 let parsed = parse(&json!({
90 "search_result": [{
91 "title": "Release",
92 "content": "Release notes",
93 "link": "https://example.com/release",
94 "publish_date": "2026-08-28"
95 }]
96 }));
97 assert_eq!(parsed.citations.len(), 1);
98 assert_eq!(parsed.citations[0].title, "Release");
99 assert_eq!(parsed.citations[0].published.as_deref(), Some("2026-08-28"));
100 }
101 }
102
102 lines RUST