Skip to content

feat: per-request pass-through headers on ModelConfig - #2692

Open
TarasLykhenko wants to merge 2 commits into
kagent-dev:mainfrom
TarasLykhenko:feat/modelconfig-passthrough-headers
Open

feat: per-request pass-through headers on ModelConfig#2692
TarasLykhenko wants to merge 2 commits into
kagent-dev:mainfrom
TarasLykhenko:feat/modelconfig-passthrough-headers

Conversation

@TarasLykhenko

Copy link
Copy Markdown

Motivation

kagent supports static defaultHeaders on outbound LLM calls, and apiKeyPassthrough for 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

apiVersion: kagent.dev/v1alpha3
kind: ModelConfig
spec:
  defaultHeaders:            # unchanged: static values
    x-tenant: acme
  passthroughHeaders:        # NEW: names resolved from each incoming A2A request
    - x-guardrail-token

Header names are case-insensitive; values are read per request from the incoming A2A call context. A pass-through value overrides a defaultHeaders entry of the same name; headers absent from the request are omitted. Authorization is rejected at admission (CEL) — credential forwarding stays on apiKeyPassthrough — and both runtimes additionally drop credential carriers (Proxy-Authorization, Cookie) and RFC 9110 hop-by-hop names at runtime.

Changes by layer

  • CRD (v1alpha3): passthroughHeaders []string with bounds and CEL validation; envtest CEL test; manifests + Helm CRDs regenerated.
  • Wire: passthrough_headers on adk.BaseModel; populateHeaderFields helper (mirrors populateTLSFields) in the adkconfig translator, covering all 10 provider branches.
  • Go runtime: allowedRequestHeaders lifted from pkg/mcp into a shared pkg/headers package (a2a-go v2); headerTransport overlays per-request headers from the call context after static ones. 8/10 providers get this via BuildHTTPClient; GeminiVertexAI and SAPAICore log a warning — they build clients without the shared transport and already drop defaultHeaders/TLS today (pre-existing gap, left as-is here).
  • A2A gateway: a callerHeadersInterceptor on 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, neither passthroughHeaders nor the existing MCP allowedHeaders can see caller headers through the gateway; the runtime-side allowlists remain the actual gate.
  • Python runtime: LLMHeaderPassthroughPlugin resolves session-state headers (case-insensitive, restricted names filtered, empty values omitted) onto llm_request.config.http_options for OpenAI/Azure (per-call extra_headers) and Gemini (honoured natively); Anthropic uses a cache-invalidating set_passthrough_headers, the same mechanism as set_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

  1. Gateway forwarding scope — the interceptor forwards all non-restricted custom caller headers rather than resolving the instance's configured allowlist at the gateway (the dialer has no ModelConfig access, and the runtime filters by allowlist anyway). If you'd rather plumb the configured names into the resolved bundle instead, happy to rework.
  2. Anthropic delivery — per-call headers can't reach messages.create inside 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.

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.>
@TarasLykhenko
TarasLykhenko requested review from a team and supreme-gg-gg as code owners September 4, 2026 08:29
@github-actions github-actions Bot added the enhancement New feature or request label Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants