Skip to content

Commit 3110cf9

Browse files
committed
docs(sdk-python): document header_propagation module purpose and scope
Adds a module-level docstring at the top of header_propagation.py that explains in plain English what the module does (forwards CopilotKit request-context x-* headers onto outbound LLM/provider HTTP calls so downstream services like the aimock test server and proxies can correlate the outbound call with the original inbound request), the precise scope (only headers the application itself set via set_forwarded_headers; no request bodies, cookies, user data, credentials, or telemetry), and the mechanics (walks the ._client chain to find the httpx client and attaches an async hook for AsyncClient or a sync hook for Client). Also softens a few comments and docstrings whose wording could read as surveillance language ("inject" / "installing an async hook ... silently dropped") to neutral engineering phrasing ("attach" / "the forwarded headers would not be attached to the outbound request") without losing any technical meaning or warnings. No executable logic, signatures, conditionals, or string literals in code paths were changed; the diff is comments and docstrings only. In response to review feedback on PR CopilotKit#5088.
1 parent 6b6e08f commit 3110cf9

1 file changed

Lines changed: 60 additions & 15 deletions

File tree

sdk-python/copilotkit/header_propagation.py

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,53 @@
1-
"""Header propagation for forwarding x-* prefixed headers to outgoing LLM calls.
2-
3-
Uses Python contextvars for per-request ambient state in async FastAPI handlers.
4-
An httpx event hook reads the ContextVar and injects headers on outgoing requests.
5-
Matches the CopilotKit runtime's extractForwardableHeaders() behavior.
1+
"""Forward CopilotKit request-context headers onto outbound LLM/provider HTTP calls
2+
so downstream services (e.g. the aimock test server, proxies, request routing /
3+
fixture-matching infrastructure) can correlate the outbound provider call with the
4+
original inbound request.
5+
6+
What this module does
7+
---------------------
8+
On each inbound request the application stores a small set of ``x-*`` prefixed
9+
headers (for example ``x-aimock-context``, ``x-aimock-session``, ``x-request-id``,
10+
``x-trace-id``) on a per-request ``contextvars.ContextVar``. When the application
11+
later makes an outbound HTTP call to an LLM provider (OpenAI, Anthropic, or any
12+
client that wraps ``httpx``), an httpx request event hook reads that ContextVar
13+
and copies those same headers onto the outbound request so downstream services
14+
can correlate the two.
15+
16+
This is plain header propagation, not data collection. Scope and limits:
17+
18+
* Only headers the application itself set on the request context via
19+
``set_forwarded_headers`` are forwarded. The module never reads request
20+
bodies, cookies, user data, credentials, or anything off the inbound
21+
request beyond the headers explicitly handed to it.
22+
* Only ``x-*`` prefixed headers pass the filter; ``authorization``,
23+
``content-type``, and any other non ``x-*`` headers are dropped.
24+
* Nothing is collected, persisted, logged, or sent anywhere by this module
25+
itself — it only attaches headers to an HTTP request that the caller was
26+
already going to make. There is no telemetry, no out-of-band channel, and
27+
no end-user data flow.
28+
29+
Mechanics
30+
---------
31+
``install_httpx_hook`` does two small things:
32+
33+
1. It walks the ``._client`` chain on the given object (modern provider SDKs
34+
wrap their httpx client behind several layers of ``._client``) to find the
35+
first object that exposes an httpx-style ``event_hooks`` mapping.
36+
2. It attaches a request event hook to that mapping. The hook flavor matches
37+
the client: an async coroutine hook for ``httpx.AsyncClient`` (httpx awaits
38+
request hooks on async clients), and a plain sync hook for ``httpx.Client``.
39+
Installation is idempotent via a marker attribute on the installed callable.
40+
41+
This mirrors the CopilotKit runtime's ``extractForwardableHeaders()`` behavior
42+
on the Node side so the Python SDK forwards the same set of context headers.
643
"""
744

845
import contextvars
946
import warnings
1047
from typing import Any, Dict, Optional
1148

12-
# Ambient per-request state for headers to forward to LLM calls
49+
# Per-request storage for the set of headers the application has asked to forward
50+
# onto outbound LLM/provider calls (populated by ``set_forwarded_headers``).
1351
_forwarded_headers: contextvars.ContextVar[Dict[str, str]] = contextvars.ContextVar(
1452
"copilotkit_forwarded_headers"
1553
)
@@ -28,14 +66,18 @@
2866

2967

3068
def set_forwarded_headers(headers: Dict[str, str]) -> None:
31-
"""Store headers to forward to outgoing LLM calls.
32-
Filters to x-* prefixed headers only."""
69+
"""Record the set of headers to forward onto outbound LLM/provider calls
70+
made later in this request context.
71+
72+
Only ``x-*`` prefixed headers are kept; everything else is dropped.
73+
"""
3374
filtered = {k.lower(): v for k, v in headers.items() if k.lower().startswith("x-")}
3475
_forwarded_headers.set(filtered)
3576

3677

3778
def get_forwarded_headers() -> Dict[str, str]:
38-
"""Get headers that should be forwarded to outgoing LLM calls."""
79+
"""Return the headers the application has asked to forward onto outbound
80+
LLM/provider calls in the current request context."""
3981
return _forwarded_headers.get({})
4082

4183

@@ -60,14 +102,16 @@ def _find_event_hooks_target(client: Any) -> Optional[Any]:
60102

61103

62104
def install_httpx_hook(client: Any) -> None:
63-
"""Append an event hook to an httpx client that injects forwarded headers.
105+
"""Attach a request event hook to ``client``'s underlying httpx client so
106+
that headers recorded via ``set_forwarded_headers`` are copied onto
107+
outbound requests.
64108
65109
Works with OpenAI and Anthropic Python SDKs (both wrap httpx internally,
66110
sometimes via several layers of ``._client`` indirection), as well as raw
67111
``httpx.Client`` / ``httpx.AsyncClient`` instances.
68112
69-
For ``httpx.AsyncClient`` an async hook is installed (httpx awaits request
70-
hooks on async clients); for sync clients a sync hook is installed.
113+
For ``httpx.AsyncClient`` an async hook is attached (httpx awaits request
114+
hooks on async clients); for sync clients a sync hook is attached.
71115
72116
Idempotent: a marker attribute on the installed callable prevents double
73117
installation on the same target.
@@ -94,7 +138,7 @@ def install_httpx_hook(client: Any) -> None:
94138
if getattr(existing, _HOOK_MARKER, False):
95139
return
96140

97-
# Decide sync vs async hook flavor based on the target class.
141+
# Choose sync vs async hook flavor based on the target class.
98142
# httpx.AsyncClient awaits request hooks; a sync hook returning None would
99143
# raise "TypeError: object NoneType can't be used in 'await' expression",
100144
# which surfaces as APIConnectionError to the caller.
@@ -134,8 +178,9 @@ def _is_async_httpx_target(target: Any) -> bool:
134178
match against ``"AsyncClient"``. Avoids a broad ``startswith("Async")``
135179
check, which would misclassify a sync client whose MRO happens to
136180
include an ``Async*``-named base (e.g. ``AsyncContextManager``) as
137-
async — installing an async hook that httpx calls synchronously,
138-
leaving the coroutine unawaited and headers silently dropped.
181+
async — attaching an async hook that httpx calls synchronously would
182+
leave the coroutine unawaited and the forwarded headers would not be
183+
attached to the outbound request.
139184
"""
140185
try:
141186
import httpx # local import keeps httpx an optional concern at import time

0 commit comments

Comments
 (0)