ADR-001: Use the Anthropic API as the LLM provider¶
Status: Accepted
Date: 2026-06-19
Deciders: San Lee
Context¶
The classifier and generator both require an LLM. The main alternatives were:
- Anthropic API (
claude-sonnet-4-6, the workhorse at the time of this decision) — strong instruction-following, native tool-use support for structured output, Python SDK - OpenAI API (
gpt-4o) — comparable capability, also has function-calling for structured output - Local model (Ollama, llama.cpp) — no API cost, but weaker instruction-following and no reliable structured-output enforcement at this tier
The project needed reliable structured JSON output (valid enum values, no hallucinated labels) without application-level validation code as the primary safety net.
Decision¶
Use the Anthropic Python SDK. The API key is read from the ANTHROPIC_API_KEY environment
variable.
The provider is what this ADR decides. The model is a knob under it, pinned in
src/classify.py as MODEL and standardized by
SYS-002
(exact IDs, no date suffixes). It was claude-sonnet-4-6 at the 2026-06-19 decision date and
is claude-sonnet-5 now. See Amendment 2026-08-20.
Consequences¶
- Tool use (
tool_choice: "tool") forces the model to call a named tool with a defined JSON schema, so the response arrives as structured fields instead of free text to parse. (Amended 2026-08-20: this bullet went on to describe enum membership as a client-side guard with a re-sample. ADR-008 retired that.CLASSIFY_TOOLnow carriesstrict: true, so the API's constrained decoding guarantees enum validity server-side,classify()makes exactly one call, and_validate/InvalidLabelErrorare kept as a defensive backstop rather than the primary guard.) - A capable mid-tier model is fast and inexpensive enough for the 330 API calls this project's v1 needed (30 generation + 300 eval), while being strong enough to follow nuanced label definitions. That reasoning is what the tier choice rests on, and it survives a model migration.
- Switching to OpenAI would require swapping the client and rewriting the tool-use schema to function-calling format, but the overall architecture would be unchanged.
Alternatives Considered¶
| Option | Reason Not Chosen |
|---|---|
OpenAI gpt-4o |
Anthropic key was available; no meaningful quality difference at this task |
| Local model | Structured output enforcement is unreliable at open-weight model tier; adds local GPU dependency |
Amended 2026-06-29: the original Consequences said out-of-enum labels are rejected "at the API layer." They were not, at the time: tool use enforced the response shape, while enum membership was validated in client code (
classify.py's_validate, which re-sampled once). Corrected to match the code and the v1.1.0 doc sweep (seeCHANGELOG.md). This amendment is itself now historical — see below.Amended 2026-08-20, two corrections. Both claims this ADR made about the code had gone stale, and one of them was the 2026-06-29 amendment above.
- The model. The title and body named
claude-sonnet-4-6as the model in use.src/classify.pyhas runclaude-sonnet-5since the 2026-07-19 workhorse migration (ADR-010 records the migration and the RAG-path regression it surfaced). The title now names the provider, which is the durable decision, so the next migration does not re-stale it.src/classify_rag.pystill pinsclaude-sonnet-4-6deliberately, on the dormant grounding path; that is a live pin, not a stale one.- The enum guard. The 2026-06-29 amendment describes a client-side re-sample. That mechanism was retired by ADR-008 when Structured Outputs reached GA:
strict: truemoved enum enforcement server-side, andclassify()now makes exactly one call. ADR-002 carried a matching amendment at the time and this ADR did not, so the two disagreed for roughly seven weeks.The general lesson, recorded because it is the second time it has bitten: an ADR that describes how the code currently works goes stale silently, because nothing tests prose. An ADR that records what was decided and why does not. Prefer the second shape.