1use crate::ai::adapter::{AiError, ProviderAdapter, ProviderErrorBody, ProviderRequest};
15use crate::ai::types::{
16 ChatMessage, ChatRequest, ChatResponse, ChatRole, Choice, ContentPart, FinishReason,
17 StreamDelta, StreamEvent, ToolCallDelta, Usage,
18};
19use crate::config::ai::AiProviderKind;
20use serde_json::{json, Map, Value};
21
22pub struct OpenAiAdapter;
24
25impl ProviderAdapter for OpenAiAdapter {
26 fn kind(&self) -> AiProviderKind {
27 AiProviderKind::Openai
28 }
29
30 fn build_request(
31 &self,
32 req: &ChatRequest,
33 provider_model: &str,
34 ) -> Result<ProviderRequest, AiError> {
35 let mut body = Map::new();
36 body.insert("model".into(), json!(provider_model));
37 let messages: Vec<Value> = req.messages.iter().map(message_to_openai).collect();
38 body.insert("messages".into(), Value::Array(messages));
39 if !req.tools.is_empty() {
40 let tools: Vec<Value> = req
41 .tools
42 .iter()
43 .map(|t| {
44 let mut f = Map::new();
45 f.insert("name".into(), json!(t.name));
46 if let Some(d) = &t.description {
47 f.insert("description".into(), json!(d));
48 }
49 if let Some(p) = &t.parameters {
50 f.insert("parameters".into(), p.clone());
51 }
52 json!({"type": "function", "function": Value::Object(f)})
53 })
54 .collect();
55 body.insert("tools".into(), Value::Array(tools));
56 if let Some(choice) = &req.tool_choice {
57 body.insert(
58 "tool_choice".into(),
59 match choice {
60 crate::ai::types::ToolChoice::Auto => json!("auto"),
61 crate::ai::types::ToolChoice::None => json!("none"),
62 crate::ai::types::ToolChoice::Any => json!("required"),
63 crate::ai::types::ToolChoice::Tool(name) => json!({
64 "type": "function",
65 "function": {"name": name}
66 }),
67 },
68 );
69 }
70 }
71 if let Some(t) = req.temperature {
72 body.insert("temperature".into(), json!(t));
73 }
74 if let Some(p) = req.top_p {
75 body.insert("top_p".into(), json!(p));
76 }
77 if let Some(m) = req.max_tokens {
78 body.insert("max_tokens".into(), json!(m));
79 }
80 if let Some(stop) = &req.stop {
81 body.insert("stop".into(), json!(stop));
82 }
83 if req.stream {
84 body.insert("stream".into(), json!(true));
85 if req.stream_options_include_usage {
86 body.insert("stream_options".into(), json!({"include_usage": true}));
87 }
88 }
89 if let Some(rf) = &req.response_format {
92 body.insert("response_format".into(), response_format_to_openai(rf));
93 }
94 for (k, v) in &req.other {
97 body.insert(k.clone(), v.clone());
98 }
99 Ok(ProviderRequest {
100 method: http::Method::POST,
101 path: "/v1/chat/completions".to_string(),
102 headers: vec![],
103 body: Value::Object(body),
104 })
105 }
106
107 fn parse_response(&self, body: &Value) -> Result<ChatResponse, AiError> {
108 let obj = body.as_object().ok_or_else(|| {
109 AiError::Translation("openai response is not a JSON object".to_string())
110 })?;
111 let choices_arr = obj
112 .get("choices")
113 .and_then(Value::as_array)
114 .ok_or_else(|| AiError::Translation("openai response has no choices".to_string()))?;
115 let mut choices = Vec::with_capacity(choices_arr.len());
116 for c in choices_arr {
117 let index = c.get("index").and_then(Value::as_u64).unwrap_or(0);
118 let message = c
119 .get("message")
120 .ok_or_else(|| AiError::Translation("openai choice has no message".to_string()))?;
121 choices.push(Choice {
122 index,
123 message: parse_openai_message(message)?,
124 finish_reason: parse_finish_reason(c.get("finish_reason")),
125 });
126 }
127 let usage = obj.get("usage").map(parse_usage);
128 Ok(ChatResponse {
129 id: obj.get("id").and_then(Value::as_str).map(String::from),
130 model: obj.get("model").and_then(Value::as_str).map(String::from),
131 choices,
132 usage,
133 })
134 }
135
136 fn parse_error(&self, body: &Value) -> ProviderErrorBody {
137 match body.get("error") {
140 Some(Value::Object(e)) => ProviderErrorBody {
141 message: e
142 .get("message")
143 .and_then(Value::as_str)
144 .unwrap_or("provider returned an error")
145 .to_string(),
146 error_type: e.get("type").and_then(Value::as_str).map(String::from),
147 code: e.get("code").map(|c| match c {
148 Value::String(s) => s.clone(),
149 other => other.to_string(),
150 }),
151 },
152 Some(Value::String(s)) => ProviderErrorBody {
153 message: s.clone(),
154 error_type: None,
155 code: None,
156 },
157 _ => ProviderErrorBody {
158 message: "provider returned an error".to_string(),
159 error_type: None,
160 code: None,
161 },
162 }
163 }
164
165 fn parse_stream_event(&self, data: &Value) -> Result<Vec<StreamEvent>, AiError> {
166 let obj = data.as_object().ok_or_else(|| {
167 AiError::Translation("openai stream chunk is not a JSON object".to_string())
168 })?;
169 let mut out = Vec::new();
170 if let Some(choices) = obj.get("choices").and_then(Value::as_array) {
171 for c in choices {
172 let index = c.get("index").and_then(Value::as_u64).unwrap_or(0);
173 let delta = c.get("delta").cloned().unwrap_or(Value::Null);
174 let mut sd = StreamDelta {
175 index,
176 role: None,
177 content: None,
178 tool_calls: Vec::new(),
179 finish_reason: None,
180 };
181 if let Some(role) = delta.get("role").and_then(Value::as_str) {
182 sd.role = match role {
183 "assistant" => Some(ChatRole::Assistant),
184 _ => None,
185 };
186 }
187 if let Some(text) = delta.get("content").and_then(Value::as_str) {
188 sd.content = Some(text.to_string());
189 }
190 if let Some(calls) = delta.get("tool_calls").and_then(Value::as_array) {
191 for tc in calls {
192 sd.tool_calls.push(ToolCallDelta {
193 index: tc.get("index").and_then(Value::as_u64).unwrap_or(0),
194 id: tc.get("id").and_then(Value::as_str).map(String::from),
195 name: tc
196 .get("function")
197 .and_then(|f| f.get("name"))
198 .and_then(Value::as_str)
199 .map(String::from),
200 arguments: tc
201 .get("function")
202 .and_then(|f| f.get("arguments"))
203 .and_then(Value::as_str)
204 .unwrap_or_default()
205 .to_string(),
206 });
207 }
208 }
209 sd.finish_reason = c
210 .get("finish_reason")
211 .filter(|v| !v.is_null())
212 .map(|v| parse_finish_reason(Some(v)));
213 out.push(StreamEvent::Delta(sd));
214 }
215 }
216 if let Some(usage) = obj.get("usage").filter(|u| !u.is_null()) {
217 out.push(StreamEvent::Usage(parse_usage(usage)));
218 }
219 Ok(out)
220 }
221
222 fn stream_done_sentinel(&self) -> Option<&'static str> {
223 Some("[DONE]")
224 }
225}
226
227pub(crate) fn parse_openai_message(v: &Value) -> Result<ChatMessage, AiError> {
229 let obj = v
230 .as_object()
231 .ok_or_else(|| AiError::Translation("openai message is not an object".to_string()))?;
232 let role = match obj.get("role").and_then(Value::as_str) {
233 Some("system") => ChatRole::System,
234 Some("user") => ChatRole::User,
235 Some("assistant") => ChatRole::Assistant,
236 Some("tool") => ChatRole::Tool,
237 _ => ChatRole::Assistant,
238 };
239 let mut content = Vec::new();
240 match obj.get("content") {
241 Some(Value::String(s)) => content.push(ContentPart::Text { text: s.clone() }),
242 Some(Value::Array(parts)) => {
243 for p in parts {
244 match p.get("type").and_then(Value::as_str) {
245 Some("text") => content.push(ContentPart::Text {
246 text: p
247 .get("text")
248 .and_then(Value::as_str)
249 .unwrap_or_default()
250 .to_string(),
251 }),
252 Some("image_url") => content.push(ContentPart::Image {
253 url: p
254 .get("image_url")
255 .and_then(|i| i.get("url"))
256 .and_then(Value::as_str)
257 .map(String::from),
258 media_type: None,
259 data_b64: None,
260 }),
261 _ => {}
262 }
263 }
264 }
265 _ => {}
266 }
267 let mut tool_calls = Vec::new();
268 if let Some(calls) = obj.get("tool_calls").and_then(Value::as_array) {
269 for tc in calls {
270 let func = tc.get("function").cloned().unwrap_or(Value::Null);
271 tool_calls.push(crate::ai::types::ToolCall {
272 id: tc
273 .get("id")
274 .and_then(Value::as_str)
275 .unwrap_or_default()
276 .to_string(),
277 name: func
278 .get("name")
279 .and_then(Value::as_str)
280 .unwrap_or_default()
281 .to_string(),
282 arguments: func
283 .get("arguments")
284 .and_then(Value::as_str)
285 .unwrap_or_default()
286 .to_string(),
287 });
288 }
289 }
290 Ok(ChatMessage {
291 role,
292 content,
293 name: obj.get("name").and_then(Value::as_str).map(String::from),
294 tool_calls,
295 tool_call_id: obj
296 .get("tool_call_id")
297 .and_then(Value::as_str)
298 .map(String::from),
299 })
300}
301
302pub(crate) fn message_to_openai(m: &ChatMessage) -> Value {
307 let mut obj = Map::new();
308 obj.insert("role".into(), json!(m.role.as_str()));
309 if m.content.len() == 1 {
310 if let ContentPart::Text { text } = &m.content[0] {
311 obj.insert("content".into(), json!(text));
312 }
313 } else if !m.content.is_empty() {
314 let parts: Vec<Value> = m
315 .content
316 .iter()
317 .map(|p| match p {
318 ContentPart::Text { text } => json!({"type": "text", "text": text}),
319 ContentPart::Image { url, .. } => json!({
320 "type": "image_url",
321 "image_url": {"url": url.clone().unwrap_or_default()}
322 }),
323 })
324 .collect();
325 obj.insert("content".into(), Value::Array(parts));
326 } else {
327 obj.insert("content".into(), Value::Null);
328 }
329 if let Some(name) = &m.name {
330 obj.insert("name".into(), json!(name));
331 }
332 if !m.tool_calls.is_empty() {
333 let calls: Vec<Value> = m
334 .tool_calls
335 .iter()
336 .map(|tc| {
337 json!({
338 "id": tc.id,
339 "type": "function",
340 "function": {"name": tc.name, "arguments": tc.arguments}
341 })
342 })
343 .collect();
344 obj.insert("tool_calls".into(), Value::Array(calls));
345 }
346 if let Some(id) = &m.tool_call_id {
347 obj.insert("tool_call_id".into(), json!(id));
348 }
349 Value::Object(obj)
350}
351
352fn parse_finish_reason(v: Option<&Value>) -> FinishReason {
354 match v.and_then(Value::as_str) {
355 Some("stop") | None => FinishReason::Stop,
356 Some("length") => FinishReason::Length,
357 Some("tool_calls") | Some("function_call") => FinishReason::ToolCalls,
358 Some("content_filter") => FinishReason::ContentFilter,
359 Some(other) => FinishReason::Other(other.to_string()),
360 }
361}
362
363fn parse_usage(v: &Value) -> Usage {
367 Usage {
368 prompt_tokens: v.get("prompt_tokens").and_then(Value::as_u64),
369 completion_tokens: v.get("completion_tokens").and_then(Value::as_u64),
370 total_tokens: v.get("total_tokens").and_then(Value::as_u64),
371 cached_tokens: v
372 .get("prompt_tokens_details")
373 .and_then(|d| d.get("cached_tokens"))
374 .and_then(Value::as_u64),
375 }
376}
377
378fn response_format_to_openai(rf: &crate::ai::types::ResponseFormat) -> Value {
381 use crate::ai::types::ResponseFormat;
382 match rf {
383 ResponseFormat::Text => json!({"type": "text"}),
384 ResponseFormat::JsonObject => json!({"type": "json_object"}),
385 ResponseFormat::JsonSchema { name, schema } => json!({
386 "type": "json_schema",
387 "json_schema": {"name": name, "schema": schema}
388 }),
389 }
390}