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}