Skip to main content

formal_ai/
proxy.rs

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/// Runtime configuration for `formal-ai proxy`.
15#[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/// One JSONL row describing a proxied request/response exchange.
24#[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/// A normalized tool/function call emitted by a proxied model response.
41#[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
99/// Run the blocking logging proxy until the process is terminated.
100///
101/// The proxy accepts HTTP/1.1 requests, forwards them to the configured HTTP
102/// upstream, streams response bytes back to the client, and appends a structured
103/// JSON row per completed exchange.
104pub 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/// Build the JSONL exchange summary without opening sockets.
141#[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}