Pixir.Provider (pixir v0.1.0)

Copy Markdown View Source

The Provider (ADR 0002/0003/0019): the OpenAI Responses API dialect, reached with either Credential kind. Pixir always sends store: false; the local Log remains the source of truth. The HTTP/SSE path sends folded History as full input. The WebSocket path may use connection-local previous_response_id and a late delta as an optimization, but it never replaces Log replay.

stream/2 performs one streamed model call:

  • ephemeral text / reasoning deltas are delivered to the :on_delta callback as {:text_delta, chunk} / {:reasoning_delta, chunk} (the Turn loop turns these into ephemeral Events — keeping this module free of bus/Session deps);
  • the assembled result %{text, reasoning, function_calls, finish_reason} is returned for the Turn loop to persist (the final assistant_message) and to decide whether to run tools.

Errors are structured (ADR 0005). Tests may still inject :transport directly; the production path uses Pixir.Provider.TransportPolicy (:auto | :websocket | :http_sse) so WebSocket can be preferred while HTTP/SSE remains fallback.

Summary

Functions

Resolve the model id (open knob). Precedence

Whether model_id is in the advertised catalog (models/0). Used to reject an unknown per-turn _meta.model before it reaches the backend (epic A.5, decision #8 — fail early with -32602 rather than passing through to a backend model_not_supported).

The model catalog Pixir advertises to a client (epic A.5). A list of %{"id" => slug, "name" => label, "default" => bool} (string-keyed so it rides ACP _meta verbatim). Source: the built-in list (gpt-5.5 & family), extended/overridden by a ~/.pixir/config.json "models" array of slugs if present. The active default_model/0 is always included and flagged default: true (so the client's picker has a default even if config narrows the list).

Cheaply validate connectivity and the model id: a minimal streamed call (tiny prompt, no tools). Returns {:ok, %{model: id}} if the backend accepts it, or the structured error otherwise (e.g. a provider_http_error for a bad model id). Useful before committing to a full Turn.

Build a Responses request body preview without auth or network.

Stream one Responses call, retrying transient failures (network, :rate_limited, 5xx) with capped exponential backoff. Terminal errors (:usage_limit_reached, :model_not_supported, auth) are not retried. See the module doc for the result shape. Options: :max_retries (default 2), :sleep (injectable for tests).

Map a tool definition from a __tool__/0 callback to a Responses function tool.

Normalize OpenAI usage payloads into the fields Pixir records as Provider evidence.

Types

request()

@type request() :: %{
  optional(:model) => String.t(),
  optional(:system_prompt) => String.t(),
  optional(:developer_context) => String.t(),
  optional(:history) => [map()],
  optional(:tools) => [map()],
  optional(:hosted_tools) => [map()],
  optional(:web_search) => map() | keyword() | boolean(),
  optional(:prompt_cache_key) => String.t(),
  optional(:prompt_cache_retention) => String.t(),
  optional(:output_schema) => map()
}

result()

@type result() :: %{
  text: String.t(),
  reasoning: String.t(),
  reasoning_items: [map()],
  function_calls: [%{call_id: String.t(), name: String.t(), args: map()}],
  output_items: [
    reasoning: map(),
    function_call: map(),
    provider_hosted_tool: map()
  ],
  provider_hosted_tools: map(),
  web_search: map(),
  finish_reason: :stop | :tool_calls,
  usage: map() | nil,
  usage_summary: map(),
  provider_metadata: map()
}

Functions

default_model()

Resolve the model id (open knob). Precedence:

  1. config :pixir, :model (programmatic override)
  2. PIXIR_MODEL env var
  3. ~/.pixir/config.json"model"
  4. the built-in default (gpt-5.5)

model_supported?(model_id)

@spec model_supported?(String.t()) :: boolean()

Whether model_id is in the advertised catalog (models/0). Used to reject an unknown per-turn _meta.model before it reaches the backend (epic A.5, decision #8 — fail early with -32602 rather than passing through to a backend model_not_supported).

models()

@spec models() :: [%{required(String.t()) => String.t() | boolean()}]

The model catalog Pixir advertises to a client (epic A.5). A list of %{"id" => slug, "name" => label, "default" => bool} (string-keyed so it rides ACP _meta verbatim). Source: the built-in list (gpt-5.5 & family), extended/overridden by a ~/.pixir/config.json "models" array of slugs if present. The active default_model/0 is always included and flagged default: true (so the client's picker has a default even if config narrows the list).

probe(opts \\ [])

@spec probe(keyword()) :: {:ok, %{model: String.t()}} | {:error, map()}

Cheaply validate connectivity and the model id: a minimal streamed call (tiny prompt, no tools). Returns {:ok, %{model: id}} if the backend accepts it, or the structured error otherwise (e.g. a provider_http_error for a bad model id). Useful before committing to a full Turn.

request_body_preview(request, opts \\ [])

@spec request_body_preview(
  request(),
  keyword()
) :: {:ok, map()} | {:error, map()}

Build a Responses request body preview without auth or network.

This uses the same request-shaping path as stream/2, including Provider-hosted tools, prompt-cache fields, and the px2 developer-context item. Smoke tasks should still bound/redact the returned body before printing it.

stream(request, opts \\ [])

@spec stream(
  request(),
  keyword()
) :: {:ok, result()} | {:error, map()}

Stream one Responses call, retrying transient failures (network, :rate_limited, 5xx) with capped exponential backoff. Terminal errors (:usage_limit_reached, :model_not_supported, auth) are not retried. See the module doc for the result shape. Options: :max_retries (default 2), :sleep (injectable for tests).

tool_spec(map)

@spec tool_spec(map()) :: map()

Map a tool definition from a __tool__/0 callback to a Responses function tool.

usage_summary(usage)

@spec usage_summary(map() | nil) :: map()

Normalize OpenAI usage payloads into the fields Pixir records as Provider evidence.

Supports the Responses shape (input_tokens_details.cached_tokens) and the legacy prompt/completion naming used by older examples.