Skip to main content

llmshim/
lib.rs

1pub mod breaker;
2pub mod cache;
3pub mod client;
4pub mod schema;
5pub mod shim;
6/// Offline-first model catalog, also available as the standalone `llmshim-catalog` crate.
7pub use llmshim_catalog as catalog;
8pub mod config;
9pub mod cost;
10pub mod credentials;
11mod default_secret_file;
12mod derived_response;
13pub mod env;
14pub mod error;
15pub mod fallback;
16mod json_bounds;
17pub mod log;
18pub mod models;
19pub mod policy;
20pub mod provider;
21pub mod providers;
22pub mod reasoning;
23#[cfg(feature = "redis-coordination")]
24mod redis_operation;
25pub mod router;
26mod sse;
27mod stream_retention;
28pub mod streaming;
29pub mod toolcall;
30pub mod usage;
31pub mod vision;
32
33#[cfg(feature = "proxy")]
34pub mod proxy;
35
36#[cfg(feature = "gateway")]
37pub mod gateway;
38
39use client::ShimClient;
40use error::Result;
41pub use fallback::{completion_with_fallback, completion_with_fallback_and_policy, FallbackConfig};
42use log::{LogEntry, Logger, RequestTimer};
43use policy::DispatchPolicyContext;
44use router::Router;
45use serde_json::Value;
46
47use futures::Stream;
48use std::pin::Pin;
49use std::sync::LazyLock;
50
51/// Shared HTTP client — reuses connection pool across all requests.
52/// Shared HTTP client with connection pooling.
53pub static SHARED_CLIENT: LazyLock<ShimClient> = LazyLock::new(ShimClient::new);
54
55/// Pre-establish TCP+TLS connections to all configured provider endpoints.
56/// Call once after creating the Router to eliminate cold-start latency on first request.
57pub async fn warmup(router: &Router) {
58    let urls: Vec<&str> = router
59        .provider_keys()
60        .iter()
61        .filter_map(|name| match *name {
62            "openai" => Some("https://api.openai.com"),
63            "anthropic" => Some("https://api.anthropic.com"),
64            "gemini" => Some("https://generativelanguage.googleapis.com"),
65            "xai" => Some("https://api.x.ai"),
66            _ => None,
67        })
68        .collect();
69    SHARED_CLIENT.warmup(&urls).await;
70}
71
72/// Top-level entry point. Resolves the provider from the model string and fires the request.
73pub async fn completion(router: &Router, request: &Value) -> Result<Value> {
74    completion_with_logger(router, request, None).await
75}
76
77pub async fn completion_with_policy(
78    router: &Router,
79    request: &Value,
80    policy_context: &DispatchPolicyContext,
81) -> Result<Value> {
82    completion_with_logger_and_policy(router, request, None, policy_context).await
83}
84
85/// Completion with optional logging.
86pub async fn completion_with_logger(
87    router: &Router,
88    request: &Value,
89    logger: Option<&Logger>,
90) -> Result<Value> {
91    completion_inner(router, request, logger, None).await
92}
93
94pub async fn completion_with_logger_and_policy(
95    router: &Router,
96    request: &Value,
97    logger: Option<&Logger>,
98    policy_context: &DispatchPolicyContext,
99) -> Result<Value> {
100    completion_inner(router, request, logger, Some(policy_context)).await
101}
102
103async fn completion_inner(
104    router: &Router,
105    request: &Value,
106    logger: Option<&Logger>,
107    policy_context: Option<&DispatchPolicyContext>,
108) -> Result<Value> {
109    // A named route resolves to its model and settings before dispatch.
110    let request = router.expand_route(request)?;
111    let request = request.as_ref();
112    let model_str = request
113        .get("model")
114        .and_then(|m| m.as_str())
115        .ok_or(error::ShimError::MissingModel)?;
116
117    let (provider, model) = router.resolve(model_str)?;
118    let client = bound_client(router);
119    let timer = RequestTimer::start();
120
121    // Ordinary traffic feeds provider health too, so a chain's first fallback
122    // decision is not the first thing that ever noticed a provider is down.
123    // The client does the counting; see `ShimClient::with_breaker`.
124    let result = match policy_context {
125        Some(context) => {
126            client
127                .completion_with_policy(provider, &model, request, context)
128                .await
129        }
130        None => client.completion(provider, &model, request).await,
131    };
132
133    match result {
134        Ok(resp) => {
135            if let Some(logger) = logger {
136                logger.log(&LogEntry::from_response(
137                    provider.name(),
138                    model_str,
139                    &resp,
140                    timer.elapsed(),
141                ));
142            }
143            Ok(resp)
144        }
145        Err(e) => {
146            if let Some(logger) = logger {
147                logger.log(&LogEntry::from_error(
148                    provider.name(),
149                    model_str,
150                    &e.to_string(),
151                    timer.elapsed(),
152                ));
153            }
154            Err(e)
155        }
156    }
157}
158
159/// Streaming entry point. Returns an SSE stream of OpenAI-format chunks.
160pub async fn stream(
161    router: &Router,
162    request: &Value,
163) -> Result<Pin<Box<dyn Stream<Item = Result<String>> + Send>>> {
164    stream_inner(router, request, None).await
165}
166
167pub async fn stream_with_policy(
168    router: &Router,
169    request: &Value,
170    policy_context: &DispatchPolicyContext,
171) -> Result<Pin<Box<dyn Stream<Item = Result<String>> + Send>>> {
172    stream_inner(router, request, Some(policy_context)).await
173}
174
175async fn stream_inner(
176    router: &Router,
177    request: &Value,
178    policy_context: Option<&DispatchPolicyContext>,
179) -> Result<Pin<Box<dyn Stream<Item = Result<String>> + Send>>> {
180    let request = router.expand_route(request)?;
181    let request = request.as_ref();
182    let model_str = request
183        .get("model")
184        .and_then(|m| m.as_str())
185        .ok_or(error::ShimError::MissingModel)?;
186
187    let (provider, model) = router.resolve_owned(model_str)?;
188    // The breaker observes a single target without refusing it. An explicit
189    // dispatch policy still gates every actual stream-open attempt below.
190    match policy_context {
191        Some(context) => {
192            bound_client(router)
193                .stream_owned_with_policy(provider, &model, request, context)
194                .await
195        }
196        None => {
197            bound_client(router)
198                .stream_owned(provider, &model, request)
199                .await
200        }
201    }
202}
203
204/// The shared HTTP client, reporting to this router's breaker. The pool is
205/// shared by clone; only the breaker handle is per call.
206pub(crate) fn bound_client(router: &Router) -> ShimClient {
207    SHARED_CLIENT.clone().with_breaker(router.breaker().clone())
208}