Request lifecycle
This page traces a request through the gateway exactly as the code runs it. The
two anchors are the HTTP middleware chain in
internal/httpserver/router.go
and the routing core in
gateway.go
(Route and RouteStream).
Overviewโ
1. HTTP middleware chainโ
The chain is assembled in NewRouter (router.go lines 52โ68) and runs in this
order for every request:
- OpenTelemetry โ
gwotel.Middleware(router.go:52). Runs first so any inbound W3Ctraceparentis extracted into the request context before logging needs it. With no OTLP provider configured it is a cheap no-op. - Request logging โ
logging.Middleware(router.go:53). Resolves a trace ID and sets theX-Request-IDresponse header (internal/logging/logger.go:94). Precedence: (1) a trace ID already on the context from the OTel layer, (2) the inboundX-Request-IDheader, (3) a freshly generated 16-byte hex ID. This is what keeps the OTeltrace_id, the log trace ID, and theX-Request-IDheader equal for a request. - Recoverer โ
chimw.Recoverer(router.go:54). Recovers from panics in downstream handlers and turns them into a 500 instead of crashing the server. - RealIP โ
chimw.RealIP(router.go:62). RewritesRemoteAddrfromX-Forwarded-For/X-Real-IPso per-IP rate limiting and request logs see the real client IP rather than the load balancer's. Intended for deployment behind a trusted proxy. - CORS โ
middleware.CORS(corsOrigins...)(router.go:63). Applies the configured allowed origins. - Per-IP rate limit โ
middleware.RateLimit(rlStore)(router.go:67), mounted only when a rate-limit store is configured. A token-bucket keyed on client IP; over-limit requests get429with an OpenAI-shapedrate_limit_exceedederror (internal/middleware/ratelimit.go). - Auth โ
middleware.ProxyAuth(store, masterKey)(router.go:195, applied insidemountOpenAIRoutes). Guards the/v1/*routes with bearer-token auth. It delegates toadmin.AuthMiddleware, unlessALLOW_UNAUTHENTICATED_PROXY=trueis set for local dev (internal/middleware/proxyauth.go).
The first five run for the whole router; rate limit is conditional; auth is
scoped to the OpenAI-compatible /v1/* group. /health, the dashboard, and
admin routes are mounted separately (router.go:70โ73).
2. Handler and gateway routingโ
POST /v1/chat/completions lands in the ChatCompletions handler, which calls
Gateway.Route (non-streaming) or Gateway.RouteStream (when stream: true).
Route (gateway.go:397) executes these stages in order:
- Alias resolution โ
resolveAlias(gateway.go:422) rewritesreq.Modelto its configured target fromconfig.Aliasesbefore any routing decision (gateway.go:2085). before_requestplugins โrunBeforePlugins(gateway.go:432). Runs guardrails, transforms, and rate-limit-style plugins. If a plugin setsSkipwith a response (for example a cache hit),after_requestplugins still fire and that early response is returned immediately (gateway.go:382). A plugin error increments theRejectedmetric and aborts.- Routing strategy selects a target โ
getStrategy(gateway.go:427, built ingateway.go:890) constructs the strategy fromconfig.strategy.mode:single,fallback,loadbalance,latency,cost-optimized,conditional,content-based, orab-test. The provider lookup closure transparently wraps each target in a circuit breaker (cbProvider,gateway.go:1030): an open circuit short-circuits withErrCircuitOpeninstead of calling the provider. - Provider call with retry / fallback โ
s.Execute(gateway.go:489) performs provider selection and the actual upstream call. Infallbackmode, per-target retry (attempts, status codes, backoff) is applied (gateway.go:938) and the strategy advances to the next target on eligible failures. after_requestplugins oron_errorplugins โ on success,after_requestplugins run (logging, caching) and may rewrite the response (gateway.go:579). On failure,on_errorplugins run, the error is classified (circuit_openvsprovider_error), metrics and afailedlifecycle event are emitted, and the error is returned (gateway.go:495).- Response โ
recordSuccess(gateway.go:595) emits Prometheus metrics, computes cost from the model catalog, stamps the trace span, and dispatches thecompletedevent. The response carries theX-Request-IDtrace header set back in the logging middleware.
3. MCP agentic loopโ
When MCP servers are configured, the gateway injects their tool definitions into
the request before routing (gateway.go:450), de-duplicating against any tools
the caller already supplied. After the first provider response, the agentic loop
runs (gateway.go:544):
ShouldContinueLoopchecks whether the model returnedtool_callsand the call depth is under the configured limit.ResolvePendingToolCallsexecutes the requested tools and returns the assistant message plus one tool-result message per call.- Those messages are appended to
req.Messagesand the provider is called again (gateway.go:565).
The loop repeats until the model stops emitting tool_calls or the depth limit
is reached. Intermediate calls are forced non-streaming so each response can be
inspected for tool calls (gateway.go:477).
4. Streaming pathโ
RouteStream (gateway.go:1163) handles stream: true:
- MCP redirect โ if MCP servers are registered, the request is routed
through
Routeso the full agentic loop runs to completion, then the final response is wrapped into a single final chunk viaresponseStream(gateway.go:1200,gateway.go:1420). This is the current Phase 1 behavior: the streaming caller still seesstream: truein emitted events, but receives one terminalchat.completion.chunk. - Normal streaming โ
before_requestplugins run, thenresolveStreamingProviderLocked(gateway.go:1242) orders targets per strategy mode and picks a circuit-breaker-aware streaming provider.sp.CompleteStreamopens the upstream channel (gateway.go:1278). - Token metering โ the raw channel is wrapped by
streamwrap.Meter(gateway.go:1417), which counts input/output tokens, computes cost from the catalog, records TTFT/TTLT timings, runsafter_requestplugins through itsCompletionFn, updates circuit-breaker state, and emits Prometheus metrics plus thecompletedorfailedevent once the stream drains.