From 4a0d9a5bbc9062e3cf38a4a46adfe36a88cc2f0b Mon Sep 17 00:00:00 2001 From: Cosmin Maria Date: Wed, 22 Apr 2026 14:20:02 +0300 Subject: [PATCH 1/3] 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) --- packages/uipath_langchain_client/CHANGELOG.md | 5 ++ .../uipath_langchain_client/__version__.py | 2 +- .../clients/normalized/chat_models.py | 86 +++++++++++++------ 3 files changed, 64 insertions(+), 29 deletions(-) diff --git a/packages/uipath_langchain_client/CHANGELOG.md b/packages/uipath_langchain_client/CHANGELOG.md index c42e951..86704c6 100644 --- a/packages/uipath_langchain_client/CHANGELOG.md +++ b/packages/uipath_langchain_client/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to `uipath_langchain_client` will be documented in this file. +## [1.9.7] - 2026-04-22 + +### Changed +- **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. + ## [1.9.6] - 2026-04-22 ### Added diff --git a/packages/uipath_langchain_client/src/uipath_langchain_client/__version__.py b/packages/uipath_langchain_client/src/uipath_langchain_client/__version__.py index cc17247..40fb322 100644 --- a/packages/uipath_langchain_client/src/uipath_langchain_client/__version__.py +++ b/packages/uipath_langchain_client/src/uipath_langchain_client/__version__.py @@ -1,3 +1,3 @@ __title__ = "UiPath LangChain Client" __description__ = "A Python client for interacting with UiPath's LLM services via LangChain." -__version__ = "1.9.6" +__version__ = "1.9.7" diff --git a/packages/uipath_langchain_client/src/uipath_langchain_client/clients/normalized/chat_models.py b/packages/uipath_langchain_client/src/uipath_langchain_client/clients/normalized/chat_models.py index 7a26887..5e812db 100644 --- a/packages/uipath_langchain_client/src/uipath_langchain_client/clients/normalized/chat_models.py +++ b/packages/uipath_langchain_client/src/uipath_langchain_client/clients/normalized/chat_models.py @@ -72,6 +72,32 @@ _DictOrPydantic = Union[dict[str, Any], BaseModel] +class _UnsetType: + """Singleton sentinel for request params that should be omitted from the payload. + + `UiPathChat` param fields default to `_UNSET` so we can tell "caller did not + pass anything" apart from "caller explicitly passed `None`". `_default_params` + filters out `_UNSET` values only — explicit `None` (and other falsy values) + flow through to the normalized API as `null`. + """ + + _instance: "_UnsetType | None" = None + + def __new__(cls) -> "_UnsetType": + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __bool__(self) -> Literal[False]: + return False + + def __repr__(self) -> str: + return "_UNSET" + + +_UNSET = _UnsetType() + + def _oai_structured_outputs_parser(ai_msg: AIMessage, schema: type[BaseModel]) -> BaseModel: if not ai_msg.content: raise ValueError("Expected non-empty content from model.") @@ -159,47 +185,47 @@ class UiPathChat(UiPathBaseChatModel): ) # Common - max_tokens: int | None = Field( - default=None, + max_tokens: int | None | _UnsetType = Field( + default=_UNSET, validation_alias=AliasChoices("max_tokens", "max_output_tokens", "max_completion_tokens"), ) - temperature: float | None = None - top_p: float | None = None - top_k: int | None = None - stop: list[str] | str | None = Field( - default=None, + temperature: float | None | _UnsetType = _UNSET + top_p: float | None | _UnsetType = _UNSET + top_k: int | None | _UnsetType = _UNSET + stop: list[str] | str | None | _UnsetType = Field( + default=_UNSET, validation_alias=AliasChoices("stop", "stop_sequences"), ) - n: int | None = Field( - default=None, + n: int | None | _UnsetType = Field( + default=_UNSET, validation_alias=AliasChoices("n", "candidate_count"), ) - frequency_penalty: float | None = None - presence_penalty: float | None = None - seed: int | None = None + frequency_penalty: float | None | _UnsetType = _UNSET + presence_penalty: float | None | _UnsetType = _UNSET + seed: int | None | _UnsetType = _UNSET model_kwargs: dict[str, Any] = Field(default_factory=dict) disabled_params: dict[str, Any] | None = None # OpenAI - logit_bias: dict[str, int] | None = None - logprobs: bool | None = None - top_logprobs: int | None = None - parallel_tool_calls: bool | None = None - reasoning_effort: str | None = None - reasoning: dict[str, Any] | None = None + logit_bias: dict[str, int] | None | _UnsetType = _UNSET + logprobs: bool | None | _UnsetType = _UNSET + top_logprobs: int | None | _UnsetType = _UNSET + parallel_tool_calls: bool | None | _UnsetType = _UNSET + reasoning_effort: str | None | _UnsetType = _UNSET + reasoning: dict[str, Any] | None | _UnsetType = _UNSET # Anthropic - thinking: dict[str, Any] | None = None + thinking: dict[str, Any] | None | _UnsetType = _UNSET # Google - thinking_level: str | None = None - thinking_budget: int | None = None - include_thoughts: bool | None = None - safety_settings: list[dict[str, Any]] | None = None + thinking_level: str | None | _UnsetType = _UNSET + thinking_budget: int | None | _UnsetType = _UNSET + include_thoughts: bool | None | _UnsetType = _UNSET + safety_settings: list[dict[str, Any]] | None | _UnsetType = _UNSET # Shared - verbosity: str | None = None + verbosity: str | None | _UnsetType = _UNSET @property def _llm_type(self) -> str: @@ -213,13 +239,17 @@ def _identifying_params(self) -> dict[str, Any]: @property def _default_params(self) -> dict[str, Any]: - """Get the default parameters for the normalized API request.""" - exclude_if_none = { + """Get the default parameters for the normalized API request. + + Fields default to `_UNSET` and are excluded from the payload only when + still `_UNSET`. Explicit `None` passes through as JSON `null`. + """ + candidates: dict[str, Any] = { "max_tokens": self.max_tokens, "temperature": self.temperature, "top_p": self.top_p, "top_k": self.top_k, - "stop": self.stop or None, + "stop": self.stop, "n": self.n, "frequency_penalty": self.frequency_penalty, "presence_penalty": self.presence_penalty, @@ -243,7 +273,7 @@ def _default_params(self) -> dict[str, Any]: } return { - **{k: v for k, v in exclude_if_none.items() if v is not None}, + **{k: v for k, v in candidates.items() if not isinstance(v, _UnsetType)}, **self.model_kwargs, } From 892fc3f26e8697ca78b5da168a7599667124a721 Mon Sep 17 00:00:00 2001 From: Cosmin Maria Date: Wed, 22 Apr 2026 14:27:47 +0300 Subject: [PATCH 2/3] Feat: use _UNSET sentinel to exclude unset UiPathChat params from payload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fields default to `_UNSET = object()` instead of `None`. `_default_params` filters with `v is not _UNSET` — unset params are omitted, explicit `None` passes through to the API as null. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../clients/normalized/chat_models.py | 80 +++++++------------ 1 file changed, 28 insertions(+), 52 deletions(-) diff --git a/packages/uipath_langchain_client/src/uipath_langchain_client/clients/normalized/chat_models.py b/packages/uipath_langchain_client/src/uipath_langchain_client/clients/normalized/chat_models.py index 5e812db..8f9fb3d 100644 --- a/packages/uipath_langchain_client/src/uipath_langchain_client/clients/normalized/chat_models.py +++ b/packages/uipath_langchain_client/src/uipath_langchain_client/clients/normalized/chat_models.py @@ -62,7 +62,7 @@ convert_to_openai_tool, ) from langchain_core.utils.pydantic import is_basemodel_subclass -from pydantic import AliasChoices, BaseModel, Field +from pydantic import AliasChoices, BaseModel, ConfigDict, Field from uipath_langchain_client.base_client import UiPathBaseChatModel from uipath_langchain_client.settings import ApiType, RoutingMode, UiPathAPIConfig @@ -72,30 +72,8 @@ _DictOrPydantic = Union[dict[str, Any], BaseModel] -class _UnsetType: - """Singleton sentinel for request params that should be omitted from the payload. - - `UiPathChat` param fields default to `_UNSET` so we can tell "caller did not - pass anything" apart from "caller explicitly passed `None`". `_default_params` - filters out `_UNSET` values only — explicit `None` (and other falsy values) - flow through to the normalized API as `null`. - """ - - _instance: "_UnsetType | None" = None - - def __new__(cls) -> "_UnsetType": - if cls._instance is None: - cls._instance = super().__new__(cls) - return cls._instance - - def __bool__(self) -> Literal[False]: - return False - - def __repr__(self) -> str: - return "_UNSET" - - -_UNSET = _UnsetType() +# Sentinel — params whose value is still _UNSET are omitted from the request payload. +_UNSET: Any = object() def _oai_structured_outputs_parser(ai_msg: AIMessage, schema: type[BaseModel]) -> BaseModel: @@ -184,48 +162,50 @@ class UiPathChat(UiPathBaseChatModel): freeze_base_url=True, ) + model_config = ConfigDict(validate_default=False) + # Common - max_tokens: int | None | _UnsetType = Field( + max_tokens: int | None = Field( default=_UNSET, validation_alias=AliasChoices("max_tokens", "max_output_tokens", "max_completion_tokens"), ) - temperature: float | None | _UnsetType = _UNSET - top_p: float | None | _UnsetType = _UNSET - top_k: int | None | _UnsetType = _UNSET - stop: list[str] | str | None | _UnsetType = Field( + temperature: float | None = _UNSET # type: ignore[assignment] + top_p: float | None = _UNSET # type: ignore[assignment] + top_k: int | None = _UNSET # type: ignore[assignment] + stop: list[str] | str | None = Field( default=_UNSET, validation_alias=AliasChoices("stop", "stop_sequences"), ) - n: int | None | _UnsetType = Field( + n: int | None = Field( default=_UNSET, validation_alias=AliasChoices("n", "candidate_count"), ) - frequency_penalty: float | None | _UnsetType = _UNSET - presence_penalty: float | None | _UnsetType = _UNSET - seed: int | None | _UnsetType = _UNSET + frequency_penalty: float | None = _UNSET # type: ignore[assignment] + presence_penalty: float | None = _UNSET # type: ignore[assignment] + seed: int | None = _UNSET # type: ignore[assignment] model_kwargs: dict[str, Any] = Field(default_factory=dict) disabled_params: dict[str, Any] | None = None # OpenAI - logit_bias: dict[str, int] | None | _UnsetType = _UNSET - logprobs: bool | None | _UnsetType = _UNSET - top_logprobs: int | None | _UnsetType = _UNSET - parallel_tool_calls: bool | None | _UnsetType = _UNSET - reasoning_effort: str | None | _UnsetType = _UNSET - reasoning: dict[str, Any] | None | _UnsetType = _UNSET + logit_bias: dict[str, int] | None = _UNSET # type: ignore[assignment] + logprobs: bool | None = _UNSET # type: ignore[assignment] + top_logprobs: int | None = _UNSET # type: ignore[assignment] + parallel_tool_calls: bool | None = _UNSET # type: ignore[assignment] + reasoning_effort: str | None = _UNSET # type: ignore[assignment] + reasoning: dict[str, Any] | None = _UNSET # type: ignore[assignment] # Anthropic - thinking: dict[str, Any] | None | _UnsetType = _UNSET + thinking: dict[str, Any] | None = _UNSET # type: ignore[assignment] # Google - thinking_level: str | None | _UnsetType = _UNSET - thinking_budget: int | None | _UnsetType = _UNSET - include_thoughts: bool | None | _UnsetType = _UNSET - safety_settings: list[dict[str, Any]] | None | _UnsetType = _UNSET + thinking_level: str | None = _UNSET # type: ignore[assignment] + thinking_budget: int | None = _UNSET # type: ignore[assignment] + include_thoughts: bool | None = _UNSET # type: ignore[assignment] + safety_settings: list[dict[str, Any]] | None = _UNSET # type: ignore[assignment] # Shared - verbosity: str | None | _UnsetType = _UNSET + verbosity: str | None = _UNSET # type: ignore[assignment] @property def _llm_type(self) -> str: @@ -239,11 +219,7 @@ def _identifying_params(self) -> dict[str, Any]: @property def _default_params(self) -> dict[str, Any]: - """Get the default parameters for the normalized API request. - - Fields default to `_UNSET` and are excluded from the payload only when - still `_UNSET`. Explicit `None` passes through as JSON `null`. - """ + """Get the default parameters for the normalized API request.""" candidates: dict[str, Any] = { "max_tokens": self.max_tokens, "temperature": self.temperature, @@ -273,7 +249,7 @@ def _default_params(self) -> dict[str, Any]: } return { - **{k: v for k, v in candidates.items() if not isinstance(v, _UnsetType)}, + **{k: v for k, v in candidates.items() if v is not _UNSET}, **self.model_kwargs, } From 7d2bc9528cf81bf3aeda896d1e1609bfa06f6cd9 Mon Sep 17 00:00:00 2001 From: Cosmin Maria Date: Wed, 22 Apr 2026 14:42:09 +0300 Subject: [PATCH 3/3] Refactor: use model_fields_set to exclude unset UiPathChat params Replace the _UNSET sentinel approach with pydantic's built-in model_fields_set. Fields keep plain `int | None = None` defaults; _default_params includes only fields that were explicitly set by the caller. Explicit None passes through to the API as null. No sentinel class, no type: ignore, no extra model_config needed. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/uipath_langchain_client/CHANGELOG.md | 2 +- .../clients/normalized/chat_models.py | 53 +++++++++---------- 2 files changed, 25 insertions(+), 30 deletions(-) diff --git a/packages/uipath_langchain_client/CHANGELOG.md b/packages/uipath_langchain_client/CHANGELOG.md index 86704c6..da3519e 100644 --- a/packages/uipath_langchain_client/CHANGELOG.md +++ b/packages/uipath_langchain_client/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to `uipath_langchain_client` will be documented in this file ## [1.9.7] - 2026-04-22 ### Changed -- **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. +- **Behavior change:** `UiPathChat._default_params` now uses pydantic's `model_fields_set` to decide which params to include in the request payload instead of filtering on `v is not None`. Fields that were not explicitly passed are omitted; fields explicitly set to `None` (e.g. `UiPathChat(temperature=None)`) now forward `null` to the API. Previously both "not passed" and "explicitly `None`" were silently dropped. ## [1.9.6] - 2026-04-22 diff --git a/packages/uipath_langchain_client/src/uipath_langchain_client/clients/normalized/chat_models.py b/packages/uipath_langchain_client/src/uipath_langchain_client/clients/normalized/chat_models.py index 8f9fb3d..88a7040 100644 --- a/packages/uipath_langchain_client/src/uipath_langchain_client/clients/normalized/chat_models.py +++ b/packages/uipath_langchain_client/src/uipath_langchain_client/clients/normalized/chat_models.py @@ -62,7 +62,7 @@ convert_to_openai_tool, ) from langchain_core.utils.pydantic import is_basemodel_subclass -from pydantic import AliasChoices, BaseModel, ConfigDict, Field +from pydantic import AliasChoices, BaseModel, Field from uipath_langchain_client.base_client import UiPathBaseChatModel from uipath_langchain_client.settings import ApiType, RoutingMode, UiPathAPIConfig @@ -72,10 +72,6 @@ _DictOrPydantic = Union[dict[str, Any], BaseModel] -# Sentinel — params whose value is still _UNSET are omitted from the request payload. -_UNSET: Any = object() - - def _oai_structured_outputs_parser(ai_msg: AIMessage, schema: type[BaseModel]) -> BaseModel: if not ai_msg.content: raise ValueError("Expected non-empty content from model.") @@ -162,50 +158,48 @@ class UiPathChat(UiPathBaseChatModel): freeze_base_url=True, ) - model_config = ConfigDict(validate_default=False) - # Common max_tokens: int | None = Field( - default=_UNSET, + default=None, validation_alias=AliasChoices("max_tokens", "max_output_tokens", "max_completion_tokens"), ) - temperature: float | None = _UNSET # type: ignore[assignment] - top_p: float | None = _UNSET # type: ignore[assignment] - top_k: int | None = _UNSET # type: ignore[assignment] + temperature: float | None = None + top_p: float | None = None + top_k: int | None = None stop: list[str] | str | None = Field( - default=_UNSET, + default=None, validation_alias=AliasChoices("stop", "stop_sequences"), ) n: int | None = Field( - default=_UNSET, + default=None, validation_alias=AliasChoices("n", "candidate_count"), ) - frequency_penalty: float | None = _UNSET # type: ignore[assignment] - presence_penalty: float | None = _UNSET # type: ignore[assignment] - seed: int | None = _UNSET # type: ignore[assignment] + frequency_penalty: float | None = None + presence_penalty: float | None = None + seed: int | None = None model_kwargs: dict[str, Any] = Field(default_factory=dict) disabled_params: dict[str, Any] | None = None # OpenAI - logit_bias: dict[str, int] | None = _UNSET # type: ignore[assignment] - logprobs: bool | None = _UNSET # type: ignore[assignment] - top_logprobs: int | None = _UNSET # type: ignore[assignment] - parallel_tool_calls: bool | None = _UNSET # type: ignore[assignment] - reasoning_effort: str | None = _UNSET # type: ignore[assignment] - reasoning: dict[str, Any] | None = _UNSET # type: ignore[assignment] + logit_bias: dict[str, int] | None = None + logprobs: bool | None = None + top_logprobs: int | None = None + parallel_tool_calls: bool | None = None + reasoning_effort: str | None = None + reasoning: dict[str, Any] | None = None # Anthropic - thinking: dict[str, Any] | None = _UNSET # type: ignore[assignment] + thinking: dict[str, Any] | None = None # Google - thinking_level: str | None = _UNSET # type: ignore[assignment] - thinking_budget: int | None = _UNSET # type: ignore[assignment] - include_thoughts: bool | None = _UNSET # type: ignore[assignment] - safety_settings: list[dict[str, Any]] | None = _UNSET # type: ignore[assignment] + thinking_level: str | None = None + thinking_budget: int | None = None + include_thoughts: bool | None = None + safety_settings: list[dict[str, Any]] | None = None # Shared - verbosity: str | None = _UNSET # type: ignore[assignment] + verbosity: str | None = None @property def _llm_type(self) -> str: @@ -248,8 +242,9 @@ def _default_params(self) -> dict[str, Any]: "verbosity": self.verbosity, } + set_fields = self.model_fields_set return { - **{k: v for k, v in candidates.items() if v is not _UNSET}, + **{k: v for k, v in candidates.items() if k in set_fields}, **self.model_kwargs, }