open_routerer/
client.rs

1use std::marker::PhantomData;
2
3use reqwest::Client as ReqwestClient;
4use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue};
5use url::Url;
6
7use crate::api::chat::ChatApi;
8use crate::error::{Error, Result};
9
10#[derive(Debug, Clone)]
11pub struct ClientConfig {
12    pub(crate) api_key: Option<String>,
13    pub(crate) base_url: Url,
14}
15
16impl ClientConfig {
17    pub fn build_headers(&self) -> Result<HeaderMap> {
18        let mut headers = HeaderMap::new();
19        if let Some(ref key) = self.api_key {
20            let auth_header = HeaderValue::from_str(&format!("Bearer {}", key))
21                .map_err(|e| Error::ConfigError(format!("Invalid API key header format: {}", e)))?;
22            headers.insert(AUTHORIZATION, auth_header);
23        }
24
25        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
26
27        Ok(headers)
28    }
29}
30
31#[derive(Clone)]
32pub struct Unconfigured;
33#[derive(Clone)]
34pub struct Ready;
35
36#[derive(Clone)]
37pub struct Client<State = Unconfigured> {
38    pub(crate) config: ClientConfig,
39    pub(crate) http_client: Option<ReqwestClient>,
40    pub(crate) _state: PhantomData<State>,
41}
42
43impl Default for Client<Unconfigured> {
44    fn default() -> Self {
45        Self {
46            config: ClientConfig {
47                api_key: None,
48                base_url: "https://openrouter.ai/api/v1/".parse().unwrap(),
49            },
50            http_client: None,
51            _state: PhantomData,
52        }
53    }
54}
55
56impl Client<Unconfigured> {
57    pub fn new() -> Self {
58        Self::default()
59    }
60
61    pub fn with_api_key(mut self, api_key: impl Into<String>) -> Result<Client<Ready>> {
62        self.config.api_key = Some(api_key.into());
63        self.transition_to_ready()
64    }
65
66    fn transition_to_ready(self) -> Result<Client<Ready>> {
67        let headers = self.config.build_headers()?;
68        let http_client = reqwest::Client::builder()
69            .default_headers(headers)
70            .build()
71            .map_err(|e| Error::ConfigError(format!("Failed to create HTTP client: {}", e)))?;
72        Ok(Client {
73            config: self.config,
74            http_client: Some(http_client),
75            _state: PhantomData,
76        })
77    }
78}
79
80impl Client<Ready> {
81    pub fn chat(&self) -> Result<ChatApi> {
82        let client = self
83            .http_client
84            .clone()
85            .ok_or_else(|| Error::ConfigError("HTTP client is missing".into()))?;
86        Ok(ChatApi::new(client, &self.config))
87    }
88
89    pub async fn handle_response<T>(response: reqwest::Response) -> Result<T>
90    where
91        T: serde::de::DeserializeOwned,
92    {
93        let status = response.status(); // ✅ Extract status BEFORE consuming response
94        let body = response.text().await?; // ✅ Now consume the response
95        if !status.is_success() {
96            return Err(Error::ApiError {
97                code: status.as_u16(),
98                message: body.clone(),
99                metadata: None,
100            });
101        }
102        if body.trim().is_empty() {
103            return Err(Error::ApiError {
104                code: status.as_u16(),
105                message: "Empty response body".into(),
106                metadata: None,
107            });
108        }
109        serde_jsonc2::from_str::<T>(&body).map_err(|e| Error::ApiError {
110            code: status.as_u16(),
111            message: format!("Failed to decode JSON: {}. Body was: {}", e, body),
112            metadata: None,
113        })
114    }
115}