smithery/majiayu000

provider-architecture

LiteLLM-RS provider system in two tiers - data-driven OpenAI-compatible catalog entries auto-routed through OpenAILikeProvider, plus code-based provider modules implementing the LLMProvider trait behind the closed Provider enum.

Installation

$ npx skills add smithery/majiayu000 --skill provider-architecture

Summary

  • LiteLLM-RS provider system in two tiers - data-driven OpenAI-compatible catalog entries auto-routed through OpenAILikeProvider, plus code-based provider modules implementing the LLMProvider trait behind the closed Provider enum.
  • Covers unified ProviderError handling, connection pooling, capabilities, and model metadata.
  • Use when adding a new provider, editing registry/catalog.rs, or implementing/debugging the LLMProvider trait.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery/majiayu000 · top by installs.

npx skills add smithery/majiayu000

Browse all from smithery/majiayu000

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 19,546 B
  • docs SUMMARY.md 213 B

History

  1. First recorded snapshot · 0 installs

SKILL.md

Provider Architecture Guide

Architecture Overview

Providers come in two tiers behind one implementation trait:

Tier 1 — Catalog-only (zero code). An OpenAI-compatible endpoint that differs only in base URL, auth env var, and advertised capabilities/models. It has no Rust module: one static entry in src/core/providers/registry/catalog.rs fully describes it, and the factory builds an OpenAILikeProvider from that data at runtime. This is the primary path for new integrations.

Tier 2 — Code-based. A provider needing custom request/response transformation, custom auth signing, non-standard streaming, or rich model metadata lives in src/core/providers/<name>/, implements LLMProvider, and is wired into routing.

Routing does not use trait objects. Router deployments store the closed Provider enum (src/core/providers/mod.rs), which dispatches to concrete provider structs. LLMProvider (src/core/traits/provider/llmprovider/traitdefinition.rs) is the interface every variant implements — implementing the trait alone does not make a provider routeable; enum variant, dispatch arm, and factory wiring are crate-level changes. Trait objects appear only at the edges: the error mapper (Box<dyn ErrorMapper<ProviderError>>) and the streaming return type (Pin<Box<dyn Stream<Item = Result<ChatChunk, ProviderError>> + Send>>).

Enumerating Current Providers

Do not rely on memorized counts — they change often. Count from source:

# Tier 1: one def_chat()/def_local_chat() call per entry
# (each count includes the helper fn definition itself, so subtract 1)
grep -c 'def_chat(' src/core/providers/registry/catalog.rs
grep -c 'def_local_chat(' src/core/providers/registry/catalog.rs

