Skip to main content

perspt_sdk/prompt/
route.rs

1//! Prompt routes and per-section override resolution (Definition 4).
2//!
3//! A route is `(adapter, family, optional exact model)`. Resolution
4//! operates per section: an *active* exact-model override wins, else an
5//! *active* family override, else the base — which always exists, so
6//! resolution is total for built-in stages. A configured endpoint name
7//! never selects a prompt; endpoint ids name credentials and locations,
8//! not model behavior.
9
10use serde::{Deserialize, Serialize};
11
12use crate::model::ModelFamily;
13
14use super::manifest::ActivationState;
15use super::section::{OverrideOrigin, SectionTemplate};
16
17/// The prompt identity of one model call.
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19pub struct PromptRoute {
20    /// Transport adapter identity (`ModelTransport::adapter_kind`).
21    pub adapter: String,
22    pub family: ModelFamily,
23    pub exact_model: Option<String>,
24}
25
26/// One override variant of a section, carrying its activation state.
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub struct SectionOverride {
29    /// Which routes this override targets.
30    pub origin: OverrideOrigin,
31    /// Gate AE state; an override resolves only when [`ActivationState::Active`].
32    pub activation: ActivationState,
33    pub template: SectionTemplate,
34}
35
36/// A section's base template plus its (possibly empty) override set.
37#[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    /// Definition 4's `R`: first matching case wins — active exact-model
52    /// override, then active family override, then base. An inactive
53    /// override never resolves; an unknown family gets the base, never an
54    /// error.
55    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}