1use crate::error::ProxyError as ProxyLifecycleError;
6use async_stream::try_stream;
7use axum::Json;
8use axum::body::Body;
9use axum::http::{HeaderMap, Response, StatusCode, header};
10use axum::response::IntoResponse;
11use bytes::Bytes;
12use futures_util::{FutureExt, Stream, StreamExt};
13use serde::Serialize;
14use serde_json::Value;
15use std::env;
16use std::fmt;
17use std::future::Future;
18use std::sync::atomic::{AtomicUsize, Ordering};
19use tokio::task::JoinHandle;
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub struct ProxyMeta {
26 pub id: &'static str,
27 pub version: u32,
28}
29
30pub fn run<F, Fut>(run_async: F) -> Result<(), ProxyLifecycleError>
35where
36 F: FnOnce() -> Fut,
37 Fut: Future<Output = Result<(), ProxyLifecycleError>>,
38{
39 let runtime = tokio::runtime::Builder::new_multi_thread()
40 .enable_all()
41 .build()
42 .map_err(|error| ProxyLifecycleError::Lifecycle {
43 message: format!("failed to create proxy tokio runtime: {error}"),
44 })?;
45 runtime.block_on(run_async())
46}
47
48#[derive(Serialize)]
50pub struct ProxyHealthcheckResponse {
51 pub ready: bool,
52 pub prefill_instances: usize,
53 pub decode_instances: usize,
54}
55
56pub async fn forward_response(
59 response: reqwest::Response,
60) -> Result<Response<Body>, ProxyHttpError> {
61 let status = status_code(response.status())?;
62 let content_type = response
63 .headers()
64 .get(reqwest::header::CONTENT_TYPE)
65 .and_then(|value| value.to_str().ok())
66 .map(str::to_owned);
67 let bytes = response
68 .bytes()
69 .await
70 .map_err(|error| ProxyHttpError::upstream("upstream response body read failed", error))?;
71 let mut builder = Response::builder().status(status);
72 if let Some(content_type) = content_type {
73 builder = builder.header(header::CONTENT_TYPE, content_type);
74 }
75 builder.body(Body::from(bytes)).map_err(|error| {
76 ProxyHttpError::internal(format!("failed to build proxy response: {error}"))
77 })
78}
79
80pub async fn upstream_status_error(context: &str, response: reqwest::Response) -> ProxyHttpError {
83 let status = response.status();
84 let body = match response.text().await {
85 Ok(text) => text,
86 Err(error) => format!("<failed to read upstream error body: {error}>"),
87 };
88 ProxyHttpError::status(
89 StatusCode::BAD_GATEWAY,
90 format!("{context} returned HTTP {status}: {body}"),
91 )
92}
93
94pub fn outbound_authorization(headers: &HeaderMap) -> Option<String> {
97 headers
98 .get(header::AUTHORIZATION)
99 .and_then(|value| value.to_str().ok())
100 .map(str::to_owned)
101 .or_else(|| {
102 env::var("OPENAI_API_KEY")
103 .ok()
104 .map(|key| format!("Bearer {key}"))
105 })
106}
107
108pub fn join_path(base: &str, path: &str) -> String {
111 format!("{}{}", base.trim_end_matches('/'), path)
112}
113
114pub fn status_code(status: reqwest::StatusCode) -> Result<StatusCode, ProxyHttpError> {
116 StatusCode::from_u16(status.as_u16())
117 .map_err(|error| ProxyHttpError::internal(format!("invalid upstream status code: {error}")))
118}
119
120pub(crate) fn round_robin_index(cursor: &AtomicUsize, len: usize) -> usize {
124 cursor.fetch_add(1, Ordering::SeqCst) % len
125}
126
127pub(crate) async fn send_json_post(
133 client: reqwest::Client,
134 url: String,
135 body: &Value,
136 request_id: Option<&str>,
137 authorization: Option<&str>,
138 extra_headers: &[(&str, String)],
139 context: &'static str,
140) -> Result<reqwest::Response, ProxyHttpError> {
141 let mut request = client.post(url).json(body);
142 if let Some(request_id) = request_id {
143 request = request.header("X-Request-Id", request_id);
144 }
145 for (name, value) in extra_headers {
148 request = request.header(*name, value);
149 }
150 if let Some(authorization) = authorization {
151 request = request.header(reqwest::header::AUTHORIZATION, authorization);
152 }
153 let response = request
154 .send()
155 .await
156 .map_err(|error| ProxyHttpError::upstream(&format!("{context} failed"), error))?;
157 if !response.status().is_success() {
158 return Err(upstream_status_error(context, response).await);
159 }
160 Ok(response)
161}
162
163pub(crate) fn next_request_id(counter: &AtomicUsize) -> String {
166 let value = counter.fetch_add(1, Ordering::SeqCst);
167 format!("{}-{value}", std::process::id())
168}
169
170pub(crate) fn build_pooled_client() -> reqwest::Result<reqwest::Client> {
174 reqwest::Client::builder()
175 .pool_max_idle_per_host(usize::MAX)
176 .build()
177}
178
179#[derive(Debug)]
181pub struct ProxyHttpError {
182 status: StatusCode,
183 message: String,
184}
185
186impl ProxyHttpError {
187 pub fn status(status: StatusCode, message: impl Into<String>) -> Self {
188 Self {
189 status,
190 message: message.into(),
191 }
192 }
193
194 pub fn upstream(context: &str, error: reqwest::Error) -> Self {
195 Self::status(StatusCode::BAD_GATEWAY, format!("{context}: {error}"))
196 }
197
198 pub fn internal(message: impl Into<String>) -> Self {
199 Self::status(StatusCode::INTERNAL_SERVER_ERROR, message)
200 }
201}
202
203impl fmt::Display for ProxyHttpError {
204 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
205 write!(formatter, "{}", self.message)
206 }
207}
208
209impl std::error::Error for ProxyHttpError {}
210
211impl IntoResponse for ProxyHttpError {
212 fn into_response(self) -> axum::response::Response {
213 let body = Json(ProxyErrorResponse {
214 error: self.message,
215 });
216 (self.status, body).into_response()
217 }
218}
219
220#[derive(Serialize)]
221pub struct ProxyErrorResponse {
222 pub error: String,
223}
224
225pub(crate) fn stream_decode_response(
232 response: reqwest::Response,
233 prefill_task: JoinHandle<Result<(), ProxyHttpError>>,
234) -> Result<Response<Body>, ProxyHttpError> {
235 let status = status_code(response.status())?;
236 let content_type = response
237 .headers()
238 .get(reqwest::header::CONTENT_TYPE)
239 .and_then(|value| value.to_str().ok())
240 .map(str::to_owned);
241 let stream = decode_response_stream(response.bytes_stream(), prefill_task);
242 let mut builder = Response::builder().status(status);
243 if let Some(content_type) = content_type {
244 builder = builder.header(header::CONTENT_TYPE, content_type);
245 }
246 builder.body(Body::from_stream(stream)).map_err(|error| {
247 ProxyHttpError::internal(format!("failed to build proxy response: {error}"))
248 })
249}
250
251pub(crate) fn stream_response(
253 response: reqwest::Response,
254) -> Result<Response<Body>, ProxyHttpError> {
255 let status = status_code(response.status())?;
256 let content_type = response
257 .headers()
258 .get(reqwest::header::CONTENT_TYPE)
259 .and_then(|value| value.to_str().ok())
260 .map(str::to_owned);
261 let stream = response
262 .bytes_stream()
263 .map(|chunk| chunk.map_err(|error| stream_error(format!("decode stream failed: {error}"))));
264 let mut builder = Response::builder().status(status);
265 if let Some(content_type) = content_type {
266 builder = builder.header(header::CONTENT_TYPE, content_type);
267 }
268 builder.body(Body::from_stream(stream)).map_err(|error| {
269 ProxyHttpError::internal(format!("failed to build proxy response: {error}"))
270 })
271}
272
273pub(crate) fn decode_response_stream<S, E>(
279 decode_stream: S,
280 prefill_task: JoinHandle<Result<(), ProxyHttpError>>,
281) -> impl Stream<Item = std::result::Result<Bytes, std::io::Error>>
282where
283 S: Stream<Item = std::result::Result<Bytes, E>> + Unpin,
284 E: fmt::Display,
285{
286 let prefill_abort = prefill_task.abort_handle();
287 try_stream! {
288 let mut decode_stream = decode_stream;
289 let mut prefill_task = prefill_task;
290 let mut prefill_abort = AbortOnDrop::new(prefill_abort);
291 let mut prefill_done = false;
292 loop {
293 match next_stream_event(&mut prefill_task, &mut decode_stream, prefill_done).await {
294 StreamEvent::Prefill(prefill) => {
295 prefill_done = true;
296 match decode_stream.next().now_or_never() {
305 Some(Some(Ok(bytes))) => yield bytes,
306 Some(Some(Err(error))) => {
307 Err(stream_error(format!("decode stream failed: {error}")))?;
308 }
309 Some(None) | None => {}
310 }
311 prefill
312 .map_err(join_error)?
313 .map_err(|error| stream_error(error.to_string()))?;
314 prefill_abort.disarm();
315 }
316 StreamEvent::Decode(Some(Ok(bytes))) => yield bytes,
317 StreamEvent::Decode(Some(Err(error))) => {
318 Err(stream_error(format!("decode stream failed: {error}")))?;
319 }
320 StreamEvent::Decode(None) => break,
321 }
322 }
323 if !prefill_done {
324 prefill_task
325 .await
326 .map_err(join_error)?
327 .map_err(|error| stream_error(error.to_string()))?;
328 prefill_abort.disarm();
329 }
330 }
331}
332
333enum StreamEvent<E> {
334 Prefill(std::result::Result<Result<(), ProxyHttpError>, tokio::task::JoinError>),
335 Decode(Option<std::result::Result<Bytes, E>>),
336}
337
338async fn next_stream_event<S, E>(
339 prefill_task: &mut JoinHandle<Result<(), ProxyHttpError>>,
340 decode_stream: &mut S,
341 prefill_done: bool,
342) -> StreamEvent<E>
343where
344 S: Stream<Item = std::result::Result<Bytes, E>> + Unpin,
345{
346 if !prefill_done && prefill_task.is_finished() {
353 return StreamEvent::Prefill(prefill_task.await);
354 }
355 tokio::select! {
361 prefill = prefill_task, if !prefill_done => StreamEvent::Prefill(prefill),
362 chunk = decode_stream.next() => StreamEvent::Decode(chunk),
363 }
364}
365
366fn join_error(error: tokio::task::JoinError) -> std::io::Error {
367 stream_error(format!("prefill task failed: {error}"))
368}
369
370fn stream_error(message: String) -> std::io::Error {
371 std::io::Error::other(message)
372}
373
374struct AbortOnDrop {
376 handle: tokio::task::AbortHandle,
377 armed: bool,
378}
379
380impl AbortOnDrop {
381 fn new(handle: tokio::task::AbortHandle) -> Self {
382 Self {
383 handle,
384 armed: true,
385 }
386 }
387
388 fn disarm(&mut self) {
389 self.armed = false;
390 }
391}
392
393impl Drop for AbortOnDrop {
394 fn drop(&mut self) {
395 if self.armed {
396 self.handle.abort();
397 }
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404 use anyhow::{Context, Result};
405
406 #[test]
407 fn join_path_normalizes_single_trailing_slash() {
408 assert_eq!(
409 join_path("http://h:1/", "/v1/models"),
410 "http://h:1/v1/models"
411 );
412 assert_eq!(
413 join_path("http://h:1", "/v1/models"),
414 "http://h:1/v1/models"
415 );
416 }
417
418 #[test]
419 fn status_code_maps_reqwest_status() -> Result<()> {
420 let mapped = status_code(reqwest::StatusCode::OK)
421 .map_err(|error| anyhow::anyhow!(error.to_string()))?;
422 assert_eq!(mapped, StatusCode::OK);
423 Ok(())
424 }
425
426 #[test]
427 fn outbound_authorization_prefers_inbound_header() -> Result<()> {
428 let mut headers = HeaderMap::new();
429 headers.insert(header::AUTHORIZATION, "Bearer inbound".parse()?);
430 assert_eq!(
431 outbound_authorization(&headers),
432 Some("Bearer inbound".to_owned())
433 );
434 Ok(())
435 }
436
437 #[test]
438 fn proxy_error_internal_uses_500() {
439 let error = ProxyHttpError::internal("boom");
440 assert_eq!(error.status, StatusCode::INTERNAL_SERVER_ERROR);
441 assert_eq!(error.to_string(), "boom");
442 }
443
444 use std::sync::Arc;
445 use std::sync::atomic::{AtomicBool, Ordering};
446
447 struct SetOnDrop(Arc<AtomicBool>);
450
451 impl Drop for SetOnDrop {
452 fn drop(&mut self) {
453 self.0.store(true, Ordering::SeqCst);
454 }
455 }
456
457 fn proxy_test_runtime() -> Result<tokio::runtime::Runtime> {
458 tokio::runtime::Builder::new_multi_thread()
459 .enable_all()
460 .build()
461 .map_err(|error| anyhow::anyhow!(error.to_string()))
462 }
463
464 #[test]
465 fn streamed_decode_yields_bytes_in_order_when_prefill_succeeds() -> Result<()> {
466 let runtime = proxy_test_runtime()?;
467 let bytes = runtime.block_on(async {
468 let decode = Box::pin(futures_util::stream::iter(vec![
469 std::result::Result::<Bytes, std::io::Error>::Ok(Bytes::from_static(b"hello")),
470 Ok(Bytes::from_static(b" world")),
471 ]));
472 let prefill = tokio::spawn(async { Ok::<(), ProxyHttpError>(()) });
473 let mut stream = Box::pin(decode_response_stream(decode, prefill));
474 let mut out = Vec::new();
475 while let Some(item) = stream.next().await {
476 out.push(item.map_err(|error| anyhow::anyhow!(error.to_string()))?);
477 }
478 anyhow::Ok(out)
479 })?;
480 let joined: Vec<u8> = bytes.into_iter().flatten().collect();
481 assert_eq!(joined, b"hello world");
482 Ok(())
483 }
484
485 #[test]
486 fn streamed_decode_surfaces_prefill_error_after_decode_ends() -> Result<()> {
487 let runtime = proxy_test_runtime()?;
488 let (bytes, error) = runtime.block_on(async {
489 let decode = Box::pin(futures_util::stream::iter(vec![std::result::Result::<
490 Bytes,
491 std::io::Error,
492 >::Ok(
493 Bytes::from_static(b"partial"),
494 )]));
495 let prefill = tokio::spawn(async {
496 Err::<(), ProxyHttpError>(ProxyHttpError::internal("prefill boom"))
497 });
498 let mut stream = Box::pin(decode_response_stream(decode, prefill));
499 let mut bytes = Vec::new();
500 let mut error = None;
501 while let Some(item) = stream.next().await {
502 match item {
503 Ok(chunk) => bytes.extend_from_slice(&chunk),
504 Err(stream_error) => {
505 error = Some(stream_error.to_string());
506 break;
507 }
508 }
509 }
510 anyhow::Ok((bytes, error))
511 })?;
512 assert_eq!(bytes, b"partial");
513 let error = error.context("expected a prefill error to surface after decode ended")?;
514 assert!(error.contains("prefill boom"), "got {error}");
515 Ok(())
516 }
517
518 #[test]
523 fn prefill_error_surfaces_even_while_decode_stays_ready() -> Result<()> {
524 let runtime = proxy_test_runtime()?;
525 let error = runtime.block_on(async {
526 let decode = Box::pin(futures_util::stream::repeat_with(|| {
528 std::result::Result::<Bytes, std::io::Error>::Ok(Bytes::from_static(b"x"))
529 }));
530 let prefill = tokio::spawn(async {
531 Err::<(), ProxyHttpError>(ProxyHttpError::internal("prefill boom"))
532 });
533 let mut stream = Box::pin(decode_response_stream(decode, prefill));
534 let mut chunks = 0usize;
535 let mut error = None;
536 while let Some(item) = stream.next().await {
537 match item {
538 Ok(_) => {
539 chunks += 1;
540 assert!(
544 chunks < 100_000,
545 "prefill error was suppressed by a continuously-ready decode stream"
546 );
547 }
548 Err(stream_error) => {
549 error = Some(stream_error.to_string());
550 break;
551 }
552 }
553 }
554 anyhow::Ok(error)
555 })?;
556 let error = error.context("a prefill error must surface even while decode stays ready")?;
557 assert!(error.contains("prefill boom"), "got {error}");
558 Ok(())
559 }
560
561 #[test]
567 fn decode_error_ready_at_tiebreak_is_not_swallowed() -> Result<()> {
568 let runtime = proxy_test_runtime()?;
569 let error = runtime.block_on(async {
570 let prefill = tokio::spawn(async { Ok::<(), ProxyHttpError>(()) });
573 while !prefill.is_finished() {
574 tokio::task::yield_now().await;
575 }
576 let decode = Box::pin(futures_util::stream::iter(vec![std::result::Result::<
578 Bytes,
579 std::io::Error,
580 >::Err(
581 std::io::Error::other("decode boom"),
582 )]));
583 let mut stream = Box::pin(decode_response_stream(decode, prefill));
584 let mut error = None;
585 while let Some(item) = stream.next().await {
586 if let Err(stream_error) = item {
587 error = Some(stream_error.to_string());
588 break;
589 }
590 }
591 anyhow::Ok(error)
592 })?;
593 let error =
594 error.context("a decode error ready at the tie-break must surface, not truncate")?;
595 assert!(error.contains("decode boom"), "got {error}");
596 Ok(())
597 }
598
599 #[test]
600 fn dropping_the_stream_before_prefill_finishes_aborts_prefill() -> Result<()> {
601 let runtime = proxy_test_runtime()?;
602 let aborted = Arc::new(AtomicBool::new(false));
603 let flag = aborted.clone();
604 let cancelled = runtime.block_on(async move {
605 let (started_tx, started_rx) = tokio::sync::oneshot::channel::<()>();
606 let prefill = tokio::spawn(async move {
611 let _guard = SetOnDrop(flag);
612 let _ = started_tx.send(());
613 futures_util::future::pending::<()>().await;
614 Ok::<(), ProxyHttpError>(())
615 });
616 let _ = started_rx.await;
617 let decode = Box::pin(
620 futures_util::stream::once(async {
621 std::result::Result::<Bytes, std::io::Error>::Ok(Bytes::from_static(b"a"))
622 })
623 .chain(futures_util::stream::pending::<
624 std::result::Result<Bytes, std::io::Error>,
625 >()),
626 );
627 let mut stream = Box::pin(decode_response_stream(decode, prefill));
628 assert!(matches!(stream.next().await, Some(Ok(_))));
629 drop(stream);
630 for _ in 0..200 {
631 if aborted.load(Ordering::SeqCst) {
632 return true;
633 }
634 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
635 }
636 false
637 });
638 assert!(
639 cancelled,
640 "prefill task was not aborted when the response stream was dropped"
641 );
642 Ok(())
643 }
644}