synth_ai_core/api/client.rs
1//! Main Synth API client.
2//!
3//! The `SynthClient` is the primary entry point for interacting with the Synth API.
4
5use crate::auth;
6use crate::http::HttpClient;
7use crate::CoreError;
8
9use super::eval::EvalClient;
10use super::graphs::GraphsClient;
11use super::jobs::JobsClient;
12
13/// Default backend URL.
14pub const DEFAULT_BACKEND_URL: &str = "https://api.usesynth.ai";
15
16/// Default request timeout in seconds.
17pub const DEFAULT_TIMEOUT_SECS: u64 = 120;
18
19/// Synth API client.
20///
21/// This is the main entry point for interacting with the Synth API.
22/// It provides access to sub-clients for different API endpoints.
23///
24/// # Example
25///
26/// ```ignore
27/// use synth_ai_core::api::SynthClient;
28///
29/// // Create from environment variable
30/// let client = SynthClient::from_env()?;
31///
32/// // Or with explicit API key
33/// let client = SynthClient::new("sk_live_...", None)?;
34///
35/// // Access sub-clients
36/// let jobs = client.jobs();
37/// let eval = client.eval();
38/// let graphs = client.graphs();
39/// ```
40pub struct SynthClient {
41 pub(crate) http: HttpClient,
42 pub(crate) base_url: String,
43}
44
45impl SynthClient {
46 /// Create a new Synth client with an API key.
47 ///
48 /// # Arguments
49 ///
50 /// * `api_key` - Your Synth API key
51 /// * `base_url` - Optional base URL (defaults to https://api.usesynth.ai)
52 ///
53 /// # Example
54 ///
55 /// ```ignore
56 /// let client = SynthClient::new("sk_live_...", None)?;
57 /// ```
58 pub fn new(api_key: &str, base_url: Option<&str>) -> Result<Self, CoreError> {
59 let base_url = base_url.unwrap_or(DEFAULT_BACKEND_URL).to_string();
60 let http = HttpClient::new(&base_url, api_key, DEFAULT_TIMEOUT_SECS)
61 .map_err(|e| CoreError::Internal(format!("failed to create HTTP client: {}", e)))?;
62
63 Ok(Self { http, base_url })
64 }
65
66 /// Create a new Synth client with custom timeout.
67 ///
68 /// # Arguments
69 ///
70 /// * `api_key` - Your Synth API key
71 /// * `base_url` - Optional base URL
72 /// * `timeout_secs` - Request timeout in seconds
73 pub fn with_timeout(
74 api_key: &str,
75 base_url: Option<&str>,
76 timeout_secs: u64,
77 ) -> Result<Self, CoreError> {
78 let base_url = base_url.unwrap_or(DEFAULT_BACKEND_URL).to_string();
79 let http = HttpClient::new(&base_url, api_key, timeout_secs)
80 .map_err(|e| CoreError::Internal(format!("failed to create HTTP client: {}", e)))?;
81
82 Ok(Self { http, base_url })
83 }
84
85 /// Create a client from environment variables.
86 ///
87 /// Reads the API key from `SYNTH_API_KEY` environment variable.
88 /// Optionally reads base URL from `SYNTH_BACKEND_URL`.
89 ///
90 /// # Example
91 ///
92 /// ```ignore
93 /// std::env::set_var("SYNTH_API_KEY", "sk_live_...");
94 /// let client = SynthClient::from_env()?;
95 /// ```
96 pub fn from_env() -> Result<Self, CoreError> {
97 let api_key = auth::get_api_key(None).ok_or_else(|| {
98 CoreError::Authentication("SYNTH_API_KEY environment variable not set".to_string())
99 })?;
100
101 let base_url = std::env::var("SYNTH_BACKEND_URL").ok();
102 Self::new(&api_key, base_url.as_deref())
103 }
104
105 /// Create a client, minting a demo key if needed.
106 ///
107 /// This will:
108 /// 1. Try to get an API key from environment
109 /// 2. If not found and `allow_mint` is true, mint a demo key
110 ///
111 /// # Arguments
112 ///
113 /// * `allow_mint` - Whether to mint a demo key if no key is found
114 /// * `base_url` - Optional base URL
115 pub async fn from_env_or_mint(
116 allow_mint: bool,
117 base_url: Option<&str>,
118 ) -> Result<Self, CoreError> {
119 let api_key = auth::get_or_mint_api_key(base_url, allow_mint).await?;
120 Self::new(&api_key, base_url)
121 }
122
123 /// Get the base URL for this client.
124 pub fn base_url(&self) -> &str {
125 &self.base_url
126 }
127
128 /// Get a reference to the HTTP client.
129 pub fn http(&self) -> &HttpClient {
130 &self.http
131 }
132
133 /// Get a Jobs API client.
134 ///
135 /// Use this to submit, poll, and cancel optimization jobs.
136 ///
137 /// # Example
138 ///
139 /// ```ignore
140 /// let job_id = client.jobs().submit_gepa(request).await?;
141 /// let result = client.jobs().poll_until_complete(&job_id, 3600.0, 15.0).await?;
142 /// ```
143 pub fn jobs(&self) -> JobsClient<'_> {
144 JobsClient::new(self)
145 }
146
147 /// Get an Eval API client.
148 ///
149 /// Use this to run evaluation jobs.
150 ///
151 /// # Example
152 ///
153 /// ```ignore
154 /// let job_id = client.eval().submit(request).await?;
155 /// let result = client.eval().poll_until_complete(&job_id, 3600.0, 15.0).await?;
156 /// ```
157 pub fn eval(&self) -> EvalClient<'_> {
158 EvalClient::new(self)
159 }
160
161 /// Get a Graphs API client.
162 ///
163 /// Use this for graph completions and verifier inference.
164 ///
165 /// # Example
166 ///
167 /// ```ignore
168 /// let result = client.graphs().verify(trace, rubric, None).await?;
169 /// ```
170 pub fn graphs(&self) -> GraphsClient<'_> {
171 GraphsClient::new(self)
172 }
173}
174
175impl std::fmt::Debug for SynthClient {
176 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177 f.debug_struct("SynthClient")
178 .field("base_url", &self.base_url)
179 .finish_non_exhaustive()
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn test_default_backend_url() {
189 assert_eq!(DEFAULT_BACKEND_URL, "https://api.usesynth.ai");
190 }
191}