Skip to content

Commit a2c7c6e

Browse files
cosminachoclaude
andcommitted
Refactor: move sampling helpers to core utils + resolve model_details via field_validator
- New src/uipath/llm_client/utils/sampling.py in the core package exports DISABLED_SAMPLING_PARAMS, should_skip_sampling, and strip_disabled_sampling_kwargs. These are framework-agnostic and fit the existing core utils pattern (one file per concern). - Langchain's utils.py re-exports them, so the public import path uipath_langchain_client.utils.strip_disabled_sampling_kwargs is preserved. - model_details resolution is now a @field_validator("model_details", mode="after") collocated with the field declaration. It reads already-validated client_settings and model_name off info.data and calls get_model_info, instead of living in a separate @model_validator method. - Core version 1.9.9 -> 1.10.0 with changelog entry; langchain's core-dep floor bumped to >=1.10.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b60296a commit a2c7c6e

8 files changed

Lines changed: 62 additions & 32 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.10.0] - 2026-04-23
6+
7+
### Added
8+
- `uipath.llm_client.utils.sampling` module exposing `DISABLED_SAMPLING_PARAMS`, `should_skip_sampling(model_details)`, and `strip_disabled_sampling_kwargs(...)`. Centralizes the gateway's rule that `modelDetails.shouldSkipTemperature=True` implies the full sampling set is rejected (temperature, top_p, top_k, frequency/presence penalty, seed, logit_bias, logprobs, top_logprobs). Framework-agnostic helpers intended for reuse by any wrapper layer.
9+
510
## [1.9.9] - 2026-04-23
611

712
### Changed

packages/uipath_langchain_client/CHANGELOG.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ All notable changes to `uipath_langchain_client` will be documented in this file
55
## [1.10.0] - 2026-04-23
66

77
### Added
8-
- `UiPathBaseChatModel` now strips sampling kwargs (`temperature`, `top_p`, `top_k`, `frequency_penalty`, `presence_penalty`, `seed`, `logit_bias`, `logprobs`, `top_logprobs`) at invocation time when the model's `modelDetails.shouldSkipTemperature` is true. Fixes `anthropic.claude-opus-4-7` rejecting any sampling parameter passed to `.invoke()` / `.ainvoke()` / streams.
9-
- `model_details` field on `UiPathBaseLLMClient`, populated eagerly: `get_chat_model` forwards it from the discovery response it already fetches; direct instantiation resolves it in `model_post_init` via `client_settings.get_model_info` (backed by the class-cached discovery response, so at most one network call per process). Each strip logs a warning via `self.logger` when one is configured.
8+
- `UiPathBaseChatModel` now strips sampling kwargs (`temperature`, `top_p`, `top_k`, `frequency_penalty`, `presence_penalty`, `seed`, `logit_bias`, `logprobs`, `top_logprobs`) at invocation time when the model's `modelDetails.shouldSkipTemperature` is true. Fixes `anthropic.claude-opus-4-7` rejecting any sampling parameter passed to `.invoke()` / `.ainvoke()` / streams. The shared helpers live in the core package at `uipath.llm_client.utils.sampling` and are re-exported from `uipath_langchain_client.utils`.
9+
- `model_details` field on `UiPathBaseLLMClient`, populated eagerly via a `@field_validator("model_details", mode="after")`: `get_chat_model` forwards it from the discovery response it already fetches; direct instantiation resolves it via `client_settings.get_model_info` (backed by the class-cached discovery response, so at most one network call per process). Each strip logs a warning via `self.logger` when one is configured.
10+
11+
### Changed
12+
- Bumped `uipath-llm-client` floor to `>=1.10.0` to match the release that adds `uipath.llm_client.utils.sampling`.
1013

1114
## [1.9.9] - 2026-04-23
1215

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.9.9,<2.0.0",
9+
"uipath-llm-client>=1.10.0,<2.0.0",
1010
]
1111

1212
[project.optional-dependencies]

packages/uipath_langchain_client/src/uipath_langchain_client/base_client.py

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from abc import ABC
2828
from collections.abc import AsyncGenerator, Generator, Mapping, Sequence
2929
from functools import cached_property
30-
from typing import Any, ClassVar, Literal, Self
30+
from typing import Any, ClassVar, Literal
3131

