use crate::io::api::{ApiResult, Configuration, Endpoint, Fallback, Param, Params, RemoteResource, TextResponse};
use crate::param;
use crate::prelude::var;
use crate::util::Label;
use bon::Builder;
use color_eyre::eyre::eyre;
use dotenvy;
use serde::{Deserialize, Serialize};
use tracing::debug;
pub type Response = serde_json::Value;
pub type AudioSpeechResponse = TextResponse;
enum BodyMode {
Empty,
Required,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Error {
pub code: Option<String>,
pub message: String,
pub param: Option<String>,
#[serde(rename = "type")]
pub error_type: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ErrorResponse {
pub error: Error,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ListModelsResponse {
pub object: String,
pub data: Vec<Model>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Model {
pub id: String,
pub object: String,
pub created: i64,
pub owned_by: String,
}
#[derive(Builder, Clone, Debug)]
#[builder(start_fn = with_token, on(String, into))]
pub struct Options {
#[builder(start_fn)]
pub token: String,
pub body: Option<String>,
#[builder(default = String::from("api.openai.com"))]
pub domain: String,
pub identifier: Option<String>,
#[builder(default = vec![])]
pub custom_params: Vec<Param>,
}
impl Configuration for Options {
fn from_env() -> Self {
if let Err(why) = dotenvy::from_filename(".env") {
debug!("=> {} Load .env — {why}", Label::skip());
}
Self {
token: var("OPENAI_API_KEY").unwrap_or_default(),
body: None,
domain: var("OPENAI_SERVER_HOST").unwrap_or_else(|_| String::from("api.openai.com")),
identifier: None,
custom_params: vec![],
}
}
fn with_body(self, value: impl Into<String>) -> Self {
Self {
body: Some(value.into()),
..self
}
}
fn with_domain(self, value: impl Into<String>) -> Self {
Self {
domain: value.into(),
..self
}
}
fn with_identifier(self, value: impl Into<String>) -> Self {
Self {
identifier: Some(value.into()),
..self
}
}
fn token(&self) -> &str {
&self.token
}
fn domain(&self) -> &str {
&self.domain
}
fn identifier(&self) -> Option<&str> {
self.identifier.as_deref()
}
fn with_params(self, params: Vec<Param>) -> Self {
Self {
custom_params: params,
..self
}
}
fn params(&self) -> &[Param] {
&self.custom_params
}
}
pub async fn audio_speech(options: &Options) -> ApiResult<AudioSpeechResponse> {
invoke(options, "audio-speech", BodyMode::Required).await
}
pub async fn audio_transcription(options: &Options) -> ApiResult<Response> {
invoke(options, "audio-transcription", BodyMode::Required).await
}
pub async fn audio_voices(options: &Options) -> ApiResult<Response> {
invoke(options, "audio-voices", BodyMode::Empty).await
}
pub async fn chat_completion(options: &Options) -> ApiResult<Response> {
invoke(options, "chat-completion", BodyMode::Required).await
}
pub async fn completion(options: &Options) -> ApiResult<Response> {
invoke(options, "completion", BodyMode::Required).await
}
pub async fn embedding(options: &Options) -> ApiResult<Response> {
invoke(options, "embedding", BodyMode::Required).await
}
pub async fn image_edit(options: &Options) -> ApiResult<Response> {
invoke(options, "image-edit", BodyMode::Required).await
}
pub async fn image_generation(options: &Options) -> ApiResult<Response> {
invoke(options, "image-generation", BodyMode::Required).await
}
pub async fn models(options: &Options) -> ApiResult<ListModelsResponse> {
invoke(options, "models", BodyMode::Empty).await
}
pub async fn response(options: &Options) -> ApiResult<Response> {
invoke(options, "response", BodyMode::Required).await
}
async fn invoke<R>(options: &Options, action: &str, body_mode: BodyMode) -> ApiResult<R>
where
R: for<'de> Deserialize<'de>,
{
let template = "openai::api";
match Endpoint::from_template(template).map(|e| e.with_domain(options.domain())) {
| Ok(endpoint) => match (body_mode, &options.body) {
| (BodyMode::Required, Some(value)) if !value.is_empty() => {
let params = Params::new()
.with_auth(options.token(), None)
.with(param!(Body, value.as_str()))
.with_custom(options.params())
.build();
let response = endpoint.invoke(action, Some(params)).await;
endpoint.handle_or::<R, Fallback<ErrorResponse>>(response)
}
| (BodyMode::Required, _) => Err(eyre!(format!("OpenAI {action} request body is required"))),
| (BodyMode::Empty, _) => {
let params = Params::from_config(options).with_custom(options.params()).build();
let response = endpoint.invoke(action, Some(params)).await;
endpoint.handle_or::<R, Fallback<ErrorResponse>>(response)
}
},
| Err(why) => Err(why),
}
}