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(); let body = response.text().await?; 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}