# Tier 2: code-based provider modules (base/factory/macros/registry are infrastructure)
ls -d src/core/providers/*/ | grep -vE '/(base|factory|macros|registry)/'

Adding a Tier 1 Provider (Primary Path)

Two edits, nothing else:

// src/core/providers/registry/catalog.rs
def_chat(
    "myprovider",
    "My Provider",
    "https://api.myprovider.com/v1",
    "MYPROVIDER_API_KEY",
),
// src/core/providers/mod.rs — annotation comment alongside the other Tier 1 notes
// myprovider: Tier 1 -> registry/catalog.rs

At runtime createprovider (src/core/providers/factory/mod.rs) matches the selector against the catalog, builds an OpenAILikeConfig via ProviderDefinition::toopenailikeconfig, and constructs openailike::OpenAILikeProvider::newforcatalog(oaiconfig, def.capabilities) into the Provider::OpenAILike variant.

Keyless local servers use deflocalchat (AuthType::None, skipapikey = true):

def_local_chat("myrunner", "My Runner", "http://localhost:1234/v1"),

Each entry is a ProviderDefinition (src/core/providers/registry/definition.rs). Defaults from def_chat can be overridden with struct-update syntax:

  • alternateauthenvvars — env vars checked after authenv_var (see together)
  • model_prefix: Some("xai/") — selector/model prefix stripping (see xai)
  • custom capabilities profile replacing the default

OPENAILIKECATALOG_CAPABILITIES (ChatCompletion, ChatCompletionStream, ToolCalling, FunctionCalling)

  • name aliases resolve via canonicalcatalogname() in catalog.rs (e.g.

"zhipuai" -> "zhipu")

Registry API (src/core/providers/registry/, re-exported in registry/mod.rs): istier1provider(name), getdefinition(name), canonicalcatalogname(name), PROVIDERCATALOG.


Core Trait Definition: LLMProvider

There are no associated types. Every fallible method returns the unified ProviderError directly.

// src/core/traits/provider/llm_provider/trait_definition.rs
pub trait LLMProvider: Send + Sync + Debug + 'static {
    // ===== Required =====
    fn name(&self) -> &str;
    fn capabilities(&self) -> &'static [ProviderCapability];
    fn models(&self) -> &[ModelInfo];

    fn get_supported_openai_params(&self, model: &str) -> &'static [&'static str];
    async fn map_openai_params(
        &self,
        params: HashMap<String, Value>,
        model: &str,
    ) -> Result<HashMap<String, Value>, ProviderError>;
    async fn transform_request(
        &self,
        request: ChatRequest,
        context: RequestContext,
    ) -> Result<Value, ProviderError>;
    async fn transform_response(
        &self,
        raw_response: &[u8],
        model: &str,
        request_id: &str,
    ) -> Result<ChatResponse, ProviderError>;
    fn get_error_mapper(&self) -> Box<dyn ErrorMapper<ProviderError>>;
    async fn chat_completion(
        &self,
        request: ChatRequest,
        context: RequestContext,
    ) -> Result<ChatResponse, ProviderError>;
    async fn health_check(&self) -> HealthStatus;
    async fn calculate_cost(
        &self,
        model: &str,
        input_tokens: u32,
        output_tokens: u32,
    ) -> Result<f64, ProviderError>;

    // ===== Provided (default = not_supported / trivial) =====
    fn error_provider_name(&self) -> &'static str { "provider" }
    fn supports_capability(&self, capability: &ProviderCapability) -> bool {
        self.capabilities().contains(capability)
    }
    fn supports_model(&self, model: &str) -> bool {
        self.models().iter().any(|m| m.id == model)
    }
    // supports_tools / supports_streaming / supports_embeddings /
    // supports_image_generation delegate to supports_capability();
    // supports_vision() currently returns false.
    async fn chat_completion_stream(&self, request: ChatRequest, context: RequestContext)
        -> Result<Pin<Box<dyn Stream<Item = Result<ChatChunk, ProviderError>> + Send>>, ProviderError>;
    async fn embeddings(&self, request: EmbeddingRequest, context: RequestContext)
        -> Result<EmbeddingResponse, ProviderError>;
    async fn image_generation(&self, request: ImageGenerationRequest, context: RequestContext)
        -> Result<ImageGenerationResponse, ProviderError>;
    async fn audio_transcription(&self, request: TranscriptionRequest, context: RequestContext)
        -> Result<TranscriptionResponse, ProviderError>;
    async fn audio_translation(&self, request: TranslationRequest, context: RequestContext)
        -> Result<TranslationResponse, ProviderError>;
    async fn text_to_speech(&self, request: SpeechRequest, context: RequestContext)
        -> Result<SpeechResponse, ProviderError>;
    async fn get_average_latency(&self) -> Result<std::time::Duration, ProviderError>; // default 100ms
    async fn get_success_rate(&self) -> Result<f32, ProviderError>;                   // default 0.99
    async fn estimate_tokens(&self, text: &str) -> Result<u32, ProviderError>;        // len()/4
}

Optional dispatch methods (streaming, embeddings, images, audio) must still be gated by the matching ProviderCapability — route selection checks supports_capability() before calling them.


Tier 2: Code-Based Provider Pattern

Directory Structure

Real examples: src/core/providers/cloudflare/ (small) and src/core/providers/openai_like/.

src/core/providers/my_provider/
├── mod.rs           # Module exports, LLMProvider impl may live here too
├── config.rs        # Config struct (often via define_provider_config!)
├── provider.rs      # Provider struct + LLMProvider impl
├── model_info.rs    # Static model metadata
├── streaming.rs     # SSE parsing (optional)
└── error.rs         # Error helpers/mappers (optional; legacy name = ProviderError alias)

Configuration

BaseConfig (src/core/providers/base/config.rs) carries apikey, apibase, endpointaccess, timeout (secs), maxretries, headers, organization, apiversion, with env fallbacks ({PROVIDER}APIKEY, {PROVIDER}APIBASE, ...) via fromenv(provider) / for_provider(provider).

Implement the ProviderConfig trait (src/core/traits/provider/config.rs) — note the accessor names differ from BaseConfig's helpers:

pub trait ProviderConfig: Send + Sync + Clone + Debug + 'static {
    fn validate(&self) -> Result<(), String>;
    fn api_key(&self) -> Option<&str>;
    fn api_base(&self) -> Option<&str>;
    fn timeout(&self) -> std::time::Duration;
    fn max_retries(&self) -> u32;
    fn endpoint_access(&self) -> ProviderEndpointAccess { ProviderEndpointAccess::PublicOnly }
    fn use_ssrf_safe_client(&self) -> bool { false }
    // validate_standard(provider_name): shared key/timeout/retries checks
}

The defineproviderconfig! macro (exported from base/config.rs) generates the struct, builders (withapikey, withbaseurl, withtimeout), fromenv(), getapikey()/getapibase(), and the ProviderConfig impl in one call.

Provider Implementation

Modeled on src/core/providers/cloudflare/provider.rs:

use std::sync::Arc;
use crate::core::providers::base::{header, BaseConfig, GlobalPoolManager, HttpMethod};
use crate::core::providers::ProviderError;
use crate::core::traits::error_mapper::trait_def::ErrorMapper;
use crate::core::traits::error_mapper::DefaultErrorMapper;
use crate::core::traits::provider::llm_provider::trait_definition::LLMProvider;
use crate::core::traits::provider::ProviderConfig;

pub struct MyProvider {
    config: MyProviderConfig,
    pool_manager: Arc<GlobalPoolManager>,
    models: Vec<ModelInfo>,
}

impl MyProvider {
    pub fn new(config: MyProviderConfig) -> Result<Self, ProviderError> {
        config.validate()
            .map_err(|e| ProviderError::configuration(PROVIDER_NAME, e))?;
        let http_config = BaseConfig {
            api_key: config.api_key().map(str::to_owned),
            api_base: config.api_base().map(str::to_owned),
            endpoint_access: config.endpoint_access(),
            timeout: config.timeout().as_secs(),
            max_retries: config.max_retries(),
            ..BaseConfig::default()
        };
        Ok(Self {
            config,
            pool_manager: Arc::new(GlobalPoolManager::new_for_provider(
                PROVIDER_NAME,
                http_config,
            )?),
            models: load_models(),
        })
    }
}

impl LLMProvider for MyProvider {
    fn name(&self) -> &'static str { PROVIDER_NAME }
    fn error_provider_name(&self) -> &'static str { PROVIDER_NAME }

    async fn chat_completion(
        &self,
        request: ChatRequest,
        context: RequestContext,
    ) -> Result<ChatResponse, ProviderError> {
        let api_key = self.config.api_key()
            .ok_or_else(|| ProviderError::authentication(PROVIDER_NAME, "API key required"))?;
        let mut headers = vec![header("Authorization", format!("Bearer {api_key}"))];
        headers.push(header("Content-Type", "application/json".to_string()));

        // url/model/request_id derived from config + request (elided)
        let body = self.transform_request(request, context).await?;
        let response = self.pool_manager
            .execute_request(&url, HttpMethod::POST, headers, Some(body))
            .await?;

        let status = response.status();
        if !status.is_success() {
            let body_text = response.text().await
                .map_err(|e| ProviderError::network(PROVIDER_NAME, e.to_string()))?;
            return Err(self.get_error_mapper().map_http_error(status.as_u16(), &body_text));
        }
        let raw = response.bytes().await
            .map_err(|e| ProviderError::network(PROVIDER_NAME, e.to_string()))?;
        self.transform_response(&raw, &model, &request_id).await
    }

    fn get_error_mapper(&self) -> Box<dyn ErrorMapper<ProviderError>> {
        Box::new(DefaultErrorMapper)
    }
    // ... remaining required methods
}

Registration (routing) Requirements

Adding a Tier 2 provider means crate-level wiring, in this order of authority:

  1. Add src/core/providers/<name>/, its module declaration, and a closed Provider

enum variant under the same feature gate in src/core/providers/mod.rs.

  1. Add the variant to all four dispatch_provider! @expand arms (sync,

asyncerr, value, asyncdirect) and to Provider::name() and provider_type().

  1. Add a ProviderType variant and allnoncustomprovidertypes() entry in

provider_type.rs.

  1. Add canonical name, aliases, catalog_backed and the correct

ProviderDispatchKind to PROVIDERTYPEREGISTRY in registry/types.rs. For a feature-gated native implementation, use or add a cfg-sensitive dispatch-kind helper that reports the correct enabled and disabled modes; registry entries themselves do not have a feature field.

  1. Add the config builder in factory/builder.rs and match branch in

factory/registry.rs, with matching feature gates. Keep cfg gates synchronized across module, Provider, dispatch and factory wiring.

  1. Update provider-type/registry lifecycle, factory support and feature-on/off

tests so aliases, support state and construction stay in sync.


Connection Pooling

The recommended newforprovider path runs requests through BaseHttpClient and ProviderHttpClient. ProviderHttpClient keeps a process-wide client cache keyed by endpoint policy, request timeout, and mode (ordinary, streaming, or no-redirect). Those clients use HttpClientPoolConfig::default() (src/utils/net/http.rs): 100 maximum idle connections per host, 90-second idle timeout, 10-second connect timeout, and 60-second TCP keepalive.

PoolConfig in src/core/providers/base/connection_pool.rs (there is no pool.rs) instead configures the legacy global ConnectionPool used by the unbound new() / shared() path. These constants do not tune the policy-bound clients above:

pub struct PoolConfig;
impl PoolConfig {
    pub const TIMEOUT_SECS: u64 = 600;
    pub const POOL_SIZE: usize = 80;      // pool_max_idle_per_host
    pub const KEEPALIVE_SECS: u64 = 90;   // pool_idle_timeout
}
// Provider implementations use new_for_provider(provider, BaseConfig) so
// endpoint-access and timeout policy are installed. new()/shared() omit that policy.
pub async fn execute_request(
    &self,
    url: &str,
    method: HttpMethod,               // GET | POST | PUT | DELETE
    headers: Vec<HeaderPair>,         // (Cow<'static, str>, Cow<'static, str>)
    body: Option<serde_json::Value>,  // serialized as JSON; not generic
) -> Result<reqwest::Response, ProviderError>;

pub async fn execute_streaming_request(
    &self,
    url: &str,
    headers: Vec<HeaderPair>,
    body: serde_json::Value,
    legacy_provider: &'static str,
) -> Result<reqwest::Response, ProviderError>;

Build header pairs zero-copy where possible: header(key, value) (static key, owned value), headerstatic(key, value), headerowned(key, value). Streaming callers get a separate client without a total-body timeout via streamingunboundedclient(); header phase bounded by STREAMINGHEADERTIMEOUTSECS, error bodies by readstreamingerrorbody (10s / 64KiB caps).


Model Information

ModelInfo (src/core/types/model.rs) is a plain serializable struct with Default:

pub struct ModelInfo {
    pub id: String,
    pub name: String,
    pub provider: String,
    pub max_context_length: u32,
    pub max_output_length: Option<u32>,
    pub supports_streaming: bool,
    pub supports_tools: bool,
    pub supports_multimodal: bool,
    pub input_cost_per_1k_tokens: Option<f64>,
    pub output_cost_per_1k_tokens: Option<f64>,
    pub currency: String,
    pub capabilities: Vec<ProviderCapability>,
    pub created_at: Option<SystemTime>,
    pub updated_at: Option<SystemTime>,
    pub metadata: HashMap<String, serde_json::Value>,
}

Costs are per 1K tokens (catalog pricing constants in registry/catalog.rs are per million and divided by 1000 when converted). Return &self.models from LLMProvider::models(); supportsmodel() scans that slice.


Provider Capabilities

ProviderCapability (src/core/types/model.rs) — the full current variant list:

pub enum ProviderCapability {
    ChatCompletion,
    ChatCompletionStream,
    Embeddings,
    ImageGeneration,
    ImageEdit,
    ImageVariation,
    AudioTranscription,
    AudioTranslation,
    TextToSpeech,
    Moderation,
    Rerank,
    ToolCalling,
    FunctionCalling,
    CodeExecution,
    FileUpload,
    FineTuning,
    BatchProcessing,
    RealtimeApi,
    GeminiGenerateContent,
}

Return a static slice (a temporary array literal cannot back &'static [...]):

const MY_CAPABILITIES: &[ProviderCapability] = &[
    ProviderCapability::ChatCompletion,
    ProviderCapability::ChatCompletionStream,
    ProviderCapability::ToolCalling,
];

fn capabilities(&self) -> &'static [ProviderCapability] {
    MY_CAPABILITIES
}

Unified Error Handling

ProviderError (src/core/providers/unifiedprovidererror.rs, re-exported as crate::core::providers::ProviderError and unifiedprovider::ProviderError) is the single error type across the trait. Construct with factory methods (src/core/providers/unifiedprovider_methods.rs) rather than struct literals — some variants carry extra optional fields (e.g. RateLimit rpm/tpm limits):

ProviderError::authentication(provider, msg)
ProviderError::rate_limit(provider, retry_after: Option<u64>)
ProviderError::rate_limit_with_retry(provider, msg, retry_after)
ProviderError::model_not_found(provider, model)
ProviderError::invalid_request(provider, msg)
ProviderError::network(provider, msg)
ProviderError::timeout(provider, msg)
ProviderError::api_error(provider, status: u16, msg)
ProviderError::provider_unavailable(provider, msg)
ProviderError::not_supported(provider, feature)
ProviderError::configuration(provider, msg)
ProviderError::serialization(provider, msg)

ErrorMapper<E> (src/core/traits/errormapper/traitdef.rs) converts HTTP statuses and JSON error bodies into errors: required maphttperror(u16, &str); defaulted mapjsonerror, mapnetworkerror, mapparsingerror, maptimeouterror. Ready-made mappers:

  • GenericErrorMapper — core::traits::error_mapper::types, aliased

DefaultErrorMapper at core::traits::errormapper (what most providers return from geterror_mapper())

  • OpenAIErrorMapper, AnthropicErrorMapper — core::traits::error_mapper::implementations

Legacy per-provider error enums were removed; surviving names like AnthropicError or GeminiError are pub type ... = ProviderError aliases (see [reference/migration-from-legacy-errors.md](reference/migration-from-legacy-errors.md)).


References

  • [reference/best-practices-and-checklist.md](reference/best-practices-and-checklist.md) — error factory conventions, coding practices, and new-provider checklist (Tier 1 and Tier 2)
  • [reference/migration-from-legacy-errors.md](reference/migration-from-legacy-errors.md) — how legacy per-provider error types map onto unified ProviderError