Skip to content

Commit f29d87d

Browse files
committed
fixes
1 parent 7249d59 commit f29d87d

9 files changed

Lines changed: 48 additions & 26 deletions

File tree

packages/uipath_langchain_client/src/uipath_langchain_client/base_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ class UiPathBaseLLMClient(BaseModel, ABC):
102102
)
103103

104104
default_headers: Mapping[str, str] | None = Field(
105-
default={
105+
default_factory=lambda: {
106106
"X-UiPath-LLMGateway-TimeoutSeconds": "295", # server side timeout, default is 10, maximum is 300
107107
"X-UiPath-LLMGateway-AllowFull4xxResponse": "true", # allow full 4xx responses (default is false)
108108
},
@@ -210,7 +210,7 @@ def uipath_request(
210210
async def uipath_arequest(
211211
self,
212212
method: Literal["POST", "GET"] = "POST",
213-
url: str = "/",
213+
url: URL | str = "/",
214214
*,
215215
request_body: dict[str, Any] | None = None,
216216
**kwargs: Any,

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ def _llm_type(self) -> str:
140140

141141
@property
142142
def _default_params(self) -> dict[str, Any]:
143-
"""Get the default parameters for calling OpenAI API."""
143+
"""Get the default parameters for the normalized API request."""
144144
exclude_if_none = {
145145
"frequency_penalty": self.frequency_penalty,
146146
"presence_penalty": self.presence_penalty,
@@ -175,7 +175,7 @@ def _get_usage_metadata(self, json_data: dict[str, Any]) -> UsageMetadata:
175175
cache_creation=json_data.get("cache_creation_input_tokens", 0),
176176
),
177177
output_token_details=OutputTokenDetails(
178-
audio=json_data.get("audio_tokens", 0),
178+
audio=json_data.get("output_audio_tokens", 0),
179179
reasoning=json_data.get("thoughts_tokens", 0),
180180
),
181181
)
@@ -241,7 +241,11 @@ def _preprocess_request(
241241
{
242242
"id": tool_call["id"],
243243
"name": tool_call["function"]["name"],
244-
"arguments": json.loads(tool_call["function"]["arguments"]),
244+
"arguments": (
245+
tool_call["function"]["arguments"]
246+
if isinstance(tool_call["function"]["arguments"], dict)
247+
else (json.loads(tool_call["function"]["arguments"]) if tool_call["function"]["arguments"] else {})
248+
),
245249
}
246250
for tool_call in converted_message["tool_calls"]
247251
]
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
__titile__ = "UiPath LLM Client"
1+
__title__ = "UiPath LLM Client"
22
__description__ = "A Python client for interacting with UiPath's LLM services."
33
__version__ = "1.3.0"

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ def get_llmgw_token_header(
3030
) -> str:
3131
"""Retrieve a new access token from the LLM Gateway identity endpoint."""
3232
url_get_token = f"{self.settings.base_url}/{LLMGatewayEndpoints.IDENTITY_ENDPOINT.value}"
33-
assert self.settings.client_id is not None
34-
assert self.settings.client_secret is not None
33+
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")
3535
token_credentials = dict(
3636
client_id=self.settings.client_id.get_secret_value(),
3737
client_secret=self.settings.client_secret.get_secret_value(),
@@ -40,11 +40,15 @@ def get_llmgw_token_header(
4040
with Client() as http_client:
4141
response = http_client.post(url_get_token, data=token_credentials)
4242
if response.is_client_error:
43+
try:
44+
body = response.json()
45+
except Exception:
46+
body = response.text
4347
raise UiPathAuthenticationError(
4448
message="Failed to authenticate with LLM Gateway, invalid credentials",
4549
request=response.request,
4650
response=response,
47-
body=response.json(),
51+
body=body,
4852
)
4953
elif response.is_error:
5054
raise UiPathAPIError.from_response(response)

src/uipath/llm_client/settings/llmgateway/settings.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,11 @@ def build_base_url(
7575
) -> str:
7676
base_url = f"{self.base_url}/{self.org_id}/{self.tenant_id}"
7777
if api_config is not None and api_config.client_type == "normalized":
78-
url = f"{base_url}/{LLMGatewayEndpoints.NORMALIZED_ENDPOOINT.value.format(api_type='chat/completions' if api_config.api_type == 'completions' else 'embeddings')}"
78+
url = f"{base_url}/{LLMGatewayEndpoints.NORMALIZED_ENDPOINT.value.format(api_type='chat/completions' if api_config.api_type == 'completions' else 'embeddings')}"
7979
else:
80-
url = f"{base_url}/{LLMGatewayEndpoints.PASSTHROUGH_ENDPOOINT.value.format(vendor=api_config.vendor_type if api_config is not None else None, model=model_name, api_type=api_config.api_type if api_config is not None else None)}"
80+
if api_config is None:
81+
raise ValueError("api_config is required for passthrough client_type")
82+
url = f"{base_url}/{LLMGatewayEndpoints.PASSTHROUGH_ENDPOINT.value.format(vendor=api_config.vendor_type, model=model_name, api_type=api_config.api_type)}"
8183
return url
8284

8385
@override
@@ -115,16 +117,16 @@ def get_available_models(self) -> list[dict[str, Any]]:
115117
@override
116118
def validate_byo_model(self, model_info: dict[str, Any]) -> None:
117119
byom_details = model_info.get("byomDetails", {})
118-
operaion_codes = byom_details.get("operationCodes", [])
119-
if self.operation_code and self.operation_code not in operaion_codes:
120+
operation_codes = byom_details.get("operationCodes", [])
121+
if self.operation_code and self.operation_code not in operation_codes:
120122
raise ValueError(
121123
f"The operation code {self.operation_code} is not allowed for the model {model_info['modelName']}"
122124
)
123-
if not self.operation_code and len(operaion_codes) > 0:
124-
if len(operaion_codes) > 1:
125+
if not self.operation_code and len(operation_codes) > 0:
126+
if len(operation_codes) > 1:
125127
logging.warning(
126128
"Multiple operation codes are allowed for the model %s, but no operation code was provided, picking the first one available: %s",
127129
model_info["modelName"],
128-
operaion_codes[0],
130+
operation_codes[0],
129131
)
130-
self.operation_code = operaion_codes[0]
132+
self.operation_code = operation_codes[0]

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ class LLMGatewayEndpoints(StrEnum):
1010

1111
IDENTITY_ENDPOINT = "identity_/connect/token"
1212
DISCOVERY_ENDPOINT = "llmgateway_/api/discovery"
13-
NORMALIZED_ENDPOOINT = "llmgateway_/api/{api_type}"
14-
PASSTHROUGH_ENDPOOINT = "llmgateway_/api/raw/vendor/{vendor}/model/{model}/{api_type}"
13+
NORMALIZED_ENDPOINT = "llmgateway_/api/{api_type}"
14+
PASSTHROUGH_ENDPOINT = "llmgateway_/api/raw/vendor/{vendor}/model/{model}/{api_type}"
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1+
import threading
12
from typing import Any
23

34

45
class SingletonMeta(type):
56
"""Metaclass for creating singleton classes. Used to keep global configs shared between instances."""
67

78
_instances: dict[type, Any] = {}
9+
_lock: threading.Lock = threading.Lock()
810

911
def __call__(cls, *args: Any, **kwargs: Any) -> Any:
1012
if cls not in SingletonMeta._instances:
11-
instance = super().__call__(*args, **kwargs)
12-
SingletonMeta._instances[cls] = instance
13+
with SingletonMeta._lock:
14+
if cls not in SingletonMeta._instances:
15+
instance = super().__call__(*args, **kwargs)
16+
SingletonMeta._instances[cls] = instance
1317
return SingletonMeta._instances[cls]

src/uipath/llm_client/utils/exceptions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def from_response(cls, response: Response, request: Request | None = None) -> "U
7070
7171
Args:
7272
response: The httpx Response object.
73+
request: The original httpx Request object. Falls back to response.request if None.
7374
7475
Returns:
7576
A UiPathAPIError instance (or subclass) matching the response status code.

src/uipath/llm_client/utils/retry.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,17 @@
4747
from tenacity.wait import wait_base
4848
from typing_extensions import TypedDict
4949

50-
from uipath.llm_client.utils.exceptions import UiPathAPIError, UiPathRateLimitError
50+
from uipath.llm_client.utils.exceptions import (
51+
UiPathAPIError,
52+
UiPathRateLimitError,
53+
UiPathTooManyRequestsError,
54+
)
5155

5256
# Default retry configuration values
53-
_DEFAULT_RETRY_ON_EXCEPTIONS: tuple[type[Exception], ...] = (UiPathRateLimitError,)
57+
_DEFAULT_RETRY_ON_EXCEPTIONS: tuple[type[Exception], ...] = (
58+
UiPathRateLimitError,
59+
UiPathTooManyRequestsError,
60+
)
5461
_DEFAULT_INITIAL_DELAY: float = 2.0
5562
_DEFAULT_MAX_DELAY: float = 60.0
5663
_DEFAULT_EXP_BASE: float = 2.0
@@ -157,7 +164,7 @@ def _build_retryer(
157164
"""Build a tenacity retryer from configuration.
158165
159166
Args:
160-
max_retries: Maximum number of retry attempts. Returns None if <= 1.
167+
max_retries: Maximum number of retry attempts. Returns None if < 1 (i.e., 0 or negative).
161168
retry_config: Configuration for retry behavior. Uses defaults if not provided.
162169
logger: Logger for retry attempt warnings.
163170
async_mode: If True, returns AsyncRetrying; otherwise returns Retrying.
@@ -217,7 +224,7 @@ def __init__(
217224
"""Initialize the retryable transport.
218225
219226
Args:
220-
max_retries: Maximum number of retry attempts. Set to 1 to disable retries.
227+
max_retries: Maximum number of retry attempts. Set to 0 (default) to disable retries.
221228
retry_config: Configuration for retry behavior. Uses defaults if not provided.
222229
logger: Logger for retry attempt warnings.
223230
*args: Positional arguments passed to HTTPTransport.
@@ -281,7 +288,7 @@ def __init__(
281288
"""Initialize the retryable async transport.
282289
283290
Args:
284-
max_retries: Maximum number of retry attempts. Set to 1 to disable retries.
291+
max_retries: Maximum number of retry attempts. Set to 0 (default) to disable retries.
285292
retry_config: Configuration for retry behavior. Uses defaults if not provided.
286293
logger: Logger for retry attempt warnings.
287294
*args: Positional arguments passed to AsyncHTTPTransport.

0 commit comments

Comments
 (0)