# `PhoenixKitAI.Completion`
[🔗](https://github.com/BeamLabEU/phoenix_kit_ai/blob/0.19.3/lib/phoenix_kit_ai/completion.ex#L1)

OpenRouter completion client for making AI API calls.

This module handles the actual HTTP requests to OpenRouter's chat completions
and other endpoints. It's used internally by `PhoenixKitAI` public functions.

## Supported Endpoints

- `/chat/completions` - Text and vision completions
- `/embeddings` - Text embeddings
- `/audio/speech` / xAI's `/tts` - Text-to-speech synthesis
- `/images/generations` - Image generation

## Logging conventions

- `Logger.warning` — expected/recoverable external failures (non-2xx HTTP
  responses, transport errors, rate limits). Callers see a user-facing error.
- `Logger.error` — unexpected internal failures (unknown error shapes,
  parse failures).

# `chat_completion`

Makes a chat completion request to OpenRouter.

## Parameters

- `endpoint` - The AI endpoint struct with API key and model
- `messages` - List of message maps with `:role` and `:content`
- `opts` - Additional options (temperature, max_tokens, etc.)

## Options

- `:temperature` - Sampling temperature (0-2)
- `:max_tokens` - Maximum tokens in response
- `:top_p` - Nucleus sampling parameter
- `:top_k` - Top-k sampling parameter
- `:frequency_penalty` - Frequency penalty (-2 to 2)
- `:presence_penalty` - Presence penalty (-2 to 2)
- `:repetition_penalty` - Repetition penalty (0 to 2)
- `:stop` - Stop sequences (list of strings)
- `:seed` - Random seed for reproducibility
- `:stream` - Enable streaming (default: false)

## Returns

- `{:ok, response}` - Successful response with completion
- `{:error, reason}` - Error atom or tagged tuple. See
  `PhoenixKitAI.Errors` for the full reason vocabulary and translation.

## Response Structure

```elixir
%{
  "id" => "gen-...",
  "model" => "anthropic/claude-3-haiku",
  "choices" => [
    %{
      "message" => %{
        "role" => "assistant",
        "content" => "Hello! How can I help you today?"
      },
      "finish_reason" => "stop"
    }
  ],
  "usage" => %{
    "prompt_tokens" => 10,
    "completion_tokens" => 15,
    "total_tokens" => 25
  }
}
```

# `embeddings`

Makes an embeddings request to OpenRouter.

## Parameters

- `endpoint` - The AI endpoint struct with API key and model
- `input` - Text or list of texts to embed
- `opts` - Additional options

## Options

- `:dimensions` - Output dimensions (model-specific)

## Returns

- `{:ok, response}` - Response with embeddings
- `{:error, reason}` - Error atom or tagged tuple. See
  `PhoenixKitAI.Errors` for the full reason vocabulary and translation.

# `extract_content`

Extracts the text content from a chat completion response.

# `extract_reasoning`

```elixir
@spec extract_reasoning(map()) :: String.t() | nil
```

Extracts the reasoning / chain-of-thought from a chat completion response,
for reasoning models (DeepSeek-R1, Mistral Magistral, OpenAI o-series, etc.).

Different providers put the chain-of-thought in different fields:
- OpenRouter (and most providers it proxies): `message.reasoning`
- DeepSeek native API: `message.reasoning_content`
- Some providers may use `message.thinking`

Returns the first non-empty string found, or `nil` if no reasoning is
present (i.e. for non-reasoning models or when the operator opted out
of returning reasoning via `reasoning_exclude: true`).

## `reasoning_exclude: true` and buggy providers

The endpoint's `reasoning_exclude` flag controls the REQUEST payload —
it tells the provider not to send reasoning back. A correctly-behaving
provider then returns a response without any of the three reasoning
fields and `extract_reasoning/1` returns `nil`.

A buggy provider (or one that doesn't honour the flag) might still
include reasoning. We deliberately extract it anyway rather than
gating the response-side capture on `reasoning_exclude` — the
reasoning in `metadata.response_reasoning` then doubles as a
breadcrumb that lets operators correlate "request asked for no
reasoning but provider sent it anyway" against a specific provider
+ model + request id (PR #6 review finding #9). PII / data-retention
concerns are covered by the `capture_request_content?/0`
application-config gate — when content capture is off, the
`response_reasoning` metadata is dropped too.

If you specifically want "discard reasoning when `reasoning_exclude:
true` regardless of what the provider sent", that's a separate
faithfulness-mode opt the caller would have to set on top of this
helper. The helper itself stays transparent.

# `extract_usage`

Extracts usage information from a response.

Returns a map with token counts and cost (if available from OpenRouter).
Cost is stored in nanodollars (1/1,000,000 of a dollar) to preserve precision
for cheap API calls. Stored in the cost_cents field for backward compatibility.

# `generate_image`

Generates image(s) from a text prompt via the provider's
`POST /images/generations` endpoint.

Unlike TTS, this one genuinely is the same path and response envelope
across OpenAI, OpenRouter, and xAI — `{"data": [{"url" | "b64_json",
...}]}` — so a single implementation covers all three; only the
optional request fields differ per provider/model family.

## Options

- `:n` - Number of images to generate (default 1; provider-limited —
  e.g. DALL-E 3 only supports 1)
- `:response_format` - `"url"` or `"b64_json"` (DALL-E 2/3 only — GPT
  image models always return `b64_json` regardless; xAI honors it)
- `:size` - Pixel dimensions, e.g. `"1024x1024"` (OpenAI / DALL-E / GPT
  image models)
- `:quality` - `"standard"`/`"hd"` (DALL-E 3) or `"low"`/`"medium"`/`"high"`
  (GPT image models)
- `:style` - `"vivid"`/`"natural"` (DALL-E 3 only)
- `:background`, `:output_format` - GPT image models only
- `:aspect_ratio` - e.g. `"16:9"` (**xAI only**)
- `:resolution` - `"1k"` or `"2k"` (**xAI only**)

Only options the caller passes are sent — the module stays
provider-neutral and does not assume which fields a given model
supports.

## Returns

- `{:ok, %{images: [%{url: String.t() | nil, data: binary() | nil}], latency_ms: integer()}}` —
  `data` is the decoded bytes when the provider returned `b64_json`;
  `url` is the provider's URL when it returned one instead (DALL-E's
  default, valid ~60 minutes). Never both nil for a successfully
  decoded entry.
- `{:error, reason}` - Error atom or tagged tuple. See
  `PhoenixKitAI.Errors` for the full reason vocabulary and translation.

# `text_to_speech`

Synthesizes speech from text via the provider's `/audio/speech` endpoint
(OpenAI-compatible providers), or xAI's own `POST /v1/tts` (a completely
different request/response shape — see `xai_text_to_speech/3`).

This is xAI's **batch** REST endpoint — a single request, a complete
audio file back in the response, no persistent connection. Suited to
"generate once, cache, serve from your own storage" use cases (e.g. a
language-learning app pre-rendering vocabulary audio). For live,
low-latency streaming playback, use `Xai.Realtime` directly (what the
Playground's "Streaming Voice (xAI)" panel does) — that path returns no
cacheable file, just a stream of chunks meant for one immediate playback.

## Parameters

- `endpoint` - The AI endpoint struct with API key and model
- `text` - The text to synthesize
- `opts` - Additional options

## Options

- `:response_format` - Audio container/codec (default `"mp3"`)
- `:voice` - Preset voice identifier (OpenRouter / OSS vLLM shape)
- `:voice_id` - Saved/cloned voice id (Mistral hosted shape; also xAI's
  voice field — see `fetch_xai_voices/2` in `OpenRouterClient` for the
  list, e.g. `"eve"`)
- `:instructions` - Steering text for models that support it (e.g.
  `gpt-4o-mini-tts`'s accent/tone/pacing/language control). Only sent
  to models known to support it (the `gpt-4o*-tts` family); silently
  dropped otherwise. This is a real gate, not just documentation —
  some providers (Mistral's `voxtral-*-tts`) hard-fail the whole
  request with HTTP 422 on an unrecognized `instructions` field
  rather than ignoring it, so the module must not send it blindly.
- `:language` - **xAI only**, ignored by other providers. BCP-47 code
  (`"en"`, `"pt-BR"`, ...) or `"auto"` for automatic detection.
  Defaults to `"auto"`.
- `:sample_rate`, `:bit_rate`, `:speed` - **xAI only**, passed through
  to its `output_format` / `speed` fields when present.
- `:with_timestamps` - **xAI only**, ignored by other providers (neither
  Mistral's Voxtral TTS nor OpenAI's `/audio/speech` offer anything
  equivalent). When `true`, xAI returns character-level timing data
  alongside the audio (see `Returns` below) — no cost difference, per
  xAI's pricing page; adds latency for the post-synthesis alignment
  pass on xAI's side.

Only the voice option that is present is sent; the module stays
provider-neutral and does not assume a field name.

## Returns

- `{:ok, %{audio: binary(), format: String.t(), latency_ms: integer(),
  timestamps: [%{char:, start:, end:}] | nil}}` - `timestamps` is `nil`
  unless `:with_timestamps` was passed to an xAI endpoint and the
  response actually included them; `char` is a single input character,
  `start`/`end` its position in seconds as floats. Word boundaries
  aren't provided directly — derive them by splitting on whitespace.
- `{:error, reason}` - Error atom or tagged tuple. See
  `PhoenixKitAI.Errors` for the full reason vocabulary and translation.

---

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