mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-28 14:07:55 +00:00
* fix(leaderboard): correct telemetry pipeline and outlier handling The public leaderboard at /leaderboard showed a clear bimodal Wh/token distribution: most users at ~3-5 J/token, ~30% inflated by 1000-4000×. A 5-agent investigation workflow + 4-agent verification pass traced the inflation to a cluster of related bugs across telemetry, server, and display layers. This PR fixes the in-repo half. Backfilling existing Supabase data is a separate follow-up. Bug 1 — Dual telemetry recording (`server/routes.py`) ===================================================== `_handle_direct` wrapped the engine with `instrumented_generate` unconditionally. When `app.state.engine` was already an `InstrumentedEngine` (the common case when telemetry is wired in), BOTH layers published `TELEMETRY_RECORD` — once from the inner `InstrumentedEngine.generate`, once from the outer wrapper. Every chat-completion request was counted twice in the leaderboard pipeline. Fix: detect the InstrumentedEngine and unwrap to `._inner` before passing to the wrapper so only one layer fires. Bug 2 — KV-cache fallback over-counts multi-turn (`server/savings.py`) ===================================================================== `compute_savings` falls back from `prompt_tokens_evaluated` to `prompt_tokens` when the KV-cache-aware count is missing. But routes.py aggregates by summing each turn's full prompt — which counts the system prompt N times for an N-turn conversation. The fallback inflated FLOPs and energy by N×. Fix: use 0 (conservative under-count) when the evaluated count is missing rather than falling back to the inflated `prompt_tokens` sum. Bug 3 — TelemetryRecord lacked methodology versioning ===================================================== There was no per-record version tag, so legacy (pre-fix) and current records were silently aggregated together in the public leaderboard even though they used different methodologies. Fix: add `token_counting_version: Optional[int]` field to the `TelemetryRecord` dataclass + a nullable column to the SQLite schema (with idempotent migration); the constant moves from `server.savings` to `core.types` to avoid the server→telemetry layering. New records write the current version; pre-fix rows remain NULL. The aggregator gains a `current_methodology_only=True` flag that filters NULL rows out of leaderboard sums — local dashboards leave it off so historical aggregates still render. Bug 4 — Leaderboard JS displayed missing telemetry as legit zeros ================================================================= Rows with significant token counts but `energy_wh_saved = 0` and `flops_saved = 0` rendered as `0.00 Wh / 0 FLOPs` — visually identical to a user who genuinely did almost nothing. The headline totals also included these rows. Fix: new `isMissingTelemetry()` detector renders missing-telemetry energy/FLOPs cells as `—` (with a tooltip explaining why); a new `.lb-missing` CSS class differentiates the placeholder visually. Bug 5 — Outlier filter was too generous ======================================= `MAX_ENERGY_WH_PER_TOKEN = 10` left a 10,000× margin that admitted every Group B row even though those values are physically impossible (would imply a space-heater per token). Same for the FLOPs cap at `1e17`. Fix: tighten to `0.5` Wh/token (still 500× over a typical consumer GPU) and `1e15` FLOPs/token (still 10,000× over typical). This removes existing pre-fix corrupt rows from the public view without touching Supabase. Tests ===== New regression tests (all pass, ruff clean): - `tests/server/test_routes.py::test_instrumented_engine_unwrapped_to_avoid_dual_telemetry` — pins the bug 1 fix; asserts exactly ONE TELEMETRY_RECORD event per request when the engine is already an InstrumentedEngine. - `tests/server/test_savings.py` (NEW file, 3 tests) — pins the bug 2 fix (FLOPs not inflated via fallback) and the cost-side invariant (dollar savings still use full prompt_tokens because cloud providers bill per input token even when local KV cache hit). - `tests/telemetry/test_aggregator.py::TestMethodologyFilter` (3 tests) — default behaviour includes legacy rows (local dashboard parity); `current_methodology_only=True` excludes them; the summary surface honors the same filter. Unrelated test note =================== `tests/telemetry/test_energy_wiring.py::TestTelemetryStatsEnergy::test_export_includes_energy_fields` and `::TestEndToEndPipeline::test_ask_to_export_with_energy` are flaky on this branch but ALSO flaky on `main` (confirmed via stash + the banner-only run). Root cause: `_version_check.py:132-135` prints the "new version of OpenJarvis is available" banner to stdout outside the throttle window, contaminating `json.loads(result.output)` in those two tests. Reproducible with `OPENJARVIS_NO_UPDATE_CHECK=1` → all pass. That's a separate bug (the banner shouldn't write to the same stream as machine-readable output); not addressing it here to keep this PR focused on the leaderboard pipeline. What this PR explicitly does NOT do ==================================== - Backfill ~30% of Supabase rows that already shipped with inflated values from pre-fix clients. That requires Supabase write access and is the natural follow-up — see PR comments for proposed dry-run audit query and the additive `methodology_version` column migration. - Distinguish which past submissions came from buggy vs correct clients retroactively (no `app_version` tag on existing submissions). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(evals): fix test_energy_scales_linearly under leaderboard fix CI on #498 failed in the slow `test` job: tests/evals/test_use_case_benchmarks.py::TestSavings:: test_energy_scales_linearly — ZeroDivisionError: float division by zero Root cause: the test (and `test_energy_wh_matches_direct_formula`) called `compute_savings(N, 0)` without `prompt_tokens_evaluated`. Under the OLD buggy fallback, an unset evaluated count silently became N, energy scaled linearly with N, and the test passed by accident. Under this branch's conservative fix the fallback is 0, FLOPs collapse to 0, and the `p10.energy_wh / p1.energy_wh` ratio divides 0 by 0. The test's own docstring says "evaluated tokens (KV-cache model)" — it was always meant to exercise the explicit-evaluated path, the API call just didn't match. Pass `prompt_tokens_evaluated` explicitly so the test now expresses the invariant it claims to. Same one-line fix for `test_energy_wh_matches_direct_formula` plus an explicit `flops > 0` sanity assertion — that test was passing trivially with `0 == 0` after the conservative fallback fix, masking whether the formula was actually being exercised. No production code change in this commit. Verified locally: all 6 TestSavings tests pass; the 5 new regression tests added on this branch still pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: krypticmouse <herumbshandilya123@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
484 lines
17 KiB
Python
484 lines
17 KiB
Python
"""Tests for the 5 use-case benchmark datasets and scorers.
|
|
|
|
Tests verify:
|
|
1. Each dataset can be instantiated with correct attributes
|
|
2. Each dataset loads synthetic records (no HuggingFace download)
|
|
3. Records have expected fields
|
|
4. Each scorer can be instantiated
|
|
5. Coding task scorer can score a correct answer
|
|
6. Email triage scorer handles exact match
|
|
7. CLI factory functions work for all 5 benchmarks
|
|
8. Cost calculator and savings modules work
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock # noqa: I001
|
|
|
|
import pytest
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Dataset instantiation and loading
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestEmailTriageDataset:
|
|
def test_instantiation(self) -> None:
|
|
from openjarvis.evals.datasets.email_triage import EmailTriageDataset
|
|
|
|
ds = EmailTriageDataset()
|
|
assert ds.dataset_id == "email_triage"
|
|
assert ds.dataset_name == "Email Triage"
|
|
|
|
def test_load(self) -> None:
|
|
from openjarvis.evals.datasets.email_triage import EmailTriageDataset
|
|
|
|
ds = EmailTriageDataset()
|
|
ds.load(max_samples=5, seed=42)
|
|
assert ds.size() == 5
|
|
records = list(ds.iter_records())
|
|
assert len(records) == 5
|
|
r = records[0]
|
|
assert r.record_id.startswith("email-triage-")
|
|
assert r.category == "use-case"
|
|
assert "urgency" in r.metadata
|
|
assert "category" in r.metadata
|
|
|
|
def test_load_all(self) -> None:
|
|
from openjarvis.evals.datasets.email_triage import EmailTriageDataset
|
|
|
|
ds = EmailTriageDataset()
|
|
ds.load()
|
|
assert ds.size() == 30
|
|
|
|
|
|
class TestMorningBriefDataset:
|
|
def test_instantiation(self) -> None:
|
|
from openjarvis.evals.datasets.morning_brief import MorningBriefDataset
|
|
|
|
ds = MorningBriefDataset()
|
|
assert ds.dataset_id == "morning_brief"
|
|
assert ds.dataset_name == "Morning Brief"
|
|
|
|
def test_load(self) -> None:
|
|
from openjarvis.evals.datasets.morning_brief import MorningBriefDataset
|
|
|
|
ds = MorningBriefDataset()
|
|
ds.load(max_samples=5, seed=42)
|
|
assert ds.size() == 5
|
|
records = list(ds.iter_records())
|
|
r = records[0]
|
|
assert r.record_id.startswith("morning-brief-")
|
|
assert r.reference # key_priorities not empty
|
|
|
|
def test_load_all(self) -> None:
|
|
from openjarvis.evals.datasets.morning_brief import MorningBriefDataset
|
|
|
|
ds = MorningBriefDataset()
|
|
ds.load()
|
|
assert ds.size() == 15
|
|
|
|
|
|
class TestResearchMiningDataset:
|
|
def test_instantiation(self) -> None:
|
|
from openjarvis.evals.datasets.research_mining import ResearchMiningDataset
|
|
|
|
ds = ResearchMiningDataset()
|
|
assert ds.dataset_id == "research_mining"
|
|
assert ds.dataset_name == "Research Mining"
|
|
|
|
def test_load(self) -> None:
|
|
from openjarvis.evals.datasets.research_mining import ResearchMiningDataset
|
|
|
|
ds = ResearchMiningDataset()
|
|
ds.load(max_samples=5, seed=42)
|
|
assert ds.size() == 5
|
|
records = list(ds.iter_records())
|
|
r = records[0]
|
|
assert r.record_id.startswith("research-mining-")
|
|
assert "domain" in r.metadata
|
|
|
|
def test_load_all(self) -> None:
|
|
from openjarvis.evals.datasets.research_mining import ResearchMiningDataset
|
|
|
|
ds = ResearchMiningDataset()
|
|
ds.load()
|
|
assert ds.size() == 31
|
|
|
|
|
|
class TestKnowledgeBaseDataset:
|
|
def test_instantiation(self) -> None:
|
|
from openjarvis.evals.datasets.knowledge_base import KnowledgeBaseDataset
|
|
|
|
ds = KnowledgeBaseDataset()
|
|
assert ds.dataset_id == "knowledge_base"
|
|
assert ds.dataset_name == "Knowledge Base"
|
|
|
|
def test_load(self) -> None:
|
|
from openjarvis.evals.datasets.knowledge_base import KnowledgeBaseDataset
|
|
|
|
ds = KnowledgeBaseDataset()
|
|
ds.load(max_samples=5, seed=42)
|
|
assert ds.size() == 5
|
|
records = list(ds.iter_records())
|
|
r = records[0]
|
|
assert r.record_id.startswith("knowledge-base-")
|
|
assert r.reference # answer not empty
|
|
|
|
def test_load_all(self) -> None:
|
|
from openjarvis.evals.datasets.knowledge_base import KnowledgeBaseDataset
|
|
|
|
ds = KnowledgeBaseDataset()
|
|
ds.load()
|
|
assert ds.size() == 30
|
|
|
|
|
|
class TestCodingTaskDataset:
|
|
def test_instantiation(self) -> None:
|
|
from openjarvis.evals.datasets.coding_task import CodingTaskDataset
|
|
|
|
ds = CodingTaskDataset()
|
|
assert ds.dataset_id == "coding_task"
|
|
assert ds.dataset_name == "Coding Task"
|
|
|
|
def test_load(self) -> None:
|
|
from openjarvis.evals.datasets.coding_task import CodingTaskDataset
|
|
|
|
ds = CodingTaskDataset()
|
|
ds.load(max_samples=5, seed=42)
|
|
assert ds.size() == 5
|
|
records = list(ds.iter_records())
|
|
r = records[0]
|
|
assert r.record_id.startswith("coding-task-")
|
|
assert "test_cases" in r.metadata
|
|
assert "signature" in r.metadata
|
|
|
|
def test_load_all(self) -> None:
|
|
from openjarvis.evals.datasets.coding_task import CodingTaskDataset
|
|
|
|
ds = CodingTaskDataset()
|
|
ds.load()
|
|
assert ds.size() == 29
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Scorer instantiation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestScorerInstantiation:
|
|
def _mock_backend(self):
|
|
return MagicMock()
|
|
|
|
def test_email_triage_scorer(self) -> None:
|
|
from openjarvis.evals.scorers.email_triage import EmailTriageScorer
|
|
|
|
scorer = EmailTriageScorer(self._mock_backend(), "gpt-5-mini")
|
|
assert scorer.scorer_id == "email_triage"
|
|
|
|
def test_morning_brief_scorer(self) -> None:
|
|
from openjarvis.evals.scorers.morning_brief import MorningBriefScorer
|
|
|
|
scorer = MorningBriefScorer(self._mock_backend(), "gpt-5-mini")
|
|
assert scorer.scorer_id == "morning_brief"
|
|
|
|
def test_research_mining_scorer(self) -> None:
|
|
from openjarvis.evals.scorers.research_mining import ResearchMiningScorer
|
|
|
|
scorer = ResearchMiningScorer(self._mock_backend(), "gpt-5-mini")
|
|
assert scorer.scorer_id == "research_mining"
|
|
|
|
def test_knowledge_base_scorer(self) -> None:
|
|
from openjarvis.evals.scorers.knowledge_base import KnowledgeBaseScorer
|
|
|
|
scorer = KnowledgeBaseScorer(self._mock_backend(), "gpt-5-mini")
|
|
assert scorer.scorer_id == "knowledge_base"
|
|
|
|
def test_coding_task_scorer(self) -> None:
|
|
from openjarvis.evals.scorers.coding_task import CodingTaskScorer
|
|
|
|
scorer = CodingTaskScorer(self._mock_backend(), "gpt-5-mini")
|
|
assert scorer.scorer_id == "coding_task"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Scorer functional tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCodingTaskScoring:
|
|
"""Test the coding task scorer with actual code execution."""
|
|
|
|
def test_correct_answer(self) -> None:
|
|
from openjarvis.evals.core.types import EvalRecord
|
|
from openjarvis.evals.scorers.coding_task import CodingTaskScorer
|
|
|
|
scorer = CodingTaskScorer()
|
|
record = EvalRecord(
|
|
record_id="test-1",
|
|
problem="Write is_palindrome",
|
|
reference="",
|
|
category="use-case",
|
|
subject="coding_task",
|
|
metadata={
|
|
"test_cases": (
|
|
'assert is_palindrome("racecar") == True\n'
|
|
'assert is_palindrome("hello") == False\n'
|
|
'assert is_palindrome("") == True'
|
|
),
|
|
},
|
|
)
|
|
answer = "def is_palindrome(s):\n return s == s[::-1]"
|
|
is_correct, meta = scorer.score(record, answer)
|
|
assert is_correct is True
|
|
assert meta["tests_passed"] == 3
|
|
assert meta["pass_rate"] == 1.0
|
|
|
|
def test_incorrect_answer(self) -> None:
|
|
from openjarvis.evals.core.types import EvalRecord
|
|
from openjarvis.evals.scorers.coding_task import CodingTaskScorer
|
|
|
|
scorer = CodingTaskScorer()
|
|
record = EvalRecord(
|
|
record_id="test-2",
|
|
problem="Write add",
|
|
reference="",
|
|
category="use-case",
|
|
metadata={
|
|
"test_cases": ("assert add(1, 2) == 3\nassert add(0, 0) == 0"),
|
|
},
|
|
)
|
|
# Wrong implementation
|
|
answer = "def add(a, b):\n return a * b"
|
|
is_correct, meta = scorer.score(record, answer)
|
|
assert is_correct is False
|
|
|
|
def test_empty_answer(self) -> None:
|
|
from openjarvis.evals.core.types import EvalRecord
|
|
from openjarvis.evals.scorers.coding_task import CodingTaskScorer
|
|
|
|
scorer = CodingTaskScorer()
|
|
record = EvalRecord(
|
|
record_id="test-3",
|
|
problem="Write something",
|
|
reference="",
|
|
category="use-case",
|
|
metadata={"test_cases": "assert True"},
|
|
)
|
|
is_correct, meta = scorer.score(record, "")
|
|
assert is_correct is False
|
|
assert meta["reason"] == "empty_response"
|
|
|
|
|
|
class TestEmailTriageScoring:
|
|
"""Test exact-match path of email triage scorer."""
|
|
|
|
def test_exact_match(self) -> None:
|
|
from openjarvis.evals.core.types import EvalRecord
|
|
from openjarvis.evals.scorers.email_triage import EmailTriageScorer
|
|
|
|
scorer = EmailTriageScorer(MagicMock(), "gpt-5-mini")
|
|
record = EvalRecord(
|
|
record_id="test-1",
|
|
problem="...",
|
|
reference="urgency: high\ncategory: action",
|
|
category="use-case",
|
|
metadata={"urgency": "high", "category": "action"},
|
|
)
|
|
answer = "urgency: high\ncategory: action\ndraft: I'll look into this."
|
|
is_correct, meta = scorer.score(record, answer)
|
|
assert is_correct is True
|
|
assert meta["match_type"] == "exact"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CLI factory tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCLIFactories:
|
|
"""Test that _build_dataset and _build_scorer work for new benchmarks."""
|
|
|
|
@pytest.mark.parametrize(
|
|
"benchmark",
|
|
[
|
|
"email_triage",
|
|
"morning_brief",
|
|
"research_mining",
|
|
"knowledge_base",
|
|
"coding_task",
|
|
],
|
|
)
|
|
def test_build_dataset(self, benchmark: str) -> None:
|
|
from openjarvis.evals.cli import _build_dataset
|
|
|
|
ds = _build_dataset(benchmark)
|
|
assert ds.dataset_id == benchmark
|
|
|
|
@pytest.mark.parametrize(
|
|
"benchmark",
|
|
[
|
|
"email_triage",
|
|
"morning_brief",
|
|
"research_mining",
|
|
"knowledge_base",
|
|
"coding_task",
|
|
],
|
|
)
|
|
def test_build_scorer(self, benchmark: str) -> None:
|
|
from openjarvis.evals.cli import _build_scorer
|
|
|
|
scorer = _build_scorer(benchmark, MagicMock(), "gpt-5-mini")
|
|
assert scorer.scorer_id == benchmark
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Cost calculator tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCostCalculator:
|
|
"""Test the cost calculator module."""
|
|
|
|
def test_estimate_monthly_cost(self) -> None:
|
|
from openjarvis.server.cost_calculator import estimate_monthly_cost
|
|
|
|
est = estimate_monthly_cost(
|
|
calls_per_month=1000,
|
|
avg_input_tokens=500,
|
|
avg_output_tokens=200,
|
|
provider_key="gpt-5.3",
|
|
)
|
|
assert est.monthly_cost > 0
|
|
assert est.annual_cost == est.monthly_cost * 12
|
|
assert est.total_calls_per_month == 1000
|
|
|
|
def test_estimate_scenario(self) -> None:
|
|
from openjarvis.server.cost_calculator import estimate_scenario
|
|
|
|
estimates = estimate_scenario("daily_briefing")
|
|
assert len(estimates) == 3 # 3 cloud providers
|
|
for est in estimates:
|
|
assert est.monthly_cost > 0
|
|
|
|
def test_estimate_all_scenarios(self) -> None:
|
|
from openjarvis.server.cost_calculator import estimate_all_scenarios
|
|
|
|
all_est = estimate_all_scenarios()
|
|
assert len(all_est) == 5 # 5 scenarios
|
|
|
|
def test_unknown_provider(self) -> None:
|
|
from openjarvis.server.cost_calculator import estimate_monthly_cost
|
|
|
|
with pytest.raises(ValueError, match="Unknown provider"):
|
|
estimate_monthly_cost(100, 100, 100, "nonexistent")
|
|
|
|
def test_unknown_scenario(self) -> None:
|
|
from openjarvis.server.cost_calculator import estimate_scenario
|
|
|
|
with pytest.raises(ValueError, match="Unknown scenario"):
|
|
estimate_scenario("nonexistent")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Savings module tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSavings:
|
|
"""Test the savings computation module."""
|
|
|
|
def test_compute_savings_basic(self) -> None:
|
|
from openjarvis.server.savings import compute_savings
|
|
|
|
summary = compute_savings(1000, 500, total_calls=10)
|
|
assert summary.total_calls == 10
|
|
assert summary.total_tokens == 1500
|
|
assert summary.local_cost == 0.0
|
|
assert len(summary.per_provider) == 3
|
|
for p in summary.per_provider:
|
|
assert p.total_cost > 0
|
|
|
|
def test_compute_savings_with_session(self) -> None:
|
|
import time
|
|
|
|
from openjarvis.server.savings import compute_savings
|
|
|
|
start = time.time() - 3600 # 1 hour ago
|
|
summary = compute_savings(
|
|
100000,
|
|
50000,
|
|
total_calls=100,
|
|
session_start=start,
|
|
)
|
|
assert summary.session_duration_hours > 0
|
|
assert summary.monthly_projection # not empty
|
|
|
|
def test_savings_to_dict(self) -> None:
|
|
from openjarvis.server.savings import compute_savings, savings_to_dict
|
|
|
|
summary = compute_savings(1000, 500, total_calls=5)
|
|
d = savings_to_dict(summary)
|
|
assert isinstance(d, dict)
|
|
assert "per_provider" in d
|
|
assert "total_calls" in d
|
|
|
|
def test_energy_scales_linearly(self) -> None:
|
|
"""Energy should scale linearly with evaluated tokens (KV-cache model).
|
|
|
|
The test must pass `prompt_tokens_evaluated` explicitly — under the
|
|
leaderboard-correctness fix, `compute_savings` no longer falls back
|
|
from a missing `prompt_tokens_evaluated` to the (multi-turn-inflated)
|
|
`prompt_tokens` sum. When the KV-cache-aware count is missing, FLOPs
|
|
derive from `completion_tokens` only. With completion=0, that path
|
|
produces zero energy and divides by zero. Passing the evaluated
|
|
count explicitly is the API contract that lets this invariant be
|
|
tested without depending on the (now intentionally conservative)
|
|
fallback behaviour.
|
|
"""
|
|
from openjarvis.server.savings import compute_savings
|
|
|
|
s1 = compute_savings(1000, 0, prompt_tokens_evaluated=1000)
|
|
s10 = compute_savings(10000, 0, prompt_tokens_evaluated=10000)
|
|
# FLOPs = 2*P*T (linear), so 10x tokens => 10x FLOPs => 10x energy
|
|
for p1, p10 in zip(s1.per_provider, s10.per_provider):
|
|
ratio = p10.energy_wh / p1.energy_wh
|
|
assert 9 < ratio < 11, (
|
|
f"{p1.provider}: energy ratio {ratio:.1f}, expected ~10"
|
|
)
|
|
|
|
def test_energy_wh_matches_direct_formula(self) -> None:
|
|
"""Energy must equal flops * wh_per_flop for known constants.
|
|
|
|
Pass `prompt_tokens_evaluated` explicitly for the same reason as
|
|
`test_energy_scales_linearly` — the conservative-fallback fix in
|
|
savings.py means an unset `prompt_tokens_evaluated` produces 0
|
|
FLOPs and would make this test pass trivially with 0 == 0.
|
|
"""
|
|
from openjarvis.server.savings import CLOUD_PRICING, compute_savings
|
|
|
|
summary = compute_savings(10000, 0, prompt_tokens_evaluated=10000)
|
|
for p in summary.per_provider:
|
|
pricing = CLOUD_PRICING[p.provider]
|
|
wh_per_flop = pricing["energy_wh_per_1k_tokens"] / (
|
|
1000 * pricing.get("flops_per_token", 3e12)
|
|
)
|
|
expected = p.flops * wh_per_flop
|
|
assert abs(p.energy_wh - expected) < 1e-6, (
|
|
f"{p.provider}: energy_wh={p.energy_wh}, expected={expected}"
|
|
)
|
|
# Sanity: assert the formula isn't matching trivially with both
|
|
# sides equal to zero (would happen if FLOPs collapse to 0).
|
|
assert p.flops > 0, (
|
|
f"{p.provider}: FLOPs must be positive when "
|
|
f"prompt_tokens_evaluated is explicitly set"
|
|
)
|
|
|
|
def test_energy_not_zero(self) -> None:
|
|
"""Energy must be positive for non-zero token counts."""
|
|
from openjarvis.server.savings import compute_savings
|
|
|
|
summary = compute_savings(500, 500)
|
|
for p in summary.per_provider:
|
|
assert p.energy_wh > 0, f"{p.provider}: energy_wh should be > 0"
|