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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

All notable changes to `uipath_llm_client` (core package) will be documented in this file.

## [1.13.0] - 2026-05-27

### Added
- `UiPathRequestTimeoutError` (HTTP 408) and `UiPathBadGatewayError` (HTTP 502) exception classes. Both are registered in `_STATUS_CODE_TO_EXCEPTION`, re-exported from `uipath.llm_client`, and inherit from `UiPathAPIError` for compatibility with existing handlers.

### Changed
- **Default retry set expanded.** `_DEFAULT_RETRY_ON_EXCEPTIONS` in `uipath.llm_client.utils.retry` now covers `UiPathRequestTimeoutError` (408), `UiPathRateLimitError` (429), `UiPathBadGatewayError` (502), `UiPathServiceUnavailableError` (503), `UiPathGatewayTimeoutError` (504), and `UiPathTooManyRequestsError` (529) — up from `{429, 529}`. Applies to every provider client (`UiPathOpenAI`, `UiPathAnthropic*`, `UiPathGoogle`) since they all share the same `UiPathHttpxClient`-backed retry transport. `Retry-After` / `x-retry-after` headers and exponential backoff with jitter behave as before.
- **`UiPathHttpxClient` / `UiPathHttpxAsyncClient` default `max_retries` raised from `0` to `3`.** Callers that pass `max_retries=None` (or omit it entirely) now get 3 retries by default. Pass `max_retries=0` explicitly to opt out — `max_retries=0` continues to disable retries, so the opt-out path is unchanged.

## [1.12.2] - 2026-05-24

### Changed
Expand Down
6 changes: 6 additions & 0 deletions packages/uipath_langchain_client/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

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

## [1.13.0] - 2026-05-27

### Changed
- **`UiPathBaseLLMClient.max_retries` field default raised from `0` to `3`.** Every LangChain chat and embedding client built on this base (`UiPathChat`, `UiPathChatOpenAI`, `UiPathAzureChatOpenAI`, `UiPathChatAnthropic`, `UiPathChatAnthropicBedrock`, `UiPathChatBedrock`, `UiPathChatBedrockConverse`, `UiPathChatVertexAI`, `UiPathChatFireworks`, `UiPathChatLiteLLM`, plus the matching embeddings classes) now retries failed requests 3 times by default. Pass `max_retries=0` explicitly to disable retries — the opt-out path is unchanged. Combined with the expanded default retry set in `uipath-llm-client` 1.13.0, every LangChain client now retries on HTTP 408, 429, 502, 503, 504, and 529 out of the box.
- Bumped `uipath-llm-client` floor to `>=1.13.0` to pick up the expanded default retry set and the new `UiPathRequestTimeoutError` / `UiPathBadGatewayError` typed exceptions.

## [1.12.2] - 2026-05-24

### Changed
Expand Down
2 changes: 1 addition & 1 deletion packages/uipath_langchain_client/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"langchain>=1.2.15,<2.0.0",
"uipath-llm-client>=1.12.2,<2.0.0",
"uipath-llm-client>=1.13.0,<2.0.0",
]

