Skip to main content

chat_responses/
lib.rs

1//! Generic OpenAI Responses API client.
2//!
3//! Targets the `/responses` endpoint and its SSE event stream
4//! (`response.created`, `response.output_text.delta`, etc.). This crate
5//! owns the wire types — provider crates that ship a Responses-API
6//! surface (today: `chat-openai`; planned: `chat-groq`'s Responses
7//! path) wrap [`ResponsesBuilder`] and preset URL + auth + any
8//! provider-specific tool declarations.
9//!
10//! Provider-specific native tools (OpenAI's `web_search`,
11//! `image_generation`, etc.) are passed in as **pre-materialized
12//! `Value`s** via [`ResponsesBuilder::with_tool_declaration`]. The
13//! wrapper owns the tool trait; this crate stays trait-agnostic.
14//!
15//! ```no_run
16//! use chat_responses::ResponsesBuilder;
17//!
18//! let client = ResponsesBuilder::new()
19//!     .with_base_url("https://api.openai.com/v1")
20//!     .with_model("gpt-4o")
21//!     .with_api_key("sk-...")
22//!     .build();
23//! ```
24
25mod api;
26mod client;
27
28use std::marker::PhantomData;
29
30use chat_core::types::provider_meta::ProviderMeta;
31
32pub use crate::api::types::error::{
33    ResponsesErrorDetail, ResponsesErrorResponse, handle_responses_error,
34};
35pub use crate::api::types::request::{ReasoningConfig, ResponsesRequest, ResponsesRequestConfig};
36pub use crate::api::types::response::{
37    ResponsesApiResponse, ResponsesContentPart, ResponsesFunctionCall,
38    ResponsesImageGenerationCall, ResponsesMessage, ResponsesOutputItem, ResponsesReasoning,
39    ResponsesSummaryPart, ResponsesUsage, ResponsesWebSearchCall, output_items_to_parts,
40};
41pub use crate::client::ResponsesClient;
42pub use chat_core::error::{ChatError, ChatFailure};
43pub use chat_core::transport::{Request, ReqwestTransport, Response, Transport, TransportError};
44
45use serde_json::Value;
46
47pub struct WithoutModel;
48pub struct WithModel;
49
50pub struct WithoutUrl;
51pub struct WithUrl;
52
53pub struct ResponsesBuilder<M = WithoutModel, U = WithoutUrl, T: Transport = ReqwestTransport> {
54    model_name: Option<String>,
55    api_key: Option<String>,
56    scheme: String,
57    host: String,
58    base_path: String,
59    extra_tool_declarations: Vec<Value>,
60    reasoning_effort: Option<String>,
61    use_previous_response_id: bool,
62    store: Option<bool>,
63    transport: Option<T>,
64    meta: ProviderMeta,
65    _m: PhantomData<M>,
66    _u: PhantomData<U>,
67}
68
69impl Default for ResponsesBuilder<WithoutModel, WithoutUrl, ReqwestTransport> {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75impl ResponsesBuilder<WithoutModel, WithoutUrl, ReqwestTransport> {
76    pub fn new() -> Self {
77        Self {
78            model_name: None,
79            api_key: None,
80            scheme: String::new(),
81            host: String::new(),
82            base_path: String::new(),
83            extra_tool_declarations: Vec::new(),
84            reasoning_effort: None,
85            use_previous_response_id: true,
86            store: None,
87            transport: Some(ReqwestTransport::default()),
88            meta: ProviderMeta::default(),
89            _m: PhantomData,
90            _u: PhantomData,
91        }
92    }
93}
94
95impl<M, U, T: Transport> ResponsesBuilder<M, U, T> {
96    pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
97        self.api_key = Some(api_key.into());
98        self
99    }
100
101    pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
102        self.reasoning_effort = Some(effort.into());
103        self
104    }
105
106    pub fn without_previous_response_id(mut self) -> Self {
107        self.use_previous_response_id = false;
108        self
109    }
110
111    pub fn with_store(mut self, store: bool) -> Self {
112        self.store = Some(store);
113        self
114    }
115
116    pub fn with_description(mut self, description: impl Into<String>) -> Self {
117        self.meta.description = Some(description.into());
118        self
119    }
120
121    pub fn with_metadata(
122        mut self,
123        key: impl Into<String>,
124        value: impl std::any::Any + Send + Sync + 'static,
125    ) -> Self {
126        self.meta.data.insert(key.into(), Box::new(value));
127        self
128    }
129
130    /// Replace the whole `ProviderMeta`. Useful for wrappers that
131    /// accumulate metadata on their own builder and want to pass it
132    /// through wholesale at construction time.
133    pub fn with_meta(mut self, meta: ProviderMeta) -> Self {
134        self.meta = meta;
135        self
136    }
137
138    /// Append a single pre-materialized tool declaration. Wrappers use
139    /// this to inject their provider-specific native tools without
140    /// coupling the wire layer to a NativeTool trait.
141    pub fn with_tool_declaration(mut self, declaration: Value) -> Self {
142        self.extra_tool_declarations.push(declaration);
143        self
144    }
145
146    pub fn with_tool_declarations(mut self, declarations: impl IntoIterator<Item = Value>) -> Self {
147        self.extra_tool_declarations.extend(declarations);
148        self
149    }
150
151    pub fn with_transport<T2: Transport>(self, transport: T2) -> ResponsesBuilder<M, U, T2> {
152        ResponsesBuilder {
153            model_name: self.model_name,
154            api_key: self.api_key,
155            scheme: self.scheme,
156            host: self.host,
157            base_path: self.base_path,
158            extra_tool_declarations: self.extra_tool_declarations,
159            reasoning_effort: self.reasoning_effort,
160            use_previous_response_id: self.use_previous_response_id,
161            store: self.store,
162            transport: Some(transport),
163            meta: self.meta,
164            _m: PhantomData,
165            _u: PhantomData,
166        }
167    }
168}
169
170impl<U, T: Transport> ResponsesBuilder<WithoutModel, U, T> {
171    pub fn with_model(self, model: impl Into<String>) -> ResponsesBuilder<WithModel, U, T> {
172        ResponsesBuilder {
173            model_name: Some(model.into()),
174            api_key: self.api_key,
175            scheme: self.scheme,
176            host: self.host,
177            base_path: self.base_path,
178            extra_tool_declarations: self.extra_tool_declarations,
179            reasoning_effort: self.reasoning_effort,
180            use_previous_response_id: self.use_previous_response_id,
181            store: self.store,
182            transport: self.transport,
183            meta: self.meta,
184            _m: PhantomData,
185            _u: PhantomData,
186        }
187    }
188}
189
190impl<M, T: Transport> ResponsesBuilder<M, WithoutUrl, T> {
191    pub fn with_base_url(self, url: impl Into<String>) -> ResponsesBuilder<M, WithUrl, T> {
192        let url = url.into();
193        let parsed = url::Url::parse(&url).expect("Invalid base URL");
194        let scheme = parsed.scheme().to_string();
195        let host = parsed.host_str().expect("No host in URL").to_string()
196            + &parsed.port().map(|p| format!(":{p}")).unwrap_or_default();
197        let base_path = parsed.path().trim_end_matches('/').to_string();
198        ResponsesBuilder {
199            model_name: self.model_name,
200            api_key: self.api_key,
201            scheme,
202            host,
203            base_path,
204            extra_tool_declarations: self.extra_tool_declarations,
205            reasoning_effort: self.reasoning_effort,
206            use_previous_response_id: self.use_previous_response_id,
207            store: self.store,
208            transport: self.transport,
209            meta: self.meta,
210            _m: PhantomData,
211            _u: PhantomData,
212        }
213    }
214}
215
216impl<T: Transport> ResponsesBuilder<WithModel, WithUrl, T> {
217    pub fn build(self) -> ResponsesClient<T> {
218        let api_key = self
219            .api_key
220            .expect("No API key. Call .with_api_key() before .build().");
221
222        let transport = self.transport.expect("transport set");
223        let model = self.model_name.expect("model set");
224
225        ResponsesClient {
226            model_name: model,
227            api_key,
228            scheme: self.scheme,
229            host: self.host,
230            base_path: self.base_path,
231            transport,
232            extra_tool_declarations: self.extra_tool_declarations,
233            reasoning_effort: self.reasoning_effort,
234            use_previous_response_id: self.use_previous_response_id,
235            last_response_id: None,
236            store: self.store,
237            meta: self.meta,
238        }
239    }
240}