返回 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 }
53 }
54
55 #[cfg(test)]
56 mod tests {
57 use super::*;
58
59 fn request() -> ProviderNativeSearchRequest {
60 ProviderNativeSearchRequest {
61 query: "current release".to_string(),
62 max_results: 3,
63 domains: vec!["example.com".to_string()],
64 }
65 }
66
67 #[test]
68 fn request_uses_site_specific_search_engine() {
69 for (base_url, expected_engine) in [
70 ("https://api.z.ai/api/paas/v4", "search-prime"),
71 ("https://open.bigmodel.cn/api/paas/v4/", "search_std"),
72 ] {
73 let body = build_body(&request(), base_url).expect("official endpoint");
74 assert_eq!(body["search_engine"], expected_engine);
75 assert_eq!(body["search_query"], "current release");
76 assert_eq!(body["count"], 3);
77 assert!(body.get("search_domain_filter").is_none());
78 }
79 }
80
81 #[test]
82 fn request_rejects_unproven_product_surface() {
83 assert!(build_body(&request(), "https://api.z.ai/api/coding/paas/v4").is_err());
84 }
85
86 #[test]
87 fn parses_structured_search_results() {
88 let parsed = parse(&json!({
89 "search_result": [{
90 "title": "Release",
91 "content": "Release notes",
92 "link": "https://example.com/release",
93 "publish_date": "2026-08-28"
94 }]
95 }));
96 assert_eq!(parsed.citations.len(), 1);
97 assert_eq!(parsed.citations[0].title, "Release");
98 assert_eq!(parsed.citations[0].published.as_deref(), Some("2026-08-28"));
99 }
100 }
101
101 lines RUST