use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelChoice {
pub id: String,
pub name: String,
pub reasoning: bool,
pub parameters: Vec<(String, String)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderChoice {
pub id: String,
pub name: String,
pub models: Vec<ModelChoice>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Effort {
Low,
Medium,
High,
}
impl Effort {
pub const ALL: [Self; 3] = [Self::Low, Self::Medium, Self::High];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
}
}
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Low => "Low",
Self::Medium => "Medium",
Self::High => "High",
}
}
#[must_use]
pub fn from_value(value: &str) -> Option<Self> {
Self::ALL.into_iter().find(|e| e.as_str() == value)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContextState {
pub provider: String,
pub model: String,
pub effort: Option<Effort>,
pub parameters: Vec<(String, String)>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ContextModel {
providers: Vec<ProviderChoice>,
provider: String,
model: String,
effort: Option<Effort>,
remembered: HashMap<String, String>,
}
impl ContextModel {
#[must_use]
pub fn new(providers: Vec<ProviderChoice>) -> Self {
let first = providers.first().map(|p| p.id.clone()).unwrap_or_default();
let mut model = Self {
providers,
..Self::default()
};
model.set_provider(&first);
model
}
#[must_use]
pub fn providers(&self) -> &[ProviderChoice] {
&self.providers
}
#[must_use]
pub fn provider(&self) -> Option<&ProviderChoice> {
self.providers.iter().find(|p| p.id == self.provider)
}
#[must_use]
pub fn model(&self) -> Option<&ModelChoice> {
self.provider()?.models.iter().find(|m| m.id == self.model)
}
#[must_use]
pub fn stored_effort(&self) -> Option<Effort> {
self.effort
}
#[must_use]
pub fn state(&self) -> ContextState {
let model = self.model();
ContextState {
provider: self.provider.clone(),
model: self.model.clone(),
effort: model.filter(|m| m.reasoning).and(self.effort),
parameters: model.map(|m| m.parameters.clone()).unwrap_or_default(),
}
}
pub fn set_provider(&mut self, provider: &str) {
if !self.providers.iter().any(|p| p.id == provider) {
return;
}
if !self.provider.is_empty() {
self.remembered
.insert(self.provider.clone(), self.model.clone());
}
let previous = std::mem::take(&mut self.model);
provider.clone_into(&mut self.provider);
let available: Vec<String> = self
.provider()
.map(|p| p.models.iter().map(|m| m.id.clone()).collect())
.unwrap_or_default();
let candidates = [self.remembered.get(provider).cloned(), Some(previous)];
self.model = candidates
.into_iter()
.flatten()
.find(|id| !id.is_empty() && available.contains(id))
.or_else(|| available.first().cloned())
.unwrap_or_default();
self.default_effort();
}
pub fn set_model(&mut self, model: &str) {
if self
.provider()
.is_some_and(|p| p.models.iter().any(|m| m.id == model))
{
model.clone_into(&mut self.model);
self.default_effort();
}
}
pub fn set_effort(&mut self, effort: Effort) {
if self.model().is_some_and(|m| m.reasoning) {
self.effort = Some(effort);
}
}
fn default_effort(&mut self) {
if self.model().is_some_and(|m| m.reasoning) && self.effort.is_none() {
self.effort = Some(Effort::Medium);
}
}
#[must_use]
pub fn notice(&self) -> Option<&'static str> {
if self.model().is_some_and(|m| m.reasoning) {
None
} else if self.effort.is_some() {
Some("Effort is not used by this model")
} else {
Some("This model has no effort setting")
}
}
}