Skip to main content

codex_helper_core/
config_profiles.rs

1use super::*;
2
3#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
4pub struct ServiceControlProfile {
5    #[serde(default, skip_serializing_if = "Option::is_none")]
6    pub extends: Option<String>,
7    /// Retained for legacy/v2 profiles. V4 route graph configs should express provider
8    /// selection in the service routing block instead of profile-level station bindings.
9    #[serde(default, skip_serializing_if = "Option::is_none")]
10    pub station: Option<String>,
11    #[serde(default, skip_serializing_if = "Option::is_none")]
12    pub model: Option<String>,
13    #[serde(default, skip_serializing_if = "Option::is_none")]
14    pub reasoning_effort: Option<String>,
15    #[serde(default, skip_serializing_if = "Option::is_none")]
16    pub service_tier: Option<String>,
17}
18
19fn merge_service_control_profile(
20    mut base: ServiceControlProfile,
21    overlay: &ServiceControlProfile,
22) -> ServiceControlProfile {
23    base.extends = overlay.extends.clone();
24    if overlay.station.is_some() {
25        base.station = overlay.station.clone();
26    }
27    if overlay.model.is_some() {
28        base.model = overlay.model.clone();
29    }
30    if overlay.reasoning_effort.is_some() {
31        base.reasoning_effort = overlay.reasoning_effort.clone();
32    }
33    if overlay.service_tier.is_some() {
34        base.service_tier = overlay.service_tier.clone();
35    }
36    base
37}
38
39pub fn resolve_service_profile_from_catalog(
40    profiles: &BTreeMap<String, ServiceControlProfile>,
41    profile_name: &str,
42) -> Result<ServiceControlProfile> {
43    fn resolve_inner(
44        profiles: &BTreeMap<String, ServiceControlProfile>,
45        profile_name: &str,
46        stack: &mut Vec<String>,
47        cache: &mut BTreeMap<String, ServiceControlProfile>,
48    ) -> Result<ServiceControlProfile> {
49        if let Some(profile) = cache.get(profile_name) {
50            return Ok(profile.clone());
51        }
52
53        if let Some(pos) = stack.iter().position(|name| name == profile_name) {
54            let mut cycle = stack[pos..].to_vec();
55            cycle.push(profile_name.to_string());
56            anyhow::bail!("profile inheritance cycle: {}", cycle.join(" -> "));
57        }
58
59        let profile = profiles
60            .get(profile_name)
61            .with_context(|| format!("profile '{}' not found", profile_name))?;
62
63        stack.push(profile_name.to_string());
64        let resolved = if let Some(parent_name) = profile
65            .extends
66            .as_deref()
67            .map(str::trim)
68            .filter(|name| !name.is_empty())
69        {
70            let parent = resolve_inner(profiles, parent_name, stack, cache)?;
71            merge_service_control_profile(parent, profile)
72        } else {
73            profile.clone()
74        };
75        stack.pop();
76
77        cache.insert(profile_name.to_string(), resolved.clone());
78        Ok(resolved)
79    }
80
81    let mut stack = Vec::new();
82    let mut cache = BTreeMap::new();
83    resolve_inner(profiles, profile_name, &mut stack, &mut cache)
84}
85
86pub fn resolve_service_profile(
87    mgr: &ServiceConfigManager,
88    profile_name: &str,
89) -> Result<ServiceControlProfile> {
90    resolve_service_profile_from_catalog(&mgr.profiles, profile_name)
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94enum ExplicitCapabilitySupport {
95    Unknown,
96    Supported,
97    Unsupported,
98}
99
100fn parse_boolish_capability_tag(value: &str) -> Option<bool> {
101    match value.trim().to_ascii_lowercase().as_str() {
102        "1" | "true" | "yes" | "y" | "on" | "supported" => Some(true),
103        "0" | "false" | "no" | "n" | "off" | "unsupported" => Some(false),
104        _ => None,
105    }
106}
107
108fn explicit_capability_support_for_upstreams(
109    upstreams: &[UpstreamConfig],
110    tag_keys: &[&str],
111) -> ExplicitCapabilitySupport {
112    let mut saw_supported = false;
113    let mut saw_explicit_unsupported = false;
114    let mut saw_unknown = false;
115
116    for upstream in upstreams {
117        match tag_keys
118            .iter()
119            .find_map(|key| upstream.tags.get(*key))
120            .and_then(|value| parse_boolish_capability_tag(value))
121        {
122            Some(true) => saw_supported = true,
123            Some(false) => saw_explicit_unsupported = true,
124            None => saw_unknown = true,
125        }
126    }
127
128    if saw_supported {
129        ExplicitCapabilitySupport::Supported
130    } else if saw_explicit_unsupported && !saw_unknown {
131        ExplicitCapabilitySupport::Unsupported
132    } else {
133        ExplicitCapabilitySupport::Unknown
134    }
135}
136
137pub fn validate_profile_station_compatibility(
138    service_name: &str,
139    mgr: &ServiceConfigManager,
140    profile_name: &str,
141    profile: &ServiceControlProfile,
142) -> Result<()> {
143    let Some(station) = profile
144        .station
145        .as_deref()
146        .map(str::trim)
147        .filter(|station| !station.is_empty())
148    else {
149        return Ok(());
150    };
151
152    let Some(config) = mgr.station(station) else {
153        anyhow::bail!(
154            "[{service_name}] profile '{}' references missing station '{}'",
155            profile_name,
156            station
157        );
158    };
159
160    if let Some(model) = profile
161        .model
162        .as_deref()
163        .map(str::trim)
164        .filter(|model| !model.is_empty())
165    {
166        let supported = config.upstreams.is_empty()
167            || config.upstreams.iter().any(|upstream| {
168                crate::model_routing::is_model_supported(
169                    &upstream.supported_models,
170                    &upstream.model_mapping,
171                    model,
172                )
173            });
174        if !supported {
175            anyhow::bail!(
176                "[{service_name}] profile '{}' model '{}' is not supported by station '{}'",
177                profile_name,
178                model,
179                station
180            );
181        }
182    }
183
184    if let Some(service_tier) = profile
185        .service_tier
186        .as_deref()
187        .map(str::trim)
188        .filter(|service_tier| !service_tier.is_empty())
189        && explicit_capability_support_for_upstreams(
190            &config.upstreams,
191            &[
192                "supports_service_tier",
193                "supports_service_tiers",
194                "supports_fast_mode",
195                "supports_fast",
196            ],
197        ) == ExplicitCapabilitySupport::Unsupported
198    {
199        anyhow::bail!(
200            "[{service_name}] profile '{}' requires service_tier '{}' but station '{}' explicitly disables fast/service-tier support",
201            profile_name,
202            service_tier,
203            station
204        );
205    }
206
207    if let Some(reasoning_effort) = profile
208        .reasoning_effort
209        .as_deref()
210        .map(str::trim)
211        .filter(|reasoning_effort| !reasoning_effort.is_empty())
212        && explicit_capability_support_for_upstreams(
213            &config.upstreams,
214            &["supports_reasoning_effort", "supports_reasoning"],
215        ) == ExplicitCapabilitySupport::Unsupported
216    {
217        anyhow::bail!(
218            "[{service_name}] profile '{}' requires reasoning_effort '{}' but station '{}' explicitly disables reasoning support",
219            profile_name,
220            reasoning_effort,
221            station
222        );
223    }
224
225    Ok(())
226}
227
228pub(crate) fn validate_service_profiles(
229    service_name: &str,
230    mgr: &ServiceConfigManager,
231) -> Result<()> {
232    if let Some(default_profile) = mgr.default_profile.as_deref()
233        && !mgr.profiles.contains_key(default_profile)
234    {
235        anyhow::bail!(
236            "[{service_name}] default_profile '{}' does not exist in profiles",
237            default_profile
238        );
239    }
240
241    for profile_name in mgr.profiles.keys() {
242        let resolved = resolve_service_profile(mgr, profile_name)?;
243        validate_profile_station_compatibility(service_name, mgr, profile_name, &resolved)?;
244    }
245
246    Ok(())
247}