feat(frontier): add replay variability suite and token-cost telemetry (#66)
Agent-Logs-Url: https://github.com/AssetOverflow/core/sessions/f88b48fa-0c2a-4f9d-a42b-d275596e43b8 Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: AssetOverflow <109810776+AssetOverflow@users.noreply.github.com>
This commit is contained in:
parent
9e6fa4be75
commit
dedf05565d
8 changed files with 384 additions and 27 deletions
|
|
@ -6,7 +6,7 @@
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# OpenAI
|
# OpenAI
|
||||||
# Used by: evals/frontier_compare/providers.py (OpenAIAdapter)
|
# Used by: evals/frontier_compare/providers.py (OpenAIAdapter)
|
||||||
# Benchmarks: determinism, truth_lock, axis_orthogonality (provider=openai)
|
# Benchmarks: prompt_battery, replay_variability (provider=openai)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
OPENAI_API_KEY=
|
OPENAI_API_KEY=
|
||||||
|
|
||||||
|
|
@ -21,7 +21,7 @@ OPENAI_MODEL=gpt-4o
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Anthropic
|
# Anthropic
|
||||||
# Used by: evals/frontier_compare/providers.py (AnthropicAdapter)
|
# Used by: evals/frontier_compare/providers.py (AnthropicAdapter)
|
||||||
# Benchmarks: determinism, truth_lock, axis_orthogonality (provider=anthropic)
|
# Benchmarks: prompt_battery, replay_variability (provider=anthropic)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
ANTHROPIC_API_KEY=
|
ANTHROPIC_API_KEY=
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,18 @@ If CORE can solve something the LLM cannot structurally audit, CORE must prove i
|
||||||
If both solve it, compare correctness, determinism, traceability, latency, cost, memory, and failure mode.
|
If both solve it, compare correctness, determinism, traceability, latency, cost, memory, and failure mode.
|
||||||
```
|
```
|
||||||
|
|
||||||
Wave 1 is deliberately local and CORE-native. It does **not** call external frontier APIs, does **not** require provider keys, and does **not** change runtime behavior. It creates the benchmark harness, report schema, recording UI, and first suites that measure the things CORE should already be able to defend:
|
Wave 1 now supports both CORE-native and cross-provider execution through adapter wiring. CORE-only suites remain local and deterministic; cross-provider suites can call external APIs when credentials are configured.
|
||||||
|
|
||||||
|
Current suites focus on:
|
||||||
|
|
||||||
* deterministic replay
|
* deterministic replay
|
||||||
* truth-lock / groundedness behavior
|
* truth-lock / groundedness behavior
|
||||||
* register vs anchor-lens axis discipline
|
* register vs anchor-lens axis discipline
|
||||||
* compact machine-readable reports suitable for later head-to-head frontier runs
|
* compact machine-readable reports suitable for head-to-head frontier runs
|
||||||
* a static visual report viewer for clean recordings and demos
|
* a static visual report viewer for clean recordings and demos
|
||||||
|
* usage-token and formula-based cost telemetry when provider APIs expose usage
|
||||||
|
|
||||||
Provider adapters for GPT / Claude / Gemini / open-weight baselines are intentionally deferred to a later wave so this PR remains testable without secrets.
|
Provider adapters for OpenAI / Anthropic / Ollama are available; CORE still remains runnable with no API keys.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -74,6 +77,29 @@ surface_variation_observed
|
||||||
anchor_lens_engagement_observed
|
anchor_lens_engagement_observed
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `prompt_battery` (cross-provider)
|
||||||
|
|
||||||
|
Provider-agnostic prompt battery over the adapter interface.
|
||||||
|
|
||||||
|
Primary metrics:
|
||||||
|
|
||||||
|
```text
|
||||||
|
non_empty_surface_rate
|
||||||
|
mean_latency_ms
|
||||||
|
token_usage_capture_rate
|
||||||
|
formula_cost_estimate (when pricing + usage are available)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `replay_variability` (cross-provider)
|
||||||
|
|
||||||
|
Runs repeated calls per prompt and scores stability.
|
||||||
|
|
||||||
|
Primary metric:
|
||||||
|
|
||||||
|
```text
|
||||||
|
stability_score = 1 / unique_surface_count
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
@ -98,6 +124,12 @@ Human-readable table:
|
||||||
uv run python -m evals.frontier_compare --suite all
|
uv run python -m evals.frontier_compare --suite all
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Cross-provider (example):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run python -m evals.frontier_compare --provider openai --suite all --json
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Recording UI
|
## Recording UI
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ import json
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from .cross_provider import run_prompt_battery
|
from .cross_provider import run_prompt_battery, run_replay_variability
|
||||||
from .model_registry import require_model_card
|
from .model_registry import require_model_card
|
||||||
from .providers import ProviderConfig, build_adapter, load_dotenv_if_present
|
from .providers import ProviderConfig, build_observing_adapter, load_dotenv_if_present
|
||||||
from .runner import (
|
from .runner import (
|
||||||
BenchmarkReport,
|
BenchmarkReport,
|
||||||
format_human_report,
|
format_human_report,
|
||||||
|
|
@ -18,7 +18,7 @@ from .runner import (
|
||||||
|
|
||||||
|
|
||||||
_CORE_ONLY_SUITES = ("determinism", "truth_lock", "axis_orthogonality", "all")
|
_CORE_ONLY_SUITES = ("determinism", "truth_lock", "axis_orthogonality", "all")
|
||||||
_CROSS_PROVIDER_SUITES = ("prompt_battery",)
|
_CROSS_PROVIDER_SUITES = ("prompt_battery", "replay_variability")
|
||||||
_VALID_SUITES = _CORE_ONLY_SUITES + _CROSS_PROVIDER_SUITES
|
_VALID_SUITES = _CORE_ONLY_SUITES + _CROSS_PROVIDER_SUITES
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ def build_parser() -> argparse.ArgumentParser:
|
||||||
"benchmark suite to run. CORE-only suites: "
|
"benchmark suite to run. CORE-only suites: "
|
||||||
f"{', '.join(_CORE_ONLY_SUITES)}. Cross-provider suites: "
|
f"{', '.join(_CORE_ONLY_SUITES)}. Cross-provider suites: "
|
||||||
f"{', '.join(_CROSS_PROVIDER_SUITES)}. Default 'all' runs every "
|
f"{', '.join(_CROSS_PROVIDER_SUITES)}. Default 'all' runs every "
|
||||||
"CORE-only suite when --provider=core, otherwise prompt_battery."
|
"CORE-only suite when --provider=core, otherwise all cross-provider suites."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
|
@ -86,6 +86,15 @@ def build_parser() -> argparse.ArgumentParser:
|
||||||
default=Path(".env"),
|
default=Path(".env"),
|
||||||
help="path to a .env file with provider credentials (default: ./.env).",
|
help="path to a .env file with provider credentials (default: ./.env).",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--repeats",
|
||||||
|
type=int,
|
||||||
|
default=3,
|
||||||
|
help=(
|
||||||
|
"repeat count for repeat-sensitive cross-provider suites "
|
||||||
|
"(currently replay_variability)."
|
||||||
|
),
|
||||||
|
)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -147,13 +156,31 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
else:
|
else:
|
||||||
# Cross-provider path — runs over the provider adapter.
|
# Cross-provider path — runs over the provider adapter.
|
||||||
cfg = _build_cfg(args)
|
cfg = _build_cfg(args)
|
||||||
adapter = build_adapter(cfg)
|
adapter = build_observing_adapter(cfg)
|
||||||
suite_report = run_prompt_battery(adapter, cfg=cfg)
|
if args.suite == "all" and args.provider != "core":
|
||||||
|
suite_reports = (
|
||||||
|
run_prompt_battery(adapter, cfg=cfg),
|
||||||
|
run_replay_variability(
|
||||||
|
adapter,
|
||||||
|
cfg=cfg,
|
||||||
|
repeats=max(1, int(args.repeats)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
elif args.suite == "replay_variability":
|
||||||
|
suite_reports = (
|
||||||
|
run_replay_variability(
|
||||||
|
adapter,
|
||||||
|
cfg=cfg,
|
||||||
|
repeats=max(1, int(args.repeats)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
suite_reports = (run_prompt_battery(adapter, cfg=cfg),)
|
||||||
report = BenchmarkReport(
|
report = BenchmarkReport(
|
||||||
benchmark_family="frontier_compare_wave1",
|
benchmark_family="frontier_compare_wave1",
|
||||||
model=cfg.model,
|
model=cfg.model,
|
||||||
mode=cfg.provider,
|
mode=cfg.provider,
|
||||||
suites=(suite_report,),
|
suites=suite_reports,
|
||||||
)
|
)
|
||||||
# Always persist non-CORE runs — they're rate-limited / paid, so
|
# Always persist non-CORE runs — they're rate-limited / paid, so
|
||||||
# losing the artifact is genuinely costly. CORE adapter runs of
|
# losing the artifact is genuinely costly. CORE adapter runs of
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
|
from .model_registry import resolve_model_card
|
||||||
from .providers import ProviderConfig
|
from .providers import ProviderConfig
|
||||||
from .runner import CaseResult, SuiteReport
|
from .runner import CaseResult, SuiteReport
|
||||||
|
|
||||||
|
|
@ -78,6 +79,10 @@ class ProviderObservation:
|
||||||
provider: str
|
provider: str
|
||||||
model: str
|
model: str
|
||||||
elapsed_ms: float
|
elapsed_ms: float
|
||||||
|
input_tokens: int | None = None
|
||||||
|
output_tokens: int | None = None
|
||||||
|
total_tokens: int | None = None
|
||||||
|
estimated_cost_usd: float | None = None
|
||||||
error_type: str = ""
|
error_type: str = ""
|
||||||
error_message: str = ""
|
error_message: str = ""
|
||||||
|
|
||||||
|
|
@ -92,6 +97,10 @@ class ProviderObservation:
|
||||||
"provider": self.provider,
|
"provider": self.provider,
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"elapsed_ms": self.elapsed_ms,
|
"elapsed_ms": self.elapsed_ms,
|
||||||
|
"input_tokens": self.input_tokens,
|
||||||
|
"output_tokens": self.output_tokens,
|
||||||
|
"total_tokens": self.total_tokens,
|
||||||
|
"estimated_cost_usd": self.estimated_cost_usd,
|
||||||
"error_type": self.error_type,
|
"error_type": self.error_type,
|
||||||
"error_message": self.error_message,
|
"error_message": self.error_message,
|
||||||
}
|
}
|
||||||
|
|
@ -103,13 +112,14 @@ class ProviderObservation:
|
||||||
|
|
||||||
|
|
||||||
def _observe_one(
|
def _observe_one(
|
||||||
adapter: Callable[[str], str],
|
adapter: Callable[[str], object],
|
||||||
cfg: ProviderConfig,
|
cfg: ProviderConfig,
|
||||||
prompt: str,
|
prompt: str,
|
||||||
) -> ProviderObservation:
|
) -> ProviderObservation:
|
||||||
|
card = resolve_model_card(cfg.provider, cfg.model)
|
||||||
start = time.perf_counter()
|
start = time.perf_counter()
|
||||||
try:
|
try:
|
||||||
surface = adapter(prompt)
|
raw = adapter(prompt)
|
||||||
except Exception as exc: # noqa: BLE001 - record failure, never abort the suite
|
except Exception as exc: # noqa: BLE001 - record failure, never abort the suite
|
||||||
return ProviderObservation(
|
return ProviderObservation(
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
|
|
@ -121,17 +131,45 @@ def _observe_one(
|
||||||
error_message=str(exc),
|
error_message=str(exc),
|
||||||
)
|
)
|
||||||
elapsed_ms = (time.perf_counter() - start) * 1000.0
|
elapsed_ms = (time.perf_counter() - start) * 1000.0
|
||||||
|
surface = ""
|
||||||
|
input_tokens: int | None = None
|
||||||
|
output_tokens: int | None = None
|
||||||
|
total_tokens: int | None = None
|
||||||
|
if isinstance(raw, str):
|
||||||
|
surface = raw
|
||||||
|
else:
|
||||||
|
surface = str(getattr(raw, "surface", "") or "")
|
||||||
|
in_val = getattr(raw, "input_tokens", None)
|
||||||
|
out_val = getattr(raw, "output_tokens", None)
|
||||||
|
total_val = getattr(raw, "total_tokens", None)
|
||||||
|
input_tokens = int(in_val) if in_val is not None else None
|
||||||
|
output_tokens = int(out_val) if out_val is not None else None
|
||||||
|
total_tokens = int(total_val) if total_val is not None else None
|
||||||
|
estimated_cost_usd: float | None = None
|
||||||
|
if (
|
||||||
|
card is not None
|
||||||
|
and input_tokens is not None
|
||||||
|
and output_tokens is not None
|
||||||
|
):
|
||||||
|
estimated_cost_usd = card.estimate_cost_usd(
|
||||||
|
input_tokens=input_tokens,
|
||||||
|
output_tokens=output_tokens,
|
||||||
|
)
|
||||||
return ProviderObservation(
|
return ProviderObservation(
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
surface=surface,
|
surface=surface,
|
||||||
provider=cfg.provider,
|
provider=cfg.provider,
|
||||||
model=cfg.model,
|
model=cfg.model,
|
||||||
elapsed_ms=elapsed_ms,
|
elapsed_ms=elapsed_ms,
|
||||||
|
input_tokens=input_tokens,
|
||||||
|
output_tokens=output_tokens,
|
||||||
|
total_tokens=total_tokens,
|
||||||
|
estimated_cost_usd=estimated_cost_usd,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def run_prompt_battery(
|
def run_prompt_battery(
|
||||||
adapter: Callable[[str], str],
|
adapter: Callable[[str], object],
|
||||||
*,
|
*,
|
||||||
cfg: ProviderConfig,
|
cfg: ProviderConfig,
|
||||||
prompts: tuple[tuple[str, str], ...] = _PROMPT_BATTERY,
|
prompts: tuple[tuple[str, str], ...] = _PROMPT_BATTERY,
|
||||||
|
|
@ -175,7 +213,89 @@ def run_prompt_battery(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_replay_variability(
|
||||||
|
adapter: Callable[[str], object],
|
||||||
|
*,
|
||||||
|
cfg: ProviderConfig,
|
||||||
|
repeats: int = 3,
|
||||||
|
prompts: tuple[tuple[str, str], ...] = _PROMPT_BATTERY,
|
||||||
|
) -> SuiteReport:
|
||||||
|
"""Run repeated calls per prompt and score surface stability.
|
||||||
|
|
||||||
|
Score formula per case:
|
||||||
|
stability_score = 1 / unique_surface_count
|
||||||
|
where unique_surface_count >= 1 over `repeats` runs.
|
||||||
|
"""
|
||||||
|
runs = max(1, int(repeats))
|
||||||
|
cases: list[CaseResult] = []
|
||||||
|
for case_id, prompt in prompts:
|
||||||
|
observations = [_observe_one(adapter, cfg, prompt) for _ in range(runs)]
|
||||||
|
successful = [o for o in observations if not o.error_type]
|
||||||
|
failures: list[str] = []
|
||||||
|
if not successful:
|
||||||
|
failures.append("adapter_error")
|
||||||
|
unique_count = 0
|
||||||
|
score = 0.0
|
||||||
|
elapsed_ms = sum(o.elapsed_ms for o in observations)
|
||||||
|
details = {
|
||||||
|
"repeats": runs,
|
||||||
|
"observations": [o.as_dict() for o in observations],
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
surfaces = {
|
||||||
|
o.surface.strip()
|
||||||
|
for o in successful
|
||||||
|
if o.surface.strip()
|
||||||
|
}
|
||||||
|
if not surfaces:
|
||||||
|
failures.append("empty_surface")
|
||||||
|
unique_count = len(surfaces) if surfaces else 0
|
||||||
|
score = 0.0 if unique_count == 0 else (1.0 / float(unique_count))
|
||||||
|
if any(o.error_type for o in observations):
|
||||||
|
failures.append("partial_adapter_error")
|
||||||
|
elapsed_ms = sum(o.elapsed_ms for o in observations)
|
||||||
|
costs = [
|
||||||
|
float(o.estimated_cost_usd)
|
||||||
|
for o in successful
|
||||||
|
if o.estimated_cost_usd is not None
|
||||||
|
]
|
||||||
|
details = {
|
||||||
|
"repeats": runs,
|
||||||
|
"successful_runs": len(successful),
|
||||||
|
"unique_surface_count": unique_count,
|
||||||
|
"mean_elapsed_ms": (elapsed_ms / runs) if runs else 0.0,
|
||||||
|
"mean_estimated_cost_usd": (
|
||||||
|
(sum(costs) / len(costs))
|
||||||
|
if costs else None
|
||||||
|
),
|
||||||
|
"observations": [o.as_dict() for o in observations],
|
||||||
|
}
|
||||||
|
passed = not failures and unique_count == 1
|
||||||
|
cases.append(
|
||||||
|
CaseResult(
|
||||||
|
suite="replay_variability",
|
||||||
|
case_id=case_id,
|
||||||
|
prompt=prompt,
|
||||||
|
passed=passed,
|
||||||
|
score=score,
|
||||||
|
elapsed_ms=elapsed_ms,
|
||||||
|
details=details,
|
||||||
|
failures=tuple(failures),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
primary = (
|
||||||
|
sum(c.score for c in cases) / len(cases) if cases else 0.0
|
||||||
|
)
|
||||||
|
return SuiteReport(
|
||||||
|
suite="replay_variability",
|
||||||
|
cases=tuple(cases),
|
||||||
|
primary_score=primary,
|
||||||
|
passed=all(c.passed for c in cases),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"ProviderObservation",
|
"ProviderObservation",
|
||||||
"run_prompt_battery",
|
"run_prompt_battery",
|
||||||
|
"run_replay_variability",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,15 @@ class ModelCard:
|
||||||
notes: str = ""
|
notes: str = ""
|
||||||
"""Free-form notes: known quirks, benchmark-specific observations, version history."""
|
"""Free-form notes: known quirks, benchmark-specific observations, version history."""
|
||||||
|
|
||||||
|
input_usd_per_million_tokens: float | None = None
|
||||||
|
"""Public list price for input tokens in USD per 1M tokens."""
|
||||||
|
|
||||||
|
output_usd_per_million_tokens: float | None = None
|
||||||
|
"""Public list price for output tokens in USD per 1M tokens."""
|
||||||
|
|
||||||
|
pricing_source: str = ""
|
||||||
|
"""Source URL/note for pricing metadata."""
|
||||||
|
|
||||||
tags: tuple[str, ...] = field(default_factory=tuple)
|
tags: tuple[str, ...] = field(default_factory=tuple)
|
||||||
"""Searchable tags, e.g. ('reasoning', 'code', 'vision')."""
|
"""Searchable tags, e.g. ('reasoning', 'code', 'vision')."""
|
||||||
|
|
||||||
|
|
@ -69,6 +78,35 @@ class ModelCard:
|
||||||
d["tags"] = list(self.tags)
|
d["tags"] = list(self.tags)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_pricing(self) -> bool:
|
||||||
|
return (
|
||||||
|
self.input_usd_per_million_tokens is not None
|
||||||
|
and self.output_usd_per_million_tokens is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
def estimate_cost_usd(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
input_tokens: int | float,
|
||||||
|
output_tokens: int | float,
|
||||||
|
) -> float | None:
|
||||||
|
"""Compute provider list-price cost for one request.
|
||||||
|
|
||||||
|
Formula:
|
||||||
|
cost_usd =
|
||||||
|
(input_tokens / 1_000_000) * input_usd_per_million_tokens +
|
||||||
|
(output_tokens / 1_000_000) * output_usd_per_million_tokens
|
||||||
|
"""
|
||||||
|
if not self.has_pricing:
|
||||||
|
return None
|
||||||
|
in_rate = float(self.input_usd_per_million_tokens or 0.0)
|
||||||
|
out_rate = float(self.output_usd_per_million_tokens or 0.0)
|
||||||
|
return (
|
||||||
|
(float(input_tokens) / 1_000_000.0) * in_rate
|
||||||
|
+ (float(output_tokens) / 1_000_000.0) * out_rate
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Registry
|
# Registry
|
||||||
|
|
@ -107,6 +145,9 @@ _REGISTRY: dict[str, ModelCard] = {
|
||||||
"Use a dated snapshot (e.g. gpt-4o-2024-08-06) for reproducible benchmarks. "
|
"Use a dated snapshot (e.g. gpt-4o-2024-08-06) for reproducible benchmarks. "
|
||||||
"Set OPENAI_MODEL=gpt-4o-2024-08-06 in .env."
|
"Set OPENAI_MODEL=gpt-4o-2024-08-06 in .env."
|
||||||
),
|
),
|
||||||
|
input_usd_per_million_tokens=2.50,
|
||||||
|
output_usd_per_million_tokens=10.00,
|
||||||
|
pricing_source="https://openai.com/api/pricing (captured 2026-05-20)",
|
||||||
tags=("frontier", "multimodal", "reasoning", "code"),
|
tags=("frontier", "multimodal", "reasoning", "code"),
|
||||||
),
|
),
|
||||||
"openai/gpt-4o-2024-08-06": ModelCard(
|
"openai/gpt-4o-2024-08-06": ModelCard(
|
||||||
|
|
@ -119,6 +160,9 @@ _REGISTRY: dict[str, ModelCard] = {
|
||||||
architecture="GPT-4 class transformer, multimodal (text + vision).",
|
architecture="GPT-4 class transformer, multimodal (text + vision).",
|
||||||
sampling="Stochastic at T>0. T=0 is near-deterministic for a fixed snapshot but backend routing can still vary.",
|
sampling="Stochastic at T>0. T=0 is near-deterministic for a fixed snapshot but backend routing can still vary.",
|
||||||
notes="Pinned snapshot. Preferred for reproducible benchmark comparisons.",
|
notes="Pinned snapshot. Preferred for reproducible benchmark comparisons.",
|
||||||
|
input_usd_per_million_tokens=2.50,
|
||||||
|
output_usd_per_million_tokens=10.00,
|
||||||
|
pricing_source="https://openai.com/api/pricing (captured 2026-05-20)",
|
||||||
tags=("frontier", "multimodal", "reasoning", "code", "pinned"),
|
tags=("frontier", "multimodal", "reasoning", "code", "pinned"),
|
||||||
),
|
),
|
||||||
"openai/gpt-4o-mini": ModelCard(
|
"openai/gpt-4o-mini": ModelCard(
|
||||||
|
|
@ -131,6 +175,9 @@ _REGISTRY: dict[str, ModelCard] = {
|
||||||
architecture="Smaller GPT-4o class model optimised for latency and cost.",
|
architecture="Smaller GPT-4o class model optimised for latency and cost.",
|
||||||
sampling="Stochastic at T>0.",
|
sampling="Stochastic at T>0.",
|
||||||
notes="Useful for cost/latency baseline comparisons in the benchmark cost suite.",
|
notes="Useful for cost/latency baseline comparisons in the benchmark cost suite.",
|
||||||
|
input_usd_per_million_tokens=0.15,
|
||||||
|
output_usd_per_million_tokens=0.60,
|
||||||
|
pricing_source="https://openai.com/api/pricing (captured 2026-05-20)",
|
||||||
tags=("frontier", "fast", "cost-efficient"),
|
tags=("frontier", "fast", "cost-efficient"),
|
||||||
),
|
),
|
||||||
"openai/o3": ModelCard(
|
"openai/o3": ModelCard(
|
||||||
|
|
@ -146,6 +193,7 @@ _REGISTRY: dict[str, ModelCard] = {
|
||||||
"o-series models use a reasoning_effort parameter instead of temperature. "
|
"o-series models use a reasoning_effort parameter instead of temperature. "
|
||||||
"Pass via ProviderConfig.extra = {'reasoning_effort': 'high'} for benchmark use."
|
"Pass via ProviderConfig.extra = {'reasoning_effort': 'high'} for benchmark use."
|
||||||
),
|
),
|
||||||
|
pricing_source="https://openai.com/api/pricing (captured 2026-05-20)",
|
||||||
tags=("frontier", "reasoning", "chain-of-thought"),
|
tags=("frontier", "reasoning", "chain-of-thought"),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
@ -164,6 +212,9 @@ _REGISTRY: dict[str, ModelCard] = {
|
||||||
"Default ANTHROPIC_MODEL in .env.example. "
|
"Default ANTHROPIC_MODEL in .env.example. "
|
||||||
"For extended thinking benchmarks, pass extra={'thinking': {'type': 'enabled', 'budget_tokens': 10000}}."
|
"For extended thinking benchmarks, pass extra={'thinking': {'type': 'enabled', 'budget_tokens': 10000}}."
|
||||||
),
|
),
|
||||||
|
input_usd_per_million_tokens=15.00,
|
||||||
|
output_usd_per_million_tokens=75.00,
|
||||||
|
pricing_source="https://www.anthropic.com/pricing (captured 2026-05-20)",
|
||||||
tags=("frontier", "reasoning", "code", "extended-thinking"),
|
tags=("frontier", "reasoning", "code", "extended-thinking"),
|
||||||
),
|
),
|
||||||
"anthropic/claude-sonnet-4-5": ModelCard(
|
"anthropic/claude-sonnet-4-5": ModelCard(
|
||||||
|
|
@ -176,6 +227,9 @@ _REGISTRY: dict[str, ModelCard] = {
|
||||||
architecture="Claude 4 class transformer (Anthropic). Balanced speed/capability.",
|
architecture="Claude 4 class transformer (Anthropic). Balanced speed/capability.",
|
||||||
sampling="Stochastic at T>0.",
|
sampling="Stochastic at T>0.",
|
||||||
notes="Good default for high-volume benchmark sweeps where Opus cost is prohibitive.",
|
notes="Good default for high-volume benchmark sweeps where Opus cost is prohibitive.",
|
||||||
|
input_usd_per_million_tokens=3.00,
|
||||||
|
output_usd_per_million_tokens=15.00,
|
||||||
|
pricing_source="https://www.anthropic.com/pricing (captured 2026-05-20)",
|
||||||
tags=("frontier", "balanced", "cost-efficient"),
|
tags=("frontier", "balanced", "cost-efficient"),
|
||||||
),
|
),
|
||||||
"anthropic/claude-haiku-3-5": ModelCard(
|
"anthropic/claude-haiku-3-5": ModelCard(
|
||||||
|
|
@ -188,6 +242,9 @@ _REGISTRY: dict[str, ModelCard] = {
|
||||||
architecture="Claude 3 class transformer (Anthropic). Optimised for latency.",
|
architecture="Claude 3 class transformer (Anthropic). Optimised for latency.",
|
||||||
sampling="Stochastic at T>0.",
|
sampling="Stochastic at T>0.",
|
||||||
notes="Useful for latency/cost baseline comparisons. Lower capability ceiling than Sonnet/Opus.",
|
notes="Useful for latency/cost baseline comparisons. Lower capability ceiling than Sonnet/Opus.",
|
||||||
|
input_usd_per_million_tokens=0.80,
|
||||||
|
output_usd_per_million_tokens=4.00,
|
||||||
|
pricing_source="https://www.anthropic.com/pricing (captured 2026-05-20)",
|
||||||
tags=("frontier", "fast", "cost-efficient"),
|
tags=("frontier", "fast", "cost-efficient"),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -164,23 +164,41 @@ class ProviderConfig:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class AdapterResponse:
|
||||||
|
"""Unified provider response payload for benchmark telemetry."""
|
||||||
|
|
||||||
|
surface: str
|
||||||
|
input_tokens: int | None = None
|
||||||
|
output_tokens: int | None = None
|
||||||
|
total_tokens: int | None = None
|
||||||
|
|
||||||
|
def as_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
"surface": self.surface,
|
||||||
|
"input_tokens": self.input_tokens,
|
||||||
|
"output_tokens": self.output_tokens,
|
||||||
|
"total_tokens": self.total_tokens,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Adapter builders
|
# Adapter builders
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _build_core_adapter(cfg: ProviderConfig) -> Callable[[str], str]:
|
def _build_core_adapter(cfg: ProviderConfig) -> Callable[[str], AdapterResponse]:
|
||||||
"""CORE ChatRuntime adapter. Fresh runtime per call — no session bleed."""
|
"""CORE ChatRuntime adapter. Fresh runtime per call — no session bleed."""
|
||||||
from chat.runtime import ChatRuntime
|
from chat.runtime import ChatRuntime
|
||||||
|
|
||||||
def adapter(prompt: str) -> str:
|
def adapter(prompt: str) -> AdapterResponse:
|
||||||
rt = ChatRuntime()
|
rt = ChatRuntime()
|
||||||
resp = rt.chat(prompt, max_tokens=cfg.max_tokens)
|
resp = rt.chat(prompt, max_tokens=cfg.max_tokens)
|
||||||
return resp.surface or ""
|
return AdapterResponse(surface=resp.surface or "")
|
||||||
|
|
||||||
return adapter
|
return adapter
|
||||||
|
|
||||||
|
|
||||||
def _build_openai_adapter(cfg: ProviderConfig) -> Callable[[str], str]:
|
def _build_openai_adapter(cfg: ProviderConfig) -> Callable[[str], AdapterResponse]:
|
||||||
"""OpenAI Chat Completions adapter.
|
"""OpenAI Chat Completions adapter.
|
||||||
|
|
||||||
Requires: ``pip install openai``
|
Requires: ``pip install openai``
|
||||||
|
|
@ -200,19 +218,28 @@ def _build_openai_adapter(cfg: ProviderConfig) -> Callable[[str], str]:
|
||||||
client_kwargs["base_url"] = cfg.extra["base_url"]
|
client_kwargs["base_url"] = cfg.extra["base_url"]
|
||||||
client = openai.OpenAI(**client_kwargs)
|
client = openai.OpenAI(**client_kwargs)
|
||||||
|
|
||||||
def adapter(prompt: str) -> str:
|
def adapter(prompt: str) -> AdapterResponse:
|
||||||
response = client.chat.completions.create(
|
response = client.chat.completions.create(
|
||||||
model=cfg.model,
|
model=cfg.model,
|
||||||
messages=[{"role": "user", "content": prompt}],
|
messages=[{"role": "user", "content": prompt}],
|
||||||
temperature=cfg.temperature,
|
temperature=cfg.temperature,
|
||||||
max_tokens=cfg.max_tokens,
|
max_tokens=cfg.max_tokens,
|
||||||
)
|
)
|
||||||
return (response.choices[0].message.content or "").strip()
|
usage = getattr(response, "usage", None)
|
||||||
|
input_tokens = int(getattr(usage, "prompt_tokens", 0)) if usage else None
|
||||||
|
output_tokens = int(getattr(usage, "completion_tokens", 0)) if usage else None
|
||||||
|
total_tokens = int(getattr(usage, "total_tokens", 0)) if usage else None
|
||||||
|
return AdapterResponse(
|
||||||
|
surface=(response.choices[0].message.content or "").strip(),
|
||||||
|
input_tokens=input_tokens,
|
||||||
|
output_tokens=output_tokens,
|
||||||
|
total_tokens=total_tokens,
|
||||||
|
)
|
||||||
|
|
||||||
return adapter
|
return adapter
|
||||||
|
|
||||||
|
|
||||||
def _build_anthropic_adapter(cfg: ProviderConfig) -> Callable[[str], str]:
|
def _build_anthropic_adapter(cfg: ProviderConfig) -> Callable[[str], AdapterResponse]:
|
||||||
"""Anthropic Messages adapter.
|
"""Anthropic Messages adapter.
|
||||||
|
|
||||||
Requires: ``pip install anthropic``
|
Requires: ``pip install anthropic``
|
||||||
|
|
@ -229,7 +256,7 @@ def _build_anthropic_adapter(cfg: ProviderConfig) -> Callable[[str], str]:
|
||||||
api_key = _require_env("ANTHROPIC_API_KEY")
|
api_key = _require_env("ANTHROPIC_API_KEY")
|
||||||
client = ant.Anthropic(api_key=api_key)
|
client = ant.Anthropic(api_key=api_key)
|
||||||
|
|
||||||
def adapter(prompt: str) -> str:
|
def adapter(prompt: str) -> AdapterResponse:
|
||||||
message = client.messages.create(
|
message = client.messages.create(
|
||||||
model=cfg.model,
|
model=cfg.model,
|
||||||
max_tokens=cfg.max_tokens,
|
max_tokens=cfg.max_tokens,
|
||||||
|
|
@ -239,12 +266,25 @@ def _build_anthropic_adapter(cfg: ProviderConfig) -> Callable[[str], str]:
|
||||||
temperature=max(0.0, min(1.0, cfg.temperature)),
|
temperature=max(0.0, min(1.0, cfg.temperature)),
|
||||||
)
|
)
|
||||||
block = message.content[0] if message.content else None
|
block = message.content[0] if message.content else None
|
||||||
return (getattr(block, "text", "") or "").strip()
|
usage = getattr(message, "usage", None)
|
||||||
|
input_tokens = int(getattr(usage, "input_tokens", 0)) if usage else None
|
||||||
|
output_tokens = int(getattr(usage, "output_tokens", 0)) if usage else None
|
||||||
|
total_tokens = (
|
||||||
|
(input_tokens or 0) + (output_tokens or 0)
|
||||||
|
if (input_tokens is not None or output_tokens is not None)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
return AdapterResponse(
|
||||||
|
surface=(getattr(block, "text", "") or "").strip(),
|
||||||
|
input_tokens=input_tokens,
|
||||||
|
output_tokens=output_tokens,
|
||||||
|
total_tokens=total_tokens,
|
||||||
|
)
|
||||||
|
|
||||||
return adapter
|
return adapter
|
||||||
|
|
||||||
|
|
||||||
def _build_ollama_adapter(cfg: ProviderConfig) -> Callable[[str], str]:
|
def _build_ollama_adapter(cfg: ProviderConfig) -> Callable[[str], AdapterResponse]:
|
||||||
"""Ollama local model adapter (HTTP /api/chat endpoint).
|
"""Ollama local model adapter (HTTP /api/chat endpoint).
|
||||||
|
|
||||||
Requires: ``pip install httpx`` (already present in most Python envs)
|
Requires: ``pip install httpx`` (already present in most Python envs)
|
||||||
|
|
@ -267,7 +307,7 @@ def _build_ollama_adapter(cfg: ProviderConfig) -> Callable[[str], str]:
|
||||||
if api_key:
|
if api_key:
|
||||||
headers["Authorization"] = f"Bearer {api_key}"
|
headers["Authorization"] = f"Bearer {api_key}"
|
||||||
|
|
||||||
def adapter(prompt: str) -> str:
|
def adapter(prompt: str) -> AdapterResponse:
|
||||||
payload = {
|
payload = {
|
||||||
"model": cfg.model,
|
"model": cfg.model,
|
||||||
"messages": [{"role": "user", "content": prompt}],
|
"messages": [{"role": "user", "content": prompt}],
|
||||||
|
|
@ -282,7 +322,19 @@ def _build_ollama_adapter(cfg: ProviderConfig) -> Callable[[str], str]:
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
return (data.get("message", {}).get("content") or "").strip()
|
input_tokens = data.get("prompt_eval_count")
|
||||||
|
output_tokens = data.get("eval_count")
|
||||||
|
total_tokens = (
|
||||||
|
int(input_tokens or 0) + int(output_tokens or 0)
|
||||||
|
if input_tokens is not None or output_tokens is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
return AdapterResponse(
|
||||||
|
surface=(data.get("message", {}).get("content") or "").strip(),
|
||||||
|
input_tokens=int(input_tokens) if input_tokens is not None else None,
|
||||||
|
output_tokens=int(output_tokens) if output_tokens is not None else None,
|
||||||
|
total_tokens=total_tokens,
|
||||||
|
)
|
||||||
|
|
||||||
return adapter
|
return adapter
|
||||||
|
|
||||||
|
|
@ -307,6 +359,16 @@ def build_adapter(cfg: ProviderConfig) -> Callable[[str], str]:
|
||||||
it is safe to pass to ``compare_to_llm`` and the frontier_compare
|
it is safe to pass to ``compare_to_llm`` and the frontier_compare
|
||||||
runner suites.
|
runner suites.
|
||||||
"""
|
"""
|
||||||
|
observing = build_observing_adapter(cfg)
|
||||||
|
|
||||||
|
def _surface_only(prompt: str) -> str:
|
||||||
|
return observing(prompt).surface
|
||||||
|
|
||||||
|
return _surface_only
|
||||||
|
|
||||||
|
|
||||||
|
def build_observing_adapter(cfg: ProviderConfig) -> Callable[[str], AdapterResponse]:
|
||||||
|
"""Return a provider adapter that includes usage telemetry when available."""
|
||||||
builder = _BUILDERS.get(cfg.provider)
|
builder = _BUILDERS.get(cfg.provider)
|
||||||
if builder is None:
|
if builder is None:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,28 @@ def test_cli_prompt_battery_with_core_provider(tmp_path: Path, capsys) -> None:
|
||||||
assert report_path.exists()
|
assert report_path.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_replay_variability_with_core_provider(tmp_path: Path, capsys) -> None:
|
||||||
|
"""Replay-variability suite should run through cross-provider path."""
|
||||||
|
report_path = tmp_path / "cross_core_replay.json"
|
||||||
|
code = main(
|
||||||
|
[
|
||||||
|
"--provider", "core",
|
||||||
|
"--suite", "replay_variability",
|
||||||
|
"--repeats", "2",
|
||||||
|
"--json",
|
||||||
|
"--report", str(report_path),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
payload = json.loads(out)
|
||||||
|
assert payload["model"] == "core-native"
|
||||||
|
assert payload["mode"] == "core"
|
||||||
|
assert len(payload["suites"]) == 1
|
||||||
|
assert payload["suites"][0]["suite"] == "replay_variability"
|
||||||
|
assert code == 0
|
||||||
|
assert report_path.exists()
|
||||||
|
|
||||||
|
|
||||||
def test_cli_rejects_core_only_suite_with_non_core_provider(capsys) -> None:
|
def test_cli_rejects_core_only_suite_with_non_core_provider(capsys) -> None:
|
||||||
"""Loud failure when an operator asks for a CORE-only suite with a
|
"""Loud failure when an operator asks for a CORE-only suite with a
|
||||||
non-CORE provider — no silent telemetry degradation."""
|
non-CORE provider — no silent telemetry degradation."""
|
||||||
|
|
|
||||||
37
tests/test_frontier_model_registry_pricing.py
Normal file
37
tests/test_frontier_model_registry_pricing.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from evals.frontier_compare.cross_provider import run_prompt_battery
|
||||||
|
from evals.frontier_compare.model_registry import require_model_card
|
||||||
|
from evals.frontier_compare.providers import AdapterResponse, ProviderConfig
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_card_estimate_cost_formula() -> None:
|
||||||
|
card = require_model_card("openai", "gpt-4o-2024-08-06")
|
||||||
|
cost = card.estimate_cost_usd(input_tokens=2000, output_tokens=1000)
|
||||||
|
assert cost is not None
|
||||||
|
# (2000 / 1e6 * 2.5) + (1000 / 1e6 * 10.0) = 0.015
|
||||||
|
assert round(cost, 6) == 0.015
|
||||||
|
|
||||||
|
|
||||||
|
def test_prompt_battery_records_usage_and_cost_when_available() -> None:
|
||||||
|
cfg = ProviderConfig(provider="openai", model="gpt-4o-2024-08-06")
|
||||||
|
|
||||||
|
def fake_adapter(_: str) -> AdapterResponse:
|
||||||
|
return AdapterResponse(
|
||||||
|
surface="ok",
|
||||||
|
input_tokens=120,
|
||||||
|
output_tokens=80,
|
||||||
|
total_tokens=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
report = run_prompt_battery(
|
||||||
|
fake_adapter,
|
||||||
|
cfg=cfg,
|
||||||
|
prompts=(("case", "What is truth?"),),
|
||||||
|
)
|
||||||
|
obs = report.cases[0].details["observation"]
|
||||||
|
assert obs["input_tokens"] == 120
|
||||||
|
assert obs["output_tokens"] == 80
|
||||||
|
assert obs["total_tokens"] == 200
|
||||||
|
assert obs["estimated_cost_usd"] is not None
|
||||||
|
assert obs["estimated_cost_usd"] > 0
|
||||||
Loading…
Reference in a new issue