Skip to content

Commit d0ea322

Browse files
cosminachoclaude
andcommitted
fix: strip constructor-set sampling fields at invoke time
`UiPathChatAnthropicBedrock(model="anthropic.claude-opus-4-7", temperature=0.7)` previously leaked the disabled field into the request body because langchain- anthropic and langchain-aws build payloads from `self.<field>`, bypassing the existing kwargs-level `strip_disabled_kwargs`. The gateway then rejected the call with 400 (modelDetails.shouldSkipTemperature: true for reasoning models). Adds `disabled_fields_stripped` context manager in core sampling utils — a sibling of `strip_disabled_kwargs` that temporarily nulls matching instance attributes for the duration of the underlying call and restores them on exit. Wired into the four `_generate`/`_agenerate`/`_stream`/`_astream` wrappers in `UiPathBaseChatModel`, so caller-visible state (`chat.temperature`) is unchanged but the vendor SDK reads `None` while building the request. Plugs the init-time leak called out as a known follow-up in 1.10.0. Tests: - 7 new unit tests pin down the during-call/after-call semantics, exception restoration, value-list spec handling, and warning logging. - 2 new VCR-cassetted integration tests against `anthropic.claude-opus-4-7` exercise both vendor SDK families (anthropic-bedrock + bedrock-converse). The recorded 200 is itself proof of the fix — `before_record_response` drops 4xx, so a pre-fix run would have refused to persist the cassette. Core 1.11.0 -> 1.11.2 (new helper exposed) Langchain 1.11.1 -> 1.11.2 (wiring + dep floor bump) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 202136e commit d0ea322

10 files changed

Lines changed: 362 additions & 22 deletions

