Skip to main content

oxios_kernel/
engine.rs

1//! Engine provider — thin wrapper around oxi-sdk's Oxi.
2//!
3//! oxios uses oxi-sdk as the AI engine. This module re-exports the
4//! EngineProvider trait for the kernel so it can be swapped for testing.
5//!
6//! All provider/model resolution goes through `oxi_sdk::OxiBuilder`.
7//! The `OxiosEngine` struct wraps the SDK instance and exposes a clean API.
8
9use anyhow::Result;
10use oxi_sdk::{Oxi, OxiBuilder};
11use std::sync::Arc;
12
13/// The kernel's engine — wraps oxi-sdk's Oxi instance.
14pub struct OxiosEngine {
15    oxi: Oxi,
16    default_model_id: String,
17}
18
19impl OxiosEngine {
20    /// Create a new engine with the given default model.
21    ///
22    /// Internally calls `OxiBuilder::new().with_builtins()` to load all
23    /// 50+ built-in models and providers.
24    pub fn new(default_model_id: impl Into<String>) -> Self {
25        let oxi = OxiBuilder::new().with_builtins().build();
26        Self {
27            oxi,
28            default_model_id: default_model_id.into(),
29        }
30    }
31
32    /// Get a reference to the underlying Oxi instance.
33    ///
34    /// Use this when you need to pass the engine to oxi-sdk APIs directly
35    /// (e.g., `AgentBuilder`, `MessageBus`, `AgentGroup`).
36    pub fn oxi(&self) -> &Oxi {
37        &self.oxi
38    }
39
40    /// Resolve a model ID to a Model.
41    ///
42    /// Accepts both `"provider/model"` and bare `"model"` forms.
43    /// When no provider prefix is given, defaults to `"anthropic"`.
44    pub fn resolve_model(&self, model_id: &str) -> Result<oxi_sdk::Model> {
45        self.oxi.resolve_model(model_id)
46    }
47
48    /// Create a provider for the given provider name.
49    ///
50    /// Checks custom providers first, then falls back to built-in
51    /// providers (stateless creation).
52    pub fn create_provider(&self, name: &str) -> Result<Arc<dyn oxi_sdk::Provider>> {
53        self.oxi.create_provider(name)
54    }
55}
56
57// ---------------------------------------------------------------------------
58// EngineProvider trait (kept for API compatibility)
59// ---------------------------------------------------------------------------
60
61/// Engine provider trait — abstracts how the kernel obtains AI providers.
62///
63/// This trait is implemented by `OxiEngineProvider` and can be replaced
64/// with a mock for testing.
65pub trait EngineProvider: Send + Sync {
66    /// Create a provider for the given provider name.
67    fn create_provider(&self, provider_name: &str) -> Result<Arc<dyn oxi_sdk::Provider>>;
68
69    /// Resolve a "provider/model" string to a Model.
70    fn resolve_model(&self, model_id: &str) -> Result<oxi_sdk::Model>;
71
72    /// Get the default model ID.
73    fn default_model_id(&self) -> &str;
74}
75
76/// Default engine provider using oxi-sdk.
77pub struct OxiEngineProvider {
78    engine: OxiosEngine,
79}
80
81impl OxiEngineProvider {
82    /// Create a new engine provider with the given default model ID.
83    pub fn new(default_model_id: impl Into<String>) -> Self {
84        Self {
85            engine: OxiosEngine::new(default_model_id),
86        }
87    }
88}
89
90impl EngineProvider for OxiEngineProvider {
91    fn create_provider(&self, provider_name: &str) -> Result<Arc<dyn oxi_sdk::Provider>> {
92        self.engine.create_provider(provider_name)
93    }
94
95    fn resolve_model(&self, model_id: &str) -> Result<oxi_sdk::Model> {
96        self.engine.resolve_model(model_id)
97    }
98
99    fn default_model_id(&self) -> &str {
100        &self.engine.default_model_id
101    }
102}
103
104impl std::fmt::Debug for OxiEngineProvider {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        f.debug_struct("OxiEngineProvider")
107            .field("default_model_id", &self.engine.default_model_id)
108            .finish()
109    }
110}
111
112// ---------------------------------------------------------------------------
113// Tests
114// ---------------------------------------------------------------------------
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_resolve_model_with_provider_prefix() {
122        let engine = OxiEngineProvider::new("anthropic/claude-sonnet-4-20250514");
123        let model = engine.resolve_model("openai/gpt-4o").unwrap();
124        assert_eq!(model.provider, "openai");
125        assert_eq!(model.id, "gpt-4o");
126    }
127
128    #[test]
129    fn test_resolve_model_without_provider_prefix() {
130        let engine = OxiEngineProvider::new("anthropic/claude-sonnet-4-20250514");
131        let model = engine.resolve_model("claude-sonnet-4-20250514").unwrap();
132        assert_eq!(model.provider, "anthropic");
133    }
134
135    #[test]
136    fn test_default_model_id() {
137        let engine = OxiEngineProvider::new("anthropic/claude-sonnet-4-20250514");
138        assert_eq!(
139            engine.default_model_id(),
140            "anthropic/claude-sonnet-4-20250514"
141        );
142    }
143
144    #[test]
145    fn test_resolve_model_not_found() {
146        let engine = OxiEngineProvider::new("anthropic/claude-sonnet-4-20250514");
147        let result = engine.resolve_model("nonexistent/model-xyz");
148        assert!(result.is_err());
149    }
150
151    #[test]
152    fn test_create_provider_anthropic() {
153        let engine = OxiEngineProvider::new("anthropic/claude-sonnet-4-20250514");
154        let provider = engine.create_provider("anthropic");
155        assert!(provider.is_ok());
156    }
157
158    #[test]
159    fn test_create_provider_not_found() {
160        let engine = OxiEngineProvider::new("anthropic/claude-sonnet-4-20250514");
161        let result = engine.create_provider("nonexistent_provider");
162        assert!(result.is_err());
163    }
164}