ADR-0012: Inline forwarder and streaming
Status
Accepted
Context
Inline mode (routed serve --mode inline --upstream <gateway>) is an
OpenAI-compatible HTTP hop in front of exactly one gateway. It must rewrite the
model field and headers without ever buffering or altering the upstream
response, including server-sent events, and it must fail closed on bodies it
cannot parse.
Decision
- Server:
axum0.8 onhyper1. One HTTP listener serves the proxy paths,POST /v1/decide,POST /v1/feedback,/healthz,/readyzand/metrics. - Routed paths:
POST /v1/chat/completions,/v1/completions,/v1/embeddings,/v1/messages,/v1/responses. Every other path and method is forwarded verbatim (headers minus hop-by-hop and inboundx-routed-*, body streamed). - Request side:
DefaultBodyLimit(default 10 MiB, configurable) is enforced before any parsing; the body is parsed as a top-levelMap<String, Box<RawValue>>so onlymodeland the injected parameters are rewritten and every other field is forwarded byte for byte. Parse failures are a 400 in OpenAI error format; nothing is forwarded. - Decision outcomes:
ROUTErewritesmodel(andreasoning_effort/reasoning.effortwhen the policy sets a reasoning budget);PASS_THROUGHforwards untouched;BLOCKreturns 403 with an OpenAI error envelope (code: routed_policy_blocked) even forstream: true.X-Routed-Dry-Runreturns the Decision JSON and never forwards. - Response side: the upstream
hyper::body::Incomingis returned as the axum body unchanged; no compression layer, no buffering,TCP_NODELAY, hop-by-hop headers dropped,content-lengthpreserved. Client disconnects drop the upstream request; an idle watchdog (--stream-idle-timeout, default 60 s) ends stalled streams. - Upstream client:
hyper-utillegacy client withhyper-rustls(HTTP and HTTPS, HTTP/1.1 and HTTP/2 over TLS), connection pooling, no automatic decompression. - Response headers:
X-Routed-Decision-Id,X-Routed-Tier,X-Routed-Data-Class,X-Routed-Outcome,X-Routed-Estimated-CostandX-Routed-Decision(base64 JSON; when the full document exceeds 4 KiB a compact subset is sent and the full document lives on the span). - Classifiers run on
tokio::task::spawn_blockingbehind a semaphore with the profile timeout; timeouts and errors become degraded findings (ADR-0006).
Consequences
- Streaming integrity is proven by byte-identity and gated-release tests, not timing (a dedicated test-pyramid ADR may follow).
- Inline mode terminates the client connection and therefore needs TLS in front (Ingress / mesh); ext_proc mode (phase 5) avoids the extra hop.
Alternatives considered
reqwestfor the upstream: rejected (automatic decompression and larger dependency surface);hyper-utilgives direct control over the body.- Typed request structs: rejected; unknown vendor fields would be dropped.