Speech-to-text provider adapter contract.
Layer B — runtime. Implementations take an ALLM.TranscriptionRequest
plus a keyword opts list and return either
{:ok, %ALLM.TranscriptionResponse{}} or
{:error, %ALLM.Error.TranscriptionAdapterError{}}. An engine carries its
transcription adapter in the :transcription_adapter slot, independently
of the chat :adapter and of :speech_adapter.
Minimum impl skeleton
defmodule MyTranscriptionProvider do
@behaviour ALLM.TranscriptionAdapter
alias ALLM.Error.TranscriptionAdapterError
@impl true
def max_audio_bytes, do: 25 * 1024 * 1024
@impl true
def transcribe(%ALLM.TranscriptionRequest{audio: audio} = request, opts) do
# Invariants 3 and 4, in this order, before any I/O and before
# ALLM.Keys.fetch!/2.
max = max_audio_bytes()
case ALLM.Audio.size(audio) do
{:error, cause} ->
{:error,
TranscriptionAdapterError.new(:invalid_request, metadata: %{cause: cause})}
{:ok, count} when count > max ->
{:error,
TranscriptionAdapterError.new(:invalid_request,
metadata: %{count: count, max: max}
)}
{:ok, _count} ->
# Resolve the key, upload the audio via Req, decode the text.
{:ok,
%ALLM.TranscriptionResponse{
text: "...",
request_id: Keyword.get(opts, :request_id),
metadata: request.metadata
}}
end
end
endGate order
Every transcription adapter runs its gates in one fixed order, all before
ALLM.Keys.fetch!/2: resolvable (invariant 3) → size
(invariant 4) → MIME (adapter-specific) → key resolution. MIME
acceptance is not a behaviour invariant, because the accepted set differs
per provider; each adapter gates its own and documents it.
ALLM.Audio.size/1 is the byte resolver for the first two gates. It stats
a {:file, path} source without reading it.
Invariants
transcribe/2returns exactly{:ok, %ALLM.TranscriptionResponse{}}or{:error, %ALLM.Error.TranscriptionAdapterError{}}— never a bare struct, never a three-tuple. The one documented exception isALLM.Keys.fetch!/2, which raises%ALLM.Error.EngineError{reason: :missing_key}by design. The transcription façade enforces this by raisingArgumentErroron any other shape, so the conformance suite cannot observe it.- On success,
response.textis a binary (possibly""for silence), andresponse.usageis an%ALLM.Usage{}, nevernil. - Audio whose bytes cannot be resolved (a missing file, invalid base64,
an off-shape source) is rejected with
:invalid_requestand ametadata.causenaming why, before any I/O and beforeALLM.Keys.fetch!/2. - Audio larger than
max_audio_bytes/0is rejected with:invalid_request,metadata.count(the byte count) andmetadata.max, under the same ordering. max_audio_bytes/0returns apos_integer().opts[:request_id]is reflected ontoresponse.request_idwhen supplied.request.metadataround-trips ontoresponse.metadataunchanged.opts[:request_timeout]is honoured; expiry produces{:error, %ALLM.Error.TranscriptionAdapterError{reason: :timeout}}. When absent, the adapter applies its own default and documents it in itstranscribe/2@doc.prepare_request/2(optional) returns an unfiredReq.Requestconfigured exactly astranscribe/2would fire it, and is defined only for audio that passes invariants 3 and 4.- Span flags.
request.timestampsandrequest.logprobscount only when exactlytrue. A set flag the adapter cannot honour is refused with:unsupported_featureandmetadata.field(the flag),:timestampschecked first, before any I/O and beforeALLM.Keys.fetch!/2. On success,response.spansisniliff neither flag istrue; otherwise it is a list ofALLM.TranscriptSpan([]when nothing was spoken). On every span,start_secondsandend_secondsare non-nilonly iftimestamps: true, andlogprobonly iflogprobs: true: an attribute the provider sent but the caller did not ask for is dropped. With both flags off the adapter ignores the provider's span data. The one exception is a scripted%ALLM.TranscriptionResponse{}thatALLM.Providers.FakeTranscriptionreturns verbatim.
Cleanup invariant: none. Req.request/1 owns its connection
lifecycle.
Summary
Callbacks
Return the largest audio payload, in bytes, the adapter accepts in one request.
Escape hatch: return a configured but unfired Req.Request that the caller
can further customize before firing.
Transcribe an audio clip.
Callbacks
@callback max_audio_bytes() :: pos_integer()
Return the largest audio payload, in bytes, the adapter accepts in one request.
Per-module, one number for the adapter. It is the cap the size gate (invariant 4) measures against, and what a caller checks a file against before uploading it.
@callback prepare_request( ALLM.TranscriptionRequest.t(), keyword() ) :: {:ok, Req.Request.t()} | {:error, ALLM.Error.TranscriptionAdapterError.t()}
Escape hatch: return a configured but unfired Req.Request that the caller
can further customize before firing.
Optional. Per invariant 9 it is defined only for audio that passes the resolvable and size gates.
@callback transcribe( ALLM.TranscriptionRequest.t(), keyword() ) :: {:ok, ALLM.TranscriptionResponse.t()} | {:error, ALLM.Error.TranscriptionAdapterError.t()}
Transcribe an audio clip.
Returns {:ok, %ALLM.TranscriptionResponse{}} on success, or
{:error, %ALLM.Error.TranscriptionAdapterError{}} on every failure shape.
See ALLM.Error.TranscriptionAdapterError for the closed reason enum.