feat: per-request pass-through headers on ModelConfig - #2692
Open
TarasLykhenko wants to merge 2 commits into
Open
feat: per-request pass-through headers on ModelConfig#2692TarasLykhenko wants to merge 2 commits into
TarasLykhenko wants to merge 2 commits into
Conversation
Add ModelConfig.spec.passthroughHeaders: a list of HTTP header names whose values are forwarded per request from the incoming A2A call onto the outbound LLM call, alongside the existing static defaultHeaders. This lets a caller-supplied token (e.g. a guardrail or tenant token) reach the LLM provider or a proxy in front of it without static configuration. - v1alpha3 CRD field with CEL validation rejecting Authorization case-insensitively (credential forwarding stays on apiKeyPassthrough), plus an envtest CEL test; manifests regenerated. - Wire field passthrough_headers on adk.BaseModel; populateHeaderFields helper in the adkconfig translator covering all provider branches. - Go runtime: allowedRequestHeaders is lifted from pkg/mcp into a shared pkg/headers package; headerTransport overlays per-request headers from the A2A call context after static ones (pass-through wins over defaultHeaders), with Authorization, other credential carriers, and hop-by-hop names filtered so provider credentials are never clobbered. GeminiVertexAI and SAPAICore log a warning (no shared HTTP transport). - a2a gateway: a callerHeadersInterceptor relays caller-supplied custom headers from the public request onto the private runtime call (minus credential, hop-by-hop, and transport/system metadata) so per-feature allowlists on the runtime can see them; without it neither passthroughHeaders nor MCP allowedHeaders can work through the gateway. - Python runtime: LLMHeaderPassthroughPlugin resolves session-state headers onto llm_request.config.http_options for OpenAI/Azure (sent as per-call extra_headers) and Gemini (native); Anthropic uses a cache-invalidating set_passthrough_headers mirroring set_passthrough_key. Bedrock/Ollama/VertexAI/SAP AI Core log a warning and ignore the field. Unit tests cover every layer; a config with only defaultHeaders produces byte-identical requests. Signed-off-by: TarasLykhenko <18290033+TarasLykhenko@users.noreply.github.com.>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
kagent supports static
defaultHeaderson outbound LLM calls, andapiKeyPassthroughfor forwarding the caller's Bearer credential. There is no way to forward an arbitrary caller-supplied header per request — e.g. a guardrail token, tenant token, or request id that a proxy in front of the LLM provider needs. Organizations running LLM traffic through policy gateways need the caller's own token on each outbound call, which static configuration cannot express.What this adds
Header names are case-insensitive; values are read per request from the incoming A2A call context. A pass-through value overrides a
defaultHeadersentry of the same name; headers absent from the request are omitted.Authorizationis rejected at admission (CEL) — credential forwarding stays onapiKeyPassthrough— and both runtimes additionally drop credential carriers (Proxy-Authorization,Cookie) and RFC 9110 hop-by-hop names at runtime.Changes by layer
passthroughHeaders []stringwith bounds and CEL validation; envtest CEL test; manifests + Helm CRDs regenerated.passthrough_headersonadk.BaseModel;populateHeaderFieldshelper (mirrorspopulateTLSFields) in the adkconfig translator, covering all 10 provider branches.allowedRequestHeaderslifted frompkg/mcpinto a sharedpkg/headerspackage (a2a-go v2);headerTransportoverlays per-request headers from the call context after static ones. 8/10 providers get this viaBuildHTTPClient; GeminiVertexAI and SAPAICore log a warning — they build clients without the shared transport and already dropdefaultHeaders/TLS today (pre-existing gap, left as-is here).callerHeadersInterceptoron the runtime dialer relays caller-supplied custom headers from the public request onto the private runtime call, excluding credential, hop-by-hop, gRPC pseudo/system, and interceptor-owned (traceparent,x-a2a-extensions) metadata. Without this hop, neitherpassthroughHeadersnor the existing MCPallowedHeaderscan see caller headers through the gateway; the runtime-side allowlists remain the actual gate.LLMHeaderPassthroughPluginresolves session-state headers (case-insensitive, restricted names filtered, empty values omitted) ontollm_request.config.http_optionsfor OpenAI/Azure (per-callextra_headers) and Gemini (honoured natively); Anthropic uses a cache-invalidatingset_passthrough_headers, the same mechanism asset_passthrough_key(same shared-client concurrency caveat, documented). Bedrock, Ollama, VertexAI, and SAP AI Core log a warning and ignore the field.Design points for reviewers
messages.createinside the ADK Claude base class, hence the cache-invalidation setter. A per-request carrier in google-adk would allow removing it.Testing
Unit tests at every layer (transport overlay + credential protection, restricted-name filter, wire round-trip, gateway interceptor, CEL admission via envtest, Python plugin + provider behavior), plus a regression asserting a
defaultHeaders-only config produces byte-identical requests.