aither-openai 0.6.0

OpenAI provider integration for aither
Documentation
use crate::{
    client::{Config, OpenAI},
    error::OpenAIError,
};
use aither_core::{EmbeddingModel, Result as CoreResult};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use zenwave::{Client, client, header};

impl EmbeddingModel for OpenAI {
    fn dim(&self) -> usize {
        self.config().embedding_dimensions
    }

    fn embed(&self, text: &str) -> impl core::future::Future<Output = CoreResult<Vec<f32>>> + Send {
        let cfg = self.config();
        let input = text.to_owned();
        async move {
            let vector = embed_once(cfg, input).await?;
            Ok(vector)
        }
    }
}

async fn embed_once(cfg: Arc<Config>, input: String) -> Result<Vec<f32>, OpenAIError> {
    let endpoint = cfg.request_url("/embeddings");
    let mut backend = client();
    let mut builder = backend
        .post(endpoint)
        .map_err(OpenAIError::Http)?
        .header(header::AUTHORIZATION.as_str(), cfg.request_auth())
        .map_err(OpenAIError::Http)?
        .header(header::USER_AGENT.as_str(), "aither-openai/0.1")
        .map_err(OpenAIError::Http)?;
    if let Some(org) = &cfg.organization {
        builder = builder
            .header("OpenAI-Organization", org.clone())
            .map_err(OpenAIError::Http)?;
    }
    let request = EmbeddingRequest {
        model: &cfg.embedding_model,
        input: &input,
        dimensions: embedding_dimensions_for(&cfg.embedding_model, cfg.embedding_dimensions),
    };
    let response: EmbeddingResponse = builder
        .json_body(&request)
        .map_err(OpenAIError::Http)?
        .json()
        .await
        .map_err(OpenAIError::Http)?;
    response
        .data
        .into_iter()
        .next()
        .map(|item| item.embedding)
        .ok_or_else(|| OpenAIError::Api("embedding response missing vector data".into()))
}

#[derive(Debug, Serialize)]
struct EmbeddingRequest<'a> {
    model: &'a str,
    input: &'a str,
    #[serde(skip_serializing_if = "Option::is_none")]
    dimensions: Option<usize>,
}

#[derive(Debug, Deserialize)]
struct EmbeddingResponse {
    data: Vec<EmbeddingItem>,
}

#[derive(Debug, Deserialize)]
struct EmbeddingItem {
    embedding: Vec<f32>,
}

fn embedding_dimensions_for(model: &str, dimensions: usize) -> Option<usize> {
    match model {
        "text-embedding-3-large" | "text-embedding-3-small" => Some(dimensions),
        _ => None,
    }
}