Packages
phoenix_kit
1.7.53
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/modules/ai/request.ex
defmodule PhoenixKit.Modules.AI.Request do
@moduledoc """
AI request schema for PhoenixKit AI system.
Tracks every AI API request for usage history and statistics.
Used for monitoring costs, performance, and debugging.
## Schema Fields
### Request Identity
- `endpoint_uuid`: Foreign key to the AI endpoint used
- `endpoint_name`: Denormalized endpoint name for historical display
- `prompt_uuid`: Foreign key to the AI prompt used (if request used a prompt template)
- `prompt_name`: Denormalized prompt name for historical display
- `user_uuid`: Foreign key to the user who made the request (nullable if user deleted)
- `slot_index`: Which slot was used (deprecated, for backward compatibility)
### Request Details
- `model`: Model identifier (e.g., "anthropic/claude-3-haiku")
- `request_type`: Type of request (e.g., "text_completion", "chat")
### Token Usage
- `input_tokens`: Number of tokens in the prompt
- `output_tokens`: Number of tokens in the response
- `total_tokens`: Total tokens used (input + output)
### Performance & Cost
- `cost_cents`: Estimated cost in nanodollars (when available)
- `latency_ms`: Response time in milliseconds
- `status`: Request status - "success", "error", or "timeout"
- `error_message`: Error details if status is not "success"
### Metadata
- `metadata`: Additional context (temperature, max_tokens, etc.)
## Status Types
- `success` - Request completed successfully
- `error` - Request failed with an error
- `timeout` - Request timed out
## Usage Examples
# Log a successful request
{:ok, request} = PhoenixKit.Modules.AI.create_request(%{
endpoint_uuid: endpoint.uuid,
endpoint_name: "Claude Fast",
user_uuid: user.uuid,
model: "anthropic/claude-3-haiku",
request_type: "chat",
input_tokens: 150,
output_tokens: 320,
total_tokens: 470,
latency_ms: 850,
status: "success",
metadata: %{"temperature" => 0.7}
})
# Log a failed request
{:ok, request} = PhoenixKit.Modules.AI.create_request(%{
endpoint_uuid: endpoint.uuid,
endpoint_name: "Claude Fast",
model: "anthropic/claude-3-haiku",
status: "error",
error_message: "Rate limit exceeded"
})
"""
use Ecto.Schema
import Ecto.Changeset
alias PhoenixKit.Modules.AI.Endpoint
alias PhoenixKit.Modules.AI.Prompt
alias PhoenixKit.Users.Auth.User
@primary_key {:uuid, UUIDv7, autogenerate: true}
@valid_statuses ~w(success error timeout)
@valid_request_types ~w(text_completion chat embedding)
@derive {Jason.Encoder,
only: [
:uuid,
:endpoint_name,
:prompt_name,
:slot_index,
:model,
:request_type,
:input_tokens,
:output_tokens,
:total_tokens,
:cost_cents,
:latency_ms,
:status,
:error_message,
:metadata,
:inserted_at
]}
schema "phoenix_kit_ai_requests" do
# New endpoint system fields
field :endpoint_name, :string
# Prompt tracking (when request uses a prompt template)
field :prompt_name, :string
# Request details
field :slot_index, :integer
field :model, :string
field :request_type, :string, default: "chat"
field :input_tokens, :integer, default: 0
field :output_tokens, :integer, default: 0
field :total_tokens, :integer, default: 0
field :cost_cents, :integer
field :latency_ms, :integer
field :status, :string, default: "success"
field :error_message, :string
field :metadata, :map, default: %{}
# Associations
belongs_to :endpoint, Endpoint, foreign_key: :endpoint_uuid, references: :uuid, type: UUIDv7
belongs_to :prompt, Prompt, foreign_key: :prompt_uuid, references: :uuid, type: UUIDv7
field :account_uuid, UUIDv7
belongs_to :user, User, foreign_key: :user_uuid, references: :uuid, type: UUIDv7
timestamps(type: :utc_datetime)
end
@doc """
Creates a changeset for request creation.
"""
def changeset(request, attrs) do
request
|> cast(attrs, [
:endpoint_uuid,
:endpoint_name,
:prompt_uuid,
:prompt_name,
:account_uuid,
:user_uuid,
:slot_index,
:model,
:request_type,
:input_tokens,
:output_tokens,
:total_tokens,
:cost_cents,
:latency_ms,
:status,
:error_message,
:metadata
])
|> validate_required([:status])
|> validate_inclusion(:status, @valid_statuses)
|> validate_inclusion(:request_type, @valid_request_types)
|> validate_number(:input_tokens, greater_than_or_equal_to: 0)
|> validate_number(:output_tokens, greater_than_or_equal_to: 0)
|> validate_number(:total_tokens, greater_than_or_equal_to: 0)
|> validate_number(:latency_ms, greater_than_or_equal_to: 0)
|> calculate_total_tokens()
|> foreign_key_constraint(:endpoint_uuid)
|> foreign_key_constraint(:user_uuid)
end
@doc """
Returns the list of valid status types.
"""
def valid_statuses, do: @valid_statuses
@doc """
Returns the list of valid request types.
"""
def valid_request_types, do: @valid_request_types
@doc """
Returns a human-readable status label.
"""
def status_label("success"), do: "Success"
def status_label("error"), do: "Error"
def status_label("timeout"), do: "Timeout"
def status_label(_), do: "Unknown"
@doc """
Returns a CSS class for the status badge.
"""
def status_color("success"), do: "badge-success"
def status_color("error"), do: "badge-error"
def status_color("timeout"), do: "badge-warning"
def status_color(_), do: "badge-neutral"
@doc """
Formats the latency for display.
"""
def format_latency(nil), do: "-"
def format_latency(ms) when ms < 1000, do: "#{ms}ms"
def format_latency(ms), do: "#{Float.round(ms / 1000, 1)}s"
@doc """
Formats the token count for display.
"""
def format_tokens(nil), do: "-"
def format_tokens(0), do: "0"
def format_tokens(tokens) when tokens < 1000, do: "#{tokens}"
def format_tokens(tokens) when tokens < 1_000_000, do: "#{Float.round(tokens / 1000, 1)}K"
def format_tokens(tokens), do: "#{Float.round(tokens / 1_000_000, 2)}M"
@doc """
Formats the cost for display.
Cost is stored in nanodollars (1/1000000 of a dollar) for precision.
Shows appropriate precision based on the amount:
- >= $1.00: 2 decimal places ($1.23)
- >= $0.01: 2 decimal places ($0.05)
- >= $0.0001: 4 decimal places ($0.0012)
- > $0: 6 decimal places ($0.000030)
"""
def format_cost(nil), do: "-"
def format_cost(0), do: "$0.00"
def format_cost(nanodollars) when is_integer(nanodollars) do
# Convert from nanodollars (1/1000000 of a dollar) to dollars
dollars = nanodollars / 1_000_000
cond do
dollars >= 0.01 -> "$#{:erlang.float_to_binary(dollars, decimals: 2)}"
dollars >= 0.0001 -> "$#{:erlang.float_to_binary(dollars, decimals: 4)}"
dollars > 0 -> "$#{:erlang.float_to_binary(dollars, decimals: 6)}"
true -> "$0.00"
end
end
@doc """
Extracts the model name without provider prefix.
"""
def short_model_name(nil), do: "-"
def short_model_name(""), do: "-"
def short_model_name(model) do
case String.split(model, "/") do
[_provider, name | _rest] -> name
[name] -> name
_ -> model
end
end
# Private functions
defp calculate_total_tokens(changeset) do
input = get_field(changeset, :input_tokens) || 0
output = get_field(changeset, :output_tokens) || 0
total = get_field(changeset, :total_tokens) || 0
# Only calculate if total is 0 or not set
if total == 0 and (input > 0 or output > 0) do
put_change(changeset, :total_tokens, input + output)
else
changeset
end
end
end