Skip to content

Commit 2126bba

Browse files
feat: add uipath headers llm calls (#663)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent dd572e4 commit 2126bba

17 files changed

Lines changed: 497 additions & 137 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-langchain"
3-
version = "0.8.5"
3+
version = "0.8.6"
44
description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

src/uipath_langchain/_utils/_request_mixin.py

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import os
55
import time
66
from typing import Any, AsyncIterator, Dict, Iterator, Mapping
7-
from urllib.parse import quote
87

98
import httpx
109
import openai
@@ -34,6 +33,7 @@
3433
get_uipath_token_header,
3534
)
3635
from uipath_langchain._utils._sleep_policy import before_sleep_log
36+
from uipath_langchain.chat.http_client import build_uipath_headers, resolve_gateway_url
3737
from uipath_langchain.runtime.errors import (
3838
LangGraphErrorCode,
3939
LangGraphRuntimeError,
@@ -79,8 +79,6 @@ class UiPathRequestMixin(BaseModel):
7979

8080
default_headers: Mapping[str, str] | None = {
8181
"X-UiPath-Streaming-Enabled": "false",
82-
"X-UiPath-JobKey": os.getenv("UIPATH_JOB_KEY", ""),
83-
"X-UiPath-ProcessKey": quote(os.getenv("UIPATH_PROCESS_KEY", ""), safe=""),
8482
}
8583
model_name: str | None = Field(
8684
default_factory=lambda: os.getenv(
@@ -155,6 +153,7 @@ class UiPathRequestMixin(BaseModel):
155153
max_delay: float = 60.0
156154

157155
_url: str | None = None
156+
_is_override: bool = False
158157
_auth_headers: dict[str, str] | None = None
159158

160159
# required to instantiate AzureChatOpenAI subclasses
@@ -732,17 +731,26 @@ def _prepare_url(self, url: str) -> httpx.URL:
732731
def _build_headers(self, options, retries_taken: int = 0) -> httpx.Headers:
733732
return httpx.Headers(self.auth_headers)
734733

734+
def _resolve_url_and_override(self) -> None:
735+
"""Resolve ``_url`` and ``_is_override`` idempotently."""
736+
if self._url:
737+
return
738+
try:
739+
self._url, self._is_override = resolve_gateway_url(self.endpoint)
740+
except ValueError:
741+
self._url = (
742+
f"{self.base_url}/{self.org_id}/{self.tenant_id}/{self.endpoint}"
743+
)
744+
except NotImplementedError:
745+
pass
746+
735747
@property
736748
def url(self) -> str:
749+
self._resolve_url_and_override()
737750
if not self._url:
738-
env_uipath_url = os.getenv("UIPATH_URL")
739-
740-
if env_uipath_url:
741-
self._url = f"{env_uipath_url.rstrip('/')}/{self.endpoint}"
742-
else:
743-
self._url = (
744-
f"{self.base_url}/{self.org_id}/{self.tenant_id}/{self.endpoint}"
745-
)
751+
raise NotImplementedError(
752+
"The endpoint property is not implemented for this class."
753+
)
746754
return self._url
747755

748756
@property
@@ -754,17 +762,21 @@ def endpoint(self) -> str:
754762
@property
755763
def auth_headers(self) -> dict[str, str]:
756764
if not self._auth_headers:
765+
self._resolve_url_and_override()
757766
self._auth_headers = {
758767
**self.default_headers, # type: ignore
759-
"Authorization": f"Bearer {self.access_token}",
760-
"X-UiPath-LlmGateway-TimeoutSeconds": str(self.default_request_timeout),
761768
}
762-
if self.agenthub_config:
763-
self._auth_headers["X-UiPath-AgentHub-Config"] = self.agenthub_config
764-
if self.byo_connection_id:
765-
self._auth_headers["X-UiPath-LlmGateway-ByoIsConnectionId"] = (
766-
self.byo_connection_id
769+
self._auth_headers["Authorization"] = f"Bearer {self.access_token}"
770+
self._auth_headers.update(
771+
build_uipath_headers(
772+
agenthub_config=self.agenthub_config,
773+
byo_connection_id=self.byo_connection_id,
774+
inject_routing=self._is_override,
767775
)
776+
)
777+
self._auth_headers["X-UiPath-LlmGateway-TimeoutSeconds"] = str(
778+
self.default_request_timeout
779+
)
768780
if self.is_normalized and self.model_name:
769781
self._auth_headers["X-UiPath-LlmGateway-NormalizedApi-ModelName"] = (
770782
self.model_name

src/uipath_langchain/chat/bedrock.py

Lines changed: 21 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22
import os
33
from collections.abc import Iterator
44
from typing import Any, Optional
5-
from urllib.parse import quote
65

76
from langchain_core.callbacks import CallbackManagerForLLMRun
87
from langchain_core.messages import BaseMessage
98
from langchain_core.outputs import ChatGenerationChunk, ChatResult
109
from tenacity import AsyncRetrying, Retrying
1110
from uipath.platform.common import EndpointManager, resource_override
1211

13-
from .header_capture import HeaderCapture
14-
from .retryers.bedrock import AsyncBedrockRetryer, BedrockRetryer
12+
from .http_client import build_uipath_headers, resolve_gateway_url
13+
from .http_client.header_capture import HeaderCapture
14+
from .http_client.retryers.bedrock import AsyncBedrockRetryer, BedrockRetryer
1515
from .supported_models import BedrockModels
1616
from .types import APIFlavor, LLMProvider
1717

@@ -72,6 +72,7 @@ def __init__(
7272
self.byo_connection_id = byo_connection_id
7373
self._vendor = "awsbedrock"
7474
self._url: Optional[str] = None
75+
self._is_override: bool = False
7576
self.header_capture = header_capture
7677

7778
@property
@@ -83,16 +84,10 @@ def endpoint(self) -> str:
8384
)
8485
return formatted_endpoint
8586

86-
def _build_base_url(self) -> str:
87+
def _resolve_url(self) -> tuple[str, bool]:
8788
if not self._url:
88-
env_uipath_url = os.getenv("UIPATH_URL")
89-
90-
if env_uipath_url:
91-
self._url = f"{env_uipath_url.rstrip('/')}/{self.endpoint}"
92-
else:
93-
raise ValueError("UIPATH_URL environment variable is required")
94-
95-
return self._url
89+
self._url, self._is_override = resolve_gateway_url(self.endpoint)
90+
return self._url, self._is_override
9691

9792
def _capture_response_headers(self, parsed, model, **kwargs):
9893
if "ResponseMetadata" in parsed:
@@ -122,29 +117,24 @@ def get_client(self):
122117
return client
123118

124119
def _modify_request(self, request, **kwargs):
125-
"""Intercept boto3 request and redirect to LLM Gateway"""
120+
"""Intercept boto3 request and redirect to LLM Gateway."""
126121
# Detect streaming based on URL suffix:
127122
# - converse-stream / invoke-with-response-stream -> streaming
128123
# - converse / invoke -> non-streaming
129124
streaming = "true" if request.url.endswith("-stream") else "false"
130-
request.url = self._build_base_url()
131-
132-
headers = {
133-
"Authorization": f"Bearer {self.token}",
134-
"X-UiPath-LlmGateway-ApiFlavor": self.api_flavor,
135-
"X-UiPath-Streaming-Enabled": streaming,
136-
}
137-
138-
if self.agenthub_config:
139-
headers["X-UiPath-AgentHub-Config"] = self.agenthub_config
140-
if self.byo_connection_id:
141-
headers["X-UiPath-LlmGateway-ByoIsConnectionId"] = self.byo_connection_id
142-
job_key = os.getenv("UIPATH_JOB_KEY")
143-
process_key = os.getenv("UIPATH_PROCESS_KEY")
144-
if job_key:
145-
headers["X-UiPath-JobKey"] = job_key
146-
if process_key:
147-
headers["X-UiPath-ProcessKey"] = quote(process_key, safe="")
125+
url, is_override = self._resolve_url()
126+
request.url = url
127+
128+
headers: dict[str, str] = {"Authorization": f"Bearer {self.token}"}
129+
headers.update(
130+
build_uipath_headers(
131+
agenthub_config=self.agenthub_config,
132+
byo_connection_id=self.byo_connection_id,
133+
inject_routing=is_override,
134+
)
135+
)
136+
headers["X-UiPath-LlmGateway-ApiFlavor"] = self.api_flavor
137+
headers["X-UiPath-Streaming-Enabled"] = streaming
148138

149139
request.headers.update(headers)
150140

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .headers import build_uipath_headers
2+
from .url import resolve_gateway_url
3+
4+
__all__ = ["build_uipath_headers", "resolve_gateway_url"]
File renamed without changes.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""Shared UiPath HTTP headers for LLM Gateway requests."""
2+
3+
import os
4+
from urllib.parse import quote
5+
6+
from uipath.platform.common.constants import (
7+
ENV_FOLDER_KEY,
8+
ENV_JOB_KEY,
9+
ENV_ORGANIZATION_ID,
10+
ENV_PROCESS_KEY,
11+
ENV_TENANT_ID,
12+
ENV_UIPATH_TRACE_ID,
13+
HEADER_AGENTHUB_CONFIG,
14+
HEADER_FOLDER_KEY,
15+
HEADER_INTERNAL_ACCOUNT_ID,
16+
HEADER_INTERNAL_TENANT_ID,
17+
HEADER_JOB_KEY,
18+
HEADER_LLMGATEWAY_BYO_CONNECTION_ID,
19+
HEADER_PROCESS_KEY,
20+
HEADER_TRACE_ID,
21+
)
22+
23+
24+
def build_uipath_headers(
25+
*,
26+
agenthub_config: str | None = None,
27+
byo_connection_id: str | None = None,
28+
inject_routing: bool = False,
29+
) -> dict[str, str]:
30+
"""Build common UiPath headers for LLM Gateway requests.
31+
32+
Reads process_key, job_key, folder_key, and trace_id directly from
33+
environment variables when set.
34+
35+
Args:
36+
agenthub_config: Optional AgentHub configuration identifier.
37+
byo_connection_id: Optional BYO connection identifier.
38+
inject_routing: When True, adds tenant and account routing
39+
headers that are normally injected by the platform routing
40+
layer. Set this when using a service URL override that
41+
bypasses the platform.
42+
"""
43+
headers: dict[str, str] = {}
44+
if agenthub_config:
45+
headers[HEADER_AGENTHUB_CONFIG] = agenthub_config
46+
if byo_connection_id:
47+
headers[HEADER_LLMGATEWAY_BYO_CONNECTION_ID] = byo_connection_id
48+
if process_key := os.getenv(ENV_PROCESS_KEY):
49+
headers[HEADER_PROCESS_KEY] = quote(process_key, safe="")
50+
if job_key := os.getenv(ENV_JOB_KEY):
51+
headers[HEADER_JOB_KEY] = job_key
52+
if folder_key := os.getenv(ENV_FOLDER_KEY):
53+
headers[HEADER_FOLDER_KEY] = folder_key
54+
if trace_id := os.getenv(ENV_UIPATH_TRACE_ID):
55+
headers[HEADER_TRACE_ID] = trace_id
56+
if inject_routing:
57+
if tenant_id := os.getenv(ENV_TENANT_ID):
58+
headers[HEADER_INTERNAL_TENANT_ID] = tenant_id
59+
if organization_id := os.getenv(ENV_ORGANIZATION_ID):
60+
headers[HEADER_INTERNAL_ACCOUNT_ID] = organization_id
61+
return headers
File renamed without changes.
File renamed without changes.
File renamed without changes.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Gateway URL resolution with service override support.
2+
3+
Integrates chat models into the platform's per-service URL override system
4+
(UIPATH_SERVICE_URL_<SERVICE> env vars) so that local development redirects
5+
work consistently across all UiPath HTTP clients.
6+
"""
7+
8+
import os
9+
from collections.abc import Callable
10+
11+
import uipath.platform.common as _platform_common
12+
13+
_resolve_service_url: Callable[[str], str | None] | None = getattr(
14+
_platform_common, "resolve_service_url", None
15+
)
16+
17+
18+
def resolve_gateway_url(endpoint_path: str) -> tuple[str, bool]:
19+
"""Resolve the full gateway URL for a given endpoint path.
20+
21+
Checks for a per-service URL override first (e.g.
22+
``UIPATH_SERVICE_URL_AGENTHUB``). Falls back to building the URL
23+
from ``UIPATH_URL``.
24+
25+
Args:
26+
endpoint_path: Endpoint path with service prefix, e.g.
27+
``"agenthub_/llm/raw/vendor/openai/model/gpt-4/completions"``.
28+
29+
Returns:
30+
Tuple of (resolved_url, is_override). When *is_override* is True
31+
the caller should inject routing headers since the platform
32+
routing layer is bypassed.
33+
34+
Raises:
35+
ValueError: If neither a service override nor UIPATH_URL is set.
36+
"""
37+
if _resolve_service_url is not None:
38+
override_url = _resolve_service_url(endpoint_path)
39+
if override_url:
40+
return override_url, True
41+
42+
env_uipath_url = os.getenv("UIPATH_URL")
43+
if not env_uipath_url:
44+
raise ValueError("UIPATH_URL environment variable is required")
45+
46+
return f"{env_uipath_url.rstrip('/')}/{endpoint_path}", False

0 commit comments

Comments
 (0)