Skip to content

Commit 4a0d9a5

Browse files
cosminachoclaude
andcommitted
Feat: use _UNSET sentinel to exclude unset UiPathChat params from payload
`UiPathChat` parameter fields now default to an internal `_UNSET` singleton instead of `None`, and `_default_params` filters on the sentinel rather than on `None`. Behavior change: passing an explicit `None` (e.g. `UiPathChat( temperature=None)`) now forwards `null` to the normalized API instead of being silently dropped. Unset fields continue to be omitted. Callers relying on `None` as shorthand for "omit" should stop passing the argument. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4ad3bcf commit 4a0d9a5

3 files changed

Lines changed: 64 additions & 29 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.9.7] - 2026-04-22
6+
7+
### Changed
8+
- **Behavior change:** `UiPathChat` parameter fields now default to an internal `_UNSET` sentinel instead of `None`, and `_default_params` filters on the sentinel rather than on `None`. Unset fields are still omitted from the request payload, but passing an explicit `None` (e.g. `UiPathChat(temperature=None)`) now forwards `null` to the normalized API instead of being silently dropped. Previously, both "not passed" and "explicitly `None`" produced the same omission. Callers relying on `None` as shorthand for "omit" should switch to simply not passing the argument.
9+
510
## [1.9.6] - 2026-04-22
611

712
### Added
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.9.6"
3+
__version__ = "1.9.7"

packages/uipath_langchain_client/src/uipath_langchain_client/clients/normalized/chat_models.py

Lines changed: 58 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,32 @@
7272
_DictOrPydantic = Union[dict[str, Any], BaseModel]
7373

7474

75+
class _UnsetType:
76+
"""Singleton sentinel for request params that should be omitted from the payload.
77+
78+
`UiPathChat` param fields default to `_UNSET` so we can tell "caller did not
79+
pass anything" apart from "caller explicitly passed `None`". `_default_params`
80+
filters out `_UNSET` values only — explicit `None` (and other falsy values)
81+
flow through to the normalized API as `null`.
82+
"""
83+
84+
_instance: "_UnsetType | None" = None
85+
86+
def __new__(cls) -> "_UnsetType":
87+
if cls._instance is None:
88+
cls._instance = super().__new__(cls)
89+
return cls._instance
90+
91+
def __bool__(self) -> Literal[False]:
92+
return False
93+
94+
def __repr__(self) -> str:
95+
return "_UNSET"
96+
97+
98+
_UNSET = _UnsetType()
99+
100+
75101
def _oai_structured_outputs_parser(ai_msg: AIMessage, schema: type[BaseModel]) -> BaseModel:
76102
if not ai_msg.content:
77103
raise ValueError("Expected non-empty content from model.")
@@ -159,47 +185,47 @@ class UiPathChat(UiPathBaseChatModel):
159185
)
160186

161187
# Common
162-
max_tokens: int | None = Field(
163-
default=None,
188+
max_tokens: int | None | _UnsetType = Field(
189+
default=_UNSET,
164190
validation_alias=AliasChoices("max_tokens", "max_output_tokens", "max_completion_tokens"),
165191
)
166-
temperature: float | None = None
167-
top_p: float | None = None
168-
top_k: int | None = None
169-
stop: list[str] | str | None = Field(
170-
default=None,
192+
temperature: float | None | _UnsetType = _UNSET
193+
top_p: float | None | _UnsetType = _UNSET
194+
top_k: int | None | _UnsetType = _UNSET
195+
stop: list[str] | str | None | _UnsetType = Field(
196+
default=_UNSET,
171197
validation_alias=AliasChoices("stop", "stop_sequences"),
172198
)
173-
n: int | None = Field(
174-
default=None,
199+
n: int | None | _UnsetType = Field(
200+
default=_UNSET,
175201
validation_alias=AliasChoices("n", "candidate_count"),
176202
)
177-
frequency_penalty: float | None = None
178-
presence_penalty: float | None = None
179-
seed: int | None = None
203+
frequency_penalty: float | None | _UnsetType = _UNSET
204+
presence_penalty: float | None | _UnsetType = _UNSET
205+
seed: int | None | _UnsetType = _UNSET
180206

181207
model_kwargs: dict[str, Any] = Field(default_factory=dict)
182208
disabled_params: dict[str, Any] | None = None
183209

184210
# OpenAI
185-
logit_bias: dict[str, int] | None = None
186-
logprobs: bool | None = None
187-
top_logprobs: int | None = None
188-
parallel_tool_calls: bool | None = None
189-
reasoning_effort: str | None = None
190-
reasoning: dict[str, Any] | None = None
211+
logit_bias: dict[str, int] | None | _UnsetType = _UNSET
212+
logprobs: bool | None | _UnsetType = _UNSET
213+
top_logprobs: int | None | _UnsetType = _UNSET
214+
parallel_tool_calls: bool | None | _UnsetType = _UNSET
215+
reasoning_effort: str | None | _UnsetType = _UNSET
216+
reasoning: dict[str, Any] | None | _UnsetType = _UNSET
191217

192218
# Anthropic
193-
thinking: dict[str, Any] | None = None
219+
thinking: dict[str, Any] | None | _UnsetType = _UNSET
194220

195221
# Google
196-
thinking_level: str | None = None
197-
thinking_budget: int | None = None
198-
include_thoughts: bool | None = None
199-
safety_settings: list[dict[str, Any]] | None = None
222+
thinking_level: str | None | _UnsetType = _UNSET
223+
thinking_budget: int | None | _UnsetType = _UNSET
224+
include_thoughts: bool | None | _UnsetType = _UNSET
225+
safety_settings: list[dict[str, Any]] | None | _UnsetType = _UNSET
200226

201227
# Shared
202-
verbosity: str | None = None
228+
verbosity: str | None | _UnsetType = _UNSET
203229

204230
@property
205231
def _llm_type(self) -> str:
@@ -213,13 +239,17 @@ def _identifying_params(self) -> dict[str, Any]:
213239

214240
@property
215241
def _default_params(self) -> dict[str, Any]:
216-
"""Get the default parameters for the normalized API request."""
217-
exclude_if_none = {
242+
"""Get the default parameters for the normalized API request.
243+
244+
Fields default to `_UNSET` and are excluded from the payload only when
245+
still `_UNSET`. Explicit `None` passes through as JSON `null`.
246+
"""
247+
candidates: dict[str, Any] = {
218248
"max_tokens": self.max_tokens,
219249
"temperature": self.temperature,
220250
"top_p": self.top_p,
221251
"top_k": self.top_k,
222-
"stop": self.stop or None,
252+
"stop": self.stop,
223253
"n": self.n,
224254
"frequency_penalty": self.frequency_penalty,
225255
"presence_penalty": self.presence_penalty,
@@ -243,7 +273,7 @@ def _default_params(self) -> dict[str, Any]:
243273
}
244274

245275
return {
246-
**{k: v for k, v in exclude_if_none.items() if v is not None},
276+
**{k: v for k, v in candidates.items() if not isinstance(v, _UnsetType)},
247277
**self.model_kwargs,
248278
}
249279

0 commit comments

Comments
 (0)