use std::time::Duration;
use crate::error::{LlmError, body_is_context_length_error};
#[must_use]
pub fn llm_client(request_timeout_secs: u64) -> reqwest::Client {
reqwest::Client::builder()
.connect_timeout(Duration::from_secs(30))
.timeout(Duration::from_secs(request_timeout_secs))
.user_agent(concat!("zeph/", env!("CARGO_PKG_VERSION")))
.redirect(reqwest::redirect::Policy::limited(10))
.build()
.expect("LLM HTTP client construction must not fail")
}
pub(crate) async fn check_response(
response: reqwest::Response,
provider: &str,
) -> Result<String, LlmError> {
let status = response.status();
let text = response.text().await.map_err(LlmError::Http)?;
if !status.is_success() {
tracing::error!(%provider, %status, body = %text, "API error response");
return Err(map_error_response(status, &text, provider));
}
Ok(text)
}
pub(crate) async fn read_response_body(
response: reqwest::Response,
) -> Result<(reqwest::StatusCode, String), LlmError> {
let status = response.status();
let text = response.text().await.map_err(LlmError::Http)?;
Ok((status, text))
}
pub(crate) fn map_error_response(
status: reqwest::StatusCode,
body: &str,
provider: &str,
) -> LlmError {
if status == reqwest::StatusCode::BAD_REQUEST && body_is_context_length_error(body) {
LlmError::ContextLengthExceeded
} else {
LlmError::ApiError {
provider: provider.into(),
status: status.as_u16(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::assert_matches;
#[test]
fn bad_request_with_context_length_body_returns_exceeded() {
let err = map_error_response(
reqwest::StatusCode::BAD_REQUEST,
"context length exceeded",
"p",
);
assert_matches!(err, LlmError::ContextLengthExceeded);
}
#[test]
fn bad_request_without_context_body_returns_api_error() {
let err = map_error_response(
reqwest::StatusCode::BAD_REQUEST,
"invalid parameter",
"myprovider",
);
assert_matches!(
err,
LlmError::ApiError { ref provider, status: 400 } if provider == "myprovider"
);
}
#[test]
fn server_error_returns_api_error() {
let err = map_error_response(reqwest::StatusCode::INTERNAL_SERVER_ERROR, "", "svc");
assert_matches!(
err,
LlmError::ApiError { ref provider, status: 500 } if provider == "svc"
);
}
#[test]
fn unauthorized_returns_api_error() {
let err = map_error_response(reqwest::StatusCode::UNAUTHORIZED, "", "claude");
assert_matches!(
err,
LlmError::ApiError { ref provider, status: 401 } if provider == "claude"
);
}
#[test]
fn too_many_requests_returns_api_error() {
let err = map_error_response(reqwest::StatusCode::TOO_MANY_REQUESTS, "", "openai");
assert_matches!(
err,
LlmError::ApiError { ref provider, status: 429 } if provider == "openai"
);
}
}