[project.optional-dependencies]
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.12.2"
__version__ = "1.13.0"
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@ class UiPathBaseLLMClient(BaseModel, ABC):
description="Client-side request timeout in seconds",
)
max_retries: int = Field(
default=0,
description="Maximum number of retries for failed requests",
default=3,
description="Maximum number of retries for failed requests. Pass 0 to disable retries.",
)
retry_config: RetryConfig | None = Field(
default=None,
Expand Down
4 changes: 4 additions & 0 deletions src/uipath/llm_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@
from uipath.llm_client.utils.exceptions import (
UiPathAPIError,
UiPathAuthenticationError,
UiPathBadGatewayError,
UiPathBadRequestError,
UiPathConflictError,
UiPathGatewayTimeoutError,
UiPathInternalServerError,
UiPathNotFoundError,
UiPathPermissionDeniedError,
UiPathRateLimitError,
UiPathRequestTimeoutError,
UiPathRequestTooLargeError,
UiPathServiceUnavailableError,
UiPathTooManyRequestsError,
Expand All @@ -69,13 +71,15 @@
# Exceptions
"UiPathAPIError",
"UiPathAuthenticationError",
"UiPathBadGatewayError",
"UiPathBadRequestError",
"UiPathConflictError",
"UiPathGatewayTimeoutError",
"UiPathInternalServerError",
"UiPathNotFoundError",
"UiPathPermissionDeniedError",
"UiPathRateLimitError",
"UiPathRequestTimeoutError",
"UiPathRequestTooLargeError",
"UiPathServiceUnavailableError",
"UiPathTooManyRequestsError",
Expand Down
2 changes: 1 addition & 1 deletion src/uipath/llm_client/__version__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__title__ = "UiPath LLM Client"
__description__ = "A Python client for interacting with UiPath's LLM services."
__version__ = "1.12.2"
__version__ = "1.13.0"
12 changes: 8 additions & 4 deletions src/uipath/llm_client/httpx_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@
# Sentinel to distinguish "not provided" from an explicit ``None`` / ``False``.
_UNSET: Any = object()

# Default applied when ``max_retries`` is left as ``None``. Callers can still
# opt out by passing ``max_retries=0`` explicitly.
_DEFAULT_MAX_RETRIES: typing.Final[int] = 3


class UiPathHttpxClient(Client):
"""Synchronous HTTP client configured for UiPath LLM services.
Expand Down Expand Up @@ -135,8 +139,8 @@ def __init__(
captured_headers: Case-insensitive header name prefixes to capture from
responses. Captured headers are stored in a ContextVar and can be
retrieved with get_captured_response_headers(). Defaults to ("x-uipath-",).
max_retries: Maximum retry attempts for failed requests. Defaults to 0
(retries disabled). Set to a positive integer to enable retries.
max_retries: Maximum retry attempts for failed requests. Defaults to 3
when left as ``None``. Pass ``0`` to disable retries explicitly.
retry_config: Custom retry configuration (backoff, retryable status codes).
logger: Logger instance for request/response logging.
auth: HTTP authentication (same as httpx.Client). Derived from
Expand Down Expand Up @@ -193,7 +197,7 @@ def __init__(
# Setup retry transport if not provided
if transport is None:
transport = RetryableHTTPTransport(
max_retries=max_retries if max_retries is not None else 0,
max_retries=max_retries if max_retries is not None else _DEFAULT_MAX_RETRIES,
retry_config=retry_config,
logger=logger,
)
Expand Down Expand Up @@ -354,7 +358,7 @@ def __init__(
# Setup retry transport if not provided
if transport is None:
transport = RetryableAsyncHTTPTransport(
max_retries=max_retries if max_retries is not None else 0,
max_retries=max_retries if max_retries is not None else _DEFAULT_MAX_RETRIES,
retry_config=retry_config,
logger=logger,
)
Expand Down
16 changes: 16 additions & 0 deletions src/uipath/llm_client/utils/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ class UiPathNotFoundError(UiPathAPIError):
status_code: Literal[404] = 404 # pyright: ignore[reportIncompatibleVariableOverride]


class UiPathRequestTimeoutError(UiPathAPIError):
"""HTTP 408 Request Timeout error."""

status_code: Literal[408] = 408 # pyright: ignore[reportIncompatibleVariableOverride]


class UiPathConflictError(UiPathAPIError):
"""HTTP 409 Conflict error."""

Expand Down Expand Up @@ -211,6 +217,12 @@ class UiPathInternalServerError(UiPathAPIError):
status_code: Literal[500] = 500 # pyright: ignore[reportIncompatibleVariableOverride]


class UiPathBadGatewayError(UiPathAPIError):
"""HTTP 502 Bad Gateway error."""

status_code: Literal[502] = 502 # pyright: ignore[reportIncompatibleVariableOverride]


class UiPathServiceUnavailableError(UiPathAPIError):
"""HTTP 503 Service Unavailable error."""

Expand All @@ -234,11 +246,13 @@ class UiPathTooManyRequestsError(UiPathAPIError):
401: UiPathAuthenticationError,
403: UiPathPermissionDeniedError,
404: UiPathNotFoundError,
408: UiPathRequestTimeoutError,
409: UiPathConflictError,
413: UiPathRequestTooLargeError,
422: UiPathUnprocessableEntityError,
429: UiPathRateLimitError,
500: UiPathInternalServerError,
502: UiPathBadGatewayError,
503: UiPathServiceUnavailableError,
504: UiPathGatewayTimeoutError,
529: UiPathTooManyRequestsError,
Expand Down Expand Up @@ -266,11 +280,13 @@ def raise_for_status() -> Response:
"UiPathAuthenticationError",
"UiPathPermissionDeniedError",
"UiPathNotFoundError",
"UiPathRequestTimeoutError",
"UiPathConflictError",
"UiPathRequestTooLargeError",
"UiPathUnprocessableEntityError",
"UiPathRateLimitError",
"UiPathInternalServerError",
"UiPathBadGatewayError",
"UiPathServiceUnavailableError",
"UiPathGatewayTimeoutError",
"UiPathTooManyRequestsError",
Expand Down
11 changes: 10 additions & 1 deletion src/uipath/llm_client/utils/retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,22 @@

from uipath.llm_client.utils.exceptions import (
UiPathAPIError,
UiPathBadGatewayError,
UiPathGatewayTimeoutError,
UiPathRateLimitError,
UiPathRequestTimeoutError,
UiPathServiceUnavailableError,
UiPathTooManyRequestsError,
)

# Default retry configuration values
# Status codes retried by default: 408, 429, 502, 503, 504, 529.
_DEFAULT_RETRY_ON_EXCEPTIONS: tuple[type[Exception], ...] = (
UiPathRequestTimeoutError,
UiPathRateLimitError,
UiPathBadGatewayError,
UiPathServiceUnavailableError,
UiPathGatewayTimeoutError,
UiPathTooManyRequestsError,
)
_DEFAULT_INITIAL_DELAY: float = 2.0
Expand Down Expand Up @@ -127,7 +136,7 @@ class RetryConfig(TypedDict):

Attributes:
retry_on_exceptions: Tuple of exception types to retry on.
Defaults to (UiPathRateLimitError,).
Defaults to the typed exceptions for HTTP 408, 429, 502, 503, 504, 529.
initial_delay: Initial delay in seconds before first retry.
Defaults to 2.0.
max_delay: Maximum delay in seconds between retries.
Expand Down
4 changes: 4 additions & 0 deletions tests/core/features/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
from uipath.llm_client.utils.exceptions import (
UiPathAPIError,
UiPathAuthenticationError,
UiPathBadGatewayError,
UiPathBadRequestError,
UiPathGatewayTimeoutError,
UiPathInternalServerError,
UiPathNotFoundError,
UiPathPermissionDeniedError,
UiPathRateLimitError,
UiPathRequestTimeoutError,
UiPathServiceUnavailableError,
UiPathTooManyRequestsError,
patch_raise_for_status,
Expand Down Expand Up @@ -170,8 +172,10 @@ def test_all_status_code_mappings(self):
401: UiPathAuthenticationError,
403: UiPathPermissionDeniedError,
404: UiPathNotFoundError,
408: UiPathRequestTimeoutError,
429: UiPathRateLimitError,
500: UiPathInternalServerError,
502: UiPathBadGatewayError,
503: UiPathServiceUnavailableError,
504: UiPathGatewayTimeoutError,
529: UiPathTooManyRequestsError,
Expand Down
34 changes: 34 additions & 0 deletions tests/core/features/test_httpx_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,24 @@ def test_client_with_retry_config(self):
assert isinstance(client._transport, RetryableHTTPTransport)
client.close()

def test_client_default_max_retries_is_three(self):
"""Caller passing no ``max_retries`` should get the 3-retry default."""
from uipath.llm_client.httpx_client import UiPathHttpxClient

client = UiPathHttpxClient(base_url="https://example.com")
assert isinstance(client._transport, RetryableHTTPTransport)
assert client._transport.retryer is not None
client.close()

def test_client_explicit_zero_disables_retries(self):
"""Passing ``max_retries=0`` must still disable retries."""
from uipath.llm_client.httpx_client import UiPathHttpxClient

client = UiPathHttpxClient(base_url="https://example.com", max_retries=0)
assert isinstance(client._transport, RetryableHTTPTransport)
assert client._transport.retryer is None
client.close()

def test_client_with_byo_connection_id(self):
"""Test client adds BYO connection ID header."""
from uipath.llm_client.httpx_client import UiPathHttpxClient
Expand Down Expand Up @@ -123,6 +141,22 @@ def test_async_client_with_retry_config(self):
# Transport should be RetryableAsyncHTTPTransport
assert isinstance(client._transport, RetryableAsyncHTTPTransport)

def test_async_client_default_max_retries_is_three(self):
"""Async caller passing no ``max_retries`` should get the 3-retry default."""
from uipath.llm_client.httpx_client import UiPathHttpxAsyncClient

client = UiPathHttpxAsyncClient(base_url="https://example.com")
assert isinstance(client._transport, RetryableAsyncHTTPTransport)
assert client._transport.retryer is not None

def test_async_client_explicit_zero_disables_retries(self):
"""Async client: passing ``max_retries=0`` must still disable retries."""
from uipath.llm_client.httpx_client import UiPathHttpxAsyncClient

client = UiPathHttpxAsyncClient(base_url="https://example.com", max_retries=0)
assert isinstance(client._transport, RetryableAsyncHTTPTransport)
assert client._transport.retryer is None


class TestBuildRoutingHeaders:
"""Tests for build_routing_headers function."""
Expand Down
Loading
Loading