core/evals/frontier_compare/cross_provider.py
Shay 9459f815b0
feat(evals): wire ADR-0082 providers into frontier_compare runner (#61)
#58 shipped providers.py + model_registry.py for cross-provider
benchmarking but never connected them to runner.py — the adapters
sat unused.  This PR wires them through with a clear lane split.

Why a new suite instead of refactoring existing ones
-----------------------------------------------------
The three existing suites (determinism / truth_lock / axis_orthogonality)
pull CORE-only telemetry: trace_hash, versor_condition, register_id,
register_variant_id, anchor_lens_id, register_canonical_surface.
None of those fields can come from OpenAI / Anthropic / Ollama.

Forcing those suites cross-provider would silently produce reports
where the cross-provider rows have empty telemetry — a worse failure
mode than not running them at all.  So the routing is explicit:

  CORE-only suites          → --provider must be 'core'
  Cross-provider suites     → any provider; CORE is one adapter among many

Operator asks for the wrong combo → loud error with the right alternative.

New module: evals/frontier_compare/cross_provider.py
-----------------------------------------------------
- ProviderObservation dataclass — provider-agnostic observation shape
  (prompt, surface, provider, model, elapsed_ms, error fields).  No
  CORE-internal telemetry expected.
- run_prompt_battery(adapter, *, cfg) → SuiteReport reusing existing
  CaseResult / SuiteReport shapes so the report viewer renders both
  lanes without schema branching.
- _PROMPT_BATTERY: 7 fixed cases spanning definition / cause /
  verification / comparison / procedure / unknown intent shapes.
  Stable case_ids so future re-runs against the same provider produce
  diffable JSON.
- Per-case 'passed' is loose by design (non-empty surface, no
  exception).  Cross-provider quality is for human review — not for
  the runner to silently score.

Updated CLI: evals/frontier_compare/__main__.py
-----------------------------------------------
- --provider {core, openai, anthropic, ollama}    (default: core)
- --model <id>                                     (validated via require_model_card)
- --env-file <path>                                (default: ./.env)
- Auto-persist non-CORE runs to
  evals/frontier_compare/results/<provider>_<model>_<utc>.json
  even when --report is omitted.  API calls are rate-limited / paid;
  losing the artifact is costly.
- Existing CORE-native behavior unchanged when --provider not set.

Results directory: evals/frontier_compare/results/
--------------------------------------------------
Created with .gitkeep — matches the convention used by other lanes
(evals/long_context_cost/results/, evals/koine_greek_fluency/results/,
etc.).  Distinct from reports/ which .gitignore excludes for
transient debug output.

Tests: tests/test_frontier_compare_cross_provider.py (9 cases)
--------------------------------------------------------------
- prompt_battery runs with CORE adapter (no API needed)
- adapter exceptions recorded as failed observations, never propagated
- empty surfaces flagged distinctly from adapter errors
- CLI default runs CORE-native (no breaking change)
- CLI prompt_battery with --provider core routes through cross-provider path
- CLI rejects CORE-only suite + non-CORE provider with operator-helpful error
- --help surfaces both suite families
- unregistered model is rejected before any benchmark cycles burn
- ProviderObservation.succeeded handles error / empty / whitespace cases

Live evidence
-------------
$ core test --suite smoke -q
67 passed in 26.55s   (no regression)

$ python -m evals.frontier_compare --provider core --suite prompt_battery --json
model=core-native mode=core suite=prompt_battery passed=True score=1.000
  [definition_truth              ] PASS  Truth is a claim or state grounded by evidence...
  [definition_knowledge          ] PASS  Knowledge is justified understanding grounded...
  [cause_understanding           ] PASS  understanding — teaching-grounded (cognition_chains_v1)...
  [verification_evidence         ] PASS  evidence — teaching-grounded (cognition_chains_v1)...
  [comparison_knowledge_wisdom   ] PASS  knowledge contrasts with wisdom...
  [procedure_recall              ] PASS  To recall means to retrieve a stored state from memory...
  [unknown_term                  ] PASS  I haven't learned 'xylomorphic' yet...

$ python -m evals.frontier_compare --provider openai --suite determinism
error: suite 'determinism' is CORE-only; pass --suite prompt_battery
(the cross-provider suite) when --provider='openai'.

.gitignore: adds frontier_wave1.json (stray report file repeatedly
written by ad-hoc test invocations).
2026-05-20 13:22:37 -07:00

181 lines
5.8 KiB
Python

"""Cross-provider benchmark suite for frontier_compare.
The existing suites (``determinism``, ``truth_lock``,
``axis_orthogonality``) pull CORE-only telemetry (``trace_hash``,
``versor_condition``, ``register_id``, ``anchor_lens_id``) — they
cannot run cross-provider as-is.
This module is the cross-provider lane:
- Pure ``(prompt) -> str`` over any provider adapter.
- No CORE-internal telemetry expected.
- Per-case ``passed`` is loose by design — non-empty surface within
the elapsed-ms budget — because we are not in a position to
semantically judge GPT-4o vs Claude vs CORE here. The point of
the suite is **operator-visible, side-by-side surface evidence**
across providers on a fixed prompt battery, not automatic
quality scoring.
The prompt battery (``_PROMPT_BATTERY``) is the load-bearing data —
edit it to expand cross-provider coverage. Each entry pairs a
``case_id`` (stable across runs, used by reviewers to diff results)
with the prompt itself.
Trust boundary
--------------
- Read-only. Never writes packs, vault, or runtime state.
- Provider adapters are constructed via
``evals.frontier_compare.providers.build_adapter`` and made of a
single ``(prompt) -> str`` callable each.
- Per-call exceptions are recorded as case failures, never propagated.
"""
from __future__ import annotations
import time
from dataclasses import dataclass
from typing import Callable
from .providers import ProviderConfig
from .runner import CaseResult, SuiteReport
# ---------------------------------------------------------------------------
# Prompt battery
# ---------------------------------------------------------------------------
# Stable case_ids so a future re-run on the same provider produces
# diffable JSON. Prompts span definitional / causal / verification /
# comparison / procedural / unknown shapes — enough to surface obvious
# provider behavior differences without ballooning credit cost.
_PROMPT_BATTERY: tuple[tuple[str, str], ...] = (
("definition_truth", "What is truth?"),
("definition_knowledge", "What is knowledge?"),
("cause_understanding", "What causes understanding?"),
("verification_evidence", "Does evidence ground knowledge?"),
("comparison_knowledge_wisdom", "Compare knowledge and wisdom."),
("procedure_recall", "Walk me through recall."),
("unknown_term", "What is xylomorphic?"),
)
# ---------------------------------------------------------------------------
# Observation shape — provider-agnostic
# ---------------------------------------------------------------------------
@dataclass(frozen=True, slots=True)
class ProviderObservation:
"""One ``(prompt, provider) -> surface`` observation.
Cross-provider sibling of :class:`runner.RuntimeObservation`.
Carries only fields any provider can supply; CORE-only telemetry
deliberately omitted.
"""
prompt: str
surface: str
provider: str
model: str
elapsed_ms: float
error_type: str = ""
error_message: str = ""
@property
def succeeded(self) -> bool:
return not self.error_type and bool(self.surface.strip())
def as_dict(self) -> dict:
return {
"prompt": self.prompt,
"surface": self.surface,
"provider": self.provider,
"model": self.model,
"elapsed_ms": self.elapsed_ms,
"error_type": self.error_type,
"error_message": self.error_message,
}
# ---------------------------------------------------------------------------
# Suite runner
# ---------------------------------------------------------------------------
def _observe_one(
adapter: Callable[[str], str],
cfg: ProviderConfig,
prompt: str,
) -> ProviderObservation:
start = time.perf_counter()
try:
surface = adapter(prompt)
except Exception as exc: # noqa: BLE001 - record failure, never abort the suite
return ProviderObservation(
prompt=prompt,
surface="",
provider=cfg.provider,
model=cfg.model,
elapsed_ms=(time.perf_counter() - start) * 1000.0,
error_type=exc.__class__.__name__,
error_message=str(exc),
)
elapsed_ms = (time.perf_counter() - start) * 1000.0
return ProviderObservation(
prompt=prompt,
surface=surface,
provider=cfg.provider,
model=cfg.model,
elapsed_ms=elapsed_ms,
)
def run_prompt_battery(
adapter: Callable[[str], str],
*,
cfg: ProviderConfig,
prompts: tuple[tuple[str, str], ...] = _PROMPT_BATTERY,
) -> SuiteReport:
"""Run the cross-provider prompt battery against one adapter.
Per-case ``passed`` is loose by design (non-empty surface, no
exception). Reviewers should diff ``details.observation.surface``
side-by-side rather than rely on the boolean.
"""
cases: list[CaseResult] = []
for case_id, prompt in prompts:
obs = _observe_one(adapter, cfg, prompt)
passed = obs.succeeded
score = 1.0 if passed else 0.0
failures: tuple[str, ...] = ()
if not passed:
failures = (
("adapter_error",) if obs.error_type else ("empty_surface",)
)
cases.append(
CaseResult(
suite="prompt_battery",
case_id=case_id,
prompt=prompt,
passed=passed,
score=score,
elapsed_ms=obs.elapsed_ms,
details={"observation": obs.as_dict()},
failures=failures,
)
)
primary = (
sum(c.score for c in cases) / len(cases) if cases else 0.0
)
return SuiteReport(
suite="prompt_battery",
cases=tuple(cases),
primary_score=primary,
passed=all(c.passed for c in cases),
)
__all__ = [
"ProviderObservation",
"run_prompt_battery",
]