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

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

## [1.3.0] - 2026-03-10

### Version Bump
- Stable release
- Fixes

## [1.2.4] - 2026-02-26

### Refactor
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.3.0] - 2026-03-10

### Version Bump
- Stable release
- Fixes

## [1.2.7] - 2026-02-26

### Fix
Expand Down
18 changes: 9 additions & 9 deletions packages/uipath_langchain_client/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,29 @@ dynamic = ["version"]
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"langchain>=1.2.7",
"uipath-llm-client>=1.2.4",
"langchain>=1.2.10",
"uipath-llm-client>=1.3.0",
]

[project.optional-dependencies]
openai = [
"langchain-openai>=1.1.7",
"langchain-openai>=1.1.11",
]
aws = [
"langchain-aws>=1.2.1",
"langchain-aws>=1.4.0",
]
google = [
"langchain-google-genai>=4.2.0",
"langchain-google-genai>=4.2.1",
]
anthropic = [
"langchain-anthropic>=1.3.1",
"anthropic[bedrock,vertex]>=0.77.0",
"langchain-anthropic>=1.3.4",
"anthropic[bedrock,vertex]>=0.84.0",
]
azure = [
"langchain-azure-ai>=1.0.0",
"langchain-azure-ai>=1.1.0",
]
vertexai = [
"langchain-google-vertexai>=3.2.1",
"langchain-google-vertexai>=3.2.2",
]
fireworks = [
"langchain-fireworks>=1.1.0",
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.2.7"
__version__ = "1.3.0"
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ class UiPathBaseLLMClient(BaseModel, ABC):
)