3232
from httpx import URL, Response
3333
from langchain_core.callbacks import (
@@ -38,7 +38,14 @@
3838
from langchain_core.language_models.chat_models import BaseChatModel
3939
from langchain_core.messages import BaseMessage
4040
from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult
41-
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, model_validator
41+
from pydantic import (
42+
AliasChoices,
43+
BaseModel,
44+
ConfigDict,
45+
Field,
46+
ValidationInfo,
47+
field_validator,
48+
)
4249

4350
from uipath.llm_client.httpx_client import (
4451
UiPathHttpxAsyncClient,
@@ -49,13 +56,12 @@
4956
get_captured_response_headers,
5057
set_captured_response_headers,
5158
)
52-
from uipath_langchain_client._sampling import strip_disabled_sampling_kwargs
5359
from uipath_langchain_client.settings import (
5460
UiPathAPIConfig,
5561
UiPathBaseSettings,
5662
get_default_client_settings,
5763
)
58-
from uipath_langchain_client.utils import RetryConfig
64+
from uipath_langchain_client.utils import RetryConfig, strip_disabled_sampling_kwargs
5965

6066

6167
class UiPathBaseLLMClient(BaseModel, ABC):
@@ -112,10 +118,36 @@ class UiPathBaseLLMClient(BaseModel, ABC):
112118
model_details: dict[str, Any] | None = Field(
113119
default=None,
114120
description="Per-model capability flags sourced from the discovery endpoint "
115-
"(e.g. {'shouldSkipTemperature': True}). The factory forwards it; direct "
116-
"instantiation lazy-resolves it from client_settings on first construction.",
121+
"(e.g. {'shouldSkipTemperature': True}). The factory forwards it; when absent, "
122+
"the field validator below eagerly resolves it from client_settings.",
117123
)
118124

125+
@field_validator("model_details", mode="after")
126+
@classmethod
127+
def _resolve_model_details(
128+
cls, value: dict[str, Any] | None, info: ValidationInfo
129+
) -> dict[str, Any]:
130+
# Fields validate in declaration order, so by the time this runs both
131+
# ``client_settings`` and ``model_name`` are already in ``info.data``.
132+
# Eager resolution here keeps direct instantiation and the factory
133+
# path consistent. ``get_available_models`` is class-cached inside
134+
# the settings layer, so at most one discovery HTTP call fires per
135+
# process regardless of how many chat/embedding models are built.
136+
if value is not None:
137+
return value
138+
settings = info.data.get("client_settings")
139+
model_name = info.data.get("model_name")
140+
if settings is None or not model_name:
141+
return {}
142+
try:
143+
model_info = settings.get_model_info(
144+
model_name,
145+
byo_connection_id=info.data.get("byo_connection_id"),
146+
)
147+
return model_info.get("modelDetails") or {}
148+
except Exception:
149+
return {}
150+
119151
default_headers: Mapping[str, str] | None = Field(
120152
default=None,
121153
description="Caller-supplied request headers. Merged on top of `class_default_headers`; "
@@ -148,25 +180,6 @@ class UiPathBaseLLMClient(BaseModel, ABC):
148180
description="Logger for request/response logging",
149181
)
150182

151-
@model_validator(mode="after")
152-
def _resolve_model_details(self) -> Self:
153-
# Populate model_details eagerly so direct instantiation behaves the
154-
# same as the factory path. get_available_models is class-cached inside
155-
# the settings layer, so at most one discovery HTTP call fires per
156-
# process regardless of how many chat/embedding models are built.
157-
# Placed on UiPathBaseLLMClient (not just the chat subclass) because
158-
# model_details is meaningful for embedding wrappers too.
159-
if self.model_details is None:
160-
try:
161-
info = self.client_settings.get_model_info(
162-
self.model_name,
163-
byo_connection_id=self.byo_connection_id,
164-
)
165-
self.model_details = info.get("modelDetails") or {}
166-
except Exception:
167-
self.model_details = {}
168-
return self
169-
170183
@cached_property
171184
def uipath_sync_client(self) -> UiPathHttpxClient:
172185
"""Here we instantiate a synchronous HTTP client with the proper authentication pipeline, retry logic, logging etc."""

packages/uipath_langchain_client/src/uipath_langchain_client/utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
is_anthropic_model_name,
1919
)
2020
from uipath.llm_client.utils.retry import RetryConfig
21+
from uipath.llm_client.utils.sampling import (
22+
DISABLED_SAMPLING_PARAMS,
23+
should_skip_sampling,
24+
strip_disabled_sampling_kwargs,
25+
)
2126

2227
__all__ = [
2328
"RetryConfig",
@@ -36,4 +41,7 @@
3641
"UiPathTooManyRequestsError",
3742
"ANTHROPIC_MODEL_NAME_KEYWORDS",
3843
"is_anthropic_model_name",
44+
"DISABLED_SAMPLING_PARAMS",
45+
"should_skip_sampling",
46+
"strip_disabled_sampling_kwargs",
3947
]
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.9.9"
3+
__version__ = "1.10.0"

packages/uipath_langchain_client/src/uipath_langchain_client/_sampling.py renamed to src/uipath/llm_client/utils/sampling.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
Reasoning-style models (e.g. ``anthropic.claude-opus-4-7``) advertise
44
``modelDetails.shouldSkipTemperature: true`` on the discovery endpoint. When
55
that flag is set, the gateway rejects the entire sampling set, not just
6-
``temperature``. The helpers here centralize that knowledge so every chat
7-
wrapper can reuse them via ``UiPathBaseChatModel``.
6+
``temperature``. The helpers here centralize that knowledge so every framework
7+
wrapper (LangChain chat models, future LlamaIndex wrappers, the core
8+
normalized client, etc.) can reuse the same rule.
89
"""
910

1011
from __future__ import annotations

tests/langchain/test_disabled_sampling_params.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
import pytest
1515
from langchain_core.messages import AIMessage
1616
from langchain_core.outputs import ChatGeneration, ChatResult
17-
from uipath_langchain_client._sampling import DISABLED_SAMPLING_PARAMS
1817
from uipath_langchain_client.clients.normalized.chat_models import UiPathChat
1918
from uipath_langchain_client.factory import get_chat_model
19+
from uipath_langchain_client.utils import DISABLED_SAMPLING_PARAMS
2020

2121
from uipath.llm_client.settings import UiPathBaseSettings
2222

0 commit comments

Comments
 (0)