Completions
The OpenAI Chat Completions wire client you can point at any OpenAI-compatible server.
chat-completions is a wire crate for the OpenAI Chat Completions API
(POST /v1/chat/completions). It speaks that one format and nothing more, which
is exactly what makes it useful: a huge slice of the ecosystem implements the
same wire spec. Point it at Ollama, vLLM, llama.cpp's server, LiteLLM, Groq,
Together, Fireworks, your own gateway, or anything else that answers on
/v1/chat/completions.
It is also the foundation the vendor helpers build on. chat-ollama,
chat-deepseek, chat-cerebras, and chat-huggingface are thin crates that
wrap CompletionsBuilder and preset a base URL and an API key convention.
If you understand this page, you understand all of them.
Direct use
Build a client with CompletionsBuilder, hand it a base URL and a model,
then pass it straight to ChatBuilder::with_model.
use chat_completions::CompletionsBuilder;
use chat_core::{builder::ChatBuilder, types::messages, types::outcome::ChatOutcome};
let client = CompletionsBuilder::new()
.with_base_url("http://localhost:11434/v1")
.with_model("llama3")
.build();
let mut chat = ChatBuilder::new().with_model(client).build();
let mut msgs = messages::from_user(vec!["Say hello in one sentence."]);
if let ChatOutcome::Complete(res) = chat.complete(&mut msgs).await? {
println!("{:#?}", res.content);
}The builder uses type-state, so both pieces are required before build() is
available. with_base_url moves you into WithUrl, with_model into
WithModel, and only CompletionsBuilder<WithModel, WithUrl, _> exposes
build(). Leave either out and the code will not compile, which keeps a
half-configured client from ever existing.
With an optional API key
Many local servers need no auth at all, so the API key is optional. A common pattern is to add it only when an env var is present:
let base_url = std::env::var("CHAT_COMPLETIONS_BASE_URL")
.unwrap_or_else(|_| "http://localhost:11434/v1".to_string());
let model = std::env::var("CHAT_COMPLETIONS_MODEL")
.unwrap_or_else(|_| "llama3".to_string());
let mut builder = CompletionsBuilder::new()
.with_base_url(base_url)
.with_model(model);
if let Ok(api_key) = std::env::var("CHAT_COMPLETIONS_API_KEY") {
builder = builder.with_api_key(api_key);
}
let client = builder.build();When a key is set it goes out as an Authorization: Bearer <key> header. When
it is not, the request simply omits that header.
Builder methods
| Method | What it does |
|---|---|
with_base_url(impl AsRef<str>) | The server's base URL, e.g. http://localhost:11434/v1. The path is kept as a prefix and /chat/completions or /embeddings is appended. Required. |
with_model(impl Into<String>) | The model name to request. Required. |
with_api_key(impl Into<String>) | Bearer token. Omit it for no-auth servers. |
with_header(k, v) | Add an extra request header. Call it more than once for several. |
with_description(impl Into<String>) | A human-readable note attached to the provider metadata. |
with_metadata(key, value) | Stash arbitrary typed metadata on the provider. |
with_transport(T2) | Swap the HTTP transport. Defaults to ReqwestTransport. |
Capabilities
CompletionsClient implements CompletionProvider, so it plugs into the
normal chat loop with tools and structured output. Under the stream feature it
also implements the streaming provider, and it implements the embeddings
provider for servers that expose /embeddings.
chat-completions = { version = "0.2.4", features = ["stream"] }Embeddings hit the same base URL with /embeddings appended, so they work
wherever the server supports that endpoint:
let mut input = messages::from_user(vec!["embed me"]);
let vectors = client.embed(&mut input).await?;Building your own provider on top
A vendor helper is usually a small chat-<vendor> crate that wraps
CompletionsBuilder. The recipe is short:
- Preset the base URL (and read a host override from an env var if it makes sense).
- Preset the API key env var the vendor uses.
- Re-export a friendly builder, optionally adding vendor-specific niceties.
- Have
build()return aCompletionsClient, so all the chat, streaming, tool, and embedding logic stays in this crate.
chat-ollama is a good model to follow. Its OllamaBuilder defaults to
http://localhost:11434 (honoring OLLAMA_HOST), and at build time it hands the
work back to CompletionsBuilder:
let base_url = format!("{}://{}/v1", self.scheme, self.host);
let mut b = CompletionsBuilder::new()
.with_base_url(base_url)
.with_model(model)
.with_transport(transport);
if let Some(key) = self.api_key {
b = b.with_api_key(key);
}
b.build()On top of that it adds a .pull() helper that issues Ollama's native
POST /api/pull to download a model before the first request, then returns the
builder so it slots into the normal chain:
let client = OllamaBuilder::new()
.with_model("llama3.2")
.pull().await?
.build();That is the whole pattern: lean on chat-completions for the wire, and add only
the convenience that is specific to your vendor.
Custom transport
By default requests go out over reqwest. To use something else, hand
.with_transport(...) a value implementing the Transport trait. The pieces you
need are re-exported from the crate root: Transport, ReqwestTransport,
Request, Response, and TransportError.
use chat_completions::{CompletionsBuilder, Transport};
let client = CompletionsBuilder::new()
.with_base_url("http://localhost:8000/v1")
.with_model("my-model")
.with_transport(my_transport)
.build();This is handy for tests (return canned responses), for custom retry or logging layers, or for routing requests through a transport your environment requires.