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 }
160 }
161
162 async fn chat(&self, request: &ChatRequest<'_>) -> anyhow::Result<ChatResponse> {
163 let client = reqwest::Client::new();
164
165 let messages: Vec<Value> = request
166 .messages
167 .iter()
168 .map(|m| serde_json::json!({ "role": &m.role, "content": &m.content }))
169 .collect();
170
171 let mut body = serde_json::json!({
172 "model": request.model,
173 "messages": messages,
174 "temperature": request.temperature,
175 });
176
177 if let Some(max) = request.max_tokens {
178 body["max_tokens"] = Value::Number(max.into());
179 }
180
181 if let Some(tools) = request.tools {
182 if !tools.is_empty() {
183 body["tools"] = Value::Array(self.build_tools_payload(tools));
184 }
185 }
186
187 let resp = send_with_retry(
188 client
189 .post(format!("{}/chat/completions", self.base_url))
190 .header("Authorization", format!("Bearer {}", self.api_key))
191 .header("Content-Type", "application/json")
192 .json(&body),
193 self.name(),
194 )
195 .await?;
196
197 if !resp.status().is_success() {
198 let status = resp.status();
199 let text = resp.text().await.unwrap_or_default();
200 anyhow::bail!(
201 "{} API error {}: {}",
202 self.provider_name,
203 status,
204 truncate_chars(&text, 200)
205 );
206 }
207
208 let data: Value = resp.json().await?;
209 let choice = &data["choices"][0];
210
211 let text = choice["message"]["content"].as_str().map(String::from);
212
213 let tool_calls = choice["message"]["tool_calls"]
214 .as_array()
215 .map(|calls| {
216 calls
217 .iter()
218 .map(|tc| ToolCall {
219 id: tc["id"].as_str().unwrap_or("").to_string(),
220 name: tc["function"]["name"].as_str().unwrap_or("").to_string(),
221 arguments: tc["function"]["arguments"]
222 .as_str()
223 .unwrap_or("{}")
224 .to_string(),
225 })
226 .collect()
227 })
228 .unwrap_or_default();
229
230 let usage = data["usage"].as_object().map(|u| Usage {
231 input_tokens: u.get("prompt_tokens").and_then(|v| v.as_u64()).unwrap_or(0) as u32,
232 output_tokens: u
233 .get("completion_tokens")
234 .and_then(|v| v.as_u64())
235 .unwrap_or(0) as u32,
236 });
237
238 Ok(ChatResponse {
239 text,
240 tool_calls,
241 usage,
242 })
243 }
244}