providers_kit/config/openai.rs
1use reqwest::header::HeaderValue;
2use url::Url;
3
4use super::ApiKey;
5use crate::KitError;
6use crate::error::ValidationViolation;
7
8const DEFAULT_OPENAI_BASE_URL: &str = "https://api.openai.com/v1";
9
10/// OpenAI credentials and endpoint configuration.
11///
12/// ```
13/// let config = providers_kit::config::OpenAiConfig::new(
14/// providers_kit::config::ApiKey::new("sk-test")?,
15/// );
16/// let _ = config;
17///
18/// # Ok::<(), providers_kit::KitError>(())
19/// ```
20#[derive(Clone)]
21pub struct OpenAiConfig {
22 api_key: ApiKey,
23 base_url: Url,
24 organization: Option<String>,
25 project: Option<String>,
26}
27
28impl OpenAiConfig {
29 /// Creates a configuration for the public OpenAI endpoint.
30 ///
31 /// This only constructs configuration; provider operations perform network I/O.
32 ///
33 /// # Examples
34 ///
35 /// ```
36 /// use providers_kit::config::{ApiKey, OpenAiConfig};
37 /// let config = OpenAiConfig::new(ApiKey::new("example-key")?);
38 /// let _owned_configuration = config;
39 ///
40 /// # Ok::<(), providers_kit::KitError>(())
41 /// ```
42 ///
43 #[must_use]
44 pub fn new(api_key: ApiKey) -> Self {
45 Self {
46 api_key,
47 base_url: Url::parse(DEFAULT_OPENAI_BASE_URL)
48 .expect("crate-owned OpenAI base URL should parse"),
49 organization: None,
50 project: None,
51 }
52 }
53
54 /// Replaces the API base URL.
55 ///
56 /// This only constructs configuration; provider operations perform network I/O.
57 ///
58 /// # Examples
59 ///
60 /// ```
61 /// use providers_kit::config::{ApiKey, OpenAiConfig};
62 /// let config = OpenAiConfig::new(ApiKey::new("example-key")?);
63 /// let config = config.with_base_url("https://example.com/api".parse().unwrap());
64 /// let _owned_configuration = config;
65 ///
66 /// # Ok::<(), providers_kit::KitError>(())
67 /// ```
68 ///
69 #[must_use]
70 pub fn with_base_url(mut self, base_url: Url) -> Self {
71 self.base_url = base_url;
72 self
73 }
74
75 /// Sets the optional organization header.
76 ///
77 /// # Errors
78 ///
79 /// Returns a configuration error for an empty or padded value, or one that cannot be represented as an HTTP header value.
80 ///
81 /// # Examples
82 ///
83 /// ```
84 /// use providers_kit::config::{ApiKey, OpenAiConfig};
85 /// let config = OpenAiConfig::new(ApiKey::new("example-key")?);
86 /// assert!(config.clone().with_organization(" padded ").is_err());
87 /// let _config = config.with_organization("team-1")?;
88 ///
89 /// # Ok::<(), providers_kit::KitError>(())
90 /// ```
91 ///
92 pub fn with_organization(mut self, organization: impl Into<String>) -> Result<Self, KitError> {
93 self.organization = Some(Self::scope(
94 "credentials.organization",
95 organization.into(),
96 )?);
97 Ok(self)
98 }
99
100 /// Sets the optional project header.
101 ///
102 /// # Errors
103 ///
104 /// Returns a configuration error for an empty or padded value, or one that cannot be represented as an HTTP header value.
105 ///
106 /// # Examples
107 ///
108 /// ```
109 /// use providers_kit::config::{ApiKey, OpenAiConfig};
110 /// let config = OpenAiConfig::new(ApiKey::new("example-key")?);
111 /// assert!(config.clone().with_project(" padded ").is_err());
112 /// let _config = config.with_project("team-1")?;
113 ///
114 /// # Ok::<(), providers_kit::KitError>(())
115 /// ```
116 ///
117 pub fn with_project(mut self, project: impl Into<String>) -> Result<Self, KitError> {
118 self.project = Some(Self::scope("credentials.project", project.into())?);
119 Ok(self)
120 }
121
122 pub(crate) fn api_key(&self) -> &ApiKey {
123 &self.api_key
124 }
125
126 pub(crate) fn base_url(&self) -> &Url {
127 &self.base_url
128 }
129
130 pub(crate) fn organization(&self) -> Option<&str> {
131 self.organization.as_deref()
132 }
133
134 pub(crate) fn project(&self) -> Option<&str> {
135 self.project.as_deref()
136 }
137
138 fn scope(label: &'static str, value: String) -> Result<String, KitError> {
139 if value.is_empty() || value.trim() != value || HeaderValue::try_from(&value).is_err() {
140 Err(KitError::config(
141 ValidationViolation::InvalidFormat {
142 field: label,
143 rule: "nonblank, unpadded HTTP header value",
144 },
145 format!("{label} must be a nonblank, unpadded HTTP header value"),
146 ))
147 } else {
148 Ok(value)
149 }
150 }
151}