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}