ALLM.Validate (allm v0.3.0)

Copy Markdown View Source

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

image_request(req)

@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:

  • :generate requires non-empty :prompt AND :input_images == [].
  • :edit requires non-empty :prompt AND length(:input_images) in 1..2.
  • :variation requires :prompt in [nil, ""] AND length(: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

message(msg)

@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

request(req)

@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

session(s)

@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

thread(t)

@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

tool(t)

@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