gglib_core/request_pipeline/
resolve.rs1use tracing::{debug, warn};
4
5use super::ModelContext;
6use crate::ports::ModelCatalogPort;
7
8pub async fn resolve(catalog: &dyn ModelCatalogPort, model: Option<&str>) -> ModelContext {
21 let Some(model_name) = model else {
22 return ModelContext::passthrough();
23 };
24
25 match catalog.resolve_model(model_name).await {
26 Ok(Some(summary)) => ModelContext::from(&summary),
27 Ok(None) => {
28 debug!(model = %model_name, "model not found in catalog; using pass-through context");
29 ModelContext::passthrough()
30 }
31 Err(e) => {
32 warn!(model = %model_name, error = %e, "failed to resolve model context; using pass-through context");
33 ModelContext::passthrough()
34 }
35 }
36}
37
38#[cfg(test)]
39mod tests {
40 use super::super::tests_support::summary;
41 use super::*;
42 use crate::domain::ModelCapabilities;
43 use crate::ports::{CatalogError, ModelLaunchSpec, ModelSummary};
44 use async_trait::async_trait;
45 use std::sync::atomic::{AtomicUsize, Ordering};
46
47 #[derive(Debug, Default)]
49 struct SpyCatalog {
50 found: Option<ModelSummary>,
52 fails: bool,
54 lookups: AtomicUsize,
55 }
56
57 #[async_trait]
58 impl ModelCatalogPort for SpyCatalog {
59 async fn list_models(&self) -> Result<Vec<ModelSummary>, CatalogError> {
60 unimplemented!("not exercised by these tests")
61 }
62
63 async fn resolve_model(&self, _name: &str) -> Result<Option<ModelSummary>, CatalogError> {
64 self.lookups.fetch_add(1, Ordering::SeqCst);
65 if self.fails {
66 return Err(CatalogError::QueryFailed("catalog is down".into()));
67 }
68 Ok(self.found.clone())
69 }
70
71 async fn resolve_for_launch(
72 &self,
73 _name: &str,
74 ) -> Result<Option<ModelLaunchSpec>, CatalogError> {
75 unimplemented!("not exercised by these tests")
76 }
77 }
78
79 #[tokio::test]
80 async fn found_model_yields_all_three_fields() {
81 let catalog = SpyCatalog {
82 found: Some(ModelSummary {
83 tags: vec!["format:qwen".to_string()],
84 capabilities: ModelCapabilities::REQUIRES_STRICT_TURNS,
85 ..summary()
86 }),
87 ..Default::default()
88 };
89
90 let ctx = resolve(&catalog, Some("qwen3")).await;
91 assert_eq!(ctx.tags, vec!["format:qwen".to_string()]);
92 assert_eq!(ctx.capabilities, ModelCapabilities::REQUIRES_STRICT_TURNS);
93 }
94
95 #[tokio::test]
96 async fn unknown_model_yields_passthrough() {
97 let catalog = SpyCatalog::default();
98 assert_eq!(
99 resolve(&catalog, Some("ghost")).await,
100 ModelContext::passthrough()
101 );
102 }
103
104 #[tokio::test]
106 async fn catalog_error_yields_passthrough() {
107 let catalog = SpyCatalog {
108 fails: true,
109 ..Default::default()
110 };
111 assert_eq!(
112 resolve(&catalog, Some("qwen3")).await,
113 ModelContext::passthrough()
114 );
115 }
116
117 #[tokio::test]
118 async fn no_model_name_skips_the_catalog_entirely() {
119 let catalog = SpyCatalog {
120 found: Some(summary()),
121 ..Default::default()
122 };
123 assert_eq!(resolve(&catalog, None).await, ModelContext::passthrough());
124 assert_eq!(catalog.lookups.load(Ordering::SeqCst), 0);
125 }
126}