Skip to main content

openai_compat/
client.rs

1//! The [`Client`] type: a cheaply-cloneable handle around a shared
2//! `reqwest::Client` and resolved [`Config`].
3
4use std::sync::Arc;
5
6use crate::config::{ClientBuilder, Config};
7use crate::error::OpenAIError;
8use crate::resources::assistants::{Assistants, Threads};
9use crate::resources::audio::Audio;
10use crate::resources::batches::Batches;
11use crate::resources::chat::Chat;
12use crate::resources::completions::Completions;
13use crate::resources::embeddings::Embeddings;
14use crate::resources::files::Files;
15use crate::resources::fine_tuning::FineTuning;
16use crate::resources::images::Images;
17use crate::resources::models::Models;
18use crate::resources::moderations::Moderations;
19use crate::resources::responses::Responses;
20use crate::resources::uploads::Uploads;
21use crate::resources::vector_stores::VectorStores;
22
23/// Asynchronous client for OpenAI-compatible APIs.
24///
25/// Construct with [`Client::new`] (environment variables) or
26/// [`Client::builder`]. Cloning is cheap (shared connection pool).
27#[derive(Clone)]
28pub struct Client {
29    inner: Arc<Inner>,
30}
31
32struct Inner {
33    http: reqwest::Client,
34    config: Config,
35}
36
37impl std::fmt::Debug for Client {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        f.debug_struct("Client")
40            .field("config", &self.inner.config)
41            .finish()
42    }
43}
44
45impl Client {
46    /// Build a client from environment variables (`OPENAI_API_KEY`,
47    /// `OPENAI_BASE_URL`, `OPENAI_ORG_ID`, `OPENAI_PROJECT_ID`).
48    pub fn new() -> Result<Self, OpenAIError> {
49        Self::builder().build()
50    }
51
52    /// Start configuring a client.
53    pub fn builder() -> ClientBuilder {
54        ClientBuilder::new()
55    }
56
57    pub(crate) fn from_config(config: Config) -> Result<Self, OpenAIError> {
58        // Mirror httpx semantics from the Python SDK: the timeout applies per
59        // read operation, not as a whole-request deadline — otherwise
60        // long-lived SSE streams would be aborted mid-flight. A total
61        // deadline can still be set per request via RequestOptions::timeout.
62        let http = reqwest::Client::builder()
63            .read_timeout(config.timeout)
64            .connect_timeout(config.connect_timeout)
65            .build()
66            .map_err(|e| OpenAIError::Config(format!("failed to build HTTP client: {e}")))?;
67        Ok(Self {
68            inner: Arc::new(Inner { http, config }),
69        })
70    }
71
72    pub(crate) fn http(&self) -> &reqwest::Client {
73        &self.inner.http
74    }
75
76    pub(crate) fn config(&self) -> &Config {
77        &self.inner.config
78    }
79
80    /// The resolved base URL (no trailing slash).
81    pub fn base_url(&self) -> &str {
82        &self.inner.config.base_url
83    }
84
85    /// Chat endpoints: `client.chat().completions()`.
86    pub fn chat(&self) -> Chat {
87        Chat::new(self.clone())
88    }
89
90    /// The embeddings resource.
91    pub fn embeddings(&self) -> Embeddings {
92        Embeddings::new(self.clone())
93    }
94
95    /// The models resource.
96    pub fn models(&self) -> Models {
97        Models::new(self.clone())
98    }
99
100    /// The moderations resource.
101    pub fn moderations(&self) -> Moderations {
102        Moderations::new(self.clone())
103    }
104
105    /// The legacy completions resource.
106    pub fn completions(&self) -> Completions {
107        Completions::new(self.clone())
108    }
109
110    /// The images resource.
111    pub fn images(&self) -> Images {
112        Images::new(self.clone())
113    }
114
115    /// The files resource.
116    pub fn files(&self) -> Files {
117        Files::new(self.clone())
118    }
119
120    /// Audio endpoints (speech, transcriptions).
121    pub fn audio(&self) -> Audio {
122        Audio::new(self.clone())
123    }
124
125    /// The batches resource.
126    pub fn batches(&self) -> Batches {
127        Batches::new(self.clone())
128    }
129
130    /// The resumable uploads resource.
131    pub fn uploads(&self) -> Uploads {
132        Uploads::new(self.clone())
133    }
134
135    /// Fine-tuning endpoints: `client.fine_tuning().jobs()`.
136    pub fn fine_tuning(&self) -> FineTuning {
137        FineTuning::new(self.clone())
138    }
139
140    /// The vector stores resource.
141    pub fn vector_stores(&self) -> VectorStores {
142        VectorStores::new(self.clone())
143    }
144
145    /// The assistants resource (beta v2): `client.assistants()`.
146    pub fn assistants(&self) -> Assistants {
147        Assistants::new(self.clone())
148    }
149
150    /// The threads resource (beta v2): `client.threads()`, with nested
151    /// `.messages()` and `.runs()`.
152    pub fn threads(&self) -> Threads {
153        Threads::new(self.clone())
154    }
155
156    /// The Responses API resource: `client.responses()`.
157    pub fn responses(&self) -> Responses {
158        Responses::new(self.clone())
159    }
160
161    /// Open a realtime WebSocket session for `model`, using this client's
162    /// API key, base URL, and organization/project settings.
163    ///
164    /// Azure realtime (deployment paths, `api-version` query, `api-key`
165    /// header over WebSocket) is not supported; Azure-configured clients
166    /// return an error.
167    pub async fn connect_realtime(
168        &self,
169        model: &str,
170    ) -> Result<crate::realtime::RealtimeSession, crate::realtime::RealtimeError> {
171        let config = self.config();
172        if config.azure.is_some() {
173            return Err(crate::realtime::RealtimeError::Connect(
174                "Azure realtime is not supported; use realtime::connect with explicit options"
175                    .into(),
176            ));
177        }
178        crate::realtime::connect(crate::realtime::RealtimeConnectOptions {
179            api_key: config.api_key.clone(),
180            base_url: config.base_url.clone(),
181            model: model.to_string(),
182            organization: config.organization.clone(),
183            project: config.project.clone(),
184            extra_headers: Vec::new(),
185        })
186        .await
187    }
188}