1use std::collections::BTreeMap;
2use std::fs::{File, OpenOptions};
3use std::io::{self, BufRead, BufReader, Read, Write};
4use std::net::{TcpListener, TcpStream};
5use std::path::PathBuf;
6use std::sync::{Arc, Mutex};
7use std::thread;
8
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12use crate::protocol_policy::tool_definition_names;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct ProxyConfig {
17 pub listen: String,
18 pub upstream: String,
19 pub log_path: PathBuf,
20 pub log_bodies: bool,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub struct ProxyExchangeLog {
26 pub method: String,
27 pub path: String,
28 pub request_model: Option<String>,
29 pub request_tools: Vec<String>,
30 pub status: u16,
31 pub response_model: Option<String>,
32 pub response_tool_calls: Vec<ProxyToolCallLog>,
33 pub response_content_preview: String,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub request_body: Option<String>,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub response_body: Option<String>,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub struct ProxyToolCallLog {
43 pub name: String,
44 pub arguments: Value,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
48struct Upstream {
49 authority: String,
50 base_path: String,
51}
52
53#[derive(Debug)]
54struct HttpHeader {
55 name: String,
56 value: String,
57}
58
59#[derive(Debug)]
60struct HttpRequest {
61 method: String,
62 path: String,
63 headers: Vec<HttpHeader>,
64 body: Vec<u8>,
65}
66
67#[derive(Debug)]
68struct HttpResponseHead {
69 status_line: String,
70 status_code: u16,
71 headers: Vec<HttpHeader>,
72}
73
74#[derive(Debug, Default)]
75struct ResponseSummary {
76 model: Option<String>,
77 tool_calls: Vec<ProxyToolCallLog>,
78 content: String,
79}
80
81#[derive(Debug, Default)]
82struct StreamingChatAccumulator {
83 model: Option<String>,
84 content: String,
85 tool_calls: BTreeMap<u64, StreamingToolCall>,
86}
87
88#[derive(Debug, Default)]
89struct StreamingToolCall {
90 name: String,
91 arguments: String,
92}
93
94#[derive(Debug)]
95struct SseEvent {
96 data: String,
97}
98
99pub fn run_proxy(config: &ProxyConfig) -> io::Result<()> {
105 let upstream = Upstream::parse(&config.upstream)?;
106 let log_file = OpenOptions::new()
107 .create(true)
108 .append(true)
109 .open(&config.log_path)?;
110 let logger = Arc::new(Mutex::new(log_file));
111 let listener = TcpListener::bind(&config.listen)?;
112 eprintln!(
113 "formal-ai proxy listening on http://{} -> http://{}, logging to {}",
114 config.listen,
115 upstream.display_target(),
116 config.log_path.display()
117 );
118
119 for stream in listener.incoming() {
120 match stream {
121 Ok(stream) => {
122 let upstream = upstream.clone();
123 let logger = Arc::clone(&logger);
124 let log_bodies = config.log_bodies;
125 thread::spawn(move || {
126 if let Err(error) =
127 handle_proxy_connection(stream, &upstream, &logger, log_bodies)
128 {
129 eprintln!("proxy request failed: {error}");
130 }
131 });
132 }
133 Err(error) => eprintln!("proxy connection failed: {error}"),
134 }
135 }
136
137 Ok(())
138}
139
140#[must_use]
142pub fn summarize_proxy_exchange(
143 method: &str,
144 path: &str,
145 request_body: &[u8],
146 status: u16,
147 response_content_type: &str,
148 response_body: &[u8],
149 log_bodies: bool,
150) -> ProxyExchangeLog {
151 let request_json = serde_json::from_slice::<Value>(request_body).ok();
152 let response_text = String::from_utf8_lossy(response_body);
153 let response_summary = if response_content_type
154 .split(';')
155 .next()
156 .is_some_and(|content_type| {
157 content_type
158 .trim()
159 .eq_ignore_ascii_case("text/event-stream")
160 }) {
161 summarize_sse_response(&response_text)
162 } else {
163 serde_json::from_slice::<Value>(response_body).map_or_else(
164 |_| ResponseSummary {
165 content: response_text.chars().take(160).collect(),
166 ..ResponseSummary::default()
167 },
168 |value| summarize_response_value(&value),
169 )
170 };
171
172 ProxyExchangeLog {
173 method: method.to_owned(),
174 path: path.to_owned(),
175 request_model: request_json
176 .as_ref()
177 .and_then(|value| value.get("model"))
178 .and_then(Value::as_str)
179 .map(ToOwned::to_owned),
180 request_tools: request_json
181 .as_ref()
182 .map_or_else(Vec::new, collect_request_tool_names),
183 status,
184 response_model: response_summary.model,
185 response_tool_calls: response_summary.tool_calls,
186 response_content_preview: response_summary.content.chars().take(160).collect(),
187 request_body: log_bodies.then(|| String::from_utf8_lossy(request_body).into_owned()),
188 response_body: log_bodies.then(|| response_text.into_owned()),
189 }
190}
191
192impl Upstream {
193 fn parse(raw: &str) -> io::Result<Self> {
194 let trimmed = raw.trim();
195 let without_scheme = if let Some(rest) = trimmed.strip_prefix("http://") {
196 rest
197 } else if trimmed.contains("://") {
198 return Err(io::Error::new(
199 io::ErrorKind::InvalidInput,
200 "formal-ai proxy currently supports only http:// upstream URLs",
201 ));
202 } else {
203 trimmed
204 };
205 let (authority, path) = without_scheme
206 .split_once('/')
207 .map_or((without_scheme, ""), |(authority, path)| (authority, path));
208 if authority.trim().is_empty() {
209 return Err(io::Error::new(
210 io::ErrorKind::InvalidInput,
211 "proxy upstream must include a host and port",
212 ));
213 }
214 let base_path = if path.trim().is_empty() {
215 String::new()
216 } else {
217 format!("/{}", path.trim_matches('/'))
218 };
219 Ok(Self {
220 authority: authority.to_owned(),
221 base_path,
222 })
223 }
224
225 fn target_path(&self, request_path: &str) -> String {
226 if self.base_path.is_empty() {
227 request_path.to_owned()
228 } else if request_path.starts_with('/') {
229 format!("{}{}", self.base_path, request_path)
230 } else {
231 format!("{}/{}", self.base_path, request_path)
232 }
233 }
234
235 fn display_target(&self) -> String {
236 if self.base_path.is_empty() {
237 self.authority.clone()
238 } else {
239 format!("{}{}", self.authority, self.base_path)
240 }
241 }
242}
243
244fn handle_proxy_connection(
245 client: TcpStream,
246 upstream: &Upstream,
247 logger: &Arc<Mutex<File>>,
248 log_bodies: bool,
249) -> io::Result<()> {
250 let mut client_reader = BufReader::new(client.try_clone()?);
251 let mut client_writer = client;
252 let Some(request) = read_request(&mut client_reader)? else {
253 return Ok(());
254 };
255
256 let mut upstream_stream = match TcpStream::connect(&upstream.authority) {
257 Ok(stream) => stream,
258 Err(error) => {
259 let response_body = format!("proxy upstream connection failed: {error}");
260 write_error_response(&mut client_writer, 502, &response_body)?;
261 let log = summarize_proxy_exchange(
262 &request.method,
263 &request.path,
264 &request.body,
265 502,
266 "text/plain",
267 response_body.as_bytes(),
268 log_bodies,
269 );
270 write_log(logger, &log)?;
271 return Ok(());
272 }
273 };
274 write_upstream_request(&mut upstream_stream, upstream, &request)?;
275
276 let mut upstream_reader = BufReader::new(upstream_stream);
277 let Some(response_head) = read_response_head(&mut upstream_reader)? else {
278 let response_body = "proxy upstream closed without a response";
279 write_error_response(&mut client_writer, 502, response_body)?;
280 let log = summarize_proxy_exchange(
281 &request.method,
282 &request.path,
283 &request.body,
284 502,
285 "text/plain",
286 response_body.as_bytes(),
287 log_bodies,
288 );
289 write_log(logger, &log)?;
290 return Ok(());
291 };
292
293 write_response_head(&mut client_writer, &response_head)?;
294 let mut response_body = Vec::new();
295 forward_response_body(
296 &mut upstream_reader,
297 &mut client_writer,
298 &response_head,
299 &mut response_body,
300 )?;
301 client_writer.flush()?;
302
303 let content_type = response_head
304 .headers
305 .iter()
306 .find(|header| header.name.eq_ignore_ascii_case("content-type"))
307 .map_or("", |header| header.value.as_str());
308 let log = summarize_proxy_exchange(
309 &request.method,
310 &request.path,
311 &request.body,
312 response_head.status_code,
313 content_type,
314 &response_body,
315 log_bodies,
316 );
317 write_log(logger, &log)
318}
319
320fn write_log(logger: &Arc<Mutex<File>>, log: &ProxyExchangeLog) -> io::Result<()> {
321 let line = serde_json::to_string(log)
322 .map_err(|error| io::Error::other(format!("failed to serialize proxy log: {error}")))?;
323 let mut file = logger
324 .lock()
325 .map_err(|_| io::Error::other("proxy log mutex poisoned"))?;
326 writeln!(file, "{line}")?;
327 file.flush()
328}
329
330fn read_request(reader: &mut BufReader<TcpStream>) -> io::Result<Option<HttpRequest>> {
331 let Some(request_line) = read_http_line(reader)? else {
332 return Ok(None);
333 };
334 let request_line = trim_http_line(&request_line);
335 if request_line.is_empty() {
336 return Ok(None);
337 }
338 let mut parts = request_line.split_whitespace();
339 let method = parts.next().unwrap_or_default().to_owned();
340 let path = parts.next().unwrap_or_default().to_owned();
341 let headers = read_headers(reader)?;
342 let body = read_body(reader, &headers)?;
343 Ok(Some(HttpRequest {
344 method,
345 path,
346 headers,
347 body,
348 }))
349}
350
351fn read_response_head(reader: &mut BufReader<TcpStream>) -> io::Result<Option<HttpResponseHead>> {
352 let Some(status_line) = read_http_line(reader)? else {
353 return Ok(None);
354 };
355 let status_line = trim_http_line(&status_line);
356 let status_code = status_line
357 .split_whitespace()
358 .nth(1)
359 .and_then(|code| code.parse::<u16>().ok())
360 .unwrap_or_default();
361 let headers = read_headers(reader)?;
362 Ok(Some(HttpResponseHead {
363 status_line,
364 status_code,
365 headers,
366 }))
367}
368
369fn read_http_line(reader: &mut BufReader<TcpStream>) -> io::Result<Option<Vec<u8>>> {
370 let mut line = Vec::new();
371 let bytes = reader.read_until(b'\n', &mut line)?;
372 if bytes == 0 {
373 Ok(None)
374 } else {
375 Ok(Some(line))
376 }
377}
378
379fn trim_http_line(line: &[u8]) -> String {
380 String::from_utf8_lossy(line)
381 .trim_end_matches('\n')
382 .trim_end_matches('\r')
383 .to_owned()
384}
385
386fn read_headers(reader: &mut BufReader<TcpStream>) -> io::Result<Vec<HttpHeader>> {
387 let mut headers = Vec::new();
388 while let Some(line) = read_http_line(reader)? {
389 if line == b"\r\n" || line == b"\n" {
390 break;
391 }
392 let line = trim_http_line(&line);
393 if let Some((name, value)) = line.split_once(':') {
394 headers.push(HttpHeader {
395 name: name.trim().to_owned(),
396 value: value.trim().to_owned(),
397 });
398 }
399 }
400 Ok(headers)
401}
402
403fn read_body(reader: &mut BufReader<TcpStream>, headers: &[HttpHeader]) -> io::Result<Vec<u8>> {
404 if is_chunked(headers) {
405 return read_chunked_body(reader);
406 }
407 let Some(length) = content_length(headers) else {
408 return Ok(Vec::new());
409 };
410 let mut body = vec![0_u8; length];
411 reader.read_exact(&mut body)?;
412 Ok(body)
413}
414
415fn read_chunked_body(reader: &mut BufReader<TcpStream>) -> io::Result<Vec<u8>> {
416 let mut body = Vec::new();
417 while let Some(size_line) = read_http_line(reader)? {
418 let size = parse_chunk_size(&size_line)?;
419 if size == 0 {
420 discard_chunk_trailers(reader)?;
421 break;
422 }
423 let mut chunk = vec![0_u8; size];
424 reader.read_exact(&mut chunk)?;
425 body.extend_from_slice(&chunk);
426 let mut crlf = [0_u8; 2];
427 reader.read_exact(&mut crlf)?;
428 }
429 Ok(body)
430}
431
432fn discard_chunk_trailers(reader: &mut BufReader<TcpStream>) -> io::Result<()> {
433 while let Some(line) = read_http_line(reader)? {
434 if line == b"\r\n" || line == b"\n" {
435 break;
436 }
437 }
438 Ok(())
439}
440
441fn write_upstream_request(
442 upstream_stream: &mut TcpStream,
443 upstream: &Upstream,
444 request: &HttpRequest,
445) -> io::Result<()> {
446 write!(
447 upstream_stream,
448 "{} {} HTTP/1.1\r\n",
449 request.method,
450 upstream.target_path(&request.path)
451 )?;
452 write!(upstream_stream, "host: {}\r\n", upstream.authority)?;
453 for header in &request.headers {
454 if skip_request_header(&header.name) {
455 continue;
456 }
457 write!(upstream_stream, "{}: {}\r\n", header.name, header.value)?;
458 }
459 write!(upstream_stream, "accept-encoding: identity\r\n")?;
460 write!(upstream_stream, "connection: close\r\n")?;
461 if !request.body.is_empty() {
462 write!(
463 upstream_stream,
464 "content-length: {}\r\n",
465 request.body.len()
466 )?;
467 }
468 write!(upstream_stream, "\r\n")?;
469 upstream_stream.write_all(&request.body)?;
470 upstream_stream.flush()
471}
472
473fn write_response_head(client: &mut TcpStream, response: &HttpResponseHead) -> io::Result<()> {
474 write!(client, "{}\r\n", response.status_line)?;
475 for header in &response.headers {
476 if skip_response_header(&header.name) {
477 continue;
478 }
479 write!(client, "{}: {}\r\n", header.name, header.value)?;
480 }
481 write!(client, "connection: close\r\n\r\n")
482}
483
484fn write_error_response(client: &mut TcpStream, status: u16, body: &str) -> io::Result<()> {
485 let status_text = match status {
486 502 => "502 Bad Gateway",
487 _ => "500 Internal Server Error",
488 };
489 write!(
490 client,
491 "HTTP/1.1 {status_text}\r\n\
492 content-type: text/plain\r\n\
493 content-length: {}\r\n\
494 connection: close\r\n\
495 \r\n{body}",
496 body.len()
497 )
498}
499
500fn forward_response_body(
501 reader: &mut BufReader<TcpStream>,
502 client: &mut TcpStream,
503 response: &HttpResponseHead,
504 captured_body: &mut Vec<u8>,
505) -> io::Result<()> {
506 if is_chunked(&response.headers) {
507 return forward_chunked_body(reader, client, captured_body);
508 }
509 if let Some(length) = content_length(&response.headers) {
510 return forward_fixed_body(reader, client, captured_body, length);
511 }
512 forward_until_eof(reader, client, captured_body)
513}
514
515fn forward_fixed_body(
516 reader: &mut BufReader<TcpStream>,
517 client: &mut TcpStream,
518 captured_body: &mut Vec<u8>,
519 length: usize,
520) -> io::Result<()> {
521 let mut remaining = length;
522 let mut buffer = [0_u8; 8192];
523 while remaining > 0 {
524 let take = remaining.min(buffer.len());
525 reader.read_exact(&mut buffer[..take])?;
526 client.write_all(&buffer[..take])?;
527 captured_body.extend_from_slice(&buffer[..take]);
528 remaining -= take;
529 }
530 Ok(())
531}
532
533fn forward_until_eof(
534 reader: &mut BufReader<TcpStream>,
535 client: &mut TcpStream,
536 captured_body: &mut Vec<u8>,
537) -> io::Result<()> {
538 let mut buffer = [0_u8; 8192];
539 loop {
540 let bytes = reader.read(&mut buffer)?;
541 if bytes == 0 {
542 break;
543 }
544 client.write_all(&buffer[..bytes])?;
545 captured_body.extend_from_slice(&buffer[..bytes]);
546 }
547 Ok(())
548}
549
550fn forward_chunked_body(
551 reader: &mut BufReader<TcpStream>,
552 client: &mut TcpStream,
553 captured_body: &mut Vec<u8>,
554) -> io::Result<()> {
555 while let Some(size_line) = read_http_line(reader)? {
556 client.write_all(&size_line)?;
557 let size = parse_chunk_size(&size_line)?;
558 if size == 0 {
559 forward_chunk_trailers(reader, client)?;
560 break;
561 }
562 let mut chunk = vec![0_u8; size];
563 reader.read_exact(&mut chunk)?;
564 client.write_all(&chunk)?;
565 captured_body.extend_from_slice(&chunk);
566 let mut crlf = [0_u8; 2];
567 reader.read_exact(&mut crlf)?;
568 client.write_all(&crlf)?;
569 }
570 Ok(())
571}
572
573fn forward_chunk_trailers(
574 reader: &mut BufReader<TcpStream>,
575 client: &mut TcpStream,
576) -> io::Result<()> {
577 while let Some(line) = read_http_line(reader)? {
578 client.write_all(&line)?;
579 if line == b"\r\n" || line == b"\n" {
580 break;
581 }
582 }
583 Ok(())
584}
585
586fn parse_chunk_size(line: &[u8]) -> io::Result<usize> {
587 let text = String::from_utf8_lossy(line);
588 let size_text = text.trim().split(';').next().unwrap_or_default().trim();
589 usize::from_str_radix(size_text, 16).map_err(|error| {
590 io::Error::new(
591 io::ErrorKind::InvalidData,
592 format!("invalid chunk size `{size_text}`: {error}"),
593 )
594 })
595}
596
597const fn skip_request_header(name: &str) -> bool {
598 name.eq_ignore_ascii_case("host")
599 || name.eq_ignore_ascii_case("connection")
600 || name.eq_ignore_ascii_case("proxy-connection")
601 || name.eq_ignore_ascii_case("keep-alive")
602 || name.eq_ignore_ascii_case("transfer-encoding")
603 || name.eq_ignore_ascii_case("content-length")
604 || name.eq_ignore_ascii_case("accept-encoding")
605}
606
607const fn skip_response_header(name: &str) -> bool {
608 name.eq_ignore_ascii_case("connection")
609 || name.eq_ignore_ascii_case("proxy-connection")
610 || name.eq_ignore_ascii_case("keep-alive")
611}
612
613fn content_length(headers: &[HttpHeader]) -> Option<usize> {
614 headers.iter().find_map(|header| {
615 header
616 .name
617 .eq_ignore_ascii_case("content-length")
618 .then(|| header.value.parse::<usize>().ok())
619 .flatten()
620 })
621}
622
623fn is_chunked(headers: &[HttpHeader]) -> bool {
624 headers.iter().any(|header| {
625 header.name.eq_ignore_ascii_case("transfer-encoding")
626 && header
627 .value
628 .split(',')
629 .any(|part| part.trim().eq_ignore_ascii_case("chunked"))
630 })
631}
632
633fn collect_request_tool_names(value: &Value) -> Vec<String> {
634 let mut names = Vec::new();
635 if let Some(tools) = value.get("tools").and_then(Value::as_array) {
636 for tool in tools {
637 append_tool_definition_names(tool, &mut names);
638 }
639 }
640 if let Some(functions) = value.get("functions").and_then(Value::as_array) {
641 for function in functions {
642 if let Some(name) = function.get("name").and_then(Value::as_str) {
643 names.push(name.to_owned());
644 }
645 }
646 }
647 for choice_key in ["tool_choice", "function_call"] {
648 if let Some(name) = value
649 .get(choice_key)
650 .and_then(|choice| choice.get("function").or(Some(choice)))
651 .and_then(|function| function.get("name"))
652 .and_then(Value::as_str)
653 {
654 names.push(name.to_owned());
655 }
656 }
657 names.sort();
658 names.dedup();
659 names
660}
661
662fn append_tool_definition_names(value: &Value, names: &mut Vec<String>) {
663 names.extend(tool_definition_names(value));
664 if let Some(declarations) = value.get("functionDeclarations").and_then(Value::as_array) {
665 for declaration in declarations {
666 if let Some(name) = declaration.get("name").and_then(Value::as_str) {
667 names.push(name.to_owned());
668 }
669 }
670 }
671}
672
673fn summarize_sse_response(body: &str) -> ResponseSummary {
674 let events = parse_sse_events(body);
675 for event in &events {
676 let data = event.data.trim();
677 if data.is_empty() || data == "[DONE]" {
678 continue;
679 }
680 let Ok(value) = serde_json::from_str::<Value>(data) else {
681 continue;
682 };
683 if value.get("type").and_then(Value::as_str) == Some("response.completed") {
684 if let Some(response) = value.get("response") {
685 return summarize_response_value(response);
686 }
687 }
688 }
689
690 let mut chat = StreamingChatAccumulator::default();
691 let mut summary = ResponseSummary::default();
692 for event in events {
693 let data = event.data.trim();
694 if data.is_empty() || data == "[DONE]" {
695 continue;
696 }
697 let Ok(value) = serde_json::from_str::<Value>(data) else {
698 continue;
699 };
700 if chat.apply_chunk(&value) {
701 continue;
702 }
703 merge_response_summary(&mut summary, summarize_response_value(&value));
704 }
705 merge_response_summary(&mut summary, chat.finish());
706 summary
707}
708
709fn parse_sse_events(body: &str) -> Vec<SseEvent> {
710 let normalized = body.replace("\r\n", "\n");
711 normalized
712 .split("\n\n")
713 .filter_map(|block| {
714 let mut data = String::new();
715 for line in block.lines() {
716 if let Some(value) = line.strip_prefix("data:") {
717 if !data.is_empty() {
718 data.push('\n');
719 }
720 data.push_str(value.trim_start());
721 }
722 }
723 (!data.is_empty()).then_some(SseEvent { data })
724 })
725 .collect()
726}
727
728impl StreamingChatAccumulator {
729 fn apply_chunk(&mut self, value: &Value) -> bool {
730 let Some(choices) = value.get("choices").and_then(Value::as_array) else {
731 return false;
732 };
733 if self.model.is_none() {
734 self.model = value
735 .get("model")
736 .and_then(Value::as_str)
737 .map(ToOwned::to_owned);
738 }
739 for choice in choices {
740 let Some(delta) = choice.get("delta") else {
741 continue;
742 };
743 if let Some(content) = delta.get("content").and_then(Value::as_str) {
744 self.content.push_str(content);
745 }
746 if let Some(tool_calls) = delta.get("tool_calls").and_then(Value::as_array) {
747 for call in tool_calls {
748 let index = call.get("index").and_then(Value::as_u64).unwrap_or(0);
749 let entry = self.tool_calls.entry(index).or_default();
750 if let Some(name) = call
751 .get("function")
752 .and_then(|function| function.get("name"))
753 .and_then(Value::as_str)
754 {
755 entry.name.push_str(name);
756 }
757 if let Some(arguments) = call
758 .get("function")
759 .and_then(|function| function.get("arguments"))
760 .and_then(Value::as_str)
761 {
762 entry.arguments.push_str(arguments);
763 }
764 }
765 }
766 }
767 true
768 }
769
770 fn finish(self) -> ResponseSummary {
771 ResponseSummary {
772 model: self.model,
773 tool_calls: self
774 .tool_calls
775 .into_values()
776 .filter(|call| !call.name.is_empty())
777 .map(|call| ProxyToolCallLog {
778 name: call.name,
779 arguments: arguments_from_str(&call.arguments),
780 })
781 .collect(),
782 content: self.content,
783 }
784 }
785}
786
787fn summarize_response_value(value: &Value) -> ResponseSummary {
788 let mut summary = ResponseSummary::default();
789 apply_response_value(value, &mut summary);
790 summary
791}
792
793fn merge_response_summary(target: &mut ResponseSummary, source: ResponseSummary) {
794 if target.model.is_none() {
795 target.model = source.model;
796 }
797 target.tool_calls.extend(source.tool_calls);
798 target.content.push_str(&source.content);
799}
800
801fn apply_response_value(value: &Value, summary: &mut ResponseSummary) {
802 set_model(summary, value.get("model").and_then(Value::as_str));
803 set_model(summary, value.get("modelVersion").and_then(Value::as_str));
804
805 if let Some(response) = value.get("response") {
806 apply_response_value(response, summary);
807 }
808 if let Some(item) = value.get("item") {
809 apply_response_item(item, summary);
810 }
811 if let Some(choices) = value.get("choices").and_then(Value::as_array) {
812 for choice in choices {
813 if let Some(message) = choice.get("message") {
814 apply_chat_message(message, summary);
815 }
816 }
817 }
818 if let Some(output) = value.get("output").and_then(Value::as_array) {
819 for item in output {
820 apply_response_item(item, summary);
821 }
822 }
823 if value.get("type").and_then(Value::as_str) == Some("function_call") {
824 apply_response_item(value, summary);
825 }
826 if let Some(candidates) = value.get("candidates").and_then(Value::as_array) {
827 for candidate in candidates {
828 if let Some(parts) = candidate
829 .get("content")
830 .and_then(|content| content.get("parts"))
831 .and_then(Value::as_array)
832 {
833 for part in parts {
834 apply_gemini_part(part, summary);
835 }
836 }
837 }
838 }
839 if let Some(content) = value.get("content").and_then(Value::as_array) {
840 for block in content {
841 apply_anthropic_content_block(block, summary);
842 }
843 }
844}
845
846fn set_model(summary: &mut ResponseSummary, model: Option<&str>) {
847 if summary.model.is_none() {
848 summary.model = model.map(ToOwned::to_owned);
849 }
850}
851
852fn apply_chat_message(message: &Value, summary: &mut ResponseSummary) {
853 if let Some(tool_calls) = message.get("tool_calls").and_then(Value::as_array) {
854 for call in tool_calls {
855 if let Some(function) = call.get("function") {
856 append_function_call(function, summary);
857 }
858 }
859 }
860 if let Some(function) = message.get("function_call") {
861 append_function_call(function, summary);
862 }
863 append_content_value(message.get("content"), summary);
864}
865
866fn apply_response_item(item: &Value, summary: &mut ResponseSummary) {
867 match item.get("type").and_then(Value::as_str) {
868 Some("function_call") => {
869 if let Some(name) = item.get("name").and_then(Value::as_str) {
870 summary.tool_calls.push(ProxyToolCallLog {
871 name: name.to_owned(),
872 arguments: arguments_from_value(item.get("arguments")),
873 });
874 }
875 }
876 Some("message") => {
877 if let Some(content) = item.get("content").and_then(Value::as_array) {
878 for part in content {
879 append_content_value(part.get("text"), summary);
880 }
881 }
882 }
883 _ => {}
884 }
885}
886
887fn apply_gemini_part(part: &Value, summary: &mut ResponseSummary) {
888 if let Some(call) = part.get("functionCall") {
889 if let Some(name) = call.get("name").and_then(Value::as_str) {
890 summary.tool_calls.push(ProxyToolCallLog {
891 name: name.to_owned(),
892 arguments: arguments_from_value(call.get("args")),
893 });
894 }
895 }
896 append_content_value(part.get("text"), summary);
897}
898
899fn apply_anthropic_content_block(block: &Value, summary: &mut ResponseSummary) {
900 match block.get("type").and_then(Value::as_str) {
901 Some("tool_use") => {
902 if let Some(name) = block.get("name").and_then(Value::as_str) {
903 summary.tool_calls.push(ProxyToolCallLog {
904 name: name.to_owned(),
905 arguments: arguments_from_value(block.get("input")),
906 });
907 }
908 }
909 Some("text") => append_content_value(block.get("text"), summary),
910 _ => {}
911 }
912}
913
914fn append_function_call(function: &Value, summary: &mut ResponseSummary) {
915 if let Some(name) = function.get("name").and_then(Value::as_str) {
916 summary.tool_calls.push(ProxyToolCallLog {
917 name: name.to_owned(),
918 arguments: arguments_from_value(function.get("arguments")),
919 });
920 }
921}
922
923fn append_content_value(value: Option<&Value>, summary: &mut ResponseSummary) {
924 match value {
925 Some(Value::String(text)) => summary.content.push_str(text),
926 Some(Value::Array(parts)) => {
927 for part in parts {
928 append_content_value(part.get("text"), summary);
929 }
930 }
931 _ => {}
932 }
933}
934
935fn arguments_from_value(value: Option<&Value>) -> Value {
936 match value {
937 Some(Value::String(arguments)) => arguments_from_str(arguments),
938 Some(value) => value.clone(),
939 None => Value::Null,
940 }
941}
942
943fn arguments_from_str(arguments: &str) -> Value {
944 serde_json::from_str(arguments).unwrap_or_else(|_| Value::String(arguments.to_owned()))
945}