Skip to content

Commit 69d9cf3

Browse files
cosminachoclaude
andauthored
Feat: BYOM API flavor constants and factory resolution (#61)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3c747c5 commit 69d9cf3

14 files changed

Lines changed: 147 additions & 32 deletions

File tree

CHANGELOG.md

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

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

5+
## [1.8.3] - 2026-04-16
6+
7+
### Added
8+
- BYOM API flavor constants for discovery endpoint: `OpenAiChatCompletions`, `OpenAiResponses`, `OpenAiEmbeddings`, `GeminiGenerateContent`, `GeminiEmbeddings`, `AwsBedrockInvoke`, `AwsBedrockConverse`
9+
- `BYOM_TO_ROUTING_FLAVOR` mapping to resolve BYOM discovery flavors to routing-level API flavors
10+
- Extended `API_FLAVOR_TO_VENDOR_TYPE` with BYOM flavor entries for automatic vendor resolution
11+
- LiteLLM client now resolves BYOM discovery flavors to correct routing flavors and litellm providers
12+
513
## [1.8.2] - 2026-04-13
614

715
### Fixed

packages/uipath_langchain_client/CHANGELOG.md

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

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

5+
## [1.8.3] - 2026-04-16
6+
7+
### Added
8+
- Factory functions (`get_chat_model`, `get_embedding_model`) now automatically resolve BYOM discovery API flavors to the correct client and routing flavor
9+
510
## [1.8.2] - 2026-04-13
611

712
### Changed

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.2",
9+
"uipath-llm-client>=1.8.3",
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.2"
3+
__version__ = "1.8.3"

packages/uipath_langchain_client/src/uipath_langchain_client/clients/azure/chat_models.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from uipath_langchain_client.base_client import UiPathBaseChatModel
77
from uipath_langchain_client.clients.openai.utils import fix_url_and_api_flavor_header
88
from uipath_langchain_client.settings import (
9+
ApiFlavor,
910
ApiType,
1011
RoutingMode,
1112
UiPathAPIConfig,
@@ -31,6 +32,7 @@ class UiPathAzureAIChatCompletionsModel(UiPathBaseChatModel, AzureAIOpenAIApiCha
3132
vendor_type=VendorType.AZURE,
3233
freeze_base_url=False,
3334
)
35+
api_flavor: ApiFlavor | str | None = None
3436

3537
# Override fields to avoid env var lookup / validation errors at instantiation
3638
endpoint: str | None = Field(default="PLACEHOLDER")
@@ -40,13 +42,16 @@ class UiPathAzureAIChatCompletionsModel(UiPathBaseChatModel, AzureAIOpenAIApiCha
4042

4143
@model_validator(mode="after")
4244
def setup_uipath_client(self) -> Self:
45+
if self.api_flavor is not None:
46+
self.api_config.api_flavor = self.api_flavor
4347
base_url = str(self.uipath_sync_client.base_url).rstrip("/")
48+
locked_flavor = str(self.api_config.api_flavor) if self.api_config.api_flavor else None
4449

4550
def on_request(request: Request) -> None:
46-
fix_url_and_api_flavor_header(base_url, request)
51+
fix_url_and_api_flavor_header(base_url, request, api_flavor=locked_flavor)
4752

4853
async def on_request_async(request: Request) -> None:
49-
fix_url_and_api_flavor_header(base_url, request)
54+
fix_url_and_api_flavor_header(base_url, request, api_flavor=locked_flavor)
5055

5156
self.uipath_sync_client.event_hooks["request"].append(on_request)
5257
self.uipath_async_client.event_hooks["request"].append(on_request_async)

packages/uipath_langchain_client/src/uipath_langchain_client/clients/openai/chat_models.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from uipath_langchain_client.base_client import UiPathBaseChatModel
88
from uipath_langchain_client.clients.openai.utils import fix_url_and_api_flavor_header
99
from uipath_langchain_client.settings import (
10+
ApiFlavor,
1011
ApiType,
1112
RoutingMode,
1213
UiPathAPIConfig,
@@ -31,6 +32,7 @@ class UiPathChatOpenAI(UiPathBaseChatModel, ChatOpenAI): # type: ignore[overrid
3132
api_version="2025-03-01-preview",
3233
freeze_base_url=False,
3334
)
35+
api_flavor: ApiFlavor | str | None = None
3436

3537
# Override fields to avoid errors when instantiating the class
3638
openai_api_key: SecretStr | None | Callable[[], str] | Callable[[], Awaitable[str]] = Field(
@@ -39,13 +41,24 @@ class UiPathChatOpenAI(UiPathBaseChatModel, ChatOpenAI): # type: ignore[overrid
3941

4042
@model_validator(mode="after")
4143
def setup_uipath_client(self) -> Self:
44+
if self.api_flavor is not None:
45+
self.api_config.api_flavor = self.api_flavor
46+
# Lock LangChain's use_responses_api to match the discovered flavor.
47+
# Without this, features like reasoning={} or certain model names
48+
# silently switch LangChain to the Responses API, which would fail
49+
# if the model only supports chat-completions (or vice versa).
50+
if self.api_flavor == ApiFlavor.CHAT_COMPLETIONS:
51+
self.use_responses_api = False
52+
elif self.api_flavor == ApiFlavor.RESPONSES:
53+
self.use_responses_api = True
4254
base_url = str(self.uipath_sync_client.base_url).rstrip("/")
55+
locked_flavor = str(self.api_config.api_flavor) if self.api_config.api_flavor else None
4356

4457
def on_request(request: Request) -> None:
45-
fix_url_and_api_flavor_header(base_url, request)
58+
fix_url_and_api_flavor_header(base_url, request, api_flavor=locked_flavor)
4659

4760
async def on_request_async(request: Request) -> None:
48-
fix_url_and_api_flavor_header(base_url, request)
61+
fix_url_and_api_flavor_header(base_url, request, api_flavor=locked_flavor)
4962

5063
self.uipath_sync_client.event_hooks["request"].append(on_request)
5164
self.uipath_async_client.event_hooks["request"].append(on_request_async)
@@ -75,6 +88,7 @@ class UiPathAzureChatOpenAI(UiPathBaseChatModel, AzureChatOpenAI): # type: igno
7588
api_version="2025-03-01-preview",
7689
freeze_base_url=False,
7790
)
91+
api_flavor: ApiFlavor | str | None = None
7892

7993
# Override fields to avoid errors when instantiating the class
8094
azure_endpoint: str | None = Field(default="PLACEHOLDER")
@@ -83,13 +97,20 @@ class UiPathAzureChatOpenAI(UiPathBaseChatModel, AzureChatOpenAI): # type: igno
8397

8498
@model_validator(mode="after")
8599
def setup_uipath_client(self) -> Self:
100+
if self.api_flavor is not None:
101+
self.api_config.api_flavor = self.api_flavor
102+
if self.api_flavor == ApiFlavor.CHAT_COMPLETIONS:
103+
self.use_responses_api = False
104+
elif self.api_flavor == ApiFlavor.RESPONSES:
105+
self.use_responses_api = True
86106
base_url = str(self.uipath_sync_client.base_url).rstrip("/")
107+
locked_flavor = str(self.api_config.api_flavor) if self.api_config.api_flavor else None
87108

88109
def on_request(request: Request) -> None:
89-
fix_url_and_api_flavor_header(base_url, request)
110+
fix_url_and_api_flavor_header(base_url, request, api_flavor=locked_flavor)
90111

91112
async def on_request_async(request: Request) -> None:
92-
fix_url_and_api_flavor_header(base_url, request)
113+
fix_url_and_api_flavor_header(base_url, request, api_flavor=locked_flavor)
93114

94115
self.uipath_sync_client.event_hooks["request"].append(on_request)
95116
self.uipath_async_client.event_hooks["request"].append(on_request_async)

packages/uipath_langchain_client/src/uipath_langchain_client/clients/openai/utils.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,28 @@
55
from uipath_langchain_client.settings import ApiFlavor
66

77

8-
def fix_url_and_api_flavor_header(base_url: str, request: Request) -> None:
9-
"""Detect API flavor from URL suffix and rewrite the URL to the base gateway URL.
8+
def fix_url_and_api_flavor_header(
9+
base_url: str, request: Request, *, api_flavor: str | None = None
10+
) -> None:
11+
"""Set the API flavor header and rewrite the URL to the base gateway URL.
1012
11-
Inspects the outgoing request URL to determine whether it targets the
12-
OpenAI *responses* or *chat completions* endpoint and sets the
13-
``X-UiPath-LlmGateway-ApiFlavor`` header accordingly. The request URL
14-
is then collapsed back to *base_url* so that the gateway receives a
15-
clean path.
13+
When *api_flavor* is provided (e.g. from the discovery endpoint), it is
14+
used directly — the model only supports that specific flavor. Otherwise
15+
the flavor is inferred from the outgoing URL suffix (``/responses`` vs
16+
``/chat/completions``).
1617
1718
Args:
1819
base_url: The UiPath gateway base URL to rewrite the request to.
1920
request: The outgoing httpx request (mutated in place).
21+
api_flavor: Locked API flavor from discovery. When set, overrides
22+
dynamic detection from the URL path.
2023
"""
21-
url_suffix = str(request.url).split(base_url)[-1]
22-
if "responses" in url_suffix:
23-
request.headers["X-UiPath-LlmGateway-ApiFlavor"] = ApiFlavor.RESPONSES.value
24+
if api_flavor is not None:
25+
request.headers["X-UiPath-LlmGateway-ApiFlavor"] = api_flavor
2426
else:
25-
request.headers["X-UiPath-LlmGateway-ApiFlavor"] = ApiFlavor.CHAT_COMPLETIONS.value
27+
url_suffix = str(request.url).split(base_url)[-1]
28+
if "responses" in url_suffix:
29+
request.headers["X-UiPath-LlmGateway-ApiFlavor"] = ApiFlavor.RESPONSES.value
30+
else:
31+
request.headers["X-UiPath-LlmGateway-ApiFlavor"] = ApiFlavor.CHAT_COMPLETIONS.value
2632
request.url = URL(base_url)

packages/uipath_langchain_client/src/uipath_langchain_client/factory.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
)
2929
from uipath_langchain_client.settings import (
3030
API_FLAVOR_TO_VENDOR_TYPE,
31+
BYOM_TO_ROUTING_FLAVOR,
3132
ApiFlavor,
3233
RoutingMode,
3334
UiPathBaseSettings,
@@ -160,11 +161,16 @@ def get_chat_model(
160161
raise ValueError("No vendor type or api flavor found in model info")
161162
discovered_vendor_type = discovered_vendor_type.lower()
162163

164+
# Discovered api_flavor takes precedence over user-supplied api_flavor
165+
if discovered_api_flavor is not None:
166+
routing_flavor = BYOM_TO_ROUTING_FLAVOR.get(discovered_api_flavor)
167+
if routing_flavor is not None:
168+
api_flavor = routing_flavor
169+
else:
170+
api_flavor = discovered_api_flavor
171+
163172
match discovered_vendor_type:
164173
case VendorType.OPENAI:
165-
if api_flavor == ApiFlavor.RESPONSES:
166-
model_kwargs["use_responses_api"] = True
167-
168174
if is_uipath_owned:
169175
from uipath_langchain_client.clients.openai.chat_models import (
170176
UiPathAzureChatOpenAI,
@@ -173,6 +179,7 @@ def get_chat_model(
173179
return UiPathAzureChatOpenAI(
174180
model=model_name,
175181
settings=client_settings,
182+
api_flavor=api_flavor,
176183
byo_connection_id=byo_connection_id,
177184
**model_kwargs,
178185
)
@@ -184,6 +191,7 @@ def get_chat_model(
184191
return UiPathChatOpenAI(
185192
model=model_name,
186193
settings=client_settings,
194+
api_flavor=api_flavor,
187195
byo_connection_id=byo_connection_id,
188196
**model_kwargs,
189197
)
@@ -323,10 +331,9 @@ def get_embedding_model(
323331
)
324332

325333
discovered_vendor_type = model_info.get("vendor")
326-
if discovered_vendor_type is None:
327-
discovered_api_flavor = model_info.get("apiFlavor")
328-
if discovered_api_flavor is not None:
329-
discovered_vendor_type = API_FLAVOR_TO_VENDOR_TYPE.get(discovered_api_flavor)
334+
discovered_api_flavor = model_info.get("apiFlavor")
335+
if discovered_vendor_type is None and discovered_api_flavor is not None:
336+
discovered_vendor_type = API_FLAVOR_TO_VENDOR_TYPE.get(discovered_api_flavor)
330337
if discovered_vendor_type is None:
331338
raise ValueError(
332339
f"No vendor type found in model info for embedding model '{model_name}'. "

packages/uipath_langchain_client/src/uipath_langchain_client/settings.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@
2424
)
2525
from uipath.llm_client.settings.constants import (
2626
API_FLAVOR_TO_VENDOR_TYPE,
27+
BYOM_TO_ROUTING_FLAVOR,
2728
ApiFlavor,
2829
ApiType,
30+
ByomApiFlavor,
2931
RoutingMode,
3032
VendorType,
3133
)
@@ -39,6 +41,8 @@
3941
"ApiType",
4042
"RoutingMode",
4143
"ApiFlavor",
44+
"ByomApiFlavor",
4245
"VendorType",
4346
"API_FLAVOR_TO_VENDOR_TYPE",
47+
"BYOM_TO_ROUTING_FLAVOR",
4448
]
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.2"
3+
__version__ = "1.8.3"

0 commit comments

Comments
 (0)