# `Dsxir.LM.Sycophant`

Sycophant-backed implementation of the `Dsxir.LM` behaviour.

Config shape:

    [model: "openai:gpt-4o-mini", api_key: nil | binary,
     base_url: nil | binary, temperature: float, max_tokens: integer,
     top_p: float, num_retries: integer]

Unknown config keys pass through to Sycophant; Sycophant validates them
against the resolved wire protocol's param schema.

Per-call opts override per-config opts via `Keyword.merge/2`. `api_key` and
`base_url` are lifted into `credentials: %{...}` for Sycophant. The `:headers`
config key is reserved for future use and intentionally ignored.

## Streaming

The `:stream` opt is forwarded to `Sycophant.generate_text/3` unchanged.
Sycophant invokes the 1-arity callback with `%Sycophant.StreamChunk{}` values
(`:text_delta`, `:tool_call_delta`, `:reasoning_delta`, `:usage`, `:failed`,
`:incomplete`, `:cancelled`, `:done`) as the response streams in; the final
assembled `{:ok, text, usage}` tuple is still returned by this callback so
`Dsxir.Predictor.Predict` can build its `%Dsxir.Prediction{}` normally.

## Usage extraction

On a successful response, the `%Sycophant.Usage{}` struct is flattened into
the `Dsxir.LM` usage map. When Sycophant reports `nil` usage, the empty
usage map (`tokens_in: nil`, `tokens_out: nil`, `cost: nil`) is returned so
downstream telemetry can rely on the keys always being present.

| `Dsxir.LM` usage key | `Sycophant.Usage` field |
| -------------------- | ----------------------- |
| `:tokens_in`         | `:input_tokens`         |
| `:tokens_out`        | `:output_tokens`        |
| `:cost`              | `:total_cost`           |

## Error translation

Provider errors are translated into typed `Dsxir.Errors.LM.*` structs. A
`%Sycophant.Error.Provider.BadRequest{status: 400}` is classified against a
small set of regexes (case-insensitive) on its `:body` to detect upstream
context-window-exceeded responses:

  * `~r/context length/i`
  * `~r/maximum context/i`
  * `~r/prompt is too long/i`
  * `~r/too many tokens/i`
  * `~r/exceeds.*token/i`
  * `~r/request is too large/i`

When any of those match the body, the error becomes a
`Dsxir.Errors.LM.ContextWindow` (with `prompt_tokens`/`limit` extracted when
the body carries them). All other BadRequest bodies stay as
`Dsxir.Errors.LM.RequestFailed` with `status: 400`.

---

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