1use crate::io::api::{ApiResult, Configuration, Endpoint, Fallback, Param, Params, RemoteResource};
6use crate::param;
7use crate::prelude::var;
8use crate::util::Label;
9use bon::Builder;
10use color_eyre::eyre::eyre;
11use dotenvy;
12use serde::{Deserialize, Serialize};
13use tracing::debug;
14
15pub type ChatCompletionListResponse = serde_json::Value;
20pub type ChatCompletionResponse = serde_json::Value;
25pub type ChatCompletionMessagesResponse = serde_json::Value;
30pub type CreateResponseOutput = serde_json::Value;
35pub type DeleteResponseOutput = serde_json::Value;
40pub type ModelDeleteOutput = serde_json::Value;
45pub type ResponseInputItemsOutput = serde_json::Value;
50#[derive(Clone, Debug, Serialize, Deserialize)]
54pub struct Error {
55 pub code: Option<String>,
57 pub message: String,
59 pub param: Option<String>,
61 #[serde(rename = "type")]
63 pub error_type: String,
64}
65#[derive(Clone, Debug, Serialize, Deserialize)]
69pub struct ErrorResponse {
70 pub error: Error,
72}
73#[derive(Clone, Debug, Serialize, Deserialize)]
77pub struct ListModelsResponse {
78 pub object: String,
80 pub data: Vec<Model>,
82}
83#[derive(Clone, Debug, Serialize, Deserialize)]
87pub struct Model {
88 pub id: String,
90 pub object: String,
92 pub created: i64,
94 pub owned_by: String,
96}
97#[derive(Builder, Clone, Debug)]
101#[builder(start_fn = with_token, on(String, into))]
102pub struct Options {
103 #[builder(start_fn)]
105 pub token: String,
106 pub body: Option<String>,
108 #[builder(default = String::from("api.openai.com"))]
110 pub domain: String,
111 pub identifier: Option<String>,
113 pub after: Option<String>,
115 pub limit: Option<u32>,
117 pub order: Option<String>,
119 pub model: Option<String>,
121 #[builder(default = vec![])]
123 pub custom_params: Vec<Param>,
124}
125impl Configuration for Options {
126 fn from_env() -> Self {
130 if let Err(why) = dotenvy::from_filename(".env") {
131 debug!("=> {} Load .env — {why}", Label::skip());
132 }
133 Self {
134 token: var("OPENAI_API_KEY").unwrap_or_default(),
135 body: None,
136 domain: var("OPENAI_SERVER_HOST").unwrap_or_else(|_| String::from("api.openai.com")),
137 identifier: None,
138 after: None,
139 limit: None,
140 order: None,
141 model: None,
142 custom_params: vec![],
143 }
144 }
145 fn with_body(self, value: impl Into<String>) -> Self {
147 Self {
148 body: Some(value.into()),
149 ..self
150 }
151 }
152 fn with_domain(self, value: impl Into<String>) -> Self {
154 Self {
155 domain: value.into(),
156 ..self
157 }
158 }
159 fn with_identifier(self, value: impl Into<String>) -> Self {
161 Self {
162 identifier: Some(value.into()),
163 ..self
164 }
165 }
166 fn token(&self) -> &str {
168 &self.token
169 }
170 fn domain(&self) -> &str {
172 &self.domain
173 }
174 fn identifier(&self) -> Option<&str> {
176 self.identifier.as_deref()
177 }
178 fn with_params(self, params: Vec<Param>) -> Self {
180 Self {
181 custom_params: params,
182 ..self
183 }
184 }
185 fn params(&self) -> &[Param] {
187 &self.custom_params
188 }
189}
190impl Options {
191 pub fn with_after(self, value: impl Into<String>) -> Self {
193 Self {
194 after: Some(value.into()),
195 ..self
196 }
197 }
198 pub fn with_limit(self, value: u32) -> Self {
200 Self { limit: Some(value), ..self }
201 }
202 pub fn with_model(self, value: impl Into<String>) -> Self {
204 Self {
205 model: Some(value.into()),
206 ..self
207 }
208 }
209 pub fn with_order(self, value: impl Into<String>) -> Self {
211 Self {
212 order: Some(value.into()),
213 ..self
214 }
215 }
216}
217pub async fn chat_completion(options: &Options) -> ApiResult<ChatCompletionResponse> {
219 let template = "openai::api";
220 let action = "chat-completion";
221 match Endpoint::from_template(template).map(|e| e.with_domain(options.domain())) {
222 | Ok(endpoint) => match &options.body {
223 | Some(value) if !value.is_empty() => {
224 let params = Params::new()
225 .with_auth(options.token(), None)
226 .with(param!(Body, value.as_str()))
227 .with_custom(options.params())
228 .build();
229 let response = endpoint.invoke(action, Some(params)).await;
230 endpoint.handle_or::<ChatCompletionResponse, Fallback<ErrorResponse>>(response)
231 }
232 | Some(_) | None => Err(eyre!("OpenAI chat completion request body is required")),
233 },
234 | Err(why) => Err(why),
235 }
236}
237pub async fn chat_completion_list(options: &Options) -> ApiResult<ChatCompletionListResponse> {
239 let template = "openai::api";
240 let action = "chat-completion::list";
241 match Endpoint::from_template(template).map(|e| e.with_domain(options.domain())) {
242 | Ok(endpoint) => {
243 let params = Params::new()
244 .with_auth(options.token(), None)
245 .with_keyvalue("after", options.after.as_deref().filter(|v| !v.is_empty()))
246 .with_keyvalue("limit", options.limit.map(|v| v.to_string()).as_deref())
247 .with_keyvalue("order", options.order.as_deref().filter(|v| !v.is_empty()))
248 .with_keyvalue("model", options.model.as_deref().filter(|v| !v.is_empty()))
249 .with_custom(options.params())
250 .build();
251 let response = endpoint.invoke(action, Some(params)).await;
252 endpoint.handle_or::<ChatCompletionListResponse, Fallback<ErrorResponse>>(response)
253 }
254 | Err(why) => Err(why),
255 }
256}
257pub async fn chat_completion_delete(options: &Options) -> ApiResult<DeleteResponseOutput> {
259 let template = "openai::api";
260 let action = "chat-completion::delete";
261 match Endpoint::from_template(template).map(|e| e.with_domain(options.domain())) {
262 | Ok(endpoint) => match options.identifier() {
263 | Some(value) if !value.is_empty() => {
264 let params = Params::from_config(options).with_custom(options.params()).build();
265 let response = endpoint.invoke(action, Some(params)).await;
266 endpoint.handle_or::<DeleteResponseOutput, Fallback<ErrorResponse>>(response)
267 }
268 | Some(_) | None => Err(eyre!("OpenAI chat completion identifier is required")),
269 },
270 | Err(why) => Err(why),
271 }
272}
273pub async fn chat_completion_messages(options: &Options) -> ApiResult<ChatCompletionMessagesResponse> {
275 let template = "openai::api";
276 let action = "chat-completion::messages";
277 match Endpoint::from_template(template).map(|e| e.with_domain(options.domain())) {
278 | Ok(endpoint) => match &options.identifier {
279 | Some(value) if !value.is_empty() => {
280 let params = Params::new()
281 .with_auth(options.token(), None)
282 .with_template("identifier", Some(value))
283 .with_keyvalue("after", options.after.as_deref().filter(|v| !v.is_empty()))
284 .with_keyvalue("limit", options.limit.map(|v| v.to_string()).as_deref())
285 .with_keyvalue("order", options.order.as_deref().filter(|v| !v.is_empty()))
286 .with_custom(options.params())
287 .build();
288 let response = endpoint.invoke(action, Some(params)).await;
289 endpoint.handle_or::<ChatCompletionMessagesResponse, Fallback<ErrorResponse>>(response)
290 }
291 | Some(_) | None => Err(eyre!("OpenAI chat completion identifier is required")),
292 },
293 | Err(why) => Err(why),
294 }
295}
296pub async fn chat_completion_retrieve(options: &Options) -> ApiResult<ChatCompletionResponse> {
298 let template = "openai::api";
299 let action = "chat-completion::retrieve";
300 match Endpoint::from_template(template).map(|e| e.with_domain(options.domain())) {
301 | Ok(endpoint) => match options.identifier() {
302 | Some(value) if !value.is_empty() => {
303 let params = Params::from_config(options).with_custom(options.params()).build();
304 let response = endpoint.invoke(action, Some(params)).await;
305 endpoint.handle_or::<ChatCompletionResponse, Fallback<ErrorResponse>>(response)
306 }
307 | Some(_) | None => Err(eyre!("OpenAI chat completion identifier is required")),
308 },
309 | Err(why) => Err(why),
310 }
311}
312pub async fn chat_completion_update(options: &Options) -> ApiResult<ChatCompletionResponse> {
314 let template = "openai::api";
315 let action = "chat-completion::update";
316 match Endpoint::from_template(template).map(|e| e.with_domain(options.domain())) {
317 | Ok(endpoint) => match (&options.identifier, &options.body) {
318 | (Some(id), Some(value)) if !id.is_empty() && !value.is_empty() => {
319 let params = Params::new()
320 .with_auth(options.token(), None)
321 .with_template("identifier", Some(id))
322 .with(param!(Body, value.as_str()))
323 .with_custom(options.params())
324 .build();
325 let response = endpoint.invoke(action, Some(params)).await;
326 endpoint.handle_or::<ChatCompletionResponse, Fallback<ErrorResponse>>(response)
327 }
328 | (Some(_), Some(_)) => Err(eyre!("OpenAI chat completion identifier and body are required")),
329 | _ => Err(eyre!("OpenAI chat completion identifier and body are required")),
330 },
331 | Err(why) => Err(why),
332 }
333}
334pub async fn model(options: &Options) -> ApiResult<Model> {
336 let template = "openai::api";
337 let action = "model";
338 match Endpoint::from_template(template).map(|e| e.with_domain(options.domain())) {
339 | Ok(endpoint) => match options.identifier() {
340 | Some(value) if !value.is_empty() => {
341 let params = Params::from_config(options).with_custom(options.params()).build();
342 let response = endpoint.invoke(action, Some(params)).await;
343 endpoint.handle_or::<Model, Fallback<ErrorResponse>>(response)
344 }
345 | Some(_) | None => Err(eyre!("OpenAI model identifier is required")),
346 },
347 | Err(why) => Err(why),
348 }
349}
350pub async fn model_delete(options: &Options) -> ApiResult<ModelDeleteOutput> {
352 let template = "openai::api";
353 let action = "model::delete";
354 match Endpoint::from_template(template).map(|e| e.with_domain(options.domain())) {
355 | Ok(endpoint) => match options.identifier() {
356 | Some(value) if !value.is_empty() => {
357 let params = Params::from_config(options).with_custom(options.params()).build();
358 let response = endpoint.invoke(action, Some(params)).await;
359 endpoint.handle_or::<ModelDeleteOutput, Fallback<ErrorResponse>>(response)
360 }
361 | Some(_) | None => Err(eyre!("OpenAI model identifier is required")),
362 },
363 | Err(why) => Err(why),
364 }
365}
366pub async fn models(options: &Options) -> ApiResult<ListModelsResponse> {
368 let template = "openai::api";
369 let action = "models";
370 match Endpoint::from_template(template).map(|e| e.with_domain(options.domain())) {
371 | Ok(endpoint) => {
372 let params = Params::from_config(options).with_custom(options.params()).build();
373 let response = endpoint.invoke(action, Some(params)).await;
374 endpoint.handle_or::<ListModelsResponse, Fallback<ErrorResponse>>(response)
375 }
376 | Err(why) => Err(why),
377 }
378}
379pub async fn response(options: &Options) -> ApiResult<CreateResponseOutput> {
381 let template = "openai::api";
382 let action = "response";
383 match Endpoint::from_template(template).map(|e| e.with_domain(options.domain())) {
384 | Ok(endpoint) => match &options.body {
385 | Some(value) if !value.is_empty() => {
386 let params = Params::new()
387 .with_auth(options.token(), None)
388 .with(param!(Body, value.as_str()))
389 .with_custom(options.params())
390 .build();
391 let response = endpoint.invoke(action, Some(params)).await;
392 endpoint.handle_or::<CreateResponseOutput, Fallback<ErrorResponse>>(response)
393 }
394 | Some(_) | None => Err(eyre!("OpenAI responses request body is required")),
395 },
396 | Err(why) => Err(why),
397 }
398}
399pub async fn response_delete(options: &Options) -> ApiResult<DeleteResponseOutput> {
401 let template = "openai::api";
402 let action = "response::delete";
403 match Endpoint::from_template(template).map(|e| e.with_domain(options.domain())) {
404 | Ok(endpoint) => match options.identifier() {
405 | Some(value) if !value.is_empty() => {
406 let params = Params::from_config(options).with_custom(options.params()).build();
407 let response = endpoint.invoke(action, Some(params)).await;
408 endpoint.handle_or::<DeleteResponseOutput, Fallback<ErrorResponse>>(response)
409 }
410 | Some(_) | None => Err(eyre!("OpenAI response identifier is required")),
411 },
412 | Err(why) => Err(why),
413 }
414}
415pub async fn response_input_items(options: &Options) -> ApiResult<ResponseInputItemsOutput> {
417 let template = "openai::api";
418 let action = "response::input-items";
419 match Endpoint::from_template(template).map(|e| e.with_domain(options.domain())) {
420 | Ok(endpoint) => match &options.identifier {
421 | Some(value) if !value.is_empty() => {
422 let params = Params::new()
423 .with_auth(options.token(), None)
424 .with_template("identifier", Some(value))
425 .with_keyvalue("after", options.after.as_deref().filter(|v| !v.is_empty()))
426 .with_keyvalue("limit", options.limit.map(|v| v.to_string()).as_deref())
427 .with_keyvalue("order", options.order.as_deref().filter(|v| !v.is_empty()))
428 .with_custom(options.params())
429 .build();
430 let response = endpoint.invoke(action, Some(params)).await;
431 endpoint.handle_or::<ResponseInputItemsOutput, Fallback<ErrorResponse>>(response)
432 }
433 | Some(_) | None => Err(eyre!("OpenAI response identifier is required")),
434 },
435 | Err(why) => Err(why),
436 }
437}
438pub async fn response_cancel(options: &Options) -> ApiResult<CreateResponseOutput> {
440 let template = "openai::api";
441 let action = "response::cancel";
442 match Endpoint::from_template(template).map(|e| e.with_domain(options.domain())) {
443 | Ok(endpoint) => match options.identifier() {
444 | Some(value) if !value.is_empty() => {
445 let params = Params::from_config(options).with_custom(options.params()).build();
446 let response = endpoint.invoke(action, Some(params)).await;
447 endpoint.handle_or::<CreateResponseOutput, Fallback<ErrorResponse>>(response)
448 }
449 | Some(_) | None => Err(eyre!("OpenAI response identifier is required")),
450 },
451 | Err(why) => Err(why),
452 }
453}
454pub async fn response_retrieve(options: &Options) -> ApiResult<CreateResponseOutput> {
456 let template = "openai::api";
457 let action = "response::retrieve";
458 match Endpoint::from_template(template).map(|e| e.with_domain(options.domain())) {
459 | Ok(endpoint) => match options.identifier() {
460 | Some(value) if !value.is_empty() => {
461 let params = Params::from_config(options).with_custom(options.params()).build();
462 let response = endpoint.invoke(action, Some(params)).await;
463 endpoint.handle_or::<CreateResponseOutput, Fallback<ErrorResponse>>(response)
464 }
465 | Some(_) | None => Err(eyre!("OpenAI response identifier is required")),
466 },
467 | Err(why) => Err(why),
468 }
469}