Custom LLM Providers
This guide explains how to implement and use custom LLM providers in HyperInfer. We cover the core LlmProvider trait for developers extending the system and how to interact with providers via the Python client.
Conceptual Overview
HyperInfer uses a provider-based abstraction to decouple the core routing and telemetry logic from specific LLM implementations (like OpenAI, Anthropic, or local models).
The Request Lifecycle
- Client Request: A user sends a
ChatRequestthrough theRouter. - Routing: The
Routerselects a specificDeploymentbased on your routing strategy. - Provider Execution: The
Routerinvokes theLlmProviderassociated with that deployment. - Response: The provider translates the generic
ChatRequestinto a vendor-specific API call and returns aChatResponseor aStreamofChatChunks.
Implementing the LlmProvider Trait
The LlmProvider trait defines how the engine interacts with an LLM backend. This is the core interface you must implement when adding a new provider.
In Python, you generally consume providers by name. The high-level Client abstracts away the underlying implementation.
use async_trait::async_trait;
use hyperinfer_providers::LlmProvider;
use hyperinfer_core::{ChatRequest, ChatResponse, HyperInferError};
pub struct MyCustomProvider {
base_url: String,
}
impl MyCustomProvider {
pub fn new(base_url: impl Into<String>) -> Self {
Self { base_url: base_url.into() }
}
}
#[async_trait]
impl LlmProvider for MyCustomProvider {
fn name(&self) -> &str {
"my-custom-provider"
}
fn base_url(&self) -> &str {
&self.base_url
}
async fn chat(
&self,
request: &ChatRequest,
api_key: &str,
) -> Result<ChatResponse, HyperInferError> {
// Your logic to call the external API goes here
unimplemented!()
}
}
Handling Asynchronous Streams
Implementing streaming requires careful management of asynchronous streams to ensure they are thread-safe and pinned for the async executor.
Python uses native async generators (async for) to handle streams cleanly. The Client.stream method returns an AsyncIterator.
from hyperinfer import Client, Config
async def main():
client = Client(config=Config())
await client.init()
messages = [{"role": "user", "content": "Tell me a story."}]
# Stream chunks as they arrive
async for chunk in client.stream(
key="my-key",
model="my-custom-provider",
messages=messages
):
print(chunk.delta, end="", flush=True)
use futures::Stream;
use std::pin::Pin;
// ... inside impl LlmProvider for MyCustomProvider ...
fn stream(
&self,
_request: &ChatRequest,
_api_key: &str,
) -> Pin<Box<dyn Stream<Item = Result<ChatChunk, HyperInferError>> + Send + 'static>> {
// 1. Create a stream of chunks
let stream = async_stream::stream! {
yield Ok(ChatChunk { /* ... */ });
};
// 2. Box and Pin the stream for the trait requirement
Box::pin(stream)
}
Using Custom Endpoints
If you are running a local model that is OpenAI-compatible (e.g., Ollama, vLLM), you can route requests to it by using an alias that points to the local model name and configuring the appropriate API key.
from hyperinfer import Client, Config
# Configure the alias to point to your local model
# and provide the API key for the local server
config = (Config()
.with_api_key("openai", "ollama") # Local servers often accept any key
.with_alias("local-llama", "llama3")
)
client = Client(redis_url="redis://localhost:6379", config=config)
async def main():
await client.init()
response = await client.chat(
key="local-key",
model="local-llama",
messages=[{"role": "user", "content": "Explain quantum physics."}]
)
print(response["choices"][0]["message"]["content"])
if __name__ == "__main__":
import asyncio
asyncio.run(main())
Custom Provider Endpoints
Pointing a client at a non-default endpoint requires implementing a custom LlmProvider in Rust (with the desired base_url()) and registering it with the data plane. The Python client itself does not expose a base_url configuration option.
Comparison Table
| Feature | Rust (Implementation) | Python (Usage) |
|---|---|---|
| Goal | Define how a provider works | Consume a provider |
| Custom Logic | Implement LlmProvider trait |
Configure base_url |
| Async Model | Pin<Box<dyn Stream...>> |
AsyncIterator / async for |
| Configuration | Hardcoded in Trait or via API | Passed via Config object |