PhoenixKitAI.Completion (PhoenixKitAI v0.15.0)

Copy Markdown View Source

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

chat_completion(endpoint, messages, opts \\ [])

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

%{
  "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(endpoint, input, opts \\ [])

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(response)

Extracts the text content from a chat completion response.

extract_reasoning(response)

@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(arg1)

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(endpoint, prompt, opts \\ [])

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(endpoint, text, opts \\ [])

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.

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. See PhoenixKitAI.Errors for the full reason vocabulary and translation.