This commit is contained in:
@@ -10,10 +10,10 @@ from cloud_api.app import create_app
|
||||
|
||||
class _FakePlannerClient:
|
||||
def __init__(self) -> None:
|
||||
self.calls = 0
|
||||
self.calls: list[dict[str, object]] = []
|
||||
|
||||
def decide(self, **_kwargs) -> ToolCallDecision:
|
||||
self.calls += 1
|
||||
def decide(self, **kwargs) -> ToolCallDecision:
|
||||
self.calls.append(kwargs)
|
||||
return ToolCallDecision(tool_name="tap", arguments={"x": 1, "y": 2})
|
||||
|
||||
|
||||
@@ -38,7 +38,12 @@ def _login_admin(client: TestClient) -> dict[str, str]:
|
||||
|
||||
|
||||
def _create_profile(
|
||||
client: TestClient, headers: dict[str, str], *, name: str, model: str
|
||||
client: TestClient,
|
||||
headers: dict[str, str],
|
||||
*,
|
||||
name: str,
|
||||
model: str,
|
||||
timeout_seconds: float,
|
||||
) -> dict:
|
||||
response = client.post(
|
||||
"/v1/planner/providers",
|
||||
@@ -48,7 +53,7 @@ def _create_profile(
|
||||
"provider_type": "openai-compatible",
|
||||
"model": model,
|
||||
"base_url": "https://compat.example/v1",
|
||||
"timeout_seconds": 30,
|
||||
"timeout_seconds": timeout_seconds,
|
||||
"api_key": f"key-for-{name}",
|
||||
},
|
||||
)
|
||||
@@ -95,7 +100,11 @@ def test_planner_uses_the_newly_activated_database_profile(monkeypatch) -> None:
|
||||
with TestClient(app) as client:
|
||||
admin_headers = _login_admin(client)
|
||||
first = _create_profile(
|
||||
client, admin_headers, name="First", model="first-model"
|
||||
client,
|
||||
admin_headers,
|
||||
name="First",
|
||||
model="first-model",
|
||||
timeout_seconds=41,
|
||||
)
|
||||
assert (
|
||||
client.post(
|
||||
@@ -115,9 +124,14 @@ def test_planner_uses_the_newly_activated_database_profile(monkeypatch) -> None:
|
||||
assert first_decision.status_code == 200, first_decision.text
|
||||
assert resolved_profiles[-1].profile.model == "first-model"
|
||||
assert resolved_profiles[-1].api_key == "key-for-First"
|
||||
assert fake.calls[-1]["timeout"] == 41
|
||||
|
||||
second = _create_profile(
|
||||
client, admin_headers, name="Second", model="second-model"
|
||||
client,
|
||||
admin_headers,
|
||||
name="Second",
|
||||
model="second-model",
|
||||
timeout_seconds=57,
|
||||
)
|
||||
settings = client.get("/v1/planner/providers").json()["settings"]
|
||||
activated = client.post(
|
||||
@@ -134,7 +148,8 @@ def test_planner_uses_the_newly_activated_database_profile(monkeypatch) -> None:
|
||||
)
|
||||
assert second_decision.status_code == 200, second_decision.text
|
||||
assert resolved_profiles[-1].profile.model == "second-model"
|
||||
assert fake.calls == 2
|
||||
assert fake.calls[-1]["timeout"] == 57
|
||||
assert len(fake.calls) == 2
|
||||
|
||||
|
||||
def test_planner_fails_closed_without_an_active_database_profile(monkeypatch) -> None:
|
||||
|
||||
@@ -28,10 +28,21 @@ import httpx
|
||||
from cloud.internal_api.models import PlannerDecisionError, PlannerDecisionResponse
|
||||
from host_agent.config import HostAgentConfig
|
||||
from host_agent.planner_context import current_planner_execution_context
|
||||
from runtime.tool_calling_client import ToolCallDecision, ToolCallUnavailable, ToolCallUsage
|
||||
from runtime.tool_calling_client import (
|
||||
ToolCallDecision,
|
||||
ToolCallUnavailable,
|
||||
ToolCallUsage,
|
||||
)
|
||||
from runtime.tool_specs import ToolSpec
|
||||
|
||||
|
||||
_CLOUD_PROVIDER_MAX_TIMEOUT_SECONDS = 120.0
|
||||
_CLOUD_PROXY_TRANSPORT_GRACE_SECONDS = 5.0
|
||||
_CLOUD_PROXY_HTTP_TIMEOUT_SECONDS = (
|
||||
_CLOUD_PROVIDER_MAX_TIMEOUT_SECONDS + _CLOUD_PROXY_TRANSPORT_GRACE_SECONDS
|
||||
)
|
||||
|
||||
|
||||
class CloudProxyToolCallingClient:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -69,7 +80,9 @@ class CloudProxyToolCallingClient:
|
||||
}
|
||||
for spec in tools
|
||||
],
|
||||
"timeout_seconds": timeout,
|
||||
# Legacy Cloud API versions use this field. Current Cloud API versions
|
||||
# resolve the provider timeout from the active Provider profile.
|
||||
"timeout_seconds": min(timeout, _CLOUD_PROVIDER_MAX_TIMEOUT_SECONDS),
|
||||
}
|
||||
context = current_planner_execution_context()
|
||||
if context is not None:
|
||||
@@ -85,7 +98,7 @@ class CloudProxyToolCallingClient:
|
||||
f"/internal/v1/hosts/{self.config.host_id}/planner/decide",
|
||||
json=payload,
|
||||
headers={"Authorization": f"Bearer {self.config.token}"},
|
||||
timeout=timeout + 5,
|
||||
timeout=_CLOUD_PROXY_HTTP_TIMEOUT_SECONDS,
|
||||
)
|
||||
except httpx.HTTPError as exc:
|
||||
raise ToolCallUnavailable(str(exc)) from exc
|
||||
|
||||
@@ -5,7 +5,10 @@ import json
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from host_agent.cloud_planner_client import CloudProxyToolCallingClient
|
||||
from host_agent.cloud_planner_client import (
|
||||
_CLOUD_PROXY_HTTP_TIMEOUT_SECONDS,
|
||||
CloudProxyToolCallingClient,
|
||||
)
|
||||
from host_agent.config import HostAgentConfig
|
||||
from host_agent.planner_context import PlannerExecutionContext, _context
|
||||
from runtime.tool_calling_client import ToolCallDecision, ToolCallUnavailable
|
||||
@@ -79,6 +82,29 @@ def test_decide_base64_encodes_screenshot() -> None:
|
||||
assert body["screenshot_base64"] == "aGVsbG8="
|
||||
|
||||
|
||||
def test_decide_clamps_legacy_timeout_and_waits_for_cloud_profile_timeout() -> None:
|
||||
seen_requests: list[httpx.Request] = []
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
seen_requests.append(request)
|
||||
return httpx.Response(200, json={"tool_name": "tap", "arguments": {}})
|
||||
|
||||
client = _client(handler)
|
||||
|
||||
client.decide(
|
||||
system_prompt="sp",
|
||||
user_prompt="up",
|
||||
screenshot=None,
|
||||
tools=_TOOLS,
|
||||
timeout=180.0,
|
||||
)
|
||||
|
||||
request = seen_requests[0]
|
||||
body = json.loads(request.content)
|
||||
assert body["timeout_seconds"] == 120.0
|
||||
assert request.extensions["timeout"]["read"] == _CLOUD_PROXY_HTTP_TIMEOUT_SECONDS
|
||||
|
||||
|
||||
def test_decide_includes_bound_assignment_context() -> None:
|
||||
seen_requests: list[httpx.Request] = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user