1use anyhow::Result;
10use oxi_sdk::{Oxi, OxiBuilder};
11use std::sync::Arc;
12
13fn register_compatible_providers(builder: OxiBuilder, _default_provider: &str) -> OxiBuilder {
19 let compatible_providers: &[(&str, &str)] = &[
20 ("zai", "https://api.z.ai/api/coding/paas/v4"),
21 ];
23
24 let mut builder = builder;
25 for (name, default_url) in compatible_providers {
26 let name_owned = name.to_string();
27 let url_owned = default_url.to_string();
28 builder = builder.provider_factory(name, move || {
29 let api_key =
30 crate::credential::CredentialStore::resolve(&name_owned, None).map(|(key, _)| key);
31 let base_url = std::env::var(format!("{}_BASE_URL", name_owned.to_uppercase()))
32 .unwrap_or_else(|_| url_owned.clone());
33 let provider = oxi_ai::OpenAiProvider::with_base_url_and_key(&base_url, api_key);
34 tracing::info!(
35 "Registered {} provider (OpenAI-compatible, base_url: {})",
36 name_owned,
37 base_url
38 );
39 Ok(Arc::new(provider))
40 });
41 }
42 builder
43}
44
45pub struct OxiosEngine {
47 oxi: Oxi,
48 default_model_id: String,
49}
50
51impl OxiosEngine {
52 pub fn new(default_model_id: impl Into<String>) -> Self {
57 let model_id = default_model_id.into();
58 let provider_name = model_id
59 .split_once('/')
60 .map(|(p, _)| p)
61 .unwrap_or("anthropic");
62
63 let mut builder = OxiBuilder::new().with_builtins();
66
67 builder = register_compatible_providers(builder, provider_name);
69
70 let oxi = builder.build();
71 Self {
72 oxi,
73 default_model_id: model_id,
74 }
75 }
76
77 pub fn oxi(&self) -> &Oxi {
82 &self.oxi
83 }
84
85 pub fn resolve_model(&self, model_id: &str) -> Result<oxi_sdk::Model> {
90 self.oxi.resolve_model(model_id)
91 }
92
93 pub fn create_provider(&self, name: &str) -> Result<Arc<dyn oxi_sdk::Provider>> {
98 self.oxi.create_provider(name)
99 }
100}
101
102pub trait EngineProvider: Send + Sync {
111 fn create_provider(&self, provider_name: &str) -> Result<Arc<dyn oxi_sdk::Provider>>;
113
114 fn resolve_model(&self, model_id: &str) -> Result<oxi_sdk::Model>;
116
117 fn default_model_id(&self) -> &str;
119}
120
121pub struct OxiEngineProvider {
123 engine: OxiosEngine,
124}
125
126impl OxiEngineProvider {
127 pub fn new(default_model_id: impl Into<String>) -> Self {
129 Self {
130 engine: OxiosEngine::new(default_model_id),
131 }
132 }
133}
134
135impl EngineProvider for OxiEngineProvider {
136 fn create_provider(&self, provider_name: &str) -> Result<Arc<dyn oxi_sdk::Provider>> {
137 self.engine.create_provider(provider_name)
138 }
139
140 fn resolve_model(&self, model_id: &str) -> Result<oxi_sdk::Model> {
141 self.engine.resolve_model(model_id)
142 }
143
144 fn default_model_id(&self) -> &str {
145 &self.engine.default_model_id
146 }
147}
148
149impl std::fmt::Debug for OxiEngineProvider {
150 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151 f.debug_struct("OxiEngineProvider")
152 .field("default_model_id", &self.engine.default_model_id)
153 .finish()
154 }
155}
156
157#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn test_resolve_model_with_provider_prefix() {
167 let engine = OxiEngineProvider::new("anthropic/claude-sonnet-4-20250514");
168 let model = engine.resolve_model("openai/gpt-4o").unwrap();
169 assert_eq!(model.provider, "openai");
170 assert_eq!(model.id, "gpt-4o");
171 }
172
173 #[test]
174 fn test_resolve_model_without_provider_prefix() {
175 let engine = OxiEngineProvider::new("anthropic/claude-sonnet-4-20250514");
176 let model = engine.resolve_model("claude-sonnet-4-20250514").unwrap();
177 assert_eq!(model.provider, "anthropic");
178 }
179
180 #[test]
181 fn test_default_model_id() {
182 let engine = OxiEngineProvider::new("anthropic/claude-sonnet-4-20250514");
183 assert_eq!(
184 engine.default_model_id(),
185 "anthropic/claude-sonnet-4-20250514"
186 );
187 }
188
189 #[test]
190 fn test_resolve_model_not_found() {
191 let engine = OxiEngineProvider::new("anthropic/claude-sonnet-4-20250514");
192 let result = engine.resolve_model("nonexistent/model-xyz");
193 assert!(result.is_err());
194 }
195
196 #[test]
197 fn test_create_provider_anthropic() {
198 let engine = OxiEngineProvider::new("anthropic/claude-sonnet-4-20250514");
199 let provider = engine.create_provider("anthropic");
200 assert!(provider.is_ok());
201 }
202
203 #[test]
204 fn test_create_provider_not_found() {
205 let engine = OxiEngineProvider::new("anthropic/claude-sonnet-4-20250514");
206 let result = engine.create_provider("nonexistent_provider");
207 assert!(result.is_err());
208 }
209}