Skip to main content

mesh_client/network/
http_parse.rs

1use anyhow::{Context, Result, anyhow, bail};
2
3use crate::network::transport::TransportIo;
4
5const MAX_HEADER_BYTES: usize = 64 * 1024;
6const MAX_BODY_BYTES: usize = 8 * 1024 * 1024;
7const MAX_OBJECT_UPLOAD_BODY_BYTES: usize = 64 * 1024 * 1024;
8const MAX_CHUNKED_WIRE_BYTES: usize = MAX_BODY_BYTES * 6 + 64 * 1024;
9const MAX_OBJECT_UPLOAD_CHUNKED_WIRE_BYTES: usize = MAX_OBJECT_UPLOAD_BODY_BYTES * 6 + 64 * 1024;
10const MAX_HEADERS: usize = 64;
11
12#[derive(Debug, Clone, Copy)]
13struct HttpReadLimits {
14    max_header_bytes: usize,
15    max_body_bytes: usize,
16    max_chunked_wire_bytes: usize,
17}
18
19const HTTP_READ_LIMITS: HttpReadLimits = HttpReadLimits {
20    max_header_bytes: MAX_HEADER_BYTES,
21    max_body_bytes: MAX_BODY_BYTES,
22    max_chunked_wire_bytes: MAX_CHUNKED_WIRE_BYTES,
23};
24
25struct ParsedHeaders {
26    header_end: usize,
27    method: String,
28    path: String,
29    content_length: Option<usize>,
30    is_chunked: bool,
31    expects_continue: bool,
32}
33
34struct RequestNormalization {
35    changed: bool,
36    rewritten_path: Option<String>,
37    response_adapter: ResponseAdapter,
38}
39
40#[derive(Debug)]
41pub struct BufferedHttpRequest {
42    pub raw: Vec<u8>,
43    pub method: String,
44    pub path: String,
45    pub body_json: Option<serde_json::Value>,
46    pub model_name: Option<String>,
47    pub session_hint: Option<String>,
48    pub request_object_request_ids: Vec<String>,
49    pub response_adapter: ResponseAdapter,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum ResponseAdapter {
54    None,
55    OpenAiResponsesJson,
56    OpenAiResponsesStream,
57}
58
59pub fn is_models_list_request(method: &str, path: &str) -> bool {
60    let path = path.split('?').next().unwrap_or(path);
61    method == "GET" && (path == "/v1/models" || path == "/models")
62}
63
64pub fn pipeline_request_supported(path: &str, body: &serde_json::Value) -> bool {
65    let path = path.split('?').next().unwrap_or(path);
66    path == "/v1/chat/completions"
67        && body
68            .get("messages")
69            .map(|messages| messages.is_array())
70            .unwrap_or(false)
71}
72
73pub async fn read_http_request(transport: &mut dyn TransportIo) -> Result<BufferedHttpRequest> {
74    let mut raw = Vec::with_capacity(8192);
75    let parsed =
76        read_until_headers_parsed(transport, &mut raw, HTTP_READ_LIMITS.max_header_bytes).await?;
77    let limits = body_limits_for_path(&parsed.path, HTTP_READ_LIMITS);
78    let header_end = parsed.header_end;
79
80    let body = if parsed.is_chunked {
81        let mut sent_continue = false;
82        loop {
83            if let Some((consumed, decoded)) =
84                try_decode_chunked_body(&raw[header_end..], limits.max_body_bytes)?
85            {
86                raw.truncate(header_end + consumed);
87                break decoded;
88            }
89            if !sent_continue && parsed.expects_continue {
90                transport
91                    .write_all(b"HTTP/1.1 100 Continue\r\n\r\n")
92                    .await?;
93                sent_continue = true;
94            }
95            read_more(transport, &mut raw).await?;
96            if raw.len().saturating_sub(header_end) > limits.max_chunked_wire_bytes {
97                bail!(
98                    "HTTP chunked wire body exceeds {} bytes",
99                    limits.max_chunked_wire_bytes
100                );
101            }
102        }
103    } else if let Some(content_length) = parsed.content_length {
104        if content_length > limits.max_body_bytes {
105            bail!("HTTP body exceeds {} bytes", limits.max_body_bytes);
106        }
107        let body_end = header_end + content_length;
108        let mut sent_continue = false;
109        while raw.len() < body_end {
110            if !sent_continue && parsed.expects_continue && content_length > 0 {
111                transport
112                    .write_all(b"HTTP/1.1 100 Continue\r\n\r\n")
113                    .await?;
114                sent_continue = true;
115            }
116            read_more(transport, &mut raw).await?;
117        }
118        raw.truncate(body_end);
119        raw[header_end..body_end].to_vec()
120    } else {
121        raw.truncate(header_end);
122        Vec::new()
123    };
124
125    let mut body_json: Option<serde_json::Value> = if body.is_empty() {
126        None
127    } else {
128        serde_json::from_slice(&body).ok()
129    };
130    let mut request_path = parsed.path.clone();
131    let mut response_adapter = ResponseAdapter::None;
132    let rewritten_body = if let Some(body_json) = body_json.as_mut() {
133        let normalization = normalize_openai_compat_request(&parsed.path, body_json)?;
134        if let Some(rewritten_path) = normalization.rewritten_path {
135            request_path = rewritten_path;
136        }
137        response_adapter = normalization.response_adapter;
138        if normalization.changed {
139            Some(
140                serde_json::to_vec(body_json)
141                    .context("serialize normalized OpenAI-compatible request body")?,
142            )
143        } else {
144            None
145        }
146    } else {
147        None
148    };
149    let model_name = body_json.as_ref().and_then(extract_model_from_json);
150    let session_hint = body_json.as_ref().and_then(extract_session_hint_from_json);
151    let raw = finalize_forwarded_request(
152        raw,
153        header_end,
154        parsed.expects_continue,
155        Some(&request_path),
156        rewritten_body.as_deref(),
157    )?;
158
159    Ok(BufferedHttpRequest {
160        raw,
161        method: parsed.method,
162        path: request_path,
163        body_json,
164        model_name,
165        session_hint,
166        request_object_request_ids: Vec::new(),
167        response_adapter,
168    })
169}
170
171fn body_limits_for_path(path: &str, default: HttpReadLimits) -> HttpReadLimits {
172    let path_only = path.split('?').next().unwrap_or(path);
173    if path_only == "/api/objects" {
174        HttpReadLimits {
175            max_header_bytes: default.max_header_bytes,
176            max_body_bytes: MAX_OBJECT_UPLOAD_BODY_BYTES,
177            max_chunked_wire_bytes: MAX_OBJECT_UPLOAD_CHUNKED_WIRE_BYTES,
178        }
179    } else {
180        default
181    }
182}
183
184fn finalize_forwarded_request(
185    mut raw: Vec<u8>,
186    header_end: usize,
187    strip_expect: bool,
188    rewritten_path: Option<&str>,
189    rewritten_body: Option<&[u8]>,
190) -> Result<Vec<u8>> {
191    let original_body = raw.split_off(header_end);
192    let mut headers_buf = [httparse::EMPTY_HEADER; MAX_HEADERS];
193    let mut req = httparse::Request::new(&mut headers_buf);
194    let _ = req.parse(&raw).context("re-parse headers for forwarding")?;
195
196    let method = req.method.unwrap_or("GET");
197    let path = rewritten_path.unwrap_or_else(|| req.path.unwrap_or("/"));
198    let version = req.version.unwrap_or(1);
199
200    let mut rebuilt = format!("{method} {path} HTTP/1.{version}\r\n");
201
202    for header in req.headers.iter() {
203        let name = header.name;
204        if name.eq_ignore_ascii_case("connection") {
205            continue;
206        }
207        if strip_expect && name.eq_ignore_ascii_case("expect") {
208            continue;
209        }
210        if rewritten_body.is_some()
211            && (name.eq_ignore_ascii_case("content-length")
212                || name.eq_ignore_ascii_case("transfer-encoding"))
213        {
214            continue;
215        }
216        let value = std::str::from_utf8(header.value).unwrap_or("");
217        rebuilt.push_str(&format!("{name}: {value}\r\n"));
218    }
219    if let Some(body) = rewritten_body {
220        rebuilt.push_str(&format!("Content-Length: {}\r\n", body.len()));
221    }
222
223    rebuilt.push_str("Connection: close\r\n\r\n");
224
225    let mut forwarded = rebuilt.into_bytes();
226    forwarded.extend_from_slice(rewritten_body.unwrap_or(&original_body));
227    Ok(forwarded)
228}
229
230async fn read_until_headers_parsed(
231    transport: &mut dyn TransportIo,
232    buf: &mut Vec<u8>,
233    max_header_bytes: usize,
234) -> Result<ParsedHeaders> {
235    loop {
236        let mut headers_buf = [httparse::EMPTY_HEADER; MAX_HEADERS];
237        let mut req = httparse::Request::new(&mut headers_buf);
238        match req.parse(buf) {
239            Ok(httparse::Status::Complete(header_end)) => {
240                let method = req.method.unwrap_or("GET").to_string();
241                let path = req.path.unwrap_or("/").to_string();
242
243                let mut content_length = None;
244                let mut is_chunked = false;
245                let mut expects_continue = false;
246
247                for header in req.headers.iter() {
248                    if header.name.eq_ignore_ascii_case("content-length") {
249                        let val = std::str::from_utf8(header.value)
250                            .context("invalid Content-Length encoding")?;
251                        content_length = Some(
252                            val.trim()
253                                .parse::<usize>()
254                                .with_context(|| format!("invalid Content-Length: {val}"))?,
255                        );
256                    } else if header.name.eq_ignore_ascii_case("transfer-encoding") {
257                        let val = std::str::from_utf8(header.value).unwrap_or("");
258                        is_chunked = val
259                            .split(',')
260                            .any(|part| part.trim().eq_ignore_ascii_case("chunked"));
261                    } else if header.name.eq_ignore_ascii_case("expect") {
262                        let val = std::str::from_utf8(header.value).unwrap_or("");
263                        expects_continue = val
264                            .split(',')
265                            .any(|part| part.trim().eq_ignore_ascii_case("100-continue"));
266                    }
267                }
268
269                if is_chunked {
270                    content_length = None;
271                }
272
273                return Ok(ParsedHeaders {
274                    header_end,
275                    method,
276                    path,
277                    content_length,
278                    is_chunked,
279                    expects_continue,
280                });
281            }
282            Ok(httparse::Status::Partial) => {
283                if buf.len() >= max_header_bytes {
284                    bail!("HTTP headers exceed {max_header_bytes} bytes");
285                }
286                read_more(transport, buf).await?;
287            }
288            Err(e) => bail!("HTTP parse error: {e}"),
289        }
290    }
291}
292
293async fn read_more(transport: &mut dyn TransportIo, buf: &mut Vec<u8>) -> Result<()> {
294    let mut chunk = [0u8; 8192];
295    let n = transport.read(&mut chunk).await?;
296    if n == 0 {
297        bail!("unexpected EOF while reading HTTP request");
298    }
299    buf.extend_from_slice(&chunk[..n]);
300    Ok(())
301}
302
303fn try_decode_chunked_body(buf: &[u8], max_body_bytes: usize) -> Result<Option<(usize, Vec<u8>)>> {
304    let mut pos = 0usize;
305    let mut decoded = Vec::new();
306
307    loop {
308        let Some(line_end_rel) = buf[pos..].windows(2).position(|window| window == b"\r\n") else {
309            return Ok(None);
310        };
311        let line_end = pos + line_end_rel;
312        let size_line = std::str::from_utf8(&buf[pos..line_end]).context("invalid chunk header")?;
313        let size_text = size_line.split(';').next().unwrap_or("").trim();
314        let size = usize::from_str_radix(size_text, 16)
315            .with_context(|| format!("invalid chunk size: {size_text}"))?;
316        pos = line_end + 2;
317
318        if size == 0 {
319            if buf.len() < pos + 2 {
320                return Ok(None);
321            }
322            if &buf[pos..pos + 2] == b"\r\n" {
323                return Ok(Some((pos + 2, decoded)));
324            }
325            let Some(trailer_end_rel) = buf[pos..]
326                .windows(4)
327                .position(|window| window == b"\r\n\r\n")
328            else {
329                return Ok(None);
330            };
331            return Ok(Some((pos + trailer_end_rel + 4, decoded)));
332        }
333
334        if buf.len() < pos + size + 2 {
335            return Ok(None);
336        }
337        decoded.extend_from_slice(&buf[pos..pos + size]);
338        pos += size;
339
340        if &buf[pos..pos + 2] != b"\r\n" {
341            return Err(anyhow!("invalid chunk terminator"));
342        }
343        pos += 2;
344
345        if decoded.len() > max_body_bytes {
346            bail!("HTTP chunked body exceeds {max_body_bytes} bytes");
347        }
348    }
349}
350
351fn extract_model_from_json(body: &serde_json::Value) -> Option<String> {
352    body.get("model")
353        .and_then(|value| value.as_str())
354        .map(ToString::to_string)
355}
356
357fn extract_session_hint_from_json(body: &serde_json::Value) -> Option<String> {
358    ["user", "session_id"].into_iter().find_map(|key| {
359        body.get(key)
360            .and_then(|value| value.as_str())
361            .map(ToString::to_string)
362    })
363}
364
365fn path_only(path: &str) -> &str {
366    path.split('?').next().unwrap_or(path)
367}
368
369fn rewrite_path_preserving_query(path: &str, new_path: &str) -> String {
370    match path.split_once('?') {
371        Some((_, query)) => format!("{new_path}?{query}"),
372        None => new_path.to_string(),
373    }
374}
375
376fn alias_max_tokens(object: &mut serde_json::Map<String, serde_json::Value>) -> bool {
377    let mut changed = false;
378    for alias in ["max_completion_tokens", "max_output_tokens"] {
379        let Some(value) = object.remove(alias) else {
380            continue;
381        };
382        changed = true;
383        object.entry("max_tokens".to_string()).or_insert(value);
384    }
385    changed
386}
387
388fn normalize_openai_compat_request(
389    path: &str,
390    body: &mut serde_json::Value,
391) -> Result<RequestNormalization> {
392    let Some(object) = body.as_object_mut() else {
393        return Ok(RequestNormalization {
394            changed: false,
395            rewritten_path: None,
396            response_adapter: ResponseAdapter::None,
397        });
398    };
399
400    let mut changed = alias_max_tokens(object);
401    let mut rewritten_path = None;
402    let mut response_adapter = ResponseAdapter::None;
403
404    if path_only(path) == "/v1/responses" {
405        let is_stream = object
406            .get("stream")
407            .and_then(|value| value.as_bool())
408            .unwrap_or(false);
409        changed |= translate_openai_responses_input(object)?;
410        rewritten_path = Some(rewrite_path_preserving_query(path, "/v1/chat/completions"));
411        response_adapter = if is_stream {
412            ResponseAdapter::OpenAiResponsesStream
413        } else {
414            ResponseAdapter::OpenAiResponsesJson
415        };
416    }
417
418    Ok(RequestNormalization {
419        changed,
420        rewritten_path,
421        response_adapter,
422    })
423}
424
425fn map_response_role(role: &str) -> String {
426    match role {
427        "developer" => "system".to_string(),
428        other => other.to_string(),
429    }
430}
431
432fn object_or_url_container(
433    value: Option<&serde_json::Value>,
434    fallback_url: Option<&str>,
435) -> Option<serde_json::Map<String, serde_json::Value>> {
436    match value {
437        Some(serde_json::Value::Object(map)) => Some(map.clone()),
438        Some(serde_json::Value::String(url)) => Some(serde_json::Map::from_iter([(
439            "url".to_string(),
440            serde_json::Value::String(url.clone()),
441        )])),
442        _ => fallback_url.map(|url| {
443            serde_json::Map::from_iter([(
444                "url".to_string(),
445                serde_json::Value::String(url.to_string()),
446            )])
447        }),
448    }
449}
450
451fn translate_responses_content_item(item: &serde_json::Value) -> Result<serde_json::Value> {
452    let Some(object) = item.as_object() else {
453        return Ok(serde_json::json!({
454            "type": "text",
455            "text": item.as_str().unwrap_or_default(),
456        }));
457    };
458    let item_type = object
459        .get("type")
460        .and_then(|value| value.as_str())
461        .unwrap_or("text");
462
463    match item_type {
464        "input_text" | "text" => {
465            let text = object
466                .get("text")
467                .and_then(|value| value.as_str())
468                .unwrap_or_default();
469            Ok(serde_json::json!({"type": "text", "text": text}))
470        }
471        "input_image" | "image_url" | "image" => {
472            let container = object_or_url_container(
473                object.get("image_url").or_else(|| object.get("image")),
474                object.get("url").and_then(|value| value.as_str()),
475            )
476            .ok_or_else(|| anyhow!("responses input_image block is missing image_url/url"))?;
477            Ok(serde_json::json!({"type": "image_url", "image_url": container}))
478        }
479        "input_audio" | "audio" | "audio_url" => {
480            let mut container = object_or_url_container(
481                object
482                    .get("input_audio")
483                    .or_else(|| object.get("audio_url")),
484                object.get("url").and_then(|value| value.as_str()),
485            )
486            .unwrap_or_default();
487            for key in [
488                "data",
489                "format",
490                "mime_type",
491                "mesh_token",
492                "blob_token",
493                "token",
494            ] {
495                if let Some(value) = object.get(key) {
496                    container
497                        .entry(key.to_string())
498                        .or_insert_with(|| value.clone());
499                }
500            }
501            if container.is_empty() {
502                bail!("responses input_audio block is missing input_audio/audio_url/url");
503            }
504            Ok(serde_json::json!({"type": "input_audio", "input_audio": container}))
505        }
506        "input_file" | "file" => {
507            let mut container = object_or_url_container(
508                object.get("input_file").or_else(|| object.get("file")),
509                object.get("url").and_then(|value| value.as_str()),
510            )
511            .ok_or_else(|| anyhow!("responses input_file block is missing input_file/file/url"))?;
512            for key in [
513                "mime_type",
514                "file_name",
515                "filename",
516                "mesh_token",
517                "blob_token",
518                "token",
519            ] {
520                if let Some(value) = object.get(key) {
521                    container
522                        .entry(key.to_string())
523                        .or_insert_with(|| value.clone());
524                }
525            }
526            Ok(serde_json::json!({"type": "input_file", "input_file": container}))
527        }
528        other => bail!("unsupported /v1/responses content block type '{other}'"),
529    }
530}
531
532fn collapse_blocks_if_text_only(blocks: Vec<serde_json::Value>) -> serde_json::Value {
533    if blocks.len() == 1
534        && let Some(text) = blocks[0].get("text").and_then(|value| value.as_str())
535    {
536        return serde_json::Value::String(text.to_string());
537    }
538    serde_json::Value::Array(blocks)
539}
540
541fn translate_responses_message_content(content: &serde_json::Value) -> Result<serde_json::Value> {
542    match content {
543        serde_json::Value::String(text) => Ok(serde_json::Value::String(text.clone())),
544        serde_json::Value::Array(items) => {
545            let blocks = items
546                .iter()
547                .map(translate_responses_content_item)
548                .collect::<Result<Vec<_>>>()?;
549            Ok(collapse_blocks_if_text_only(blocks))
550        }
551        serde_json::Value::Object(_) => Ok(collapse_blocks_if_text_only(vec![
552            translate_responses_content_item(content)?,
553        ])),
554        _ => bail!("unsupported /v1/responses input content shape"),
555    }
556}
557
558fn translate_responses_input_message(
559    message: &serde_json::Value,
560) -> Result<serde_json::Map<String, serde_json::Value>> {
561    let Some(object) = message.as_object() else {
562        bail!("unsupported /v1/responses message shape");
563    };
564
565    let role = map_response_role(
566        object
567            .get("role")
568            .and_then(|value| value.as_str())
569            .unwrap_or("user"),
570    );
571    let content_value = object
572        .get("content")
573        .map(translate_responses_message_content)
574        .transpose()?
575        .unwrap_or_else(|| serde_json::Value::String(String::new()));
576
577    Ok(serde_json::Map::from_iter([
578        ("role".to_string(), serde_json::Value::String(role)),
579        ("content".to_string(), content_value),
580    ]))
581}
582
583fn translate_responses_input_to_messages(
584    input: &serde_json::Value,
585) -> Result<Vec<serde_json::Value>> {
586    match input {
587        serde_json::Value::String(text) => Ok(vec![serde_json::json!({
588            "role": "user",
589            "content": text,
590        })]),
591        serde_json::Value::Array(items) => {
592            let looks_like_messages = items.iter().all(|item| {
593                item.as_object()
594                    .map(|object| object.contains_key("role") || object.contains_key("content"))
595                    .unwrap_or(false)
596            });
597            if looks_like_messages {
598                items
599                    .iter()
600                    .map(translate_responses_input_message)
601                    .map(|result| result.map(serde_json::Value::Object))
602                    .collect()
603            } else {
604                let content = translate_responses_message_content(input)?;
605                Ok(vec![serde_json::json!({
606                    "role": "user",
607                    "content": content,
608                })])
609            }
610        }
611        serde_json::Value::Object(object) => {
612            if object.contains_key("role") || object.contains_key("content") {
613                Ok(vec![serde_json::Value::Object(
614                    translate_responses_input_message(input)?,
615                )])
616            } else {
617                let content = translate_responses_message_content(input)?;
618                Ok(vec![serde_json::json!({
619                    "role": "user",
620                    "content": content,
621                })])
622            }
623        }
624        _ => bail!("unsupported /v1/responses input shape"),
625    }
626}
627
628fn translate_openai_responses_input(
629    object: &mut serde_json::Map<String, serde_json::Value>,
630) -> Result<bool> {
631    let mut changed = false;
632    let mut messages = Vec::new();
633
634    if let Some(instructions_value) = object.remove("instructions") {
635        if let Some(instructions) = instructions_value.as_str().map(str::trim)
636            && !instructions.is_empty()
637        {
638            messages.push(serde_json::json!({
639                "role": "system",
640                "content": instructions,
641            }));
642        }
643        changed = true;
644    }
645
646    if let Some(input) = object.remove("input") {
647        messages.extend(translate_responses_input_to_messages(&input)?);
648        changed = true;
649    } else if let Some(existing_messages) = object.remove("messages") {
650        messages.extend(translate_responses_input_to_messages(&existing_messages)?);
651        changed = true;
652    }
653
654    if !messages.is_empty() {
655        object.insert("messages".to_string(), serde_json::Value::Array(messages));
656    }
657
658    for key in [
659        "include",
660        "output",
661        "output_text",
662        "parallel_tool_calls",
663        "previous_response_id",
664        "reasoning",
665        "store",
666        "text",
667        "truncation",
668    ] {
669        if object.remove(key).is_some() {
670            changed = true;
671        }
672    }
673
674    Ok(changed)
675}