use crate::llm_config;
pub use crate::llm::capabilities::{
DecisionProtocol, DecisionQuestionKind, StructuredOutputStrategy,
};
#[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,
pub request_window_tokens: Option<usize>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct DecisionContract {
pub protocol: DecisionProtocol,
pub structured_output_strategy: Option<StructuredOutputStrategy>,
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 declared = crate::provider_catalog::decision_contract_for_route(provider, model)?;
let id = llm_config::model_catalog_id_for_route(provider, model)?;
let entry = llm_config::model_catalog_entry(&id)?;
if declared.protocol.is_native() {
let limits = declared.limits?;
return Some(DecisionContract {
protocol: declared.protocol,
structured_output_strategy: declared.structured_output_strategy,
question_kinds: declared.question_kinds,
limits: DecisionLimits {
max_questions: limits.max_questions.map(|n| n as usize),
max_choice_options: limits.max_choice_options as usize,
score_levels_min: limits.score_levels_min as usize,
score_levels_max: limits.score_levels_max as usize,
state_window_tokens: usize::try_from(limits.state_window_tokens).ok()?,
request_window_tokens: limits
.request_window_tokens
.map(usize::try_from)
.transpose()
.ok()?,
},
input_price_per_mtok: declared.input_price_per_mtok,
output_price_per_mtok: declared.output_price_per_mtok,
served_model_id: declared.served_model_id,
});
}
let window = entry.context_window as usize;
Some(DecisionContract {
protocol: DecisionProtocol::StructuredLlm,
structured_output_strategy: declared.structured_output_strategy,
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),
request_window_tokens: None,
},
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()),
})
}