use crate::config::errors::ConfigError;
use crate::config::partial::{or_map, OutMode, PartialConfig, PartialProvider};
use crate::config::resolved::ResolvedConfig;
mod row;
impl PartialConfig {
pub fn into_resolved(self, req_model: Option<&str>) -> Result<ResolvedConfig, ConfigError> {
self.check_scalars()?;
let routing_model = req_model.or(self.model.as_deref());
let (name, mut partial) = self.route(routing_model)?;
partial.base_url = self.base_url.or(partial.base_url);
let mut bd = std::mem::take(&mut partial.body_defaults);
let max_tokens = self.max_tokens.or(row::take_u32(&mut bd, "max_tokens")?);
let temperature = self.temperature.or(row::take_f32(&mut bd, "temperature")?);
let top_p = self.top_p.or(row::take_f32(&mut bd, "top_p")?);
let stream = self.stream.or(row::take_bool(&mut bd, "stream")?);
let extra = or_map(bd, self.extra);
let provider = row::complete(name, partial)?;
let model = match routing_model {
Some(m) => provider
.model_aliases
.get(m)
.cloned()
.unwrap_or_else(|| m.to_owned()),
None => String::new(),
};
Ok(ResolvedConfig {
provider,
model,
model_from_cache: false,
output: self.output.unwrap_or(OutMode::Text),
thinking: self.thinking.unwrap_or(false),
inline_key: self.api_key,
max_tokens,
temperature,
top_p,
reasoning: self.reasoning,
stream,
timeout: self.timeout,
system: self.system,
extra,
})
}
fn check_scalars(&self) -> Result<(), ConfigError> {
if self.max_tokens == Some(0) {
return Err(bad("max_tokens", "must be greater than zero"));
}
if self.temperature.is_some_and(f32::is_nan) {
return Err(bad("temperature", "must be a number"));
}
if self.top_p.is_some_and(f32::is_nan) {
return Err(bad("top_p", "must be a number"));
}
Ok(())
}
fn route(&self, routing_model: Option<&str>) -> Result<(String, PartialProvider), ConfigError> {
if let Some(name) = &self.provider {
let row = self
.providers
.get(name)
.ok_or_else(|| ConfigError::UnknownProvider { name: name.clone() })?;
return Ok((name.clone(), row.clone()));
}
let Some(model) = routing_model else {
return self
.default_provider
.as_deref()
.and_then(|name| self.providers.get_key_value(name))
.map(|(name, row)| (name.clone(), row.clone()))
.ok_or(ConfigError::NoProvider);
};
let mut matches: Vec<(String, PartialProvider)> = self
.providers
.iter()
.filter(|(_, row)| row_owns(row, model))
.map(|(name, row)| (name.clone(), row.clone()))
.collect();
if matches.is_empty() {
return Err(ConfigError::NoProvider);
}
if matches.len() > 1 {
return Err(ConfigError::AmbiguousModel {
model: model.to_owned(),
providers: matches.into_iter().map(|(name, _)| name).collect(),
});
}
Ok(matches.swap_remove(0))
}
}
fn row_owns(row: &PartialProvider, model: &str) -> bool {
let aliased = row
.model_aliases
.as_ref()
.is_some_and(|a| a.contains_key(model));
let prefixed = row
.model_prefixes
.as_ref()
.is_some_and(|ps| ps.iter().any(|p| model.starts_with(p.as_str())));
aliased || prefixed
}
fn bad(key: &str, detail: &str) -> ConfigError {
ConfigError::BadValue {
key: key.to_owned(),
detail: detail.to_owned(),
}
}