Pure validators for Layer A input shapes. See spec §16 and Phase 1 design sub-phase 1.4.
Every validator returns :ok or {:error, %ALLM.Error.ValidationError{}}
with a machine-readable :errors list of {field, reason} tuples. The
field is either a single atom (top-level) or a path of atoms/indices
(e.g. [:messages, 2, :role]) when the failure is nested.
Multimodal content parts use the %ALLM.TextPart{} and %ALLM.ImagePart{}
Layer A structs (spec §35.6, Phase 14.4). A content list whose elements are
not one of those two structs is rejected with
{:content, :invalid_part_type} — raw maps in content lists are no longer
accepted (Phase 14.4 Decision #11; previously the v0.2 validator accepted
any list of maps and short-circuited image-typed maps with the now-removed
:vision_not_in_v0_2 reason).
Validators are opt-in: constructors like ALLM.Request.new/2 do not call
these functions (see Phase 1 design non-obvious decision #7). Users invoke
request/1, message/1, tool/1, thread/1, session/1, or
image_request/1 (Phase 13.3, spec §35.2.2) explicitly when they need a
check before dispatch.
Summary
Functions
Validate an %ALLM.ImageRequest{} (spec §35.2.2).
Validate an %ALLM.Message{}.
Validate an %ALLM.Request{}.
Validate an %ALLM.Session{}.
Validate an %ALLM.Thread{}.
Validate an %ALLM.Tool{}.
Functions
@spec image_request(ALLM.ImageRequest.t()) :: :ok | {:error, ALLM.Error.ValidationError.t()}
Validate an %ALLM.ImageRequest{} (spec §35.2.2).
Returns :ok when every rule passes, or
{:error, %ALLM.Error.ValidationError{reason: :invalid_image_request, errors: [...]}}
accumulating ALL failed rules — no hard-reject (matches request/1's
accumulator pattern).
Operation-arity rules:
:generaterequires non-empty:promptAND:input_images == [].:editrequires non-empty:promptANDlength(:input_images) in 1..2.:variationrequires:prompt in [nil, ""]ANDlength(:input_images) == 1.
Field rules: :n integer ≥ 1; :response_format in [:binary, :base64, :url];
:size in {pos_integer, pos_integer} | String.t() | :auto | nil;
:input_images a list of %ALLM.Image{}; :mask %ALLM.Image{} or nil.
Examples
iex> ALLM.Validate.image_request(ALLM.ImageRequest.new(prompt: "a kestrel"))
:ok
iex> req = ALLM.ImageRequest.new(prompt: nil, operation: :generate)
iex> {:error, err} = ALLM.Validate.image_request(req)
iex> err.reason
:invalid_image_request
iex> {:prompt, :required_for_operation} in err.errors
true
@spec message(ALLM.Message.t()) :: :ok | {:error, ALLM.Error.ValidationError.t()}
Validate an %ALLM.Message{}.
Returns :ok or {:error, %ALLM.Error.ValidationError{}}. A content list
whose elements are not %ALLM.TextPart{} or %ALLM.ImagePart{} is
rejected with {:content, :invalid_part_type} (Phase 14.4 Decision #11);
raw maps are no longer accepted in content lists.
Examples
iex> ALLM.Validate.message(%ALLM.Message{role: :user, content: "hi"})
:ok
iex> {:error, err} = ALLM.Validate.message(%ALLM.Message{role: :tool, content: "ok"})
iex> err.reason
:invalid_message
iex> {:tool_call_id, :required} in err.errors
true
@spec request(ALLM.Request.t()) :: :ok | {:error, ALLM.Error.ValidationError.t()}
Validate an %ALLM.Request{}.
Returns :ok when every field is well-formed, or
{:error, %ALLM.Error.ValidationError{reason: :invalid_request, errors: [...]}}.
Examples
iex> req = ALLM.Request.new([%ALLM.Message{role: :user, content: "hi"}])
iex> ALLM.Validate.request(req)
:ok
iex> req = ALLM.Request.new([])
iex> {:error, err} = ALLM.Validate.request(req)
iex> err.reason
:invalid_request
iex> {:messages, :empty} in err.errors
true
@spec session(ALLM.Session.t()) :: :ok | {:error, ALLM.Error.ValidationError.t()}
Validate an %ALLM.Session{}.
Enforces status/pending_* invariants (spec §5.7) and recursively validates
the embedded thread. Thread errors carry a [:thread, :messages, idx, :field]
path prefix.
Examples
iex> ALLM.Validate.session(%ALLM.Session{})
:ok
iex> {:error, err} = ALLM.Validate.session(%ALLM.Session{status: :awaiting_user, pending_question: nil})
iex> err.reason
:invalid_session
iex> {:pending_question, :required_for_status} in err.errors
true
@spec thread(ALLM.Thread.t()) :: :ok | {:error, ALLM.Error.ValidationError.t()}
Validate an %ALLM.Thread{}.
Every message must pass message/1. Errors from nested messages carry a
[:messages, idx, :field] path prefix so callers can locate the offender.
Examples
iex> t = ALLM.Thread.from_messages([%ALLM.Message{role: :user, content: "hi"}])
iex> ALLM.Validate.thread(t)
:ok
iex> t = ALLM.Thread.from_messages([%ALLM.Message{role: :bogus, content: "x"}])
iex> {:error, err} = ALLM.Validate.thread(t)
iex> err.reason
:invalid_thread
iex> {[:messages, 0, :role], :unknown} in err.errors
true
@spec tool(ALLM.Tool.t()) :: :ok | {:error, ALLM.Error.ValidationError.t()}
Validate an %ALLM.Tool{}.
Returns :ok or {:error, %ALLM.Error.ValidationError{}}. The top-level
shape of :schema is intentionally not checked — providers differ on
whether "type" => "object" is required — but non-map schemas are rejected.
Examples
iex> tool = %ALLM.Tool{name: "weather", description: "d", schema: %{}}
iex> ALLM.Validate.tool(tool)
:ok
iex> {:error, err} = ALLM.Validate.tool(%ALLM.Tool{name: "", description: "d", schema: %{}})
iex> err.reason
:invalid_tool
iex> {:name, :empty} in err.errors
true