Skip to main content

agent_proxy_rust_storage/
lib.rs

1//! Backend-agnostic storage abstraction for agent-proxy-rust.
2//!
3//! Defines the [`Storage`] trait and all data types shared across backends.
4//! Middleware crates depend on `Box<dyn Storage>` injected at construction time,
5//! and never know whether the backend is `SQLite`, `PostgreSQL`, or an in-memory mock.
6
7#![forbid(unsafe_code)]
8#![warn(missing_docs, missing_debug_implementations)]
9
10mod error;
11mod types;
12
13use std::fmt::Debug;
14
15use async_trait::async_trait;
16pub use error::StorageError;
17use secrecy::SecretString;
18pub use types::{
19    AvailableChannelInfo, AvailableModelInfo, Channel, CompressionSavingsReport, CostAggregate,
20    CostFilter, CostGroupBy, CostRecord, Model, ModelMapping, ProtocolEntry, Provider,
21    SeedEntryStatus, SeedManifest, SeedManifestEntry, SeedStatus, SubscriptionFee, SwitchLog,
22    TimeRange,
23};
24
25/// Backend-agnostic storage for providers, models, channels, and cost records.
26///
27/// Every method except [`max_connections`](Self::max_connections) is async and
28/// returns `Result<T, StorageError>`. Implementations must be `Send + Sync`
29/// so the trait object can be shared across Tokio tasks behind an `Arc`.
30#[async_trait]
31pub trait Storage: Send + Sync + Debug {
32    // ── Provider ────────────────────────────────────────────
33
34    /// List all providers.
35    async fn list_providers(&self) -> Result<Vec<Provider>, StorageError>;
36
37    /// Get a single provider by ID.
38    async fn get_provider(&self, id: &str) -> Result<Option<Provider>, StorageError>;
39
40    // ── Model ───────────────────────────────────────────────
41
42    /// List models, optionally filtered by provider.
43    async fn list_models(&self, provider_id: Option<&str>) -> Result<Vec<Model>, StorageError>;
44
45    /// Get a single model by ID.
46    async fn get_model(&self, id: &str) -> Result<Option<Model>, StorageError>;
47
48    // ── Channel ─────────────────────────────────────────────
49
50    /// List all channels, optionally filtered by model ID.
51    async fn list_channels(&self, model_id: Option<&str>) -> Result<Vec<Channel>, StorageError>;
52
53    /// Get a single channel by ID.
54    async fn get_channel(&self, id: &str) -> Result<Option<Channel>, StorageError>;
55
56    /// Insert or replace a channel (upsert).
57    async fn upsert_channel(&self, channel: &Channel) -> Result<(), StorageError>;
58
59    /// Toggle a channel enabled/disabled.
60    async fn set_channel_enabled(&self, id: &str, enabled: bool) -> Result<(), StorageError>;
61
62    /// Update just the API key for a channel.
63    async fn set_channel_api_key(&self, id: &str, key: &SecretString) -> Result<(), StorageError>;
64
65    /// Update channel fields (name, enabled, priority, quota, protocols, `force_protocol`).
66    #[allow(clippy::too_many_arguments)]
67    async fn update_channel(
68        &self,
69        id: &str,
70        name: Option<&str>,
71        enabled: Option<bool>,
72        priority: Option<u32>,
73        monthly_quota: Option<u64>,
74        quota_policy: Option<&str>,
75        protocols: Option<&str>,
76        force_protocol: Option<&str>,
77    ) -> Result<Channel, StorageError>;
78
79    /// Delete a channel and its model mappings (cascade).
80    async fn delete_channel(&self, id: &str) -> Result<(), StorageError>;
81
82    /// Mark a channel as healthy (reset failures).
83    async fn mark_channel_healthy(&self, id: &str) -> Result<(), StorageError>;
84
85    /// Record a channel failure (increments counter, may set Degraded/Cooldown).
86    async fn record_channel_failure(&self, id: &str) -> Result<(), StorageError>;
87
88    // ── Model Mapping ───────────────────────────────────────
89
90    /// List all model mappings for a channel.
91    async fn list_mappings(&self, channel_id: &str) -> Result<Vec<ModelMapping>, StorageError>;
92
93    /// Upsert a single model mapping.
94    async fn upsert_mapping(&self, mapping: &ModelMapping) -> Result<(), StorageError>;
95
96    /// Toggle a model mapping enabled/disabled.
97    async fn set_mapping_enabled(&self, id: &str, enabled: bool) -> Result<(), StorageError>;
98
99    /// Delete a model mapping.
100    async fn delete_mapping(&self, id: &str) -> Result<(), StorageError>;
101
102    /// List all model mappings.
103    async fn list_all_mappings(&self) -> Result<Vec<ModelMapping>, StorageError>;
104
105    // ── Cost Records ────────────────────────────────────────
106
107    /// Record a completed request.
108    async fn insert_cost_record(&self, record: &CostRecord) -> Result<(), StorageError>;
109
110    /// Query cost records with optional filters.
111    async fn query_cost_records(&self, filter: CostFilter)
112    -> Result<Vec<CostRecord>, StorageError>;
113
114    /// Aggregate costs grouped by the given dimension within a time range.
115    async fn aggregate_costs(
116        &self,
117        group_by: CostGroupBy,
118        range: TimeRange,
119    ) -> Result<Vec<CostAggregate>, StorageError>;
120
121    /// Delete records older than N days, returning the count of deleted rows.
122    async fn prune_cost_records(&self, older_than_days: u32) -> Result<u64, StorageError>;
123
124    /// List distinct project paths from cost records, sorted alphabetically.
125    async fn list_projects(&self) -> Result<Vec<String>, StorageError>;
126
127    // ── Switch Log ──────────────────────────────────────────
128
129    /// Record a channel switch event.
130    async fn insert_switch_log(&self, log: &SwitchLog) -> Result<(), StorageError>;
131
132    /// Query recent switch logs, optionally limited.
133    async fn query_switch_logs(&self, limit: Option<u32>) -> Result<Vec<SwitchLog>, StorageError>;
134
135    // ── Available Channels ───────────────────────────────────
136
137    /// List all enabled channels with their bound models.
138    /// Used by token-fleet-switch for Claude direct-connect mode.
139    async fn list_available_channels(&self) -> Result<Vec<AvailableChannelInfo>, StorageError>;
140
141    // ── Subscription Fees ───────────────────────────────────
142
143    /// Record a monthly subscription fee.
144    async fn insert_subscription_fee(&self, fee: &SubscriptionFee) -> Result<(), StorageError>;
145
146    /// Query subscription fees optionally filtered by channel and/or month.
147    async fn query_subscription_fees(
148        &self,
149        channel: Option<&str>,
150        month: Option<&str>,
151    ) -> Result<Vec<SubscriptionFee>, StorageError>;
152
153    // ── Lifecycle ───────────────────────────────────────────
154
155    /// Run migrations / schema init. Idempotent — safe to call on every startup.
156    async fn migrate(&self) -> Result<(), StorageError>;
157
158    /// Health check — returns `true` if the backend is reachable.
159    async fn health_check(&self) -> Result<bool, StorageError>;
160
161    /// Maximum number of concurrent connections this backend supports.
162    fn max_connections(&self) -> usize;
163}
164
165/// Seed data manager — initializes and refreshes reference data.
166///
167/// Seed data includes providers, models, channels, and model mappings
168/// with pricing. It is versioned separately from schema migrations so
169/// it can be hot-updated without redeploying.
170#[async_trait]
171pub trait SeedManager: Send + Sync {
172    /// Initialize seed data from the embedded fallback (compile-time JSON).
173    ///
174    /// Idempotent — safe to call on every startup. Only inserts when the
175    /// local database has no seed data yet (version = 0).
176    async fn seed_init(&self) -> Result<SeedStatus, StorageError>;
177
178    /// Fetch and apply seed data from a remote URL.
179    ///
180    /// `url` is the base URL to the seed directory (e.g.
181    /// `"https://raw.githubusercontent.com/.../refs/tags/seed-v3/crates/storage-sqlite/seed/"`).
182    /// When `None`, tries the environment variable `AGENT_PROXY_SEED_URL`,
183    /// then falls back to the last-used URL stored in `seed_metadata`.
184    async fn seed_refresh(&self, url: Option<&str>) -> Result<SeedStatus, StorageError>;
185
186    /// Query the current seed data status without fetching.
187    async fn seed_status(&self) -> Result<SeedStatus, StorageError>;
188
189    /// Fetch the remote manifest and compare with local status, but do NOT
190    /// apply any changes. Used by the admin API `?remote=true` query parameter.
191    async fn seed_check_remote(&self, url: Option<&str>) -> Result<SeedStatus, StorageError>;
192}