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

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

## [1.14.1] - 2026-06-23

### Fixed
- `WrappedBotoClient` (the httpx-backed Bedrock shim) now calls `raise_for_status()` in `converse`, `invoke_model`, and the streaming generator before reading the response. Previously a non-2xx gateway response (e.g. 403 License-not-available) was parsed as a normal result and handed to `langchain_aws`, which raised a misleading `ValueError("No 'output' key found in the response from the Bedrock Converse API ... misconfiguration of endpoint or region")` — the real status code and `detail` were lost. Gateway HTTP errors now surface as the patched `UiPathAPIError` subclass (e.g. `UiPathPermissionDeniedError`), matching the OpenAI and Vertex paths. For streaming responses the error body is read first so the typed exception retains its `detail`.

## [1.14.0] - 2026-06-15

### Added
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.14.0"
__version__ = "1.14.1"
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ def _stream_generator(
if self.httpx_client is None:
raise ValueError("httpx_client is not set")
with self.httpx_client.stream("POST", "/", json=_serialize_bytes(request_body)) as response:
if response.is_error:
# The gateway returns a non-streamed JSON error body; read it so
# the patched raise_for_status surfaces it (with detail) instead
# of the EventStreamBuffer choking on a non-event payload.
response.read()
response.raise_for_status()
buffer = EventStreamBuffer()
for chunk in response.iter_bytes():
buffer.add_data(chunk)
Expand All @@ -71,12 +77,12 @@ def _stream_generator(
def invoke_model(self, **kwargs: Any) -> Any:
if self.httpx_client is None:
raise ValueError("httpx_client is not set")
return {
"body": self.httpx_client.post(
"/",
json=json.loads(kwargs.get("body", "{}")),
)
}
response = self.httpx_client.post(
"/",
json=json.loads(kwargs.get("body", "{}")),
)
response.raise_for_status()
return {"body": response}

def invoke_model_with_response_stream(self, **kwargs: Any) -> Any:
return {"body": self._stream_generator(json.loads(kwargs.get("body", "{}")))}
Expand All @@ -90,7 +96,7 @@ def converse(
) -> Any:
if self.httpx_client is None:
raise ValueError("httpx_client is not set")
return self.httpx_client.post(
response = self.httpx_client.post(
"/",
json=_serialize_bytes(
{
Expand All @@ -99,7 +105,9 @@ def converse(
**params,
}
),
).json()
)
response.raise_for_status()
return response.json()

def converse_stream(
self,
Expand Down
60 changes: 60 additions & 0 deletions tests/langchain/clients/bedrock/test_wrapped_boto_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Unit tests for ``WrappedBotoClient`` HTTP error surfacing.

The shim talks to the LLM Gateway over httpx instead of AWS. It must call
``raise_for_status()`` so gateway HTTP errors (e.g. 403 License-not-available)
propagate as exceptions, rather than being parsed as a normal result and then
mis-reported downstream (langchain_aws raises a misleading "No 'output' key"
``ValueError`` when the response lacks the expected fields).
"""

import json

import httpx
import pytest
from uipath_langchain_client.clients.bedrock.utils import WrappedBotoClient

_ERROR_BODY = {
"title": "License not available",
"status": 403,
"detail": "License not available for LLM usage.",
}


def _wrapped(handler: object) -> WrappedBotoClient:
transport = httpx.MockTransport(handler) # type: ignore[arg-type]
return WrappedBotoClient(
httpx_client=httpx.Client(transport=transport, base_url="http://gateway")
)


def test_converse_raises_on_http_error() -> None:
client = _wrapped(lambda request: httpx.Response(403, json=_ERROR_BODY))
with pytest.raises(httpx.HTTPStatusError):
client.converse(messages=[{"role": "user", "content": [{"text": "hi"}]}])


def test_converse_returns_body_on_success() -> None:
payload = {"output": {"message": {"role": "assistant", "content": [{"text": "ok"}]}}}
client = _wrapped(lambda request: httpx.Response(200, json=payload))
assert client.converse(messages=[]) == payload


def test_invoke_model_raises_on_http_error() -> None:
client = _wrapped(lambda request: httpx.Response(403, json=_ERROR_BODY))
with pytest.raises(httpx.HTTPStatusError):
client.invoke_model(body=json.dumps({"prompt": "hi"}))


def test_converse_stream_raises_on_http_error() -> None:
# The generator defers work until iterated, so the error surfaces on consume.
client = _wrapped(lambda request: httpx.Response(403, json=_ERROR_BODY))
stream = client.converse_stream(messages=[])["stream"]
with pytest.raises(httpx.HTTPStatusError):
list(stream)


def test_invoke_model_with_response_stream_raises_on_http_error() -> None:
client = _wrapped(lambda request: httpx.Response(403, json=_ERROR_BODY))
stream = client.invoke_model_with_response_stream(body=json.dumps({"prompt": "hi"}))["body"]
with pytest.raises(httpx.HTTPStatusError):
list(stream)
Loading