ADR-125: Per-adapter collaborator classes
- Status
-
Accepted
- Date
-
2026-08-04
Context
Every provider adapter keeps its private helpers inline; shared behaviour has
so far moved up into
Abstract or sideways into traits
(
Response,
Error). There was no
precedent for a class that belongs to exactly one adapter.
Open carried one cluster that neither direction fits: the
model routing — six methods, 163 contiguous lines, pure decision logic with no
HTTP, no PSR-7 and no response objects. It is also the part most likely to
grow, because routing strategies are the product's premise. Pulled up it would
burden six adapters that route nothing; as a trait it would stay untestable in
isolation and keep the adapter's line count.
Decision
Adapter-specific logic that is pure — no transport, no credential path, no
response parsing — MAY be extracted into a collaborator class in a
sub-namespace named after the adapter (here
\Netresearch\). Rules:
- The provider constructs it inline. The adapter constructor signature is
owned by
Abstract, shared by all seven adapters and wired by the compiler pass; a collaborator is an implementation detail and never a DI service of its own.Provider - The collaborator is stateless. Configuration the adapter's
configureowns and validates (here the routing strategy) stays on the adapter and arrives per call. Runtime caches (here the fetched model catalogue) stay on the adapter and arrive as an argument — as a closure where the inline code fetched lazily, so extraction cannot introduce a network call that was not there before.() - The architecture guard covers the sub-namespace. The PHPat rule that
keeps
Service\*off concrete adapters matches classes ending inProvider; a collaborator named anything else would quietly escape it. Each new adapter sub-namespace is added to the deny set in the same change that creates it. - Transport is out of scope. Anything touching the HTTP client, auth
headers, retries or error mapping stays on the adapter, where
Abstract's vault client and SSRF gating are unavoidable.Provider
Consequences
Opendrops from 983 to 829 lines and the routing becomes directly unit-testable; the existing 22 routing tests keep passing unchanged because they assert through the captured request body.Router Provider - The first collaborator sets the naming and placement pattern for any future one (e.g. a Gemini message converter), each requiring the same guard extension.
- The golden-sample note in
AGENTS.still points atmd Open— the inline shape remains the default; extraction is for clusters that meet the purity bar above, not a target size.Ai Provider