1use crate::context::ResolverContext;
19use crate::manifest::{Constraint, MatchExpr, Variant};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct Selected<'m> {
26 pub index: u8,
28 pub variant: &'m Variant,
30}
31
32#[must_use]
37pub fn select_variant<'m>(variants: &'m [Variant], ctx: &ResolverContext) -> Option<Selected<'m>> {
38 let mut best: Option<(u8, Selected<'m>)> = None;
39 for (i, variant) in variants.iter().enumerate() {
40 if !matches(&variant.match_expr, ctx) {
41 continue;
42 }
43 let spec = variant.match_expr.specificity();
44 let candidate_is_better = match &best {
45 None => true,
46 Some((best_spec, _)) => spec > *best_spec,
48 };
49 if candidate_is_better {
50 #[allow(clippy::cast_possible_truncation)] let selected = Selected {
52 index: i as u8,
53 variant,
54 };
55 best = Some((spec, selected));
56 }
57 }
58 best.map(|(_, s)| s)
59}
60
61fn matches(expr: &MatchExpr, ctx: &ResolverContext) -> bool {
63 constraint_accepts(&expr.model_family, ctx.model_family.as_deref())
64 && constraint_accepts(&expr.harness, ctx.harness.as_deref())
65 && expr
66 .modalities
67 .is_none_or(|required| ctx.modalities.contains(required))
68 && expr
69 .posture
70 .as_ref()
71 .is_none_or(|allowed| allowed.contains(&ctx.posture))
72}
73
74fn constraint_accepts(c: &Constraint, value: Option<&str>) -> bool {
78 match c {
79 Constraint::Any => true,
80 Constraint::OneOf(allowed) => {
81 value.is_some_and(|v| allowed.iter().any(|a| a.eq_ignore_ascii_case(v)))
82 }
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use crate::context::ConsumerKind;
90 use crate::manifest::{ModalitySet, Posture, VariantBody};
91
92 fn inline(tag: &str) -> VariantBody {
93 VariantBody::Inline {
94 content_type: "text/plain".into(),
95 data: tag.into(),
96 }
97 }
98
99 fn tag(v: &Variant) -> &str {
100 match &v.body {
101 VariantBody::Inline { data, .. } => data,
102 VariantBody::Media(_) => "media",
103 }
104 }
105
106 fn agent(family: Option<&str>, modalities: ModalitySet, posture: Posture) -> ResolverContext {
107 ResolverContext {
108 kind: ConsumerKind::Agent,
109 model_family: family.map(str::to_owned),
110 harness: None,
111 modalities,
112 posture,
113 }
114 }
115
116 #[test]
117 fn empty_variant_list_yields_none_never_panics() {
118 assert!(select_variant(&[], &ResolverContext::human()).is_none());
119 }
120
121 #[test]
122 fn undeclared_context_value_fails_a_constrained_dimension() {
123 let variants = vec![Variant {
124 match_expr: MatchExpr {
125 model_family: Constraint::OneOf(vec!["claude".into()]),
126 ..MatchExpr::default()
127 },
128 body: inline("claude-only"),
129 revalidate_after_ms: None,
130 }];
131 let anon = agent(None, ModalitySet::TEXT, Posture::Headless);
132 assert!(
133 select_variant(&variants, &anon).is_none(),
134 "a variant asking for claude must not serve an anonymous consumer"
135 );
136 }
137
138 #[test]
139 fn family_match_is_case_insensitive() {
140 let variants = vec![Variant {
141 match_expr: MatchExpr {
142 model_family: Constraint::OneOf(vec!["claude".into()]),
143 ..MatchExpr::default()
144 },
145 body: inline("c"),
146 revalidate_after_ms: None,
147 }];
148 let ctx = agent(Some("Claude"), ModalitySet::TEXT, Posture::Headless);
149 assert_eq!(select_variant(&variants, &ctx).unwrap().index, 0);
150 }
151
152 #[test]
153 fn modalities_are_superset_matched() {
154 let variants = vec![Variant {
155 match_expr: MatchExpr {
156 modalities: Some(ModalitySet::VISION),
157 ..MatchExpr::default()
158 },
159 body: inline("needs-eyes"),
160 revalidate_after_ms: None,
161 }];
162 let with_eyes = agent(
163 None,
164 ModalitySet::TEXT.with(ModalitySet::VISION),
165 Posture::Attended,
166 );
167 let without = agent(None, ModalitySet::TEXT, Posture::Attended);
168 assert!(select_variant(&variants, &with_eyes).is_some());
169 assert!(select_variant(&variants, &without).is_none());
170 }
171
172 #[test]
173 fn specificity_beats_declaration_order_but_ties_do_not() {
174 let variants = vec![
175 Variant {
176 match_expr: MatchExpr::any(),
177 body: inline("catch-all"),
178 revalidate_after_ms: None,
179 },
180 Variant {
181 match_expr: MatchExpr {
182 model_family: Constraint::OneOf(vec!["gpt".into()]),
183 harness: Constraint::OneOf(vec!["codex".into()]),
184 ..MatchExpr::default()
185 },
186 body: inline("gpt+codex"),
187 revalidate_after_ms: None,
188 },
189 Variant {
190 match_expr: MatchExpr {
191 model_family: Constraint::OneOf(vec!["gpt".into()]),
192 ..MatchExpr::default()
193 },
194 body: inline("gpt-early"),
195 revalidate_after_ms: None,
196 },
197 Variant {
198 match_expr: MatchExpr {
199 harness: Constraint::OneOf(vec!["codex".into()]),
200 ..MatchExpr::default()
201 },
202 body: inline("codex-late"),
203 revalidate_after_ms: None,
204 },
205 ];
206 let mut ctx = agent(Some("gpt"), ModalitySet::TEXT, Posture::Headless);
207 ctx.harness = Some("codex".into());
208 let sel = select_variant(&variants, &ctx).unwrap();
210 assert_eq!(tag(sel.variant), "gpt+codex");
211
212 let tied: Vec<Variant> = variants
216 .iter()
217 .filter(|v| tag(v) != "gpt+codex")
218 .cloned()
219 .collect();
220 let sel = select_variant(&tied, &ctx).unwrap();
221 assert_eq!(
222 tag(sel.variant),
223 "gpt-early",
224 "ties break by declaration order"
225 );
226 }
227}