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/v1";
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
24#[derive(Clone)]
25pub struct Client {
26 inner: ReqwestClient,
27 base_url: String,
28 api_key: String,
29}
30
31impl Client {
32 pub fn new(api_key: impl Into<String>) -> Self {
33 Self::with_base_url(api_key, DEFAULT_BASE_URL)
34 }
35
36 pub fn with_base_url(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
37 let api_key = api_key.into();
38 let base_url = base_url.into();
39
40 let mut headers = HeaderMap::new();
41 headers.insert(
42 CONTENT_TYPE,
43 HeaderValue::from_static("application/json"),
44 );
45
46 let inner = ReqwestClient::builder()
47 .default_headers(headers)
48 .build()
49 .expect("Failed to create HTTP client");
50
51 Self { inner, base_url, api_key }
52 }
53
54 pub async fn create_response(&self, request: CreateResponseBody) -> Result<ResponseResource, ClientError> {
55 let url = format!("{}/responses", self.base_url);
56
57 let response = self.inner
58 .post(&url)
59 .header(AUTHORIZATION, format!("Bearer {}", self.api_key))
60 .json(&request)
61 .send()
62 .await?;
63
64 let status = response.status();
65
66 if !status.is_success() {
67 let error_text = response.text().await?;
68 return Err(ClientError::ApiError {
69 code: status.to_string(),
70 message: error_text,
71 });
72 }
73
74 let response_body = response.json::<ResponseResource>().await?;
75 Ok(response_body)
76 }
77
78 pub async fn create_response_raw(&self, request: CreateResponseBody) -> Result<String, ClientError> {
79 let url = format!("{}/responses", self.base_url);
80
81 let response = self.inner
82 .post(&url)
83 .header(AUTHORIZATION, format!("Bearer {}", self.api_key))
84 .json(&request)
85 .send()
86 .await?;
87
88 let status = response.status();
89 let body = response.text().await?;
90
91 if !status.is_success() {
92 return Err(ClientError::ApiError {
93 code: status.to_string(),
94 message: body,
95 });
96 }
97
98 Ok(body)
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::types::{Input, Item};
106
107 #[test]
108 fn test_client_creation() {
109 let client = Client::new("test-api-key");
110 assert_eq!(client.api_key, "test-api-key");
111 assert_eq!(client.base_url, DEFAULT_BASE_URL);
112 }
113
114 #[test]
115 fn test_client_with_base_url() {
116 let client = Client::with_base_url("test-key", "https://custom.api.com");
117 assert_eq!(client.api_key, "test-key");
118 assert_eq!(client.base_url, "https://custom.api.com");
119 }
120
121 #[tokio::test]
122 async fn test_request_serialization() {
123 let request = CreateResponseBody {
124 model: Some("gpt-4o".to_string()),
125 input: Some(Input::Items(vec![
126 Item::user_message("Hello, world!")
127 ])),
128 ..Default::default()
129 };
130
131 let json = serde_json::to_string(&request).unwrap();
132 assert!(json.contains("gpt-4o"));
133 assert!(json.contains("Hello, world!"));
134 }
135}