use crate::chat_client::openai_api::chat_completions::ChatCompletions;
use eventsource_stream::{EventStream, Eventsource};
use futures::stream::Stream;
use reqwest::{
header::{HeaderMap, HeaderName, HeaderValue, InvalidHeaderValue, AUTHORIZATION},
Client, Method, Request, RequestBuilder, StatusCode,
};
use serde::Deserialize;
use serde_json::value::Value;
use std::{fmt::Display, str::FromStr, time::Duration};
use url::{ParseError, Url};
const CHAT_COMPLETIONS_ENDPOINT: &str = "chat/completions";
#[derive(Debug, Clone)]
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 OpenAiClientConfig {
pub client: Client,
pub auth: Auth,
pub base_url: String,
pub api_version: Option<String>,
pub timeout: Duration,
}
pub struct OpenAiClient {
client: Client,
endpoint: Url,
headers: HeaderMap,
timeout: Duration,
}
impl OpenAiClient {
pub fn new(
OpenAiClientConfig {
client,
auth,
base_url,
api_version,
timeout,
}: OpenAiClientConfig,
) -> Result<Self, Error> {
Ok(Self {
client,
endpoint: Url::parse(&build_url(base_url, api_version))?,
headers: auth.try_into()?,
timeout,
})
}
pub async fn chat_completions(&mut self, body: Value) -> Result<ChatCompletions, Error> {
let response = self.build_request(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())
}
}
pub async fn chat_completions_stream(
&mut self,
body: Value,
) -> Result<EventStream<impl Stream<Item = Result<bytes::Bytes, reqwest::Error>>>, Error> {
Ok(self
.build_request(body)
.send()
.await?
.bytes_stream()
.eventsource())
}
fn build_request(&mut self, body: Value) -> RequestBuilder {
RequestBuilder::from_parts(
self.client.clone(),
Request::new(Method::POST, self.endpoint.clone()),
)
.headers(self.headers.clone())
.json(&body)
.timeout(self.timeout)
}
}
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),
#[error("Invalid URL: {0}")]
InvalidUrl(#[from] ParseError),
}
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,
}