Resolves a same-day numbering collision: the prior session produced
ADR-0080 + ADR-0081 (geometric stress field, falsified) in
docs/decisions/ while the frontier-provider-adapters work was
authored as ADR-0081 in a newly-created docs/adr/ directory,
unaware of the concurrent track.
This commit takes the minimum-blast-radius fix:
- docs/adr/ADR-0081-...md → docs/adr/ADR-0082-...md
- Update title header to ADR-0082, add "Renumbered from" breadcrumb
- Update the two source-file docstrings that cite the ADR number
(providers.py, model_registry.py)
The "two ADR directories" question (docs/adr/ vs docs/decisions/)
is NOT resolved here — docs/adr/ now has exactly one entry, while
docs/decisions/ is the canonical location per CLAUDE.md. A future
PR should either consolidate or document the split; this commit
just unblocks the immediate naming collision.
Out of scope:
- Consolidating directories
- Renumbering anything in docs/decisions/
- Re-numbering on the dev's local main (already pulled into this branch)
6.4 KiB
ADR-0082 — Frontier Provider Adapters
Status: Ratified
Date: 2026-05-20
Author: Shay
Renumbered from: ADR-0081 (collided with a parallel docs/decisions/ track; this docs/adr/ track is renumbered to 0082 to keep both sequences monotonic).
Context
Wave 1 of the frontier comparison benchmark family (evals/frontier_compare/) was deliberately local and CORE-native — it required no API keys and made no external calls. The README called out feat/frontier-compare-provider-adapters as the intended next wave.
We now have API credentials for OpenAI, Anthropic, and a local Ollama install. To run the Wave 1 suites (and future suites) against real frontier models, we need:
- A consistent interface so any benchmark can call any provider with the same
(prompt: str) -> strshape. - A
.env.examplethat documents every env var the codebase consumes so a new operator knows exactly what to set. - A model registry that enforces documented, pinned model identity in every benchmark report — floating aliases like
gpt-4oare banned in report metadata because the underlying weights can change silently. .gitignorecoverage for.envfiles so secrets are never committed.
Decision
Provider adapter module: evals/frontier_compare/providers.py
A single module exposes:
ProviderConfig— immutable, hashable dataclass carrying(provider, model, temperature, max_tokens, extra). Built from env vars viaProviderConfig.from_env(provider_name). Serialises to a stable dict for embedding in report JSON.build_adapter(cfg: ProviderConfig) -> Callable[[str], str]— the single entry point for all benchmark code. Returns a stateless(prompt) -> surfacecallable. Registered builders:core,openai,anthropic,ollama.load_dotenv_if_present(path)— minimal.envreader with no external dependency. Shell exports always win over.envvalues.
Adapter behaviour per provider:
| Provider | Package required | Auth env var | Notes |
|---|---|---|---|
core |
none | none | Fresh ChatRuntime per call; deterministic |
openai |
openai |
OPENAI_API_KEY |
Uses client.chat.completions.create |
anthropic |
anthropic |
ANTHROPIC_API_KEY |
Uses client.messages.create |
ollama |
httpx |
OLLAMA_API_KEY (optional) |
POSTs to {OLLAMA_URL}/api/chat |
Model registry: evals/frontier_compare/model_registry.py
Every model ever used in a CORE benchmark must have a ModelCard entry before it can be used via require_model_card(). Fields:
provider,model_id(exact API slug)display_name,knowledge_cutoff,context_window,output_tokensarchitecture— plain-English descriptionsampling— note on determinism/stochasticitynotes— known quirks, version history, benchmark-specific observationstags— searchable taxonomy
Initially registered models:
| Key | Display name |
|---|---|
core/core-native |
CORE (native) |
openai/gpt-4o |
GPT-4o (floating alias) |
openai/gpt-4o-2024-08-06 |
GPT-4o (2024-08-06 snapshot) |
openai/gpt-4o-mini |
GPT-4o mini |
openai/o3 |
OpenAI o3 |
anthropic/claude-opus-4-5 |
Claude Opus 4.5 |
anthropic/claude-sonnet-4-5 |
Claude Sonnet 4.5 |
anthropic/claude-haiku-3-5 |
Claude Haiku 3.5 |
ollama/llama3.2 |
Llama 3.2 (latest tag) |
ollama/llama3.2:3b-instruct-q8_0 |
Llama 3.2 3B Instruct Q8_0 |
ollama/mistral |
Mistral 7B |
ollama/gemma3:12b |
Gemma 3 12B |
.env.example
Documents all env vars in one place:
OPENAI_API_KEY=
OPENAI_MODEL=gpt-4o
OPENAI_BASE_URL= # optional proxy/Azure override
ANTHROPIC_API_KEY=
ANTHROPIC_MODEL=claude-opus-4-5
OLLAMA_URL=http://localhost:11434
OLLAMA_API_KEY= # empty for local installs
OLLAMA_MODEL=llama3.2
BENCHMARK_PROVIDERS=core # comma-separated: core,openai,anthropic,ollama
BENCHMARK_RUNS=3
BENCHMARK_TEMPERATURE=0
BENCHMARK_REPORT_DIR=reports
.gitignore additions
.env
.env.local
.env.*.local
reports/
How to run a multi-provider benchmark
from evals.frontier_compare.providers import load_dotenv_if_present, ProviderConfig, build_adapter
from evals.frontier_compare.model_registry import require_model_card
from evals.frontier_compare.runner import run_all
from benchmarks.replay_vs_llm import compare_to_llm, DEFAULT_LONGFORM_PROMPTS
# 1. Load .env
load_dotenv_if_present()
# 2. Build an adapter (will raise if API key is missing)
cfg = ProviderConfig.from_env("openai")
card = require_model_card(cfg.provider, cfg.model) # enforces registry entry
adapter = build_adapter(cfg)
# 3. Run the replay determinism comparison
report = compare_to_llm(
list(DEFAULT_LONGFORM_PROMPTS),
llm_callable=adapter,
runs=5,
)
print(report.summary())
# 4. The frontier_compare runner can also accept a provider adapter
# directly in future suite extensions — the ProviderConfig.as_dict()
# embeds cleanly into BenchmarkReport metadata.
Consequences
- Good: Any benchmark file can call
build_adapter(cfg)and receive a uniform callable. No provider SDK leaks into benchmark logic. - Good:
require_model_card()enforces that unregistered models cannot produce undocumented benchmark results. This is the key discipline addition. - Good:
load_dotenv_if_present()requires no new dependency (python-dotenvis not added). Shell env always wins. - Good:
.env.exampleis the single source of truth for all secrets and tuning knobs; it lives at the repo root so any operator finds it immediately. - Neutral: Provider SDK packages (
openai,anthropic,httpx) are not added topyproject.tomlas hard dependencies — they are soft requirements that the adapter import will surface with a clear error message. This keeps the base install lean and avoids forcing all CI runs to hold API keys. - Watch: Floating model aliases (e.g. bare
gpt-4o) are registered with a warning note but not banned at the registry level. The discipline is enforced by convention: benchmark operators should pin to dated snapshots.
Non-goals
- This ADR does not add async adapter support (synchronous calls are sufficient for current benchmark volumes).
- It does not add streaming.
- It does not add retry/backoff logic (callers can wrap
build_adapteroutput with tenacity or similar). - It does not modify
ChatRuntimeor any pack/lens behaviour. - It does not add provider SDK packages to
pyproject.tomlhard dependencies.