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

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

## [1.9.0] - 2026-04-17

### Added
- `ModelFamily` StrEnum constants (`OPENAI`, `GOOGLE_GEMINI`, `ANTHROPIC_CLAUDE`) for model family matching
- `get_model_info()` on `UiPathBaseSettings` — centralized model lookup with filtering by name, vendor, and BYO connection ID
- Discovery cache on `get_available_models()` keyed by settings properties, with `refresh` parameter to bypass

### Changed
- `get_available_models()` is now a concrete cached method on the base class; subclasses implement `_fetch_available_models()` instead
- `validate_byo_model()` is now a default no-op on the base class (only LLMGateway overrides it) and is called automatically inside `get_model_info()`
- LiteLLM client uses `get_model_info()` instead of duplicating model discovery logic

## [1.8.3] - 2026-04-16

### Added
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.9.0] - 2026-04-17

### Changed
- Factory functions use `ModelFamily` constants and `get_model_info()` from core instead of inline discovery logic
- Azure vs non-Azure OpenAI routing now uses `modelFamily` instead of `modelSubscriptionType`

## [1.8.3] - 2026-04-16

### Added
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",
"uipath-llm-client>=1.8.3",
"uipath-llm-client>=1.9.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.8.3"
__version__ = "1.9.0"
Original file line number Diff line number Diff line change
Expand Up @@ -30,56 +30,14 @@
API_FLAVOR_TO_VENDOR_TYPE,
BYOM_TO_ROUTING_FLAVOR,
ApiFlavor,
ModelFamily,
RoutingMode,
UiPathBaseSettings,
VendorType,
get_default_client_settings,
)


def _get_model_info(
model_name: str,
*,
client_settings: UiPathBaseSettings,
byo_connection_id: str | None = None,
vendor_type: VendorType | str | None = None,
) -> dict[str, Any]:
available_models = client_settings.get_available_models()

matching_models = [m for m in available_models if m["modelName"].lower() == model_name.lower()]

if vendor_type is not None:
matching_models = [
m for m in matching_models if m.get("vendor", "").lower() == str(vendor_type).lower()
]

if byo_connection_id:
matching_models = [
m
for m in matching_models
if (byom_details := m.get("byomDetails"))
and byom_details.get("integrationServiceConnectionId", "").lower()
== byo_connection_id.lower()
]

if not byo_connection_id and len(matching_models) > 1:
matching_models = [
m
for m in matching_models
if (
(m.get("modelSubscriptionType", "") == "UiPathOwned")
or (m.get("byomDetails") is None)
)
]

if not matching_models:
raise ValueError(
f"Model {model_name} not found. Available models are: {[m['modelName'] for m in available_models]}"
)

return matching_models[0]


