use crate::llm_config::{self, ModelOperation};
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DecisionProtocol {
TypesafeSystemOne,
VercelEvaluate,
OpenrouterDecisions,
StructuredLlm,
}
impl DecisionProtocol {
pub fn as_str(self) -> &'static str {
match self {
Self::TypesafeSystemOne => "typesafe_system_one",
Self::VercelEvaluate => "vercel_evaluate",
Self::OpenrouterDecisions => "openrouter_decisions",
Self::StructuredLlm => "structured_llm",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DecisionQuestionKind {
Boolean,
Choice,
Score,
}
impl DecisionQuestionKind {
pub fn as_str(self) -> &'static str {
match self {
Self::Boolean => "boolean",
Self::Choice => "choice",
Self::Score => "score",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct DecisionLimits {
pub max_questions: Option<usize>,
pub max_choice_options: usize,
pub score_levels_min: usize,
pub score_levels_max: usize,
pub state_window_tokens: usize,
}
#[derive(Clone, Debug, PartialEq)]
pub struct DecisionContract {
pub protocol: DecisionProtocol,
pub question_kinds: Vec<DecisionQuestionKind>,
pub limits: DecisionLimits,
pub input_price_per_mtok: Option<f64>,
pub output_price_per_mtok: Option<f64>,
pub served_model_id: String,
}
impl DecisionContract {
pub fn supports(&self, kind: DecisionQuestionKind) -> bool {
self.question_kinds.contains(&kind)
}
}
const STRUCTURED_LLM_MAX_CHOICE_OPTIONS: usize = 255;
const STRUCTURED_LLM_SCORE_LEVELS_MIN: usize = 2;
const STRUCTURED_LLM_SCORE_LEVELS_MAX: usize = 10;
const STRUCTURED_LLM_OVERHEAD_TOKENS: usize = 2048;
pub fn decision_contract_for_route(provider: &str, model: &str) -> Option<DecisionContract> {
let id = llm_config::model_catalog_id_for_route(provider, model)?;
let entry = llm_config::model_catalog_entry(&id)?;
if !entry.supports_operation(ModelOperation::Decision)
|| !entry.supports_operation(ModelOperation::TextGeneration)
{
return None;
}
let window = entry.context_window as usize;
Some(DecisionContract {
protocol: DecisionProtocol::StructuredLlm,
question_kinds: vec![
DecisionQuestionKind::Boolean,
DecisionQuestionKind::Choice,
DecisionQuestionKind::Score,
],
limits: DecisionLimits {
max_questions: None,
max_choice_options: STRUCTURED_LLM_MAX_CHOICE_OPTIONS,
score_levels_min: STRUCTURED_LLM_SCORE_LEVELS_MIN,
score_levels_max: STRUCTURED_LLM_SCORE_LEVELS_MAX,
state_window_tokens: window.saturating_sub(STRUCTURED_LLM_OVERHEAD_TOKENS),
},
input_price_per_mtok: entry.pricing.as_ref().map(|price| price.input_per_mtok),
output_price_per_mtok: entry.pricing.as_ref().map(|price| price.output_per_mtok),
served_model_id: entry.wire_model.unwrap_or_else(|| model.to_string()),
})
}