greentic-designer 0.6.0

Greentic Designer — orchestrator that powers Adaptive Card design via the adaptive-card-mcp toolkit
Documentation
//! OpenAI Chat Completions provider.

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());
    }
}