mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-27 01:22:18 +00:00
* refactor(messages): take the provider client from the injected HTTP pool The messages route kept its own process-wide reqwest client, so it ignored ssl_verify, CA bundles, client certs, proxies and every other setting that litellm-http resolves. The machine now takes the HttpClientPool and the call's HttpClientConfig, as OCR does, and the bridge passes its shared pool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(http): hand out an owned Client and move chat, audio and OIDC onto the pool HttpClientPool now returns litellm_http::Client, a newtype only crates/http can build, so every provider client carries the resolved TLS, proxy and timeout settings. Chat completions and audio transcription drop their process-wide reqwest clients and take the pool and call config like messages; their 600s ceiling moves to the request. OidcResolver takes its client instead of building one, and the bridge hands it the pooled one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(secrets): build Google, Azure and CyberArk manager clients from the pool The native secret managers built bare reqwest clients, so they ignored the host's TLS and proxy settings. load_native_manager now takes the pool and the host config and hands each manager a pooled client. CyberArk's CYBERARK_SSL_VERIFY and CYBERARK_CLIENT_CERT/KEY become an override on the host config instead of a hand-built client. To express a certificate and key in separate files, HttpClientConfig::client_certificate is now a ClientIdentity that is either one PEM or a split pair. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(clippy): only crates/http may build a reqwest client Fence reqwest::Client, ClientBuilder and the TLS builder methods with disallowed-types and disallowed-methods so new code takes a litellm_http::Client from the pool. crates/http is exempt as the one place clients are built, and testkit as a dev-only installer. Tests move to litellm_http::Client::plain_for_test or a pooled client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(secrets-cyberark): keep verifying certificates when the host disables it Python hands CyberArk its own ssl_verify, which wins over the global setting, so CYBERARK_SSL_VERIFY unset or true still verifies even when the host sets ssl_verify false. The pooled client copied the host's Disabled and would send the API key unverified; fall back to the built-in roots instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(python-bridge): treat a missing litellm package as no host HTTP settings Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Yujong Lee <yujong@berri.ai> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
79 lines
2.8 KiB
Rust
79 lines
2.8 KiB
Rust
use std::time::Duration;
|
|
|
|
use litellm_llms::base_llm::ocr::{error::Error, handler::read_response_bytes};
|
|
use rstest::rstest;
|
|
use tokio::{
|
|
io::{AsyncReadExt, AsyncWriteExt},
|
|
net::TcpListener,
|
|
};
|
|
|
|
/// Answers one request with raw `response` bytes and then holds the connection open, so a
|
|
/// read that waits for the rest of an oversized body hangs instead of passing.
|
|
async fn read_bounded(response: String, limit: usize) -> Result<bytes::Bytes, Error> {
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let address = listener.local_addr().unwrap();
|
|
let server = tokio::spawn(async move {
|
|
let (mut socket, _) = listener.accept().await.unwrap();
|
|
let mut request = [0; 4096];
|
|
assert!(socket.read(&mut request).await.unwrap() > 0);
|
|
socket.write_all(response.as_bytes()).await.unwrap();
|
|
std::future::pending::<()>().await;
|
|
});
|
|
let response = litellm_http::Client::plain_for_test()
|
|
.get(format!("http://{address}"))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
let result =
|
|
tokio::time::timeout(Duration::from_secs(2), read_response_bytes(response, limit)).await;
|
|
server.abort();
|
|
result.expect("bounded reads must finish without waiting for the rest of an oversized body")
|
|
}
|
|
|
|
#[rstest]
|
|
#[case::declared("HTTP/1.1 200 OK\r\nContent-Length: 8\r\n\r\nabcdefgh")]
|
|
#[case::chunked(
|
|
"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nabcd\r\n4\r\nefgh\r\n0\r\n\r\n"
|
|
)]
|
|
#[tokio::test]
|
|
async fn a_body_of_exactly_the_limit_is_read(#[case] response: &str) {
|
|
assert_eq!(read_bounded(response.into(), 8).await.unwrap(), "abcdefgh");
|
|
}
|
|
|
|
#[rstest]
|
|
#[case::declared("HTTP/1.1 200 OK\r\nContent-Length: 9\r\n\r\n")]
|
|
#[case::chunked("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nabcd\r\n5\r\nefghi\r\n")]
|
|
#[tokio::test]
|
|
async fn a_body_over_the_limit_is_rejected(#[case] response: &str) {
|
|
assert!(matches!(
|
|
read_bounded(response.into(), 8).await,
|
|
Err(Error::TooLarge { limit: 8 })
|
|
));
|
|
}
|
|
|
|
#[rstest]
|
|
#[case::declared("Content-Length: 1000000")]
|
|
#[case::chunked("Transfer-Encoding: chunked")]
|
|
#[tokio::test]
|
|
async fn an_oversized_error_keeps_its_status_and_a_bounded_body_without_draining(
|
|
#[case] headers: &str,
|
|
) {
|
|
let prefix = "x".repeat(4096);
|
|
let body = match headers.starts_with("Transfer") {
|
|
true => format!("{:x}\r\n{prefix}\r\n", prefix.len()),
|
|
false => prefix.clone(),
|
|
};
|
|
|
|
let error = read_bounded(
|
|
format!("HTTP/1.1 429 Too Many Requests\r\n{headers}\r\n\r\n{body}"),
|
|
prefix.len(),
|
|
)
|
|
.await
|
|
.unwrap_err();
|
|
|
|
let Error::Transport(litellm_http::transport::Error::Http { status, body }) = error else {
|
|
panic!("unexpected error: {error}");
|
|
};
|
|
assert_eq!(status, 429);
|
|
assert_eq!(body, prefix);
|
|
}
|