feat(cloud): enforce host governance budgets
Tests / Test passed: 662

This commit is contained in:
2026-07-13 22:56:31 +08:00
parent 2cd314b183
commit b4803f90e6
28 changed files with 1311 additions and 14 deletions
@@ -44,6 +44,7 @@ from cloud.repository import (
DeviceEnrollmentConflictError,
HostEnrollmentConflictError,
)
from cloud.governance import TokenBudgetExceededError
from core.models import Device, utc_now
from runtime.tool_calling_client import ToolCallingClient, ToolCallUnavailable
from runtime.tool_specs import ToolSpec
@@ -65,11 +66,17 @@ def create_internal_router(
sleep: Callable[[float], Awaitable[None]] = asyncio.sleep,
planner_client_factory: Callable[[], ToolCallingClient] | None = None,
scheduler: TaskScheduler | None = None,
planner_token_reservation_ceiling: int = 4096,
planner_token_reservation_ttl_seconds: float = 300.0,
) -> APIRouter:
if claim_poll_interval_seconds <= 0:
raise ValueError("claim_poll_interval_seconds must be greater than zero")
if lease_duration_seconds <= 0:
raise ValueError("lease_duration_seconds must be greater than zero")
if planner_token_reservation_ceiling <= 0:
raise ValueError("planner_token_reservation_ceiling must be greater than zero")
if planner_token_reservation_ttl_seconds <= 0:
raise ValueError("planner_token_reservation_ttl_seconds must be greater than zero")
router = APIRouter(prefix=version_prefix, tags=["host-agent"])
build_planner_client = planner_client_factory or _default_planner_client_factory
@@ -352,6 +359,7 @@ def create_internal_router(
response_model_exclude_none=True,
responses={
status.HTTP_502_BAD_GATEWAY: {"model": PlannerDecisionError},
status.HTTP_429_TOO_MANY_REQUESTS: {"model": PlannerDecisionError},
},
)
def decide_planner_call(
@@ -385,6 +393,25 @@ def create_internal_router(
for tool in payload.tools
]
now = utc_now()
reservation = None
try:
reservation = pool.store.reserve_host_token_budget(
reservation_id=uuid4().hex,
host_id=host_id,
usage_day=now.date().isoformat(),
reserved_tokens=planner_token_reservation_ceiling,
task_id=None,
attempt=None,
created_at=now,
expires_at=now + timedelta(seconds=planner_token_reservation_ttl_seconds),
)
except TokenBudgetExceededError as exc:
return JSONResponse(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
content=PlannerDecisionError(detail=str(exc)).model_dump(),
)
started_at = monotonic()
client = build_planner_client()
try:
@@ -408,6 +435,19 @@ def create_internal_router(
status_code=status.HTTP_502_BAD_GATEWAY,
content=PlannerDecisionError(detail=str(exc)).model_dump(),
)
usage = decision.usage
if reservation is not None and usage is not None and usage.total_tokens is not None:
planner_config = load_cloud_planner_config()
pool.store.settle_host_token_reservation(
reservation_id=reservation.id,
event_id=uuid4().hex,
provider=planner_config.provider,
model=planner_config.resolved_model(),
input_tokens=usage.input_tokens,
output_tokens=usage.output_tokens,
total_tokens=usage.total_tokens,
occurred_at=utc_now(),
)
logger.info(
"planner-decision request resolved",
extra={