Skip to content

Commit 91b8711

Browse files
committed
fixes
1 parent de99137 commit 91b8711

5 files changed

Lines changed: 85 additions & 95 deletions

File tree

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1+
import logging
12
from collections.abc import Generator
23

34
from httpx import Auth, Client, Request, Response
45

56
from uipath.llm_client.settings.llmgateway.settings import LLMGatewayBaseSettings
67
from uipath.llm_client.settings.llmgateway.utils import LLMGatewayEndpoints
78
from uipath.llm_client.settings.utils import SingletonMeta
8-
from uipath.llm_client.utils.exceptions import UiPathAPIError, UiPathAuthenticationError
9+
10+
logger = logging.getLogger(__name__)
911

1012

1113
class LLMGatewayS2SAuth(Auth, metaclass=SingletonMeta):
1214
"""Bearer authentication handler with automatic token refresh.
1315
1416
Singleton class that reuses the same token across all requests to minimize
1517
token generation overhead. Automatically refreshes the token on 401 responses.
18+
19+
Does not raise errors on token retrieval failures — the request is sent
20+
without a valid token and the downstream client handles the error response.
1621
"""
1722

1823
def __init__(
@@ -21,45 +26,50 @@ def __init__(
2126
):
2227
self.settings = settings
2328
if self.settings.access_token is None:
24-
self.access_token = self.get_llmgw_token_header()
29+
self.access_token = self.get_llmgw_token()
2530
else:
2631
self.access_token = self.settings.access_token.get_secret_value()
2732

28-
def get_llmgw_token_header(
33+
def get_llmgw_token(
2934
self,
30-
) -> str:
31-
"""Retrieve a new access token from the LLM Gateway identity endpoint."""
32-
url_get_token = f"{self.settings.base_url}/{LLMGatewayEndpoints.IDENTITY_ENDPOINT.value}"
35+
) -> str | None:
36+
"""Retrieve a new access token from the LLM Gateway identity endpoint.
37+
38+
Returns None on failure instead of raising, so the request proceeds
39+
and the client receives the actual error response from the server.
40+
"""
3341
if self.settings.client_id is None or self.settings.client_secret is None:
34-
raise ValueError("client_id and client_secret are required for S2S authentication")
42+
logger.warning("client_id and client_secret are required for S2S authentication")
43+
return None
44+
url_get_token = f"{self.settings.base_url}/{LLMGatewayEndpoints.IDENTITY_ENDPOINT.value}"
3545
token_credentials = dict(
3646
client_id=self.settings.client_id.get_secret_value(),
3747
client_secret=self.settings.client_secret.get_secret_value(),
3848
grant_type="client_credentials",
3949
)
40-
with Client() as http_client:
41-
response = http_client.post(url_get_token, data=token_credentials)
42-
if response.is_client_error:
43-
try:
44-
body = response.json()
45-
except Exception:
46-
body = response.text
47-
raise UiPathAuthenticationError(
48-
message="Failed to authenticate with LLM Gateway, invalid credentials",
49-
request=response.request,
50-
response=response,
51-
body=body,
52-
)
53-
elif response.is_error:
54-
raise UiPathAPIError.from_response(response)
55-
llmgw_token_header = response.json().get("access_token")
56-
return llmgw_token_header
50+
try:
51+
with Client() as http_client:
52+
response = http_client.post(url_get_token, data=token_credentials)
53+
if response.is_error:
54+
logger.warning(
55+
"Failed to retrieve LLM Gateway token: %s %s",
56+
response.status_code,
57+
response.reason_phrase,
58+
)
59+
return None
60+
return response.json().get("access_token")
61+
except Exception:
62+
logger.warning("Failed to retrieve LLM Gateway token", exc_info=True)
63+
return None
5764

5865
def auth_flow(self, request: Request) -> Generator[Request, Response, None]:
5966
"""HTTPX auth flow that handles token refresh on authentication failures."""
60-
request.headers["Authorization"] = f"Bearer {self.access_token}"
67+
if self.access_token:
68+
request.headers["Authorization"] = f"Bearer {self.access_token}"
6169
response = yield request
6270
if response.status_code == 401:
63-
self.access_token = self.get_llmgw_token_header()
64-
request.headers["Authorization"] = f"Bearer {self.access_token}"
65-
yield request
71+
new_token = self.get_llmgw_token()
72+
if new_token:
73+
self.access_token = new_token
74+
request.headers["Authorization"] = f"Bearer {self.access_token}"
75+
yield request

src/uipath/llm_client/settings/platform/auth.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,22 +37,36 @@ def get_access_token(self, refresh: bool = False) -> str:
3737

3838
assert self.settings.base_url is not None
3939
assert self.settings.client_id is not None
40-
assert self.settings.refresh_token is not None
40+
if self.settings.refresh_token is None:
41+
return access_token
42+
43+
try:
44+
identity_service = IdentityService(self.settings.base_url)
45+
new_token_data = identity_service.refresh_access_token(
46+
refresh_token=self.settings.refresh_token.get_secret_value(),
47+
client_id=self.settings.client_id,
48+
)
49+
except Exception:
50+
return access_token
4151

42-
identity_service = IdentityService(self.settings.base_url)
43-
new_token_data = identity_service.refresh_access_token(
44-
self.settings.refresh_token.get_secret_value(), self.settings.client_id
45-
)
4652
self.settings.access_token = SecretStr(new_token_data.access_token)
4753
self.settings.refresh_token = SecretStr(
4854
new_token_data.refresh_token or self.settings.refresh_token.get_secret_value()
4955
)
5056
return new_token_data.access_token
5157

5258
def auth_flow(self, request: Request) -> Generator[Request, Response, None]:
53-
"""HTTPX auth flow that handles token refresh on authentication failures."""
54-
request.headers["Authorization"] = f"Bearer {self.get_access_token()}"
59+
"""HTTPX auth flow that handles token refresh on authentication failures.
60+
61+
On 401, attempts to refresh the token. If refresh succeeds (new token differs
62+
from the old one), retries the request. Otherwise, returns the 401 response
63+
as-is so the client receives the actual error.
64+
"""
65+
old_token = self.get_access_token()
66+
request.headers["Authorization"] = f"Bearer {old_token}"
5567
response = yield request
5668
if response.status_code == 401:
57-
request.headers["Authorization"] = f"Bearer {self.get_access_token(refresh=True)}"
58-
yield request
69+
new_token = self.get_access_token(refresh=True)
70+
if new_token != old_token:
71+
request.headers["Authorization"] = f"Bearer {new_token}"
72+
yield request

src/uipath/llm_client/settings/platform/settings.py

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,7 @@
88
from uipath.platform.common import EndpointManager
99

1010
from uipath.llm_client.settings.base import UiPathAPIConfig, UiPathBaseSettings
11-
from uipath.llm_client.settings.platform.utils import (
12-
get_auth_data,
13-
is_token_expired,
14-
parse_access_token,
15-
)
11+
from uipath.llm_client.settings.platform.utils import is_token_expired, parse_access_token
1612

1713

1814
class PlatformBaseSettings(UiPathBaseSettings):
@@ -43,9 +39,7 @@ class PlatformBaseSettings(UiPathBaseSettings):
4339

4440
# Credentials used for refreshing the access token
4541
client_id: str | None = Field(default=None)
46-
refresh_token: SecretStr | None = Field(
47-
default=None,
48-
)
42+
refresh_token: SecretStr | None = Field(default=None, validation_alias="UIPATH_REFRESH_TOKEN")
4943

5044
# AgentHub configuration (used for discovery)
5145
agenthub_config: str | None = Field(
@@ -68,16 +62,14 @@ def validate_environment(self) -> Self:
6862
raise ValueError(
6963
"Base URL, access token, tenant ID, and organization ID are required. Try running `uipath auth` to authenticate."
7064
)
71-
auth_data = get_auth_data()
72-
if auth_data.access_token != self.access_token.get_secret_value():
73-
raise ValueError("Access token mismatch between .auth.json and environment variables")
74-
if is_token_expired(auth_data.access_token):
65+
66+
access_token = self.access_token.get_secret_value()
67+
if is_token_expired(access_token):
7568
raise ValueError(
7669
"Access token is expired. Try running `uipath auth` to refresh the token."
7770
)
78-
if auth_data.refresh_token is not None:
79-
self.refresh_token = SecretStr(auth_data.refresh_token)
80-
parsed_token_data = parse_access_token(auth_data.access_token)
71+
72+
parsed_token_data = parse_access_token(access_token)
8173
self.client_id = parsed_token_data.get("client_id", None)
8274
return self
8375

src/uipath/llm_client/settings/platform/utils.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,6 @@
11
import base64
22
import json
33
import time
4-
from pathlib import Path
5-
6-
from uipath.platform.common.auth import TokenData
7-
8-
9-
def get_auth_data() -> TokenData:
10-
auth_file = Path.cwd() / ".uipath" / ".auth.json"
11-
if not auth_file.exists():
12-
raise FileNotFoundError("No authentication file found")
13-
return TokenData.model_validate(json.load(open(auth_file)))
144

155

166
def parse_access_token(access_token: str):

tests/core/test_base_client.py

Lines changed: 17 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
8. Header utilities (extract_matching_headers, context vars)
1212
9. Logging config (LoggingConfig)
1313
10. SSL config (expand_path, get_httpx_ssl_client_kwargs)
14-
11. LLMGateway S2S auth (get_llmgw_token_header)
14+
11. LLMGateway S2S auth (get_llmgw_token)
1515
12. LLMGateway BYOM validation (validate_byo_model)
1616
13. Wait strategy (wait_retry_after_with_fallback)
1717
14. HTTPX client send() behavior (streaming header, URL freezing, header capture)
@@ -105,24 +105,16 @@ def platform_env_vars():
105105

106106
@pytest.fixture
107107
def mock_platform_auth():
108-
"""Context manager that patches get_auth_data and parse_access_token for PlatformSettings tests."""
109-
mock_auth_data = MagicMock()
110-
mock_auth_data.access_token = "test-access-token"
111-
mock_auth_data.refresh_token = None
112-
108+
"""Patches is_token_expired and parse_access_token for PlatformSettings tests."""
113109
with (
114110
patch(
115-
"uipath.llm_client.settings.platform.settings.get_auth_data",
116-
return_value=mock_auth_data,
111+
"uipath.llm_client.settings.platform.settings.is_token_expired",
112+
return_value=False,
117113
),
118114
patch(
119115
"uipath.llm_client.settings.platform.settings.parse_access_token",
120116
return_value={"client_id": "test-client-id"},
121117
),
122-
patch(
123-
"uipath.llm_client.settings.platform.settings.is_token_expired",
124-
return_value=False,
125-
),
126118
):
127119
yield
128120

@@ -439,7 +431,7 @@ def test_auth_flow_refreshes_on_401(self, llmgw_s2s_env_vars):
439431

440432
# Mock the token retrieval
441433
with patch.object(
442-
LLMGatewayS2SAuth, "get_llmgw_token_header", return_value="new-token"
434+
LLMGatewayS2SAuth, "get_llmgw_token", return_value="new-token"
443435
) as mock_get_token:
444436
auth = LLMGatewayS2SAuth(settings=settings)
445437
# First call is during __init__
@@ -1702,7 +1694,7 @@ def test_response_patched_with_raise_for_status(self):
17021694

17031695

17041696
class TestLLMGatewayS2STokenAcquisition:
1705-
"""Tests for LLMGatewayS2SAuth.get_llmgw_token_header."""
1697+
"""Tests for LLMGatewayS2SAuth.get_llmgw_token."""
17061698

17071699
def test_s2s_token_success(self, llmgw_s2s_env_vars):
17081700
from uipath.llm_client.settings.llmgateway.auth import LLMGatewayS2SAuth
@@ -1718,53 +1710,45 @@ def test_s2s_token_success(self, llmgw_s2s_env_vars):
17181710
auth = LLMGatewayS2SAuth(settings=settings)
17191711
assert auth.access_token == "s2s-token-value"
17201712

1721-
def test_s2s_token_client_error_raises_auth_error(self, llmgw_s2s_env_vars):
1713+
def test_s2s_token_client_error_returns_none(self, llmgw_s2s_env_vars):
17221714
from uipath.llm_client.settings.llmgateway.auth import LLMGatewayS2SAuth
17231715

17241716
mock_response = MagicMock()
1725-
mock_response.is_client_error = True
1726-
mock_response.json.return_value = {"error": "invalid_client"}
1727-
mock_response.request = MagicMock(spec=Request)
1717+
mock_response.is_error = True
17281718
mock_response.status_code = 401
17291719
mock_response.reason_phrase = "Unauthorized"
1730-
mock_response.headers = {}
17311720

17321721
with patch.dict(os.environ, llmgw_s2s_env_vars, clear=True):
17331722
settings = LLMGatewaySettings()
17341723
with patch.object(Client, "post", return_value=mock_response):
1735-
with pytest.raises(UiPathAuthenticationError, match="invalid credentials"):
1736-
LLMGatewayS2SAuth(settings=settings)
1724+
auth = LLMGatewayS2SAuth(settings=settings)
1725+
assert auth.access_token is None
17371726

1738-
def test_s2s_token_server_error_raises_api_error(self, llmgw_s2s_env_vars):
1727+
def test_s2s_token_server_error_returns_none(self, llmgw_s2s_env_vars):
17391728
from uipath.llm_client.settings.llmgateway.auth import LLMGatewayS2SAuth
17401729

17411730
mock_response = MagicMock()
1742-
mock_response.is_client_error = False
17431731
mock_response.is_error = True
17441732
mock_response.status_code = 500
17451733
mock_response.reason_phrase = "Server Error"
1746-
mock_response.json.return_value = {"error": "internal"}
1747-
mock_response.request = MagicMock(spec=Request)
1748-
mock_response.headers = {}
17491734

17501735
with patch.dict(os.environ, llmgw_s2s_env_vars, clear=True):
17511736
settings = LLMGatewaySettings()
17521737
with patch.object(Client, "post", return_value=mock_response):
1753-
with pytest.raises(UiPathAPIError):
1754-
LLMGatewayS2SAuth(settings=settings)
1738+
auth = LLMGatewayS2SAuth(settings=settings)
1739+
assert auth.access_token is None
17551740

1756-
def test_s2s_missing_credentials_raises_value_error(self, llmgw_env_vars):
1741+
def test_s2s_missing_credentials_returns_none(self, llmgw_env_vars):
17571742
from uipath.llm_client.settings.llmgateway.auth import LLMGatewayS2SAuth
17581743

17591744
with patch.dict(os.environ, llmgw_env_vars, clear=True):
17601745
settings = LLMGatewaySettings()
17611746
# Clear access_token to force S2S flow, but credentials are missing
17621747
settings.access_token = None
17631748
settings.client_id = None
1764-
with pytest.raises(ValueError, match="client_id and client_secret are required"):
1765-
auth = LLMGatewayS2SAuth.__new__(LLMGatewayS2SAuth)
1766-
auth.settings = settings
1767-
auth.get_llmgw_token_header()
1749+
auth = LLMGatewayS2SAuth.__new__(LLMGatewayS2SAuth)
1750+
auth.settings = settings
1751+
assert auth.get_llmgw_token() is None
17681752

17691753

17701754
# ============================================================================

0 commit comments

Comments
 (0)