use std::time::Duration;
use serde::Deserialize;
use crate::{
provider,
runtime::{GatewayMember, GatewayRingPolicy},
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GatewayRingConfig {
pub members: Vec<GatewayMember>,
pub policy: Option<GatewayRingPolicy>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub(crate) enum GatewaysValue {
Members(Vec<MemberValue>),
Ring(RingValue),
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct RingValue {
members: Vec<MemberValue>,
failure_threshold: Option<u32>,
cooldown_secs: Option<u64>,
#[serde(default)]
sticky: bool,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct MemberValue {
base_url: String,
api_key_env: Option<String>,
api_key_file: Option<String>,
strip_model_prefix: Option<String>,
}
pub(crate) fn parse(
value: GatewaysValue,
expand: &dyn Fn(&str) -> Result<String, String>,
) -> Result<GatewayRingConfig, String> {
let (members, policy) = match value {
GatewaysValue::Members(members) => (members, None),
GatewaysValue::Ring(ring) => {
let policy = policy(&ring)?;
(ring.members, policy)
}
};
if members.is_empty() {
return Err("lists no gateways; remove it to reach the model the usual way".to_string());
}
let members = members
.into_iter()
.enumerate()
.map(|(index, member)| {
self::member(member, expand).map_err(|reason| format!("gateway #{index} {reason}"))
})
.collect::<Result<_, _>>()?;
Ok(GatewayRingConfig { members, policy })
}
fn policy(ring: &RingValue) -> Result<Option<GatewayRingPolicy>, String> {
if ring.failure_threshold.is_none() && ring.cooldown_secs.is_none() && !ring.sticky {
return Ok(None);
}
if ring.failure_threshold == Some(0) {
return Err("sets `failure_threshold` to 0; the smallest that means anything is 1".into());
}
let cooldown = match (ring.sticky, ring.cooldown_secs) {
(true, Some(_)) => {
return Err(
"sets both `sticky` and `cooldown_secs`; a sticky ring never \
returns, so it has no cooldown to wait"
.into(),
);
}
(true, None) => None,
(false, Some(secs)) => Some(Duration::from_secs(secs)),
(false, None) => Some(GatewayRingPolicy::DEFAULT_COOLDOWN),
};
Ok(Some(GatewayRingPolicy {
failure_threshold: ring
.failure_threshold
.unwrap_or(GatewayRingPolicy::DEFAULT_FAILURE_THRESHOLD),
cooldown,
}))
}
fn member(
value: MemberValue,
expand: &dyn Fn(&str) -> Result<String, String>,
) -> Result<GatewayMember, String> {
let base_url = expand(&value.base_url)?;
provider::normalize_base_url(&base_url).map_err(|error| error.to_string())?;
let mut member = GatewayMember::new(base_url);
member = match (value.api_key_env, value.api_key_file) {
(Some(_), Some(_)) => {
return Err("names both `api_key_env` and `api_key_file`; a member has one key".into());
}
(Some(var), None) => {
let var = var.trim().to_string();
if var.is_empty() {
return Err("has an empty `api_key_env`".into());
}
member.with_api_key_env(var)
}
(None, Some(path)) => {
let path = expand(&path)?;
if path.trim().is_empty() {
return Err("has an empty `api_key_file`".into());
}
member.with_api_key_file(path.trim())
}
(None, None) => member,
};
if let Some(prefix) = value.strip_model_prefix {
if prefix.is_empty() {
return Err("has an empty `strip_model_prefix`".into());
}
member = member.with_model_prefix_stripped(prefix);
}
Ok(member)
}
pub(crate) fn parse_var(raw: &str) -> Result<GatewayRingConfig, String> {
let value: GatewaysValue = serde_json::from_str(raw).map_err(|error| {
let problem = match error.classify() {
serde_json::error::Category::Data => {
"is not a list of gateways or a ring of them (an unknown key, or a value of the wrong type)"
}
_ => "is not valid JSON",
};
format!("{problem} at column {}", error.column())
})?;
parse(value, &|raw| Ok(raw.to_string()))
}