use serde::Serialize;
use serde_json::Value;
use super::{ChatResponse, LlmProvider, ToolDef};
pub struct OpenAiProvider {
pub api_key: String,
pub model: String,
}
#[derive(Serialize)]
struct OpenAiRequest<'a> {
model: &'a str,
messages: Vec<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
tools: Option<&'a [ToolDef]>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_choice: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
response_format: Option<Value>,
}
impl LlmProvider for OpenAiProvider {
async fn chat_with_tools(
&self,
messages: Vec<Value>,
tools: &[ToolDef],
tool_choice: Option<&str>,
) -> anyhow::Result<ChatResponse> {
let request = OpenAiRequest {
model: &self.model,
messages,
tools: if tools.is_empty() { None } else { Some(tools) },
tool_choice,
response_format: None,
};
let client = reqwest::Client::new();
let response = client
.post("https://api.openai.com/v1/chat/completions")
.header("Authorization", format!("Bearer {}", self.api_key))
.header("Content-Type", "application/json")
.json(&request)
.send()
.await?;
if !response.status().is_success() {
let status = response.status();
let text = response.text().await.unwrap_or_default();
anyhow::bail!("OpenAI API error {status}: {text}");
}
let parsed: ChatResponse = response.json().await?;
Ok(parsed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_serializes_without_empty_tools() {
let req = OpenAiRequest {
model: "gpt-4o",
messages: vec![serde_json::json!({"role": "user", "content": "hi"})],
tools: None,
tool_choice: None,
response_format: None,
};
let v = serde_json::to_value(&req).unwrap();
assert!(v.get("tools").is_none());
assert!(v.get("tool_choice").is_none());
}
}