qai-sdk 0.1.1

Universal Rust SDK for AI Providers
Documentation
use crate::core::types::{CompletionOptions, CompletionResult, Usage};
use anyhow::anyhow;
use async_trait::async_trait;
use reqwest::Client;
use serde::{Deserialize, Serialize};

/// `OpenAI` completion model (legacy completions API).
pub struct OpenAICompletionModel {
    pub api_key: String,
    pub base_url: String,
    pub client: Client,
}

impl OpenAICompletionModel {
    #[must_use]
    pub fn new(api_key: String) -> Self {
        Self {
            api_key,
            base_url: "https://api.openai.com/v1".to_string(),
            client: Client::new(),
        }
    }
}

#[derive(Serialize)]
struct OpenAICompletionRequest {
    model: String,
    prompt: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    max_tokens: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    temperature: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    top_p: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    stop: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    suffix: Option<String>,
}

#[derive(Deserialize)]
struct OpenAICompletionResponse {
    choices: Vec<OpenAICompletionChoice>,
    usage: OpenAICompletionUsage,
}

#[derive(Deserialize)]
struct OpenAICompletionChoice {
    text: String,
    finish_reason: Option<String>,
}

#[derive(Deserialize)]
struct OpenAICompletionUsage {
    prompt_tokens: u32,
    completion_tokens: u32,
}

#[async_trait]
impl crate::core::CompletionModel for OpenAICompletionModel {
    async fn complete(&self, options: CompletionOptions) -> crate::core::Result<CompletionResult> {
        let request = OpenAICompletionRequest {
            model: options.model_id,
            prompt: options.prompt,
            max_tokens: options.max_tokens,
            temperature: options.temperature,
            top_p: options.top_p,
            stop: options.stop,
            suffix: options.suffix,
        };

        let resp = self
            .client
            .post(format!("{}/completions", self.base_url))
            .header("Authorization", &format!("Bearer {}", self.api_key))
            .json(&request)
            .send()
            .await?;

        if !resp.status().is_success() {
            let error_text = resp.text().await?;
            return Err(anyhow!("OpenAI Completion API error: {error_text}").into());
        }

        let completion_resp: OpenAICompletionResponse = resp.json().await?;
        let choice =
            completion_resp
                .choices
                .first()
                .ok_or_else(|| -> crate::core::ProviderError {
                    crate::core::ProviderError::Other(anyhow::anyhow!(
                        "No completion choices returned"
                    ))
                })?;

        Ok(CompletionResult {
            text: choice.text.clone(),
            usage: Usage {
                prompt_tokens: completion_resp.usage.prompt_tokens,
                completion_tokens: completion_resp.usage.completion_tokens,
            },
            finish_reason: choice
                .finish_reason
                .clone()
                .unwrap_or_else(|| "stop".to_string()),
        })
    }
}