Skip to content

Commit 110e21e

Browse files
Chibionosclaude
andauthored
fix(platform): drop temperature for models that reject it (#1834)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent dddfe7e commit 110e21e

7 files changed

Lines changed: 380 additions & 7 deletions

File tree

packages/uipath-platform/src/uipath/platform/chat/_llm_gateway_service.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from ..common._endpoints_manager import EndpointManager
3030
from ..common._execution_context import UiPathExecutionContext
3131
from ..common._models import Endpoint
32+
from ._model_capabilities import should_skip_temperature
3233
from .llm_gateway import (
3334
ChatCompletion,
3435
SpecificToolChoice,
@@ -175,6 +176,7 @@ def __init__(
175176
action_id: Optional[str] = None,
176177
) -> None:
177178
super().__init__(config=config, execution_context=execution_context)
179+
self._agenthub_config = agenthub_config
178180
self._llm_headers = _build_llm_headers(
179181
requesting_product, requesting_feature, agenthub_config, action_id
180182
)
@@ -329,13 +331,18 @@ class Country(BaseModel):
329331

330332
is_reasoning_model = model.lower().startswith(("o1", "o3", "o4"))
331333

334+
# Reasoning models (o1, o3, o4) don't support temperature; newer models
335+
# reject it too, which only discovery knows about.
336+
skip_temperature = is_reasoning_model or await should_skip_temperature(
337+
self, model, self._agenthub_config
338+
)
339+
332340
request_body: dict[str, Any] = {
333341
"messages": messages,
334342
"max_tokens": max_tokens,
335343
}
336344

337-
# Reasoning models (o1, o3, o4) don't support temperature
338-
if not is_reasoning_model:
345+
if not skip_temperature:
339346
request_body["temperature"] = temperature
340347

341348
# Handle response_format - convert BaseModel to schema if needed
@@ -392,6 +399,7 @@ def __init__(
392399
action_id: Optional[str] = None,
393400
) -> None:
394401
super().__init__(config=config, execution_context=execution_context)
402+
self._agenthub_config = agenthub_config
395403
self._llm_headers = _build_llm_headers(
396404
requesting_product, requesting_feature, agenthub_config, action_id
397405
)
@@ -558,13 +566,18 @@ class Country(BaseModel):
558566
is_claude_model = "claude" in model_lower
559567
is_reasoning_model = model_lower.startswith(("o1", "o3", "o4"))
560568

569+
# Reasoning models (o1, o3, o4) don't support temperature; newer models
570+
# reject it too, which only discovery knows about.
571+
skip_temperature = is_reasoning_model or await should_skip_temperature(
572+
self, model, self._agenthub_config
573+
)
574+
561575
request_body: dict[str, Any] = {
562576
"messages": converted_messages,
563577
"max_tokens": max_tokens,
564578
}
565579

566-
# Reasoning models (o1, o3, o4) don't support temperature and sampling parameters
567-
if not is_reasoning_model:
580+
if not skip_temperature:
568581
request_body["temperature"] = temperature
569582

570583
# Only add OpenAI-specific parameters for non-Claude and non-reasoning models
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""Per-model parameter support, resolved from the LLM Gateway discovery API.
2+
3+
Newer models reject parameters that older ones accept: passing `temperature` to
4+
a model that dropped it fails the whole request with HTTP 400. The gateway
5+
publishes these constraints per model under `modelDetails`, so callers can strip
6+
the parameter instead of guessing from the model name.
7+
"""
8+
9+
import logging
10+
from typing import Any
11+
12+
from ..common._base_service import BaseService
13+
from ..common._endpoints_manager import EndpointManager
14+
from ..common._models import Endpoint
15+
from ..constants import HEADER_AGENTHUB_CONFIG
16+
17+
logger = logging.getLogger(__name__)
18+
19+
CacheKey = tuple[str, str | None]
20+
21+
# One fetch per (base_url, agenthub_config); an empty mapping caches "couldn't read
22+
# discovery". Deliberately unlocked: a racing duplicate GET is cheaper than an
23+
# asyncio.Lock pinned to whichever event loop touched it first.
24+
_model_details: dict[CacheKey, dict[str, dict[str, Any]]] = {}
25+
26+
27+
async def should_skip_temperature(
28+
service: BaseService, model: str, agenthub_config: str | None
29+
) -> bool:
30+
"""Whether `model` rejects the `temperature` parameter.
31+
32+
Args:
33+
service: Service used to call discovery; supplies base URL and auth.
34+
model: Model name as sent to the gateway.
35+
agenthub_config: AgentHub config scoping which models are visible.
36+
37+
Returns:
38+
True only when discovery explicitly reports the model skips temperature.
39+
Unknown models, unreachable discovery, and any unexpected failure return
40+
False, leaving the caller's request untouched.
41+
"""
42+
try:
43+
details = await _details_for(service, model, agenthub_config)
44+
except Exception as e:
45+
logger.warning(
46+
"Could not resolve parameter support for model %s (%s); "
47+
"sending model parameters unchanged",
48+
model,
49+
e,
50+
)
51+
return False
52+
53+
return bool(details.get("shouldSkipTemperature", False))
54+
55+
56+
async def _details_for(
57+
service: BaseService, model: str, agenthub_config: str | None
58+
) -> dict[str, Any]:
59+
key = (service._config.base_url, agenthub_config)
60+
61+
if key not in _model_details:
62+
_model_details[key] = await _fetch_model_details(service, agenthub_config)
63+
64+
return _model_details[key].get(model, {})
65+
66+
67+
async def _fetch_model_details(
68+
service: BaseService, agenthub_config: str | None
69+
) -> dict[str, dict[str, Any]]:
70+
headers = {HEADER_AGENTHUB_CONFIG: agenthub_config} if agenthub_config else {}
71+
endpoint = Endpoint("/" + EndpointManager.get_discovery_endpoint())
72+
73+
try:
74+
response = await service.request_async("GET", endpoint, headers=headers)
75+
models = response.json()
76+
except Exception as e:
77+
# Falling back to the caller's parameters is the pre-existing behaviour;
78+
# failing the LLM call because discovery is down would be worse.
79+
logger.warning(
80+
"LLM Gateway discovery unavailable (%s); sending model parameters unchanged",
81+
e,
82+
)
83+
return {}
84+
85+
if not isinstance(models, list):
86+
logger.warning("Unexpected LLM Gateway discovery payload; expected a list")
87+
return {}
88+
89+
return {
90+
model["modelName"]: model.get("modelDetails") or {}
91+
for model in models
92+
if isinstance(model, dict) and model.get("modelName")
93+
}
94+
95+
96+
def _reset_cache() -> None:
97+
"""Clear the discovery cache. For tests."""
98+
_model_details.clear()

packages/uipath-platform/src/uipath/platform/common/_endpoints_manager.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class UiPathEndpoints(Enum):
2121
"agenthub_/llm/raw/vendor/{vendor}/model/{model}/completions"
2222
)
2323
AH_CAPABILITIES_ENDPOINT = "agenthub_/llm/api/capabilities"
24+
AH_DISCOVERY_ENDPOINT = "agenthub_/llm/api/discovery"
2425

2526
OR_NORMALIZED_COMPLETION_ENDPOINT = "orchestrator_/llm/api/chat/completions"
2627
OR_PASSTHROUGH_COMPLETION_ENDPOINT = "orchestrator_/llm/openai/deployments/{model}/chat/completions?api-version={api_version}"
@@ -29,6 +30,7 @@ class UiPathEndpoints(Enum):
2930
"orchestrator_/llm/raw/vendor/{vendor}/model/{model}/completions"
3031
)
3132
OR_CAPABILITIES_ENDPOINT = "orchestrator_/llm/api/capabilities"
33+
OR_DISCOVERY_ENDPOINT = "orchestrator_/llm/api/discovery"
3234

3335

3436
class EndpointManager:
@@ -185,6 +187,14 @@ def get_normalized_endpoint(cls) -> str:
185187
UiPathEndpoints.OR_NORMALIZED_COMPLETION_ENDPOINT,
186188
)
187189

190+
@classmethod
191+
def get_discovery_endpoint(cls) -> str:
192+
"""Get the model discovery endpoint."""
193+
return cls._select_endpoint(
194+
UiPathEndpoints.AH_DISCOVERY_ENDPOINT,
195+
UiPathEndpoints.OR_DISCOVERY_ENDPOINT,
196+
)
197+
188198
@classmethod
189199
def get_embeddings_endpoint(cls) -> str:
190200
"""Get the embeddings endpoint."""

0 commit comments

Comments
 (0)