1use anyhow::Result;
10use oxi_sdk::{Oxi, OxiBuilder};
11use std::sync::Arc;
12
13pub struct OxiosEngine {
15 oxi: Oxi,
16 default_model_id: String,
17}
18
19impl OxiosEngine {
20 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 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 pub fn oxi(&self) -> &Oxi {
60 &self.oxi
61 }
62
63 pub fn resolve_model(&self, model_id: &str) -> Result<oxi_sdk::Model> {
68 self.oxi.resolve_model(model_id)
69 }
70
71 pub fn create_provider(&self, name: &str) -> Result<Arc<dyn oxi_sdk::Provider>> {
76 self.oxi.create_provider(name)
77 }
78}
79
80pub trait EngineProvider: Send + Sync {
89 fn create_provider(&self, provider_name: &str) -> Result<Arc<dyn oxi_sdk::Provider>>;
91
92 fn resolve_model(&self, model_id: &str) -> Result<oxi_sdk::Model>;
94
95 fn default_model_id(&self) -> &str;
97}
98
99pub struct OxiEngineProvider {
101 engine: OxiosEngine,
102}
103
104impl OxiEngineProvider {
105 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#[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}