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).
Summary
Functions
Makes a chat completion request to OpenRouter.
Makes an embeddings request to OpenRouter.
Extracts the text content from a chat completion response.
Extracts the reasoning / chain-of-thought from a chat completion response, for reasoning models (DeepSeek-R1, Mistral Magistral, OpenAI o-series, etc.).
Extracts usage information from a response.
Generates image(s) from a text prompt via the provider's
POST /images/generations endpoint.
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).
Functions
Makes a chat completion request to OpenRouter.
Parameters
endpoint- The AI endpoint struct with API key and modelmessages- List of message maps with:roleand:contentopts- 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. SeePhoenixKitAI.Errorsfor the full reason vocabulary and translation.
Response Structure
%{
"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
}
}
Makes an embeddings request to OpenRouter.
Parameters
endpoint- The AI endpoint struct with API key and modelinput- Text or list of texts to embedopts- Additional options
Options
:dimensions- Output dimensions (model-specific)
Returns
{:ok, response}- Response with embeddings{:error, reason}- Error atom or tagged tuple. SeePhoenixKitAI.Errorsfor the full reason vocabulary and translation.
Extracts the text content from a chat completion response.
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?/0application-config gate — when content capture is off, theresponse_reasoningmetadata 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.
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.
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 returnb64_jsonregardless; 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()}}—datais the decoded bytes when the provider returnedb64_json;urlis 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. SeePhoenixKitAI.Errorsfor the full reason vocabulary and translation.
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 modeltext- The text to synthesizeopts- 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 — seefetch_xai_voices/2inOpenRouterClientfor 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 (thegpt-4o*-ttsfamily); silently dropped otherwise. This is a real gate, not just documentation — some providers (Mistral'svoxtral-*-tts) hard-fail the whole request with HTTP 422 on an unrecognizedinstructionsfield 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 itsoutput_format/speedfields when present.
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()}}{:error, reason}- Error atom or tagged tuple. SeePhoenixKitAI.Errorsfor the full reason vocabulary and translation.