use chrono::{Days, NaiveDate};
use serde::{Deserialize, Serialize};
use systemprompt_identifiers::{ModelId, ProviderId};
use super::{ProviderModel, ProviderRegistryError, ProviderRegistryResult};
use crate::services::ai::{ModelCapabilities, ModelLimits, ModelPricing};
const VERTEX_RATE_CARD_YAML: &str = include_str!("vertex_rate_card.yaml");
pub const RETIREMENT_NOTICE_DAYS: u64 = 30;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DocumentedLaunchStage {
Ga,
Preview,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VertexRateCardEntry {
pub upstream: String,
pub provider: ProviderId,
pub id: ModelId,
pub upstream_model: String,
#[serde(default)]
pub allow_preview: bool,
pub launch_stage: DocumentedLaunchStage,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub released: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retires_on: Option<NaiveDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub price_until: Option<NaiveDate>,
pub docs: String,
#[serde(default)]
pub pricing: ModelPricing,
#[serde(default)]
pub capabilities: ModelCapabilities,
#[serde(default)]
pub limits: ModelLimits,
#[serde(default)]
pub aliases: Vec<ModelId>,
}
impl VertexRateCardEntry {
#[must_use]
pub fn publisher(&self) -> &str {
self.upstream.split('/').next().unwrap_or(&self.upstream)
}
#[must_use]
pub fn is_supported(&self, today: NaiveDate) -> bool {
let stage_ok = self.launch_stage == DocumentedLaunchStage::Ga || self.allow_preview;
stage_ok && !self.is_retiring(today)
}
#[must_use]
pub fn is_retiring(&self, today: NaiveDate) -> bool {
let horizon = today
.checked_add_days(Days::new(RETIREMENT_NOTICE_DAYS))
.unwrap_or(today);
self.retires_on.is_some_and(|retires| retires <= horizon)
}
#[must_use]
pub fn to_provider_model(&self) -> ProviderModel {
ProviderModel {
id: self.id.clone(),
aliases: self.aliases.clone(),
upstream_model: Some(self.upstream_model.clone()),
pricing: self.pricing,
capabilities: self.capabilities,
limits: self.limits,
governance: None,
}
}
fn validate(&self) -> ProviderRegistryResult<()> {
let id = self.id.as_str();
if !self.docs.starts_with("https://") {
return Err(ProviderRegistryError::InvalidVertexRateCard(format!(
"{id}: `docs` must be the official documentation URL"
)));
}
let Some(released) = self.released else {
return Ok(());
};
if self.retires_on.is_some_and(|retires| retires <= released) {
return Err(ProviderRegistryError::InvalidVertexRateCard(format!(
"{id}: `retires_on` is not after `released`"
)));
}
if self.price_until.is_some_and(|until| until <= released) {
return Err(ProviderRegistryError::InvalidVertexRateCard(format!(
"{id}: `price_until` is not after `released`"
)));
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VertexRateCard {
pub entries: Vec<VertexRateCardEntry>,
}
impl VertexRateCard {
pub fn embedded() -> ProviderRegistryResult<Self> {
let card: Self = serde_yaml::from_str(VERTEX_RATE_CARD_YAML)
.map_err(|e| ProviderRegistryError::InvalidVertexRateCard(e.to_string()))?;
card.validate()?;
Ok(card)
}
pub fn validate(&self) -> ProviderRegistryResult<()> {
self.entries
.iter()
.try_for_each(VertexRateCardEntry::validate)
}
#[must_use]
pub fn lookup(&self, upstream: &str) -> Option<&VertexRateCardEntry> {
self.entries.iter().find(|e| e.upstream == upstream)
}
#[must_use]
pub fn lookup_id(&self, id: &str) -> Option<&VertexRateCardEntry> {
self.entries.iter().find(|e| e.id.as_str() == id)
}
pub fn entries_for<'a>(
&'a self,
provider: &'a str,
) -> impl Iterator<Item = &'a VertexRateCardEntry> {
self.entries
.iter()
.filter(move |e| e.provider.as_str() == provider)
}
#[must_use]
pub fn publishers_for(&self, provider: &str) -> Vec<String> {
let mut publishers: Vec<String> = Vec::new();
for entry in self.entries_for(provider) {
let publisher = entry.publisher();
if !publishers.iter().any(|p| p == publisher) {
publishers.push(publisher.to_owned());
}
}
publishers
}
}