apollo/providers/
openai_compat.rs1use async_trait::async_trait;
4use serde_json::Value;
5
6use super::retry::send_with_retry;
7use super::traits::*;
8use crate::text::truncate_chars;
9use crate::tools::ToolSpec;
10
11pub struct OpenAiCompatProvider {
12 api_key: String,
13 base_url: String,
14 provider_name: String,
15}
16
17impl OpenAiCompatProvider {
18 pub fn new(
19 api_key: impl Into<String>,
20 base_url: impl Into<String>,
21 name: impl Into<String>,
22 ) -> Self {
23 Self {
24 api_key: api_key.into(),
25 base_url: base_url.into(),
26 provider_name: name.into(),
27 }
28 }
29
30 pub fn openai(api_key: impl Into<String>) -> Self {
32 Self::new(api_key, "https://api.openai.com/v1", "openai")
33 }
34
35 pub fn openrouter(api_key: impl Into<String>) -> Self {
37 Self::new(api_key, "https://openrouter.ai/api/v1", "openrouter")
38 }
39
40 pub fn groq(api_key: impl Into<String>) -> Self {
42 Self::new(api_key, "https://api.groq.com/openai/v1", "groq")
43 }
44
45 pub fn together(api_key: impl Into<String>) -> Self {
47 Self::new(api_key, "https://api.together.xyz/v1", "together")
48 }
49
50 pub fn mistral(api_key: impl Into<String>) -> Self {
52 Self::new(api_key, "https://api.mistral.ai/v1", "mistral")
53 }
54
55 pub fn deepseek(api_key: impl Into<String>) -> Self {
57 Self::new(api_key, "https://api.deepseek.com/v1", "deepseek")
58 }
59
60 pub fn fireworks(api_key: impl Into<String>) -> Self {
62 Self::new(
63 api_key,
64 "https://api.fireworks.ai/inference/v1",
65 "fireworks",
66 )
67 }
68
69 pub fn perplexity(api_key: impl Into<String>) -> Self {
71 Self::new(api_key, "https://api.perplexity.ai", "perplexity")
72 }
73
74 pub fn xai(api_key: impl Into<String>) -> Self {
76 Self::new(api_key, "https://api.x.ai/v1", "xai")
77 }
78
79 pub fn moonshot(api_key: impl Into<String>) -> Self {
81 Self::new(api_key, "https://api.moonshot.ai/v1", "moonshot")
82 }
83
84 pub fn venice(api_key: impl Into<String>) -> Self {
86 Self::new(api_key, "https://api.venice.ai/api/v1", "venice")
87 }
88
89 pub fn huggingface(api_key: impl Into<String>) -> Self {
91 Self::new(
92 api_key,
93 "https://api-inference.huggingface.co/v1",
94 "huggingface",
95 )
96 }
97
98 pub fn siliconflow(api_key: impl Into<String>) -> Self {
100 Self::new(api_key, "https://api.siliconflow.cn/v1", "siliconflow")
101 }
102
103 pub fn cerebras(api_key: impl Into<String>) -> Self {
105 Self::new(api_key, "https://api.cerebras.ai/v1", "cerebras")
106 }
107
108 pub fn minimax(api_key: impl Into<String>) -> Self {
110 Self::new(api_key, "https://api.minimax.io/v1", "minimax")
111 }
112
113 pub fn vercel(api_key: impl Into<String>) -> Self {
115 Self::new(api_key, "https://gateway.vercel.ai/v1", "vercel")
116 }
117
118 pub fn cloudflare(api_key: impl Into<String>, account_id: &str) -> Self {
120 Self::new(
121 api_key,
122 format!(
123 "https://api.cloudflare.com/client/v4/accounts/{}/ai/v1",
124 account_id
125 ),
126 "cloudflare",
127 )
128 }
129
130 fn build_tools_payload(&self, tools: &[ToolSpec]) -> Vec<Value> {
131 tools
132 .iter()
133 .map(|t| {
134 serde_json::json!({
135 "type": "function",
136 "function": {
137 "name": t.name,
138 "description": t.description,
139 "parameters": t.parameters,
140 }
141 })
142 })
143 .collect()
144 }
145}
146
147#[async_trait]
148impl Provider for OpenAiCompatProvider {
149 fn name(&self) -> &str {
150 &self.provider_name
151 }
152
153 fn capabilities(&self) -> ProviderCapabilities {
154 ProviderCapabilities {
155 native_tools: true,
156 streaming: true,
157 vision: true,
158 max_context: 128_000,
159 native_web_search: false,
160 }
161 }
162
163 async fn chat(&self, request: &ChatRequest<'_>) -> anyhow::Result<ChatResponse> {
164 let client = crate::http::shared();
165
166 let messages: Vec<Value> = request
167 .messages
168 .iter()
169 .map(|m| serde_json::json!({ "role": &m.role, "content": &m.content }))
170 .collect();
171
172 let mut body = serde_json::json!({
173 "model": request.model,
174 "messages": messages,
175 "temperature": request.temperature,
176 });
177
178 if let Some(max) = request.max_tokens {
179 body["max_tokens"] = Value::Number(max.into());
180 }
181
182 if let Some(tools) = request.tools {
183 if !tools.is_empty() {
184 body["tools"] = Value::Array(self.build_tools_payload(tools));
185 }
186 }
187
188 let resp = send_with_retry(
189 client
190 .post(format!("{}/chat/completions", self.base_url))
191 .header("Authorization", format!("Bearer {}", self.api_key))
192 .header("Content-Type", "application/json")
193 .json(&body),
194 self.name(),
195 )
196 .await?;
197
198 if !resp.status().is_success() {
199 let status = resp.status();
200 let text = resp.text().await.unwrap_or_default();
201 anyhow::bail!(
202 "{} API error {}: {}",
203 self.provider_name,
204 status,
205 truncate_chars(&text, 200)
206 );
207 }
208
209 let data: Value = resp.json().await?;
210 let choice = &data["choices"][0];
211
212 let text = choice["message"]["content"].as_str().map(String::from);
213
214 let tool_calls = choice["message"]["tool_calls"]
215 .as_array()
216 .map(|calls| {
217 calls
218 .iter()
219 .map(|tc| ToolCall {
220 id: tc["id"].as_str().unwrap_or("").to_string(),
221 name: tc["function"]["name"].as_str().unwrap_or("").to_string(),
222 arguments: tc["function"]["arguments"]
223 .as_str()
224 .unwrap_or("{}")
225 .to_string(),
226 })
227 .collect()
228 })
229 .unwrap_or_default();
230
231 let usage = data["usage"].as_object().map(|u| Usage {
232 input_tokens: u.get("prompt_tokens").and_then(|v| v.as_u64()).unwrap_or(0) as u32,
233 output_tokens: u
234 .get("completion_tokens")
235 .and_then(|v| v.as_u64())
236 .unwrap_or(0) as u32,
237 });
238
239 Ok(ChatResponse {
240 text,
241 tool_calls,
242 usage,
243 })
244 }
245}