Skip to content

Commit d571ab0

Browse files
cosminachoclaude
andauthored
Feat: discovery cache, get_model_info, and ModelFamily constants (#63)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 69d9cf3 commit d571ab0

16 files changed

Lines changed: 364 additions & 132 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

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

5+
## [1.9.0] - 2026-04-17
6+
7+
### Added
8+
- `ModelFamily` StrEnum constants (`OPENAI`, `GOOGLE_GEMINI`, `ANTHROPIC_CLAUDE`) for model family matching
9+
- `get_model_info()` on `UiPathBaseSettings` — centralized model lookup with filtering by name, vendor, and BYO connection ID
10+
- Discovery cache on `get_available_models()` keyed by settings properties, with `refresh` parameter to bypass
11+
12+
### Changed
13+
- `get_available_models()` is now a concrete cached method on the base class; subclasses implement `_fetch_available_models()` instead
14+
- `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()`
15+
- LiteLLM client uses `get_model_info()` instead of duplicating model discovery logic
16+
517
## [1.8.3] - 2026-04-16
618

719
### Added

packages/uipath_langchain_client/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

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

5+
## [1.9.0] - 2026-04-17
6+
7+
### Changed
8+
- Factory functions use `ModelFamily` constants and `get_model_info()` from core instead of inline discovery logic
9+
- Azure vs non-Azure OpenAI routing now uses `modelFamily` instead of `modelSubscriptionType`
10+
511
## [1.8.3] - 2026-04-16
612

713
### Added

packages/uipath_langchain_client/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ readme = "README.md"
66
requires-python = ">=3.11"
77
dependencies = [
88
"langchain>=1.2.15",
9-
"uipath-llm-client>=1.8.3",
9+
"uipath-llm-client>=1.9.0",
1010
]
1111

1212
[project.optional-dependencies]
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
__title__ = "UiPath LangChain Client"
22
__description__ = "A Python client for interacting with UiPath's LLM services via LangChain."
3-
__version__ = "1.8.3"
3+
__version__ = "1.9.0"

packages/uipath_langchain_client/src/uipath_langchain_client/factory.py

Lines changed: 8 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -30,56 +30,14 @@
3030
API_FLAVOR_TO_VENDOR_TYPE,
3131
BYOM_TO_ROUTING_FLAVOR,
3232
ApiFlavor,
33+
ModelFamily,
3334
RoutingMode,
3435
UiPathBaseSettings,
3536
VendorType,
3637
get_default_client_settings,
3738
)
3839

3940

