Skip to content

Commit afe4a3e

Browse files
cosminachoclaude
andcommitted
Switch to eager construction-time strip instead of invoke-time context manager
Prior approach used a context manager that temporarily nulled matching fields for the duration of each call and restored them on exit, so `chat.temperature` read the caller's original value between invocations. That's sneaky: the gateway will never accept the value, so reporting 0.7 on the instance is a lie that hides the truth from the user. Replace `disabled_fields_stripped` (context manager) with `strip_disabled_fields` (eager, one-shot). Wire it into `UiPathBaseLLMClient.setup_model_info` so the strip happens once, immediately after `disabled_params` resolves. Each strip logs a warning that includes the original value, so the caller sees exactly what was dropped at construction time rather than being silently surprised later. The four `_generate`/`_agenerate`/`_stream`/`_astream` wrappers are now plain again — no context-manager wrapping, no save/restore, no exception edge cases, no generator subtlety. Net effect for callers: - `UiPathChatAnthropicBedrock(model="anthropic.claude-opus-4-7", temperature=0.7)` now logs a warning and leaves `chat.temperature is None`. - Per-call `chat.invoke(..., temperature=0.7)` still gets stripped by the existing `strip_disabled_kwargs` filter, unchanged. Tests updated to assert the field is None after construction (not "restored after call"). Restore-on-exception test removed (no longer applicable). VCR cassettes still replay — the gateway request body is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d0ea322 commit afe4a3e

6 files changed

Lines changed: 147 additions & 227 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ All notable changes to `uipath_llm_client` (core package) will be documented in
55
## [1.11.3] - 2026-05-21
66

77
### 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.
8+
- `uipath.llm_client.utils.sampling.strip_disabled_fields`: eagerly nulls instance attributes whose names appear in `disabled_params` and whose current values match `is_disabled_value`. 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. Each strip logs a warning that includes the original value so callers can see exactly what was dropped.
99

1010
## [1.11.2] - 2026-05-18
1111

packages/uipath_langchain_client/CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ All notable changes to `uipath_langchain_client` will be documented in this file
55
## [1.11.3] - 2026-05-21
66

77
### 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.
8+
- `UiPathBaseLLMClient.setup_model_info` now calls `strip_disabled_fields` after merging `disabled_params`, so constructor-set sampling fields (e.g. `UiPathChatAnthropicBedrock(model="anthropic.claude-opus-4-7", temperature=0.7)`) are nulled on the instance once `disabled_params` is resolved. 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. A warning is logged per stripped field with the original value so the caller can see what was dropped.
99

1010
### Changed
11-
- Bumped `uipath-llm-client` floor to `>=1.11.3` to match the core release exposing `disabled_fields_stripped`.
11+
- Bumped `uipath-llm-client` floor to `>=1.11.3` to match the core release exposing `strip_disabled_fields`.
1212

1313
## [1.11.2] - 2026-05-18
1414

packages/uipath_langchain_client/src/uipath_langchain_client/base_client.py

