use crate::chat_client::openai_api::chat_completions::{ChatCompletions, ChatCompletionsBody};
use reqwest::{
header::{HeaderMap, HeaderName, HeaderValue, InvalidHeaderValue, AUTHORIZATION},
Client, ClientBuilder, StatusCode,
};
use serde::Deserialize;
use std::{fmt::Display, str::FromStr, time::Duration};
const CHAT_COMPLETIONS_ENDPOINT: &str = "chat/completions";
const REQUEST_TIMEOUT: Duration = Duration::from_secs(300);
#[derive(Debug)]
pub enum Auth {
Token(String),
ApiKey(String),
}
impl TryFrom<Auth> for HeaderMap {
type Error = InvalidHeaderValue;
fn try_from(auth: Auth) -> Result<Self, InvalidHeaderValue> {
let headers = match auth {
Auth::Token(token) => [(
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {token}"))?,
)],
Auth::ApiKey(api_key) => [(
HeaderName::from_str("api-key").expect("to be valid ASCII"),
HeaderValue::from_str(&api_key)?,
)],
}
.into_iter()
.collect();
Ok(headers)
}
}
pub struct OpenAiClient {
client: Client,
endpoint: String,
}
impl OpenAiClient {
pub fn new(auth: Auth, base_url: String, api_version: Option<String>) -> Result<Self, Error> {
let client = ClientBuilder::new()
.default_headers(auth.try_into()?)
.timeout(REQUEST_TIMEOUT)
.build()?;
let endpoint = build_url(base_url, api_version);
Ok(Self { client, endpoint })
}
pub fn new_with_client(client: Client, base_url: String, api_version: Option<String>) -> Self {
Self {
client,
endpoint: build_url(base_url, api_version),
}
}
pub async fn chat_completions(
&mut self,
body: ChatCompletionsBody,
) -> Result<ChatCompletions, Error> {
let response = self
.client
.post(self.endpoint.clone())
.json(&body)
.send()
.await?;
if response.status().is_success() {
Ok(response.json().await?)
} else {
let status = response.status();
let body = response
.text()
.await
.unwrap_or(String::from("<invalid UTF-8>"));
let description = serde_json::from_str::<ErrorBody>(&body)
.map(|e| e.error.message)
.unwrap_or(body);
Err(ApiError {
status,
description,
}
.into())
}
}
}
fn build_url(base_url: String, api_version: Option<String>) -> String {
if let Some(version) = api_version {
format!("{base_url}{CHAT_COMPLETIONS_ENDPOINT}?api-version={version}")
} else {
format!("{base_url}{CHAT_COMPLETIONS_ENDPOINT}")
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Non ASCII / non visible characters in API key")]
InvalidCharactersInApiKey(#[from] InvalidHeaderValue),
#[error("Request error: {0}")]
Request(reqwest::Error),
#[error("{0}")]
Api(#[from] ApiError),
}
impl From<reqwest::Error> for Error {
fn from(error: reqwest::Error) -> Self {
Self::Request(error.without_url())
}
}
#[derive(Debug, thiserror::Error)]
pub struct ApiError {
pub status: StatusCode,
pub description: String,
}
impl Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.status, self.description)
}
}
#[derive(Debug, Deserialize)]
pub struct ErrorBody {
error: OpenAiError,
}
#[derive(Debug, Deserialize)]
pub struct OpenAiError {
message: String,
}