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

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

## [1.17.2] - 2026-08-13

### Fixed
- Resource binding overwrites are now applied to `byo_connection_id`, in both the `get_model_info` discovery lookup and the `X-UiPath-LlmGateway-ByoIsConnectionId` routing header.

## [1.17.1] - 2026-07-17

### Added
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.17.1"
__version__ = "1.17.2"
2 changes: 2 additions & 0 deletions src/uipath/llm_client/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from httpx import Auth
from pydantic import BaseModel, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from uipath.platform.common import resource_override

from uipath.llm_client.settings.constants import ApiFlavor, ApiType, RoutingMode, VendorType
from uipath.llm_client.utils.exceptions import ModelNotFoundError
Expand Down Expand Up @@ -183,6 +184,7 @@ def validate_byo_model(self, model_info: dict[str, Any]) -> None:
"""Validate that the model is a BYOM model."""
return

@resource_override(resource_type="connection", resource_identifier="byo_connection_id")
def get_model_info(
self,
model_name: str,
Expand Down
2 changes: 2 additions & 0 deletions src/uipath/llm_client/utils/headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from collections.abc import Mapping, Sequence

from httpx import Headers, Request
from uipath.platform.common import resource_override

from uipath.llm_client.settings.base import UiPathAPIConfig
from uipath.llm_client.settings.constants import ApiType, RoutingMode
Expand Down Expand Up @@ -101,6 +102,7 @@ def extract_matching_headers(
return result


@resource_override(resource_type="connection", resource_identifier="byo_connection_id")
def build_routing_headers(
*,
model_name: str | None = None,
Expand Down
23 changes: 23 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,32 @@
import pytest
from uipath.platform.common import ResourceOverwriteParser
from uipath.platform.common._bindings import _resource_overwrites

from uipath.llm_client.settings import UiPathBaseSettings
from uipath.llm_client.settings.llmgateway import LLMGatewaySettings


@pytest.fixture
def activate_connection_overwrite():
"""Activate a ``connection.<design_time_id>`` resource overwrite for one test."""
tokens = []

def _activate(design_time_id: str, connection_id: str, folder_key: str = "test-folder-key"):
key = f"connection.{design_time_id}"
overwrites = {
key: ResourceOverwriteParser.parse(
key=key,
value={"connectionId": connection_id, "folderKey": folder_key},
)
}
tokens.append(_resource_overwrites.set(overwrites))

yield _activate

for token in reversed(tokens):
_resource_overwrites.reset(token)


@pytest.fixture(autouse=True, scope="session")
def setup_env():
from dotenv import find_dotenv, load_dotenv
Expand Down
30 changes: 30 additions & 0 deletions tests/core/features/settings/test_llmgateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -619,3 +619,33 @@ def test_skips_validate_byo_model_for_uipath_owned(self, llmgw_env_vars):
with patch.object(settings, "validate_byo_model") as mock_validate:
settings.get_model_info("claude-3-opus")
mock_validate.assert_not_called()

def test_remaps_byo_connection_id_from_resource_overwrite(
self, llmgw_env_vars, activate_connection_overwrite
):
"""A design-time connection id resolves to the one bound in the target folder."""
activate_connection_overwrite("design-time-conn", "conn-123")
settings = self._make_settings(llmgw_env_vars)

info = settings.get_model_info("gpt-4o", byo_connection_id="design-time-conn")

assert info["byomDetails"]["integrationServiceConnectionId"] == "conn-123"

def test_keeps_byo_connection_id_when_no_overwrite_matches(
self, llmgw_env_vars, activate_connection_overwrite
):
activate_connection_overwrite("other-conn", "conn-123")
settings = self._make_settings(llmgw_env_vars)

with pytest.raises(ValueError, match="not found"):
settings.get_model_info("gpt-4o", byo_connection_id="design-time-conn")

def test_ignores_overwrites_for_uipath_owned_models(
self, llmgw_env_vars, activate_connection_overwrite
):
activate_connection_overwrite("design-time-conn", "conn-123")
settings = self._make_settings(llmgw_env_vars)

info = settings.get_model_info("gpt-4o")

assert info["modelSubscriptionType"] == "UiPathOwned"
27 changes: 27 additions & 0 deletions tests/core/features/test_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,30 @@ def test_normalized_without_model_name(self):
)
headers = build_routing_headers(model_name=None, api_config=api_config)
assert "X-UiPath-LlmGateway-NormalizedApi-ModelName" not in headers

def test_byo_connection_id_header(self):
from uipath.llm_client.utils.headers import build_routing_headers

headers = build_routing_headers(byo_connection_id="conn-123")
assert headers["X-UiPath-LlmGateway-ByoIsConnectionId"] == "conn-123"

def test_byo_connection_id_header_uses_resource_overwrite(self, activate_connection_overwrite):
"""The bound connection id must reach the gateway, not the design-time one."""
from uipath.llm_client.utils.headers import build_routing_headers

activate_connection_overwrite("design-time-conn", "conn-123")

headers = build_routing_headers(byo_connection_id="design-time-conn")

assert headers["X-UiPath-LlmGateway-ByoIsConnectionId"] == "conn-123"

def test_byo_connection_id_header_kept_when_no_overwrite_matches(
self, activate_connection_overwrite
):
from uipath.llm_client.utils.headers import build_routing_headers

activate_connection_overwrite("other-conn", "conn-123")

headers = build_routing_headers(byo_connection_id="design-time-conn")

assert headers["X-UiPath-LlmGateway-ByoIsConnectionId"] == "design-time-conn"
106 changes: 106 additions & 0 deletions tests/langchain/features/test_connection_overwrites.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Tests that a job's connection binding reaches BYOM models.

A solution deployment binds the agent to a connection in the target folder, while the
design-time ``byo_connection_id`` travels with the package. Callers that build their own
chat models never see the binding, so the ``connection.<id>`` overwrite has to be applied
where the id is consumed: the discovery lookup and the routing header.
"""

import os
from unittest.mock import MagicMock, patch

from httpx import Client
from uipath_langchain_client.clients.normalized.chat_models import UiPathChat
from uipath_langchain_client.factory import get_chat_model

from uipath.llm_client.settings import LLMGatewaySettings, RoutingMode
from uipath.llm_client.settings.base import UiPathBaseSettings
from uipath.llm_client.settings.utils import SingletonMeta

LLMGW_ENV = {
"LLMGW_URL": "https://cloud.uipath.com",
"LLMGW_SEMANTIC_ORG_ID": "test-org-id",
"LLMGW_SEMANTIC_TENANT_ID": "test-tenant-id",
"LLMGW_REQUESTING_PRODUCT": "test-product",
"LLMGW_REQUESTING_FEATURE": "test-feature",
"LLMGW_ACCESS_TOKEN": "test-access-token",
}

DESIGN_TIME_CONNECTION_ID = "design-time-conn"
BOUND_CONNECTION_ID = "bound-conn"
BYO_MODEL_DETAILS = {"contextWindowSize": 128000}
BYO_CONNECTION_HEADER = "x-uipath-llmgateway-byoisconnectionid"

MODELS = [
{"modelName": "gpt-4o", "vendor": "OpenAi", "modelSubscriptionType": "UiPathOwned"},
{
"modelName": "gpt-4o",
"vendor": "OpenAi",
"modelSubscriptionType": "BYO",
"byomDetails": {"integrationServiceConnectionId": BOUND_CONNECTION_ID},
"modelDetails": BYO_MODEL_DETAILS,
},
]


class TestConnectionOverwriteReachesByoModels:
def setup_method(self):
SingletonMeta._instances.clear()
UiPathBaseSettings._discovery_cache.clear()

def teardown_method(self):
SingletonMeta._instances.clear()
UiPathBaseSettings._discovery_cache.clear()

def _settings(self):
"""Build settings with the discovery cache pre-populated from ``MODELS``."""
settings = LLMGatewaySettings()
response = MagicMock()
response.is_error = False
response.json.return_value = MODELS
with patch.object(Client, "get", return_value=response):
settings.get_available_models()
return settings

def test_direct_instantiation_routes_to_bound_connection(self, activate_connection_overwrite):
activate_connection_overwrite(DESIGN_TIME_CONNECTION_ID, BOUND_CONNECTION_ID)

with patch.dict(os.environ, LLMGW_ENV, clear=True):
chat = UiPathChat(
model="gpt-4o",
settings=self._settings(),
byo_connection_id=DESIGN_TIME_CONNECTION_ID,
)
headers = chat.uipath_sync_client.headers

assert chat.model_details == BYO_MODEL_DETAILS
assert headers[BYO_CONNECTION_HEADER] == BOUND_CONNECTION_ID

def test_factory_routes_to_bound_connection(self, activate_connection_overwrite):
activate_connection_overwrite(DESIGN_TIME_CONNECTION_ID, BOUND_CONNECTION_ID)

with patch.dict(os.environ, LLMGW_ENV, clear=True):
chat = get_chat_model(
model_name="gpt-4o",
client_settings=self._settings(),
byo_connection_id=DESIGN_TIME_CONNECTION_ID,
routing_mode=RoutingMode.NORMALIZED,
)
headers = chat.uipath_sync_client.headers

assert chat.model_details == BYO_MODEL_DETAILS
assert headers[BYO_CONNECTION_HEADER] == BOUND_CONNECTION_ID

def test_unbound_connection_id_is_left_alone(self, activate_connection_overwrite):
activate_connection_overwrite("some-other-conn", BOUND_CONNECTION_ID)

with patch.dict(os.environ, LLMGW_ENV, clear=True):
chat = UiPathChat(
model="gpt-4o",
settings=self._settings(),
byo_connection_id=BOUND_CONNECTION_ID,
)
headers = chat.uipath_sync_client.headers

assert chat.model_details == BYO_MODEL_DETAILS
assert headers[BYO_CONNECTION_HEADER] == BOUND_CONNECTION_ID
Loading