Normandy.LLMError exception (normandy v2.2.0)

View Source

A failed LLM call.

Adapters return {:error, %Normandy.LLMError{}} from Normandy.Agents.Model.converse/3; Once retries (if configured) are exhausted, Normandy.Agents.BaseAgent.run/3 returns it as {:error, %Normandy.LLMError{}} and run!/3 raises it.

type: :unsupported means the client can't do what was asked (a client without streaming answering Normandy.Agents.Model.stream/3); it is never retryable.

type: :max_tokens means the answer stopped before it was complete: the response hit the request's max_tokens, or the context window (model_context_window_exceeded; the message names which). type: :refusal means the model declined to answer. Both are retryable?: false; :reason is {:stop_reason, reason}, never model text, and :usage is what the call billed. Normandy builds them from the response's stop_reason, so every client gets them.

type: :pause_turn means the final call after max_tool_iterations was still paused (stop_reason pause_turn) after three continuations. It is retryable?: false; :reason is {:stop_reason, :pause_turn}, never model text, and :usage is the paused response's own when it carries any (BaseAgent's do not: each call's usage is on its llm_call span). The turn core builds it, so no circuit breaker counts it.

  • :type — failure class (see error_type/0)
  • :status — HTTP status when there was one, else nil
  • :retryable? — whether a retry can help; set by the adapter that classified the failure
  • :message — "LLM call failed: <type> (<status>): <detail>"
  • :reason — the provider's original error, for debugging
  • :usage — raw token usage already paid for before the call failed (for example the first response plus JSON-parse retries that ended in :invalid_output), nil when no call was billed
  • :usage_counts — the same usage normalized by Normandy.Telemetry.Usage.normalize/1 (:input_tokens, :output_tokens, ...), nil when :usage is

The llm_call span's [:normandy, :agent, :llm_call, :exception] metadata cannot carry extra keys, so this usage is read from its reason: metadata.reason.usage and metadata.reason.usage_counts.

The message never includes the client, its options or req_options.

Summary

Functions

Builds an error. :type and :retryable? are required; :status, :detail (text for the message) and :reason are optional.

Attaches the raw usage already billed before the failure (and its normalized counts). A nil usage is ignored, and the first usage wins: an error that already carries usage is returned unchanged.

Returns an LLMError unchanged; wraps any other term as type: :unknown, retryable?: false, keeping the term in :reason (never in the message).

Types

error_type()

@type error_type() ::
  :rate_limit
  | :overloaded
  | :server
  | :transport
  | :authentication
  | :permission
  | :not_found
  | :invalid_request
  | :invalid_output
  | :max_tokens
  | :refusal
  | :pause_turn
  | :stream
  | :circuit_open
  | :unsupported
  | :unknown

t()

@type t() :: %Normandy.LLMError{
  __exception__: true,
  message: String.t(),
  reason: term(),
  retryable?: boolean(),
  status: non_neg_integer() | nil,
  type: error_type(),
  usage: map() | struct() | nil,
  usage_counts: Normandy.Telemetry.Usage.normalized() | nil
}

Functions

new(fields)

@spec new(keyword()) :: t()

Builds an error. :type and :retryable? are required; :status, :detail (text for the message) and :reason are optional.

with_usage(error, usage)

@spec with_usage(t(), map() | struct() | nil) :: t()

Attaches the raw usage already billed before the failure (and its normalized counts). A nil usage is ignored, and the first usage wins: an error that already carries usage is returned unchanged.

wrap(error)

@spec wrap(term()) :: t()

Returns an LLMError unchanged; wraps any other term as type: :unknown, retryable?: false, keeping the term in :reason (never in the message).