systemprompt_models/services/providers/
rate_card.rs1use chrono::{Days, NaiveDate};
33use serde::{Deserialize, Serialize};
34use systemprompt_identifiers::{ModelId, ProviderId};
35
36use super::{ProviderModel, ProviderRegistryError, ProviderRegistryResult};
37use crate::services::ai::{ModelCapabilities, ModelLimits, ModelPricing};
38
39const VERTEX_RATE_CARD_YAML: &str = include_str!("vertex_rate_card.yaml");
40
41pub const RETIREMENT_NOTICE_DAYS: u64 = 30;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "lowercase")]
46pub enum DocumentedLaunchStage {
47 Ga,
48 Preview,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(deny_unknown_fields)]
64pub struct VertexRateCardEntry {
65 pub upstream: String,
66
67 pub provider: ProviderId,
68
69 pub id: ModelId,
70
71 pub upstream_model: String,
72
73 #[serde(default)]
74 pub allow_preview: bool,
75
76 pub launch_stage: DocumentedLaunchStage,
77
78 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub released: Option<NaiveDate>,
80
81 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub retires_on: Option<NaiveDate>,
83
84 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub price_until: Option<NaiveDate>,
86
87 pub docs: String,
88
89 #[serde(default)]
90 pub pricing: ModelPricing,
91
92 #[serde(default)]
93 pub capabilities: ModelCapabilities,
94
95 #[serde(default)]
96 pub limits: ModelLimits,
97
98 #[serde(default)]
99 pub aliases: Vec<ModelId>,
100}
101
102impl VertexRateCardEntry {
103 #[must_use]
104 pub fn publisher(&self) -> &str {
105 self.upstream.split('/').next().unwrap_or(&self.upstream)
106 }
107
108 #[must_use]
109 pub fn is_supported(&self, today: NaiveDate) -> bool {
110 let stage_ok = self.launch_stage == DocumentedLaunchStage::Ga || self.allow_preview;
111 stage_ok && !self.is_retiring(today)
112 }
113
114 #[must_use]
115 pub fn is_retiring(&self, today: NaiveDate) -> bool {
116 let horizon = today
117 .checked_add_days(Days::new(RETIREMENT_NOTICE_DAYS))
118 .unwrap_or(today);
119 self.retires_on.is_some_and(|retires| retires <= horizon)
120 }
121
122 #[must_use]
123 pub fn to_provider_model(&self) -> ProviderModel {
124 ProviderModel {
125 id: self.id.clone(),
126 aliases: self.aliases.clone(),
127 upstream_model: Some(self.upstream_model.clone()),
128 pricing: self.pricing,
129 capabilities: self.capabilities,
130 limits: self.limits,
131 governance: None,
132 }
133 }
134
135 fn validate(&self) -> ProviderRegistryResult<()> {
136 let id = self.id.as_str();
137 if !self.docs.starts_with("https://") {
138 return Err(ProviderRegistryError::InvalidVertexRateCard(format!(
139 "{id}: `docs` must be the official documentation URL"
140 )));
141 }
142 let Some(released) = self.released else {
143 return Ok(());
144 };
145 if self.retires_on.is_some_and(|retires| retires <= released) {
146 return Err(ProviderRegistryError::InvalidVertexRateCard(format!(
147 "{id}: `retires_on` is not after `released`"
148 )));
149 }
150 if self.price_until.is_some_and(|until| until <= released) {
151 return Err(ProviderRegistryError::InvalidVertexRateCard(format!(
152 "{id}: `price_until` is not after `released`"
153 )));
154 }
155 Ok(())
156 }
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
160#[serde(deny_unknown_fields)]
161pub struct VertexRateCard {
162 pub entries: Vec<VertexRateCardEntry>,
163}
164
165impl VertexRateCard {
166 pub fn embedded() -> ProviderRegistryResult<Self> {
167 let card: Self = serde_yaml::from_str(VERTEX_RATE_CARD_YAML)
168 .map_err(|e| ProviderRegistryError::InvalidVertexRateCard(e.to_string()))?;
169 card.validate()?;
170 Ok(card)
171 }
172
173 pub fn validate(&self) -> ProviderRegistryResult<()> {
174 self.entries
175 .iter()
176 .try_for_each(VertexRateCardEntry::validate)
177 }
178
179 #[must_use]
180 pub fn lookup(&self, upstream: &str) -> Option<&VertexRateCardEntry> {
181 self.entries.iter().find(|e| e.upstream == upstream)
182 }
183
184 #[must_use]
185 pub fn lookup_id(&self, id: &str) -> Option<&VertexRateCardEntry> {
186 self.entries.iter().find(|e| e.id.as_str() == id)
187 }
188
189 pub fn entries_for<'a>(
190 &'a self,
191 provider: &'a str,
192 ) -> impl Iterator<Item = &'a VertexRateCardEntry> {
193 self.entries
194 .iter()
195 .filter(move |e| e.provider.as_str() == provider)
196 }
197
198 #[must_use]
199 pub fn publishers_for(&self, provider: &str) -> Vec<String> {
200 let mut publishers: Vec<String> = Vec::new();
201 for entry in self.entries_for(provider) {
202 let publisher = entry.publisher();
203 if !publishers.iter().any(|p| p == publisher) {
204 publishers.push(publisher.to_owned());
205 }
206 }
207 publishers
208 }
209}