Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/uipath_langchain_client/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

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

## [1.18.0] - 2026-07-31

### Added
- `model_settings` field on `UiPathBaseChatModel` and a matching `model_settings` param on `get_chat_model`. Provider-native settings from agent.json's `settings.modelSettings` are applied verbatim: a key matching a native field is set directly on the model, anything else routes to `model_kwargs`, and keys listed in `disabled_params` are skipped. No per-provider mapping — discovery is the source of truth for the shape.

## [1.17.1] - 2026-07-17

### Changed
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__title__ = "UiPath LangChain Client"
__description__ = "A Python client for interacting with UiPath's LLM services via LangChain."
__version__ = "1.17.1"
__version__ = "1.18.0"
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,45 @@ class UiPathBaseChatModel(UiPathBaseLLMClient, BaseChatModel):
so that headers are captured transparently.
"""

model_settings: Mapping[str, Any] | None = Field(
default=None,
description="Provider-native model settings from agent.json "
"(settings.modelSettings), applied verbatim — no per-provider mapping.",
)

@model_validator(mode="after")
def apply_model_settings(self) -> Self:
self._apply_model_settings()
return self

def _apply_model_settings(self) -> None:
"""Apply each ``model_settings`` key onto the model.

Set directly when it's a native field, else routed to ``model_kwargs``;
keys in ``disabled_params`` are skipped.
"""
if not self.model_settings:
return
fields = type(self).model_fields
disabled = self.disabled_params or {}
extra: dict[str, Any] = {}
for key, value in self.model_settings.items():
if key in disabled:
continue
if key in fields:
setattr(self, key, value)
else:
extra[key] = value
if extra:
if "model_kwargs" in fields:
self.model_kwargs = {**(self.model_kwargs or {}), **extra}
else:
(self.logger or logging.getLogger(__name__)).debug(
"Dropping unsupported model settings %s for %s",
list(extra),
type(self).__name__,
)

def _generate(
self,
messages: list[BaseMessage],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
>>> embeddings = get_embedding_model(model_name="text-embedding-3-large", client_settings=settings)
"""

from collections.abc import Mapping
from typing import Any

from uipath_langchain_client.base_client import (
Expand Down Expand Up @@ -48,6 +49,7 @@ def get_chat_model(
api_flavor: ApiFlavor | str | None = None,
custom_class: type[UiPathBaseChatModel] | None = None,
agenthub_config: str | None = None,
model_settings: Mapping[str, Any] | None = None,
**model_kwargs: Any,
) -> UiPathBaseChatModel:
"""Factory function to create the appropriate LangChain chat model for a given model name.
Expand Down Expand Up @@ -93,6 +95,9 @@ def get_chat_model(
model_family = model_info.get("modelFamily", None)
model_details = model_info.get("modelDetails") or {}

if model_settings is not None:
model_kwargs["model_settings"] = model_settings

if custom_class is not None:
return custom_class(
model=model_name,
Expand Down
126 changes: 126 additions & 0 deletions tests/langchain/features/test_factory_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,3 +477,129 @@ def test_invoke_byo_alias_gets_provider(self, client_settings):
assert model.base_model_id == "anthropic.claude-sonnet-4-5-20250929-v1:0"
assert model.provider == "anthropic"
assert model._get_provider() == "anthropic"


class TestModelSettingsForwarding:
"""get_chat_model forwards model_settings into the chosen client's constructor."""

def test_factory_forwards_model_settings_to_constructor(self, monkeypatch: pytest.MonkeyPatch):
settings = MagicMock()
settings.get_model_info.return_value = {
"modelName": "gpt-4o",
"vendor": "OpenAi",
"apiFlavor": "responses",
"modelFamily": "OpenAi",
}
captured: dict = {}

class _StubModel:
def __init__(self, **kwargs):
captured.update(kwargs)

monkeypatch.setattr(
"uipath_langchain_client.clients.openai.chat_models.UiPathAzureChatOpenAI",
_StubModel,
)
get_chat_model(
model_name="gpt-4o",
client_settings=settings,
model_settings={"reasoning_effort": "high", "temperature": 1.0},
)
assert captured["model_settings"] == {
"reasoning_effort": "high",
"temperature": 1.0,
}


class TestModelSettingsApplied:
"""model_settings is applied during real construction (via the model_validator).

Native provider keys land as real fields (no per-provider mapping); unknown keys
route to model_kwargs; keys named in disabled_params are skipped.
"""

@pytest.fixture()
def settings(self) -> UiPathBaseSettings:
import os
from unittest.mock import patch

from uipath.llm_client.settings.llmgateway import LLMGatewaySettings

env = {
"LLMGW_URL": "http://test",
"LLMGW_SEMANTIC_ORG_ID": "org",
"LLMGW_SEMANTIC_TENANT_ID": "tenant",
"LLMGW_REQUESTING_PRODUCT": "test",
"LLMGW_REQUESTING_FEATURE": "test",
"LLMGW_ACCESS_TOKEN": "dummy-token",
}
with patch.dict(os.environ, env, clear=True):
return LLMGatewaySettings()

def test_openai_native_key_set_unknown_key_to_model_kwargs(self, settings: UiPathBaseSettings):
from uipath_langchain_client.clients.openai.chat_models import UiPathChatOpenAI

model = UiPathChatOpenAI(
model="some-openai-model",
settings=settings,
model_details={},
model_settings={"reasoning_effort": "high", "made_up_key": 1},
)
assert model.reasoning_effort == "high"
assert model.model_kwargs == {"made_up_key": 1}

def test_anthropic_native_keys_set_verbatim(self, settings: UiPathBaseSettings):
from uipath_langchain_client.clients.anthropic.chat_models import (
UiPathChatAnthropic,
)

model = UiPathChatAnthropic(
model="anthropic.claude-sonnet-4-6",
settings=settings,
model_details={},
model_settings={
"thinking": {"type": "adaptive"},
"output_config": {"effort": "high"},
},
)
assert model.thinking == {"type": "adaptive"}
assert model.output_config == {"effort": "high"}

def test_bedrock_additional_model_request_fields_set_verbatim(
self, settings: UiPathBaseSettings
):
UiPathBaseSettings._discovery_cache.clear()
settings._discovery_cache[settings._discovery_cache_key()] = [
{
"modelName": "AWS - Bedrock",
"vendor": "Bedrock",
"apiFlavor": "AwsBedrockConverse",
"modelFamily": "Anthropic",
"modelDetails": {"customerModelName": "anthropic.claude-sonnet-4-5-20250929-v1:0"},
}
]
amrf = {"thinking": {"type": "enabled", "budget_tokens": 4096}}
try:
model = UiPathChatBedrockConverse(
model="AWS - Bedrock",
settings=settings,
byo_connection_id="conn-x",
base_model="anthropic.claude-sonnet-4-5-20250929-v1:0",
provider="anthropic",
model_settings={"additional_model_request_fields": amrf},
)
finally:
UiPathBaseSettings._discovery_cache.clear()
assert model.additional_model_request_fields == amrf

def test_disabled_key_is_skipped(self, settings: UiPathBaseSettings):
from uipath_langchain_client.clients.openai.chat_models import UiPathChatOpenAI

model = UiPathChatOpenAI(
model="some-openai-model",
settings=settings,
model_details={},
disabled_params={"temperature": None},
model_settings={"temperature": 0.2},
)
assert model.temperature is None
Loading