openresponses_rust/
client.rs1use reqwest::{Client as ReqwestClient, header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue}};
2use serde_json;
3use thiserror::Error;
4
5use crate::types::{CreateResponseBody, ResponseResource};
6
7const DEFAULT_BASE_URL: &str = "https://api.openai.com";
8
9#[derive(Error, Debug)]
10pub enum ClientError {
11 #[error("HTTP request failed: {0}")]
12 HttpError(#[from] reqwest::Error),
13
14 #[error("JSON parsing error: {0}")]
15 JsonError(#[from] serde_json::Error),
16
17 #[error("API error: {code} - {message}")]
18 ApiError { code: String, message: String },
19
20 #[error("Invalid header value: {0}")]
21 InvalidHeader(String),
22}
23
24pub struct ClientBuilder {
25 api_key: String,
26 base_url: Option<String>,
27}
28
29impl ClientBuilder {
30 pub fn new(api_key: impl Into<String>) -> Self {
31 Self {
32 api_key: api_key.into(),
33 base_url: None,
34 }
35 }
36
37 pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
38 self.base_url = Some(base_url.into());
39 self
40 }
41
42 pub fn build(self) -> Client {
43 let mut base_url = self.base_url.unwrap_or_else(|| DEFAULT_BASE_URL.to_string());
44
45 if base_url.ends_with('/') {
47 base_url.pop();
48 }
49
50 if !base_url.ends_with("/v1") {
52 base_url.push_str("/v1");
53 }
54
55 let mut headers = HeaderMap::new();
56 headers.insert(
57 CONTENT_TYPE,
58 HeaderValue::from_static("application/json"),
59 );
60
61 let inner = ReqwestClient::builder()
62 .default_headers(headers)
63 .build()
64 .expect("Failed to create HTTP client");
65
66 Client {
67 inner,
68 base_url,
69 api_key: self.api_key,
70 }
71 }
72}
73
74#[derive(Clone)]
75pub struct Client {
76 inner: ReqwestClient,
77 base_url: String,
78 api_key: String,
79}
80
81impl Client {
82 pub fn new(api_key: impl Into<String>) -> Self {
83 ClientBuilder::new(api_key).build()
84 }
85
86 pub fn builder(api_key: impl Into<String>) -> ClientBuilder {
87 ClientBuilder::new(api_key)
88 }
89
90 pub fn with_base_url(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
91 ClientBuilder::new(api_key).base_url(base_url).build()
92 }
93
94 pub async fn create_response(&self, request: CreateResponseBody) -> Result<ResponseResource, ClientError> {
95 let url = format!("{}/responses", self.base_url);
96
97 let response = self.inner
98 .post(&url)
99 .header(AUTHORIZATION, format!("Bearer {}", self.api_key))
100 .json(&request)
101 .send()
102 .await?;
103
104 let status = response.status();
105
106 if !status.is_success() {
107 let error_text = response.text().await?;
108 return Err(ClientError::ApiError {
109 code: status.to_string(),
110 message: error_text,
111 });
112 }
113
114 let response_body = response.json::<ResponseResource>().await?;
115 Ok(response_body)
116 }
117
118 pub async fn create_response_raw(&self, request: CreateResponseBody) -> Result<String, ClientError> {
119 let url = format!("{}/responses", self.base_url);
120
121 let response = self.inner
122 .post(&url)
123 .header(AUTHORIZATION, format!("Bearer {}", self.api_key))
124 .json(&request)
125 .send()
126 .await?;
127
128 let status = response.status();
129 let body = response.text().await?;
130
131 if !status.is_success() {
132 return Err(ClientError::ApiError {
133 code: status.to_string(),
134 message: body,
135 });
136 }
137
138 Ok(body)
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::types::{Input, Item};
146
147 #[test]
148 fn test_client_creation() {
149 let client = Client::new("test-api-key");
150 assert_eq!(client.api_key, "test-api-key");
151 assert_eq!(client.base_url, "https://api.openai.com/v1");
152 }
153
154 #[test]
155 fn test_client_with_base_url_normalization() {
156 let client = Client::with_base_url("test-key", "https://openrouter.ai/api");
158 assert_eq!(client.base_url, "https://openrouter.ai/api/v1");
159
160 let client = Client::with_base_url("test-key", "https://openrouter.ai/api/v1");
162 assert_eq!(client.base_url, "https://openrouter.ai/api/v1");
163
164 let client = Client::with_base_url("test-key", "http://localhost:1234");
166 assert_eq!(client.base_url, "http://localhost:1234/v1");
167 }
168
169 #[tokio::test]
170 async fn test_request_serialization() {
171 let request = CreateResponseBody {
172 model: Some("gpt-4o".to_string()),
173 input: Some(Input::Items(vec![
174 Item::user_message("Hello, world!")
175 ])),
176 ..Default::default()
177 };
178
179 let json = serde_json::to_string(&request).unwrap();
180 assert!(json.contains("gpt-4o"));
181 assert!(json.contains("Hello, world!"));
182 }
183}