Skip to main content

relay_knowledge/model_provider/
catalog.rs

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