use std::num::NonZeroU32;
use serde::Deserialize;
use super::ModelId;
use crate::dialects::{ToolDialectId, ToolsMode};
const TEMPERATURE_MAX: f64 = 2.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Temperature(f64);
impl Temperature {
pub(crate) fn new(value: f64) -> std::result::Result<Temperature, TemperatureError> {
if !value.is_finite() {
return Err(TemperatureError::NotFinite);
}
if !(0.0..=TEMPERATURE_MAX).contains(&value) {
return Err(TemperatureError::OutOfRange { value });
}
Ok(Temperature(value))
}
#[must_use]
pub(crate) fn get(self) -> f64 {
self.0
}
}
impl TryFrom<f64> for Temperature {
type Error = TemperatureError;
fn try_from(value: f64) -> std::result::Result<Temperature, TemperatureError> {
Temperature::new(value)
}
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum TemperatureError {
#[error("temperature must be finite")]
NotFinite,
#[error("temperature {value} is outside the supported range [0.0, 2.0]")]
#[non_exhaustive]
OutOfRange {
value: f64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum ThinkingMode {
Never,
Always,
Switchable,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ModelDescriptor {
id: ModelId,
description: String,
context: NonZeroU32,
thinking: ThinkingMode,
tool_dialect: ToolDialectId,
}
impl ModelDescriptor {
#[must_use]
pub fn new(
id: ModelId,
description: impl Into<String>,
context: NonZeroU32,
thinking: ThinkingMode,
) -> Self {
Self {
id,
description: description.into(),
context,
thinking,
tool_dialect: ToolDialectId::OpenAi,
}
}
#[must_use]
pub fn with_dialect(mut self, dialect: ToolDialectId) -> Self {
self.tool_dialect = dialect;
self
}
#[must_use]
pub fn id(&self) -> &ModelId {
&self.id
}
#[must_use]
pub fn description(&self) -> &str {
&self.description
}
#[must_use]
pub fn context(&self) -> NonZeroU32 {
self.context
}
#[must_use]
pub fn thinking(&self) -> ThinkingMode {
self.thinking
}
#[must_use]
pub fn tool_dialect(&self) -> ToolDialectId {
self.tool_dialect
}
#[must_use]
pub fn tools_mode(&self) -> ToolsMode {
self.tool_dialect.tools_mode()
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub(crate) struct ModelNeedOpts {
pub(crate) thinking: Option<bool>,
pub(crate) context: Option<NonZeroU32>,
pub(crate) temperature: Option<Temperature>,
pub(crate) max_tokens: Option<NonZeroU32>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ModelInvocation {
pub(crate) temperature: Option<Temperature>,
pub(crate) max_tokens: Option<NonZeroU32>,
pub(crate) thinking: Option<bool>,
}
impl From<&ModelNeedOpts> for ModelInvocation {
fn from(opts: &ModelNeedOpts) -> Self {
Self {
temperature: opts.temperature,
max_tokens: opts.max_tokens,
thinking: opts.thinking,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ModelBinding {
alias: String,
description: String,
id: ModelId,
invocation: ModelInvocation,
tool_dialect: ToolDialectId,
context: NonZeroU32,
}
impl ModelBinding {
#[must_use]
pub(crate) fn new(
alias: impl Into<String>,
description: impl Into<String>,
id: ModelId,
invocation: ModelInvocation,
tool_dialect: ToolDialectId,
context: NonZeroU32,
) -> Self {
Self {
alias: alias.into(),
description: description.into(),
id,
invocation,
tool_dialect,
context,
}
}
#[must_use]
pub(crate) fn alias(&self) -> &str {
&self.alias
}
#[must_use]
pub(crate) fn description(&self) -> &str {
&self.description
}
#[must_use]
pub(crate) fn id(&self) -> &ModelId {
&self.id
}
#[must_use]
pub(crate) fn invocation(&self) -> &ModelInvocation {
&self.invocation
}
#[must_use]
pub(crate) fn tool_dialect(&self) -> ToolDialectId {
self.tool_dialect
}
#[must_use]
pub(crate) fn context(&self) -> NonZeroU32 {
self.context
}
#[must_use]
pub(crate) fn completion_options(&self) -> CompletionOptions {
CompletionOptions {
model: self.id.name().to_owned(),
temperature: self.invocation.temperature,
max_tokens: self.invocation.max_tokens,
thinking: self.invocation.thinking,
tool_dialect: self.tool_dialect,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct CompletionOptions {
pub(crate) model: String,
pub(crate) temperature: Option<Temperature>,
pub(crate) max_tokens: Option<NonZeroU32>,
pub(crate) thinking: Option<bool>,
pub(crate) tool_dialect: ToolDialectId,
}
impl CompletionOptions {
#[must_use]
pub fn new(model: impl Into<String>, dialect: ToolDialectId) -> CompletionOptions {
CompletionOptions {
model: model.into(),
temperature: None,
max_tokens: None,
thinking: None,
tool_dialect: dialect,
}
}
pub fn with_temperature(
mut self,
temperature: f64,
) -> std::result::Result<CompletionOptions, TemperatureError> {
self.temperature = Some(Temperature::new(temperature)?);
Ok(self)
}
#[must_use]
pub fn with_max_tokens(mut self, max_tokens: NonZeroU32) -> CompletionOptions {
self.max_tokens = Some(max_tokens);
self
}
#[must_use]
pub fn with_thinking(mut self, thinking: bool) -> CompletionOptions {
self.thinking = Some(thinking);
self
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub(crate) struct ModelBindings {
bindings: Vec<ModelBinding>,
always: Option<String>,
}
impl ModelBindings {
#[must_use]
pub(crate) fn bindings(&self) -> &[ModelBinding] {
&self.bindings
}
#[must_use]
pub(crate) fn always(&self) -> Option<&str> {
self.always.as_deref()
}
pub(crate) fn binding(&self, alias: &str) -> Option<&ModelBinding> {
self.bindings.iter().find(|binding| binding.alias == alias)
}
pub(crate) fn from_parts(bindings: Vec<ModelBinding>, always: Option<String>) -> Self {
Self { bindings, always }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn model_invocation_equality_is_not_reflexive_for_nan() {
let nan = ModelInvocation {
temperature: Some(Temperature(f64::NAN)),
max_tokens: None,
thinking: None,
};
assert_ne!(nan, nan.clone());
}
#[test]
fn completion_options_equality_is_not_reflexive_for_nan() {
let options = CompletionOptions {
model: "m".to_owned(),
temperature: Some(Temperature(f64::NAN)),
max_tokens: None,
thinking: None,
tool_dialect: ToolDialectId::OpenAi,
};
assert_ne!(options, options.clone());
}
#[test]
fn with_temperature_rejects_non_finite_and_out_of_range() {
let base = || CompletionOptions::new("m", ToolDialectId::OpenAi);
assert_eq!(
base().with_temperature(f64::NAN),
Err(TemperatureError::NotFinite)
);
assert_eq!(
base().with_temperature(f64::INFINITY),
Err(TemperatureError::NotFinite)
);
assert!(matches!(
base().with_temperature(-0.1),
Err(TemperatureError::OutOfRange { .. })
));
assert!(matches!(
base().with_temperature(2.5),
Err(TemperatureError::OutOfRange { .. })
));
assert_eq!(
base()
.with_temperature(0.0)
.expect("0.0 is valid")
.temperature
.map(Temperature::get),
Some(0.0)
);
assert_eq!(
base()
.with_temperature(TEMPERATURE_MAX)
.expect("2.0 is valid")
.temperature
.map(Temperature::get),
Some(TEMPERATURE_MAX)
);
assert_eq!(
base()
.with_temperature(0.7)
.expect("0.7 is valid")
.temperature
.map(Temperature::get),
Some(0.7)
);
}
}