mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
fix(platform): address 5 self-review items on cost tracking
- cost_tracking.py: drop asyncio.create_task fire-and-forget (risked task GC mid-flight per Python docs); await log_platform_cost_safe directly. Wrap body in try/except so logging never disrupts executor. - token_tracking.py: same create_task fix; await directly. - platform_cost.py: document that by_provider rows are keyed on (provider, tracking_type) so the same provider can appear multiple times. - PlatformCostContent.tsx: convert datetime-local (naive local time) to UTC ISO before URL serialization so filter windows match admin's wall clock regardless of backend timezone. Convert back to local for input display.
This commit is contained in:
@@ -9,7 +9,6 @@ Both the baseline (OpenRouter) and SDK (Anthropic) service layers need to:
|
||||
This module extracts that common logic so both paths stay in sync.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from backend.data.platform_cost import (
|
||||
@@ -127,28 +126,26 @@ async def persist_and_record_usage(
|
||||
tracking_type = "tokens"
|
||||
tracking_amount = total_tokens
|
||||
|
||||
asyncio.create_task(
|
||||
log_platform_cost_safe(
|
||||
PlatformCostEntry(
|
||||
user_id=user_id,
|
||||
graph_exec_id=session_id,
|
||||
block_id="copilot",
|
||||
block_name=f"copilot:{log_prefix.strip(' []')}".rstrip(":"),
|
||||
provider=provider,
|
||||
credential_id="copilot_system",
|
||||
cost_microdollars=cost_microdollars,
|
||||
input_tokens=prompt_tokens,
|
||||
output_tokens=completion_tokens,
|
||||
model=model,
|
||||
tracking_type=tracking_type,
|
||||
metadata={
|
||||
"tracking_type": tracking_type,
|
||||
"tracking_amount": tracking_amount,
|
||||
"cache_read_tokens": cache_read_tokens,
|
||||
"cache_creation_tokens": cache_creation_tokens,
|
||||
"source": "copilot",
|
||||
},
|
||||
)
|
||||
await log_platform_cost_safe(
|
||||
PlatformCostEntry(
|
||||
user_id=user_id,
|
||||
graph_exec_id=session_id,
|
||||
block_id="copilot",
|
||||
block_name=f"copilot:{log_prefix.strip(' []')}".rstrip(":"),
|
||||
provider=provider,
|
||||
credential_id="copilot_system",
|
||||
cost_microdollars=cost_microdollars,
|
||||
input_tokens=prompt_tokens,
|
||||
output_tokens=completion_tokens,
|
||||
model=model,
|
||||
tracking_type=tracking_type,
|
||||
metadata={
|
||||
"tracking_type": tracking_type,
|
||||
"tracking_amount": tracking_amount,
|
||||
"cache_read_tokens": cache_read_tokens,
|
||||
"cache_creation_tokens": cache_creation_tokens,
|
||||
"source": "copilot",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -167,6 +167,14 @@ async def get_platform_cost_dashboard(
|
||||
provider: str | None = None,
|
||||
user_id: str | None = None,
|
||||
) -> PlatformCostDashboard:
|
||||
"""Aggregate platform cost logs for the admin dashboard.
|
||||
|
||||
Note: by_provider rows are keyed on (provider, tracking_type). A single
|
||||
provider can therefore appear in multiple rows if it has entries with
|
||||
different billing models (e.g. "openai" with both "tokens" and "cost_usd"
|
||||
if pricing is later added for some entries). Frontend treats each row
|
||||
independently rather than as a provider primary key.
|
||||
"""
|
||||
where_p, params_p = _build_where(start, end, provider, user_id, "p")
|
||||
|
||||
by_provider_rows, user_count_rows, by_user_rows = await asyncio.gather(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Helpers for platform cost tracking on system-credential block executions."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
@@ -66,54 +65,59 @@ async def log_system_credential_cost(
|
||||
) -> None:
|
||||
"""Check if a system credential was used and log the platform cost.
|
||||
|
||||
Note: costMicrodollars is left null for now. To populate it, we would
|
||||
need per-token pricing tables or extract cost from provider responses
|
||||
(e.g. OpenRouter returns a cost field). The credit_cost in metadata
|
||||
captures our internal credit charge as a proxy.
|
||||
Logs only the first matching system credential field (one log per
|
||||
execution). Any unexpected error is caught and logged — cost logging
|
||||
is strictly best-effort and must never disrupt block execution.
|
||||
|
||||
Note: costMicrodollars is left null for providers that don't return
|
||||
a USD cost. The credit_cost in metadata captures our internal credit
|
||||
charge as a proxy.
|
||||
"""
|
||||
if node_exec.execution_context.dry_run:
|
||||
return
|
||||
try:
|
||||
if node_exec.execution_context.dry_run:
|
||||
return
|
||||
|
||||
input_data = node_exec.inputs
|
||||
input_model = cast(type[BlockSchema], block.input_schema)
|
||||
input_data = node_exec.inputs
|
||||
input_model = cast(type[BlockSchema], block.input_schema)
|
||||
|
||||
for field_name in input_model.get_credentials_fields():
|
||||
cred_data = input_data.get(field_name)
|
||||
if not cred_data or not isinstance(cred_data, dict):
|
||||
continue
|
||||
cred_id = cred_data.get("id", "")
|
||||
if not cred_id or not is_system_credential(cred_id):
|
||||
continue
|
||||
for field_name in input_model.get_credentials_fields():
|
||||
cred_data = input_data.get(field_name)
|
||||
if not cred_data or not isinstance(cred_data, dict):
|
||||
continue
|
||||
cred_id = cred_data.get("id", "")
|
||||
if not cred_id or not is_system_credential(cred_id):
|
||||
continue
|
||||
|
||||
model_name = input_data.get("model")
|
||||
if model_name is not None and not isinstance(model_name, str):
|
||||
model_name = str(model_name) if not isinstance(model_name, dict) else None
|
||||
model_name = input_data.get("model")
|
||||
if model_name is not None and not isinstance(model_name, str):
|
||||
model_name = (
|
||||
str(model_name) if not isinstance(model_name, dict) else None
|
||||
)
|
||||
|
||||
credit_cost, _ = block_usage_cost(block=block, input_data=input_data)
|
||||
credit_cost, _ = block_usage_cost(block=block, input_data=input_data)
|
||||
|
||||
# Convert provider_cost (USD) to microdollars if available
|
||||
cost_microdollars = None
|
||||
if stats.provider_cost is not None:
|
||||
cost_microdollars = round(stats.provider_cost * MICRODOLLARS_PER_USD)
|
||||
# Convert provider_cost (USD) to microdollars if available
|
||||
cost_microdollars = None
|
||||
if stats.provider_cost is not None:
|
||||
cost_microdollars = round(stats.provider_cost * MICRODOLLARS_PER_USD)
|
||||
|
||||
provider_name = cred_data.get("provider", "unknown")
|
||||
tracking_type, tracking_amount = resolve_tracking(
|
||||
provider=provider_name,
|
||||
stats=stats,
|
||||
input_data=input_data,
|
||||
)
|
||||
provider_name = cred_data.get("provider", "unknown")
|
||||
tracking_type, tracking_amount = resolve_tracking(
|
||||
provider=provider_name,
|
||||
stats=stats,
|
||||
input_data=input_data,
|
||||
)
|
||||
|
||||
meta: dict[str, Any] = {
|
||||
"tracking_type": tracking_type,
|
||||
"tracking_amount": tracking_amount,
|
||||
}
|
||||
if credit_cost:
|
||||
meta["credit_cost"] = credit_cost
|
||||
if stats.provider_cost is not None:
|
||||
meta["provider_cost_usd"] = stats.provider_cost
|
||||
meta: dict[str, Any] = {
|
||||
"tracking_type": tracking_type,
|
||||
"tracking_amount": tracking_amount,
|
||||
}
|
||||
if credit_cost:
|
||||
meta["credit_cost"] = credit_cost
|
||||
if stats.provider_cost is not None:
|
||||
meta["provider_cost_usd"] = stats.provider_cost
|
||||
|
||||
asyncio.create_task(
|
||||
log_platform_cost_safe(
|
||||
await log_platform_cost_safe(
|
||||
PlatformCostEntry(
|
||||
user_id=node_exec.user_id,
|
||||
graph_exec_id=node_exec.graph_exec_id,
|
||||
@@ -134,5 +138,6 @@ async def log_system_credential_cost(
|
||||
metadata=meta or None,
|
||||
)
|
||||
)
|
||||
)
|
||||
return # One log per execution is enough
|
||||
return # One log per execution is enough
|
||||
except Exception:
|
||||
logger.exception("log_system_credential_cost failed unexpectedly")
|
||||
|
||||
@@ -46,8 +46,17 @@ function PlatformCostContent({ searchParams }: Props) {
|
||||
urlParams.get("provider") || searchParams.provider || "";
|
||||
const userFilter = urlParams.get("user_id") || searchParams.user_id || "";
|
||||
|
||||
const [startInput, setStartInput] = useState(startDate);
|
||||
const [endInput, setEndInput] = useState(endDate);
|
||||
// URL holds UTC ISO; datetime-local inputs need local "YYYY-MM-DDTHH:mm".
|
||||
const toLocalInput = (iso: string) => {
|
||||
if (!iso) return "";
|
||||
const d = new Date(iso);
|
||||
if (isNaN(d.getTime())) return "";
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
};
|
||||
|
||||
const [startInput, setStartInput] = useState(toLocalInput(startDate));
|
||||
const [endInput, setEndInput] = useState(toLocalInput(endDate));
|
||||
const [providerInput, setProviderInput] = useState(providerFilter);
|
||||
const [userInput, setUserInput] = useState(userFilter);
|
||||
|
||||
@@ -92,9 +101,16 @@ function PlatformCostContent({ searchParams }: Props) {
|
||||
}
|
||||
|
||||
function handleFilter() {
|
||||
// datetime-local emits naive local time; convert to UTC ISO so the
|
||||
// backend filter window matches what the admin sees in their browser.
|
||||
const toUtcIso = (local: string) => {
|
||||
if (!local) return "";
|
||||
const d = new Date(local);
|
||||
return isNaN(d.getTime()) ? "" : d.toISOString();
|
||||
};
|
||||
updateUrl({
|
||||
start: startInput,
|
||||
end: endInput,
|
||||
start: toUtcIso(startInput),
|
||||
end: toUtcIso(endInput),
|
||||
provider: providerInput,
|
||||
user_id: userInput,
|
||||
page: "1",
|
||||
|
||||
Reference in New Issue
Block a user