Typed-classification provider adapter contract.
Layer B — runtime. Implementations take an ALLM.ClassificationRequest
(one piece of state plus N named ALLM.ClassificationQuestions) and a
keyword opts list, and return either {:ok, %ALLM.ClassificationResponse{}}
or {:error, %ALLM.Error.ClassificationAdapterError{}}.
Classification is a capability of its own, not a use of a chat model: the provider answers closed-form questions (pick one option, score on a scale, yes/no) with calibrated probabilities and generates no text. It is also request/response only, so there is no streaming counterpart.
Minimum impl skeleton
defmodule MyClassificationProvider do
@behaviour ALLM.ClassificationAdapter
@impl true
def classify(%ALLM.ClassificationRequest{questions: questions} = request, opts) do
if questions == %{} do
{:error, %ALLM.Error.ClassificationAdapterError{reason: :invalid_request}}
else
# Translate request -> HTTP body, fire via Req, translate
# response -> one %ALLM.ClassificationAnswer{} per question id.
{:ok,
%ALLM.ClassificationResponse{
request_id: Keyword.get(opts, :request_id),
metadata: request.metadata
}}
end
end
endThe empty-questions gate is mandatory (invariant 6) and MUST run before any
HTTP I/O and, for an adapter that resolves credentials, before
ALLM.Keys.fetch!/2, so a keyless environment still observes the rejection
rather than a %ALLM.Error.EngineError{reason: :missing_key}. Any
provider-specific limit (a maximum option count, a maximum level count) is
enforced the same way, as :invalid_request, ahead of the key.
HTTP transport guidance
Use Req for classification calls. Make one HTTP attempt per call: the
façade already retries retryable reasons, so an inner retry loop would
multiply the attempt count. Populate retry_after_ms on
:rate_limited errors so callers who drive the adapter directly can retry
themselves.
Invariants
classify/2returns exactly{:ok, %ALLM.ClassificationResponse{}}or{:error, %ALLM.Error.ClassificationAdapterError{}}— never a bare struct, never a three-tuple. Network failures, 4xx and 5xx all convert to the error tuple. The one documented exception isALLM.Keys.fetch!/2, which raises%ALLM.Error.EngineError{reason: :missing_key}by design; adapters do not rescue it.ALLM.classify/3enforces this invariant: any other return shape raisesArgumentErrornaming the adapter. The conformance suite cannot observe it, so a green conformance run is not evidence that every failure shape has been converted.- On
{:ok, response},Map.keys(response.answers)equalsMap.keys(request.questions)as sets: one answer per question id, no extras. - Each answer's
:typeequals its question's:type, and its fields are populated as theALLM.ClassificationAnswerfield table says for that type. - A
:choiceanswer's:choiceis one of the question's criteria keys, and its:probabilitieskeys equal the criteria keys. - A
:scoreanswer's:probabilitiesand:legendeach have one entry per level (length(question.criteria)). - Empty questions (
questions: %{}) are rejected with:invalid_requestbefore any I/O and beforeALLM.Keys.fetch!/2. request.metadataround-trips ontoresponse.metadataunchanged, andopts[:request_id]is reflected ontoresponse.request_idunchanged.opts[:request_timeout]is honoured. Exceeding it yields{:error, %ALLM.Error.ClassificationAdapterError{reason: :timeout}}.prepare_request/2(optional) returns an unfiredReq.Requestconfigured exactly asclassify/2would fire it. Callers may mutate it before firing.
Cleanup invariant: none. There is no Stream.resource/3 and no Finch
ref in a classification call — Req.request/1 owns its connection
lifecycle. Stated explicitly so the absence reads as intent rather than
omission.
Summary
Callbacks
Answer every question in a classification request synchronously.
Escape hatch: return a configured but unfired Req.Request that the caller
can further customize (headers, retries, middleware) before firing.
Callbacks
@callback classify( ALLM.ClassificationRequest.t(), keyword() ) :: {:ok, ALLM.ClassificationResponse.t()} | {:error, ALLM.Error.ClassificationAdapterError.t()}
Answer every question in a classification request synchronously.
Returns {:ok, %ALLM.ClassificationResponse{}} carrying one
%ALLM.ClassificationAnswer{} per question id, or
{:error, %ALLM.Error.ClassificationAdapterError{}} on every failure shape.
See ALLM.Error.ClassificationAdapterError for the closed reason enum.
@callback prepare_request( ALLM.ClassificationRequest.t(), keyword() ) :: {:ok, Req.Request.t()} | {:error, ALLM.Error.ClassificationAdapterError.t()}
Escape hatch: return a configured but unfired Req.Request that the caller
can further customize (headers, retries, middleware) before firing.
Optional. When unimplemented, callers dispatch to classify/2 directly.
Per invariant 9 the returned request must be configured exactly as
classify/2 would fire it.