1use anyhow::Result;
19use oxi_sdk::{Oxi, OxiBuilder, ProviderPool, RateLimitPolicy};
20use std::sync::Arc;
21
22use crate::credential::CredentialStore;
23
24pub struct OxiosEngine {
38 oxi: Oxi,
39 default_model_id: String,
40 routing_control: Option<oxi_sdk::RoutingControl>,
42 pools: parking_lot::RwLock<std::collections::HashMap<String, Arc<dyn oxi_sdk::Provider>>>,
45 authorizer: Option<Arc<oxi_sdk::Authorizer>>,
50 tracer: Option<Arc<oxi_sdk::Tracer>>,
51 cost_tracker: Option<Arc<oxi_sdk::CostTracker>>,
52}
53
54impl OxiosEngine {
55 pub fn new(default_model_id: impl Into<String>) -> Self {
60 let model_id = default_model_id.into();
61 let oxi = OxiBuilder::new().with_builtins().build();
62 Self {
63 oxi,
64 default_model_id: model_id,
65 routing_control: None,
66 pools: parking_lot::RwLock::new(std::collections::HashMap::new()),
67 authorizer: None,
69 tracer: None,
70 cost_tracker: None,
71 }
72 }
73
74 pub fn from_config(default_model_id: impl Into<String>, config_api_key: Option<&str>) -> Self {
82 let model_id = default_model_id.into();
83
84 let primary_provider = model_id
86 .split_once('/')
87 .map(|(p, _)| p)
88 .unwrap_or("anthropic");
89
90 let mut builder = OxiBuilder::new().with_builtins();
91
92 let providers = ["anthropic", "openai", "google", "deepseek", "xai"];
95 for provider in providers {
96 let config_key = if provider == primary_provider {
99 config_api_key
100 } else {
101 None
102 };
103
104 if let Some((key, source)) = CredentialStore::resolve(provider, config_key) {
105 tracing::debug!(
106 provider,
107 source = ?source,
108 "Injected credential into engine"
109 );
110 builder = builder.api_key(provider, key);
111 }
112 }
113
114 let oxi = builder.build();
115 Self {
116 oxi,
117 default_model_id: model_id,
118 routing_control: None,
119 pools: parking_lot::RwLock::new(std::collections::HashMap::new()),
120 authorizer: None,
122 tracer: None,
123 cost_tracker: None,
124 }
125 }
126
127 pub fn builder() -> OxiosEngineBuilder {
149 OxiosEngineBuilder {
150 inner: OxiBuilder::new().with_builtins(),
151 default_model_id: "anthropic/claude-sonnet-4-20250514".to_string(),
152 authorizer: None,
154 tracer: None,
155 cost_tracker: None,
156 }
157 }
158
159 pub fn oxi(&self) -> &Oxi {
164 &self.oxi
165 }
166
167 pub fn authorizer(&self) -> Option<&Arc<oxi_sdk::Authorizer>> {
172 self.authorizer.as_ref()
173 }
174
175 pub fn tracer(&self) -> Option<&Arc<oxi_sdk::Tracer>> {
180 self.tracer.as_ref()
181 }
182
183 pub fn cost_tracker(&self) -> Option<&Arc<oxi_sdk::CostTracker>> {
188 self.cost_tracker.as_ref()
189 }
190
191 pub fn resolve_model(&self, model_id: &str) -> Result<oxi_sdk::Model> {
193 self.oxi.resolve_model(model_id)
194 }
195
196 pub fn create_provider(&self, name: &str) -> Result<Arc<dyn oxi_sdk::Provider>> {
198 self.oxi.create_provider(name)
199 }
200
201 pub fn default_model_id(&self) -> &str {
203 &self.default_model_id
204 }
205
206 pub fn routing_control(&self) -> Option<&oxi_sdk::RoutingControl> {
208 self.routing_control.as_ref()
209 }
210
211 pub fn pooled_provider(&self, name: &str, rpm: u32) -> Result<Arc<dyn oxi_sdk::Provider>> {
219 {
221 let pools = self.pools.read();
222 if let Some(pooled) = pools.get(name) {
223 return Ok(pooled.clone());
224 }
225 }
226
227 let base = self.create_provider(name)?;
229 let policy = RateLimitPolicy::rpm(rpm);
230 let pool = ProviderPool::new(base, policy, name);
231 let pooled: Arc<dyn oxi_sdk::Provider> = Arc::new(pool);
232
233 {
235 let mut pools = self.pools.write();
236 pools.insert(name.to_string(), pooled.clone());
237 }
238
239 tracing::info!(provider = name, rpm, "Created provider pool");
240 Ok(pooled)
241 }
242}
243
244pub struct OxiosEngineBuilder {
250 inner: OxiBuilder,
251 default_model_id: String,
252 authorizer: Option<Arc<oxi_sdk::Authorizer>>,
255 tracer: Option<Arc<oxi_sdk::Tracer>>,
256 cost_tracker: Option<Arc<oxi_sdk::CostTracker>>,
257}
258
259impl OxiosEngineBuilder {
260 pub fn default_model(mut self, model_id: impl Into<String>) -> Self {
262 self.default_model_id = model_id.into();
263 self
264 }
265
266 pub fn api_key(self, provider: &str, key: impl Into<String>) -> Self {
268 Self {
269 inner: self.inner.api_key(provider, key),
270 default_model_id: self.default_model_id,
271 authorizer: self.authorizer,
272 tracer: self.tracer,
273 cost_tracker: self.cost_tracker,
274 }
275 }
276
277 pub fn credential(
279 self,
280 provider: &str,
281 api_key: impl Into<String>,
282 base_url: Option<&str>,
283 ) -> Self {
284 Self {
285 inner: self.inner.credential(provider, api_key, base_url),
286 default_model_id: self.default_model_id,
287 authorizer: self.authorizer,
288 tracer: self.tracer,
289 cost_tracker: self.cost_tracker,
290 }
291 }
292
293 pub fn provider(self, name: &str, p: impl oxi_sdk::Provider + 'static) -> Self {
295 Self {
296 inner: self.inner.provider(name, p),
297 default_model_id: self.default_model_id,
298 authorizer: self.authorizer,
299 tracer: self.tracer,
300 cost_tracker: self.cost_tracker,
301 }
302 }
303
304 pub fn build(self) -> OxiosEngine {
306 OxiosEngine {
307 oxi: self.inner.build(),
308 default_model_id: self.default_model_id,
309 routing_control: None,
310 pools: parking_lot::RwLock::new(std::collections::HashMap::new()),
311 authorizer: self.authorizer,
313 tracer: self.tracer,
314 cost_tracker: self.cost_tracker,
315 }
316 }
317
318 pub fn build_with_routing(self) -> (OxiosEngine, oxi_sdk::RoutingControl) {
322 use oxi_sdk::RoutingControl;
323
324 let routing_config = oxi_sdk::routing::RoutingConfig::default();
325 let routing_control = RoutingControl::new(routing_config);
326 let engine = OxiosEngine {
327 oxi: self.inner.build(),
328 default_model_id: self.default_model_id,
329 routing_control: Some(routing_control.clone()),
330 pools: parking_lot::RwLock::new(std::collections::HashMap::new()),
331 authorizer: self.authorizer,
333 tracer: self.tracer,
334 cost_tracker: self.cost_tracker,
335 };
336 (engine, routing_control)
337 }
338
339 pub fn with_authorizer(mut self, authorizer: Arc<oxi_sdk::Authorizer>) -> Self {
351 self.authorizer = Some(authorizer);
352 self
353 }
354
355 pub fn with_tracer(mut self, tracer: Arc<oxi_sdk::Tracer>) -> Self {
358 self.tracer = Some(tracer);
359 self
360 }
361
362 pub fn with_cost_tracker(mut self, cost_tracker: Arc<oxi_sdk::CostTracker>) -> Self {
365 self.cost_tracker = Some(cost_tracker);
366 self
367 }
368}
369
370pub trait EngineProvider: Send + Sync {
378 fn create_provider(&self, provider_name: &str) -> Result<Arc<dyn oxi_sdk::Provider>>;
380
381 fn resolve_model(&self, model_id: &str) -> Result<oxi_sdk::Model>;
383
384 fn default_model_id(&self) -> &str;
386}
387
388impl EngineProvider for OxiosEngine {
389 fn create_provider(&self, provider_name: &str) -> Result<Arc<dyn oxi_sdk::Provider>> {
390 self.create_provider(provider_name)
391 }
392
393 fn resolve_model(&self, model_id: &str) -> Result<oxi_sdk::Model> {
394 self.resolve_model(model_id)
395 }
396
397 fn default_model_id(&self) -> &str {
398 &self.default_model_id
399 }
400}
401
402impl std::fmt::Debug for OxiosEngine {
403 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
404 f.debug_struct("OxiosEngine")
405 .field("default_model_id", &self.default_model_id)
406 .field("routing_enabled", &self.routing_control.is_some())
407 .finish()
408 }
409}
410
411pub struct EngineHandle {
433 inner: parking_lot::RwLock<Arc<OxiosEngine>>,
434}
435
436impl EngineHandle {
437 pub fn new(engine: Arc<OxiosEngine>) -> Self {
439 Self {
440 inner: parking_lot::RwLock::new(engine),
441 }
442 }
443
444 pub fn get(&self) -> Arc<OxiosEngine> {
449 Arc::clone(&self.inner.read())
450 }
451
452 pub fn swap(&self, new_engine: OxiosEngine) {
457 let mut guard = self.inner.write();
458 let old_id = guard.default_model_id().to_string();
459 *guard = Arc::new(new_engine);
460 tracing::info!(
461 old_model = %old_id,
462 new_model = %guard.default_model_id(),
463 "Engine hot-swapped"
464 );
465 }
466}
467
468impl std::fmt::Debug for EngineHandle {
469 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
470 let engine = self.inner.read();
471 f.debug_struct("EngineHandle")
472 .field("current_model", &engine.default_model_id())
473 .finish()
474 }
475}
476
477#[cfg(test)]
482mod tests {
483 use super::*;
484
485 #[test]
486 fn test_resolve_model_with_provider_prefix() {
487 let engine = OxiosEngine::new("anthropic/claude-sonnet-4-20250514");
488 let model = engine.resolve_model("openai/gpt-4o").unwrap();
489 assert_eq!(model.provider, "openai");
490 assert_eq!(model.id, "gpt-4o");
491 }
492
493 #[test]
494 fn test_resolve_model_without_provider_prefix() {
495 let engine = OxiosEngine::new("anthropic/claude-sonnet-4-20250514");
496 let model = engine.resolve_model("claude-sonnet-4-20250514").unwrap();
497 assert_eq!(model.provider, "anthropic");
498 }
499
500 #[test]
501 fn test_default_model_id() {
502 let engine = OxiosEngine::new("anthropic/claude-sonnet-4-20250514");
503 assert_eq!(
504 engine.default_model_id(),
505 "anthropic/claude-sonnet-4-20250514"
506 );
507 }
508
509 #[test]
510 fn test_resolve_model_not_found() {
511 let engine = OxiosEngine::new("anthropic/claude-sonnet-4-20250514");
512 let result = engine.resolve_model("nonexistent/model-xyz");
513 assert!(result.is_err());
514 }
515
516 #[test]
517 fn test_create_provider_anthropic() {
518 let engine = OxiosEngine::new("anthropic/claude-sonnet-4-20250514");
519 let provider = engine.create_provider("anthropic");
520 assert!(provider.is_ok());
521 }
522
523 #[test]
524 fn test_create_provider_not_found() {
525 let engine = OxiosEngine::new("anthropic/claude-sonnet-4-20250514");
526 let result = engine.create_provider("nonexistent_provider");
527 assert!(result.is_err());
528 }
529
530 #[test]
531 fn test_builder_with_credential() {
532 let engine = OxiosEngine::builder()
533 .default_model("openai/gpt-4o")
534 .credential("openai", "sk-test", None)
535 .build();
536 assert_eq!(engine.default_model_id(), "openai/gpt-4o");
537 }
538
539 #[test]
540 fn test_engine_provider_trait_on_engine() {
541 let engine = OxiosEngine::new("anthropic/claude-sonnet-4-20250514");
542 let provider: &dyn EngineProvider = &engine;
543 assert!(provider.create_provider("anthropic").is_ok());
544 assert!(provider.resolve_model("openai/gpt-4o").is_ok());
545 }
546
547 #[test]
550 fn test_engine_handle_get_returns_current() {
551 let engine = OxiosEngine::new("anthropic/claude-sonnet-4-20250514");
552 let handle = EngineHandle::new(Arc::new(engine));
553 let e = handle.get();
554 assert_eq!(e.default_model_id(), "anthropic/claude-sonnet-4-20250514");
555 }
556
557 #[test]
558 fn test_engine_handle_swap_updates() {
559 let engine = OxiosEngine::new("anthropic/claude-sonnet-4-20250514");
560 let handle = EngineHandle::new(Arc::new(engine));
561
562 let new_engine = OxiosEngine::new("openai/gpt-4o");
563 handle.swap(new_engine);
564
565 let e = handle.get();
566 assert_eq!(e.default_model_id(), "openai/gpt-4o");
567 }
568
569 #[test]
570 fn test_engine_handle_swap_preserves_old_arc() {
571 let engine = OxiosEngine::new("anthropic/claude-sonnet-4-20250514");
573 let handle = EngineHandle::new(Arc::new(engine));
574
575 let old = handle.get();
576 assert_eq!(old.default_model_id(), "anthropic/claude-sonnet-4-20250514");
577
578 handle.swap(OxiosEngine::new("openai/gpt-4o"));
579
580 assert_eq!(old.default_model_id(), "anthropic/claude-sonnet-4-20250514");
582
583 let current = handle.get();
585 assert_eq!(current.default_model_id(), "openai/gpt-4o");
586 }
587
588 #[test]
591 fn test_rfc014_phase_d_default_fields_are_none() {
592 let engine = OxiosEngine::new("anthropic/claude-sonnet-4-20250514");
596 assert!(engine.authorizer().is_none());
597 assert!(engine.tracer().is_none());
598 assert!(engine.cost_tracker().is_none());
599
600 let engine = OxiosEngine::from_config("anthropic/claude-sonnet-4-20250514", None);
601 assert!(engine.authorizer().is_none());
602 assert!(engine.tracer().is_none());
603 assert!(engine.cost_tracker().is_none());
604
605 let engine = OxiosEngine::builder()
606 .default_model("openai/gpt-4o")
607 .build();
608 assert!(engine.authorizer().is_none());
609 assert!(engine.tracer().is_none());
610 assert!(engine.cost_tracker().is_none());
611
612 let (engine, _rc) = OxiosEngine::builder()
613 .default_model("openai/gpt-4o")
614 .build_with_routing();
615 assert!(engine.authorizer().is_none());
616 assert!(engine.tracer().is_none());
617 assert!(engine.cost_tracker().is_none());
618 }
619
620 #[test]
621 fn test_rfc014_phase_d_with_tracer() {
622 let tracer = Arc::new(oxi_sdk::Tracer::new());
624 let engine = OxiosEngine::builder()
625 .default_model("openai/gpt-4o")
626 .with_tracer(tracer.clone())
627 .build();
628 assert!(engine.tracer().is_some());
629 assert!(engine.authorizer().is_none());
630 assert!(engine.cost_tracker().is_none());
631 }
632
633 #[test]
634 fn test_rfc014_phase_d_with_cost_tracker() {
635 let oxi_for_registry = oxi_sdk::OxiBuilder::new().with_builtins().build();
640 let model_registry = oxi_for_registry.models_arc();
641 let cost_tracker = Arc::new(oxi_sdk::CostTracker::new(
642 model_registry,
643 oxi_sdk::CostTrackerConfig::default(),
644 ));
645 let engine = OxiosEngine::builder()
646 .default_model("openai/gpt-4o")
647 .with_cost_tracker(cost_tracker)
648 .build();
649 assert!(engine.cost_tracker().is_some());
650 assert!(engine.authorizer().is_none());
651 assert!(engine.tracer().is_none());
652 }
653
654 #[test]
655 fn test_rfc014_phase_d_with_authorizer() {
656 let audit = Arc::new(oxi_sdk::AuditLog::new(16));
658 let authorizer = Arc::new(oxi_sdk::Authorizer::new(audit));
659 let engine = OxiosEngine::builder()
660 .default_model("openai/gpt-4o")
661 .with_authorizer(authorizer)
662 .build();
663 assert!(engine.authorizer().is_some());
664 assert!(engine.tracer().is_none());
665 assert!(engine.cost_tracker().is_none());
666 }
667
668 #[test]
669 fn test_rfc014_phase_d_all_three_handles() {
670 let audit = Arc::new(oxi_sdk::AuditLog::new(16));
674 let authorizer = Arc::new(oxi_sdk::Authorizer::new(audit));
675 let tracer = Arc::new(oxi_sdk::Tracer::new());
676 let oxi_for_registry = oxi_sdk::OxiBuilder::new().with_builtins().build();
677 let model_registry = oxi_for_registry.models_arc();
678 let cost_tracker = Arc::new(oxi_sdk::CostTracker::new(
679 model_registry,
680 oxi_sdk::CostTrackerConfig::default(),
681 ));
682
683 let engine = OxiosEngine::builder()
684 .default_model("openai/gpt-4o")
685 .api_key("openai", "sk-test")
686 .with_authorizer(authorizer)
687 .with_tracer(tracer)
688 .with_cost_tracker(cost_tracker)
689 .build();
690
691 assert!(engine.authorizer().is_some());
692 assert!(engine.tracer().is_some());
693 assert!(engine.cost_tracker().is_some());
694 assert_eq!(engine.default_model_id(), "openai/gpt-4o");
695 }
696}