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
Summary
refactor providers and reqwest builder
Problem statement
he current
providersmodule suffers from inconsistent usage ofreqwestand model construction parameters, with significant code duplication that leads to fragmented configuration. The main issues are:reqwest::blockingand 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.create_provider_with_xxx,create_provider_with_yyy), with inconsistent parameter passing. Adding a new field requires changes in many places.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
reqwest::blocking::Clientinprovidersand its call chain withreqwest::Client(async).async fn+.await, usingtokio::spawn/futures::try_join!where concurrency helps.2. Unified provider configuration via a config struct +
DefaultIntroduce a single provider configuration struct paired with the
Defaultpattern:Associated changes:
Provider::new(config: ProviderConfig).create_provider_with_timeout,create_provider_with_vision,create_provider_with_proxy, etc. Callers override fields viaProviderConfig { timeout: ..., ..Default::default() }.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_clientmodule as the single entry point for reqwest clients across the entire project:Provide an
HttpClientBuildersupportingtimeout,headers,proxy,user_agent,danger_accept_invalid_certs, etc.Cache clients globally keyed by their configuration (e.g.
OnceCell<HashMap<ClientKey, reqwest::Client>>ormoka), so identical configs reuse the same client and connection pool instead of rebuilding one per request.Expose a simple API, for example:
Replace every direct
reqwest::Client::builder()call underproviderswith a call through this entry point.Non-goals / out of scope
Alternatives considered
No response
Acceptance criteria
No response
Architecture impact
No response
Risk and rollback
No response
Breaking change?
No
Data hygiene checks