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