40-
def _get_model_info(
41-
model_name: str,
42-
*,
43-
client_settings: UiPathBaseSettings,
44-
byo_connection_id: str | None = None,
45-
vendor_type: VendorType | str | None = None,
46-
) -> dict[str, Any]:
47-
available_models = client_settings.get_available_models()
48-
49-
matching_models = [m for m in available_models if m["modelName"].lower() == model_name.lower()]
50-
51-
if vendor_type is not None:
52-
matching_models = [
53-
m for m in matching_models if m.get("vendor", "").lower() == str(vendor_type).lower()
54-
]
55-
56-
if byo_connection_id:
57-
matching_models = [
58-
m
59-
for m in matching_models
60-
if (byom_details := m.get("byomDetails"))
61-
and byom_details.get("integrationServiceConnectionId", "").lower()
62-
== byo_connection_id.lower()
63-
]
64-
65-
if not byo_connection_id and len(matching_models) > 1:
66-
matching_models = [
67-
m
68-
for m in matching_models
69-
if (
70-
(m.get("modelSubscriptionType", "") == "UiPathOwned")
71-
or (m.get("byomDetails") is None)
72-
)
73-
]
74-
75-
if not matching_models:
76-
raise ValueError(
77-
f"Model {model_name} not found. Available models are: {[m['modelName'] for m in available_models]}"
78-
)
79-
80-
return matching_models[0]
81-
82-
8341
def get_chat_model(
8442
model_name: str,
8543
*,
@@ -120,18 +78,12 @@ def get_chat_model(
12078
ValueError: If the model is not found in available models or vendor is not supported.
12179
"""
12280
client_settings = client_settings or get_default_client_settings()
123-
model_info = _get_model_info(
81+
model_info = client_settings.get_model_info(
12482
model_name,
125-
client_settings=client_settings,
12683
byo_connection_id=byo_connection_id,
12784
vendor_type=vendor_type,
12885
)
12986
model_family = model_info.get("modelFamily", None)
130-
if model_family is not None:
131-
model_family = model_family.lower()
132-
is_uipath_owned = model_info.get("modelSubscriptionType") == "UiPathOwned"
133-
if not is_uipath_owned:
134-
client_settings.validate_byo_model(model_info)
13587

13688
if custom_class is not None:
13789
return custom_class(
@@ -171,7 +123,7 @@ def get_chat_model(
171123

172124
match discovered_vendor_type:
173125
case VendorType.OPENAI:
174-
if is_uipath_owned:
126+
if model_family == ModelFamily.OPENAI:
175127
from uipath_langchain_client.clients.openai.chat_models import (
176128
UiPathAzureChatOpenAI,
177129
)
@@ -196,7 +148,7 @@ def get_chat_model(
196148
**model_kwargs,
197149
)
198150
case VendorType.VERTEXAI:
199-
if model_family == "anthropicclaude":
151+
if model_family == ModelFamily.ANTHROPIC_CLAUDE:
200152
from uipath_langchain_client.clients.anthropic.chat_models import (
201153
UiPathChatAnthropic,
202154
)
@@ -220,7 +172,7 @@ def get_chat_model(
220172
**model_kwargs,
221173
)
222174
case VendorType.AWSBEDROCK:
223-
if model_family == "anthropicclaude" and api_flavor is None:
175+
if model_family == ModelFamily.ANTHROPIC_CLAUDE and api_flavor is None:
224176
from uipath_langchain_client.clients.bedrock.chat_models import (
225177
UiPathChatAnthropicBedrock,
226178
)
@@ -300,15 +252,12 @@ def get_embedding_model(
300252
>>> vectors = embeddings.embed_documents(["Hello world"])
301253
"""
302254
client_settings = client_settings or get_default_client_settings()
303-
model_info = _get_model_info(
255+
model_info = client_settings.get_model_info(
304256
model_name,
305-
client_settings=client_settings,
306257
byo_connection_id=byo_connection_id,
307258
vendor_type=vendor_type,
308259
)
309-
is_uipath_owned = model_info.get("modelSubscriptionType") == "UiPathOwned"
310-
if not is_uipath_owned:
311-
client_settings.validate_byo_model(model_info)
260+
model_family = model_info.get("modelFamily", None)
312261

313262
if custom_class is not None:
314263
return custom_class(
@@ -342,7 +291,7 @@ def get_embedding_model(
342291
discovered_vendor_type = discovered_vendor_type.lower()
343292
match discovered_vendor_type:
344293
case VendorType.OPENAI:
345-
if is_uipath_owned:
294+
if model_family == ModelFamily.OPENAI:
346295
from uipath_langchain_client.clients.openai.embeddings import (
347296
UiPathAzureOpenAIEmbeddings,
348297
)

packages/uipath_langchain_client/src/uipath_langchain_client/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
ApiFlavor,
2929
ApiType,
3030
ByomApiFlavor,
31+
ModelFamily,
3132
RoutingMode,
3233
VendorType,
3334
)
@@ -42,6 +43,7 @@
4243
"RoutingMode",
4344
"ApiFlavor",
4445
"ByomApiFlavor",
46+
"ModelFamily",
4547
"VendorType",
4648
"API_FLAVOR_TO_VENDOR_TYPE",
4749
"BYOM_TO_ROUTING_FLAVOR",
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
__title__ = "UiPath LLM Client"
22
__description__ = "A Python client for interacting with UiPath's LLM services."
3-
__version__ = "1.8.3"
3+
__version__ = "1.9.0"

src/uipath/llm_client/clients/litellm/client.py

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
ApiFlavor,
3232
ApiType,
3333
ByomApiFlavor,
34+
ModelFamily,
3435
RoutingMode,
3536
VendorType,
3637
)
@@ -99,8 +100,6 @@
99100
ByomApiFlavor.AWS_BEDROCK_CONVERSE: "bedrock",
100101
}
101102

102-
_ANTHROPIC_FAMILY = "anthropicclaude"
103-
104103

105104
def _drop_nones(**kwargs: Any) -> dict[str, Any]:
106105
return {k: v for k, v in kwargs.items() if v is not None}
@@ -189,27 +188,9 @@ def _discover_and_build_api_config(
189188
User-supplied ``vendor_type`` filters models during discovery.
190189
User-supplied ``api_flavor`` overrides the discovered value.
191190
"""
192-
available_models = self._client_settings.get_available_models()
193-
matching = [
194-
m for m in available_models if m["modelName"].lower() == self._model_name.lower()
195-
]
196-
197-
if vendor_type is not None:
198-
matching = [
199-
m for m in matching if m.get("vendor", "").lower() == str(vendor_type).lower()
200-
]
201-
202-
if not matching:
203-
raise ValueError(
204-
f"Model '{self._model_name}' not found. "
205-
f"Available: {[m['modelName'] for m in available_models]}"
206-
)
207-
model_info = matching[0]
208-
209-
model_family: str | None = None
210-
raw_family = model_info.get("modelFamily", None)
211-
if raw_family is not None:
212-
model_family = raw_family.lower()
191+
model_info = self._client_settings.get_model_info(self._model_name, vendor_type=vendor_type)
192+
193+
model_family = model_info.get("modelFamily", None)
213194

214195
discovered_vendor = model_info.get("vendor", None)
215196
discovered_flavor = model_info.get("apiFlavor", None)
@@ -240,15 +221,15 @@ def _discover_and_build_api_config(
240221
if (
241222
resolved_flavor is None
242223
and resolved_vendor == "awsbedrock"
243-
and model_family == _ANTHROPIC_FAMILY
224+
and model_family == ModelFamily.ANTHROPIC_CLAUDE
244225
):
245226
resolved_flavor = ApiFlavor.INVOKE
246227

247228
# Claude on Vertex defaults to anthropic-claude
248229
if (
249230
resolved_flavor is None
250231
and resolved_vendor == "vertexai"
251-
and model_family == _ANTHROPIC_FAMILY
232+
and model_family == ModelFamily.ANTHROPIC_CLAUDE
252233
):
253234
resolved_flavor = ApiFlavor.ANTHROPIC_CLAUDE
254235

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

272253
# Claude on Vertex AI → vertex_ai (uses VertexAIAnthropicConfig)

src/uipath/llm_client/settings/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
ApiFlavor,
3535
ApiType,
3636
ByomApiFlavor,
37+
ModelFamily,
3738
RoutingMode,
3839
VendorType,
3940
)
@@ -103,4 +104,5 @@ def get_default_client_settings(
103104
"VendorType",
104105
"ApiFlavor",
105106
"ByomApiFlavor",
107+
"ModelFamily",
106108
]

0 commit comments

Comments
 (0)