use crate::config::ApiProvider;
use crate::fleet::store::FleetMember;
use crate::model_routing::provider_router_candidates;
use crate::provider_lake::all_catalog_models_for_provider;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScoutSource {
Pinned,
CatalogSuggestion,
Inherited,
Unavailable(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScoutResolution {
pub provider: String,
pub model: String,
pub source: ScoutSource,
}
impl ScoutResolution {
#[must_use]
pub fn receipt_line(&self) -> String {
let source = match &self.source {
ScoutSource::Pinned => "pinned",
ScoutSource::CatalogSuggestion => "catalog suggestion",
ScoutSource::Inherited => "inherits session route",
ScoutSource::Unavailable(_) => "unavailable",
};
format!("{}/{} ({source})", self.provider, self.model)
}
}
#[must_use]
pub fn verified_fast_companion(provider_id: &str, session_model: &str) -> Option<(String, String)> {
let provider = ApiProvider::parse(provider_id)?;
let candidates = provider_router_candidates(provider, session_model);
let cheap = candidates.cheap?;
let available = all_catalog_models_for_provider(provider);
if available.iter().any(|m| m == &cheap) {
Some((provider_id.to_string(), cheap))
} else {
None
}
}
#[must_use]
pub fn resolve_scout_route(
scout_member: Option<&FleetMember>,
session_provider: &str,
session_model: &str,
) -> ScoutResolution {
if let Some(member) = scout_member
&& let (Some(provider), Some(model)) = (&member.provider, &member.model)
{
return ScoutResolution {
provider: provider.clone(),
model: model.clone(),
source: ScoutSource::Pinned,
};
}
if let Some((provider, model)) = verified_fast_companion(session_provider, session_model) {
return ScoutResolution {
provider,
model,
source: ScoutSource::CatalogSuggestion,
};
}
let session_model = session_model.trim();
if session_model.is_empty() || session_model.eq_ignore_ascii_case("auto") {
return ScoutResolution {
provider: session_provider.to_string(),
model: session_model.to_string(),
source: ScoutSource::Unavailable(
"no verified fast companion exists for this provider and no concrete \
session route is set to inherit"
.to_string(),
),
};
}
ScoutResolution {
provider: session_provider.to_string(),
model: session_model.to_string(),
source: ScoutSource::Inherited,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fleet::store::FleetMember;
fn member(pin: Option<(&str, &str)>) -> Option<FleetMember> {
pin.map(|(provider, model)| FleetMember {
id: "scout".to_string(),
role: "scout".to_string(),
provider: Some(provider.to_string()),
model: Some(model.to_string()),
reasoning: None,
instructions: None,
requires: Vec::new(),
})
}
#[test]
fn pinned_scout_wins_and_survives_operator_changes() {
let resolution = resolve_scout_route(
member(Some(("deepseek", "deepseek-v4-pro"))).as_ref(),
"deepseek",
"deepseek-v4-flash",
);
assert_eq!(resolution.provider, "deepseek");
assert_eq!(resolution.model, "deepseek-v4-pro");
assert_eq!(resolution.source, ScoutSource::Pinned);
let resolution = resolve_scout_route(
member(Some(("deepseek", "deepseek-v4-pro"))).as_ref(),
"openai",
"gpt-5",
);
assert_eq!(resolution.model, "deepseek-v4-pro");
assert_eq!(resolution.source, ScoutSource::Pinned);
}
#[test]
fn unpinned_scout_gets_verified_catalog_companion_or_inherits() {
let resolution = resolve_scout_route(None, "deepseek", "deepseek-v4-pro");
assert_eq!(resolution.provider, "deepseek");
assert_eq!(resolution.model, "deepseek-v4-flash");
assert_eq!(resolution.source, ScoutSource::CatalogSuggestion);
let resolution = resolve_scout_route(None, "anthropic", "claude-sonnet-4-6");
assert_eq!(resolution.model, "claude-haiku-4-5");
assert_eq!(resolution.source, ScoutSource::CatalogSuggestion);
let resolution = resolve_scout_route(None, "sglang", "some-model");
assert_eq!(resolution.model, "some-model");
assert_eq!(resolution.source, ScoutSource::Inherited);
}
#[test]
fn no_session_route_is_unavailable_with_a_reason() {
let resolution = resolve_scout_route(None, "sglang", "");
assert!(
matches!(&resolution.source, ScoutSource::Unavailable(reason) if !reason.is_empty()),
"{resolution:?}"
);
}
#[test]
fn verified_companion_requires_the_model_in_the_catalog() {
let resolution = resolve_scout_route(None, "zai", "GLM-5.2");
match resolution.source {
ScoutSource::CatalogSuggestion => {
let available = all_catalog_models_for_provider(ApiProvider::Zai);
assert!(
available.iter().any(|m| m == "GLM-5-Turbo"),
"a CatalogSuggestion must be verifiable in the catalog"
);
}
ScoutSource::Inherited => {
assert_eq!(resolution.model, "GLM-5.2");
}
other => panic!("unexpected source: {other:?}"),
}
}
#[test]
fn receipt_line_names_the_source() {
let resolution = resolve_scout_route(None, "deepseek", "deepseek-v4-pro");
let line = resolution.receipt_line();
assert!(line.contains("deepseek-v4-flash"), "{line}");
assert!(line.contains("catalog suggestion"), "{line}");
}
}