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 dialect: None,
84 tags: vec!["format:qwen".to_string()],
85 capabilities: ModelCapabilities::REQUIRES_STRICT_TURNS,
86 ..summary()
87 }),
88 ..Default::default()
89 };
90
91 let ctx = resolve(&catalog, Some("qwen3")).await;
92 assert_eq!(ctx.tags, vec!["format:qwen".to_string()]);
93 assert_eq!(ctx.capabilities, ModelCapabilities::REQUIRES_STRICT_TURNS);
94 }
95
96 #[tokio::test]
97 async fn unknown_model_yields_passthrough() {
98 let catalog = SpyCatalog::default();
99 assert_eq!(
100 resolve(&catalog, Some("ghost")).await,
101 ModelContext::passthrough()
102 );
103 }
104
105 #[tokio::test]
107 async fn catalog_error_yields_passthrough() {
108 let catalog = SpyCatalog {
109 fails: true,
110 ..Default::default()
111 };
112 assert_eq!(
113 resolve(&catalog, Some("qwen3")).await,
114 ModelContext::passthrough()
115 );
116 }
117
118 #[tokio::test]
129 async fn observed_caps_reach_the_pipeline_through_the_catalog() {
130 let catalog = SpyCatalog {
131 found: Some(ModelSummary {
132 template_caps: Some(crate::domain::TemplateCaps {
133 supports_reasoning_effort: Some(false),
134 ..Default::default()
135 }),
136 ..summary()
137 }),
138 ..Default::default()
139 };
140 let ctx = resolve(&catalog, Some("qwen3")).await;
141
142 let mut body = serde_json::json!({
143 "model": "qwen3",
144 "messages": [{"role": "user", "content": "hi"}],
145 });
146 let layers = super::super::SamplingLayers {
147 profile: Some(crate::domain::InferenceConfig {
148 reasoning_effort: Some(crate::domain::ReasoningEffort::High),
149 ..Default::default()
150 }),
151 ..Default::default()
152 };
153 let report =
154 super::super::apply(&mut body, &ctx, &layers, None).expect("the pipeline applies");
155
156 assert!(
157 report.effort_suppressed.is_some(),
158 "the caps never reached the gate: {body}"
159 );
160 assert!(body.get("reasoning_effort").is_none(), "{body}");
161 }
162
163 #[tokio::test]
164 async fn no_model_name_skips_the_catalog_entirely() {
165 let catalog = SpyCatalog {
166 found: Some(summary()),
167 ..Default::default()
168 };
169 assert_eq!(resolve(&catalog, None).await, ModelContext::passthrough());
170 assert_eq!(catalog.lookups.load(Ordering::SeqCst), 0);
171 }
172}