1use serde::{Deserialize, Serialize};
11
12use crate::model::ModelFamily;
13
14use super::manifest::ActivationState;
15use super::section::{OverrideOrigin, SectionTemplate};
16
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19pub struct PromptRoute {
20 pub adapter: String,
22 pub family: ModelFamily,
23 pub exact_model: Option<String>,
24}
25
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub struct SectionOverride {
29 pub origin: OverrideOrigin,
31 pub activation: ActivationState,
33 pub template: SectionTemplate,
34}
35
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38pub struct SectionVariants {
39 pub base: SectionTemplate,
40 pub overrides: Vec<SectionOverride>,
41}
42
43impl SectionVariants {
44 pub fn base_only(base: SectionTemplate) -> Self {
45 Self {
46 base,
47 overrides: Vec::new(),
48 }
49 }
50
51 pub fn resolve(&self, route: &PromptRoute) -> (&SectionTemplate, OverrideOrigin) {
56 if let Some(exact) = route.exact_model.as_deref() {
57 if let Some(hit) = self.active_override(
58 |origin| matches!(origin, OverrideOrigin::ExactModel(model) if model == exact),
59 ) {
60 return (&hit.template, hit.origin.clone());
61 }
62 }
63 let family_label = self.family_label(&route.family);
64 if let Some(hit) = self.active_override(
65 |origin| matches!(origin, OverrideOrigin::Family(family) if *family == family_label),
66 ) {
67 return (&hit.template, hit.origin.clone());
68 }
69 (&self.base, OverrideOrigin::Base)
70 }
71
72 fn active_override(
73 &self,
74 matches: impl Fn(&OverrideOrigin) -> bool,
75 ) -> Option<&SectionOverride> {
76 self.overrides
77 .iter()
78 .find(|entry| entry.activation == ActivationState::Active && matches(&entry.origin))
79 }
80
81 fn family_label(&self, family: &ModelFamily) -> String {
82 format!("{family:?}")
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use crate::prompt::section::{
90 PromptMessageRole, PromptSectionId, PromptSectionVersion, SectionSchema,
91 };
92
93 fn template(body: &str) -> SectionTemplate {
94 SectionTemplate {
95 schema: SectionSchema {
96 id: PromptSectionId("stage/role".into()),
97 version: PromptSectionVersion(1),
98 role: PromptMessageRole::System,
99 required: true,
100 priority: 0,
101 max_bytes: 1024,
102 vars: vec![],
103 },
104 content_hash: SectionTemplate::hash_body(body),
105 body: body.into(),
106 }
107 }
108
109 fn route(family: ModelFamily, exact: Option<&str>) -> PromptRoute {
110 PromptRoute {
111 adapter: "genai".into(),
112 family,
113 exact_model: exact.map(str::to_string),
114 }
115 }
116
117 #[test]
118 fn resolution_order_is_exact_then_family_then_base() {
119 let variants = SectionVariants {
120 base: template("base"),
121 overrides: vec![
122 SectionOverride {
123 origin: OverrideOrigin::Family(format!("{:?}", ModelFamily::Qwen)),
124 activation: ActivationState::Active,
125 template: template("family"),
126 },
127 SectionOverride {
128 origin: OverrideOrigin::ExactModel("qwen-x".into()),
129 activation: ActivationState::Active,
130 template: template("exact"),
131 },
132 ],
133 };
134 let (exact, origin) = variants.resolve(&route(ModelFamily::Qwen, Some("qwen-x")));
135 assert_eq!(exact.body, "exact");
136 assert!(matches!(origin, OverrideOrigin::ExactModel(_)));
137 let (family, _) = variants.resolve(&route(ModelFamily::Qwen, Some("qwen-y")));
138 assert_eq!(family.body, "family");
139 let (base, origin) = variants.resolve(&route(ModelFamily::Mistral, None));
140 assert_eq!(base.body, "base");
141 assert_eq!(origin, OverrideOrigin::Base);
142 }
143
144 #[test]
145 fn inactive_overrides_never_resolve_and_other_families_get_base() {
146 let variants = SectionVariants {
147 base: template("base"),
148 overrides: vec![SectionOverride {
149 origin: OverrideOrigin::Family(format!("{:?}", ModelFamily::Qwen)),
150 activation: ActivationState::Experimental,
151 template: template("experimental"),
152 }],
153 };
154 let (resolved, origin) = variants.resolve(&route(ModelFamily::Qwen, None));
155 assert_eq!(resolved.body, "base");
156 assert_eq!(origin, OverrideOrigin::Base);
157 let other = route(ModelFamily::Other("mystery".into()), None);
158 assert_eq!(variants.resolve(&other).0.body, "base");
159 }
160}