Skip to main content

ironflow_store/entities/
provider_account.rs

1//! Provider Account entities: AI provider accounts, their usage windows and history.
2
3use std::fmt;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use strum::{Display, EnumString};
8use uuid::Uuid;
9
10/// Prefix of the system secrets holding Provider Account credentials.
11///
12/// Keys under this prefix are hidden from the Secrets listing and refused by
13/// the Secrets API.
14pub const PROVIDER_ACCOUNT_SECRET_PREFIX: &str = "accounts/";
15
16/// Secret key holding the credential of the account `id`.
17///
18/// # Examples
19///
20/// ```
21/// use ironflow_store::entities::provider_account_secret_key;
22/// use uuid::Uuid;
23///
24/// let key = provider_account_secret_key(Uuid::nil());
25/// assert_eq!(key, "accounts/00000000-0000-0000-0000-000000000000/credential");
26/// ```
27pub fn provider_account_secret_key(id: Uuid) -> String {
28    format!("{PROVIDER_ACCOUNT_SECRET_PREFIX}{id}/credential")
29}
30
31/// Identifier of a Provider Account kind (e.g. `claude_subscription`).
32///
33/// Kinds are an open registry (custom kinds can be registered), so this is a
34/// newtype rather than a closed enum. It keeps a kind from being mixed up with
35/// an account name or any other string.
36///
37/// # Examples
38///
39/// ```
40/// use ironflow_store::entities::ProviderKind;
41///
42/// let kind = ProviderKind::new("claude_subscription");
43/// assert_eq!(kind.as_str(), "claude_subscription");
44/// assert_eq!(kind.to_string(), "claude_subscription");
45/// ```
46#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
47#[serde(transparent)]
48pub struct ProviderKind(String);
49
50impl ProviderKind {
51    /// Wrap a kind identifier.
52    ///
53    /// # Examples
54    ///
55    /// ```
56    /// use ironflow_store::entities::ProviderKind;
57    ///
58    /// assert_eq!(ProviderKind::new("claude_subscription").as_str(), "claude_subscription");
59    /// ```
60    pub fn new(kind: impl Into<String>) -> Self {
61        Self(kind.into())
62    }
63
64    /// The kind identifier.
65    ///
66    /// # Examples
67    ///
68    /// ```
69    /// use ironflow_store::entities::ProviderKind;
70    ///
71    /// assert_eq!(ProviderKind::new("x").as_str(), "x");
72    /// ```
73    pub fn as_str(&self) -> &str {
74        &self.0
75    }
76}
77
78impl fmt::Display for ProviderKind {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        f.write_str(&self.0)
81    }
82}
83
84impl From<&str> for ProviderKind {
85    fn from(kind: &str) -> Self {
86        Self::new(kind)
87    }
88}
89
90impl From<String> for ProviderKind {
91    fn from(kind: String) -> Self {
92        Self(kind)
93    }
94}
95
96/// Status of a usage window, as reported by the provider.
97///
98/// # Examples
99///
100/// ```
101/// use ironflow_store::entities::AccountWindowStatus;
102///
103/// let status: AccountWindowStatus = "allowed_warning".parse().unwrap();
104/// assert_eq!(status, AccountWindowStatus::AllowedWarning);
105/// ```
106#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
107#[cfg_attr(feature = "store-postgres", derive(sqlx::Type))]
108#[cfg_attr(
109    feature = "store-postgres",
110    sqlx(type_name = "text", rename_all = "snake_case")
111)]
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, EnumString)]
113#[serde(rename_all = "snake_case")]
114#[strum(serialize_all = "snake_case")]
115pub enum AccountWindowStatus {
116    /// Requests are allowed.
117    Allowed,
118    /// Requests are allowed, close to the limit.
119    AllowedWarning,
120    /// Requests are rejected until the window resets.
121    Rejected,
122}
123
124/// A persisted Provider Account.
125///
126/// The credential itself lives in the secret `secret_key`, never here.
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
128pub struct ProviderAccount {
129    /// Account ID (UUID v7).
130    pub id: Uuid,
131    /// Unique slug.
132    pub name: String,
133    /// Human-readable name.
134    pub display_name: String,
135    /// Account kind (e.g. `claude_subscription`).
136    pub kind: String,
137    /// Key of the secret holding the credential.
138    pub secret_key: String,
139    /// Whether the account may be selected.
140    pub enabled: bool,
141    /// Lower values are preferred by the `priority` strategy.
142    pub priority: i32,
143    /// Free-form tags.
144    pub tags: Vec<String>,
145    /// Maximum concurrent steps, `None` for unlimited.
146    pub max_concurrency: Option<u32>,
147    /// Utilization from which the account is shown as near its limit.
148    pub alert_threshold: f64,
149    /// When the credential expires.
150    pub expires_at: DateTime<Utc>,
151    /// Subscription plan (`pro`, `max`), informative.
152    pub plan: Option<String>,
153    /// When the provider last rejected the credential, `None` when valid.
154    pub auth_failed_at: Option<DateTime<Utc>>,
155    /// User who created the account.
156    pub created_by: Option<Uuid>,
157    /// Creation timestamp.
158    pub created_at: DateTime<Utc>,
159    /// Last update timestamp.
160    pub updated_at: DateTime<Utc>,
161}
162
163/// Request to create a Provider Account.
164///
165/// The caller generates `id` so the secret key can be built first.
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct NewProviderAccount {
168    /// Account ID (UUID v7).
169    pub id: Uuid,
170    /// Unique slug.
171    pub name: String,
172    /// Human-readable name.
173    pub display_name: String,
174    /// Account kind.
175    pub kind: String,
176    /// Key of the secret holding the credential.
177    pub secret_key: String,
178    /// Whether the account may be selected.
179    pub enabled: bool,
180    /// Priority.
181    pub priority: i32,
182    /// Tags.
183    pub tags: Vec<String>,
184    /// Maximum concurrent steps.
185    pub max_concurrency: Option<u32>,
186    /// Alert threshold in `(0, 1]`.
187    pub alert_threshold: f64,
188    /// When the credential expires.
189    pub expires_at: DateTime<Utc>,
190    /// Subscription plan.
191    pub plan: Option<String>,
192    /// Creating user.
193    pub created_by: Option<Uuid>,
194}
195
196/// Partial update of a Provider Account. `None` fields are left unchanged.
197///
198/// # Examples
199///
200/// ```
201/// use ironflow_store::entities::ProviderAccountUpdate;
202///
203/// let update = ProviderAccountUpdate {
204///     enabled: Some(false),
205///     max_concurrency: Some(None),
206///     ..ProviderAccountUpdate::default()
207/// };
208/// assert_eq!(update.max_concurrency, Some(None));
209/// ```
210#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
211pub struct ProviderAccountUpdate {
212    /// New display name.
213    pub display_name: Option<String>,
214    /// Enable or disable.
215    pub enabled: Option<bool>,
216    /// New priority.
217    pub priority: Option<i32>,
218    /// New tags (replaces).
219    pub tags: Option<Vec<String>>,
220    /// New max concurrency; `Some(None)` clears it.
221    pub max_concurrency: Option<Option<u32>>,
222    /// New alert threshold.
223    pub alert_threshold: Option<f64>,
224    /// New expiry.
225    pub expires_at: Option<DateTime<Utc>>,
226    /// New plan; `Some(None)` clears it.
227    pub plan: Option<Option<String>>,
228    /// New auth failure mark; `Some(None)` clears it.
229    pub auth_failed_at: Option<Option<DateTime<Utc>>>,
230}
231
232/// Latest reading of one usage window of an account.
233#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
234#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
235pub struct ProviderAccountWindow {
236    /// Account the window belongs to.
237    pub account_id: Uuid,
238    /// Window name (`five_hour`, `seven_day`).
239    pub window: String,
240    /// Fraction used, `0.0..=1.0`.
241    pub utilization: f64,
242    /// When the window resets.
243    pub resets_at: Option<DateTime<Utc>>,
244    /// Provider status.
245    pub status: AccountWindowStatus,
246    /// Model family the window applies to, `None` for every model.
247    pub model_scope: Option<String>,
248    /// When the window was observed.
249    pub observed_at: DateTime<Utc>,
250}
251
252/// One historical observation of a window.
253#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
254pub struct ProviderAccountUsagePoint {
255    /// Observation ID.
256    pub id: Uuid,
257    /// Account the observation belongs to.
258    pub account_id: Uuid,
259    /// Window name.
260    pub window: String,
261    /// Fraction used.
262    pub utilization: f64,
263    /// When the window resets.
264    pub resets_at: Option<DateTime<Utc>>,
265    /// Provider status.
266    pub status: AccountWindowStatus,
267    /// Model scope.
268    pub model_scope: Option<String>,
269    /// When the window was observed.
270    pub observed_at: DateTime<Utc>,
271}
272
273/// A window observed during an invocation or a credential check.
274#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
275pub struct NewAccountWindow {
276    /// Window name.
277    pub window: String,
278    /// Fraction used.
279    pub utilization: f64,
280    /// When the window resets.
281    pub resets_at: Option<DateTime<Utc>>,
282    /// Provider status.
283    pub status: AccountWindowStatus,
284    /// Model scope.
285    pub model_scope: Option<String>,
286    /// When the window was observed.
287    pub observed_at: DateTime<Utc>,
288}
289
290/// Observations to record for an account.
291///
292/// # Examples
293///
294/// ```
295/// use ironflow_store::entities::NewProviderAccountObservation;
296///
297/// let observation = NewProviderAccountObservation { windows: Vec::new(), auth_failed: true };
298/// assert!(observation.auth_failed);
299/// ```
300#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
301pub struct NewProviderAccountObservation {
302    /// Observed windows.
303    pub windows: Vec<NewAccountWindow>,
304    /// Whether the provider rejected the credential.
305    #[serde(default)]
306    pub auth_failed: bool,
307}
308
309/// An account eligible for selection, with its windows and load.
310#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
311pub struct ProviderAccountCandidate {
312    /// The account.
313    pub account: ProviderAccount,
314    /// Its latest windows.
315    pub windows: Vec<ProviderAccountWindow>,
316    /// Steps currently running under it.
317    pub running_steps: u32,
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    #[test]
325    fn secret_key_is_under_the_accounts_prefix() {
326        let id = Uuid::now_v7();
327        let key = provider_account_secret_key(id);
328        assert!(key.starts_with(PROVIDER_ACCOUNT_SECRET_PREFIX));
329        assert!(key.ends_with("/credential"));
330        assert!(key.contains(&id.to_string()));
331    }
332
333    #[test]
334    fn window_status_wire_format() {
335        assert_eq!(AccountWindowStatus::Rejected.to_string(), "rejected");
336        assert_eq!(
337            serde_json::to_string(&AccountWindowStatus::AllowedWarning).unwrap(),
338            "\"allowed_warning\""
339        );
340        assert!("nope".parse::<AccountWindowStatus>().is_err());
341    }
342
343    #[test]
344    fn observation_auth_failed_defaults_to_false() {
345        let observation: NewProviderAccountObservation =
346            serde_json::from_str(r#"{"windows":[]}"#).unwrap();
347        assert!(!observation.auth_failed);
348    }
349}