Skip to content

Commit 8ca2807

Browse files
fix: inject trace context headers per-request instead of at client construction (#870)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7698521 commit 8ca2807

8 files changed

Lines changed: 407 additions & 11 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
[project]
22
name = "uipath-langchain"
3-
version = "0.11.6"
3+
version = "0.11.7"
44
description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"
77
dependencies = [
88
"uipath>=2.10.70, <2.11.0",
99
"uipath-core>=0.5.15, <0.6.0",
10-
"uipath-platform>=0.1.45, <0.2.0",
10+
"uipath-platform>=0.1.58, <0.2.0",
1111
"uipath-runtime>=0.10.0, <0.11.0",
1212
"langgraph>=1.1.8, <2.0.0",
1313
"langchain-core>=1.2.11, <2.0.0",

src/uipath_langchain/chat/_legacy/bedrock.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from langchain_core.messages import BaseMessage
88
from langchain_core.outputs import ChatGenerationChunk, ChatResult
99
from tenacity import AsyncRetrying, Retrying
10+
from uipath.platform.chat.llm_trace_context import build_trace_context_headers
1011
from uipath.platform.common import (
1112
EndpointManager,
1213
get_ca_bundle_path,
@@ -181,6 +182,7 @@ def _modify_request(self, request, **kwargs):
181182
)
182183
headers["X-UiPath-LlmGateway-ApiFlavor"] = self.api_flavor
183184
headers["X-UiPath-Streaming-Enabled"] = streaming
185+
headers.update(build_trace_context_headers(extra_baggage=["source=agents"]))
184186

185187
request.headers.update(headers)
186188

src/uipath_langchain/chat/_legacy/http_client/headers.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import os
44
from urllib.parse import quote
55

6-
from uipath.platform.chat.llm_trace_context import build_trace_context_headers
76
from uipath.platform.common._config import UiPathConfig
87
from uipath.platform.common.constants import (
98
ENV_FOLDER_KEY,
@@ -65,6 +64,4 @@ def build_uipath_headers(
6564
if organization_id := os.getenv(ENV_ORGANIZATION_ID):
6665
headers[HEADER_INTERNAL_ACCOUNT_ID] = organization_id
6766

68-
headers.update(build_trace_context_headers(extra_baggage=["source=agents"]))
69-
7067
return headers

src/uipath_langchain/chat/_legacy/openai.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import httpx
66
from langchain_openai import AzureChatOpenAI
77
from pydantic import PrivateAttr
8+
from uipath.platform.chat.llm_trace_context import build_trace_context_headers
89
from uipath.platform.common import (
910
EndpointManager,
1011
get_httpx_client_kwargs,
@@ -53,6 +54,14 @@ def _inject_license_ref_id(request: httpx.Request) -> None:
5354
request.headers["X-UiPath-License-RefId"] = license_ref_id
5455

5556

57+
def _inject_trace_context_headers(request: httpx.Request) -> None:
58+
"""Inject trace context headers per-request from the active OTEL span."""
59+
for key, value in build_trace_context_headers(
60+
extra_baggage=["source=agents"]
61+
).items():
62+
request.headers[key] = value
63+
64+
5665
class UiPathURLRewriteTransport(httpx.AsyncHTTPTransport):
5766
def __init__(self, verify: bool = True, **kwargs):
5867
super().__init__(verify=verify, **kwargs)
@@ -62,6 +71,7 @@ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
6271
if new_url:
6372
request.url = new_url
6473
_inject_license_ref_id(request)
74+
_inject_trace_context_headers(request)
6575

6676
return await super().handle_async_request(request)
6777

@@ -75,6 +85,7 @@ def handle_request(self, request: httpx.Request) -> httpx.Response:
7585
if new_url:
7686
request.url = new_url
7787
_inject_license_ref_id(request)
88+
_inject_trace_context_headers(request)
7889

7990
return super().handle_request(request)
8091

src/uipath_langchain/chat/_legacy/vertex.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from tenacity import AsyncRetrying, Retrying
1414
from uipath._utils import resource_override
1515
from uipath._utils._ssl_context import get_httpx_client_kwargs
16+
from uipath.platform.chat.llm_trace_context import build_trace_context_headers
1617
from uipath.platform.common import EndpointManager
1718

1819
from .http_client import build_uipath_headers, resolve_gateway_url
@@ -96,6 +97,11 @@ def handle_request(self, request: httpx.Request) -> httpx.Response:
9697
request.headers["host"] = new_url.host
9798
request.url = new_url
9899

100+
for key, value in build_trace_context_headers(
101+
extra_baggage=["source=agents"]
102+
).items():
103+
request.headers[key] = value
104+
99105
response = super().handle_request(request)
100106
if self.header_capture:
101107
self.header_capture.set(dict(response.headers))
@@ -129,6 +135,11 @@ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
129135
request.headers["host"] = new_url.host
130136
request.url = new_url
131137

138+
for key, value in build_trace_context_headers(
139+
extra_baggage=["source=agents"]
140+
).items():
141+
request.headers[key] = value
142+
132143
response = await super().handle_async_request(request)
133144
if self.header_capture:
134145
self.header_capture.set(dict(response.headers))

src/uipath_langchain/chat/chat_model_factory.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,13 @@
1212

1313
from typing import Any, Final
1414

15-
from langchain_core.callbacks import Callbacks
15+
from langchain_core.callbacks import BaseCallbackHandler, Callbacks
1616
from langchain_core.language_models import BaseChatModel
17+
from uipath.llm_client.utils.headers import (
18+
get_dynamic_request_headers,
19+
set_dynamic_request_headers,
20+
)
21+
from uipath.platform.chat.llm_trace_context import build_trace_context_headers
1722
from uipath_langchain_client.base_client import UiPathBaseChatModel
1823
from uipath_langchain_client.factory import get_chat_model as get_chat_model_factory
1924
from uipath_langchain_client.settings import (
@@ -23,6 +28,33 @@
2328
VendorType,
2429
)
2530

31+
32+
class _TraceContextHeadersCallback(BaseCallbackHandler):
33+
"""Inject W3C-style trace context headers into each LLM gateway request.
34+
35+
Merges into the existing dynamic-headers ContextVar so that headers
36+
set by earlier callbacks (e.g. ``LicenseRefIdHeadersCallback``) are
37+
preserved instead of overwritten.
38+
"""
39+
40+
run_inline: bool = True
41+
42+
def _merge_headers(self) -> None:
43+
existing = get_dynamic_request_headers()
44+
existing.update(build_trace_context_headers(extra_baggage=["source=agents"]))
45+
set_dynamic_request_headers(existing)
46+
47+
def on_chat_model_start(
48+
self, serialized: dict[str, Any], messages: list[list[Any]], **kwargs: Any
49+
) -> None:
50+
self._merge_headers()
51+
52+
def on_llm_start(
53+
self, serialized: dict[str, Any], prompts: list[str], **kwargs: Any
54+
) -> None:
55+
self._merge_headers()
56+
57+
2658
_UNSET: Final[Any] = object()
2759
DEFAULT_TIMEOUT_SECONDS: Final[float] = 895.0
2860
DEFAULT_MAX_TOKENS: Final[int] = 1000
@@ -84,6 +116,12 @@ def get_chat_model(
84116
Returns:
85117
A configured ``BaseChatModel`` instance.
86118
"""
119+
# Always inject trace context headers per-request via a dynamic-headers
120+
# callback. For the new path the UiPathHttpxClient reads the ContextVar
121+
# set by the callback; for the legacy path the callback is a no-op but
122+
# keeps the wiring consistent.
123+
callbacks = _ensure_trace_context_callback(callbacks)
124+
87125
if not use_new_llm_clients:
88126
return _legacy_chat_model(
89127
model,
@@ -120,6 +158,17 @@ def get_chat_model(
120158
)
121159

122160

161+
def _ensure_trace_context_callback(callbacks: Callbacks) -> list[BaseCallbackHandler]:
162+
"""Append a ``_TraceContextHeadersCallback`` if one is not already present."""
163+
if callbacks is _UNSET or callbacks is None:
164+
cb_list: list[BaseCallbackHandler] = []
165+
else:
166+
cb_list = list(callbacks) # type: ignore[arg-type]
167+
if not any(isinstance(cb, _TraceContextHeadersCallback) for cb in cb_list):
168+
cb_list.append(_TraceContextHeadersCallback())
169+
return cb_list
170+
171+
123172
def _legacy_chat_model(
124173
model: str,
125174
*,

0 commit comments

Comments
 (0)