Skip to content
Open
36 changes: 36 additions & 0 deletions apps/api/routers/worker_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,42 @@ def list_worker_edit_requests(
return [dict(r) for r in rows]


class WorkerSpendResponse(BaseModel):
"""#1201: a single worker's month-to-date spend + its configured monthly
cap (if any), so an operator can see which worker is driving cost."""

worker_id: str
month_spend_usd: float
monthly_cap_usd: Optional[float] = None


@worker_admin_router.get("/workers/{worker_id}/spend", response_model=WorkerSpendResponse)
def get_worker_spend(
worker_id: str,
auth: AuthContext = Depends(get_auth_context),
repos: Repositories = Depends(get_repos),
) -> WorkerSpendResponse:
"""#1201: worker month-to-date spend, read-only. Reuses the same
aggregation + cap resolution that already gates worker-level spend caps
(services.run_cost / run_service.get_worker_config_for_run) so the number
shown here always agrees with what actually blocks a run. 404s for a
worker the caller can't see (same visibility check as GET
/workers/{worker_id})."""
worker_id = _canonical_worker_id(worker_id)
worker = _get_visible_worker(worker_id, user_id=auth.user_id, repos=repos)
if not worker:
raise HTTPException(status_code=404, detail="Worker not found")

from run_service import get_worker_config_for_run
from services.run_cost import _spend_cap_for_config, _worker_month_to_date_cost_usd

owner_id = str(worker.get("owner_id") or auth.user_id)
spend = _worker_month_to_date_cost_usd(worker_id, repos=repos, user_id=owner_id)
config = get_worker_config_for_run(worker_id, repos=repos, user_id=owner_id)
cap = _spend_cap_for_config(config)
return WorkerSpendResponse(worker_id=worker_id, month_spend_usd=spend, monthly_cap_usd=cap)


@worker_admin_router.get("/workers/{worker_id}/bundle.zip")
def download_worker_bundle(
worker_id: str,
Expand Down
51 changes: 48 additions & 3 deletions apps/api/routers/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -1365,6 +1365,7 @@ def delete_workspace_secret(
def get_workspace_settings(
request: Request,
auth: AuthContext = Depends(get_auth_context),
repos: Repositories = Depends(get_repos),
) -> Dict[str, str]:
"""#794/#797: workspace behaviour toggles + model defaults (key→value map).

Expand All @@ -1382,13 +1383,57 @@ def get_workspace_settings(
try:
from run_service import _workspace_day_to_date_cost_usd, _workspace_month_to_date_cost_usd

out["current_day_spend_usd"] = f"{_workspace_day_to_date_cost_usd():.4f}"
out["current_month_spend_usd"] = f"{_workspace_month_to_date_cost_usd():.4f}"
# #1201: pass repos + the caller's user_id so a hosted deployment
# sums cost via its own Repositories backend (e.g. Supabase). Without
# these, the aggregation silently fell back to the engine's local
# sqlite file, which is empty on a hosted deployment, so cloud always
# rendered $0.00 regardless of real spend.
out["current_day_spend_usd"] = f"{_workspace_day_to_date_cost_usd(repos=repos, user_id=auth.user_id):.4f}"
out["current_month_spend_usd"] = f"{_workspace_month_to_date_cost_usd(repos=repos, user_id=auth.user_id):.4f}"
except Exception:
pass
logger.debug("workspace settings: current spend lookup failed", exc_info=True)
return out


class WorkspaceSpendResponse(BaseModel):
"""#1201: 'who pays for this, doesn't this cost a lot', a purpose-built,
read-only readout of workspace spend-to-date against its configured caps.
Reuses the same aggregation the spend-cap enforcement already runs
(services.run_cost); never writes anything."""

day_spend_usd: float
month_spend_usd: float
daily_cap_usd: Optional[float] = None
monthly_cap_usd: Optional[float] = None


@workspace_router.get("/workspace/spend", response_model=WorkspaceSpendResponse)
def get_workspace_spend(
request: Request,
auth: AuthContext = Depends(get_auth_context),
repos: Repositories = Depends(get_repos),
) -> WorkspaceSpendResponse:
"""#1201: workspace month/day spend-to-date + the configured caps, scoped
to the caller's active workspace via the request-bound Repositories (no
cross-workspace params to accept, so there is nothing to authorize beyond
a valid session). Any member can view; only admins can change the cap
(PUT /workspace/settings/{key}, #804)."""
from run_service import (
_workspace_day_to_date_cost_usd,
_workspace_daily_spend_cap_usd,
_workspace_month_to_date_cost_usd,
_workspace_monthly_spend_cap_usd,
)
workspace_id = _active_workspace_id(request)

return WorkspaceSpendResponse(
day_spend_usd=_workspace_day_to_date_cost_usd(repos=repos, user_id=auth.user_id),
month_spend_usd=_workspace_month_to_date_cost_usd(repos=repos, user_id=auth.user_id),
daily_cap_usd=_workspace_daily_spend_cap_usd(workspace_id=workspace_id),
monthly_cap_usd=_workspace_monthly_spend_cap_usd(workspace_id=workspace_id),
)


@workspace_router.put("/workspace/settings/{key}", status_code=204, response_class=Response)
def put_workspace_setting(
key: str,
Expand Down
9 changes: 4 additions & 5 deletions apps/api/services/run_cost.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,10 @@ def _spend_cap_for_config(config: Any) -> Optional[float]:
return float(cap) if cap is not None else None


def _workspace_monthly_spend_cap_usd() -> Optional[float]:
def _workspace_monthly_spend_cap_usd(*, workspace_id: str = "local-default") -> Optional[float]:
"""#797: the workspace-level monthly spend cap from settings, then env default."""
from run_service import _workspace_setting
raw = (_workspace_setting("monthly_spend_cap_usd") or "").strip()
raw = (_workspace_setting("monthly_spend_cap_usd", workspace_id=workspace_id) or "").strip()
if not raw:
return _default_spend_cap_usd("WORKEROS_DEFAULT_MONTHLY_SPEND_CAP_USD", "25")
try:
Expand All @@ -162,10 +162,10 @@ def _workspace_monthly_spend_cap_usd() -> Optional[float]:
return _default_spend_cap_usd("WORKEROS_DEFAULT_MONTHLY_SPEND_CAP_USD", "25")


def _workspace_daily_spend_cap_usd() -> Optional[float]:
def _workspace_daily_spend_cap_usd(*, workspace_id: str = "local-default") -> Optional[float]:
"""Workspace-level daily spend cap from settings, then env default."""
from run_service import _workspace_setting
raw = (_workspace_setting("daily_spend_cap_usd") or "").strip()
raw = (_workspace_setting("daily_spend_cap_usd", workspace_id=workspace_id) or "").strip()
if not raw:
return _default_spend_cap_usd("WORKEROS_DEFAULT_DAILY_SPEND_CAP_USD", "5")
try:
Expand Down Expand Up @@ -315,4 +315,3 @@ def _user_day_to_date_cost_usd(
except Exception:
logger.debug("user day-to-date cost lookup failed for %s", user_id, exc_info=True)
return 0.0

120 changes: 120 additions & 0 deletions apps/api/tests/test_1201_worker_spend_endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""#1201: GET /workers/{worker_id}/spend, a single worker's month-to-date
spend + its configured monthly cap, read-only.

Run: cd apps/api && python -m pytest tests/test_1201_worker_spend_endpoint.py -q
"""
from __future__ import annotations

import importlib
import sys
import textwrap
import types
from pathlib import Path

import pytest

API_DIR = Path(__file__).resolve().parents[1]
if str(API_DIR) not in sys.path:
sys.path.insert(0, str(API_DIR))

SECRET = "test-secret-1201"


def _yml(worker_id: str, *, monthly_cost_cap: float | None = None) -> str:
base = textwrap.dedent(
f"""
schema_version: "0.3"
id: "{worker_id}"
name: "{worker_id}"
title: t
description: d
version: "0.1.0"
exec:
entry: run.py
runtime: python311
runner: e2b
command: python run.py
inputs: []
outputs: []
trigger:
type: manual
connections: []
"""
).strip() + "\n"
if monthly_cost_cap is not None:
base += f"limits:\n max_monthly_cost_usd: {monthly_cost_cap}\n"
return base


@pytest.fixture
def client_main(monkeypatch, tmp_path):
(tmp_path / "workers").mkdir()
monkeypatch.setenv("FLOOM_DB", str(tmp_path / "floom.db"))
monkeypatch.setenv("WORKEROS_DB", str(tmp_path / "floom.db"))
monkeypatch.setenv("FLOOM_WORKERS_DIR", str(tmp_path / "workers"))
monkeypatch.setenv("FLOOM_ARTIFACTS_DIR", str(tmp_path / "artifacts"))
monkeypatch.setenv("FLOOM_BLOBS_DIR", str(tmp_path / "blobs"))
monkeypatch.setenv("WORKEROS_API_ENV_FILE", str(tmp_path / "api.env"))
monkeypatch.setenv("WORKEROS_WORKSPACE_DIR", str(tmp_path))
monkeypatch.setenv("FLOOM_SECRET", SECRET)
monkeypatch.setenv("WORKEROS_SHARED_SECRET_ROLE", "admin")
monkeypatch.setenv("WORKEROS_DEPLOY", "local")
for name in list(sys.modules):
if name in ("main", "models", "worker_registry", "run_service", "chat_service") or name.startswith(("routers", "services", "core", "db", "auth", "contexts", "runner_sandbox")):
sys.modules.pop(name, None)
sys.modules["scheduler"] = types.SimpleNamespace(start_scheduler=lambda: None, stop_scheduler=lambda: None)
main = importlib.import_module("main")
main.start_run = lambda *a, **k: None
import run_service
run_service.start_run = main.start_run
from fastapi.testclient import TestClient

client = TestClient(main.app, headers={"x-floom-secret": SECRET}, raise_server_exceptions=False)
return client, main


def _seed_cost(worker_id, cost, created_at):
from db import get_db

with get_db() as conn:
conn.execute(
"INSERT INTO runs (id, worker_id, status, trigger_source, runner, created_at, total_cost_usd) "
"VALUES (?, ?, ?, ?, ?, ?, ?)",
(f"r_{worker_id}_{int(cost*100)}", worker_id, "completed", "manual", "e2b", created_at, cost),
)


class TestWorkerSpendEndpoint:
def test_returns_month_to_date_spend_and_cap(self, client_main):
from datetime import datetime, timezone

client, _ = client_main
assert client.post(
"/workers", json={"worker_yml": _yml("spendworkeralpha", monthly_cost_cap=25.0), "run_py": "print(1)"}
).status_code == 200
this_month = datetime.now(timezone.utc).strftime("%Y-%m-05T00:00:00+00:00")
_seed_cost("spendworkeralpha", 7.5, this_month)

resp = client.get("/workers/spendworkeralpha/spend")
assert resp.status_code == 200, resp.text
body = resp.json()
assert body["worker_id"] == "spendworkeralpha"
assert body["month_spend_usd"] >= 7.5
assert body["monthly_cap_usd"] == 25.0

def test_worker_with_no_cap_returns_null_cap(self, client_main):
client, _ = client_main
assert client.post(
"/workers", json={"worker_yml": _yml("spendworkerbeta"), "run_py": "print(1)"}
).status_code == 200

resp = client.get("/workers/spendworkerbeta/spend")
assert resp.status_code == 200, resp.text
body = resp.json()
assert body["month_spend_usd"] == 0.0
assert body["monthly_cap_usd"] is None

def test_unknown_worker_404s(self, client_main):
client, _ = client_main
resp = client.get("/workers/does-not-exist-at-all/spend")
assert resp.status_code == 404, resp.text
83 changes: 81 additions & 2 deletions apps/api/tests/test_797_workspace_defaults_enforcement.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,10 +374,89 @@ def test_settings_returns_current_month_spend(self, client_main):

client, _ = client_main
assert client.post("/workers", json={"worker_yml": _yml("capworkerdelta"), "run_py": "print(1)"}).status_code == 200
this_month = datetime.now(timezone.utc).strftime("%Y-%m-05T00:00:00+00:00")
_seed_cost("capworkerdelta", 4.25, this_month)
# #1201: seed a run for "today" (not a hardcoded day-05), so the
# day-spend assertion below isn't date-dependent flaky past the 5th
# of any given month.
today = datetime.now(timezone.utc).strftime("%Y-%m-%dT01:00:00+00:00")
_seed_cost("capworkerdelta", 4.25, today)
settings = client.get("/workspace/settings").json()
assert "current_day_spend_usd" in settings
assert "current_month_spend_usd" in settings
assert float(settings["current_day_spend_usd"]) >= 4.25
assert float(settings["current_month_spend_usd"]) >= 4.25

def test_workspace_spend_endpoint_returns_current_spend_and_caps(self, client_main):
"""#1201: GET /workspace/spend is the purpose-built readout next to
the spend-cap setting (settings-page stat + any other consumer)."""
from datetime import datetime, timezone

client, _ = client_main
assert client.post("/workers", json={"worker_yml": _yml("spendendpointalpha"), "run_py": "print(1)"}).status_code == 200
_set(client, "monthly_spend_cap_usd", "50.0")
_set(client, "daily_spend_cap_usd", "10.0")
today = datetime.now(timezone.utc).strftime("%Y-%m-%dT01:00:00+00:00")
_seed_cost("spendendpointalpha", 6.5, today)

resp = client.get("/workspace/spend")
assert resp.status_code == 200, resp.text
body = resp.json()
assert body["day_spend_usd"] >= 6.5
assert body["month_spend_usd"] >= 6.5
assert body["daily_cap_usd"] == 10.0
assert body["monthly_cap_usd"] == 50.0

def test_workspace_spend_endpoint_returns_active_workspace_caps(self, client_main):
"""The cap read uses the request's active workspace, not local-default."""
client, _ = client_main
_set(client, "daily_spend_cap_usd", "10.0")
_set(client, "monthly_spend_cap_usd", "50.0")

created = client.post("/workspaces", json={"name": "Spend visibility"})
assert created.status_code == 200, created.text
workspace_id = created.json()["id"]
headers = {"x-floom-workspace": workspace_id}
assert client.put(
"/workspace/settings/daily_spend_cap_usd",
json={"value": "21.0"},
headers=headers,
).status_code in (200, 204)
assert client.put(
"/workspace/settings/monthly_spend_cap_usd",
json={"value": "84.0"},
headers=headers,
).status_code in (200, 204)

resp = client.get("/workspace/spend", headers=headers)

assert resp.status_code == 200, resp.text
assert resp.json()["daily_cap_usd"] == 21.0
assert resp.json()["monthly_cap_usd"] == 84.0

def test_workspace_spend_endpoint_uses_repo_backend_when_available(self, client_main):
"""#1201: regression guard for the exact bug this PR fixes: the
workspace spend read must go through Repositories.runs.cost_total_usd
(workspace_scoped=True) when the deploy provides one, not silently
fall back to the engine's local sqlite (empty on a hosted deploy)."""
import run_service

client, main = client_main
calls = []

class _Runs:
def cost_total_usd(self, **kwargs):
calls.append(kwargs)
return 3.25

class _FakeRepos:
runs = _Runs()

main.app.dependency_overrides[main.get_repos] = lambda: _FakeRepos()
try:
resp = client.get("/workspace/spend")
finally:
main.app.dependency_overrides.pop(main.get_repos, None)
assert resp.status_code == 200, resp.text
body = resp.json()
assert body["day_spend_usd"] == 3.25
assert body["month_spend_usd"] == 3.25
assert any(call.get("workspace_scoped") is True for call in calls)
Loading
Loading