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) 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::*;
#[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"
));
}
}