Skip to main content

ai_runtime_adapters/
lib.rs

1#![warn(missing_docs)]
2
3//! AI Runtime Adapters — adapter implementations for AI backend protocols.
4//!
5//! Currently provides:
6//! - [`GenericOpenAIAdapter`] — OpenAI Chat Completions API adapter with
7//!   timeout, retries, and configurable auth headers
8//!
9//! ## Example
10//!
11//! ```ignore
12//! use ai_runtime_adapters::GenericOpenAIAdapter;
13//! let adapter = GenericOpenAIAdapter::new_with_auth(
14//!     api_key, endpoint, model,
15//!     "Authorization", "Bearer",
16//! )?;
17//! let (content, _) = adapter.ask_and_collect(request).await?;
18//! ```
19
20use std::time::Duration;
21
22use anyhow::Context;
23use async_trait::async_trait;
24use dracon_ai_runtime_contracts::models::ChatRequest;
25use dracon_ai_runtime_contracts::traits::AiProvider;
26
27/// Adapter for OpenAI-compatible Chat Completions API endpoints.
28pub struct GenericOpenAIAdapter {
29    api_key: String,
30    endpoint: String,
31    model: String,
32    auth_header_name: String,
33    auth_header_prefix: String,
34    client: reqwest::Client,
35}
36
37impl GenericOpenAIAdapter {
38    /// Create a new adapter with the given auth header configuration.
39    pub fn new_with_auth(
40        api_key: String,
41        endpoint: String,
42        model: String,
43        auth_header_name: String,
44        auth_header_prefix: String,
45    ) -> anyhow::Result<Self> {
46        let client = reqwest::Client::builder()
47            .timeout(Duration::from_secs(60))
48            .connect_timeout(Duration::from_secs(10))
49            .build()
50            .context("reqwest client should build")?;
51        Ok(Self {
52            api_key,
53            endpoint,
54            model,
55            auth_header_name,
56            auth_header_prefix,
57            client,
58        })
59    }
60}
61
62#[async_trait]
63impl AiProvider for GenericOpenAIAdapter {
64    async fn ask_and_collect(
65        &self,
66        request: ChatRequest,
67    ) -> anyhow::Result<(String, Option<String>)> {
68        let messages: Vec<serde_json::Value> = request
69            .messages
70            .iter()
71            .map(|m| {
72                serde_json::json!({
73                    "role": m.role,
74                    "content": m.content,
75                })
76            })
77            .collect();
78
79        let body = serde_json::json!({
80            "model": self.model,
81            "messages": messages,
82            "max_tokens": request.max_tokens.unwrap_or(200),
83            "temperature": request.temperature.unwrap_or(0.7),
84        });
85
86        let resp = self
87            .client
88            .post(&format!("{}/chat/completions", self.endpoint))
89            .header(
90                &self.auth_header_name,
91                format!("{}{}", self.auth_header_prefix, self.api_key),
92            )
93            .header("Content-Type", "application/json")
94            .json(&body)
95            .send()
96            .await?
97            .error_for_status()?;
98
99        let json: serde_json::Value = resp.json().await?;
100        let content = json["choices"][0]["message"]["content"]
101            .as_str()
102            .unwrap_or("")
103            .to_string();
104        let finish_reason = json["choices"][0]["finish_reason"]
105            .as_str()
106            .map(|s| s.to_string());
107
108        Ok((content, finish_reason))
109    }
110}