1use crate::{
2 CancellationToken, Error, HeaderMap, RetryPolicy, DEFAULT_BASE_URL, DEFAULT_MODEL,
3 DEFAULT_TIMEOUT,
4};
5use std::{fmt, time::Duration};
6
7pub type LogLevel = log::LevelFilter;
10
11#[derive(Clone, Default)]
13pub struct RequestOptions {
14 pub retry: Option<RetryPolicy>,
16 pub timeout: Option<Duration>,
18 pub headers: HeaderMap,
20 pub cancellation_token: Option<CancellationToken>,
23}
24
25impl fmt::Debug for RequestOptions {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 f.debug_struct("RequestOptions")
28 .field("retry", &self.retry)
29 .field("timeout", &self.timeout)
30 .field("header_count", &self.headers.len())
31 .field("cancellable", &self.cancellation_token.is_some())
32 .finish()
33 }
34}
35
36#[derive(Clone, Default)]
37pub(crate) struct ConfigBuilder {
38 pub api_key: Option<String>,
39 pub base_url: Option<String>,
40 pub model: Option<String>,
41 pub timeout: Option<Duration>,
42 pub retry: RetryPolicy,
43 pub headers: HeaderMap,
44 pub log_level: Option<LogLevel>,
45}
46
47impl fmt::Debug for ConfigBuilder {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 f.debug_struct("ConfigBuilder").finish_non_exhaustive()
50 }
51}
52
53#[derive(Clone)]
54pub(crate) struct Config {
55 pub authorization: reqwest::header::HeaderValue,
56 pub base_url: String,
57 pub model: String,
58 pub timeout: Duration,
59 pub retry: RetryPolicy,
60 pub headers: HeaderMap,
61 pub log_level: LogLevel,
62}
63
64impl fmt::Debug for Config {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 f.debug_struct("Config")
67 .field("base_url", &self.base_url)
68 .field("model", &self.model)
69 .finish_non_exhaustive()
70 }
71}
72
73fn env(name: &str) -> Option<String> {
74 std::env::var(name)
75 .ok()
76 .map(|v| v.trim().to_owned())
77 .filter(|v| !v.is_empty())
78}
79
80pub(crate) fn validate_timeout(timeout: Duration) -> Result<(), Error> {
81 if timeout.is_zero() || std::time::Instant::now().checked_add(timeout).is_none() {
82 return Err(Error::Configuration(
83 "timeout must be positive and representable".into(),
84 ));
85 }
86 Ok(())
87}
88
89impl ConfigBuilder {
90 pub fn resolve(self) -> Result<Config, Error> {
91 let key = self
92 .api_key
93 .or_else(|| env("TYPESAFE_API_KEY"))
94 .filter(|v| !v.trim().is_empty())
95 .ok_or_else(|| {
96 Error::Configuration("Pass an API key or set TYPESAFE_API_KEY".into())
97 })?;
98 let mut authorization = reqwest::header::HeaderValue::from_str(&format!("Bearer {key}"))
99 .map_err(|_| {
100 Error::Configuration("API key contains invalid HTTP header characters".into())
101 })?;
102 authorization.set_sensitive(true);
103 let base_url = self
104 .base_url
105 .or_else(|| env("TYPESAFE_BASE_URL"))
106 .unwrap_or_else(|| DEFAULT_BASE_URL.into());
107 let url = reqwest::Url::parse(&base_url)
108 .map_err(|_| Error::Configuration("base_url must be an absolute HTTP(S) URL".into()))?;
109 if !matches!(url.scheme(), "https" | "http")
110 || url.host_str().is_none()
111 || !url.username().is_empty()
112 || url.password().is_some()
113 || url.query().is_some()
114 || url.fragment().is_some()
115 {
116 return Err(Error::Configuration(
117 "base_url must be an HTTP(S) root without credentials, query, or fragment".into(),
118 ));
119 }
120 let timeout = self.timeout.unwrap_or(DEFAULT_TIMEOUT);
121 validate_timeout(timeout)?;
122 self.retry.validate()?;
123 let log_level = match self.log_level {
124 Some(level) => level,
125 None => match env("TYPESAFE_LOG_LEVEL").as_deref() {
126 None => LogLevel::Warn,
127 Some("debug") => LogLevel::Debug,
128 Some("info") => LogLevel::Info,
129 Some("warn" | "warning") => LogLevel::Warn,
130 Some("error") => LogLevel::Error,
131 Some("off") => LogLevel::Off,
132 Some(_) => {
133 return Err(Error::Configuration(
134 "TYPESAFE_LOG_LEVEL must be debug, info, warn, warning, error, or off"
135 .into(),
136 ))
137 }
138 },
139 };
140 Ok(Config {
141 authorization,
142 base_url: url.as_str().trim_end_matches('/').to_owned(),
143 model: self
144 .model
145 .or_else(|| env("TYPESAFE_DEFAULT_MODEL"))
146 .unwrap_or_else(|| DEFAULT_MODEL.into()),
147 timeout,
148 retry: self.retry,
149 headers: self.headers,
150 log_level,
151 })
152 }
153}
154
155macro_rules! config_methods {
157 () => {
158 pub fn api_key(mut self, value: impl Into<String>) -> Self {
160 self.config.api_key = Some(value.into());
161 self
162 }
163 pub fn base_url(mut self, value: impl Into<String>) -> Self {
165 self.config.base_url = Some(value.into());
166 self
167 }
168 pub fn model(mut self, value: impl Into<String>) -> Self {
170 self.config.model = Some(value.into());
171 self
172 }
173 pub fn timeout(mut self, value: std::time::Duration) -> Self {
175 self.config.timeout = Some(value);
176 self
177 }
178 pub fn retry(mut self, value: crate::RetryPolicy) -> Self {
180 self.config.retry = value;
181 self
182 }
183 pub fn default_headers(mut self, value: crate::HeaderMap) -> Self {
185 self.config.headers = value;
186 self
187 }
188 pub fn log_level(mut self, value: crate::LogLevel) -> Self {
190 self.config.log_level = Some(value);
191 self
192 }
193 };
194}
195pub(crate) use config_methods;