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.1.0] - 2026-02-11

### Stable release
- Adeed BYOM validation for settings
- Stable release

## [1.0.13] - 2026-02-05

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

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

## [1.1.0] - 2026-02-11

### Features
- Added langchain fireworks client and tested with GLM

### Stable release
- Fixed BYO on passthrough
- Stable release

## [1.0.13] - 2026-02-05

### Fix
Expand Down
7 changes: 5 additions & 2 deletions 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.7",
"uipath-llm-client>=1.0.13",
"uipath-llm-client>=1.1.0",
]

[project.optional-dependencies]
Expand All @@ -29,8 +29,11 @@ azure = [
vertexai = [
"langchain-google-vertexai>=3.2.1",
]
fireworks = [
"langchain-fireworks>=1.1.0",
]
all = [
"uipath-langchain-client[openai,aws,google,anthropic,azure,vertexai]"
"uipath-langchain-client[openai,aws,google,anthropic,azure,vertexai,fireworks]"
]

[build-system]
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.0.13"
__version__ = "1.1.0"
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from uipath_langchain_client.clients.fireworks.chat_models import UiPathChatFireworks
from uipath_langchain_client.clients.fireworks.embeddings import UiPathFireworksEmbeddings

__all__ = ["UiPathChatFireworks", "UiPathFireworksEmbeddings"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from typing import Self

from pydantic import Field, SecretStr, model_validator
from uipath_langchain_client.base_client import UiPathBaseLLMClient
from uipath_langchain_client.settings import UiPathAPIConfig

try:
from langchain_fireworks.chat_models import ChatFireworks

from fireworks.client.api_client import FireworksClient as FireworksClientV1
except ImportError as e:
raise ImportError(
"The 'fireworks' extra is required to use UiPathChatFireworks. "
'Install it with: uv add "uipath-langchain-client[fireworks]"'
) from e


class UiPathChatFireworks(UiPathBaseLLMClient, ChatFireworks): # type: ignore[override]
api_config: UiPathAPIConfig = UiPathAPIConfig(
api_type="completions",
client_type="passthrough",
vendor_type="openai",
api_flavor="chat-completions",
api_version="2025-03-01-preview",
freeze_base_url=True,
)

# Override fields to avoid errors when instantiating the class
fireworks_api_base: str | None = Field(alias="base_url", default="PLACEHOLDER")
fireworks_api_key: SecretStr = Field(default=SecretStr("PLACEHOLDER"), alias="api_key")

@model_validator(mode="after")
def setup_uipath_client(self) -> Self:
fireworks_client_v1 = FireworksClientV1(
api_key=self.fireworks_api_key.get_secret_value(),
base_url=self.fireworks_api_base,
)
fireworks_client_v1._client = self.uipath_sync_client
fireworks_client_v1._async_client = self.uipath_async_client
self.client._client = fireworks_client_v1
return self
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from typing import Self

from pydantic import model_validator
from uipath_langchain_client.base_client import UiPathBaseLLMClient
from uipath_langchain_client.settings import UiPathAPIConfig

try:
from langchain_fireworks.embeddings import FireworksEmbeddings
from openai import AsyncOpenAI, OpenAI
except ImportError as e:
raise ImportError(
"The 'fireworks' extra is required to use UiPathFireworksEmbeddings. "
'Install it with: uv add "uipath-langchain-client[fireworks]"'
) from e


class UiPathFireworksEmbeddings(UiPathBaseLLMClient, FireworksEmbeddings):
api_config: UiPathAPIConfig = UiPathAPIConfig(
api_type="embeddings",
client_type="passthrough",
vendor_type="openai",
api_flavor="chat-completions",
api_version="2025-03-01-preview",
freeze_base_url=True,
)

@model_validator(mode="after")
def setup_uipath_client(self) -> Self:
self.client = OpenAI(
api_key="PLACEHOLDER",
timeout=None, # handled by the UiPath client
max_retries=0, # handled by the UiPath client
http_client=self.uipath_sync_client,
)
self.async_client = AsyncOpenAI(
api_key="PLACEHOLDER",
timeout=None, # handled by the UiPath client
max_retries=0, # handled by the UiPath client
http_client=self.uipath_async_client,
)
return self

def embed_documents(self, texts: list[str]) -> list[list[float]]:
"""Embed search docs."""
return [
i.embedding for i in self.client.embeddings.create(input=texts, model=self.model).data
]

def embed_query(self, text: str) -> list[float]:
"""Embed query text."""
return self.embed_documents([text])[0]

async def aembed_documents(self, texts: list[str]) -> list[list[float]]:
"""Embed search docs asynchronously."""
return [
i.embedding
for i in (await self.async_client.embeddings.create(input=texts, model=self.model)).data
]

async def aembed_query(self, text: str) -> list[float]:
"""Embed query text asynchronously."""
return (await self.aembed_documents([text]))[0]
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,13 @@ def get_chat_model(
ValueError: If the model is not found in available models or vendor is not supported
"""
client_settings = client_settings or get_default_client_settings()

model_info = _get_model_info(model_name, client_settings, byo_connection_id)

is_uipath_owned = model_info.get("modelSubscriptionType") == "UiPathOwned"
if not is_uipath_owned:
client_settings.validate_byo_model(model_info)

if client_type == "normalized":
from uipath_langchain_client.clients.normalized.chat_models import (
UiPathNormalizedChatModel,
Expand All @@ -102,7 +107,6 @@ def get_chat_model(
)

vendor_type = model_info["vendor"].lower()
is_uipath_owned = model_info.get("modelSubscriptionType") == "UiPathOwned"
match vendor_type:
case "openai":
if is_uipath_owned:
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 @@
__titile__ = "UiPath LLM Client"
__description__ = "A Python client for interacting with UiPath's LLM services."
__version__ = "1.0.13"
__version__ = "1.1.0"
6 changes: 3 additions & 3 deletions src/uipath_llm_client/httpx_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def build_routing_headers(
if api_config.api_version is not None:
headers["X-UiPath-LlmGateway-ApiVersion"] = api_config.api_version
if byo_connection_id is not None:
headers["X-UiPath-LlmGateway-ByoConnectionId"] = byo_connection_id
headers["X-UiPath-LlmGateway-ByoIsConnectionId"] = byo_connection_id
return headers


Expand Down Expand Up @@ -141,7 +141,7 @@ def __init__(
merged_headers = Headers(self._default_headers)
merged_headers.update(
build_routing_headers(
api_config=api_config, model_name=model_name, byo_connection_id=byo_connection_id
model_name=model_name, byo_connection_id=byo_connection_id, api_config=api_config
)
)
if headers is not None:
Expand Down Expand Up @@ -261,7 +261,7 @@ def __init__(
merged_headers = Headers(self._default_headers)
merged_headers.update(
build_routing_headers(
api_config=api_config, model_name=model_name, byo_connection_id=byo_connection_id
model_name=model_name, byo_connection_id=byo_connection_id, api_config=api_config
)
)
if headers is not None:
Expand Down
7 changes: 4 additions & 3 deletions src/uipath_llm_client/settings/agenthub/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

from dotenv import load_dotenv
from pydantic import Field, SecretStr, model_validator
from pydantic_settings import SettingsConfigDict
from typing_extensions import override
from uipath._cli._auth._auth_service import AuthService
from uipath.utils import EndpointManager
Expand All @@ -33,8 +32,6 @@ class AgentHubBaseSettings(UiPathBaseSettings):
job_key: Job key for tracing.
"""

model_config = SettingsConfigDict(validate_by_alias=True)

# Environment configuration: alpha, staging, cloud
environment: str | None = Field(default=None, validation_alias="UIPATH_ENVIRONMENT")

Expand Down Expand Up @@ -139,3 +136,7 @@ def get_available_models(self) -> list[dict[str, Any]]:
headers=dict(self.build_auth_headers()),
)
return [model.model_dump(by_alias=True) for model in models]

@override
def validate_byo_model(self, model_info: dict[str, Any]) -> None:
return
11 changes: 10 additions & 1 deletion src/uipath_llm_client/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ class UiPathBaseSettings(BaseSettings, ABC):
with validation aliases allowing flexible naming conventions.
"""

model_config = SettingsConfigDict(validate_by_alias=True)
model_config = SettingsConfigDict(
validate_by_alias=True,
populate_by_name=True,
extra="allow",
)

@abstractmethod
def build_base_url(
Expand Down Expand Up @@ -138,3 +142,8 @@ def get_available_models(
A list of dictionaries containing model information.
"""
...

@abstractmethod
def validate_byo_model(self, model_info: dict[str, Any]) -> None:
"""Validate that the model is a BYOM model."""
...
21 changes: 18 additions & 3 deletions src/uipath_llm_client/settings/llmgateway/settings.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import logging
from collections.abc import Mapping
from typing import Any, Self

from httpx import Client
from pydantic import Field, SecretStr, model_validator
from pydantic_settings import SettingsConfigDict
from typing_extensions import override

from uipath_llm_client.settings.base import UiPathAPIConfig, UiPathBaseSettings
Expand All @@ -26,8 +26,6 @@ class LLMGatewayBaseSettings(UiPathBaseSettings):
additional_headers: Additional custom headers to include in requests (optional)
"""

model_config = SettingsConfigDict(validate_by_alias=True)

# Required to work, but if it's not set will be retrieved using S2S authentication
access_token: SecretStr | None = Field(default=None, validation_alias="LLMGW_ACCESS_TOKEN")

Expand Down Expand Up @@ -110,3 +108,20 @@ def get_available_models(self) -> list[dict[str, Any]]:
with Client(auth=self.build_auth_pipeline(), headers=self.build_auth_headers()) as client:
response = client.get(discovery_url)
return response.json()

@override
def validate_byo_model(self, model_info: dict[str, Any]) -> None:
byom_details = model_info.get("byomDetails", {})
operaion_codes = byom_details.get("operationCodes", [])
if self.operation_code and self.operation_code not in operaion_codes:
raise ValueError(
f"The operation code {self.operation_code} is not allowed for the model {model_info['modelName']}"
)
if not self.operation_code and len(operaion_codes) > 0:
if len(operaion_codes) > 1:
logging.warning(
"Multiple operation codes are allowed for the model %s, but no operation code was provided, picking the first one available: %s",
model_info["modelName"],
operaion_codes[0],
)
self.operation_code = operaion_codes[0]
6 changes: 3 additions & 3 deletions tests/core/test_base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -714,8 +714,8 @@ def test_client_with_byo_connection_id(self):
base_url="https://example.com",
byo_connection_id="test-connection-id",
)
assert "X-UiPath-LlmGateway-ByoConnectionId" in client.headers
assert client.headers["X-UiPath-LlmGateway-ByoConnectionId"] == "test-connection-id"
assert "X-UiPath-LlmGateway-ByoIsConnectionId" in client.headers
assert client.headers["X-UiPath-LlmGateway-ByoIsConnectionId"] == "test-connection-id"
client.close()


Expand Down Expand Up @@ -800,7 +800,7 @@ def test_byo_connection_id_header(self):
headers = build_routing_headers(
byo_connection_id="test-connection-id",
)
assert headers["X-UiPath-LlmGateway-ByoConnectionId"] == "test-connection-id"
assert headers["X-UiPath-LlmGateway-ByoIsConnectionId"] == "test-connection-id"


# ============================================================================
Expand Down
Loading