def get_chat_model(
model_name: str,
*,
Expand Down Expand Up @@ -120,18 +78,12 @@ 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_info = client_settings.get_model_info(
model_name,
client_settings=client_settings,
byo_connection_id=byo_connection_id,
vendor_type=vendor_type,
)
model_family = model_info.get("modelFamily", None)
if model_family is not None:
model_family = model_family.lower()
is_uipath_owned = model_info.get("modelSubscriptionType") == "UiPathOwned"
if not is_uipath_owned:
client_settings.validate_byo_model(model_info)

if custom_class is not None:
return custom_class(
Expand Down Expand Up @@ -171,7 +123,7 @@ def get_chat_model(

match discovered_vendor_type:
case VendorType.OPENAI:
if is_uipath_owned:
if model_family == ModelFamily.OPENAI:
from uipath_langchain_client.clients.openai.chat_models import (
UiPathAzureChatOpenAI,
)
Expand All @@ -196,7 +148,7 @@ def get_chat_model(
**model_kwargs,
)
case VendorType.VERTEXAI:
if model_family == "anthropicclaude":
if model_family == ModelFamily.ANTHROPIC_CLAUDE:
from uipath_langchain_client.clients.anthropic.chat_models import (
UiPathChatAnthropic,
)
Expand All @@ -220,7 +172,7 @@ def get_chat_model(
**model_kwargs,
)
case VendorType.AWSBEDROCK:
if model_family == "anthropicclaude" and api_flavor is None:
if model_family == ModelFamily.ANTHROPIC_CLAUDE and api_flavor is None:
from uipath_langchain_client.clients.bedrock.chat_models import (
UiPathChatAnthropicBedrock,
)
Expand Down Expand Up @@ -300,15 +252,12 @@ def get_embedding_model(
>>> vectors = embeddings.embed_documents(["Hello world"])
"""
client_settings = client_settings or get_default_client_settings()
model_info = _get_model_info(
model_info = client_settings.get_model_info(
model_name,
client_settings=client_settings,
byo_connection_id=byo_connection_id,
vendor_type=vendor_type,
)
is_uipath_owned = model_info.get("modelSubscriptionType") == "UiPathOwned"
if not is_uipath_owned:
client_settings.validate_byo_model(model_info)
model_family = model_info.get("modelFamily", None)

if custom_class is not None:
return custom_class(
Expand Down Expand Up @@ -342,7 +291,7 @@ def get_embedding_model(
discovered_vendor_type = discovered_vendor_type.lower()
match discovered_vendor_type:
case VendorType.OPENAI:
if is_uipath_owned:
if model_family == ModelFamily.OPENAI:
from uipath_langchain_client.clients.openai.embeddings import (
UiPathAzureOpenAIEmbeddings,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
ApiFlavor,
ApiType,
ByomApiFlavor,
ModelFamily,
RoutingMode,
VendorType,
)
Expand All @@ -42,6 +43,7 @@
"RoutingMode",
"ApiFlavor",
"ByomApiFlavor",
"ModelFamily",
"VendorType",
"API_FLAVOR_TO_VENDOR_TYPE",
"BYOM_TO_ROUTING_FLAVOR",
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.8.3"
__version__ = "1.9.0"
33 changes: 7 additions & 26 deletions src/uipath/llm_client/clients/litellm/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
ApiFlavor,
ApiType,
ByomApiFlavor,
ModelFamily,
RoutingMode,
VendorType,
)
Expand Down Expand Up @@ -99,8 +100,6 @@
ByomApiFlavor.AWS_BEDROCK_CONVERSE: "bedrock",
}

_ANTHROPIC_FAMILY = "anthropicclaude"


def _drop_nones(**kwargs: Any) -> dict[str, Any]:
return {k: v for k, v in kwargs.items() if v is not None}
Expand Down Expand Up @@ -189,27 +188,9 @@ def _discover_and_build_api_config(
User-supplied ``vendor_type`` filters models during discovery.
User-supplied ``api_flavor`` overrides the discovered value.
"""
available_models = self._client_settings.get_available_models()
matching = [
m for m in available_models if m["modelName"].lower() == self._model_name.lower()
]

if vendor_type is not None:
matching = [
m for m in matching if m.get("vendor", "").lower() == str(vendor_type).lower()
]

if not matching:
raise ValueError(
f"Model '{self._model_name}' not found. "
f"Available: {[m['modelName'] for m in available_models]}"
)
model_info = matching[0]

model_family: str | None = None
raw_family = model_info.get("modelFamily", None)
if raw_family is not None:
model_family = raw_family.lower()
model_info = self._client_settings.get_model_info(self._model_name, vendor_type=vendor_type)

model_family = model_info.get("modelFamily", None)

discovered_vendor = model_info.get("vendor", None)
discovered_flavor = model_info.get("apiFlavor", None)
Expand Down Expand Up @@ -240,15 +221,15 @@ def _discover_and_build_api_config(
if (
resolved_flavor is None
and resolved_vendor == "awsbedrock"
and model_family == _ANTHROPIC_FAMILY
and model_family == ModelFamily.ANTHROPIC_CLAUDE
):
resolved_flavor = ApiFlavor.INVOKE

# Claude on Vertex defaults to anthropic-claude
if (
resolved_flavor is None
and resolved_vendor == "vertexai"
and model_family == _ANTHROPIC_FAMILY
and model_family == ModelFamily.ANTHROPIC_CLAUDE
):
resolved_flavor = ApiFlavor.ANTHROPIC_CLAUDE

Expand All @@ -266,7 +247,7 @@ def _resolve_llm_provider(self) -> str:
The model_family disambiguates cases where the same vendor hosts
models from different providers (e.g. Claude on Vertex AI or Bedrock).
"""
is_claude = self._model_family == _ANTHROPIC_FAMILY
is_claude = self._model_family == ModelFamily.ANTHROPIC_CLAUDE
vendor = str(self._api_config.vendor_type or "openai")

# Claude on Vertex AI → vertex_ai (uses VertexAIAnthropicConfig)
Expand Down
2 changes: 2 additions & 0 deletions src/uipath/llm_client/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
ApiFlavor,
ApiType,
ByomApiFlavor,
ModelFamily,
RoutingMode,
VendorType,
)
Expand Down Expand Up @@ -103,4 +104,5 @@ def get_default_client_settings(
"VendorType",
"ApiFlavor",
"ByomApiFlavor",
"ModelFamily",
]
Loading