everruns-core 0.12.0

Core agent abstractions for Everruns - agent loop, events, tools, LLM providers
Documentation
// Provider entity types (specs/providers.md)
//
// A Provider is an org-scoped instance of a driver: a configured vendor
// account (credentials, endpoint) that powers services like chat. DriverId
// names the driver implementation a provider uses.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

use crate::typed_id::ProviderId;

#[cfg(feature = "openapi")]
use utoipa::ToSchema;

/// LLM provider type identifier.
///
/// Built-in variants cover providers shipped with everruns. Any string that
/// does not match a built-in id becomes `External(id)`, so embedders can store
/// and use custom provider ids without schema migrations.
///
/// Serializes to/from a plain string (e.g. `"anthropic"`, `"openai-codex"`).
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum DriverId {
    /// OpenAI using Open Responses API (<https://www.openresponses.org/>)
    OpenAI,
    /// OpenRouter using the OpenAI-compatible Responses API
    OpenRouter,
    /// Azure OpenAI using the Azure-hosted OpenAI v1 API
    AzureOpenAI,
    /// OpenAI using Chat Completions API (for backward compatibility)
    OpenAICompletions,
    Anthropic,
    /// Google Gemini API
    Gemini,
    /// LLM simulator for testing
    LlmSim,
    /// AWS Bedrock Runtime (ConverseStream API)
    Bedrock,
    /// Embedder-defined provider not compiled into everruns-core. The inner id
    /// is the canonical wire string (e.g. `"openai-codex"`).
    External(std::sync::Arc<str>),
}

impl std::fmt::Display for DriverId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}

impl DriverId {
    /// Construct an external driver id from its canonical wire id.
    ///
    /// The id is normalized to lowercase so registration and lookup match
    /// case-insensitively, consistent with built-in parsing.
    pub fn external(id: impl Into<std::sync::Arc<str>>) -> Self {
        let id: std::sync::Arc<str> = id.into();
        // Avoid reallocating when the id is already lowercase.
        if id.bytes().any(|b| b.is_ascii_uppercase()) {
            DriverId::External(std::sync::Arc::from(id.to_lowercase().as_str()))
        } else {
            DriverId::External(id)
        }
    }

    /// Return the canonical string identifier for this provider.
    pub fn as_str(&self) -> &str {
        match self {
            DriverId::OpenAI => "openai",
            DriverId::OpenRouter => "openrouter",
            DriverId::AzureOpenAI => "azure_openai",
            DriverId::OpenAICompletions => "openai_completions",
            DriverId::Anthropic => "anthropic",
            DriverId::Gemini => "gemini",
            DriverId::LlmSim => "llmsim",
            DriverId::Bedrock => "bedrock",
            DriverId::External(id) => id.as_ref(),
        }
    }
}

impl std::str::FromStr for DriverId {
    // Parsing never fails: unknown ids become `External`.
    type Err = std::convert::Infallible;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        // Normalize once so built-in matching and the External id share the
        // same lowercased form; casing variance never yields duplicate ids.
        let lower = s.to_lowercase();
        Ok(match lower.as_str() {
            "openai" => DriverId::OpenAI,
            "openrouter" => DriverId::OpenRouter,
            "azure_openai" => DriverId::AzureOpenAI,
            "openai_completions" => DriverId::OpenAICompletions,
            "anthropic" => DriverId::Anthropic,
            "gemini" => DriverId::Gemini,
            "llmsim" => DriverId::LlmSim,
            "bedrock" => DriverId::Bedrock,
            _ => DriverId::External(std::sync::Arc::from(lower.as_str())),
        })
    }
}

impl Serialize for DriverId {
    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
        s.serialize_str(self.as_str())
    }
}

impl<'de> Deserialize<'de> for DriverId {
    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
        let s = String::deserialize(d)?;
        // FromStr is infallible (unknown ids become External).
        Ok(s.parse().unwrap_or_else(|_| unreachable!()))
    }
}

// `Arc<str>` does not implement `ToSchema`, so the schema is written by hand.
// It is a plain string at the wire level regardless of the variant.
#[cfg(feature = "openapi")]
impl utoipa::ToSchema for DriverId {
    fn name() -> std::borrow::Cow<'static, str> {
        std::borrow::Cow::Borrowed("DriverId")
    }
}

#[cfg(feature = "openapi")]
impl utoipa::PartialSchema for DriverId {
    fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::Schema> {
        utoipa::openapi::ObjectBuilder::new()
            .schema_type(utoipa::openapi::schema::SchemaType::new(
                utoipa::openapi::schema::Type::String,
            ))
            .description(Some(
                "LLM provider type. Built-in: openai, openrouter, azure_openai, \
                 openai_completions, anthropic, gemini, llmsim, bedrock. \
                 Any other string is treated as an embedder-defined external provider.",
            ))
            .build()
            .into()
    }
}

/// LLM provider status
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum ProviderStatus {
    Active,
    Disabled,
}

/// LLM Provider entity (API keys never exposed)
/// Note: This is the entity struct, separate from the Provider trait in llm.rs
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct Provider {
    /// Prefixed public identifier. See [ID Schema](https://docs.everruns.com/advanced/id-schema/).
    #[cfg_attr(feature = "openapi", schema(value_type = String, example = "provider_01933b5a00007000800000000000001"))]
    pub id: ProviderId,
    /// Human-readable provider name. Safe to render in user-facing messages.
    pub name: String,
    /// Provider implementation type (OpenAI, Anthropic, Gemini, etc.).
    pub provider_type: DriverId,
    /// Custom base URL for self-hosted / proxied providers. `None` means use the provider's default endpoint.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub base_url: Option<String>,
    /// Whether an API key is configured. The key itself is never returned.
    pub api_key_set: bool,
    /// Current lifecycle status of this provider.
    pub status: ProviderStatus,
    /// Timestamp of the most recent successful model sync from the provider's API (RFC 3339).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub last_synced_at: Option<DateTime<Utc>>,
    /// Timestamp when this provider was created (RFC 3339).
    pub created_at: DateTime<Utc>,
    /// Timestamp when this provider was last updated (RFC 3339).
    pub updated_at: DateTime<Utc>,
}