use super::{builtin_ref, diff_tables, PriceChange, PriceError, PriceTable};
use super::{PRICED_PROVIDERS, PRICES_ENV_VAR};
use crate::provider::model::CostConfig;
use std::path::PathBuf;
use std::sync::{OnceLock, PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard};
const LOGGED_CHANGES: usize = 5;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum InstallPolicy {
#[default]
ReplaceAll,
AddOnly,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum EnvOverride {
Unset,
#[non_exhaustive]
Loaded {
path: PathBuf,
entries: usize,
warnings: Vec<String>,
},
#[non_exhaustive]
Rejected { path: PathBuf, error: PriceError },
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
#[must_use = "the report is how you see what the override changed and what it warned about"]
pub struct OverrideReport {
pub changes: Vec<PriceChange>,
pub reverted: Vec<PriceChange>,
pub inert: Vec<String>,
pub warnings: Vec<String>,
}
struct UserLayer {
table: PriceTable,
from_env: Option<PathBuf>,
}
struct Layers {
fetched: Option<PriceTable>,
user: Option<UserLayer>,
resolved: PriceTable,
}
impl Layers {
fn lower(&self) -> PriceTable {
match &self.fetched {
Some(fetched) => builtin_ref().layered(fetched),
None => builtin_ref().clone(),
}
}
fn rebuild(&mut self) {
let lower = self.lower();
self.resolved = match &self.user {
Some(user) => lower.layered(&user.table),
None => lower,
};
}
}
static ENV_STATUS: OnceLock<EnvOverride> = OnceLock::new();
fn layers() -> &'static RwLock<Layers> {
static LAYERS: OnceLock<RwLock<Layers>> = OnceLock::new();
LAYERS.get_or_init(|| {
let (user, status) = env_layer();
let _ = ENV_STATUS.set(status);
let mut layers = Layers {
fetched: None,
user,
resolved: PriceTable::default(),
};
layers.rebuild();
RwLock::new(layers)
})
}
fn read_layers() -> RwLockReadGuard<'static, Layers> {
layers().read().unwrap_or_else(PoisonError::into_inner)
}
fn write_layers() -> RwLockWriteGuard<'static, Layers> {
layers().write().unwrap_or_else(PoisonError::into_inner)
}
fn env_path() -> Option<PathBuf> {
#[cfg(test)]
{
None
}
#[cfg(not(test))]
{
std::env::var_os(PRICES_ENV_VAR)
.filter(|p| !p.is_empty())
.map(PathBuf::from)
}
}
fn env_layer() -> (Option<UserLayer>, EnvOverride) {
let Some(path) = env_path() else {
return (None, EnvOverride::Unset);
};
match PriceTable::from_path(&path) {
Ok(table) => {
let warnings = override_warnings(&table, builtin_ref());
tracing::info!(
path = %path.display(),
entries = table.len(),
"yoagent prices: loaded {PRICES_ENV_VAR} override"
);
for warning in &warnings {
tracing::warn!("yoagent prices: {PRICES_ENV_VAR}: {warning}");
}
let status = EnvOverride::Loaded {
path: path.clone(),
entries: table.len(),
warnings,
};
let layer = UserLayer {
table,
from_env: Some(path),
};
(Some(layer), status)
}
Err(error) => {
tracing::warn!(
path = %path.display(),
%error,
"yoagent prices: ignoring {PRICES_ENV_VAR}; no user layer installed"
);
(None, EnvOverride::Rejected { path, error })
}
}
}
fn override_warnings(table: &PriceTable, lower: &PriceTable) -> Vec<String> {
let mut out = Vec::new();
let inert = inert_entries(table);
if !inert.is_empty() {
out.push(format!(
"{inert:?} name provider(s) no constructor looks up — only \
ModelConfig::with_prices reads them (constructors look up {PRICED_PROVIDERS:?})"
));
}
for (provider, model, entry) in table.iter() {
let Some(old) = lower.entry(provider, model) else {
continue;
};
let (old, new): (&CostConfig, &CostConfig) = (&old.cost, &entry.cost);
if !old.context_tiers.is_empty() && new.context_tiers.is_empty() {
out.push(format!(
"{provider}/{model} drops the {} context tier(s) of the entry it replaces; \
every request now bills at the base rates",
old.context_tiers.len()
));
}
for (name, was, now) in [
(
"cache_read",
old.cache_read_per_million,
new.cache_read_per_million,
),
(
"cache_write",
old.cache_write_per_million,
new.cache_write_per_million,
),
] {
if was != 0.0 && now == 0.0 {
out.push(format!(
"{provider}/{model} leaves {name} unset, so it bills at the input rate \
({}); the entry it replaces set {was}",
new.input_per_million
));
}
}
}
out
}
fn is_read(provider: &str) -> bool {
PRICED_PROVIDERS.contains(&provider) || (cfg!(feature = "decision") && provider == "typesafe")
}
fn inert_entries(table: &PriceTable) -> Vec<String> {
table
.iter()
.filter(|(p, _, _)| !is_read(p))
.map(|(p, m, _)| format!("{p}/{m}"))
.collect()
}
pub fn install_override(table: PriceTable) -> OverrideReport {
let mut layers = write_layers();
let lower = layers.lower();
let mut warnings = override_warnings(&table, &lower);
let inert = inert_entries(&table);
if let Some(old) = &layers.user {
warnings.push(match &old.from_env {
Some(path) => format!(
"replaces the user layer loaded from {PRICES_ENV_VAR} ({}, {} entries)",
path.display(),
old.table.len()
),
None => format!(
"replaces a previously installed override of {} entries",
old.table.len()
),
});
}
let before = layers.resolved.clone();
layers.user = Some(UserLayer {
table,
from_env: None,
});
layers.rebuild();
let diff = diff_tables(&before, &layers.resolved);
let (mut changes, mut reverted) = (Vec::new(), Vec::new());
let user = &layers.user.as_ref().expect("just installed").table;
for change in diff {
let listed = user.entry(&change.provider, &change.model).is_some();
if !listed {
reverted.push(change);
} else if is_read(&change.provider) {
changes.push(change);
}
}
drop(layers);
for warning in &warnings {
tracing::warn!("yoagent prices: override: {warning}");
}
OverrideReport {
changes,
reverted,
inert,
warnings,
}
}
pub fn clear_override() {
let mut layers = write_layers();
layers.user = None;
layers.rebuild();
}
#[must_use = "the returned changes are how you see where the fetched prices differ"]
pub fn install_fetched(table: PriceTable) -> Vec<PriceChange> {
install_fetched_with(table, InstallPolicy::ReplaceAll)
}
#[must_use = "the returned changes are how you see where the fetched prices differ"]
pub fn install_fetched_with(table: PriceTable, policy: InstallPolicy) -> Vec<PriceChange> {
let mut warnings = Vec::new();
let mut layers = write_layers();
let before = layers.lower();
let fetched = match policy {
InstallPolicy::ReplaceAll => {
if let Some(old) = layers.fetched.as_ref().filter(|t| !t.is_empty()) {
warnings.push(format!(
"replaces a fetched layer of {} entries; models only it listed revert",
old.len()
));
}
table
}
InstallPolicy::AddOnly => {
let mut merged = layers.fetched.clone().unwrap_or_default();
for (provider, model, entry) in table.iter() {
if before.entry(provider, model).is_none() {
if let Err(e) = merged.insert(provider, model, entry.clone()) {
warnings.push(format!("skipped {provider}/{model}: {e}"));
}
}
}
merged
}
};
let disagreements: Vec<PriceChange> = fetched
.changes_from(builtin_ref())
.into_iter()
.filter(|c| c.before.is_some())
.collect();
layers.fetched = Some(fetched);
layers.rebuild();
let mut changes = diff_tables(&before, &layers.lower());
if let Some(user) = &layers.user {
for change in &mut changes {
change.shadowed = user.table.entry(&change.provider, &change.model).is_some();
}
}
drop(layers);
for warning in &warnings {
tracing::warn!("yoagent prices: install_fetched: {warning}");
}
if !disagreements.is_empty() {
let first: Vec<String> = disagreements
.iter()
.take(LOGGED_CHANGES)
.map(ToString::to_string)
.collect();
tracing::warn!(
differing = disagreements.len(),
"yoagent prices: the fetched table disagrees with the built-in data on {} \
model(s) and takes precedence for them: {}{}",
disagreements.len(),
first.join("; "),
if disagreements.len() > LOGGED_CHANGES {
format!("; and {} more", disagreements.len() - LOGGED_CHANGES)
} else {
String::new()
}
);
}
tracing::info!(
changed = changes.len(),
?policy,
"yoagent prices: installed fetched prices"
);
changes
}
pub fn clear_fetched() {
let mut layers = write_layers();
layers.fetched = None;
layers.rebuild();
}
pub fn resolved() -> PriceTable {
read_layers().resolved.clone()
}
pub fn env_override_status() -> EnvOverride {
let _ = layers();
ENV_STATUS.get().cloned().unwrap_or(EnvOverride::Unset)
}
pub fn load_env_override() -> Result<Option<PriceTable>, PriceError> {
match std::env::var_os(PRICES_ENV_VAR) {
Some(path) if !path.is_empty() => PriceTable::from_path(path).map(Some),
_ => Ok(None),
}
}
pub(crate) fn resolved_cost(provider: &str, id: &str) -> Option<CostConfig> {
read_layers().resolved.cost(provider, id)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unit_tests_do_not_read_the_env_override() {
assert!(env_path().is_none());
assert!(matches!(env_override_status(), EnvOverride::Unset));
}
}