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:
Zamil Majdy
2026-04-05 11:55:00 +02:00
parent 7afd2b249d
commit 372900c141
4 changed files with 95 additions and 69 deletions

View File

@@ -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",
},
)
)

View File

@@ -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(

View File

@@ -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")

View File

@@ -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",