everruns_provider/
provider.rs1use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10use crate::typed_id::ProviderId;
11
12#[cfg(feature = "openapi")]
13use utoipa::ToSchema;
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21pub struct DriverId(std::borrow::Cow<'static, str>);
22
23impl std::fmt::Display for DriverId {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 f.write_str(self.as_str())
26 }
27}
28
29impl DriverId {
30 #[allow(non_upper_case_globals)]
31 pub const OpenAI: Self = Self(std::borrow::Cow::Borrowed("openai"));
32 #[allow(non_upper_case_globals)]
33 pub const OpenRouter: Self = Self(std::borrow::Cow::Borrowed("openrouter"));
34 #[allow(non_upper_case_globals)]
35 pub const AzureOpenAI: Self = Self(std::borrow::Cow::Borrowed("azure_openai"));
36 #[allow(non_upper_case_globals)]
37 pub const OpenAICompletions: Self = Self(std::borrow::Cow::Borrowed("openai_completions"));
38 #[allow(non_upper_case_globals)]
39 pub const Anthropic: Self = Self(std::borrow::Cow::Borrowed("anthropic"));
40 #[allow(non_upper_case_globals)]
41 pub const Gemini: Self = Self(std::borrow::Cow::Borrowed("gemini"));
42 #[allow(non_upper_case_globals)]
43 pub const LlmSim: Self = Self(std::borrow::Cow::Borrowed("llmsim"));
44 #[allow(non_upper_case_globals)]
45 pub const Bedrock: Self = Self(std::borrow::Cow::Borrowed("bedrock"));
46 #[allow(non_upper_case_globals)]
47 pub const Mai: Self = Self(std::borrow::Cow::Borrowed("mai"));
48 #[allow(non_upper_case_globals)]
49 pub const Fireworks: Self = Self(std::borrow::Cow::Borrowed("fireworks"));
50 #[allow(non_upper_case_globals)]
51 pub const Meta: Self = Self(std::borrow::Cow::Borrowed("meta"));
52
53 pub fn external(id: impl AsRef<str>) -> Self {
58 Self(std::borrow::Cow::Owned(
59 id.as_ref().trim().to_ascii_lowercase(),
60 ))
61 }
62
63 pub fn as_str(&self) -> &str {
65 self.0.as_ref()
66 }
67
68 pub fn default_trace_templates(&self) -> (Option<String>, Option<String>) {
85 if self == &DriverId::OpenRouter {
86 (
87 Some("https://openrouter.ai/logs?id={response_id}".to_string()),
88 Some("https://openrouter.ai/logs".to_string()),
89 )
90 } else {
91 (None, None)
92 }
93 }
94}
95
96impl std::str::FromStr for DriverId {
97 type Err = std::convert::Infallible;
99
100 fn from_str(s: &str) -> Result<Self, Self::Err> {
101 Ok(DriverId::external(s))
104 }
105}
106
107impl Serialize for DriverId {
108 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
109 s.serialize_str(self.as_str())
110 }
111}
112
113impl<'de> Deserialize<'de> for DriverId {
114 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
115 let s = String::deserialize(d)?;
116 if s.trim().is_empty() {
117 return Err(serde::de::Error::custom("provider type cannot be empty"));
118 }
119 if s != s.trim() {
120 return Err(serde::de::Error::custom(
121 "provider type cannot have leading or trailing whitespace",
122 ));
123 }
124 Ok(s.parse().unwrap_or_else(|_| unreachable!()))
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::DriverId;
132
133 #[test]
134 fn wire_ids_reject_empty_and_padded_values() {
135 assert!(serde_json::from_str::<DriverId>(r#"""#).is_err());
136 assert!(serde_json::from_str::<DriverId>(r#"" openai ""#).is_err());
137 }
138
139 #[test]
140 fn wire_ids_normalize_case_and_accept_extensions() {
141 assert_eq!(
142 serde_json::from_str::<DriverId>(r#""Custom-Driver""#)
143 .unwrap()
144 .as_str(),
145 "custom-driver"
146 );
147 }
148}
149
150#[cfg(feature = "openapi")]
153impl utoipa::ToSchema for DriverId {
154 fn name() -> std::borrow::Cow<'static, str> {
155 std::borrow::Cow::Borrowed("DriverId")
156 }
157}
158
159#[cfg(feature = "openapi")]
160impl utoipa::PartialSchema for DriverId {
161 fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::Schema> {
162 utoipa::openapi::ObjectBuilder::new()
163 .schema_type(utoipa::openapi::schema::SchemaType::new(
164 utoipa::openapi::schema::Type::String,
165 ))
166 .description(Some(
167 "LLM provider type. Built-in: openai, openrouter, azure_openai, \
168 openai_completions, anthropic, gemini, llmsim, bedrock, mai, fireworks, meta. \
169 Any other string is treated as an embedder-defined external provider.",
170 ))
171 .build()
172 .into()
173 }
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178#[cfg_attr(feature = "openapi", derive(ToSchema))]
179#[serde(rename_all = "snake_case")]
180pub enum ProviderStatus {
181 Active,
182 Disabled,
183}
184
185#[derive(Debug, Clone, Default, Serialize, Deserialize)]
196#[cfg_attr(feature = "openapi", derive(ToSchema))]
197pub struct ProviderTraceConfig {
198 pub enabled: bool,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub generation_url_template: Option<String>,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub session_url_template: Option<String>,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
215#[cfg_attr(feature = "openapi", derive(ToSchema))]
216pub struct Provider {
217 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "provider_01933b5a00007000800000000000001"))]
219 pub id: ProviderId,
220 pub name: String,
222 pub provider_type: DriverId,
224 #[serde(skip_serializing_if = "Option::is_none")]
226 pub base_url: Option<String>,
227 pub api_key_set: bool,
229 pub status: ProviderStatus,
231 pub managed: bool,
235 #[serde(skip_serializing_if = "Option::is_none")]
237 pub last_synced_at: Option<DateTime<Utc>>,
238 pub created_at: DateTime<Utc>,
240 pub updated_at: DateTime<Utc>,
242 #[serde(skip_serializing_if = "Option::is_none")]
246 pub trace: Option<ProviderTraceConfig>,
247}