# `Nous.Providers.HTTP`
[🔗](https://github.com/nyo16/nous/blob/v0.17.1/lib/nous/providers/http.ex#L1)

Shared HTTP utilities for all LLM providers.

Two HTTP families, both pluggable:

- **Non-streaming** requests (one-shot model calls, web fetching, search
  APIs) go through a `Nous.HTTP.Backend`. Default is
  `Nous.HTTP.Backend.Req`; `Nous.HTTP.Backend.Hackney` is also shipped.
- **Streaming** requests (SSE / chunked LLM responses) go through a
  `Nous.HTTP.StreamBackend`. Default is `Nous.HTTP.StreamBackend.Req`
  (Req's `:into` callback driven by Finch); `Nous.HTTP.StreamBackend.Hackney`
  provides strict pull-based backpressure via `:hackney`'s `{:async, :once}`
  mode for callers whose downstream consumers can block per chunk.

Both backend layers resolve via the same precedence: per-call opt → env
var → app config → default. See `Nous.HTTP.Backend` and
`Nous.HTTP.StreamBackend` for selection details.

## Usage

    # Non-streaming request
    {:ok, body} = HTTP.post(url, body, headers)

    # Streaming request — returns a lazy stream of parsed events
    {:ok, stream} = HTTP.stream(url, body, headers)
    Enum.each(stream, &process_event/1)

    # Per-call backend override
    {:ok, stream} = HTTP.stream(url, body, headers,
      stream_backend: Nous.HTTP.StreamBackend.Hackney)

## SSE Parsing

SSE events follow the Server-Sent Events spec (https://html.spec.whatwg.org/multipage/server-sent-events.html):
- Events are separated by double newlines (`\n\n`)
- Each event contains field lines like `data: {...}`
- Multiple `data:` fields are concatenated with newlines
- `[DONE]` signals stream completion (OpenAI convention)

The default SSE parser (`parse_sse_buffer/1`) is transport-agnostic and
shared by both stream backends. Custom parsers can be plugged in via
the `:stream_parser` opt; see `Nous.Providers.HTTP.JSONArrayParser`
for an example.

## Stream backpressure

- `Nous.HTTP.StreamBackend.Req` (default): the `:into` callback runs in
  a `Task` and feeds the consumer process via `send/2`. Producer and
  consumer share an `:atomics` counter of in-flight chunk bytes; above
  the 8 MB high-water mark the producer parks until the consumer drains
  below 1 MB, which stops the socket being read. Memory per stream is
  bounded by that window, not by the (unbounded) mailbox. A consumer
  that stays stalled past `:backpressure_max_wait_ms` gets
  `{:stream_error, %{reason: :backpressure_overflow}}`.
- `Nous.HTTP.StreamBackend.Hackney`: strict pull-based — the consumer
  calls `:hackney.stream_next/1` per chunk, so the producer literally
  cannot outrun the consumer. Pick this when downstream consumers can
  block per chunk (LiveView fan-out, persistence-on-every-chunk, slow IO).

# `api_key_header`

```elixir
@spec api_key_header(String.t() | nil, String.t()) :: list()
```

Build authorization header for API key auth (Anthropic style).

Returns empty list for nil or empty string values.

# `bearer_auth_header`

```elixir
@spec bearer_auth_header(String.t() | nil) :: list()
```

Build authorization header for Bearer token auth (OpenAI style).

Returns empty list for nil, empty string, or "not-needed" values.

# `json_headers`

```elixir
@spec json_headers() :: list()
```

Base JSON content-type headers. Most providers start their header list here.

# `openai_project_header`

```elixir
@spec openai_project_header(String.t() | nil) :: list()
```

Build OpenAI-style `openai-project` header (project-scoped API keys).
Returns empty list when nil/empty.

# `organization_header`

```elixir
@spec organization_header(String.t() | nil) :: list()
```

Build OpenAI-style `openai-organization` header. Returns empty list when nil/empty.

# `parse_sse_buffer`

```elixir
@spec parse_sse_buffer(String.t() | nil | any()) ::
  {list(), String.t()} | {:error, :buffer_overflow}
```

Parse an SSE buffer into events.

Returns `{events, remaining_buffer}` where events is a list of parsed
JSON maps, `{:stream_done, reason}` tuples, or `{:parse_error, reason}` tuples.

Handles edge cases:
- Empty events (ignored)
- Whitespace-only events (ignored)
- Malformed JSON (emits `{:parse_error, reason}`)
- Multiple data fields per event (concatenated per spec)
- Comment lines (ignored)
- Buffer overflow protection

## Examples

    iex> HTTP.parse_sse_buffer("data: {\"text\": \"hi\"}\n\n")
    {[%{"text" => "hi"}], ""}

    iex> HTTP.parse_sse_buffer("data: partial")
    {[], "data: partial"}

    iex> HTTP.parse_sse_buffer("data: [DONE]\n\n")
    {[{:stream_done, "stop"}], ""}

# `parse_sse_event`

```elixir
@spec parse_sse_event(String.t()) ::
  map() | {:stream_done, String.t()} | {:parse_error, term()} | nil
```

Parse a single SSE event.

Returns parsed JSON map, `{:stream_done, reason}`, `{:parse_error, reason}`, or nil.

Handles per SSE spec:
- `data:` fields (with or without space after colon)
- Multiple `data:` fields concatenated with newlines
- `:` prefix for comments (ignored)
- `event:`, `id:`, `retry:` fields (ignored for now)
- Empty lines within events

## Examples

    iex> HTTP.parse_sse_event("data: {\"key\": \"value\"}")
    %{"key" => "value"}

    iex> HTTP.parse_sse_event("data: [DONE]")
    {:stream_done, "stop"}

    iex> HTTP.parse_sse_event(": this is a comment")
    nil

    iex> HTTP.parse_sse_event("")
    nil

# `post`

```elixir
@spec post(String.t(), map(), list(), keyword()) :: {:ok, map()} | {:error, term()}
```

Make a non-streaming POST request.

Dispatches to the configured `Nous.HTTP.Backend`. Resolution order
(highest precedence first):

1. Per-call `:backend` opt — `HTTP.post(url, body, headers, backend: Nous.HTTP.Backend.Hackney)`
2. `NOUS_HTTP_BACKEND` env var — `req`, `hackney`, or a fully-qualified
   module name (e.g. `MyApp.MyHTTPBackend`)
3. `Application.get_env(:nous, :http_backend, ...)`
4. Default: `Nous.HTTP.Backend.Req`

Returns `{:ok, body}` or `{:error, reason}`.

## Options
  * `:backend` - Backend module (overrides env / config / default)
  * `:timeout` - Request timeout in ms (default: 180_000)

## Error Reasons
  * `%{status: integer(), body: term()}` - HTTP error response
  * `%Req.TransportError{}` / `%Mint.TransportError{}` - Network error (Req backend)
  * `%JSON.DecodeError{}` - JSON decode error

# `stream`

```elixir
@spec stream(String.t(), map(), list(), keyword()) ::
  {:ok, Enumerable.t()} | {:error, term()}
```

Make a streaming POST request.

Dispatches to the configured `Nous.HTTP.StreamBackend`. Resolution
order (highest precedence first):

1. Per-call `:stream_backend` opt
2. `NOUS_HTTP_STREAM_BACKEND` env var — `req`, `hackney`, or a
   fully-qualified module name
3. `Application.get_env(:nous, :http_stream_backend, ...)`
4. Default: `Nous.HTTP.StreamBackend.Req`

Returns `{:ok, stream}` where stream is an `Enumerable.t()` of parsed
events. Events are maps with string keys (parsed JSON),
`{:stream_done, reason}` tuples on completion, or
`{:stream_error, reason}` tuples on failure.

## Options
  * `:stream_backend` - Backend module (overrides env / config / default)
  * `:timeout` - Receive timeout in ms (default: 180_000)
  * `:connect_timeout` - TCP connect timeout in ms (default: 30_000)
  * `:stream_parser` - Module for parsing the stream buffer (default: SSE).
    Must implement `parse_buffer/1` returning `{events, remaining_buffer}`.
    See `Nous.Providers.HTTP.JSONArrayParser` for an example.
  * `:pool` - (Hackney backend only) Hackney pool name (default: `:default`).

## Error Handling
The stream emits `{:stream_error, reason}` on errors and then halts.

---

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