mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
fix(backend): add semaphore to executor cost log tasks; fix type annotation
- Add `_log_semaphore = asyncio.Semaphore(50)` to cost_tracking.py to bound concurrent DB inserts (mirrors platform_cost.py's existing semaphore) - Narrow `_extract_model_name` param type from `Any` to `str | dict | None` - Add `test_get_dashboard_cache_hit` to verify TTL cache deduplicates DB calls - Add `scope="col"` to all table `<th>` elements for screen-reader accessibility - Add `(local time)` labels to date filter inputs to clarify timezone behaviour
This commit is contained in:
@@ -168,3 +168,25 @@ def test_get_dashboard_invalid_date_format() -> None:
|
||||
"""Malformed start date must be rejected with 422."""
|
||||
response = client.get("/platform-costs/dashboard", params={"start": "not-a-date"})
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_get_dashboard_cache_hit(
|
||||
mocker: pytest_mock.MockerFixture,
|
||||
) -> None:
|
||||
"""Second identical request returns cached result without calling the DB again."""
|
||||
real_dashboard = PlatformCostDashboard(
|
||||
by_provider=[],
|
||||
by_user=[],
|
||||
total_cost_microdollars=42,
|
||||
total_requests=1,
|
||||
total_users=1,
|
||||
)
|
||||
mock_fn = mocker.patch(
|
||||
"backend.api.features.admin.platform_cost_routes.get_platform_cost_dashboard",
|
||||
AsyncMock(return_value=real_dashboard),
|
||||
)
|
||||
|
||||
client.get("/platform-costs/dashboard")
|
||||
client.get("/platform-costs/dashboard")
|
||||
|
||||
mock_fn.assert_awaited_once() # second request hit the cache
|
||||
|
||||
@@ -34,6 +34,8 @@ _WALLTIME_BILLED_PROVIDERS = frozenset(
|
||||
# Hold strong references to in-flight log tasks so the event loop doesn't
|
||||
# garbage-collect them mid-execution. Tasks remove themselves on completion.
|
||||
_pending_log_tasks: set[asyncio.Task] = set()
|
||||
# Bound concurrent DB inserts to avoid unbounded queue growth under load.
|
||||
_log_semaphore = asyncio.Semaphore(50)
|
||||
|
||||
|
||||
async def drain_pending_cost_logs(timeout: float = 5.0) -> None:
|
||||
@@ -79,26 +81,27 @@ def _schedule_log(
|
||||
db_client: "DatabaseManagerAsyncClient", entry: PlatformCostEntry
|
||||
) -> None:
|
||||
async def _safe_log() -> None:
|
||||
try:
|
||||
await db_client.log_platform_cost(entry)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Failed to log platform cost for user=%s provider=%s block=%s",
|
||||
entry.user_id,
|
||||
entry.provider,
|
||||
entry.block_name,
|
||||
)
|
||||
async with _log_semaphore:
|
||||
try:
|
||||
await db_client.log_platform_cost(entry)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Failed to log platform cost for user=%s provider=%s block=%s",
|
||||
entry.user_id,
|
||||
entry.provider,
|
||||
entry.block_name,
|
||||
)
|
||||
|
||||
task = asyncio.create_task(_safe_log())
|
||||
_pending_log_tasks.add(task)
|
||||
task.add_done_callback(_pending_log_tasks.discard)
|
||||
|
||||
|
||||
def _extract_model_name(raw: Any) -> str | None:
|
||||
def _extract_model_name(raw: str | dict | None) -> str | None:
|
||||
"""Return a string model name from a block input field, or None.
|
||||
|
||||
Handles str (returned as-is), dict (e.g. an enum wrapper, skipped), and
|
||||
any other type (coerced to str as a best-effort fallback).
|
||||
None (no model field). Unexpected types are coerced to str as a fallback.
|
||||
"""
|
||||
if raw is None:
|
||||
return None
|
||||
|
||||
@@ -23,16 +23,36 @@ function LogsTable({ logs, pagination, onPageChange }: Props) {
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="border-b text-xs uppercase text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-3 py-3">Time</th>
|
||||
<th className="px-3 py-3">User</th>
|
||||
<th className="px-3 py-3">Block</th>
|
||||
<th className="px-3 py-3">Provider</th>
|
||||
<th className="px-3 py-3">Type</th>
|
||||
<th className="px-3 py-3">Model</th>
|
||||
<th className="px-3 py-3 text-right">Cost</th>
|
||||
<th className="px-3 py-3 text-right">Tokens</th>
|
||||
<th className="px-3 py-3 text-right">Duration</th>
|
||||
<th className="px-3 py-3">Session</th>
|
||||
<th scope="col" className="px-3 py-3">
|
||||
Time
|
||||
</th>
|
||||
<th scope="col" className="px-3 py-3">
|
||||
User
|
||||
</th>
|
||||
<th scope="col" className="px-3 py-3">
|
||||
Block
|
||||
</th>
|
||||
<th scope="col" className="px-3 py-3">
|
||||
Provider
|
||||
</th>
|
||||
<th scope="col" className="px-3 py-3">
|
||||
Type
|
||||
</th>
|
||||
<th scope="col" className="px-3 py-3">
|
||||
Model
|
||||
</th>
|
||||
<th scope="col" className="px-3 py-3 text-right">
|
||||
Cost
|
||||
</th>
|
||||
<th scope="col" className="px-3 py-3 text-right">
|
||||
Tokens
|
||||
</th>
|
||||
<th scope="col" className="px-3 py-3 text-right">
|
||||
Duration
|
||||
</th>
|
||||
<th scope="col" className="px-3 py-3">
|
||||
Session
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -48,7 +48,7 @@ function PlatformCostContent({ searchParams }: Props) {
|
||||
<div className="flex flex-wrap items-end gap-3 rounded-lg border p-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="start-date" className="text-sm text-muted-foreground">
|
||||
Start Date
|
||||
Start Date <span className="text-xs">(local time)</span>
|
||||
</label>
|
||||
<input
|
||||
id="start-date"
|
||||
@@ -60,7 +60,7 @@ function PlatformCostContent({ searchParams }: Props) {
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="end-date" className="text-sm text-muted-foreground">
|
||||
End Date
|
||||
End Date <span className="text-xs">(local time)</span>
|
||||
</label>
|
||||
<input
|
||||
id="end-date"
|
||||
|
||||
@@ -21,13 +21,29 @@ function ProviderTable({ data, rateOverrides, onRateOverride }: Props) {
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="border-b text-xs uppercase text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-4 py-3">Provider</th>
|
||||
<th className="px-4 py-3">Type</th>
|
||||
<th className="px-4 py-3 text-right">Usage</th>
|
||||
<th className="px-4 py-3 text-right">Requests</th>
|
||||
<th className="px-4 py-3 text-right">Known Cost</th>
|
||||
<th className="px-4 py-3 text-right">Est. Cost</th>
|
||||
<th className="px-4 py-3 text-right" title="Per-session only">
|
||||
<th scope="col" className="px-4 py-3">
|
||||
Provider
|
||||
</th>
|
||||
<th scope="col" className="px-4 py-3">
|
||||
Type
|
||||
</th>
|
||||
<th scope="col" className="px-4 py-3 text-right">
|
||||
Usage
|
||||
</th>
|
||||
<th scope="col" className="px-4 py-3 text-right">
|
||||
Requests
|
||||
</th>
|
||||
<th scope="col" className="px-4 py-3 text-right">
|
||||
Known Cost
|
||||
</th>
|
||||
<th scope="col" className="px-4 py-3 text-right">
|
||||
Est. Cost
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-4 py-3 text-right"
|
||||
title="Per-session only"
|
||||
>
|
||||
Rate <span className="text-[10px] font-normal">(unsaved)</span>
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
@@ -11,11 +11,21 @@ function UserTable({ data }: Props) {
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="border-b text-xs uppercase text-muted-foreground">
|
||||
<tr>
|
||||
<th className="px-4 py-3">User</th>
|
||||
<th className="px-4 py-3 text-right">Known Cost</th>
|
||||
<th className="px-4 py-3 text-right">Requests</th>
|
||||
<th className="px-4 py-3 text-right">Input Tokens</th>
|
||||
<th className="px-4 py-3 text-right">Output Tokens</th>
|
||||
<th scope="col" className="px-4 py-3">
|
||||
User
|
||||
</th>
|
||||
<th scope="col" className="px-4 py-3 text-right">
|
||||
Known Cost
|
||||
</th>
|
||||
<th scope="col" className="px-4 py-3 text-right">
|
||||
Requests
|
||||
</th>
|
||||
<th scope="col" className="px-4 py-3 text-right">
|
||||
Input Tokens
|
||||
</th>
|
||||
<th scope="col" className="px-4 py-3 text-right">
|
||||
Output Tokens
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
Reference in New Issue
Block a user