towerops/priv/repo/migrations/20260510170000_create_llm_usage.exs
Graham McIntire 01d64a446e refactor(llm): split token tracking into a dedicated llm_usage table
Token-usage doesn't belong on the per-insight row — every consumer of
Towerops.LLM (insight enrichment today; billing-anomaly summaries,
monitoring narratives, fleet weekly reports tomorrow) needs to record
into one queryable place. Per-insight columns also made it impossible
to track LLM calls that aren't tied to an insight.

Schema: `llm_usage` keyed (organization_id, day, model, purpose) with a
unique index. organization_id is nullable for system-level analyses.
Each row carries running totals plus a request_count — callers UPSERT
and increment atomically.

API: `Towerops.LLM.Usage.record/1` for write, `summarize/2` for
read-side rollups. The worker now calls `Usage.record(%{... purpose:
"insight_enrichment"})` after each successful enrichment.

Removed: `llm_prompt_tokens` / `llm_completion_tokens` from
preseem_insights — same migration drops the columns and creates
llm_usage in one go. `apply_llm_enrichment` reverts to /3 (no usage
arg).
2026-05-10 13:13:52 -05:00

41 lines
1.5 KiB
Elixir

defmodule Towerops.Repo.Migrations.CreateLlmUsage do
use Ecto.Migration
def change do
create table(:llm_usage, primary_key: false) do
add :id, :binary_id, primary_key: true
# Nullable: future system-level analysis ("which orgs are
# at-risk this week?") may not be scoped to a single org.
add :organization_id,
references(:organizations, type: :binary_id, on_delete: :delete_all)
add :day, :date, null: false
add :model, :string, null: false
# "insight_enrichment", "alert_summary", "billing_anomaly", etc.
# Free-form so new callers don't need a schema change.
add :purpose, :string, null: false
add :prompt_tokens, :integer, null: false, default: 0
add :completion_tokens, :integer, null: false, default: 0
add :request_count, :integer, null: false, default: 0
timestamps(type: :utc_datetime)
end
# One row per (org, day, model, purpose) so callers can UPSERT and
# increment cheaply.
create unique_index(:llm_usage, [:organization_id, :day, :model, :purpose],
name: :llm_usage_org_day_model_purpose_idx,
nulls_distinct: false
)
create index(:llm_usage, [:day])
create index(:llm_usage, [:organization_id, :day])
# Drop the per-insight token columns — superseded by the new table.
alter table(:preseem_insights) do
remove :llm_prompt_tokens, :integer
remove :llm_completion_tokens, :integer
end
end
end