relay_knowledge/model_provider/
catalog.rs1use serde_json::Value;
2use tokio::fs;
3
4use super::{
5 ModelCatalogCache, ModelCatalogResult, ModelProviderConfigService, ModelProviderError,
6 helpers::*,
7};
8use crate::net::{
9 http::{HttpConfig, send_request_with_qos},
10 qos::{QosPolicy, QosRuntime},
11};
12
13impl ModelProviderConfigService {
14 pub async fn catalog(
15 &self,
16 http: &HttpConfig,
17 refresh: bool,
18 ) -> Result<ModelCatalogResult, ModelProviderError> {
19 let qos = QosRuntime::default();
20 let policy = QosPolicy::new(
21 crate::net::qos::DEFAULT_MAX_CONNECTIONS,
22 crate::net::qos::DEFAULT_MAX_IN_FLIGHT_REQUESTS,
23 crate::net::qos::DEFAULT_MAX_QUEUE_DEPTH,
24 )
25 .expect("default QoS policy should validate");
26 self.catalog_with_qos(http, &qos, &policy, refresh).await
27 }
28
29 pub async fn catalog_with_qos(
30 &self,
31 http: &HttpConfig,
32 qos: &QosRuntime,
33 policy: &QosPolicy,
34 refresh: bool,
35 ) -> Result<ModelCatalogResult, ModelProviderError> {
36 let cached = self.load_catalog_cache().await?;
37 if !refresh {
38 return Ok(cached
39 .map(|cache| catalog_result_from_cache(cache, true, None, None))
40 .unwrap_or_else(builtin_catalog_result));
41 }
42
43 let fetched = self.fetch_catalog(http, qos, policy).await;
44 match fetched {
45 Ok(result) if result.ok => {
46 let cache = ModelCatalogCache {
47 source_url: result.source_url.clone(),
48 fetched_at_ms: result.fetched_at_ms.unwrap_or_else(now_millis),
49 providers: result.providers.clone(),
50 };
51 let _ = self.write_catalog_cache(&cache).await;
52 Ok(result)
53 }
54 Ok(result) => {
55 let fallback_error_code = result.error_code.clone();
56 let fallback_error_message = result.error_message.clone();
57 let source_url = result.source_url.clone();
58 let fetched_at_ms = result.fetched_at_ms;
59 Ok(cached
60 .map(|cache| {
61 catalog_result_from_cache(
62 cache,
63 false,
64 fallback_error_code.clone(),
65 fallback_error_message.clone(),
66 )
67 })
68 .unwrap_or_else(|| ModelCatalogResult {
69 ok: false,
70 source_url,
71 fetched_at_ms,
72 cache_age_seconds: None,
73 stale: true,
74 providers: builtin_catalog_providers(),
75 error_code: fallback_error_code,
76 error_message: fallback_error_message,
77 }))
78 }
79 Err(error) => Ok(cached
80 .map(|cache| {
81 catalog_result_from_cache(
82 cache,
83 false,
84 Some("network_error".to_owned()),
85 Some(error.to_string()),
86 )
87 })
88 .unwrap_or_else(|| ModelCatalogResult {
89 ok: false,
90 source_url: self.catalog_source_url.clone(),
91 fetched_at_ms: None,
92 cache_age_seconds: None,
93 stale: true,
94 providers: builtin_catalog_providers(),
95 error_code: Some("network_error".to_owned()),
96 error_message: Some(error.to_string()),
97 })),
98 }
99 }
100
101 async fn load_catalog_cache(&self) -> Result<Option<ModelCatalogCache>, ModelProviderError> {
102 match fs::read_to_string(self.paths.model_catalog_cache_file()).await {
103 Ok(raw) => serde_json::from_str(&raw)
104 .map(Some)
105 .map_err(ModelProviderError::from),
106 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
107 Err(error) => Err(ModelProviderError::from(error)),
108 }
109 }
110
111 pub(super) async fn write_catalog_cache(
112 &self,
113 cache: &ModelCatalogCache,
114 ) -> Result<(), ModelProviderError> {
115 write_json(self.paths.model_catalog_cache_file(), cache).await
116 }
117
118 async fn fetch_catalog(
119 &self,
120 http: &HttpConfig,
121 qos: &QosRuntime,
122 policy: &QosPolicy,
123 ) -> Result<ModelCatalogResult, ModelProviderError> {
124 let client = crate::net::http::outbound_json_client(http)
125 .map_err(|error| ModelProviderError::Network(error.to_string()))?;
126 let response = send_request_with_qos(
127 qos,
128 policy,
129 client
130 .get(&self.catalog_source_url)
131 .timeout(http.request_timeout),
132 )
133 .await
134 .map_err(|error| ModelProviderError::Network(error.to_string()))?;
135 if !response.status().is_success() {
136 return Ok(ModelCatalogResult {
137 ok: false,
138 source_url: self.catalog_source_url.clone(),
139 fetched_at_ms: None,
140 cache_age_seconds: None,
141 stale: true,
142 providers: Vec::new(),
143 error_code: Some(status_error_code(response.status().as_u16()).to_owned()),
144 error_message: Some(format!("catalog returned HTTP {}", response.status())),
145 });
146 }
147 let payload = response
148 .json::<Value>()
149 .await
150 .map_err(|error| ModelProviderError::Json(error.to_string()))?;
151 Ok(ModelCatalogResult {
152 ok: true,
153 source_url: self.catalog_source_url.clone(),
154 fetched_at_ms: Some(now_millis()),
155 cache_age_seconds: Some(0),
156 stale: false,
157 providers: parse_catalog_payload(&payload),
158 error_code: None,
159 error_message: None,
160 })
161 }
162}