From 052611c13dff94660e6d2aaa08f6e7bb780216a8 Mon Sep 17 00:00:00 2001 From: Mihalache Marius Date: Wed, 7 Oct 2026 18:08:46 +0300 Subject: [PATCH] fix(agent): don't crash the error mapper on LLM Gateway raw vendor error details LLM Gateway raw vendor passthrough errors carry an object detail ({"error_type", "message"}), not a ProblemDetails string. _classify passed it through as the AgentRuntimeError detail, so UiPathErrorContract failed validation and the run ended with a generic "unexpected error". Only the gateway's own string detail is first-party text; an object detail relays the vendor's message, which may carry customer PII, so it is dropped and the mapper's own detail is used (the canned text for 400, the reason phrase otherwise). The Jev tool recognises the gateway's "Unsupported model used" 400 as an unknown model, so a model the gateway does not serve surfaces as "Unknown Jev model" (user error, with the tool's own message) instead of a generic provider error. Bumps uipath-langchain to 0.18.31 and the floors to uipath>=2.14.36 and uipath-llm-client / uipath-langchain-client>=1.21.1, which drop the unserved jev-latest default. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XpbvUoddLSo7W61dwpjKFC --- pyproject.toml | 18 +++--- src/uipath_langchain/agent/exceptions/llm.py | 2 + .../internal_tools/jev_classifier_tool.py | 9 ++- tests/agent/test_llm.py | 32 ++++++++++ .../test_jev_classifier_tool.py | 11 ++++ uv.lock | 62 +++++++++---------- 6 files changed, 92 insertions(+), 42 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 48d76602b..c8a887db4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,15 +1,15 @@ [project] name = "uipath-langchain" -version = "0.18.31" +version = "0.18.32" description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" dependencies = [ - "uipath>=2.14.35, <2.15.0", + "uipath>=2.14.36, <2.15.0", "uipath-core>=0.5.33, <0.6.0", "uipath-platform>=0.2.34, <0.3.0", "uipath-runtime>=0.13.0, <0.14.0", - "uipath-llm-client>=1.21.0, <1.22.0", + "uipath-llm-client>=1.21.1, <1.22.0", "langgraph>=1.2.11, <2.0.0", "langchain-core>=1.6.1, <2.0.0", "langgraph-checkpoint-sqlite>=3.1.1, <4.0.0", @@ -28,7 +28,7 @@ dependencies = [ "pillow>=12.3.0, <13.0.0", "rdflib>=7.0.0, <8.0.0", "a2a-sdk>=1.1.2,<2.0.0", - "uipath-langchain-client[openai]>=1.21.0, <1.22.0", + "uipath-langchain-client[openai]>=1.21.1, <1.22.0", ] classifiers = [ @@ -45,23 +45,23 @@ maintainers = [ [project.optional-dependencies] anthropic = [ - "uipath-langchain-client[anthropic]>=1.21.0, <1.22.0", + "uipath-langchain-client[anthropic]>=1.21.1, <1.22.0", ] vertex = [ - "uipath-langchain-client[google]>=1.21.0, <1.22.0", + "uipath-langchain-client[google]>=1.21.1, <1.22.0", ] bedrock = [ - "uipath-langchain-client[bedrock]>=1.21.0, <1.22.0", + "uipath-langchain-client[bedrock]>=1.21.1, <1.22.0", "boto3-stubs>=1.41.4", ] fireworks = [ - "uipath-langchain-client[fireworks]>=1.21.0, <1.22.0", + "uipath-langchain-client[fireworks]>=1.21.1, <1.22.0", ] code-interpreter = [ "langchain-quickjs>=0.3.5, <0.4.0", ] all = [ - "uipath-langchain-client[all]>=1.21.0, <1.22.0", + "uipath-langchain-client[all]>=1.21.1, <1.22.0", "uipath-langchain[code-interpreter]", ] diff --git a/src/uipath_langchain/agent/exceptions/llm.py b/src/uipath_langchain/agent/exceptions/llm.py index 623643d48..bfff98389 100644 --- a/src/uipath_langchain/agent/exceptions/llm.py +++ b/src/uipath_langchain/agent/exceptions/llm.py @@ -204,6 +204,8 @@ def _classify(status_code: int, body: object) -> _Verdict: """ code, category, title, own_detail = _status_verdict(status_code, body) gateway_detail = body.get("detail") if isinstance(body, dict) else None + if not isinstance(gateway_detail, str): + gateway_detail = None return code, category, title, gateway_detail or own_detail diff --git a/src/uipath_langchain/agent/tools/internal_tools/jev_classifier_tool.py b/src/uipath_langchain/agent/tools/internal_tools/jev_classifier_tool.py index 86a4e8a4d..e6e434754 100644 --- a/src/uipath_langchain/agent/tools/internal_tools/jev_classifier_tool.py +++ b/src/uipath_langchain/agent/tools/internal_tools/jev_classifier_tool.py @@ -636,7 +636,8 @@ def _provider_message(error: UiPathAPIError) -> str: def _is_unknown_model_error(error: UiPathAPIError) -> bool: """Whether Jev rejected the call because the configured model does not exist. - TypeSafe answers 400 ``{"detail": {"message": "Unknown model: "}}``; a + TypeSafe answers 400 ``{"detail": {"message": "Unknown model: "}}`` and the + LLM Gateway 400 ``{"detail": {"message": "Unsupported model used. ..."}}``; a FastAPI-style 422 on ``body.model`` is accepted too. The model comes from the tool settings, so the LLM cannot fix it. """ @@ -648,7 +649,11 @@ def _is_unknown_model_error(error: UiPathAPIError) -> bool: isinstance(item, dict) and list(item.get("loc") or [])[-1:] == ["model"] for item in detail ) - return _provider_message(error).lower().startswith("unknown model") + return ( + _provider_message(error) + .lower() + .startswith(("unknown model", "unsupported model")) + ) def _read_tool_input( diff --git a/tests/agent/test_llm.py b/tests/agent/test_llm.py index 5264e168a..b820bf532 100644 --- a/tests/agent/test_llm.py +++ b/tests/agent/test_llm.py @@ -258,6 +258,16 @@ def test_unclassified_4xx_remains_unknown(status_code: int): } +# A raw vendor passthrough error: the gateway relays the vendor's message in an +# object detail, so it is vendor text, not first-party ProblemDetails text. +_RAW_VENDOR_BODY: dict[str, object] = { + "detail": { + "error_type": "api_usage_error", + "message": "Cannot classify state for jane.doe@example.com", + } +} + + @pytest.mark.parametrize( "err_factory", [ @@ -266,6 +276,7 @@ def test_unclassified_4xx_remains_unknown(status_code: int): lambda: _api_error(400, {"message": "Malformed input request."}), id="bedrock-envelope", ), + pytest.param(lambda: _api_error(400, _RAW_VENDOR_BODY), id="raw-vendor-detail"), pytest.param(lambda: _api_error_text(400, _EDGE_HTML), id="raw-html"), pytest.param(lambda: _api_error(400, {}), id="empty-body"), ], @@ -297,6 +308,27 @@ def test_400_does_not_quote_the_provider_body(err_factory): assert "doctype" not in rendered.lower() +@pytest.mark.parametrize( + ("status_code", "expected_detail"), + [ + pytest.param(400, None, id="400-canned"), + pytest.param(500, "Internal Server Error", id="500-reason-phrase"), + ], +) +def test_raw_vendor_detail_is_never_displayed( + status_code: int, expected_detail: str | None +) -> None: + # An object detail relays the vendor's message, which may carry customer PII. + # It must not crash the mapper (it is not a string) nor reach the user. + error = _raise(_api_error(status_code, _RAW_VENDOR_BODY)) + + if expected_detail is not None: + # SYSTEM errors carry the generic prefix; the reason phrase follows it. + assert error.error_info.detail.endswith(expected_detail) + for rendered in (error.error_info.detail, str(error), repr(error)): + assert "jane.doe" not in rendered + + def test_400_prefers_the_gateway_detail_over_the_canned_text(): """A ProblemDetails ``detail`` is first-party UiPath text and more specific.""" err = _api_error(400, {"status": 400, "detail": "Model not enabled."}) diff --git a/tests/agent/tools/internal_tools/test_jev_classifier_tool.py b/tests/agent/tools/internal_tools/test_jev_classifier_tool.py index cb0478962..0cd2888bd 100644 --- a/tests/agent/tools/internal_tools/test_jev_classifier_tool.py +++ b/tests/agent/tools/internal_tools/test_jev_classifier_tool.py @@ -1297,6 +1297,17 @@ def _jev_error(status: int, body: Any) -> UiPathAPIError: }, id="typesafe-400", ), + pytest.param( + 400, + { + "detail": { + "error_type": None, + "message": "Unsupported model used. The list of supported " + "models is : gpt-4o,jev-1.13.0", + } + }, + id="llm-gateway-400", + ), pytest.param( 422, {"detail": [{"loc": ["body", "model"], "msg": "Unknown model"}]}, diff --git a/uv.lock b/uv.lock index 2263be469..3df31fab9 100644 --- a/uv.lock +++ b/uv.lock @@ -173,9 +173,9 @@ name = "aiologic" version = "0.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sniffio", marker = "python_full_version < '3.13'" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, - { name = "wrapt", marker = "python_full_version < '3.13'" }, + { name = "sniffio" }, + { name = "typing-extensions" }, + { name = "wrapt" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f1/7a/d51f2fde1e8ae8a83431f8e97b7a71e9358cdb1d4d2ce6be387fa44d68de/aiologic-0.17.1.tar.gz", hash = "sha256:2e1b93b9e88ced318c2a63ad7b382688f40cbfe40e3d42258d49dc9c5aea179d", size = 252354, upload-time = "2026-06-27T20:41:33.25Z" } wheels = [ @@ -456,12 +456,12 @@ name = "black" version = "26.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", marker = "python_full_version != '3.12.*' or sys_platform != 'emscripten'" }, - { name = "mypy-extensions", marker = "python_full_version != '3.12.*' or sys_platform != 'emscripten'" }, - { name = "packaging", marker = "python_full_version != '3.12.*' or sys_platform != 'emscripten'" }, - { name = "pathspec", marker = "python_full_version != '3.12.*' or sys_platform != 'emscripten'" }, - { name = "platformdirs", marker = "python_full_version != '3.12.*' or sys_platform != 'emscripten'" }, - { name = "pytokens", marker = "python_full_version != '3.12.*' or sys_platform != 'emscripten'" }, + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c0/37/5628dd55bf2b34257fc7603f0fe97c40e3aaf24265f416a9c85c95ca1436/black-26.5.1.tar.gz", hash = "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73", size = 679439, upload-time = "2026-05-18T16:53:36.107Z" } wheels = [ @@ -1042,8 +1042,8 @@ name = "culsans" version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiologic", marker = "python_full_version < '3.13'" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "aiologic" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/e3/49afa1bc180e0d28008ec6bcdf82a4072d1c7a41032b5b759b60814ca4b0/culsans-0.11.0.tar.gz", hash = "sha256:0b43d0d05dce6106293d114c86e3fb4bfc63088cfe8ff08ed3fe36891447fe33", size = 107546, upload-time = "2025-12-31T23:15:38.196Z" } wheels = [ @@ -1698,8 +1698,8 @@ name = "httpcore2" version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "h11", marker = "python_full_version != '3.12.*' or sys_platform != 'emscripten'" }, - { name = "truststore", marker = "python_full_version != '3.12.*' or sys_platform != 'emscripten'" }, + { name = "h11" }, + { name = "truststore" }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" } wheels = [ @@ -4773,7 +4773,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.14.35" +version = "2.14.36" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "applicationinsights" }, @@ -4798,9 +4798,9 @@ dependencies = [ { name = "uipath-platform" }, { name = "uipath-runtime" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c5/26/6ea25f6fba4970382604c116ca4b62ed7b0a877086247aee55c1581de28e/uipath-2.14.35.tar.gz", hash = "sha256:e4d255a85b4e34746dcc4ccaccebd91f6e67df78592bd21958732311235b970f", size = 4595794, upload-time = "2026-10-06T19:43:23.5Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/cb/76826ee722f940748aeb6a2ebd3dc744e0054454d937e916cf526ab421cd/uipath-2.14.36.tar.gz", hash = "sha256:9ced9b7a7dda1719b29b75fe278c86dfb0bceedeb1445376a551d55ebd188e5b", size = 4595616, upload-time = "2026-10-07T14:55:49.605Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/bb/ca42f7e4448bec124a3a905bf28a0173d2cddad0c4972a7e5a8b543a40e6/uipath-2.14.35-py3-none-any.whl", hash = "sha256:0a515396db6845eb6bbb396cfdfa060794ce35e2b61fa4721fa0e72b45f777fe", size = 455286, upload-time = "2026-10-06T19:43:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/f4/23/3020af1f2f954268e294ef653301bc26a34b77f813ebd1705c22be1c798c/uipath-2.14.36-py3-none-any.whl", hash = "sha256:b5b3f715f23ea3a4e3ae195ff54dc3a388f35660f1e8b03b8d65cab6391aab65", size = 455300, upload-time = "2026-10-07T14:55:47.548Z" }, ] [[package]] @@ -4828,7 +4828,7 @@ wheels = [ [[package]] name = "uipath-langchain" -version = "0.18.31" +version = "0.18.32" source = { editable = "." } dependencies = [ { name = "a2a-sdk" }, @@ -4922,16 +4922,16 @@ requires-dist = [ { name = "pydantic-settings", specifier = ">=2.6.0" }, { name = "python-dotenv", specifier = ">=1.2.2,<2.0.0" }, { name = "rdflib", specifier = ">=7.0.0,<8.0.0" }, - { name = "uipath", specifier = ">=2.14.35,<2.15.0" }, + { name = "uipath", specifier = ">=2.14.36,<2.15.0" }, { name = "uipath-core", specifier = ">=0.5.33,<0.6.0" }, { name = "uipath-langchain", extras = ["code-interpreter"], marker = "extra == 'all'" }, - { name = "uipath-langchain-client", extras = ["all"], marker = "extra == 'all'", specifier = ">=1.21.0,<1.22.0" }, - { name = "uipath-langchain-client", extras = ["anthropic"], marker = "extra == 'anthropic'", specifier = ">=1.21.0,<1.22.0" }, - { name = "uipath-langchain-client", extras = ["bedrock"], marker = "extra == 'bedrock'", specifier = ">=1.21.0,<1.22.0" }, - { name = "uipath-langchain-client", extras = ["fireworks"], marker = "extra == 'fireworks'", specifier = ">=1.21.0,<1.22.0" }, - { name = "uipath-langchain-client", extras = ["google"], marker = "extra == 'vertex'", specifier = ">=1.21.0,<1.22.0" }, - { name = "uipath-langchain-client", extras = ["openai"], specifier = ">=1.21.0,<1.22.0" }, - { name = "uipath-llm-client", specifier = ">=1.21.0,<1.22.0" }, + { name = "uipath-langchain-client", extras = ["all"], marker = "extra == 'all'", specifier = ">=1.21.1,<1.22.0" }, + { name = "uipath-langchain-client", extras = ["anthropic"], marker = "extra == 'anthropic'", specifier = ">=1.21.1,<1.22.0" }, + { name = "uipath-langchain-client", extras = ["bedrock"], marker = "extra == 'bedrock'", specifier = ">=1.21.1,<1.22.0" }, + { name = "uipath-langchain-client", extras = ["fireworks"], marker = "extra == 'fireworks'", specifier = ">=1.21.1,<1.22.0" }, + { name = "uipath-langchain-client", extras = ["google"], marker = "extra == 'vertex'", specifier = ">=1.21.1,<1.22.0" }, + { name = "uipath-langchain-client", extras = ["openai"], specifier = ">=1.21.1,<1.22.0" }, + { name = "uipath-llm-client", specifier = ">=1.21.1,<1.22.0" }, { name = "uipath-platform", specifier = ">=0.2.34,<0.3.0" }, { name = "uipath-runtime", specifier = ">=0.13.0,<0.14.0" }, ] @@ -4960,15 +4960,15 @@ dev = [ [[package]] name = "uipath-langchain-client" -version = "1.21.0" +version = "1.21.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain" }, { name = "uipath-llm-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/65/16824fd3b0c36a13b0f2e5ace120ab8ff68c95b63ace28242d171e3fd877/uipath_langchain_client-1.21.0.tar.gz", hash = "sha256:cb6be8b6c1e56ae578e0378db7acf38f71d16b4da0e78b2829b1781424597c61", size = 45321, upload-time = "2026-10-06T08:37:38.363Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/a3/4a0c42de934c52d6b6ca1f58133cef1ddc8decfed30b6b350ba2588e49a4/uipath_langchain_client-1.21.1.tar.gz", hash = "sha256:cb51acd41fc3f9f36b36059e79f96575c020efe6f19e51c729b82fc2403b1c63", size = 45373, upload-time = "2026-10-07T14:56:01.866Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/79/68dcaa9e728d1c2391e84d1702a6dce19c28ce148ecc08e0a5cfb18b1220/uipath_langchain_client-1.21.0-py3-none-any.whl", hash = "sha256:f4d09d8adb0154779515da5d2508659ffb3e54b843e61c2902341a3b81f99736", size = 52168, upload-time = "2026-10-06T08:37:37.231Z" }, + { url = "https://files.pythonhosted.org/packages/02/e9/262e0cbef4e7f48213203554696fd795d1bb7ab2021ce9f05462a6d1f63d/uipath_langchain_client-1.21.1-py3-none-any.whl", hash = "sha256:87ecfdc303b85be2e853f6045815ff28d6effaf3cb7f42c725536094ad1f3097", size = 52168, upload-time = "2026-10-07T14:56:00.601Z" }, ] [package.optional-dependencies] @@ -5004,7 +5004,7 @@ openai = [ [[package]] name = "uipath-llm-client" -version = "1.21.0" +version = "1.21.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -5013,9 +5013,9 @@ dependencies = [ { name = "tenacity" }, { name = "uipath-platform" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/14/f11944802382755b5148cdcb5ca1439f3610f5b81553970552ad1a4dac32/uipath_llm_client-1.21.0.tar.gz", hash = "sha256:1d5ce9916e76161afef623e3c5b210818ce7dd5278b7570573827d6e39b4f6ac", size = 12552387, upload-time = "2026-10-06T08:35:38.569Z" } +sdist = { url = "https://files.pythonhosted.org/packages/22/6d/b1a9f7f87db99950f3c7a0b81c568d41e655b6b1faad0f181ca70723c32b/uipath_llm_client-1.21.1.tar.gz", hash = "sha256:5d23515b47a58b1e9f8b374d84b228032b94a294ef4c3ad777aff3e0124421c2", size = 12552763, upload-time = "2026-10-07T14:53:25.559Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/b6/e633bbc70905fd96f410c2e9f702f5a5dfce5bb88841d2939bdced00647b/uipath_llm_client-1.21.0-py3-none-any.whl", hash = "sha256:ad01ee4a965797a1f394f7345ed53dc03beea05880532e59605a25ab173308cd", size = 78265, upload-time = "2026-10-06T08:35:36.499Z" }, + { url = "https://files.pythonhosted.org/packages/ec/78/cb9a9c46f5bd7b5a798f4e56495c238da6d1a1d1496f61c9722da401e8be/uipath_llm_client-1.21.1-py3-none-any.whl", hash = "sha256:41f14ac98c631eb7c9733082802e17ee8641bced4617edd1ec37972333be2ecc", size = 78246, upload-time = "2026-10-07T14:53:23.833Z" }, ] [[package]]