default_headers: Mapping[str, str] | None = Field(
default={
default_factory=lambda: {
"X-UiPath-LLMGateway-TimeoutSeconds": "295", # server side timeout, default is 10, maximum is 300
"X-UiPath-LLMGateway-AllowFull4xxResponse": "true", # allow full 4xx responses (default is false)
},
Expand Down Expand Up @@ -210,7 +210,7 @@ def uipath_request(
async def uipath_arequest(
self,
method: Literal["POST", "GET"] = "POST",
url: str = "/",
url: URL | str = "/",
*,
request_body: dict[str, Any] | None = None,
**kwargs: Any,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,46 +1,70 @@
from typing import Self

from pydantic import model_validator
from httpx import URL, Request
from pydantic import Field, model_validator

from uipath_langchain_client.base_client import UiPathBaseChatModel
from uipath_langchain_client.settings import UiPathAPIConfig

try:
from azure.ai.inference import ChatCompletionsClient
from azure.ai.inference.aio import ChatCompletionsClient as ChatCompletionsClientAsync
from azure.core.credentials import AzureKeyCredential
from langchain_azure_ai.chat_models import AzureAIChatCompletionsModel
from azure.core.credentials import AzureKeyCredential, TokenCredential
from azure.core.credentials_async import AsyncTokenCredential
from langchain_azure_ai.chat_models import AzureAIOpenAIApiChatModel
from openai import AsyncOpenAI, OpenAI
except ImportError as e:
raise ImportError(
"The 'azure' extra is required to use UiPathAzureAIChatCompletionsModel. "
"Install it with: uv add uipath-langchain-client[azure]"
) from e


class UiPathAzureAIChatCompletionsModel(UiPathBaseChatModel, AzureAIChatCompletionsModel): # type: ignore[override]
class UiPathAzureAIChatCompletionsModel(UiPathBaseChatModel, AzureAIOpenAIApiChatModel): # type: ignore[override]
api_config: UiPathAPIConfig = UiPathAPIConfig(
api_type="completions",
client_type="passthrough",
vendor_type="azure",
freeze_base_url=True,
freeze_base_url=False,
)

# Override fields to avoid errors when instantiating the class
endpoint: str | None = "PLACEHOLDER"
# Override fields to avoid env var lookup / validation errors at instantiation
endpoint: str | None = Field(default="PLACEHOLDER")
credential: str | AzureKeyCredential | TokenCredential | AsyncTokenCredential | None = Field(
default="PLACEHOLDER"
)

@model_validator(mode="after")
def setup_uipath_client(self) -> Self:
# TODO: finish implementation once we have a proper model in UiPath API
self._client = ChatCompletionsClient(
endpoint="PLACEHOLDER",
credential=AzureKeyCredential("PLACEHOLDER"),
model=self.model_name,
**self.client_kwargs,
base_url = str(self.uipath_sync_client.base_url).rstrip("/")

def fix_url_and_api_flavor_header(request: Request):
url_suffix = str(request.url).split(base_url)[-1]
if "responses" in url_suffix:
request.headers["X-UiPath-LlmGateway-ApiFlavor"] = "responses"
else:
request.headers["X-UiPath-LlmGateway-ApiFlavor"] = "chat-completions"
request.url = URL(base_url)

async def fix_url_and_api_flavor_header_async(request: Request):
url_suffix = str(request.url).split(base_url)[-1]
if "responses" in url_suffix:
request.headers["X-UiPath-LlmGateway-ApiFlavor"] = "responses"
else:
request.headers["X-UiPath-LlmGateway-ApiFlavor"] = "chat-completions"
request.url = URL(base_url)

self.uipath_sync_client.event_hooks["request"].append(fix_url_and_api_flavor_header)
self.uipath_async_client.event_hooks["request"].append(fix_url_and_api_flavor_header_async)

self.root_client = OpenAI(
api_key="PLACEHOLDER",
max_retries=0, # handled by the UiPath client
http_client=self.uipath_sync_client,
)
self._async_client = ChatCompletionsClientAsync(
endpoint="PLACEHOLDER",
credential=AzureKeyCredential("PLACEHOLDER"),
model=self.model_name,
**self.client_kwargs,
self.root_async_client = AsyncOpenAI(
api_key="PLACEHOLDER",
max_retries=0, # handled by the UiPath client
http_client=self.uipath_async_client,
)
self.client = self.root_client.chat.completions
self.async_client = self.root_async_client.chat.completions
return self
Original file line number Diff line number Diff line change
@@ -1,46 +1,47 @@
from typing import Self

from pydantic import model_validator
from pydantic import Field, model_validator

from uipath_langchain_client.base_client import UiPathBaseEmbeddings
from uipath_langchain_client.settings import UiPathAPIConfig

try:
from azure.ai.inference import EmbeddingsClient
from azure.ai.inference.aio import EmbeddingsClient as EmbeddingsClientAsync
from langchain_azure_ai.embeddings import AzureAIEmbeddingsModel
from azure.core.credentials import AzureKeyCredential, TokenCredential
from azure.core.credentials_async import AsyncTokenCredential
from langchain_azure_ai.embeddings import AzureAIOpenAIApiEmbeddingsModel
from openai import AsyncOpenAI, OpenAI
except ImportError as e:
raise ImportError(
"The 'azure' extra is required to use UiPathAzureAIEmbeddingsModel. "
"Install it with: uv add uipath-langchain-client[azure]"
) from e


class UiPathAzureAIEmbeddingsModel(UiPathBaseEmbeddings, AzureAIEmbeddingsModel): # type: ignore[override]
class UiPathAzureAIEmbeddingsModel(UiPathBaseEmbeddings, AzureAIOpenAIApiEmbeddingsModel): # type: ignore[override]
api_config: UiPathAPIConfig = UiPathAPIConfig(
api_type="embeddings",
client_type="passthrough",
vendor_type="azure",
freeze_base_url=True,
)

# Override fields to avoid errors when instantiating the class
endpoint: str | None = "PLACEHOLDER"
credentials: str | None = "PLACEHOLDER"
# Override fields to avoid env var lookup / validation errors at instantiation
model: str = Field(default="", alias="model_name")
endpoint: str | None = Field(default="PLACEHOLDER")
credential: str | AzureKeyCredential | TokenCredential | AsyncTokenCredential | None = Field(
default="PLACEHOLDER"
)

@model_validator(mode="after")
def setup_uipath_client(self) -> Self:
# TODO: finish implementation once we have a proper model in UiPath API
self._client = EmbeddingsClient(
endpoint="PLACEHOLDER",
credentials="PLACEHOLDER",
model=self.model_name,
**self.client_kwargs,
)
self._async_client = EmbeddingsClientAsync(
endpoint="PLACEHOLDER",
credentials="PLACEHOLDER",
model=self.model_name,
**self.client_kwargs,
)
self.client = OpenAI(
api_key="PLACEHOLDER",
max_retries=0, # handled by the UiPath client
http_client=self.uipath_sync_client,
).embeddings
self.async_client = AsyncOpenAI(
api_key="PLACEHOLDER",
max_retries=0, # handled by the UiPath client
http_client=self.uipath_async_client,
).embeddings
return self
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def _llm_type(self) -> str:

@property
def _default_params(self) -> dict[str, Any]:
"""Get the default parameters for calling OpenAI API."""
"""Get the default parameters for the normalized API request."""
exclude_if_none = {
"frequency_penalty": self.frequency_penalty,
"presence_penalty": self.presence_penalty,
Expand Down Expand Up @@ -175,7 +175,7 @@ def _get_usage_metadata(self, json_data: dict[str, Any]) -> UsageMetadata:
cache_creation=json_data.get("cache_creation_input_tokens", 0),
),
output_token_details=OutputTokenDetails(
audio=json_data.get("audio_tokens", 0),
audio=json_data.get("output_audio_tokens", 0),
reasoning=json_data.get("thoughts_tokens", 0),
),
)
Expand Down Expand Up @@ -241,7 +241,15 @@ def _preprocess_request(
{
"id": tool_call["id"],
"name": tool_call["function"]["name"],
"arguments": json.loads(tool_call["function"]["arguments"]),
"arguments": (
tool_call["function"]["arguments"]
if isinstance(tool_call["function"]["arguments"], dict)
else (
json.loads(tool_call["function"]["arguments"])
if tool_call["function"]["arguments"]
else {}
)
),
}
for tool_call in converted_message["tool_calls"]
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from uipath.llm_client.utils.exceptions import (
UiPathAPIError,
UiPathAuthenticationError,
UiPathBadRequestError,
UiPathConflictError,
UiPathGatewayTimeoutError,
UiPathInternalServerError,
UiPathNotFoundError,
UiPathPermissionDeniedError,
UiPathRateLimitError,
UiPathRequestTooLargeError,
UiPathServiceUnavailableError,
UiPathTooManyRequestsError,
UiPathUnprocessableEntityError,
)
from uipath.llm_client.utils.retry import RetryConfig

__all__ = [
"RetryConfig",
"UiPathAPIError",
"UiPathBadRequestError",
"UiPathAuthenticationError",
"UiPathPermissionDeniedError",
"UiPathNotFoundError",
"UiPathConflictError",
"UiPathRequestTooLargeError",
"UiPathUnprocessableEntityError",
"UiPathRateLimitError",
"UiPathInternalServerError",
"UiPathServiceUnavailableError",
"UiPathGatewayTimeoutError",
"UiPathTooManyRequestsError",
]
20 changes: 10 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"httpx>=0.28.1",
"tenacity>=9.1.2",
"tenacity>=9.1.4",
"pydantic>=2.12.5",
"pydantic-settings>=2.12.0",
"uipath>=2.5.17",
"pydantic-settings>=2.13.1",
"uipath>=2.10.11",
]

authors = [
Expand All @@ -19,26 +19,26 @@ authors = [

[project.optional-dependencies]
openai = [
"openai>=2.15.0",
"openai>=2.24.0",
]
google = [
"google-genai>=1.59.0",
"google-genai>=1.65.0",
]
anthropic = [
"anthropic>=0.76.0",
"anthropic>=0.84.0",
]
all = [
"uipath-llm-client[openai,google,anthropic]",
]

[dependency-groups]
dev = [
"langchain-tests>=1.1.2",
"pytest>=8.4.2",
"langchain-tests>=1.1.5",
"pytest>=9.0.2",
"pytest-asyncio>=1.3.0",
"pytest-recording>=0.13.4",
"pyright>=1.1.407",
"ruff>=0.14.9",
"pyright>=1.1.408",
"ruff>=0.15.3",
"uipath-llm-client[all]",
"uipath_langchain_client[all]",
]
Expand Down
4 changes: 2 additions & 2 deletions src/uipath/llm_client/__version__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__titile__ = "UiPath LLM Client"
__title__ = "UiPath LLM Client"
__description__ = "A Python client for interacting with UiPath's LLM services."
__version__ = "1.2.4"
__version__ = "1.3.0"
10 changes: 7 additions & 3 deletions src/uipath/llm_client/settings/llmgateway/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ def get_llmgw_token_header(
) -> str:
"""Retrieve a new access token from the LLM Gateway identity endpoint."""
url_get_token = f"{self.settings.base_url}/{LLMGatewayEndpoints.IDENTITY_ENDPOINT.value}"
assert self.settings.client_id is not None
assert self.settings.client_secret is not None
if self.settings.client_id is None or self.settings.client_secret is None:
raise ValueError("client_id and client_secret are required for S2S authentication")
token_credentials = dict(
client_id=self.settings.client_id.get_secret_value(),
client_secret=self.settings.client_secret.get_secret_value(),
Expand All @@ -40,11 +40,15 @@ def get_llmgw_token_header(
with Client() as http_client:
response = http_client.post(url_get_token, data=token_credentials)
if response.is_client_error:
try:
body = response.json()
except Exception:
body = response.text
raise UiPathAuthenticationError(
message="Failed to authenticate with LLM Gateway, invalid credentials",
request=response.request,
response=response,
body=response.json(),
body=body,
)
elif response.is_error:
raise UiPathAPIError.from_response(response)
Expand Down
Loading