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 model_id = default_model_id.into();
26        let provider_name = model_id
27            .split_once('/')
28            .map(|(p, _)| p)
29            .unwrap_or("anthropic");
30
31        // Workaround: create_builtin_provider("zai") uses OpenAiProvider::with_base_url()
32        // without an API key. We register a custom provider with the key attached.
33        let mut builder = OxiBuilder::new().with_builtins();
34
35        if provider_name == "zai" {
36            let api_key = std::env::var("ZAI_API_KEY")
37                .or_else(|_| std::env::var("ANTHROPIC_API_KEY"))
38                .ok();
39
40            let zai_provider = oxi_ai::OpenAiProvider::with_base_url_and_key(
41                "https://open.bigmodel.cn/api/paas/v4",
42                api_key,
43            );
44            builder = builder.provider("zai", zai_provider);
45            tracing::info!("Registered zai provider with API key (OpenAI-compatible)");
46        }
47
48        let oxi = builder.build();
49        Self {
50            oxi,
51            default_model_id: model_id,
52        }
53    }
54
55    /// Get a reference to the underlying Oxi instance.
56    ///
57    /// Use this when you need to pass the engine to oxi-sdk APIs directly
58    /// (e.g., `AgentBuilder`, `MessageBus`, `AgentGroup`).
59    pub fn oxi(&self) -> &Oxi {
60        &self.oxi
61    }
62
63    /// Resolve a model ID to a Model.
64    ///
65    /// Accepts both `"provider/model"` and bare `"model"` forms.
66    /// When no provider prefix is given, defaults to `"anthropic"`.
67    pub fn resolve_model(&self, model_id: &str) -> Result<oxi_sdk::Model> {
68        self.oxi.resolve_model(model_id)
69    }
70
71    /// Create a provider for the given provider name.
72    ///
73    /// Checks custom providers first, then falls back to built-in
74    /// providers (stateless creation).
75    pub fn create_provider(&self, name: &str) -> Result<Arc<dyn oxi_sdk::Provider>> {
76        self.oxi.create_provider(name)
77    }
78}
79
80// ---------------------------------------------------------------------------
81// EngineProvider trait (kept for API compatibility)
82// ---------------------------------------------------------------------------
83
84/// Engine provider trait — abstracts how the kernel obtains AI providers.
85///
86/// This trait is implemented by `OxiEngineProvider` and can be replaced
87/// with a mock for testing.
88pub trait EngineProvider: Send + Sync {
89    /// Create a provider for the given provider name.
90    fn create_provider(&self, provider_name: &str) -> Result<Arc<dyn oxi_sdk::Provider>>;
91
92    /// Resolve a "provider/model" string to a Model.
93    fn resolve_model(&self, model_id: &str) -> Result<oxi_sdk::Model>;
94
95    /// Get the default model ID.
96    fn default_model_id(&self) -> &str;
97}
98
99/// Default engine provider using oxi-sdk.
100pub struct OxiEngineProvider {
101    engine: OxiosEngine,
102}
103
104impl OxiEngineProvider {
105    /// Create a new engine provider with the given default model ID.
106    pub fn new(default_model_id: impl Into<String>) -> Self {
107        Self {
108            engine: OxiosEngine::new(default_model_id),
109        }
110    }
111}
112
113impl EngineProvider for OxiEngineProvider {
114    fn create_provider(&self, provider_name: &str) -> Result<Arc<dyn oxi_sdk::Provider>> {
115        self.engine.create_provider(provider_name)
116    }
117
118    fn resolve_model(&self, model_id: &str) -> Result<oxi_sdk::Model> {
119        self.engine.resolve_model(model_id)
120    }
121
122    fn default_model_id(&self) -> &str {
123        &self.engine.default_model_id
124    }
125}
126
127impl std::fmt::Debug for OxiEngineProvider {
128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129        f.debug_struct("OxiEngineProvider")
130            .field("default_model_id", &self.engine.default_model_id)
131            .finish()
132    }
133}
134
135// ---------------------------------------------------------------------------
136// Tests
137// ---------------------------------------------------------------------------
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn test_resolve_model_with_provider_prefix() {
145        let engine = OxiEngineProvider::new("anthropic/claude-sonnet-4-20250514");
146        let model = engine.resolve_model("openai/gpt-4o").unwrap();
147        assert_eq!(model.provider, "openai");
148        assert_eq!(model.id, "gpt-4o");
149    }
150
151    #[test]
152    fn test_resolve_model_without_provider_prefix() {
153        let engine = OxiEngineProvider::new("anthropic/claude-sonnet-4-20250514");
154        let model = engine.resolve_model("claude-sonnet-4-20250514").unwrap();
155        assert_eq!(model.provider, "anthropic");
156    }
157
158    #[test]
159    fn test_default_model_id() {
160        let engine = OxiEngineProvider::new("anthropic/claude-sonnet-4-20250514");
161        assert_eq!(
162            engine.default_model_id(),
163            "anthropic/claude-sonnet-4-20250514"
164        );
165    }
166
167    #[test]
168    fn test_resolve_model_not_found() {
169        let engine = OxiEngineProvider::new("anthropic/claude-sonnet-4-20250514");
170        let result = engine.resolve_model("nonexistent/model-xyz");
171        assert!(result.is_err());
172    }
173
174    #[test]
175    fn test_create_provider_anthropic() {
176        let engine = OxiEngineProvider::new("anthropic/claude-sonnet-4-20250514");
177        let provider = engine.create_provider("anthropic");
178        assert!(provider.is_ok());
179    }
180
181    #[test]
182    fn test_create_provider_not_found() {
183        let engine = OxiEngineProvider::new("anthropic/claude-sonnet-4-20250514");
184        let result = engine.create_provider("nonexistent_provider");
185        assert!(result.is_err());
186    }
187}