allama-http
The HTTP transport seam of allama's HTTP provider crates
(allama-anthropic, allama-gemini, allama-openai-compat,
allama-openai, allama-systemone).
Providers build an HttpRequest, send it through an HttpTransport, and
decode the HttpResponse; request mapping, error classification and stream
decoding never touch an HTTP client directly. That makes the transport
replaceable:
| Transport | Use |
|---|---|
ReqwestTransport (feature reqwest, default) |
What providers use unless told otherwise, with a 10 s connect timeout (DEFAULT_CONNECT_TIMEOUT) unless you pass your own client. reqwest-middleware sends through your reqwest-middleware client; http-tracing adds reqwest-tracing spans (which do not propagate trace context; see below). TLS: default-tls (default), rustls, native-tls, or rustls-ring (ring instead of aws-lc-rs, so static musl builds need no cmake). On wasm32-unknown-unknown it sends through the host's fetch |
ReplayTransport |
Serves canned responses in memory and records requests: run a real adapter over recorded wire data with no server, on any target |
| your own | Another HTTP stack, a proxy, recording |
use *;
use ;
async
Timeouts
| Bound | Default | Change it |
|---|---|---|
| Connect (TCP + TLS) | 10 s in ReqwestTransport::new (not on wasm32) |
the provider builder's http_transport(ReqwestTransport::from_client(...)) |
| Whole non-streaming call | 120 s (DEFAULT_REQUEST_TIMEOUT), whatever the transport, set per request by the provider crates |
the provider builder's timeout(...); timeout(None) removes it |
Stream headers (setup), gap between stream chunks (idle) |
120 s and 300 s (StreamTimeouts::default); none for the Ollama and vLLM presets |
the provider builder's stream_timeouts(...) |
The stream bounds are enforced above the transport, so they apply to any
HttpTransport. The idle default is long on purpose: reasoning models can
think for minutes before their first token, and the bound exists to catch
dead connections, not slow models. For tighter, workload-specific limits
wrap the provider in allama::wrappers::TimeoutProvider; whichever bound is
shorter fires first, and both fail with a retryable Error::Timeout.
Network defaults
What a broken or hostile endpoint can make a caller do is bounded:
- No redirects.
ReqwestTransport::newnever follows one:reqwestwould re-sendx-api-key,x-goog-api-key,api-keyand gateway credential headers (and, on a307/308, the prompt) to theLocationhost. The bundled providers report a3xxasError::UnexpectedResponsenaming that host (redirect_error). A client passed toReqwestTransport::from_clientkeeps its own policy: build it with.redirect(reqwest::redirect::Policy::none()). Onwasm32, the hostfetch's policy applies. - Bounded bodies.
HttpResponse::bytes_limitedreads a success body under the builder'smax_response_size(DEFAULT_MAX_RESPONSE_SIZE, 256 MiB);HttpResponse::map_errorreads at most 64 KiB of an error body within 30 s and records a failed read on the error. A stream is bounded per event (max_stream_event_size) and in time (the stream timeouts), not in total bytes: its length is the answer's, whichmax_output_tokensbounds, and a byte cap would cut off a legitimate long answer mid-stream. - Checked configuration.
config::base_url,config::secretandconfig::static_headersrefuse malformed base URLs, keys no header can carry, and credential headers among a builder'sheader(..)settings, atbuild(), with aConfigurationerror naming the setting. - A
User-Agent. The bundled providers sendallama/<version>(DEFAULT_USER_AGENT) unlessheader("user-agent", ..)replaces it.
Versioning
This crate names these third-party crates in its public API, so a new
major (pre-1.0: minor) version of any of them is a breaking release of
allama-http:
bytes1:Bytes, re-exported, carries request and response bodies;futures-core0.3: theBodystream and the framers' stream bounds;serdeandserde_json1: the JSON form ofHttpRequestOptions,ReplayResponse::json_value, and the JSON the error and framing helpers read;reqwest0.13 (featurereqwest):ReqwestTransport::from_client;reqwest-middleware0.5 (featurereqwest-middleware):ReqwestTransport::from_middleware.
Provider crates take a client only as
http_transport(ReqwestTransport::from_client(..)), so reqwest appears
in no other public signature.
The allama crates are released together, at one version, for now, so a
breaking release of allama-http is today one of every allama crate.
That is how 0.1 is released, not a promise about every later release. The
reason is HttpRequestOptions: options are found by their type, so with
allama-http at two versions over one allama, an adapter built on one
version reads a different type than the one attached with the other. The
call fails with Error::InvalidRequest, naming the allama-http
namespace and both types, instead of sending the configured key without
the tenant's credential and headers; with one version for every crate,
the same mix is a compile error.
Per-request headers and credentials
HttpRequestOptions sends extra HTTP headers and credentials with one
request: gateway tags (tenant, route, trace) and per-tenant keys. Attach it
to a chat, embedding or decision request like any options; every bundled
HTTP adapter applies it with HttpRequest::apply_options.
use ChatRequest;
use ;
let request = new.user.with_option;
A plain header replaces the adapter's header of the same name, and others
are added. A credential is addressed to a credential scope, the account
it belongs to, and only the adapter with that scope sends it: in a fallback
chain from OpenAI to Anthropic, an OpenAI-scoped key never reaches
Anthropic. Credential::api_key replaces the adapter's configured key in
the place its API expects; Credential::header is any other auth header,
such as a gateway token (cf-aig-authorization). Bundled adapters default
their scope to their provider name, publish it as DEFAULT_CREDENTIAL_SCOPE,
and change it with their builder's credential_scope; two accounts of one
provider need two scopes. A builder given a scope and no key requires a
credential on every request (bring your own key).
A plain header that would carry a credential (authorization, x-api-key,
cf-aig-authorization, ... see is_credential_header, or the adapter's own
key header) is refused with Error::Configuration before anything is sent.
Credentials never leave the process through the option: Debug redacts
them, serialization omits them (so recorded exchanges never hold them), and
the JSON form (with_options_json("allama-http", ..)) rejects them.
Third-party HTTP adapters declare a CredentialSlot (scope, placement:
Bearer, Header(name) or Custom, and the configured headers it
replaces), call apply_options(&request.options, &slot) after setting their
own headers, and run allama_conformance::assert_transport_options_applied.
Trace context propagation
allama-http sends no trace context of its own and depends on no
OpenTelemetry crate. To continue the application's trace in a gateway
(LiteLLM proxy, vLLM, an instrumented internal gateway), wrap the
transport in an HttpTransport that injects the current span's W3C
traceparent into HttpRequest::headers before it sends, with your own
OpenTelemetry version. Adapters call apply_options before send, so the
wrapper sees the final headers and can keep a traceparent the caller
set. The
observability guide
has the recipe, for provider builders and for the provider registry, and
the trace_context example in allama-examples runs it.
Secrets in logs
Debug output of Headers, HttpRequest and HttpResponse redacts
credential headers (authorization, x-api-key, cookie, … and any name
containing key, token, secret or auth) and every credential
apply_options placed; is_sensitive_header is the rule, for provider
crates that print their own configuration. It only redacts: a plain
Idempotency-Key header is still sent.
Error helpers
Every HTTP provider, bundled or not, shares these:
error_context(status, &headers)reads an error response's status, request id (x-request-idorrequest-id), retry hint (Retry-After,retry-after-ms) and server retry directive (x-should-retry) into anErrorContext. Start the error from it, then mark the message authored.error_envelope(body)reads the common JSON error envelopes (OpenAI, which current vLLM, Ollama/v1and Anthropic also send; legacy vLLM; native Ollama).ErrorEnvelope::applyattaches the provider's message as the error's upstream message, so it stays out of the redactedError::as_log(); a body that is no envelope stays only in the capped body.error_envelope_value(&value)reads a body already parsed as JSON (an NDJSON line, an SSE event's data).TransportError::into_errormaps a request that got no response. Its message names the failure; the transport's own text, which can contain the request URL, is the upstream message.ReqwestTransportalso drops the query string (some APIs put the key there) from its error text.TransportError::into_stream_errormaps a streamed body that could not be read to the end the same way: a timeout staysError::Timeout, anything else is a retryable transport stream error.
Streaming helpers
A streamed response arrives in frames of its wire framing. A framer turns the body into frames, and one driver runs the adapter's decoder over them, whatever the framing:
SseStream::new(response.into_body())(modulesse) yields the body's SSE events (event,data,id). It handlesLF,CRandCRLFline endings (also split across chunks), multi-linedata:fields, a byte order mark, comments andretry:lines, scanning each byte once. A failed read stays the body's own error (SseError::Transport), so a stream timeout stays a timeout. A line or an event's data past the size cap (64 MiB by default,max_event_size, which provider builders expose asmax_stream_event_size) fails withSseError::EventTooLarge, a stream protocol error, not a retryable one. A line that is not UTF-8 fails withSseError::InvalidUtf8, where the WHATWG standard would replace the bytes with U+FFFD: a replaced byte would silently change the JSON an adapter decodes. It is a stream decode error, not a retryable one either.SseError::into_errormaps all three.SseDecoderis the same decoder without the stream.NdjsonStream::new(response.into_body())(modulendjson) yields the body's newline-delimited JSON lines, with the same rules where they apply:CRLFendings (also split across chunks), blank lines skipped, a last line without its line ending kept, a size cap (max_line_size, 64 MiB by default,NdjsonError::LineTooLarge), and invalid UTF-8 failing (NdjsonError::InvalidUtf8).NdjsonDecoderis the same framer without the stream.FramedChatStream::new(frames, decoder)(moduleframing) drives an adapter'sChatFrameDecoder(frame,end,fail,is_done) over any stream of frames: the stream ends after its first error, the end-of-body or failure hook runs at most once, a failure is classified with the framer's error conversion (SseError::into_error,NdjsonError::into_error), and the body is never polled after the stream ended.with_error_maptakes a body whose error is not aTransportError.SseChatStreamis its SSE form, a type alias forFramedChatStream<SseStream<_>, _>driving aChatFrameDecoder<SseEvent>.VendorFrames::surface(event, data)(moduleframing, also insse) turns a frame of a type the adapter does not model into response-level provider data under its namespace (that frame's payload), with one warning per frame type. Yield it as it comes:Prepared::finish_streamholds what arrives beforeResponseStartuntil it.finish(), yielded just beforeResponseStop, carries the whole array of each type that repeated, so each frame costs constant work.
This is a seam for provider crates, not middleware for applications: it
carries bytes, the shared error and streaming helpers, and transport options, and
the portable allama types stay above it. See ADR 0009.
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.