Skip to content
Merged
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
2 changes: 1 addition & 1 deletion packages/uipath-platform/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — correctness: OpenAI's o1/o3/o4 reasoning models also reject max_tokens (400: "Unsupported parameter: 'max_tokens' is not supported with this model. Use 'max_completion_tokens' instead.") — the same class of error this PR fixes for temperature/n/frequency_penalty/presence_penalty/top_p. Both chat_completions methods still send "max_tokens": max_tokens unconditionally for reasoning models (also at _llm_gateway_service.py:563 in UiPathLlmChatService).

If the LLM Gateway forwards this field to OpenAI unchanged on the passthrough endpoint, SRE-625200 may not be fully closed — reasoning-model calls could still 400, just on a different field. Worth confirming whether the gateway already translates max_tokensmax_completion_tokens server-side for o-series models; if not, this needs the same treatment as the other params.

if is_reasoning_model:
    request_body["max_completion_tokens"] = max_tokens
else:
    request_body["max_tokens"] = max_tokens

(Codex recheck: agree — flagged the same gateway-translation uncertainty as a risk callout.)

"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(
Expand Down Expand Up @@ -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"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using model name is unreliable sadly. But I think this is a best effort we can live with.


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
Expand Down
223 changes: 223 additions & 0 deletions packages/uipath-platform/tests/services/test_llm_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -543,3 +641,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:
Comment thread
AAgnihotry marked this conversation as resolved.
"""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
2 changes: 1 addition & 1 deletion packages/uipath-platform/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/uipath/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
4 changes: 2 additions & 2 deletions packages/uipath/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading