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/// LLM provider type identifier.
16///
17/// Built-in variants cover providers shipped with everruns. Any string that
18/// does not match a built-in id becomes `External(id)`, so embedders can store
19/// and use custom provider ids without schema migrations.
20///
21/// Serializes to/from a plain string (e.g. `"anthropic"`, `"openai-codex"`).
22#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23pub enum DriverId {
24    /// OpenAI using Open Responses API (<https://www.openresponses.org/>)
25    OpenAI,
26    /// OpenRouter using the OpenAI-compatible Responses API
27    OpenRouter,
28    /// Azure OpenAI using the Azure-hosted OpenAI v1 API
29    AzureOpenAI,
30    /// OpenAI using Chat Completions API (for backward compatibility)
31    OpenAICompletions,
32    Anthropic,
33    /// Google Gemini API
34    Gemini,
35    /// LLM simulator for testing
36    LlmSim,
37    /// AWS Bedrock Runtime (ConverseStream API)
38    Bedrock,
39    /// Microsoft MAI models (e.g. MAI-Code-1-Flash) served via Azure AI Foundry.
40    /// Uses an OpenAI-compatible Chat Completions API and supports either an
41    /// Azure AI Foundry API key or Microsoft Entra ID (OAuth) authentication.
42    Mai,
43    /// Fireworks AI — open-model inference (Llama, Qwen, DeepSeek, GLM, ...)
44    /// served via an OpenAI-compatible Chat Completions API.
45    Fireworks,
46    /// Meta Model API — Muse models served via an OpenAI-compatible Responses API.
47    Meta,
48    /// Embedder-defined provider not compiled into everruns-core. The inner id
49    /// is the canonical wire string (e.g. `"openai-codex"`).
50    External(std::sync::Arc<str>),
51}
52
53impl std::fmt::Display for DriverId {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        f.write_str(self.as_str())
56    }
57}
58
59impl DriverId {
60    /// Construct an external driver id from its canonical wire id.
61    ///
62    /// The id is normalized to lowercase so registration and lookup match
63    /// case-insensitively, consistent with built-in parsing.
64    pub fn external(id: impl Into<std::sync::Arc<str>>) -> Self {
65        let id: std::sync::Arc<str> = id.into();
66        // Avoid reallocating when the id is already lowercase.
67        if id.bytes().any(|b| b.is_ascii_uppercase()) {
68            DriverId::External(std::sync::Arc::from(id.to_lowercase().as_str()))
69        } else {
70            DriverId::External(id)
71        }
72    }
73
74    /// Return the canonical string identifier for this provider.
75    pub fn as_str(&self) -> &str {
76        match self {
77            DriverId::OpenAI => "openai",
78            DriverId::OpenRouter => "openrouter",
79            DriverId::AzureOpenAI => "azure_openai",
80            DriverId::OpenAICompletions => "openai_completions",
81            DriverId::Anthropic => "anthropic",
82            DriverId::Gemini => "gemini",
83            DriverId::LlmSim => "llmsim",
84            DriverId::Bedrock => "bedrock",
85            DriverId::Mai => "mai",
86            DriverId::Fireworks => "fireworks",
87            DriverId::Meta => "meta",
88            DriverId::External(id) => id.as_ref(),
89        }
90    }
91
92    /// Default trace-link URL templates for this driver, as
93    /// `(generation_url_template, session_url_template)`.
94    ///
95    /// These are best-effort defaults for vendors that expose an observability
96    /// dashboard. They are only *defaults*: an org overrides them per provider
97    /// (`ProviderTraceConfig`) and must opt in via `enabled`, since most vendors
98    /// retain prompt/completion content only when logging is explicitly turned
99    /// on. Templates support the `{response_id}`, `{session_id}`, `{turn_id}`
100    /// and `{model}` placeholders.
101    ///
102    /// OpenRouter stores logged generations on its **Logs** page
103    /// (<https://openrouter.ai/logs>, gated behind the account's
104    /// "Input & Output Logging" Observability setting). OpenRouter does not
105    /// document a public deep-link by generation id, so the generation template
106    /// passes the id best-effort; worst case it lands on the Logs page where the
107    /// generation can be found by recency.
108    pub fn default_trace_templates(&self) -> (Option<String>, Option<String>) {
109        match self {
110            DriverId::OpenRouter => (
111                Some("https://openrouter.ai/logs?id={response_id}".to_string()),
112                Some("https://openrouter.ai/logs".to_string()),
113            ),
114            _ => (None, None),
115        }
116    }
117}
118
119impl std::str::FromStr for DriverId {
120    // Parsing never fails: unknown ids become `External`.
121    type Err = std::convert::Infallible;
122
123    fn from_str(s: &str) -> Result<Self, Self::Err> {
124        // Normalize once so built-in matching and the External id share the
125        // same lowercased form; casing variance never yields duplicate ids.
126        let lower = s.to_lowercase();
127        Ok(match lower.as_str() {
128            "openai" => DriverId::OpenAI,
129            "openrouter" => DriverId::OpenRouter,
130            "azure_openai" => DriverId::AzureOpenAI,
131            "openai_completions" => DriverId::OpenAICompletions,
132            "anthropic" => DriverId::Anthropic,
133            "gemini" => DriverId::Gemini,
134            "llmsim" => DriverId::LlmSim,
135            "bedrock" => DriverId::Bedrock,
136            "mai" => DriverId::Mai,
137            "fireworks" => DriverId::Fireworks,
138            "meta" => DriverId::Meta,
139            _ => DriverId::External(std::sync::Arc::from(lower.as_str())),
140        })
141    }
142}
143
144impl Serialize for DriverId {
145    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
146        s.serialize_str(self.as_str())
147    }
148}
149
150impl<'de> Deserialize<'de> for DriverId {
151    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
152        let s = String::deserialize(d)?;
153        // FromStr is infallible (unknown ids become External).
154        Ok(s.parse().unwrap_or_else(|_| unreachable!()))
155    }
156}
157
158// `Arc<str>` does not implement `ToSchema`, so the schema is written by hand.
159// It is a plain string at the wire level regardless of the variant.
160#[cfg(feature = "openapi")]
161impl utoipa::ToSchema for DriverId {
162    fn name() -> std::borrow::Cow<'static, str> {
163        std::borrow::Cow::Borrowed("DriverId")
164    }
165}
166
167#[cfg(feature = "openapi")]
168impl utoipa::PartialSchema for DriverId {
169    fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::Schema> {
170        utoipa::openapi::ObjectBuilder::new()
171            .schema_type(utoipa::openapi::schema::SchemaType::new(
172                utoipa::openapi::schema::Type::String,
173            ))
174            .description(Some(
175                "LLM provider type. Built-in: openai, openrouter, azure_openai, \
176                 openai_completions, anthropic, gemini, llmsim, bedrock, mai, fireworks, meta. \
177                 Any other string is treated as an embedder-defined external provider.",
178            ))
179            .build()
180            .into()
181    }
182}
183
184/// LLM provider status
185#[derive(Debug, Clone, Serialize, Deserialize)]
186#[cfg_attr(feature = "openapi", derive(ToSchema))]
187#[serde(rename_all = "snake_case")]
188pub enum ProviderStatus {
189    Active,
190    Disabled,
191}
192
193/// Configuration for linking from the chat UI to a provider's observability
194/// dashboard ("trace"/"logs").
195///
196/// This is provider-agnostic: any driver with a dashboard can supply default
197/// templates (see [`DriverId::default_trace_templates`]), and an org enables
198/// links per provider once it has confirmed logging is on for that account.
199/// URL templates support the `{response_id}`, `{session_id}`, `{turn_id}` and
200/// `{model}` placeholders, so the same mechanism works for OpenRouter today and
201/// for third-party observability backends (Langfuse, Helicone, ...) via an
202/// override.
203#[derive(Debug, Clone, Default, Serialize, Deserialize)]
204#[cfg_attr(feature = "openapi", derive(ToSchema))]
205pub struct ProviderTraceConfig {
206    /// Whether trace links should be shown for this provider. Defaults to
207    /// `false`: vendors typically do not retain trace content unless logging is
208    /// explicitly enabled, so the org opts in once that is set up.
209    pub enabled: bool,
210    /// URL template for a single generation's trace, e.g.
211    /// `"https://openrouter.ai/logs?id={response_id}"`.
212    #[serde(default, skip_serializing_if = "Option::is_none")]
213    pub generation_url_template: Option<String>,
214    /// URL template for a session's grouped trace, e.g.
215    /// `"https://openrouter.ai/logs"`.
216    #[serde(default, skip_serializing_if = "Option::is_none")]
217    pub session_url_template: Option<String>,
218}
219
220/// LLM Provider entity (API keys never exposed)
221/// Note: This is the entity struct, separate from the Provider trait in llm.rs
222#[derive(Debug, Clone, Serialize, Deserialize)]
223#[cfg_attr(feature = "openapi", derive(ToSchema))]
224pub struct Provider {
225    /// Prefixed public identifier. See [ID Schema](https://docs.everruns.com/advanced/id-schema/).
226    #[cfg_attr(feature = "openapi", schema(value_type = String, example = "provider_01933b5a00007000800000000000001"))]
227    pub id: ProviderId,
228    /// Human-readable provider name. Safe to render in user-facing messages.
229    pub name: String,
230    /// Provider implementation type (OpenAI, Anthropic, Gemini, etc.).
231    pub provider_type: DriverId,
232    /// Custom base URL for self-hosted / proxied providers. `None` means use the provider's default endpoint.
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub base_url: Option<String>,
235    /// Whether an API key is configured. The key itself is never returned.
236    pub api_key_set: bool,
237    /// Current lifecycle status of this provider.
238    pub status: ProviderStatus,
239    /// Whether this provider is host-managed (EVE-810). A managed provider is
240    /// provisioned by the host/embedder; the OSS API rejects tenant PATCH/DELETE
241    /// on it (403). Read-only to org admins. Defaults to `false`.
242    pub managed: bool,
243    /// Timestamp of the most recent successful model sync from the provider's API (RFC 3339).
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub last_synced_at: Option<DateTime<Utc>>,
246    /// Timestamp when this provider was created (RFC 3339).
247    pub created_at: DateTime<Utc>,
248    /// Timestamp when this provider was last updated (RFC 3339).
249    pub updated_at: DateTime<Utc>,
250    /// Resolved trace/observability link configuration: the driver's default
251    /// templates overlaid with this provider's stored overrides. `None` when the
252    /// driver exposes no dashboard and the org configured nothing.
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub trace: Option<ProviderTraceConfig>,
255}