mockforge_intelligence/ai_studio/
org_controls.rs1use crate::ai_studio::budget_manager::AiFeature;
8use async_trait::async_trait;
9use chrono::{DateTime, Utc};
10use mockforge_foundation::Result;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
17pub struct OrgAiControlsConfig {
18 pub budgets: OrgBudgetConfig,
20 pub rate_limits: OrgRateLimitConfig,
22 pub feature_toggles: HashMap<String, bool>,
24}
25
26impl Default for OrgAiControlsConfig {
27 fn default() -> Self {
28 Self {
29 budgets: OrgBudgetConfig::default(),
30 rate_limits: OrgRateLimitConfig::default(),
31 feature_toggles: HashMap::from([
32 ("mock_generation".to_string(), true),
33 ("contract_diff".to_string(), true),
34 ("persona_generation".to_string(), true),
35 ("free_form_generation".to_string(), true),
36 ("debug_analysis".to_string(), true),
37 ]),
38 }
39 }
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
45pub struct OrgBudgetConfig {
46 pub max_tokens_per_period: u64,
48 pub period_type: String,
50 pub max_calls_per_period: u64,
52}
53
54impl Default for OrgBudgetConfig {
55 fn default() -> Self {
56 Self {
57 max_tokens_per_period: 1_000_000,
58 period_type: "month".to_string(),
59 max_calls_per_period: 10_000,
60 }
61 }
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
67pub struct OrgRateLimitConfig {
68 pub rate_limit_per_minute: u64,
70 pub rate_limit_per_hour: Option<u64>,
72 pub rate_limit_per_day: Option<u64>,
74}
75
76impl Default for OrgRateLimitConfig {
77 fn default() -> Self {
78 Self {
79 rate_limit_per_minute: 100,
80 rate_limit_per_hour: None,
81 rate_limit_per_day: None,
82 }
83 }
84}
85
86#[allow(clippy::too_many_arguments)]
88#[async_trait]
89pub trait OrgControlsAccessor: Send + Sync {
90 async fn load_org_config(
92 &self,
93 org_id: &str,
94 workspace_id: Option<&str>,
95 ) -> Result<Option<OrgAiControlsConfig>>;
96
97 async fn check_budget(
99 &self,
100 org_id: &str,
101 workspace_id: Option<&str>,
102 estimated_tokens: u64,
103 ) -> Result<BudgetCheckResult>;
104
105 async fn check_rate_limit(
107 &self,
108 org_id: &str,
109 workspace_id: Option<&str>,
110 ) -> Result<RateLimitCheckResult>;
111
112 async fn is_feature_enabled(
114 &self,
115 org_id: &str,
116 workspace_id: Option<&str>,
117 feature: &str,
118 ) -> Result<bool>;
119
120 async fn record_usage(
122 &self,
123 org_id: &str,
124 workspace_id: Option<&str>,
125 user_id: Option<&str>,
126 feature: AiFeature,
127 tokens: u64,
128 cost_usd: f64,
129 metadata: Option<serde_json::Value>,
130 ) -> Result<()>;
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct BudgetCheckResult {
136 pub allowed: bool,
138 pub current_tokens: u64,
140 pub max_tokens: u64,
142 pub current_calls: u64,
144 pub max_calls: u64,
146 pub period_start: Option<DateTime<Utc>>,
148 pub reason: Option<String>,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct RateLimitCheckResult {
155 pub allowed: bool,
157 pub current_requests: u64,
159 pub max_requests: u64,
161 pub window_type: String,
163 pub retry_after: Option<DateTime<Utc>>,
165 pub reason: Option<String>,
167}
168
169pub struct OrgControls {
174 yaml_config: OrgAiControlsConfig,
176 db_accessor: Option<Box<dyn OrgControlsAccessor>>,
178}
179
180impl OrgControls {
181 pub fn new(yaml_config: OrgAiControlsConfig) -> Self {
183 Self {
184 yaml_config,
185 db_accessor: None,
186 }
187 }
188
189 pub fn with_db_accessor(
191 yaml_config: OrgAiControlsConfig,
192 db_accessor: Box<dyn OrgControlsAccessor>,
193 ) -> Self {
194 Self {
195 yaml_config,
196 db_accessor: Some(db_accessor),
197 }
198 }
199
200 pub async fn load_org_config(
202 &self,
203 org_id: &str,
204 workspace_id: Option<&str>,
205 ) -> Result<OrgAiControlsConfig> {
206 if let Some(ref accessor) = self.db_accessor {
208 if let Some(db_config) = accessor.load_org_config(org_id, workspace_id).await? {
209 return Ok(self.merge_configs(self.yaml_config.clone(), db_config));
211 }
212 }
213
214 Ok(self.yaml_config.clone())
216 }
217
218 pub async fn check_budget(
220 &self,
221 org_id: &str,
222 workspace_id: Option<&str>,
223 estimated_tokens: u64,
224 ) -> Result<BudgetCheckResult> {
225 if let Some(ref accessor) = self.db_accessor {
227 return accessor.check_budget(org_id, workspace_id, estimated_tokens).await;
228 }
229
230 let config = self.load_org_config(org_id, workspace_id).await?;
232 Ok(BudgetCheckResult {
233 allowed: true, current_tokens: 0,
235 max_tokens: config.budgets.max_tokens_per_period,
236 current_calls: 0,
237 max_calls: config.budgets.max_calls_per_period,
238 period_start: None,
239 reason: None,
240 })
241 }
242
243 pub async fn check_rate_limit(
245 &self,
246 org_id: &str,
247 workspace_id: Option<&str>,
248 ) -> Result<RateLimitCheckResult> {
249 if let Some(ref accessor) = self.db_accessor {
251 return accessor.check_rate_limit(org_id, workspace_id).await;
252 }
253
254 let config = self.load_org_config(org_id, workspace_id).await?;
256 Ok(RateLimitCheckResult {
257 allowed: true, current_requests: 0,
259 max_requests: config.rate_limits.rate_limit_per_minute,
260 window_type: "minute".to_string(),
261 retry_after: None,
262 reason: None,
263 })
264 }
265
266 pub async fn is_feature_enabled(
268 &self,
269 org_id: &str,
270 workspace_id: Option<&str>,
271 feature: &str,
272 ) -> Result<bool> {
273 if let Some(ref accessor) = self.db_accessor {
275 return accessor.is_feature_enabled(org_id, workspace_id, feature).await;
276 }
277
278 let config = self.load_org_config(org_id, workspace_id).await?;
280 Ok(config.feature_toggles.get(feature).copied().unwrap_or(true))
281 }
282
283 #[allow(clippy::too_many_arguments)]
285 pub async fn record_usage(
286 &self,
287 org_id: &str,
288 workspace_id: Option<&str>,
289 user_id: Option<&str>,
290 feature: AiFeature,
291 tokens: u64,
292 cost_usd: f64,
293 metadata: Option<serde_json::Value>,
294 ) -> Result<()> {
295 if let Some(ref accessor) = self.db_accessor {
297 return accessor
298 .record_usage(org_id, workspace_id, user_id, feature, tokens, cost_usd, metadata)
299 .await;
300 }
301
302 Ok(())
304 }
305
306 fn merge_configs(
308 &self,
309 yaml: OrgAiControlsConfig,
310 db: OrgAiControlsConfig,
311 ) -> OrgAiControlsConfig {
312 let mut merged_toggles = yaml.feature_toggles.clone();
314 for (key, value) in db.feature_toggles {
315 merged_toggles.insert(key, value);
316 }
317
318 OrgAiControlsConfig {
319 budgets: db.budgets, rate_limits: db.rate_limits, feature_toggles: merged_toggles,
322 }
323 }
324}
325
326impl Default for OrgControls {
327 fn default() -> Self {
328 Self::new(OrgAiControlsConfig::default())
329 }
330}