Main context for PhoenixKit AI system.
Provides AI endpoint management and usage tracking for AI API requests.
Architecture
Each Endpoint is a unified configuration that combines:
- Provider credentials (api_key, base_url, provider_settings)
- Model selection (single model per endpoint)
- Generation parameters (temperature, max_tokens, etc.)
Users create as many endpoints as needed, each representing one complete AI configuration ready for making API requests.
Core Functions
System Management
enabled?/0- Check if AI module is enabledenable_system/0- Enable the AI moduledisable_system/0- Disable the AI moduleget_config/0- Get module configuration with statistics
Endpoint CRUD
list_endpoints/1- List all endpoints with filtersget_endpoint!/1- Get endpoint by UUID (raises)get_endpoint/1- Get endpoint by UUIDcreate_endpoint/1- Create new endpointupdate_endpoint/2- Update existing endpointdelete_endpoint/1- Delete endpoint
Completion API
ask/3- Simple single-turn completioncomplete/3- Multi-turn chat completionembed/3- Generate embeddings
Usage Tracking
list_requests/1- List requests with pagination/filterscreate_request/1- Log a new requestget_usage_stats/1- Get aggregated statisticsget_dashboard_stats/0- Get stats for dashboard display
Usage Examples
# Enable the module
PhoenixKitAI.enable_system()
# Create an endpoint
{:ok, endpoint} = PhoenixKitAI.create_endpoint(%{
name: "Claude Fast",
provider: "openrouter",
api_key: "sk-or-v1-...",
model: "anthropic/claude-3-haiku",
temperature: 0.7
})
# Use the endpoint
{:ok, response} = PhoenixKitAI.ask(endpoint.uuid, "Hello!")
# Extract the response text
{:ok, text} = PhoenixKitAI.extract_content(response)Configuration
# Persist message + response content in request metadata (default: true).
# Disable for deployments with PII / data-retention obligations — token
# counts, latency, model, and cost are still recorded.
config :phoenix_kit_ai, capture_request_content: false
# Capture process memory in request caller_context (default: false).
config :phoenix_kit_ai, capture_request_memory: true
# Allow endpoint base_url to point at private/loopback IPs (default: false).
# Required for self-hosted Ollama / intranet inference.
config :phoenix_kit_ai, allow_internal_endpoint_urls: true
Summary
Functions
Simple helper for single-turn chat completion.
Makes an AI completion using a prompt template.
Returns an endpoint changeset for use in forms.
Returns a prompt changeset for use in forms.
Makes a chat completion request using a configured endpoint.
Makes an AI completion with a prompt template as the system message.
Counts the number of enabled endpoints.
Counts the number of enabled prompts.
Counts the total number of endpoints.
Counts the total number of prompts.
Counts the total number of requests.
Creates a new AI endpoint.
Creates a new AI prompt.
Creates a new AI request record.
Deletes an AI endpoint.
Deletes an AI prompt.
Disables a prompt.
Disables the AI module.
Duplicates a prompt with a new name.
Makes an embeddings request using a configured endpoint.
Enables a prompt.
Enables the AI module.
Checks if the AI module is enabled.
Returns the PubSub topic for AI endpoints. Subscribe to this topic to receive real-time updates.
Extracts the text content from a completion response.
Extracts usage information from a response.
Gets the AI module configuration with statistics.
Gets dashboard statistics for display.
Gets a single endpoint by UUID.
Gets a single endpoint by UUID.
Returns usage statistics for each endpoint.
Gets a single prompt by UUID.
Gets a single prompt by UUID.
Gets a prompt by slug.
Gets usage statistics for all prompts.
Gets the variables defined in a prompt.
Finds all prompts that use a specific variable.
Gets a single request by UUID.
Gets a single request by UUID.
Returns filter options for requests (distinct endpoints, models, and sources).
Gets request counts grouped by day.
Gets token usage grouped by model.
Gets aggregated usage statistics.
Increments the usage count for a prompt and updates last_used_at.
Lists only enabled prompts.
Lists all AI endpoints.
Lists all AI prompts.
Lists AI requests with pagination and filters.
Marks an endpoint as validated by updating its last_validated_at timestamp.
Combined boot-time migration entry point.
Previews a rendered prompt without making an AI call.
Returns the PubSub topic for AI prompts.
Increments the usage count for a prompt and updates last_used_at.
Renders a prompt by replacing variables with provided values.
Updates the sort order for multiple prompts.
Returns the PubSub topic for AI requests/usage.
Resets the usage statistics for a prompt.
Resolves an endpoint from an ID (UUID string) or Endpoint struct.
Resolves a prompt from various input types.
One-shot auto-migrator for legacy endpoint.api_key values into
PhoenixKit.Integrations connections.
Searches prompts by name, description, or content.
Subscribes the current process to AI endpoint changes.
Subscribes the current process to AI prompt changes.
Subscribes the current process to AI request/usage changes.
Sums the total tokens used across all requests.
Updates an existing AI endpoint.
Updates an existing AI prompt.
Validates that a prompt is ready for use.
Validates that the content has valid variable syntax.
Validates that all required variables are provided for a prompt.
Functions
@spec ask(String.t() | PhoenixKitAI.Endpoint.t(), String.t(), keyword()) :: {:ok, map()} | {:error, term()}
Simple helper for single-turn chat completion.
Parameters
endpoint_uuid- Endpoint UUID string or Endpoint structprompt- User prompt stringopts- Optional parameter overrides and system message
Options
All options from complete/3 plus:
:system- System message string:source- Override auto-detected source for request tracking
Examples
# Simple question
{:ok, response} = PhoenixKitAI.ask(endpoint_uuid, "What is the capital of France?")
# With system message
{:ok, response} = PhoenixKitAI.ask(endpoint_uuid, "Translate: Hello",
system: "You are a translator. Translate to French."
)
# With custom source for tracking
{:ok, response} = PhoenixKitAI.ask(endpoint_uuid, "Hello!",
source: "Languages"
)
# Extract just the text content
{:ok, response} = PhoenixKitAI.ask(endpoint_uuid, "Hello!")
{:ok, text} = PhoenixKitAI.extract_content(response)Returns
Same as complete/3
Makes an AI completion using a prompt template.
The prompt content is rendered with the provided variables and sent as the user message.
@spec change_endpoint(PhoenixKitAI.Endpoint.t(), map()) :: Ecto.Changeset.t()
Returns an endpoint changeset for use in forms.
@spec change_prompt(PhoenixKitAI.Prompt.t(), map()) :: Ecto.Changeset.t()
Returns a prompt changeset for use in forms.
@spec complete(String.t() | PhoenixKitAI.Endpoint.t(), [map()], keyword()) :: {:ok, map()} | {:error, term()}
Makes a chat completion request using a configured endpoint.
Parameters
endpoint_uuid- Endpoint UUID string or Endpoint structmessages- List of message maps with:roleand:contentopts- Optional parameter overrides
Options
All standard completion parameters plus:
:source- Override auto-detected source for request tracking
Examples
{:ok, response} = PhoenixKitAI.complete(endpoint_uuid, [
%{role: "user", content: "Hello!"}
])
# With system message
{:ok, response} = PhoenixKitAI.complete(endpoint_uuid, [
%{role: "system", content: "You are a helpful assistant."},
%{role: "user", content: "What is 2+2?"}
])
# With parameter overrides
{:ok, response} = PhoenixKitAI.complete(endpoint_uuid, messages,
temperature: 0.5,
max_tokens: 500
)
# With custom source for tracking
{:ok, response} = PhoenixKitAI.complete(endpoint_uuid, messages,
source: "MyModule"
)Returns
{:ok, response}- Full API response including usage stats{:error, reason}- Error atom or tagged tuple. SeePhoenixKitAI.Errorsfor the vocabulary and translation.
Makes an AI completion with a prompt template as the system message.
The prompt is rendered and used as the system message, with the user_message as the user message.
@spec count_enabled_endpoints() :: non_neg_integer()
Counts the number of enabled endpoints.
@spec count_enabled_prompts() :: non_neg_integer()
Counts the number of enabled prompts.
@spec count_endpoints() :: non_neg_integer()
Counts the total number of endpoints.
@spec count_prompts() :: non_neg_integer()
Counts the total number of prompts.
@spec count_requests() :: non_neg_integer()
Counts the total number of requests.
@spec create_endpoint( map(), keyword() ) :: {:ok, PhoenixKitAI.Endpoint.t()} | {:error, Ecto.Changeset.t()}
Creates a new AI endpoint.
Examples
{:ok, endpoint} = PhoenixKitAI.create_endpoint(%{
name: "Claude Fast",
provider: "openrouter",
api_key: "sk-or-v1-...",
model: "anthropic/claude-3-haiku",
temperature: 0.7
})
@spec create_prompt( map(), keyword() ) :: {:ok, PhoenixKitAI.Prompt.t()} | {:error, Ecto.Changeset.t()}
Creates a new AI prompt.
Examples
{:ok, prompt} = PhoenixKitAI.create_prompt(%{
name: "Translator",
content: "Translate the following text to {{Language}}:\n\n{{Text}}"
})
Creates a new AI request record.
Used to log every AI API call for tracking and statistics.
@spec delete_endpoint( PhoenixKitAI.Endpoint.t(), keyword() ) :: {:ok, PhoenixKitAI.Endpoint.t()} | {:error, Ecto.Changeset.t()}
Deletes an AI endpoint.
@spec delete_prompt( PhoenixKitAI.Prompt.t(), keyword() ) :: {:ok, PhoenixKitAI.Prompt.t()} | {:error, Ecto.Changeset.t()}
Deletes an AI prompt.
@spec disable_prompt(String.t()) :: {:ok, PhoenixKitAI.Prompt.t()} | {:error, term()}
Disables a prompt.
Disables the AI module.
@spec duplicate_prompt(String.t(), String.t()) :: {:ok, PhoenixKitAI.Prompt.t()} | {:error, term()}
Duplicates a prompt with a new name.
@spec embed( String.t() | PhoenixKitAI.Endpoint.t(), String.t() | [String.t()], keyword() ) :: {:ok, map()} | {:error, term()}
Makes an embeddings request using a configured endpoint.
Parameters
endpoint_uuid- Endpoint UUID string or Endpoint structinput- Text or list of texts to embedopts- Optional parameter overrides
Options
:dimensions- Override embedding dimensions:source- Override auto-detected source for request tracking
Examples
# Single text
{:ok, response} = PhoenixKitAI.embed(endpoint_uuid, "Hello, world!")
# Multiple texts
{:ok, response} = PhoenixKitAI.embed(endpoint_uuid, ["Hello", "World"])
# With dimension override
{:ok, response} = PhoenixKitAI.embed(endpoint_uuid, "Hello", dimensions: 512)
# With custom source for tracking
{:ok, response} = PhoenixKitAI.embed(endpoint_uuid, "Hello",
source: "SemanticSearch"
)Returns
{:ok, response}- Response with embeddings{:error, reason}- Error atom or tagged tuple.
@spec enable_prompt(String.t()) :: {:ok, PhoenixKitAI.Prompt.t()} | {:error, term()}
Enables a prompt.
Enables the AI module.
@spec enabled?() :: boolean()
Checks if the AI module is enabled.
@spec endpoints_topic() :: String.t()
Returns the PubSub topic for AI endpoints. Subscribe to this topic to receive real-time updates.
Extracts the text content from a completion response.
Examples
{:ok, response} = PhoenixKitAI.ask(endpoint_uuid, "Hello!")
{:ok, text} = PhoenixKitAI.extract_content(response)
# => "Hello! How can I help you today?"
Extracts usage information from a response.
Examples
{:ok, response} = PhoenixKitAI.complete(endpoint_uuid, messages)
usage = PhoenixKitAI.extract_usage(response)
# => %{prompt_tokens: 10, completion_tokens: 15, total_tokens: 25}
@spec get_config() :: %{ enabled: boolean(), endpoints_count: non_neg_integer(), total_requests: non_neg_integer(), total_tokens: non_neg_integer() }
Gets the AI module configuration with statistics.
Stat queries are wrapped in a try/rescue so that environments without a
live Repo connection (early boot, test cases without sandbox checkout)
still get a well-formed map — matching the defensive pattern in
enabled?/0.
Gets dashboard statistics for display.
Returns stats for the last 30 days plus all-time totals.
@spec get_endpoint(term()) :: PhoenixKitAI.Endpoint.t() | nil
Gets a single endpoint by UUID.
Accepts a UUID string (e.g., "550e8400-e29b-41d4-a716-446655440000").
Returns nil if the endpoint does not exist.
@spec get_endpoint!(String.t()) :: PhoenixKitAI.Endpoint.t()
Gets a single endpoint by UUID.
Raises Ecto.NoResultsError if the endpoint does not exist.
Returns usage statistics for each endpoint.
Returns a map of endpoint_uuid => %{request_count, total_tokens, total_cost, last_used_at}
@spec get_prompt(term()) :: PhoenixKitAI.Prompt.t() | nil
Gets a single prompt by UUID.
Accepts a UUID string (e.g., "550e8400-e29b-41d4-a716-446655440000").
Returns nil if the prompt does not exist.
@spec get_prompt!(String.t()) :: PhoenixKitAI.Prompt.t()
Gets a single prompt by UUID.
Raises Ecto.NoResultsError if the prompt does not exist.
@spec get_prompt_by_slug(String.t()) :: PhoenixKitAI.Prompt.t() | nil
Gets a prompt by slug.
Returns nil if the prompt does not exist.
Gets usage statistics for all prompts.
Gets the variables defined in a prompt.
Finds all prompts that use a specific variable.
Gets a single request by UUID.
Accepts a UUID string (e.g., "550e8400-e29b-41d4-a716-446655440000").
Returns nil if the request does not exist.
Gets a single request by UUID.
Returns filter options for requests (distinct endpoints, models, and sources).
Gets request counts grouped by day.
Gets token usage grouped by model.
Gets aggregated usage statistics.
Options
:since- Start date for statistics:until- End date for statistics:endpoint_uuid- Filter by endpoint
Returns
Map with statistics including total_requests, total_tokens, success_rate, etc.
Increments the usage count for a prompt and updates last_used_at.
@spec list_enabled_prompts() :: [PhoenixKitAI.Prompt.t()] | {[PhoenixKitAI.Prompt.t()], non_neg_integer()}
Lists only enabled prompts.
Convenience wrapper for list_prompts(enabled: true).
Examples
PhoenixKitAI.list_enabled_prompts()
@spec list_endpoints(keyword()) :: {[PhoenixKitAI.Endpoint.t()], non_neg_integer()}
Lists all AI endpoints.
Options
:provider- Filter by provider type:enabled- Filter by enabled status:preload- Associations to preload
Examples
PhoenixKitAI.list_endpoints()
PhoenixKitAI.list_endpoints(provider: "openrouter", enabled: true)
@spec list_prompts(keyword()) :: [PhoenixKitAI.Prompt.t()] | {[PhoenixKitAI.Prompt.t()], non_neg_integer()}
Lists all AI prompts.
Options
:sort_by- Field to sort by (default: :sort_order):sort_dir- Sort direction, :asc or :desc (default: :asc):enabled- Filter by enabled status
Examples
PhoenixKitAI.list_prompts()
PhoenixKitAI.list_prompts(sort_by: :name, sort_dir: :asc)
PhoenixKitAI.list_prompts(enabled: true)
Lists AI requests with pagination and filters.
Options
:page- Page number (default: 1):page_size- Results per page (default: 20):endpoint_uuid- Filter by endpoint:user_uuid- Filter by user:status- Filter by status:model- Filter by model:source- Filter by source (from metadata):since- Filter by date (requests after this date):preload- Associations to preload
Returns
{requests, total_count}
@spec mark_endpoint_validated(PhoenixKitAI.Endpoint.t()) :: {:ok, PhoenixKitAI.Endpoint.t()} | {:error, Ecto.Changeset.t()}
Marks an endpoint as validated by updating its last_validated_at timestamp.
Combined boot-time migration entry point.
Runs both legacy data transitions for AI:
Local api_key → Integrations row + integration_uuid (delegates to
run_legacy_api_key_migration/0). Endpoints with bareprovider == "openrouter"and a non-emptyapi_keyget grouped, get an Integration row created, and have bothproviderANDintegration_uuidupdated to point at it.provider-string →integration_uuid(sweep). Endpoints withintegration_uuid IS NULLwhoseproviderfield resolves to a real integration uuid (either bare provider,provider:nameshape, or a uuid stuffed in the string column from pre-V107 form saves) get theirintegration_uuidpopulated. V107's migration backfilled most of these at install time; this pass catches stragglers (e.g., endpoints created post-form-update but pre-V107).
Both kinds log to PhoenixKit.Activity per migrated record / group
with mode: "auto" and module "ai". PII-safe: never logs
api_key values.
Idempotent — run on every host-app boot. Designed to be invoked via
the orchestrator (PhoenixKit.ModuleRegistry.run_all_legacy_migrations/0),
but can be called directly for ad-hoc migration runs.
Previews a rendered prompt without making an AI call.
@spec prompts_topic() :: String.t()
Returns the PubSub topic for AI prompts.
@spec record_prompt_usage(PhoenixKitAI.Prompt.t()) :: {:ok, PhoenixKitAI.Prompt.t()} | {:error, Ecto.Changeset.t()}
Increments the usage count for a prompt and updates last_used_at.
Renders a prompt by replacing variables with provided values.
Returns {:ok, rendered_text} or {:error, reason}.
Updates the sort order for multiple prompts.
Accepts prompt UUIDs.
@spec requests_topic() :: String.t()
Returns the PubSub topic for AI requests/usage.
Resets the usage statistics for a prompt.
@spec resolve_endpoint(term()) :: {:ok, PhoenixKitAI.Endpoint.t()} | {:error, :endpoint_not_found | :invalid_endpoint_identifier}
Resolves an endpoint from an ID (UUID string) or Endpoint struct.
Examples
{:ok, endpoint} = PhoenixKitAI.resolve_endpoint("019abc12-3456-7def-8901-234567890abc")
{:ok, endpoint} = PhoenixKitAI.resolve_endpoint(endpoint)
Resolves a prompt from various input types.
Accepts:
- UUID string (e.g., "019abc12-3456-7def-8901-234567890abc")
- String slug (e.g., "my-prompt")
- Prompt struct (returned as-is)
Returns {:ok, prompt} or {:error, reason}.
@spec run_legacy_api_key_migration() :: :ok
One-shot auto-migrator for legacy endpoint.api_key values into
PhoenixKit.Integrations connections.
Mirrors the pattern of PhoenixKit.Integrations.run_legacy_migrations/0
— call it at host-app boot to fold pre-Integrations endpoint api_keys
into the named-connection model. Safe to call multiple times:
multiple idempotency guards short-circuit on already-migrated state.
What it does
For each phoenix_kit_ai_endpoints row whose provider is the bare
string "openrouter" (i.e., NOT already pointing at a named
Integrations connection like "openrouter:my-key") AND whose
api_key is non-empty:
- Group by api_key value — endpoints sharing a key share one connection (dedup).
- Create a
PhoenixKit.Integrationsconnection per distinct key. Naming:"openrouter:default"if there's exactly one key in the deployment;"openrouter:imported-1","openrouter:imported-2"(1-indexed by first-seen order) if there are multiple. - Update each endpoint's
providerfield to point at the new connection key (e.g.,"openrouter:default").
The legacy api_key column is NEVER cleared — it stays on each row
as a safety net. OpenRouterClient.resolve_api_key/2 prefers
Integrations, so post-migration endpoints stop firing the legacy
warning; if Integrations later breaks for any reason, the column
still has the value and the fallback path keeps working.
Idempotency guards (any one short-circuits)
- The
ai_legacy_api_key_migration_completed_atsetting is set → already ran, skip. - ANY
integration:openrouter:*key already exists inphoenix_kit_settings(operator already set up Integrations manually) → mark completed and skip. - NO endpoints have
provider == "openrouter"with a non-emptyapi_key→ nothing to migrate, mark completed.
Failure modes
Top-level try/rescue/catch :exit so DB outages, race conditions,
or any unexpected exception NEVER crashes the host app's boot.
Per-key-group operations are isolated — one bad group doesn't abort
the others. Partial migration is safe because un-migrated endpoints
still resolve via the legacy fallback path.
Configuration
No options. Disable by simply not calling the function.
Searches prompts by name, description, or content.
@spec subscribe_endpoints() :: :ok | {:error, term()}
Subscribes the current process to AI endpoint changes.
@spec subscribe_prompts() :: :ok | {:error, term()}
Subscribes the current process to AI prompt changes.
@spec subscribe_requests() :: :ok | {:error, term()}
Subscribes the current process to AI request/usage changes.
@spec sum_tokens() :: non_neg_integer()
Sums the total tokens used across all requests.
@spec update_endpoint(PhoenixKitAI.Endpoint.t(), map(), keyword()) :: {:ok, PhoenixKitAI.Endpoint.t()} | {:error, Ecto.Changeset.t()}
Updates an existing AI endpoint.
Accepts an :actor_uuid option so the mutation can be attributed in
the activity feed. If the change toggles the enabled flag an
additional endpoint.enabled / endpoint.disabled entry is logged.
@spec update_prompt(PhoenixKitAI.Prompt.t(), map(), keyword()) :: {:ok, PhoenixKitAI.Prompt.t()} | {:error, Ecto.Changeset.t()}
Updates an existing AI prompt.
Accepts an :actor_uuid option so the mutation can be attributed in
the activity feed. If the change toggles the enabled flag an
additional prompt.enabled / prompt.disabled entry is logged.
Validates that a prompt is ready for use.
Returns {:ok, prompt} if valid, or {:error, reason} if not.
Validates that the content has valid variable syntax.
Validates that all required variables are provided for a prompt.