opendev_http/adapters/openai/
mod.rs1mod request;
11mod response;
12
13use serde_json::{Value, json};
14
15const DEFAULT_API_URL: &str = "https://api.openai.com/v1/responses";
16
17const REASONING_PREFIXES: &[&str] = &["o1", "o3"];
19
20#[derive(Debug, Clone)]
25pub struct OpenAiAdapter {
26 api_url: String,
27}
28
29impl OpenAiAdapter {
30 pub fn new() -> Self {
32 Self {
33 api_url: DEFAULT_API_URL.to_string(),
34 }
35 }
36
37 pub fn with_url(url: impl Into<String>) -> Self {
39 Self {
40 api_url: url.into(),
41 }
42 }
43}
44
45impl Default for OpenAiAdapter {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51#[async_trait::async_trait]
52impl super::base::ProviderAdapter for OpenAiAdapter {
53 fn provider_name(&self) -> &str {
54 "openai"
55 }
56
57 fn convert_request(&self, payload: Value) -> Value {
58 let mut payload = payload;
59
60 let reasoning_effort = payload
62 .as_object_mut()
63 .and_then(|obj| obj.remove("_reasoning_effort"))
64 .and_then(|v| v.as_str().map(String::from));
65
66 let messages = payload
67 .get("messages")
68 .and_then(|m| m.as_array())
69 .cloned()
70 .unwrap_or_default();
71
72 let (instructions, input_items) = Self::convert_messages(&messages);
73
74 let mut responses_payload = json!({
75 "model": payload.get("model").cloned().unwrap_or(json!("")),
76 "input": input_items,
77 "store": false,
78 });
79
80 if let Some(instr) = instructions {
81 responses_payload["instructions"] = Value::String(instr);
82 }
83
84 let max_tok = payload
86 .get("max_completion_tokens")
87 .or_else(|| payload.get("max_tokens"));
88 if let Some(tok) = max_tok {
89 responses_payload["max_output_tokens"] = tok.clone();
90 }
91
92 if let Some(ref effort) = reasoning_effort {
96 responses_payload["reasoning"] = json!({
97 "effort": effort,
98 "summary": "detailed",
99 });
100 responses_payload["include"] = json!(["reasoning.encrypted_content"]);
103 } else if !Self::is_reasoning_model(&payload) {
105 if let Some(temp) = payload.get("temperature") {
107 responses_payload["temperature"] = temp.clone();
108 }
109 }
110
111 if let Some(tools) = payload.get("tools").and_then(|t| t.as_array()) {
113 responses_payload["tools"] = Value::Array(Self::convert_tools(tools));
114 }
115
116 responses_payload
117 }
118
119 fn convert_response(&self, response: Value) -> Value {
120 Self::build_chat_completion(&response)
121 }
122
123 fn api_url(&self) -> &str {
124 &self.api_url
125 }
126
127 fn supports_streaming(&self) -> bool {
128 true
129 }
130
131 fn enable_streaming(&self, payload: &mut Value) {
132 payload["stream"] = json!(true);
133 }
134
135 fn parse_stream_event(
136 &self,
137 event_type: &str,
138 data: &Value,
139 ) -> Option<crate::streaming::StreamEvent> {
140 use crate::streaming::StreamEvent;
141 match event_type {
142 "response.output_text.delta" => {
143 let delta = data.get("delta")?.as_str()?;
144 Some(StreamEvent::TextDelta(delta.to_string()))
145 }
146 "response.reasoning_summary_part.added" => Some(StreamEvent::ReasoningBlockStart),
147 "response.reasoning_summary_text.delta" => {
148 let delta = data.get("delta")?.as_str()?;
149 Some(StreamEvent::ReasoningDelta(delta.to_string()))
150 }
151 "response.output_item.added" => {
153 let item = data.get("item")?;
154 if item.get("type").and_then(|t| t.as_str()) != Some("function_call") {
155 return None;
156 }
157 let index = data
158 .get("output_index")
159 .and_then(|i| i.as_u64())
160 .unwrap_or(0) as usize;
161 let call_id = item
162 .get("call_id")
163 .or_else(|| item.get("id"))
164 .and_then(|i| i.as_str())
165 .unwrap_or("")
166 .to_string();
167 let name = item
168 .get("name")
169 .and_then(|n| n.as_str())
170 .unwrap_or("")
171 .to_string();
172 Some(StreamEvent::FunctionCallStart {
173 index,
174 call_id,
175 name,
176 })
177 }
178 "response.function_call_arguments.delta" => {
179 let index = data
180 .get("output_index")
181 .and_then(|i| i.as_u64())
182 .unwrap_or(0) as usize;
183 let delta = data.get("delta")?.as_str()?.to_string();
184 Some(StreamEvent::FunctionCallDelta { index, delta })
185 }
186 "response.function_call_arguments.done" => {
187 let index = data
188 .get("output_index")
189 .and_then(|i| i.as_u64())
190 .unwrap_or(0) as usize;
191 let arguments = data
192 .get("arguments")
193 .and_then(|a| a.as_str())
194 .unwrap_or("{}")
195 .to_string();
196 Some(StreamEvent::FunctionCallDone { index, arguments })
197 }
198 "response.completed" | "response.incomplete" => {
200 let response = data
201 .get("response")
202 .cloned()
203 .unwrap_or_else(|| data.clone());
204 Some(StreamEvent::Done(response))
205 }
206 "error" => {
207 let msg = data
208 .get("message")
209 .and_then(|m| m.as_str())
210 .unwrap_or("Unknown streaming error");
211 Some(StreamEvent::Error(msg.to_string()))
212 }
213 _ => None,
214 }
215 }
216}
217
218#[cfg(test)]
219mod tests;