Skip to content

Commit f928d19

Browse files
cosminachoclaude
andcommitted
feat(openai): route OpenAI Realtime (WebSocket) through LLM Gateway
UiPathOpenAI / UiPathAsyncOpenAI now expose `client.realtime.connect()` exactly like the stock OpenAI SDK, opening a WebSocket to the gateway's passthrough realtime endpoint (.../vendor/<vendor>/model/<model>/realtime). The .realtime resource points websocket_base_url at the gateway, sets the S2S bearer token as api_key (sent as Authorization: Bearer on the upgrade), and injects the X-UiPath-* routing headers. Completions/embeddings are unaffected: their auth uses the httpx pipeline, and the realtime URL is built lazily on .realtime access. - openai extra now installs openai[realtime] (pulls in websockets) - langchain openai extra pulls core[openai] so realtime works from a langchain install (LangChain has no realtime chat-model abstraction) - bump core + langchain to 1.15.0 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent cc7a3bf commit f928d19

10 files changed

Lines changed: 496 additions & 4 deletions

File tree

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_llm_client` (core package) will be documented in this file.
44

5+
## [1.15.0] - 2026-06-18
6+
7+
### Added
8+
- **OpenAI Realtime (WebSocket) routed through the LLM Gateway.** `UiPathOpenAI` and `UiPathAsyncOpenAI` now expose `client.realtime.connect()`, exactly like the stock OpenAI SDK, opening a WebSocket to the gateway's passthrough realtime endpoint (`.../vendor/<vendor>/model/<model>/realtime`). On connect the client points its `websocket_base_url` at the gateway, refreshes the S2S bearer token into `api_key` (sent as `Authorization: Bearer` on the WebSocket upgrade), and injects the `X-UiPath-*` routing headers. The realtime URL uses the `nativeopenai` vendor segment and is built lazily on `.realtime` access, so completions/embeddings construction and auth are unaffected. New helper `build_realtime_ws_base_url(settings, model_name=..., vendor_type=...)` in `uipath.llm_client.clients.openai.realtime`.
9+
- The `openai` optional extra now installs `openai[realtime]`, pulling in the `websockets` dependency required for realtime connections.
10+
511
## [1.14.0] - 2026-06-15
612

713
### Added

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,35 @@ All native SDK wrappers are available in sync and async variants:
466466
| `UiPathAnthropicFoundry` / `UiPathAsyncAnthropicFoundry` | `anthropic.AnthropicFoundry` | Anthropic via Azure Foundry |
467467
| `UiPathGoogle` | `google.genai.Client` | Google Gemini models |
468468

469+
#### Realtime (WebSocket)
470+
471+
`UiPathOpenAI` / `UiPathAsyncOpenAI` also expose the OpenAI Realtime API over a WebSocket, routed through the LLM Gateway. Use `client.realtime.connect()` exactly as with the stock OpenAI SDK — the bearer token and `X-UiPath-*` routing headers are applied on the WebSocket upgrade automatically. Requires the `openai` extra (it pulls in `websockets`).
472+
473+
```python
474+
import asyncio
475+
from uipath.llm_client.clients.openai import UiPathAsyncOpenAI
476+
477+
async def main():
478+
client = UiPathAsyncOpenAI(model_name="gpt-realtime")
479+
async with client.realtime.connect() as conn:
480+
await conn.session.update(session={"type": "realtime", "output_modalities": ["text"]})
481+
await conn.conversation.item.create(
482+
item={
483+
"type": "message",
484+
"role": "user",
485+
"content": [{"type": "input_text", "text": "Say hello!"}],
486+
}
487+
)
488+
await conn.response.create()
489+
async for event in conn:
490+
if event.type == "response.output_text.delta":
491+
print(event.delta, end="", flush=True)
492+
elif event.type == "response.done":
493+
break
494+
495+
asyncio.run(main())
496+
```
497+
469498
### Low-Level HTTP Client
470499

471500
For completely custom HTTP requests, use the low-level HTTPX client directly:

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.15.0] - 2026-06-18
6+
7+
### Changed
8+
- Bumped `uipath-llm-client` floor to `>=1.15.0` to pick up OpenAI Realtime (WebSocket) support on `UiPathOpenAI` / `UiPathAsyncOpenAI` (`client.realtime.connect()` routed through the LLM Gateway). The realtime clients live in the core package (`uipath.llm_client.clients.openai`); no LangChain-specific wrapper is added, since LangChain has no realtime chat-model abstraction — realtime is used by dropping down to the core client.
9+
- The `openai` extra now also installs `uipath-llm-client[openai]` (which pulls `openai[realtime]``websockets`), so realtime works out of the box from a `uipath-langchain-client[openai]` install.
10+
511
## [1.14.0] - 2026-06-15
612

713
### Added

packages/uipath_langchain_client/pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@ readme = "README.md"
66
requires-python = ">=3.11"
77
dependencies = [
88
"langchain>=1.2.15,<2.0.0",
9-
"uipath-llm-client>=1.14.0,<2.0.0",
9+
"uipath-llm-client>=1.15.0,<2.0.0",
1010
]
1111

1212
[project.optional-dependencies]
1313
openai = [
1414
"langchain-openai>=1.2.0,<2.0.0",
15+
# Pulls the core OpenAI extra (openai[realtime] -> websockets) so realtime is
16+
# usable via uipath.llm_client.clients.openai.UiPathAsyncOpenAI from a
17+
# langchain install. LangChain itself has no realtime chat-model abstraction.
18+
"uipath-llm-client[openai]>=1.15.0,<2.0.0",
1519
]
1620
google = [
1721
"langchain-google-genai>=4.2.2,<5.0.0",
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.14.0"
3+
__version__ = "1.15.0"

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ authors = [
1919

2020
[project.optional-dependencies]
2121
openai = [
22-
"openai>=2.30.0,<3.0.0",
22+
"openai[realtime]>=2.30.0,<3.0.0",
2323
]
2424
google = [
2525
"google-genai>=1.73.1,<2.0.0",
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.14.0"
3+
__version__ = "1.15.0"

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import logging
22
from collections.abc import Mapping, Sequence
3+
from functools import cached_property
34

5+
from uipath.llm_client.clients.openai.realtime import (
6+
DEFAULT_REALTIME_VENDOR,
7+
_UiPathAsyncRealtime,
8+
_UiPathRealtime,
9+
)
410
from uipath.llm_client.clients.openai.utils import OpenAIRequestHandler
511
from uipath.llm_client.httpx_client import UiPathHttpxAsyncClient, UiPathHttpxClient
612
from uipath.llm_client.settings import UiPathBaseSettings, get_default_client_settings
713
from uipath.llm_client.utils.retry import RetryConfig
814

915
try:
1016
from openai import AsyncAzureOpenAI, AsyncOpenAI, AzureOpenAI, OpenAI
17+
from openai.resources.realtime import AsyncRealtime, Realtime
1118
except ImportError as e:
1219
raise ImportError(
1320
"The 'openai' extra is required to use UiPathOpenAIClient. "
@@ -74,6 +81,24 @@ def __init__(
7481
http_client=httpx_client,
7582
base_url=str(httpx_client.base_url).rstrip("/"),
7683
)
84+
self._uipath_client_settings = client_settings
85+
self._uipath_realtime_model = model_name
86+
87+
# Subtype override of the SDK's cached_property; returns a gateway-routed
88+
# Realtime resource. pyright flags any cached_property override, so suppress.
89+
@cached_property
90+
def realtime(self) -> Realtime: # pyright: ignore[reportIncompatibleMethodOverride]
91+
"""OpenAI Realtime (WebSocket) resource routed through the LLM Gateway.
92+
93+
Use ``with client.realtime.connect() as conn:`` — the connection targets
94+
the gateway's passthrough realtime endpoint for this client's model.
95+
"""
96+
return _UiPathRealtime(
97+
self,
98+
settings=self._uipath_client_settings,
99+
model=self._uipath_realtime_model,
100+
vendor_type=DEFAULT_REALTIME_VENDOR,
101+
)
77102

78103

79104
class UiPathAsyncOpenAI(AsyncOpenAI):
@@ -135,6 +160,24 @@ def __init__(
135160
http_client=httpx_client,
136161
base_url=str(httpx_client.base_url).rstrip("/"),
137162
)
163+
self._uipath_client_settings = client_settings
164+
self._uipath_realtime_model = model_name
165+
166+
# Subtype override of the SDK's cached_property; returns a gateway-routed
167+
# AsyncRealtime resource. pyright flags any cached_property override, so suppress.
168+
@cached_property
169+
def realtime(self) -> AsyncRealtime: # pyright: ignore[reportIncompatibleMethodOverride]
170+
"""OpenAI Realtime (WebSocket) resource routed through the LLM Gateway.
171+
172+
Use ``async with client.realtime.connect() as conn:`` — the connection
173+
targets the gateway's passthrough realtime endpoint for this client's model.
174+
"""
175+
return _UiPathAsyncRealtime(
176+
self,
177+
settings=self._uipath_client_settings,
178+
model=self._uipath_realtime_model,
179+
vendor_type=DEFAULT_REALTIME_VENDOR,
180+
)
138181

139182

140183
class UiPathAzureOpenAI(AzureOpenAI):
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
"""Realtime (WebSocket) support for the UiPath OpenAI clients.
2+
3+
The OpenAI Realtime API speaks over a WebSocket rather than HTTP, so it does not
4+
go through the httpx routing used for completions/embeddings. Instead,
5+
``UiPathOpenAI`` / ``UiPathAsyncOpenAI`` expose ``client.realtime.connect()`` —
6+
exactly like the stock OpenAI SDK — by swapping in the resource wrappers defined
7+
here. On connect these wrappers:
8+
9+
- point the client's ``websocket_base_url`` at the gateway passthrough realtime
10+
path (``.../vendor/<vendor>/model/<model>``); the SDK appends ``/realtime``,
11+
- set the S2S bearer token as ``api_key`` (the SDK sends it as
12+
``Authorization: Bearer`` on the WebSocket upgrade),
13+
- inject the ``X-UiPath-*`` routing headers on the upgrade request.
14+
15+
Completions/embeddings are unaffected: their auth comes from the httpx auth
16+
pipeline, and the realtime URL is only built when ``.realtime`` is accessed.
17+
18+
Example:
19+
>>> import asyncio
20+
>>> from uipath.llm_client.clients.openai import UiPathAsyncOpenAI
21+
>>>
22+
>>> async def main():
23+
... client = UiPathAsyncOpenAI(model_name="gpt-realtime")
24+
... async with client.realtime.connect() as conn:
25+
... await conn.session.update(
26+
... session={"type": "realtime", "output_modalities": ["text"]}
27+
... )
28+
... await conn.conversation.item.create(
29+
... item={
30+
... "type": "message",
31+
... "role": "user",
32+
... "content": [{"type": "input_text", "text": "Say hello!"}],
33+
... }
34+
... )
35+
... await conn.response.create()
36+
... async for event in conn:
37+
... if event.type == "response.output_text.delta":
38+
... print(event.delta, end="")
39+
... elif event.type == "response.done":
40+
... break
41+
>>> asyncio.run(main())
42+
"""
43+
44+
import re
45+
46+
from typing_extensions import override
47+
48+
from uipath.llm_client.settings import UiPathAPIConfig, UiPathBaseSettings
49+
from uipath.llm_client.settings.constants import RoutingMode
50+
from uipath.llm_client.settings.llmgateway.auth import LLMGatewayS2SAuth
51+
52+
try:
53+
from openai import AsyncOpenAI, OpenAI
54+
from openai._types import Headers, Omit, Query, omit
55+
from openai.resources.realtime import AsyncRealtime, Realtime
56+
from openai.resources.realtime.realtime import (
57+
AsyncRealtimeConnectionManager,
58+
RealtimeConnectionManager,
59+
)
60+
from openai.types.websocket_connection_options import WebSocketConnectionOptions
61+
except ImportError as e:
62+
raise ImportError(
63+
"The 'openai' extra is required for realtime support. "
64+
"Install it with: uv add uipath-llm-client[openai]"
65+
) from e
66+
67+
# The gateway expects the native-OpenAI realtime vendor segment in the path.
68+
DEFAULT_REALTIME_VENDOR = "nativeopenai"
69+
# Passthrough api_type segment for the realtime endpoint (not a normalized ApiType).
70+
REALTIME_API_TYPE = "realtime"
71+
72+
73+
def _realtime_api_config(vendor_type: str) -> UiPathAPIConfig:
74+
return UiPathAPIConfig(
75+
api_type=REALTIME_API_TYPE,
76+
routing_mode=RoutingMode.PASSTHROUGH,
77+
vendor_type=vendor_type,
78+
)
79+
80+
81+
def build_realtime_ws_base_url(
82+
settings: UiPathBaseSettings,
83+
*,
84+
model_name: str,
85+
vendor_type: str = DEFAULT_REALTIME_VENDOR,
86+
) -> str:
87+
"""Build the ``websocket_base_url`` to hand to the OpenAI SDK.
88+
89+
The SDK appends ``/realtime`` to ``websocket_base_url`` when connecting, so
90+
this strips the trailing ``/realtime`` produced by ``build_base_url`` and
91+
converts the scheme to ``wss``/``ws``.
92+
"""
93+
url = settings.build_base_url(
94+
model_name=model_name, api_config=_realtime_api_config(vendor_type)
95+
)
96+
suffix = f"/{REALTIME_API_TYPE}"
97+
if url.endswith(suffix):
98+
url = url[: -len(suffix)]
99+
# Collapse accidental double slashes (e.g. a trailing slash in base_url),
100+
# leaving the scheme's own "//" intact.
101+
scheme, sep, rest = url.partition("://")
102+
if sep:
103+
url = f"{scheme}://{re.sub(r'/{2,}', '/', rest)}"
104+
if url.startswith("https://"):
105+
return "wss://" + url[len("https://") :]
106+
if url.startswith("http://"):
107+
return "ws://" + url[len("http://") :]
108+
return url
109+
110+
111+
def _prepare_connection(
112+
client: "OpenAI | AsyncOpenAI",
113+
settings: UiPathBaseSettings,
114+
*,
115+
model: str,
116+
vendor_type: str,
117+
extra_headers: Headers,
118+
) -> Headers:
119+
"""Configure ``client`` for a gateway realtime connection to ``model``.
120+
121+
Sets ``websocket_base_url`` and the S2S bearer token (read straight from the
122+
gateway auth handler), and returns the ``X-UiPath-*`` routing headers merged
123+
with ``extra_headers`` for the WebSocket upgrade.
124+
"""
125+
client.websocket_base_url = build_realtime_ws_base_url(
126+
settings, model_name=model, vendor_type=vendor_type
127+
)
128+
auth = settings.build_auth_pipeline()
129+
if isinstance(auth, LLMGatewayS2SAuth) and auth.access_token:
130+
client.api_key = auth.access_token
131+
merged: dict[str, object] = {
132+
**settings.build_auth_headers(
133+
model_name=model, api_config=_realtime_api_config(vendor_type)
134+
)
135+
}
136+
if extra_headers:
137+
merged.update(extra_headers)
138+
return merged # type: ignore[return-value]
139+
140+
141+
class _UiPathRealtime(Realtime):
142+
"""``Realtime`` resource that routes ``connect()`` through the gateway."""
143+
144+
def __init__(
145+
self, client: OpenAI, *, settings: UiPathBaseSettings, model: str, vendor_type: str
146+
) -> None:
147+
super().__init__(client)
148+
self._uipath_settings = settings
149+
self._uipath_model = model
150+
self._uipath_vendor = vendor_type
151+
152+
@override
153+
def connect(
154+
self,
155+
*,
156+
call_id: str | Omit = omit,
157+
model: str | Omit = omit,
158+
extra_query: Query = {},
159+
extra_headers: Headers = {},
160+
websocket_connection_options: WebSocketConnectionOptions = {},
161+
) -> RealtimeConnectionManager:
162+
resolved_model: str = self._uipath_model if model is omit else model # type: ignore[assignment]
163+
merged_headers = _prepare_connection(
164+
self._client,
165+
self._uipath_settings,
166+
model=resolved_model,
167+
vendor_type=self._uipath_vendor,
168+
extra_headers=extra_headers,
169+
)
170+
return super().connect(
171+
call_id=call_id,
172+
model=resolved_model,
173+
extra_query=extra_query,
174+
extra_headers=merged_headers,
175+
websocket_connection_options=websocket_connection_options,
176+
)
177+
178+
179+
class _UiPathAsyncRealtime(AsyncRealtime):
180+
"""``AsyncRealtime`` resource that routes ``connect()`` through the gateway."""
181+
182+
def __init__(
183+
self, client: AsyncOpenAI, *, settings: UiPathBaseSettings, model: str, vendor_type: str
184+
) -> None:
185+
super().__init__(client)
186+
self._uipath_settings = settings
187+
self._uipath_model = model
188+
self._uipath_vendor = vendor_type
189+
190+
@override
191+
def connect(
192+
self,
193+
*,
194+
call_id: str | Omit = omit,
195+
model: str | Omit = omit,
196+
extra_query: Query = {},
197+
extra_headers: Headers = {},
198+
websocket_connection_options: WebSocketConnectionOptions = {},
199+
) -> AsyncRealtimeConnectionManager:
200+
resolved_model: str = self._uipath_model if model is omit else model # type: ignore[assignment]
201+
merged_headers = _prepare_connection(
202+
self._client,
203+
self._uipath_settings,
204+
model=resolved_model,
205+
vendor_type=self._uipath_vendor,
206+
extra_headers=extra_headers,
207+
)
208+
return super().connect(
209+
call_id=call_id,
210+
model=resolved_model,
211+
extra_query=extra_query,
212+
extra_headers=merged_headers,
213+
websocket_connection_options=websocket_connection_options,
214+
)

0 commit comments

Comments
 (0)