Skip to content

Commit 2c47265

Browse files
cosminachoclaude
andcommitted
Fix: lock api_flavor from discovery so models use only their supported flavor
The OpenAI request hooks dynamically detected api_flavor from the URL path, allowing a model to use both chat-completions and responses. But models (BYOM or otherwise) may only support one specific flavor as reported by the discovery endpoint. Changes: - fix_url_and_api_flavor_header (langchain) accepts locked api_flavor - OpenAIRequestHandler (core) accepts locked api_flavor - LangChain OpenAI/Azure chat model hooks read api_config.api_flavor - Factory constructs api_config with the discovered flavor locked in - Discovered api_flavor (routing or BYOM) always takes precedence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 13ca2db commit 2c47265

5 files changed

Lines changed: 49 additions & 20 deletions

File tree

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,13 @@ class UiPathAzureAIChatCompletionsModel(UiPathBaseChatModel, AzureAIOpenAIApiCha
4141
@model_validator(mode="after")
4242
def setup_uipath_client(self) -> Self:
4343
base_url = str(self.uipath_sync_client.base_url).rstrip("/")
44+
locked_flavor = str(self.api_config.api_flavor) if self.api_config.api_flavor else None
4445

4546
def on_request(request: Request) -> None:
46-
fix_url_and_api_flavor_header(base_url, request)
47+
fix_url_and_api_flavor_header(base_url, request, api_flavor=locked_flavor)
4748

4849
async def on_request_async(request: Request) -> None:
49-
fix_url_and_api_flavor_header(base_url, request)
50+
fix_url_and_api_flavor_header(base_url, request, api_flavor=locked_flavor)
5051

5152
self.uipath_sync_client.event_hooks["request"].append(on_request)
5253
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: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,13 @@ class UiPathChatOpenAI(UiPathBaseChatModel, ChatOpenAI): # type: ignore[overrid
4040
@model_validator(mode="after")
4141
def setup_uipath_client(self) -> Self:
4242
base_url = str(self.uipath_sync_client.base_url).rstrip("/")
43+
locked_flavor = str(self.api_config.api_flavor) if self.api_config.api_flavor else None
4344

4445
def on_request(request: Request) -> None:
45-
fix_url_and_api_flavor_header(base_url, request)
46+
fix_url_and_api_flavor_header(base_url, request, api_flavor=locked_flavor)
4647

4748
async def on_request_async(request: Request) -> None:
48-
fix_url_and_api_flavor_header(base_url, request)
49+
fix_url_and_api_flavor_header(base_url, request, api_flavor=locked_flavor)
4950

5051
self.uipath_sync_client.event_hooks["request"].append(on_request)
5152
self.uipath_async_client.event_hooks["request"].append(on_request_async)
@@ -84,12 +85,13 @@ class UiPathAzureChatOpenAI(UiPathBaseChatModel, AzureChatOpenAI): # type: igno
8485
@model_validator(mode="after")
8586
def setup_uipath_client(self) -> Self:
8687
base_url = str(self.uipath_sync_client.base_url).rstrip("/")
88+
locked_flavor = str(self.api_config.api_flavor) if self.api_config.api_flavor else None
8789

8890
def on_request(request: Request) -> None:
89-
fix_url_and_api_flavor_header(base_url, request)
91+
fix_url_and_api_flavor_header(base_url, request, api_flavor=locked_flavor)
9092

9193
async def on_request_async(request: Request) -> None:
92-
fix_url_and_api_flavor_header(base_url, request)
94+
fix_url_and_api_flavor_header(base_url, request, api_flavor=locked_flavor)
9395

9496
self.uipath_sync_client.event_hooks["request"].append(on_request)
9597
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: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
API_FLAVOR_TO_VENDOR_TYPE,
3131
BYOM_TO_ROUTING_FLAVOR,
3232
ApiFlavor,
33+
ApiType,
3334
RoutingMode,
35+
UiPathAPIConfig,
3436
UiPathBaseSettings,
3537
VendorType,
3638
get_default_client_settings,
@@ -161,17 +163,31 @@ def get_chat_model(
161163
raise ValueError("No vendor type or api flavor found in model info")
162164
discovered_vendor_type = discovered_vendor_type.lower()
163165

164-
# Discovered BYOM api_flavor takes precedence over user-supplied api_flavor
166+
# Discovered api_flavor takes precedence over user-supplied api_flavor
165167
if discovered_api_flavor is not None:
166168
routing_flavor = BYOM_TO_ROUTING_FLAVOR.get(discovered_api_flavor)
167169
if routing_flavor is not None:
168170
api_flavor = routing_flavor
171+
else:
172+
api_flavor = discovered_api_flavor
169173

170174
match discovered_vendor_type:
171175
case VendorType.OPENAI:
172176
if api_flavor == ApiFlavor.RESPONSES:
173177
model_kwargs["use_responses_api"] = True
174178

179+
# Lock the api_flavor into the api_config so the request hook
180+
# uses it instead of dynamically detecting from the URL path.
181+
if api_flavor is not None:
182+
model_kwargs["api_config"] = UiPathAPIConfig(
183+
api_type=ApiType.COMPLETIONS,
184+
routing_mode=RoutingMode.PASSTHROUGH,
185+
vendor_type=VendorType.OPENAI,
186+
api_version="2025-03-01-preview",
187+
api_flavor=str(api_flavor),
188+
freeze_base_url=False,
189+
)
190+
175191
if is_uipath_owned:
176192
from uipath_langchain_client.clients.openai.chat_models import (
177193
UiPathAzureChatOpenAI,

src/uipath/llm_client/clients/openai/utils.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ def __init__(
1111
model_name: str,
1212
client_settings: UiPathBaseSettings,
1313
byo_connection_id: str | None = None,
14+
api_flavor: ApiFlavor | str | None = None,
1415
):
1516
self.model_name = model_name
1617
self.client_settings = client_settings
1718
self.byo_connection_id = byo_connection_id
19+
self.locked_api_flavor = str(api_flavor) if api_flavor else None
1820
self.base_api_config = UiPathAPIConfig(
1921
routing_mode=RoutingMode.PASSTHROUGH,
2022
vendor_type=VendorType.OPENAI,
@@ -37,13 +39,15 @@ def _apply_routing(self, request: Request, api_config: UiPathAPIConfig) -> None:
3739

3840
def fix_url_and_headers(self, request: Request):
3941
if request.url.path.endswith("/completions"):
42+
flavor = self.locked_api_flavor or ApiFlavor.CHAT_COMPLETIONS
4043
api_config = self.base_api_config.model_copy(
41-
update={"api_flavor": ApiFlavor.CHAT_COMPLETIONS, "api_type": ApiType.COMPLETIONS}
44+
update={"api_flavor": flavor, "api_type": ApiType.COMPLETIONS}
4245
)
4346
self._apply_routing(request, api_config)
4447
elif request.url.path.endswith("/responses"):
48+
flavor = self.locked_api_flavor or ApiFlavor.RESPONSES
4549
api_config = self.base_api_config.model_copy(
46-
update={"api_flavor": ApiFlavor.RESPONSES, "api_type": ApiType.COMPLETIONS}
50+
update={"api_flavor": flavor, "api_type": ApiType.COMPLETIONS}
4751
)
4852
self._apply_routing(request, api_config)
4953
elif request.url.path.endswith("/embeddings"):

0 commit comments

Comments
 (0)