1use http::Method;
2use serde_json::Value;
3use url::Url;
4
5use crate::adapter::{
6 push_string, push_text_blocks, walk_all_strings, AdapterContract, PiiSurface, ProviderAdapter,
7 SseEvent, SurfaceSyntax,
8};
9
10#[derive(Clone, Debug)]
11#[non_exhaustive]
12pub struct OpenAiAdapter {
13 upstream: Url,
14}
15
16impl OpenAiAdapter {
17 pub fn new(upstream: Url) -> Self {
18 Self { upstream }
19 }
20}
21
22#[async_trait::async_trait]
23impl ProviderAdapter for OpenAiAdapter {
24 fn contract(&self) -> AdapterContract<'_> {
25 AdapterContract::legacy()
26 }
27
28 fn name(&self) -> &'static str {
29 "openai"
30 }
31
32 fn matches_path(&self, method: &Method, path: &str) -> bool {
33 method == Method::POST
34 && matches!(
35 path,
36 "/v1/chat/completions" | "/v1/completions" | "/v1/responses"
37 )
38 }
39
40 fn upstream_base(&self) -> &Url {
41 &self.upstream
42 }
43
44 fn request_pii_surfaces<'a>(&self, body: &'a mut Value) -> Vec<PiiSurface<'a>> {
56 let mut surfaces = Vec::new();
57 if let Value::Object(root) = body {
58 for (key, value) in root {
59 match key.as_str() {
60 "system" => push_string(&mut surfaces, "system", value, SurfaceSyntax::Text),
61 "prompt" => {
62 push_text_blocks(&mut surfaces, "prompt", value, SurfaceSyntax::Text);
63 }
64 "instructions" => {
65 push_string(&mut surfaces, "instructions", value, SurfaceSyntax::Text);
66 }
67 "messages" => {
68 if let Value::Array(messages) = value {
69 for (index, message) in messages.iter_mut().enumerate() {
70 collect_message_surfaces(
71 &mut surfaces,
72 format!("messages[{index}]"),
73 message,
74 );
75 }
76 }
77 }
78 "input" => {
79 push_text_blocks(&mut surfaces, "input", value, SurfaceSyntax::Text);
80 }
81 "user" => push_string(&mut surfaces, "user", value, SurfaceSyntax::Text),
85 "metadata" => walk_all_strings(&mut surfaces, "metadata".to_string(), value),
87 "stop" => walk_all_strings(&mut surfaces, "stop".to_string(), value),
89 "tools" => walk_all_strings(&mut surfaces, "tools".to_string(), value),
92 "functions" => walk_all_strings(&mut surfaces, "functions".to_string(), value),
93 "response_format" => {
94 walk_all_strings(&mut surfaces, "response_format".to_string(), value);
95 }
96 "prediction" => {
97 walk_all_strings(&mut surfaces, "prediction".to_string(), value);
98 }
99 _ => {}
100 }
101 }
102 }
103 surfaces
104 }
105
106 fn response_pii_surfaces<'a>(&self, body: &'a mut Value) -> Vec<PiiSurface<'a>> {
107 let mut surfaces = Vec::new();
108 if let Value::Object(root) = body {
109 for (key, value) in root {
110 match key.as_str() {
111 "choices" => {
112 if let Value::Array(choices) = value {
113 for (index, choice) in choices.iter_mut().enumerate() {
114 if let Value::Object(choice) = choice {
115 for (choice_key, choice_value) in choice {
116 match choice_key.as_str() {
117 "message" => collect_message_surfaces(
118 &mut surfaces,
119 format!("choices[{index}].message"),
120 choice_value,
121 ),
122 "delta" => collect_message_surfaces(
123 &mut surfaces,
124 format!("choices[{index}].delta"),
125 choice_value,
126 ),
127 "text" => push_string(
128 &mut surfaces,
129 format!("choices[{index}].text"),
130 choice_value,
131 SurfaceSyntax::ModelOutput,
132 ),
133 _ => {}
134 }
135 }
136 }
137 }
138 }
139 }
140 "output" => collect_output_surfaces(&mut surfaces, value),
141 _ => {}
142 }
143 }
144 }
145 surfaces
146 }
147
148 fn sse_event_pii_surfaces<'a>(&self, event: &'a mut SseEvent) -> Vec<PiiSurface<'a>> {
149 self.response_pii_surfaces(&mut event.data)
150 }
151
152 fn requests_json_output(&self, request: &Value) -> bool {
155 let is_json = |format: &Value| {
156 matches!(
157 format.get("type").and_then(Value::as_str),
158 Some("json_object" | "json_schema")
159 )
160 };
161 request.get("response_format").is_some_and(is_json)
162 || request
163 .get("text")
164 .and_then(|text| text.get("format"))
165 .is_some_and(is_json)
166 }
167}
168
169fn collect_output_surfaces<'a>(surfaces: &mut Vec<PiiSurface<'a>>, output: &'a mut Value) {
173 let Value::Array(items) = output else {
174 walk_all_strings(surfaces, "output".to_string(), output);
175 return;
176 };
177 for (index, item) in items.iter_mut().enumerate() {
178 let prefix = format!("output[{index}]");
179 let kind = item.get("type").and_then(Value::as_str);
180 let is_call = matches!(kind, Some("function_call" | "mcp_call"));
181 let is_message = kind == Some("message");
182 let Value::Object(fields) = item else {
183 walk_all_strings(surfaces, prefix, item);
184 continue;
185 };
186 for (key, value) in fields {
187 let path = format!("{prefix}.{key}");
188 match key.as_str() {
189 "arguments" if is_call && value.is_string() => {
190 push_string(surfaces, path, value, SurfaceSyntax::Json);
191 }
192 "content" if is_message => collect_output_content(surfaces, path, value),
193 _ => walk_all_strings(surfaces, path, value),
194 }
195 }
196 }
197}
198
199fn collect_output_content<'a>(
200 surfaces: &mut Vec<PiiSurface<'a>>,
201 prefix: String,
202 content: &'a mut Value,
203) {
204 let Value::Array(parts) = content else {
205 walk_all_strings(surfaces, prefix, content);
206 return;
207 };
208 for (index, part) in parts.iter_mut().enumerate() {
209 let part_prefix = format!("{prefix}[{index}]");
210 let is_output_text = part.get("type").and_then(Value::as_str) == Some("output_text");
211 let Value::Object(fields) = part else {
212 walk_all_strings(surfaces, part_prefix, part);
213 continue;
214 };
215 for (key, value) in fields {
216 let path = format!("{part_prefix}.{key}");
217 if is_output_text && key == "text" && value.is_string() {
218 push_string(surfaces, path, value, SurfaceSyntax::ModelOutput);
219 } else {
220 walk_all_strings(surfaces, path, value);
221 }
222 }
223 }
224}
225
226fn collect_message_surfaces<'a>(
227 surfaces: &mut Vec<PiiSurface<'a>>,
228 prefix: String,
229 message: &'a mut Value,
230) {
231 let Value::Object(map) = message else {
232 return;
233 };
234 for (key, value) in map {
235 match key.as_str() {
236 "content" => push_text_blocks(
237 surfaces,
238 &format!("{prefix}.content"),
239 value,
240 SurfaceSyntax::ModelOutput,
241 ),
242 "tool_results" => push_text_blocks(
243 surfaces,
244 &format!("{prefix}.tool_results"),
245 value,
246 SurfaceSyntax::Text,
247 ),
248 "name" => push_string(
251 surfaces,
252 format!("{prefix}.name"),
253 value,
254 SurfaceSyntax::Text,
255 ),
256 "tool_calls" => {
257 if let Value::Array(tool_calls) = value {
258 for (index, tool_call) in tool_calls.iter_mut().enumerate() {
259 if let Value::Object(tool_call) = tool_call {
260 if let Some(Value::Object(function)) = tool_call.get_mut("function") {
261 if let Some(args) = function.get_mut("arguments") {
263 push_string(
264 surfaces,
265 format!("{prefix}.tool_calls[{index}].function.arguments"),
266 args,
267 SurfaceSyntax::Json,
268 );
269 }
270 }
271 }
272 }
273 }
274 }
275 _ => {}
276 }
277 }
278}