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 = crate::credential::CredentialStore::resolve(&name_owned, None)
30 .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, base_url
37 );
38 Ok(Arc::new(provider))
39 });
40 }
41 builder
42}
43
44pub struct OxiosEngine {
46 oxi: Oxi,
47 default_model_id: String,
48}
49
50impl OxiosEngine {
51 pub fn new(default_model_id: impl Into<String>) -> Self {
56 let model_id = default_model_id.into();
57 let provider_name = model_id
58 .split_once('/')
59 .map(|(p, _)| p)
60 .unwrap_or("anthropic");
61
62 let mut builder = OxiBuilder::new().with_builtins();
65
66 builder = register_compatible_providers(builder, provider_name);
68
69 let oxi = builder.build();
70 Self {
71 oxi,
72 default_model_id: model_id,
73 }
74 }
75
76 pub fn oxi(&self) -> &Oxi {
81 &self.oxi
82 }
83
84 pub fn resolve_model(&self, model_id: &str) -> Result<oxi_sdk::Model> {
89 self.oxi.resolve_model(model_id)
90 }
91
92 pub fn create_provider(&self, name: &str) -> Result<Arc<dyn oxi_sdk::Provider>> {
97 self.oxi.create_provider(name)
98 }
99}
100
101pub trait EngineProvider: Send + Sync {
110 fn create_provider(&self, provider_name: &str) -> Result<Arc<dyn oxi_sdk::Provider>>;
112
113 fn resolve_model(&self, model_id: &str) -> Result<oxi_sdk::Model>;
115
116 fn default_model_id(&self) -> &str;
118}
119
120pub struct OxiEngineProvider {
122 engine: OxiosEngine,
123}
124
125impl OxiEngineProvider {
126 pub fn new(default_model_id: impl Into<String>) -> Self {
128 Self {
129 engine: OxiosEngine::new(default_model_id),
130 }
131 }
132}
133
134impl EngineProvider for OxiEngineProvider {
135 fn create_provider(&self, provider_name: &str) -> Result<Arc<dyn oxi_sdk::Provider>> {
136 self.engine.create_provider(provider_name)
137 }
138
139 fn resolve_model(&self, model_id: &str) -> Result<oxi_sdk::Model> {
140 self.engine.resolve_model(model_id)
141 }
142
143 fn default_model_id(&self) -> &str {
144 &self.engine.default_model_id
145 }
146}
147
148impl std::fmt::Debug for OxiEngineProvider {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 f.debug_struct("OxiEngineProvider")
151 .field("default_model_id", &self.engine.default_model_id)
152 .finish()
153 }
154}
155
156#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn test_resolve_model_with_provider_prefix() {
166 let engine = OxiEngineProvider::new("anthropic/claude-sonnet-4-20250514");
167 let model = engine.resolve_model("openai/gpt-4o").unwrap();
168 assert_eq!(model.provider, "openai");
169 assert_eq!(model.id, "gpt-4o");
170 }
171
172 #[test]
173 fn test_resolve_model_without_provider_prefix() {
174 let engine = OxiEngineProvider::new("anthropic/claude-sonnet-4-20250514");
175 let model = engine.resolve_model("claude-sonnet-4-20250514").unwrap();
176 assert_eq!(model.provider, "anthropic");
177 }
178
179 #[test]
180 fn test_default_model_id() {
181 let engine = OxiEngineProvider::new("anthropic/claude-sonnet-4-20250514");
182 assert_eq!(
183 engine.default_model_id(),
184 "anthropic/claude-sonnet-4-20250514"
185 );
186 }
187
188 #[test]
189 fn test_resolve_model_not_found() {
190 let engine = OxiEngineProvider::new("anthropic/claude-sonnet-4-20250514");
191 let result = engine.resolve_model("nonexistent/model-xyz");
192 assert!(result.is_err());
193 }
194
195 #[test]
196 fn test_create_provider_anthropic() {
197 let engine = OxiEngineProvider::new("anthropic/claude-sonnet-4-20250514");
198 let provider = engine.create_provider("anthropic");
199 assert!(provider.is_ok());
200 }
201
202 #[test]
203 fn test_create_provider_not_found() {
204 let engine = OxiEngineProvider::new("anthropic/claude-sonnet-4-20250514");
205 let result = engine.create_provider("nonexistent_provider");
206 assert!(result.is_err());
207 }
208}