Skip to content
Open
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.4] - 2026-09-04

### Fixed
- `temperature` now reaches the dotted gpt-5 variants (`gpt-5.2`, `gpt-5.4`) instead of being silently dropped. langchain-openai strips it for every `gpt-5*` model unless reasoning effort is explicitly `"none"`, but the dotted variants default to effort `none` and do accept it. `Gpt5TemperatureMixin` on `UiPathChatOpenAI` and `UiPathAzureChatOpenAI` overrides both strip sites and defers to langchain elsewhere, so base `gpt-5`, `pro` variants, reasoning-enabled models, and `shouldSkipTemperature` models are unaffected. Remove once langchain-ai/langchain#35424 ships.

## [1.18.3] - 2026-09-02

### Added
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.18.3"
__version__ = "1.18.4"
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pydantic import Field, SecretStr, model_validator

from uipath_langchain_client.base_client import UiPathBaseChatModel
from uipath_langchain_client.clients.openai.gpt5_temperature import Gpt5TemperatureMixin
from uipath_langchain_client.clients.openai.tool_call_extras import (
OpenAIToolCallExtrasMixin,
)
Expand All @@ -27,7 +28,9 @@
) from e


class UiPathChatOpenAI(UiPathBaseChatModel, OpenAIToolCallExtrasMixin, ChatOpenAI): # type: ignore[override]
class UiPathChatOpenAI( # type: ignore[override]
UiPathBaseChatModel, OpenAIToolCallExtrasMixin, Gpt5TemperatureMixin, ChatOpenAI
):
api_config: UiPathAPIConfig = UiPathAPIConfig(
api_type=ApiType.COMPLETIONS,
routing_mode=RoutingMode.PASSTHROUGH,
Expand Down Expand Up @@ -83,7 +86,7 @@ async def on_request_async(request: Request) -> None:
return self


class UiPathAzureChatOpenAI(UiPathBaseChatModel, AzureChatOpenAI): # type: ignore[override]
class UiPathAzureChatOpenAI(UiPathBaseChatModel, Gpt5TemperatureMixin, AzureChatOpenAI): # type: ignore[override]
api_config: UiPathAPIConfig = UiPathAPIConfig(
api_type=ApiType.COMPLETIONS,
routing_mode=RoutingMode.PASSTHROUGH,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Keep ``temperature`` on gpt-5 variants that default to no reasoning.

langchain-openai strips ``temperature`` for every ``gpt-5*`` model unless reasoning
effort is explicitly the string ``"none"``, at two sites in
``langchain_openai.chat_models.base``: ``validate_temperature`` and
``_construct_responses_api_payload``. Unset effort is ``None``, not ``"none"``, so
the value is dropped by default. That is right for base ``gpt-5`` and the ``pro``
variants (default effort medium) but wrong for the dotted ones (``gpt-5.2``,
``gpt-5.4``), which default to effort ``none`` and do accept it.

Delete once langchain-ai/langchain#35424 ships. Upstream: langchain-ai/langchain#35423.
"""

import re
from collections.abc import Mapping
from typing import Any, cast

from langchain_core.language_models import LanguageModelInput
from pydantic import model_validator

_DOTTED_GPT5 = re.compile(r"gpt-5\.\d+")


def gpt5_keeps_temperature(
model: str | None,
*,
reasoning_effort: str | None = None,
reasoning: Mapping[str, Any] | None = None,
) -> bool:
"""Whether this model accepts ``temperature`` as currently configured.

True only for dotted gpt-5 variants with effort unset or ``"none"``. False
everywhere else, so the caller defers to langchain.
"""
name = (model or "").lower()
if "chat" in name or "pro" in name:
return False
if not _DOTTED_GPT5.match(name):
return False
effort = reasoning_effort or (reasoning or {}).get("effort")
return effort is None or effort == "none"


class Gpt5TemperatureMixin:
"""Restore ``temperature`` at both sites langchain-openai strips it.

Mix in ahead of the vendor chat class so the overrides win on the MRO.
"""

@model_validator(mode="before")
@classmethod
def validate_temperature(cls, values: dict[str, Any]) -> Any:
"""Skip langchain's strip when the model does support ``temperature``."""
if gpt5_keeps_temperature(
values.get("model_name") or values.get("model"),
reasoning_effort=values.get("reasoning_effort"),
reasoning=values.get("reasoning"),
):
return values
return cast(Any, super()).validate_temperature(values)

def _get_request_payload(
self,
input_: LanguageModelInput,
*,
stop: list[str] | None = None,
**kwargs: Any,
) -> dict[str, Any]:
"""Put ``temperature`` back after the Responses payload builder drops it."""
payload = cast(
dict[str, Any],
cast(Any, super())._get_request_payload(input_, stop=stop, **kwargs),
)
if "temperature" in payload:
return payload
requested = kwargs.get("temperature", getattr(self, "temperature", None))
if requested is None:
return payload
if "temperature" in (getattr(self, "disabled_params", None) or {}):
return payload
if gpt5_keeps_temperature(payload.get("model"), reasoning=payload.get("reasoning")):
payload["temperature"] = requested
return payload
87 changes: 87 additions & 0 deletions tests/langchain/test_gpt5_temperature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Temperature survives on gpt-5 variants that default to no reasoning.

Covers both sites langchain-openai strips it, and pins the cases where stripping
is correct. Upstream: langchain-ai/langchain#35423.
"""

from typing import Any

import pytest
from langchain_core.messages import HumanMessage
from langchain_openai.chat_models import ChatOpenAI
from pydantic import SecretStr
from uipath_langchain_client.clients.openai.chat_models import (
UiPathAzureChatOpenAI,
UiPathChatOpenAI,
)
from uipath_langchain_client.settings import ApiFlavor

from uipath.llm_client.settings import UiPathBaseSettings

MESSAGES = [HumanMessage(content="hi")]


def _build(client_settings: UiPathBaseSettings, **kwargs: Any) -> Any:
chat_class = kwargs.pop("chat_class", UiPathChatOpenAI)
return chat_class(
model=kwargs.pop("model", "gpt-5.4"),
client_settings=client_settings,
model_details=kwargs.pop("model_details", {}),
api_flavor=ApiFlavor.RESPONSES,
**kwargs,
)


def _payload(chat: Any) -> dict[str, Any]:
return chat._get_request_payload(MESSAGES)


@pytest.mark.parametrize("chat_class", [UiPathChatOpenAI, UiPathAzureChatOpenAI])
def test_dotted_gpt5_keeps_temperature(
chat_class: type, client_settings: UiPathBaseSettings
) -> None:
chat = _build(client_settings, chat_class=chat_class, temperature=0.64)
assert chat.temperature == 0.64
assert _payload(chat)["temperature"] == 0.64


@pytest.mark.parametrize(
"overrides",
[
{"reasoning_effort": "low"},
{"reasoning": {"effort": "low"}},
{"model": "gpt-5"},
{"model": "gpt-5.4-pro"},
],
ids=["effort", "reasoning-dict", "base-gpt-5", "pro"],
)
def test_dropped_where_temperature_is_unsupported(
overrides: dict[str, Any], client_settings: UiPathBaseSettings
) -> None:
chat = _build(client_settings, temperature=0.64, **overrides)
assert chat.temperature is None
assert "temperature" not in _payload(chat)


def test_discovery_skip_flag_blocks_the_restore(
client_settings: UiPathBaseSettings,
) -> None:
chat = _build(client_settings, model_details={"shouldSkipTemperature": True})
assert "temperature" not in chat._get_request_payload(MESSAGES, temperature=0.5)


def test_langchain_handling_still_delegated(client_settings: UiPathBaseSettings) -> None:
assert _build(client_settings, model="o1").temperature == 1
assert _build(client_settings, model="gpt-5-chat", temperature=0.64).temperature == 0.64


def test_upstream_bug_still_present() -> None:
"""Fails when langchain-openai fixes #35423; delete the shim and this file then."""
plain = ChatOpenAI(
model="gpt-5.4",
api_key=SecretStr("x"),
temperature=0.64,
use_responses_api=True,
)
assert plain.temperature is None
assert "temperature" not in plain._get_request_payload(MESSAGES)
Loading