Skip to content

Commit 47cb6fe

Browse files
fix: header callbacks merge instead of overwriting
1 parent fe88cf5 commit 47cb6fe

4 files changed

Lines changed: 52 additions & 7 deletions

File tree

packages/uipath_langchain_client/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
All notable changes to `uipath_langchain_client` will be documented in this file.
44

5+
## [1.11.1] - 2026-05-13
6+
7+
### Fixed
8+
- `UiPathDynamicHeadersCallback` now merges `get_headers()` into the dynamic-headers ContextVar instead of replacing it wholesale. This prevents two stacked callbacks from overwriting each other.
9+
510
## [1.11.0] - 2026-05-08
611

712
### Changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
__title__ = "UiPath LangChain Client"
22
__description__ = "A Python client for interacting with UiPath's LLM services via LangChain."
3-
__version__ = "1.11.0"
3+
__version__ = "1.11.1"

packages/uipath_langchain_client/src/uipath_langchain_client/callbacks.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55

66
from langchain_core.callbacks import BaseCallbackHandler
77

8-
from uipath.llm_client.utils.headers import set_dynamic_request_headers
8+
from uipath.llm_client.utils.headers import (
9+
get_dynamic_request_headers,
10+
set_dynamic_request_headers,
11+
)
912

1013

1114
class UiPathDynamicHeadersCallback(BaseCallbackHandler, ABC):
@@ -37,21 +40,26 @@ def get_headers(self) -> dict[str, str]:
3740
"""Return headers to inject into the next LLM gateway request."""
3841
...
3942

43+
def _merge_headers(self) -> None:
44+
merged = get_dynamic_request_headers()
45+
merged.update(self.get_headers())
46+
set_dynamic_request_headers(merged)
47+
4048
def on_chat_model_start(
4149
self,
4250
serialized: dict[str, Any],
4351
messages: list[list[Any]],
4452
**kwargs: Any,
4553
) -> None:
46-
set_dynamic_request_headers(self.get_headers())
54+
self._merge_headers()
4755

4856
def on_llm_start(
4957
self,
5058
serialized: dict[str, Any],
5159
prompts: list[str],
5260
**kwargs: Any,
5361
) -> None:
54-
set_dynamic_request_headers(self.get_headers())
62+
self._merge_headers()
5563

5664
def on_llm_end(self, response: Any, **kwargs: Any) -> None:
5765
set_dynamic_request_headers({})

tests/langchain/features/test_dynamic_headers.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,10 +235,42 @@ def test_on_chat_model_start_sets_headers(self, tracer):
235235
cb.on_chat_model_start({}, [[]])
236236
assert "x-trace-id" in get_dynamic_request_headers()
237237

238-
def test_on_chat_model_start_no_span_sets_empty(self):
239-
"""When there is no active span, on_chat_model_start clears the ContextVar."""
240-
set_dynamic_request_headers({"x-stale": "value"})
238+
def test_on_chat_model_start_no_span_injects_nothing(self):
239+
"""When there is no active span, on_chat_model_start adds no headers of its own."""
241240
OtelHeadersCallback().on_chat_model_start({}, [[]])
241+
assert "x-trace-id" not in get_dynamic_request_headers()
242+
assert "x-span-id" not in get_dynamic_request_headers()
243+
244+
def test_on_chat_model_start_preserves_other_callbacks_headers(self):
245+
"""Merge semantics: a callback returning {} leaves existing headers intact."""
246+
set_dynamic_request_headers({"x-other-callback": "value"})
247+
OtelHeadersCallback().on_chat_model_start({}, [[]])
248+
assert get_dynamic_request_headers() == {"x-other-callback": "value"}
249+
250+
def test_two_callbacks_compose_without_clobbering(self, tracer):
251+
"""Two callbacks in a row produce the union of their headers."""
252+
253+
class StaticHeadersCallback(UiPathDynamicHeadersCallback):
254+
def __init__(self, headers: dict[str, str]):
255+
super().__init__()
256+
self._headers = headers
257+
258+
def get_headers(self) -> dict[str, str]:
259+
return self._headers
260+
261+
first = StaticHeadersCallback({"x-previous-header": "abc"})
262+
second = OtelHeadersCallback()
263+
with active_span(tracer, "llm-call"):
264+
first.on_chat_model_start({}, [[]])
265+
second.on_chat_model_start({}, [[]])
266+
headers = get_dynamic_request_headers()
267+
assert headers.get("x-previous-header") == "abc"
268+
assert "x-trace-id" in headers
269+
270+
def test_on_llm_end_clears_all_headers(self):
271+
"""on_llm_end resets the ContextVar wholesale for the next call."""
272+
set_dynamic_request_headers({"x-header": "1", "x-other-header": "2"})
273+
OtelHeadersCallback().on_llm_end(None)
242274
assert get_dynamic_request_headers() == {}
243275

244276

0 commit comments

Comments
 (0)