# `ALLM.Error.AdapterError`
[🔗](https://github.com/cykod/ALLM/blob/v0.3.0/lib/allm/error/adapter_error.ex#L1)

Errors returned by `ALLM.Adapter` / `ALLM.StreamAdapter` implementations.

Layer A — serializable (no PIDs, refs, funs, or raw API keys). Refines spec
§20's atom taxonomy into a struct so provider adapters report failures with
uniform shape.

See Phase 1 design §Sub-phase 1.1 for the closed reason enum.

## Error reasons

| Reason | Fires when |
|--------|------------|
| `:rate_limited` | Provider returned a rate-limit signal (HTTP 429, provider-specific header). |
| `:authentication_failed` | Credentials rejected (HTTP 401/403). |
| `:invalid_request` | Provider rejected the request shape (HTTP 400). |
| `:provider_unavailable` | Provider-side outage (HTTP 5xx, connection refused). |
| `:context_length_exceeded` | Prompt + expected output exceeds model context window. |
| `:content_filter` | Provider refused content on policy grounds. |
| `:timeout` | Request-level timeout (`opts[:request_timeout]` exceeded). |
| `:network_error` | Transport-level failure (DNS, TCP, TLS). |
| `:malformed_response` | Provider returned unparseable body. |
| `:unsupported_feature` | Model or provider does not support a requested capability. |
| `:no_scripted_response` | Testing adapters (e.g., `ALLM.Providers.Fake`) exhausted their script. Never produced by production providers (spec §31, Phase 4 amendment). |
| `:unknown` | Catch-all when no other reason fits; original term preserved in `:cause`. |

# `reason`

```elixir
@type reason() ::
  :rate_limited
  | :authentication_failed
  | :invalid_request
  | :provider_unavailable
  | :context_length_exceeded
  | :content_filter
  | :timeout
  | :network_error
  | :malformed_response
  | :unsupported_feature
  | :no_scripted_response
  | :unknown
```

Closed set of adapter-level error reasons (spec §20).

# `t`

```elixir
@type t() :: %ALLM.Error.AdapterError{
  __exception__: true,
  cause: term() | nil,
  message: String.t(),
  metadata: map(),
  provider: atom() | nil,
  reason: reason(),
  request_id: String.t() | nil,
  retry_after_ms: non_neg_integer() | nil,
  status: non_neg_integer() | nil
}
```

# `legal_reasons`

```elixir
@spec legal_reasons() :: [reason()]
```

Return the closed list of legal `:reason` atoms.

## Examples

    iex> :no_scripted_response in ALLM.Error.AdapterError.legal_reasons()
    true

    iex> length(ALLM.Error.AdapterError.legal_reasons())
    12

# `new`

```elixir
@spec new(
  reason(),
  keyword()
) :: t()
```

Build an `%AdapterError{}` from a `reason` atom and optional keyword fields.

`opts` may include `:message`, `:provider`, `:status`, `:retry_after_ms`,
`:request_id`, `:cause`, and `:metadata`. When `:message` is omitted, the
default is `"adapter error: #{reason}"` — with a provider suffix
`"adapter error (#{provider}): #{reason}"` when `:provider` is set.

Raises `ArgumentError` if `reason` is not one of the atoms in the closed
`t:reason/0` enum.

## Examples

    iex> err = ALLM.Error.AdapterError.new(:timeout)
    iex> err.reason
    :timeout
    iex> Exception.message(err)
    "adapter error: timeout"

    iex> err = ALLM.Error.AdapterError.new(:rate_limited, provider: :openai, retry_after_ms: 500)
    iex> err.retry_after_ms
    500
    iex> Exception.message(err)
    "adapter error (openai): rate_limited"

---

*Consult [api-reference.md](api-reference.md) for complete listing*
