Status: Accepted
Date: 2026-07-01
Context
ADR-007 #3 decides that “the model-call boundary is an abstraction (not a hardcoded
Anthropic client)” and that “base URL, model ID, and key source are configurable”, with
the explicit consequence that “the eval runner needs an abstraction layer for model
providers, not a single hardcoded client”. The BYOK findings
(eval-gating-byok-2026-06-29.md §3) reinforce this: “the eval boundary should be an
interface, not a hardcoded Anthropic client … at minimum support an OpenAI-compatible
base URL so a local model (Ollama, vLLM) or internal gateway can be slotted in.”
The native eval runner plan (native-eval-runner-2026-07-01.md) as originally written
was non-compliant with ADR-007 #3: it shipped only an AnthropicClient, read only
ANTHROPIC_API_KEY, hardcoded claude-sonnet-4-20250514 as the default model, and
demoted provider-agnosticism to a “TODO: OpenAI client” code comment in the BYOK gaps
table. A consumer with only an OpenAI or Gemini key, or a data-governance constraint
mandating a specific provider or a local model, could not use the LLM-judge at all. This
matters because the product is a CLI that consumers point at their own skills — BYOK is a
design constraint, not a nice-to-have.
Decision
Ship a provider-agnostic LLM client in v1 of the native eval runner with four
provider implementations:
anthropic— Messages API, default modelclaude-sonnet-4-20250514, key
ANTHROPIC_API_KEY. This repo’s default.openai— Chat Completions API, default modelgpt-4o, keyOPENAI_API_KEY.
HonoursLLM_BASE_URLso Ollama, vLLM, and internal gateways slot in unchanged.gemini— native GooglegenerateContentAPI, default modelgemini-2.0-flash,
keyGEMINI_API_KEY(falls back toGOOGLE_API_KEY). A native client is shipped
(not the OpenAI-compatibility shim) because the native API is the common path for
GEMINI_API_KEYusers.openai-compatible— the OpenAI client with a requiredLLM_BASE_URL, for
local models and gateways where no canonical default endpoint exists.
Selection is via the LLM_PROVIDER environment variable (default anthropic) or the
--provider CLI flag. NewFromEnv() reads the provider-specific key env var and returns
nil when the selected provider has no key, preserving ADR-007 #5 (graceful degradation):
no key → structural-only mode, said loudly. A consumer who will not send content to any
hosted API still gets the full structural D9 grade.
This supersedes the plan’s earlier “TODO: OpenAI client” stance, which demoted a settled
ADR-007 decision to a deferred code comment. Provider-agnosticism is a delivered
capability in v1, not a future task.
Consequences
- A consumer with any of an Anthropic, OpenAI, or Gemini API key, or a local
OpenAI-compatible endpoint, gets a working LLM-judge — ADR-007 #3 is satisfied on
delivery. - The model-call boundary is a
Client+Providerabstraction, so adding a future
provider is a new file implementing the interface, not a refactor of a hardcoded
client. - JSON output gains a
providerfield so runs are reproducible and auditable across
providers; score trends are comparable only within a(provider, model, judge_prompt_version)triple. - CI for this repo keeps
ANTHROPIC_API_KEY(this repo’s choice) but the workflow is
parameterised viaLLM_PROVIDER+ the matching secret — switching providers is a
config change, not a code change. - Graceful degradation is keyed to the selected provider’s key: selecting
LLM_PROVIDER=openaiwith onlyANTHROPIC_API_KEYset degrades to structural-only
(no silent fallback to Anthropic), keeping the auth model predictable. - This ADR does not address subscription/OAuth auth (e.g. Claude Max subscription).
That remains a known, deferred gap documented in the plan’s BYOK gaps table; it is an
auth-method concern, not a provider-choice concern. - v1 ships four implementations, adding ~2 medium files (
openai.go,gemini.go) plus
per-provider tests on top of the single-client design — a deliberate scope increase to
honour ADR-007 #3 at delivery rather than after it.