use std::collections::HashMap; use std::time::Duration; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use serde_json::{Map, Value}; pub(crate) struct RouteOptions { pub(crate) model: String, pub(crate) api_key: Option, pub(crate) api_base: Option, pub(crate) custom_llm_provider: Option, pub(crate) extra_headers: Option>, pub(crate) timeout: Option, } pub(crate) struct RouteOptionsInputs { pub(crate) model: String, pub(crate) api_key: Option, pub(crate) api_base: Option, pub(crate) custom_llm_provider: Option, pub(crate) extra_headers: Option, pub(crate) timeout_seconds: Option, } impl RouteOptions { pub(crate) fn from_python(inputs: RouteOptionsInputs) -> PyResult { Ok(Self { model: inputs.model, api_key: inputs.api_key, api_base: inputs.api_base, custom_llm_provider: inputs.custom_llm_provider, extra_headers: optional_object("extra_headers", inputs.extra_headers)?, timeout: optional_timeout(inputs.timeout_seconds), }) } } pub(crate) fn required_value( name: &'static str, value: Value, expected: fn(&Value) -> bool, expected_name: &'static str, ) -> PyResult { if expected(&value) { return Ok(value); } Err(PyValueError::new_err(format!( "{name} must be a {expected_name}" ))) } pub(crate) fn object_or_empty( name: &'static str, value: Option, ) -> PyResult> { match value { Some(value) => object(name, value), None => Ok(Map::new()), } } fn optional_object( name: &'static str, value: Option, ) -> PyResult>> { value.map(|value| object(name, value)).transpose() } fn object(name: &'static str, value: Value) -> PyResult> { match value { Value::Object(map) => Ok(map), _ => Err(PyValueError::new_err(format!("{name} must be a dict"))), } } pub(crate) fn optional_timeout(timeout_seconds: Option) -> Option { timeout_seconds.and_then(|secs| { if secs.is_finite() && secs > 0.0 { Some(Duration::from_secs_f64(secs)) } else { None } }) } pub(crate) fn marshal_headers(headers: Option) -> PyResult> { let value = match headers { Some(headers) => headers, None => Value::Object(Map::new()), }; let Value::Object(headers) = value else { return Err(PyValueError::new_err("headers must be a dict")); }; headers .into_iter() .map(|(name, value)| { value .as_str() .map(|value| (name, value.to_string())) .ok_or_else(|| PyValueError::new_err("header values must be strings")) }) .collect() }