File tree

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_llm_client` (core package) will be documented in this file.
44

5+
## [1.11.3] - 2026-05-21
6+
7+
### Added
8+
- `uipath.llm_client.utils.sampling.disabled_fields_stripped`: a context manager that temporarily nulls instance attributes matching `disabled_params` for the duration of the block, then restores them on exit. Sibling of `strip_disabled_kwargs` for the case where vendor SDKs (langchain-anthropic, langchain-aws) read `self.<field>` rather than per-call `**kwargs` when building request bodies.
9+
510
## [1.11.2] - 2026-05-18
611

712
### Changed

packages/uipath_langchain_client/CHANGELOG.md

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

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

5+
## [1.11.3] - 2026-05-21
6+
7+
### Fixed
8+
- `UiPathBaseChatModel` now wraps `_uipath_generate`/`_uipath_agenerate`/`_uipath_stream`/`_uipath_astream` in `disabled_fields_stripped`, so constructor-set sampling fields (e.g. `UiPathChatAnthropicBedrock(model="anthropic.claude-opus-4-7", temperature=0.7)`) are nulled on the instance for the duration of the underlying call and restored on exit. Plugs the init-time leak called out as a known follow-up in 1.10.0 — langchain-anthropic and langchain-aws's Bedrock Converse client read `self.temperature`/`self.top_p`/etc. when serializing the request body, so the existing kwargs-level strip alone wasn't enough.
9+
10+
### Changed
11+
- Bumped `uipath-llm-client` floor to `>=1.11.3` to match the core release exposing `disabled_fields_stripped`.
12+
513
## [1.11.2] - 2026-05-18
614

715
### Changed

packages/uipath_langchain_client/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ readme = "README.md"
66
requires-python = ">=3.11"
77
dependencies = [
88
"langchain>=1.2.15,<2.0.0",
9-
"uipath-llm-client>=1.11.2,<2.0.0",
9+
"uipath-llm-client>=1.11.3,<2.0.0",
1010
]
1111

1212
[project.optional-dependencies]
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.2"
3+
__version__ = "1.11.3"

packages/uipath_langchain_client/src/uipath_langchain_client/base_client.py

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
set_captured_response_headers,
5151
)
5252
from uipath.llm_client.utils.sampling import (
53+
disabled_fields_stripped,
5354
disabled_params_from_model_details,
5455
strip_disabled_kwargs,
5556
)
@@ -422,7 +423,15 @@ def _generate(
422423
)
423424
set_captured_response_headers({})
424425
try:
425-
result = self._uipath_generate(messages, stop=stop, run_manager=run_manager, **kwargs)
426+
with disabled_fields_stripped(
427+
self,
428+
disabled_params=self.disabled_params,
429+
model_name=self.model_name,
430+
logger=self.logger,
431+
):
432+
result = self._uipath_generate(
433+
messages, stop=stop, run_manager=run_manager, **kwargs
434+
)
426435
self._inject_gateway_headers(result.generations)
427436
return result
428437
finally:
@@ -453,9 +462,15 @@ async def _agenerate(
453462
)
454463
set_captured_response_headers({})
455464
try:
456-
result = await self._uipath_agenerate(
457-
messages, stop=stop, run_manager=run_manager, **kwargs
458-
)
465+
with disabled_fields_stripped(
466+
self,
467+
disabled_params=self.disabled_params,
468+
model_name=self.model_name,
469+
logger=self.logger,
470+
):
471+
result = await self._uipath_agenerate(
472+
messages, stop=stop, run_manager=run_manager, **kwargs
473+
)
459474
self._inject_gateway_headers(result.generations)
460475
return result
461476
finally:
@@ -486,14 +501,20 @@ def _stream(
486501
)
487502
set_captured_response_headers({})
488503
try:
489-
first = True
490-
for chunk in self._uipath_stream(
491-
messages, stop=stop, run_manager=run_manager, **kwargs
504+
with disabled_fields_stripped(
505+
self,
506+
disabled_params=self.disabled_params,
507+
model_name=self.model_name,
508+
logger=self.logger,
492509
):
493-
if first:
494-
self._inject_gateway_headers([chunk])
495-
first = False
496-
yield chunk
510+
first = True
511+
for chunk in self._uipath_stream(
512+
messages, stop=stop, run_manager=run_manager, **kwargs
513+
):
514+
if first:
515+
self._inject_gateway_headers([chunk])
516+
first = False
517+
yield chunk
497518
finally:
498519
set_captured_response_headers({})
499520

@@ -522,14 +543,20 @@ async def _astream(
522543
)
523544
set_captured_response_headers({})
524545
try:
525-
first = True
526-
async for chunk in self._uipath_astream(
527-
messages, stop=stop, run_manager=run_manager, **kwargs
546+
with disabled_fields_stripped(
547+
self,
548+
disabled_params=self.disabled_params,
549+
model_name=self.model_name,
550+
logger=self.logger,
528551
):
529-
if first:
530-
self._inject_gateway_headers([chunk])
531-
first = False
532-
yield chunk
552+
first = True
553+
async for chunk in self._uipath_astream(
554+
messages, stop=stop, run_manager=run_manager, **kwargs
555+
):
556+
if first:
557+
self._inject_gateway_headers([chunk])
558+
first = False
559+
yield chunk
533560
finally:
534561
set_captured_response_headers({})
535562

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

src/uipath/llm_client/utils/sampling.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
``anthropic.claude-opus-4-7``), the entire sampling set gets disabled.
1717
"""
1818

19-
from collections.abc import Mapping
19+
from collections.abc import Iterator, Mapping
20+
from contextlib import contextmanager
2021
from logging import Logger
2122
from typing import Any
2223

@@ -94,3 +95,53 @@ def strip_disabled_kwargs(
9495
)
9596
out.pop(key, None)
9697
return out
98+
99+
100+
@contextmanager
101+
def disabled_fields_stripped(
102+
instance: Any,
103+
*,
104+
disabled_params: Mapping[str, Any] | None,
105+
model_name: str,
106+
logger: Logger | None,
107+
) -> Iterator[None]:
108+
"""Temporarily wipe matching instance attributes for the duration of the block.
109+
110+
Sibling of :func:`strip_disabled_kwargs` for fields set at construction time.
111+
Vendor SDKs that build request bodies from ``self.<field>`` (e.g. langchain-
112+
anthropic's ``ChatAnthropic``, langchain-aws's ``ChatBedrockConverse``) bypass
113+
the kwargs-level strip; wrapping the underlying ``_generate`` call in this
114+
context manager forces them to read ``None`` for any disabled field, then
115+
restores the original value on exit so caller-visible state is unchanged.
116+
117+
Matching rule mirrors ``strip_disabled_kwargs``: a field is nulled out when
118+
its name is in ``disabled_params`` AND its current value is non-None AND
119+
``is_disabled_value`` matches the spec.
120+
"""
121+
if not disabled_params:
122+
yield
123+
return
124+
saved: dict[str, Any] = {}
125+
try:
126+
for key, spec in disabled_params.items():
127+
if not hasattr(instance, key):
128+
continue
129+
current = getattr(instance, key)
130+
if current is None:
131+
continue
132+
if is_disabled_value(current, spec):
133+
saved[key] = current
134+
if logger is not None:
135+
logger.warning(
136+
"Stripping disabled field %r for model %r",
137+
key,
138+
model_name,
139+
)
140+
# object.__setattr__ bypasses pydantic field validation so we can
141+
# always restore the original — even if the field's declared type
142+
# rejects None.
143+
object.__setattr__(instance, key, None)
144+
yield
145+
finally:
146+
for key, value in saved.items():
147+
object.__setattr__(instance, key, value)

tests/cassettes.db

0 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)