use async_trait::async_trait; use crate::ai::provider::Provider; use crate::ai::{ChatResponse, Message, ProviderInfo, TokenUsage}; use crate::state::ProviderConfig; pub struct OpenAiProvider; #[async_trait] impl Provider for OpenAiProvider { fn name(&self) -> &str { "openai" } fn info(&self) -> ProviderInfo { ProviderInfo { name: "OpenAI Compatible".to_string(), supports_streaming: true, models: vec![ "gpt-4o".to_string(), "gpt-4o-mini".to_string(), "gpt-4-turbo".to_string(), ], } } async fn chat( &self, messages: Vec, config: &ProviderConfig, ) -> anyhow::Result { let client = reqwest::Client::new(); let url = format!("{}/chat/completions", config.api_url.trim_end_matches('/')); let body = serde_json::json!({ "model": config.model, "messages": messages, "max_tokens": 4096, }); let resp = client .post(&url) .header("Authorization", format!("Bearer {}", config.api_key)) .header("Content-Type", "application/json") .json(&body) .send() .await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await?; anyhow::bail!("OpenAI API error {status}: {text}"); } let json: serde_json::Value = resp.json().await?; let content = json["choices"][0]["message"]["content"] .as_str() .ok_or_else(|| anyhow::anyhow!("No content in response"))? .to_string(); let usage = json.get("usage").and_then(|u| { Some(TokenUsage { prompt_tokens: u["prompt_tokens"].as_u64()? as u32, completion_tokens: u["completion_tokens"].as_u64()? as u32, total_tokens: u["total_tokens"].as_u64()? as u32, }) }); Ok(ChatResponse { content, model: config.model.clone(), usage, }) } }