# `Agentic.LLM.Provider`

Behaviour describing one LLM service provider.

Each provider is a small module that declares which transport it
uses, points at a base URL, declares env vars for credentials,
and optionally implements catalog and usage fetchers.

## Required callbacks

  * `id/0` — unique atom identifying this provider
  * `label/0` — human-readable name
  * `transport/0` — the transport module to use
  * `default_base_url/0` — base URL for API calls
  * `env_vars/0` — env var names in priority order
  * `default_models/0` — static seed of known models
  * `request_headers/1` — extra headers given resolved credentials
  * `supports/0` — MapSet of capability atoms

## Optional callbacks

  * `fetch_catalog/1` — dynamic model discovery
  * `fetch_usage/1` — quota / usage endpoint
  * `classify_http_error/3` — provider-specific error overrides

# `classify_http_error`
*optional* 

```elixir
@callback classify_http_error(non_neg_integer() | nil, term(), term()) ::
  {atom(), non_neg_integer() | nil} | :default
```

# `default_base_url`

```elixir
@callback default_base_url() :: String.t() | nil
```

# `default_models`

```elixir
@callback default_models() :: [Agentic.LLM.Model.t()]
```

# `env_vars`

```elixir
@callback env_vars() :: [String.t()]
```

# `fetch_catalog`
*optional* 

```elixir
@callback fetch_catalog(Agentic.LLM.Credentials.t()) ::
  {:ok, [Agentic.LLM.Model.t()]} | {:error, term()} | :not_supported
```

# `fetch_usage`
*optional* 

```elixir
@callback fetch_usage(Agentic.LLM.Credentials.t()) :: {:ok, term()} | :not_supported
```

# `id`

```elixir
@callback id() :: atom()
```

# `label`

```elixir
@callback label() :: String.t()
```

# `request_body_extras`
*optional* 

```elixir
@callback request_body_extras(canonical :: map()) :: map()
```

Optional hook for providers to inject provider-specific keys into the
outbound chat request body. Used for things like OpenRouter's
`provider` preference block, which is a request-body extension on top
of the OpenAI-compatible spec. Returns a map; an empty map is a no-op.

# `request_headers`

```elixir
@callback request_headers(Agentic.LLM.Credentials.t()) :: [{String.t(), String.t()}]
```

# `supports`

```elixir
@callback supports() :: MapSet.t(atom())
```

# `transport`

```elixir
@callback transport() :: module()
```

# `chat`

```elixir
@spec chat(provider :: module(), params :: map(), opts :: keyword()) ::
  {:ok, Agentic.LLM.Transport.request()} | {:error, term()}
```

Call a provider's `chat` via its declared transport.

Builds the canonical params, resolves credentials, delegates to
the transport for request building and response parsing, and
performs the HTTP call.

# `stream_chat`

Streaming variant of `chat/3`.

Calls the provider with `stream: true` and invokes `on_chunk.(text_delta)`
for each text chunk received. Returns the complete `{:ok, Response.t()}`
at the end, same as `chat/3`.

The `on_chunk` callback in `opts` receives each text delta as a binary.

---

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