Skip to content

[Feature]: refactor: Unify providers architecture and reqwest client management #5937

@NiuBlibing

Description

@NiuBlibing

Summary

refactor providers and reqwest builder

Problem statement

he current providers module suffers from inconsistent usage of reqwest and model construction parameters, with significant code duplication that leads to fragmented configuration. The main issues are:

  • Inconsistent reqwest usage: The project mixes reqwest::blocking and async modes. Each provider constructs its own client independently, with timeout, proxy, and header configuration scattered across the codebase. There is no unified entry point and no global client reuse.
  • Redundant provider constructors: Each provider exposes multiple semantic constructors (e.g. create_provider_with_xxx, create_provider_with_yyy), with inconsistent parameter passing. Adding a new field requires changes in many places.
  • Inconsistent base configuration: Support for user-customizable settings (timeout, vision capability, default max_tokens, etc.) varies from one provider to another — some support a given option, others don't. This makes the module hard to maintain and extend.

As a result, adding a new provider means reinventing the wheel, and adjusting shared behavior (e.g. unifying proxy or timeout policy) requires touching multiple files and is error-prone.

Proposed solution

1. Migrate reqwest blocking → async

  • Replace every remaining reqwest::blocking::Client in providers and its call chain with reqwest::Client (async).
  • Update callers to async fn + .await, using tokio::spawn / futures::try_join! where concurrency helps.
  • Remove implicit dependencies on a blocking runtime, eliminating the risk of nested blocking calls inside existing async contexts.

2. Unified provider configuration via a config struct + Default

Introduce a single provider configuration struct paired with the Default pattern:

#[derive(Debug, Clone)]
pub struct ProviderConfig {
    pub base_url: String,
    pub api_key: Option<String>,
    pub model: String,
    pub timeout: Duration,
    pub max_tokens: Option<u32>,
    pub supports_vision: bool,
    pub extra_headers: HeaderMap,
    // ...
}

impl Default for ProviderConfig { /* sensible defaults */ }

Associated changes:

  • All providers are constructed through a single Provider::new(config: ProviderConfig).
  • Remove semantic constructors such as create_provider_with_timeout, create_provider_with_vision, create_provider_with_proxy, etc. Callers override fields via ProviderConfig { timeout: ..., ..Default::default() }.
  • Lift shared capabilities (vision support, max_tokens, timeout, etc.) into the provider trait layer so that behavior is consistent across channels.

3. Unified reqwest client entry point

Introduce a new http_client module as the single entry point for reqwest clients across the entire project:

  • Provide an HttpClientBuilder supporting timeout, headers, proxy, user_agent, danger_accept_invalid_certs, etc.

  • Cache clients globally keyed by their configuration (e.g. OnceCell<HashMap<ClientKey, reqwest::Client>> or moka), so identical configs reuse the same client and connection pool instead of rebuilding one per request.

  • Expose a simple API, for example:

    let client = http_client::get_or_build(ClientConfig {
        timeout: Duration::from_secs(30),
        proxy: Some(proxy_url),
        ..Default::default()
    })?;
  • Replace every direct reqwest::Client::builder() call under providers with a call through this entry point.

Non-goals / out of scope

  • Extism-related code: The Extism plugin mechanism and its internal HTTP calls are not touched in this refactor.
  • Health check / liveness probes: Rewriting the probing logic is out of scope. It should only be kept compatible with the new client entry point without behavior changes.

Alternatives considered

No response

Acceptance criteria

No response

Architecture impact

No response

Risk and rollback

No response

Breaking change?

No

Data hygiene checks

  • I removed personal/sensitive data from examples, payloads, and logs.
  • I used neutral, project-focused wording and placeholders.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestpriority:p2Medium priorityproviderAuto scope: src/providers/** changed.risk: highAuto risk: security/runtime/gateway/tools/workflows.status:blockedBlocked on an external dependency, decision, or prerequisite.

    Type

    No type

    Projects

    Status

    Backlog

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions