#![no_std]
#![forbid(unsafe_code)]
use asimov_module::{
prelude::*,
secrecy::{ExposeSecret, SecretString},
tracing,
};
use core::error::Error;
use serde_json::{Value, json};
#[derive(Clone, Debug, bon::Builder)]
#[builder(on(String, into))]
pub struct Options {
#[builder(default = "https://api.openai.com")]
pub endpoint: String,
#[builder(default = "gpt-5-mini")]
pub model: String,
pub max_tokens: Option<usize>,
#[builder(into)]
pub api_key: SecretString,
}
pub fn generate(input: impl AsRef<str>, options: &Options) -> Result<Vec<String>, Box<dyn Error>> {
let mut req = json!({
"model": options.model,
"messages": [{
"role": "user",
"content": input.as_ref(),
}],
});
if let Some(max_tokens) = options.max_tokens {
req["max_output_tokens"] = max_tokens.into();
}
let mut resp = ureq::Agent::config_builder()
.http_status_as_error(false)
.user_agent("asimov-openai-module")
.build()
.new_agent()
.post(format!("{}/v1/chat/completions", options.endpoint))
.header(
"Authorization",
format!("Bearer {}", options.api_key.expose_secret()),
)
.header("content-type", "application/json")
.send_json(&req)
.inspect_err(|e| tracing::error!("HTTP request failed: {e}"))?;
tracing::debug!(response = ?resp);
let status = resp.status();
tracing::debug!(status = status.to_string());
let resp: Value = resp
.body_mut()
.read_json()
.inspect_err(|e| tracing::error!("unable to read HTTP response body: {e}"))?;
tracing::debug!(body = ?resp);
if !status.is_success() {
tracing::error!("Received an error response: {status}");
if let Some(message) = resp["error"]["message"].as_str() {
return Err(message.into());
}
}
let mut responses = Vec::new();
if let Some(choices) = resp["choices"].as_array() {
for choice in choices {
if choice["message"]["role"]
.as_str()
.is_none_or(|r| r != "assistant")
{
tracing::debug!("skipping output not from assistant: {choice}");
continue;
}
if let Some(content) = choice["message"]["content"].as_str() {
responses.push(content.to_string())
} else if let Some(refusal) = choice["message"]["refusal"].as_str() {
tracing::error!("Request refused: {refusal}")
}
if let Some(finish_reason) = choice["finish_reason"].as_str() {
tracing::debug!(finish_reason);
}
}
}
Ok(responses)
}