fix(cloud): align planner configuration and records
Tests / Test passed: 856

This commit is contained in:
2026-07-15 09:43:14 +08:00
parent f8054cb58c
commit 778af2da53
20 changed files with 368 additions and 52 deletions
@@ -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] = []