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