Lines changed: 33 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@
5050
set_captured_response_headers,
5151
)
5252
from uipath.llm_client.utils.sampling import (
53-
disabled_fields_stripped,
5453
disabled_params_from_model_details,
54+
strip_disabled_fields,
5555
strip_disabled_kwargs,
5656
)
5757
from uipath_langchain_client.settings import (
@@ -173,6 +173,13 @@ def setup_model_info(self) -> Self:
173173
can derive from ``model_details`` (via
174174
``disabled_params_from_model_details``). User-provided keys win on
175175
conflicts, so callers can override a derived entry by name.
176+
177+
Once ``disabled_params`` is resolved, any matching instance field set at
178+
construction time is nulled via ``strip_disabled_fields``. Vendor SDKs
179+
that read ``self.<field>`` when serializing requests (langchain-
180+
anthropic, langchain-aws) would otherwise leak disabled values past the
181+
per-call ``strip_disabled_kwargs`` filter. The strip logs a warning per
182+
field so the caller knows what was dropped.
176183
"""
177184
if self.model_details is None:
178185
try:
@@ -189,6 +196,13 @@ def setup_model_info(self) -> Self:
189196
merged = {**derived, **user_provided}
190197
self.disabled_params = merged or None
191198

199+
strip_disabled_fields(
200+
self,
201+
disabled_params=self.disabled_params,
202+
model_name=self.model_name,
203+
logger=self.logger,
204+
)
205+
192206
return self
193207

194208
@cached_property
@@ -423,15 +437,7 @@ def _generate(
423437
)
424438
set_captured_response_headers({})
425439
try:
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-
)
440+
result = self._uipath_generate(messages, stop=stop, run_manager=run_manager, **kwargs)
435441
self._inject_gateway_headers(result.generations)
436442
return result
437443
finally:
@@ -462,15 +468,9 @@ async def _agenerate(
462468
)
463469
set_captured_response_headers({})
464470
try:
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-
)
471+
result = await self._uipath_agenerate(
472+
messages, stop=stop, run_manager=run_manager, **kwargs
473+
)
474474
self._inject_gateway_headers(result.generations)
475475
return result
476476
finally:
@@ -501,20 +501,14 @@ def _stream(
501501
)
502502
set_captured_response_headers({})
503503
try:
504-
with disabled_fields_stripped(
505-
self,
506-
disabled_params=self.disabled_params,
507-
model_name=self.model_name,
508-
logger=self.logger,
504+
first = True
505+
for chunk in self._uipath_stream(
506+
messages, stop=stop, run_manager=run_manager, **kwargs
509507
):
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
508+
if first:
509+
self._inject_gateway_headers([chunk])
510+
first = False
511+
yield chunk
518512
finally:
519513
set_captured_response_headers({})
520514

@@ -543,20 +537,14 @@ async def _astream(
543537
)
544538
set_captured_response_headers({})
545539
try:
546-
with disabled_fields_stripped(
547-
self,
548-
disabled_params=self.disabled_params,
549-
model_name=self.model_name,
550-
logger=self.logger,
540+
first = True
541+
async for chunk in self._uipath_astream(
542+
messages, stop=stop, run_manager=run_manager, **kwargs
551543
):
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
544+
if first:
545+
self._inject_gateway_headers([chunk])
546+
first = False
547+
yield chunk
560548
finally:
561549
set_captured_response_headers({})
562550

src/uipath/llm_client/utils/sampling.py

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

19-
from collections.abc import Iterator, Mapping
20-
from contextlib import contextmanager
19+
from collections.abc import Mapping
2120
from logging import Logger
2221
from typing import Any
2322

@@ -97,51 +96,42 @@ def strip_disabled_kwargs(
9796
return out
9897

9998

100-
@contextmanager
101-
def disabled_fields_stripped(
99+
def strip_disabled_fields(
102100
instance: Any,
103101
*,
104102
disabled_params: Mapping[str, Any] | None,
105103
model_name: str,
106104
logger: Logger | None,
107-
) -> Iterator[None]:
108-
"""Temporarily wipe matching instance attributes for the duration of the block.
105+
) -> None:
106+
"""Null instance attributes that match ``disabled_params``.
109107
110108
Sibling of :func:`strip_disabled_kwargs` for fields set at construction time.
111109
Vendor SDKs that build request bodies from ``self.<field>`` (e.g. langchain-
112110
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.
111+
the kwargs-level strip; this helper neutralizes them once, eagerly, so they
112+
can't leak into any subsequent request.
116113
117114
Matching rule mirrors ``strip_disabled_kwargs``: a field is nulled out when
118115
its name is in ``disabled_params`` AND its current value is non-None AND
119-
``is_disabled_value`` matches the spec.
116+
``is_disabled_value`` matches the spec. Each strip logs a warning that
117+
includes the original value so the caller can see exactly what was dropped.
120118
"""
121119
if not disabled_params:
122-
yield
123120
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)
121+
for key, spec in disabled_params.items():
122+
if not hasattr(instance, key):
123+
continue
124+
current = getattr(instance, key)
125+
if current is None:
126+
continue
127+
if is_disabled_value(current, spec):
128+
if logger is not None:
129+
logger.warning(
130+
"Disabling field %r (was %r) for model %r — parameter is in disabled_params",
131+
key,
132+
current,
133+
model_name,
134+
)
135+
# object.__setattr__ bypasses pydantic field validation in case the
136+
# field's declared type forbids None.
137+
object.__setattr__(instance, key, None)

0 commit comments

Comments
 (0)