Skip to main content

everruns_provider/
provider.rs

1// Provider entity types (knowledge/foundations/providers.md)
2//
3// A Provider is an org-scoped instance of a driver: a configured vendor
4// account (credentials, endpoint) that powers services like chat. DriverId
5// names the driver implementation a provider uses.
6
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10use crate::typed_id::ProviderId;
11
12#[cfg(feature = "openapi")]
13use utoipa::ToSchema;
14
15/// Open string identifier retained as the 0.17.x integration-kind name.
16///
17/// Despite the legacy type name, this is not a built-in-provider enum: any
18/// normalized string is valid. The associated constants keep source
19/// compatibility for the 0.17.x runtime adapter and persisted HTTP shapes.
20#[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    /// Construct an external driver id from its canonical wire id.
54    ///
55    /// The id is normalized to lowercase so registration and lookup match
56    /// case-insensitively, consistent with built-in parsing.
57    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    /// Return the canonical string identifier for this provider.
64    pub fn as_str(&self) -> &str {
65        self.0.as_ref()
66    }
67
68    /// Default trace-link URL templates for this driver, as
69    /// `(generation_url_template, session_url_template)`.
70    ///
71    /// These are best-effort defaults for vendors that expose an observability
72    /// dashboard. They are only *defaults*: an org overrides them per provider
73    /// (`ProviderTraceConfig`) and must opt in via `enabled`, since most vendors
74    /// retain prompt/completion content only when logging is explicitly turned
75    /// on. Templates support the `{response_id}`, `{session_id}`, `{turn_id}`
76    /// and `{model}` placeholders.
77    ///
78    /// OpenRouter stores logged generations on its **Logs** page
79    /// (<https://openrouter.ai/logs>, gated behind the account's
80    /// "Input & Output Logging" Observability setting). OpenRouter does not
81    /// document a public deep-link by generation id, so the generation template
82    /// passes the id best-effort; worst case it lands on the Logs page where the
83    /// generation can be found by recency.
84    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    // Parsing never fails: unknown ids become `External`.
98    type Err = std::convert::Infallible;
99
100    fn from_str(s: &str) -> Result<Self, Self::Err> {
101        // Normalize once so built-in matching and the External id share the
102        // same lowercased form; casing variance never yields duplicate ids.
103        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        // FromStr is infallible (unknown ids become External).
125        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// `Arc<str>` does not implement `ToSchema`, so the schema is written by hand.
151// It is a plain string at the wire level regardless of the variant.
152#[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/// LLM provider status
177#[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/// Configuration for linking from the chat UI to a provider's observability
186/// dashboard ("trace"/"logs").
187///
188/// This is provider-agnostic: any driver with a dashboard can supply default
189/// templates (see [`DriverId::default_trace_templates`]), and an org enables
190/// links per provider once it has confirmed logging is on for that account.
191/// URL templates support the `{response_id}`, `{session_id}`, `{turn_id}` and
192/// `{model}` placeholders, so the same mechanism works for OpenRouter today and
193/// for third-party observability backends (Langfuse, Helicone, ...) via an
194/// override.
195#[derive(Debug, Clone, Default, Serialize, Deserialize)]
196#[cfg_attr(feature = "openapi", derive(ToSchema))]
197pub struct ProviderTraceConfig {
198    /// Whether trace links should be shown for this provider. Defaults to
199    /// `false`: vendors typically do not retain trace content unless logging is
200    /// explicitly enabled, so the org opts in once that is set up.
201    pub enabled: bool,
202    /// URL template for a single generation's trace, e.g.
203    /// `"https://openrouter.ai/logs?id={response_id}"`.
204    #[serde(default, skip_serializing_if = "Option::is_none")]
205    pub generation_url_template: Option<String>,
206    /// URL template for a session's grouped trace, e.g.
207    /// `"https://openrouter.ai/logs"`.
208    #[serde(default, skip_serializing_if = "Option::is_none")]
209    pub session_url_template: Option<String>,
210}
211
212/// LLM Provider entity (API keys never exposed)
213/// Note: This is the entity struct, separate from the Provider trait in llm.rs
214#[derive(Debug, Clone, Serialize, Deserialize)]
215#[cfg_attr(feature = "openapi", derive(ToSchema))]
216pub struct Provider {
217    /// Prefixed public identifier. See [ID Schema](https://docs.everruns.com/advanced/id-schema/).
218    #[cfg_attr(feature = "openapi", schema(value_type = String, example = "provider_01933b5a00007000800000000000001"))]
219    pub id: ProviderId,
220    /// Human-readable provider name. Safe to render in user-facing messages.
221    pub name: String,
222    /// Provider implementation type (OpenAI, Anthropic, Gemini, etc.).
223    pub provider_type: DriverId,
224    /// Custom base URL for self-hosted / proxied providers. `None` means use the provider's default endpoint.
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub base_url: Option<String>,
227    /// Whether an API key is configured. The key itself is never returned.
228    pub api_key_set: bool,
229    /// Current lifecycle status of this provider.
230    pub status: ProviderStatus,
231    /// Whether this provider is host-managed (EVE-810). A managed provider is
232    /// provisioned by the host/embedder; the OSS API rejects tenant PATCH/DELETE
233    /// on it (403). Read-only to org admins. Defaults to `false`.
234    pub managed: bool,
235    /// Timestamp of the most recent successful model sync from the provider's API (RFC 3339).
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub last_synced_at: Option<DateTime<Utc>>,
238    /// Timestamp when this provider was created (RFC 3339).
239    pub created_at: DateTime<Utc>,
240    /// Timestamp when this provider was last updated (RFC 3339).
241    pub updated_at: DateTime<Utc>,
242    /// Resolved trace/observability link configuration: the driver's default
243    /// templates overlaid with this provider's stored overrides. `None` when the
244    /// driver exposes no dashboard and the org configured nothing.
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub trace: Option<ProviderTraceConfig>,
247}