[1.5.4] — 2026-09-05
Added
- Request identity on every observability surface. A request can name the
end user and session it belongs to — the OpenAIuserfield, the
X-User-IDandX-Session-IDrequest headers, or the W3Cbaggage
entriesuser.id/session.id— and embedders can set request metadata
throughobservability.ContextWithRequestIdentity. The identity is
recorded asenduser.id,session.idandferro.request.metadata.<key>
on the request span; asUser,SessionIDandMetadataon
observability.RequestAttrs,EventandRoutingAttempt; and as
user_id/session_idon request-log rows (GET /admin/logs, request-log
schema migration 6). Previouslyuserreached the provider but no span,
event or log row, so nothing the gateway emitted could be grouped by user
or conversation. A request that states no identity records none; nothing is
inferred. Unknown JSON body fields are still accepted. observability.tracing.attempt_spans(defaultfalse) opens oneCLIENT
child span,gateway.routing.attempt, per routing-layer attempt — retries
and failovers included — carryingferro.routing.target_key,
ferro.routing.sequenceandferro.routing.outcome. The provider's HTTP
span nests beneath it on the unary surfaces; on a streamed request the
attempt span ends when the stream starts, so the HTTP span is its sibling
rather than its child. A trace still shows which targets were tried and in
what order without an exporter opting into attempt events. Backends plug in
through the optionalobservability.AttemptSpanProviderinterface; the
built-in OTLP provider implements it.- The
/v1/*pass-through and the fixed-target forwards (/v1/responses
create and its id sub-routes,/v1/files,/v1/batches) forward the W3C
traceparent(andtracestate) upstream, so a provider that records
traces joins the gateway's trace.observability.tracing.propagate_passthrough
(defaulttrue) governs both proxy paths; setting it tofalsestops the
injection on both. The inboundbaggage,X-User-IDandX-Session-ID
headers address the gateway rather than the provider, and an inbound
traceparent/tracestatebelongs to a trace the provider is not part of;
all five are stripped before forwarding on every pass-through and
fixed-target surface, regardless of this setting. A provider therefore
receives this gateway's trace context or none — never the caller's.
Changed
observability.RoutingAttemptandobservability.RequestAttrseach gain a
Metadatamap, alongsideUserandSessionID. Three consequences for
code that constructs or compares them: an unkeyed composite literal no
longer compiles, so use keyed fields; neither type can serve as a map key,
because a struct holding a map is not comparable; and==is likewise
unavailable, so compare withreflect.DeepEqual. Code that only reads their
fields is unaffected.
Fixed
- Provider HTTP clients no longer propagate inbound baggage upstream. The
tracing transport injects W3C trace context only, so theuser.idand
session.identries a caller may send — and that this release reads as
end-user identity — stay inside the gateway. Trace linkage is unchanged for
unary and streaming calls. - Browser clients on a configured allowed origin can send
X-User-ID,
X-Session-IDandbaggage. The identity headers this release introduces
were absent from the CORS allowlist, so a preflight blocked them. - The fixed-target forwards (
/v1/files,/v1/batches, the/v1/responses
id sub-routes) no longer hand a provider the caller's own trace context.
Those routes open no gateway span, so the context available to inject was
the one extracted from the caller's inbound headers; trace context is now
injected only when the gateway owns the span it describes. - The OpenAI body
userfield is bounded like the identity headers are —
trimmed, at most 256 bytes, no control characters — so a value that cannot
be an id is left out rather than stored on every request-log row and span. observability.RequestIdentity.Metadatais bounded to 32 entries of at most
128-byte keys and 256-byte values. An embedder forwarding caller-supplied
context verbatim could otherwise stamp unbounded span attributes, once per
routing attempt. What exceeds a limit is dropped deterministically.- An attempt span is ended, and its attempt-scoped context cancelled, when a
provider panics.observability.tracing.attempt_spanspreviously lost the
span for exactly the failed attempt it exists to show.
Contributors
Thanks to everyone who shipped this release:
- feat: v1.5.4 request identity (#431) — @MitulShah1
Full changelog: https://github.com/ferro-labs/ai-gateway/blob/v1.5.4/CHANGELOG.md