From 29fc0f5dc8fbbdadfb0b62cd34607c037f6cb2a9 Mon Sep 17 00:00:00 2001 From: Anipik Date: Mon, 20 Jul 2026 06:28:29 -0700 Subject: [PATCH 1/3] fix(llm): exclude temperature and sampling params for reasoning models (o1, o3, o4) Bump uipath to 2.13.13. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../platform/chat/_llm_gateway_service.py | 26 ++-- .../tests/services/test_llm_service.py | 125 ++++++++++++++++++ packages/uipath/pyproject.toml | 2 +- packages/uipath/uv.lock | 2 +- 4 files changed, 145 insertions(+), 10 deletions(-) diff --git a/packages/uipath-platform/src/uipath/platform/chat/_llm_gateway_service.py b/packages/uipath-platform/src/uipath/platform/chat/_llm_gateway_service.py index e07ce68aa..00e373d60 100644 --- a/packages/uipath-platform/src/uipath/platform/chat/_llm_gateway_service.py +++ b/packages/uipath-platform/src/uipath/platform/chat/_llm_gateway_service.py @@ -327,12 +327,17 @@ class Country(BaseModel): ) endpoint = Endpoint("/" + endpoint) - request_body = { + is_reasoning_model = model.lower().startswith(("o1", "o3", "o4")) + + request_body: dict[str, Any] = { "messages": messages, "max_tokens": max_tokens, - "temperature": temperature, } + # Reasoning models (o1, o3, o4) don't support temperature + if not is_reasoning_model: + request_body["temperature"] = temperature + # Handle response_format - convert BaseModel to schema if needed if response_format: if isinstance(response_format, type) and issubclass( @@ -548,17 +553,22 @@ class Country(BaseModel): ) endpoint = Endpoint("/" + endpoint) - # Build request body - Claude models don't support some OpenAI-specific parameters - is_claude_model = "claude" in model.lower() + # Build request body - some models don't support certain parameters + model_lower = model.lower() + is_claude_model = "claude" in model_lower + is_reasoning_model = model_lower.startswith(("o1", "o3", "o4")) - request_body = { + request_body: dict[str, Any] = { "messages": converted_messages, "max_tokens": max_tokens, - "temperature": temperature, } - # Only add OpenAI-specific parameters for non-Claude models - if not is_claude_model: + # Reasoning models (o1, o3, o4) don't support temperature and sampling parameters + if not is_reasoning_model: + request_body["temperature"] = temperature + + # Only add OpenAI-specific parameters for non-Claude and non-reasoning models + if not is_claude_model and not is_reasoning_model: request_body["n"] = n request_body["frequency_penalty"] = frequency_penalty request_body["presence_penalty"] = presence_penalty diff --git a/packages/uipath-platform/tests/services/test_llm_service.py b/packages/uipath-platform/tests/services/test_llm_service.py index e1ab8299b..0e50b4060 100644 --- a/packages/uipath-platform/tests/services/test_llm_service.py +++ b/packages/uipath-platform/tests/services/test_llm_service.py @@ -543,3 +543,128 @@ async def test_claude_sonnet_45_excluded_params(self, mock_request, llm_service) assert "presence_penalty" not in request_body assert "top_p" not in request_body assert request_body["max_tokens"] == 8000 + + +class TestNormalizedLlmServiceReasoningModelFiltering: + """Test that reasoning models (o1, o3, o4) correctly filter out unsupported sampling parameters. + + OpenAI reasoning models do NOT support temperature, top_p, frequency_penalty, + presence_penalty, or n, and sending them causes 400 errors. + """ + + @pytest.fixture + def config(self): + return UiPathApiConfig(base_url="https://example.com", secret="test_secret") + + @pytest.fixture + def execution_context(self): + return UiPathExecutionContext() + + @pytest.fixture + def llm_service(self, config, execution_context): + from uipath.platform.chat._llm_gateway_service import UiPathLlmChatService + + return UiPathLlmChatService(config=config, execution_context=execution_context) + + @pytest.mark.parametrize( + "model", + [ + "o3-mini-2025-01-31", + "o4-mini-2025-04-16", + "o1-2024-12-17", + "o1-mini-2024-09-12", + "o3-2025-04-16", + ], + ) + @patch( + "uipath.platform.chat._llm_gateway_service.UiPathLlmChatService.request_async" + ) + @pytest.mark.asyncio + async def test_reasoning_model_excludes_sampling_params( + self, mock_request, llm_service, model + ): + """Test that reasoning models do not include temperature or other sampling params.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "id": "chatcmpl-test", + "object": "chat.completion", + "created": 1234567890, + "model": model, + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "Hello"}, + "finish_reason": "stop", + } + ], + "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}, + } + mock_request.return_value = mock_response + + await llm_service.chat_completions( + messages=[{"role": "user", "content": "Hello"}], + model=model, + max_tokens=1000, + ) + + call_kwargs = mock_request.call_args[1] + request_body = call_kwargs["json"] + + assert "temperature" not in request_body, ( + f"Reasoning model {model} request must not include 'temperature'" + ) + assert "n" not in request_body, ( + f"Reasoning model {model} request must not include 'n'" + ) + assert "frequency_penalty" not in request_body, ( + f"Reasoning model {model} request must not include 'frequency_penalty'" + ) + assert "presence_penalty" not in request_body, ( + f"Reasoning model {model} request must not include 'presence_penalty'" + ) + assert "top_p" not in request_body, ( + f"Reasoning model {model} request must not include 'top_p'" + ) + assert request_body["max_tokens"] == 1000 + + @patch( + "uipath.platform.chat._llm_gateway_service.UiPathLlmChatService.request_async" + ) + @pytest.mark.asyncio + async def test_non_reasoning_model_includes_temperature( + self, mock_request, llm_service + ): + """Test that non-reasoning models still include temperature and sampling params.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "id": "chatcmpl-test", + "object": "chat.completion", + "created": 1234567890, + "model": "gpt-4.1-mini-2025-04-14", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "Hello"}, + "finish_reason": "stop", + } + ], + "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}, + } + mock_request.return_value = mock_response + + await llm_service.chat_completions( + messages=[{"role": "user", "content": "Hello"}], + model="gpt-4.1-mini-2025-04-14", + max_tokens=1000, + temperature=0.5, + ) + + call_kwargs = mock_request.call_args[1] + request_body = call_kwargs["json"] + + assert request_body["temperature"] == 0.5 + assert "n" in request_body + assert "frequency_penalty" in request_body + assert "presence_penalty" in request_body diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 0df94ca0c..2e5404478 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.13.12" +version = "2.13.13" description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools." readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index f103aeec0..39c684b9c 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2598,7 +2598,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.13.12" +version = "2.13.13" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From f52e44888bf2cb4d0cf505e01b6e5259ce0c88ba Mon Sep 17 00:00:00 2001 From: Anipik Date: Mon, 20 Jul 2026 06:34:23 -0700 Subject: [PATCH 2/3] fix: bump uipath-platform to 0.2.11 (version already exists on PyPI) Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/uipath-platform/pyproject.toml | 2 +- packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index f5c7522b9..cf773124f 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.2.10" +version = "0.2.11" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 744c292d3..327bd226d 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.2.10" +version = "0.2.11" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 39c684b9c..9a413a7cb 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2741,7 +2741,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.2.10" +version = "0.2.11" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 4e641021012d0ea7309c76a8c617c4b05b7d3fab Mon Sep 17 00:00:00 2001 From: Anipik Date: Mon, 20 Jul 2026 06:45:58 -0700 Subject: [PATCH 3/3] test: add UiPathOpenAIService reasoning model temperature filtering tests Covers the passthrough (non-normalized) API path that also conditionally omits temperature for o1/o3/o4 models. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../tests/services/test_llm_service.py | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/packages/uipath-platform/tests/services/test_llm_service.py b/packages/uipath-platform/tests/services/test_llm_service.py index 0e50b4060..4a6da2789 100644 --- a/packages/uipath-platform/tests/services/test_llm_service.py +++ b/packages/uipath-platform/tests/services/test_llm_service.py @@ -384,6 +384,104 @@ class Article(BaseModel): assert article_instance.title is None +class TestOpenAIServiceReasoningModelFiltering: + """Test that UiPathOpenAIService correctly filters out temperature for reasoning models.""" + + @pytest.fixture + def config(self): + return UiPathApiConfig(base_url="https://example.com", secret="test_secret") + + @pytest.fixture + def execution_context(self): + return UiPathExecutionContext() + + @pytest.fixture + def openai_service(self, config, execution_context): + return UiPathOpenAIService(config=config, execution_context=execution_context) + + @pytest.mark.parametrize( + "model", + [ + "o3-mini-2025-01-31", + "o4-mini-2025-04-16", + "o1-2024-12-17", + "o1-mini-2024-09-12", + "o3-2025-04-16", + ], + ) + @patch.object(UiPathOpenAIService, "request_async") + @pytest.mark.asyncio + async def test_reasoning_model_excludes_temperature( + self, mock_request, openai_service, model + ): + """Test that reasoning models do not include temperature in the request body.""" + mock_response = MagicMock() + mock_response.json.return_value = { + "id": "chatcmpl-test", + "object": "chat.completion", + "created": 1234567890, + "model": model, + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "Hello"}, + "finish_reason": "stop", + } + ], + "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}, + } + mock_request.return_value = mock_response + + await openai_service.chat_completions( + messages=[{"role": "user", "content": "Hello"}], + model=model, + max_tokens=1000, + ) + + call_kwargs = mock_request.call_args[1] + request_body = call_kwargs["json"] + + assert "temperature" not in request_body, ( + f"Reasoning model {model} request must not include 'temperature'" + ) + assert request_body["max_tokens"] == 1000 + + @patch.object(UiPathOpenAIService, "request_async") + @pytest.mark.asyncio + async def test_non_reasoning_model_includes_temperature( + self, mock_request, openai_service + ): + """Test that non-reasoning models still include temperature.""" + mock_response = MagicMock() + mock_response.json.return_value = { + "id": "chatcmpl-test", + "object": "chat.completion", + "created": 1234567890, + "model": "gpt-4.1-mini-2025-04-14", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "Hello"}, + "finish_reason": "stop", + } + ], + "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}, + } + mock_request.return_value = mock_response + + await openai_service.chat_completions( + messages=[{"role": "user", "content": "Hello"}], + model="gpt-4.1-mini-2025-04-14", + max_tokens=1000, + temperature=0.7, + ) + + call_kwargs = mock_request.call_args[1] + request_body = call_kwargs["json"] + + assert request_body["temperature"] == 0.7 + + class TestNormalizedLlmServiceClaudeFiltering: """Test that Claude models correctly filter out OpenAI-specific parameters.