fix(platform): close tracking gaps found during audit

- resolve_tracking: read `script` field for elevenlabs in addition to
  `script_input`/`text` — VideoNarrationBlock uses `script`, was
  producing tracking_amount=0 characters before.
- exa/similar.py + exa/research.py (3 blocks): extract provider_cost
  from response.cost_dollars.total via merge_stats so tracking_type
  ends up as "cost_usd" with real dollar amounts instead of
  falling through to per_run.
- Add test for script field resolution.

Audit finding: `output_size` set via merge_stats in blocks is
always overridden by the executor wrapper (manager.py:440 computes
byte count of serialized output), and `walltime` is also set by
the wrapper (manager.py:667). So the existing merge_stats(output_size=1)
calls in ~15 blocks are dead code for cost tracking purposes; they
don't hurt but don't add data either. The real tracking data sources
are: (1) input/output_token_count from LLM blocks, (2) provider_cost
from APIs that return USD, (3) input_data for per-character TTS,
(4) auto-populated walltime for wall-clock billing.
This commit is contained in:
Zamil Majdy
2026-04-05 14:38:24 +02:00
parent 5e595231da
commit 6f0c1dfa11
4 changed files with 28 additions and 1 deletions

View File

@@ -12,6 +12,7 @@ from typing import Any, Dict, List, Optional
from pydantic import BaseModel
from backend.data.model import NodeExecutionStats
from backend.sdk import (
APIKeyCredentials,
Block,
@@ -232,6 +233,11 @@ class ExaCreateResearchBlock(Block):
if research.cost_dollars:
yield "cost_total", research.cost_dollars.total
self.merge_stats(
NodeExecutionStats(
provider_cost=research.cost_dollars.total
)
)
return
await asyncio.sleep(check_interval)
@@ -346,6 +352,9 @@ class ExaGetResearchBlock(Block):
yield "cost_searches", research.cost_dollars.num_searches
yield "cost_pages", research.cost_dollars.num_pages
yield "cost_reasoning_tokens", research.cost_dollars.reasoning_tokens
self.merge_stats(
NodeExecutionStats(provider_cost=research.cost_dollars.total)
)
yield "error_message", research.error
@@ -432,6 +441,9 @@ class ExaWaitForResearchBlock(Block):
if research.cost_dollars:
yield "cost_total", research.cost_dollars.total
self.merge_stats(
NodeExecutionStats(provider_cost=research.cost_dollars.total)
)
return

View File

@@ -3,6 +3,7 @@ from typing import Optional
from exa_py import AsyncExa
from backend.data.model import NodeExecutionStats
from backend.sdk import (
APIKeyCredentials,
Block,
@@ -167,3 +168,6 @@ class ExaFindSimilarBlock(Block):
if response.cost_dollars:
yield "cost_dollars", response.cost_dollars
self.merge_stats(
NodeExecutionStats(provider_cost=response.cost_dollars.total)
)

View File

@@ -52,7 +52,11 @@ def resolve_tracking(
# D-ID + ElevenLabs voice: billed per character of script
if provider in ("d_id", "elevenlabs"):
text = input_data.get("script_input", "") or input_data.get("text", "")
text = (
input_data.get("script_input", "")
or input_data.get("text", "")
or input_data.get("script", "") # VideoNarrationBlock uses `script`
)
return "characters", float(len(text)) if isinstance(text, str) else 0.0
# E2B: billed per second of sandbox time

View File

@@ -73,6 +73,13 @@ class TestResolveTracking:
assert tt == "characters"
assert amt == 13.0
def test_elevenlabs_uses_script_field(self):
"""VideoNarrationBlock (elevenlabs) uses `script` field, not script_input/text."""
stats = self._stats()
tt, amt = resolve_tracking("elevenlabs", stats, {"script": "Narration"})
assert tt == "characters"
assert amt == 9.0
def test_e2b_returns_sandbox_seconds(self):
stats = self._stats(walltime=45.123)
tt, amt = resolve_tracking("e2b", stats, {})