Packages
llm_db
2026.4.8
2026.7.3
2026.7.2
2026.7.1
2026.7.0
2026.6.4
2026.6.3
2026.6.2
2026.6.1
2026.6.0
2026.5.2
2026.5.1
2026.5.0
2026.4.8
2026.4.7
2026.4.6
2026.4.5
2026.4.4
2026.4.3
2026.4.2
2026.4.1
2026.4.0
2026.3.3
2026.3.2
2026.3.1
2026.3.0
2026.2.9
2026.2.8
2026.2.7
2026.2.6
2026.2.5
2026.2.4
2026.2.3
2026.2.2
2026.2.1
2026.2.0
2026.1.5
2026.1.4
2026.1.3
2026.1.2
2026.1.1
2026.1.0
2025.12.4
2025.12.3
2025.12.2
2025.12.1
2025.11.18-preview
2025.11.14-preview
2025.11.7-preview
LLM model metadata catalog with fast, capability-aware lookups.
Current section
Files
Jump to
Current section
Files
lib/llm_db/history.ex
defmodule LLMDB.History do
@moduledoc """
Read-only runtime access to generated model history artifacts.
History is loaded from `priv/llm_db/history` (or `config :llm_db, :history_dir`).
NDJSON files are parsed into ETS-backed indexes for fast timeline/recent lookups.
"""
@table :llm_db_history
@table_heir_key {__MODULE__, :table_heir}
@load_lock {__MODULE__, :index_load}
@max_limit 500
@type event :: map()
@doc """
Returns `true` when history artifacts are available and readable.
"""
@spec available?() :: boolean()
def available? do
case ensure_loaded() do
{:ok, _} -> true
_ -> false
end
end
@doc """
Returns parsed history metadata from `meta.json`.
"""
@spec meta() :: {:ok, map()} | {:error, :history_unavailable | term()}
def meta do
with {:ok, _} <- ensure_loaded(),
[{:meta, meta}] <- :ets.lookup(@table, :meta) do
{:ok, meta}
else
{:error, _reason} -> {:error, :history_unavailable}
_ -> {:error, :history_unavailable}
end
end
@doc """
Returns all timeline events for a model, lineage-aware and ordered by
`captured_at` then `event_id` ascending.
"""
@spec timeline(atom() | String.t(), String.t()) :: {:ok, [event()]} | {:error, term()}
def timeline(provider, model_id) when is_binary(model_id) do
with {:ok, provider_str} <- normalize_provider(provider),
{:ok, _} <- ensure_loaded(),
[{:model_index, model_index}] <- :ets.lookup(@table, :model_index),
[{:lineage_index, lineage_index}] <- :ets.lookup(@table, :lineage_index),
[{:lineage_by_model, lineage_by_model}] <- :ets.lookup(@table, :lineage_by_model) do
model_key = provider_str <> ":" <> model_id
lineage_key = Map.get(lineage_by_model, model_key, model_key)
events =
Map.get(lineage_index, lineage_key) ||
Map.get(model_index, model_key, [])
{:ok, events}
else
{:error, reason} -> {:error, reason}
_ -> {:error, :history_unavailable}
end
end
@doc """
Returns most recent events globally, ordered by `captured_at` then `event_id` descending.
Limit is capped to #{@max_limit}.
"""
@spec recent(pos_integer()) :: {:ok, [event()]} | {:error, term()}
def recent(limit) when is_integer(limit) and limit > 0 do
with {:ok, _} <- ensure_loaded(),
[{:recent_events, recent_events}] <- :ets.lookup(@table, :recent_events) do
{:ok, Enum.take(recent_events, min(limit, @max_limit))}
else
{:error, reason} -> {:error, reason}
_ -> {:error, :history_unavailable}
end
end
def recent(_), do: {:error, :invalid_limit}
defp normalize_provider(provider) when is_atom(provider), do: {:ok, Atom.to_string(provider)}
defp normalize_provider(provider) when is_binary(provider) do
trimmed = String.trim(provider)
if trimmed == "" do
{:error, :bad_provider}
else
{:ok, trimmed}
end
end
defp normalize_provider(_), do: {:error, :bad_provider}
defp ensure_loaded do
do_ensure_loaded(1)
end
defp do_ensure_loaded(retries_left) when is_integer(retries_left) and retries_left >= 0 do
table = ensure_table()
with {:ok, signature} <- history_signature(),
stale? <- stale_index?(table, signature) do
if stale? do
case with_load_lock(fn -> maybe_refresh_index(table, signature) end) do
:retry when retries_left > 0 ->
do_ensure_loaded(retries_left - 1)
:retry ->
{:error, :history_unavailable}
result ->
result
end
else
{:ok, :loaded}
end
end
end
defp with_load_lock(fun) do
:global.trans(@load_lock, fun)
end
defp ensure_table do
case :ets.whereis(@table) do
:undefined ->
create_table()
tid ->
tid
end
end
defp create_table do
heir = ensure_table_heir()
tid =
try do
:ets.new(@table, [
:named_table,
:public,
{:heir, heir, :llm_db_history},
read_concurrency: true
])
rescue
ArgumentError ->
:ets.whereis(@table)
end
if tid == :undefined do
create_table()
else
tid
end
end
defp ensure_table_heir do
case :persistent_term.get(@table_heir_key, nil) do
pid when is_pid(pid) ->
if Process.alive?(pid) do
pid
else
start_table_heir()
end
_ ->
start_table_heir()
end
end
defp start_table_heir do
pid = spawn(fn -> table_heir_loop() end)
case :persistent_term.get(@table_heir_key, nil) do
existing when is_pid(existing) ->
if Process.alive?(existing) do
if existing != pid do
Process.exit(pid, :normal)
end
existing
else
:persistent_term.put(@table_heir_key, pid)
pid
end
_ ->
:persistent_term.put(@table_heir_key, pid)
pid
end
end
defp table_heir_loop do
receive do
{:"ETS-TRANSFER", _table, _from, _data} ->
table_heir_loop()
_ ->
table_heir_loop()
end
end
defp put_index(table, signature, index) do
try do
:ets.insert(table, [
{:signature, signature},
{:meta, index.meta},
{:model_index, index.model_index},
{:lineage_index, index.lineage_index},
{:lineage_by_model, index.lineage_by_model},
{:recent_events, index.recent_events}
])
:ok
rescue
ArgumentError ->
:retry
end
end
defp stale_index?(table, signature) do
try do
case :ets.lookup(table, :signature) do
[{:signature, ^signature}] -> false
_ -> true
end
rescue
ArgumentError ->
true
end
end
defp maybe_refresh_index(table, signature) do
if stale_index?(table, signature) do
with {:ok, index} <- load_index() do
case put_index(table, signature, index) do
:ok -> {:ok, :loaded}
:retry -> :retry
end
end
else
{:ok, :loaded}
end
end
defp load_index do
history_dir = history_dir()
meta_path = Path.join(history_dir, "meta.json")
with {:ok, meta_json} <- File.read(meta_path),
{:ok, meta} <- Jason.decode(meta_json) do
event_paths =
history_dir
|> Path.join("events/*.ndjson")
|> Path.wildcard()
|> Enum.sort()
reduced =
Enum.reduce(
event_paths,
%{model: %{}, lineage: %{}, lineage_by_model: %{}, recent: []},
fn path, acc ->
File.stream!(path)
|> Enum.reduce(acc, fn line, inner_acc ->
case Jason.decode(line) do
{:ok, event} ->
normalized = normalize_event(event)
model_key = normalized["model_key"]
lineage_key = normalized["lineage_key"]
%{
model:
Map.update(inner_acc.model, model_key, [normalized], fn events ->
[normalized | events]
end),
lineage:
Map.update(inner_acc.lineage, lineage_key, [normalized], fn events ->
[normalized | events]
end),
lineage_by_model: Map.put(inner_acc.lineage_by_model, model_key, lineage_key),
recent: [normalized | inner_acc.recent]
}
_ ->
inner_acc
end
end)
end
)
model_index =
Map.new(reduced.model, fn {model_key, events} ->
{model_key, events |> Enum.reverse() |> sort_events_asc()}
end)
lineage_index =
Map.new(reduced.lineage, fn {lineage_key, events} ->
{lineage_key, events |> Enum.reverse() |> sort_events_asc()}
end)
lineage_by_model =
Map.new(model_index, fn {model_key, events} ->
last_event = List.last(events) || %{}
{model_key, Map.get(last_event, "lineage_key", model_key)}
end)
recent_events =
reduced.recent
|> sort_events_desc()
{:ok,
%{
meta: meta,
model_index: model_index,
lineage_index: lineage_index,
lineage_by_model: lineage_by_model,
recent_events: recent_events
}}
else
{:error, reason} ->
{:error, reason}
end
end
defp normalize_event(event) do
model_key =
Map.get(event, "model_key") ||
[Map.get(event, "provider"), Map.get(event, "model_id")]
|> Enum.filter(&is_binary/1)
|> Enum.join(":")
lineage_key = Map.get(event, "lineage_key") || model_key
event
|> Map.put("model_key", model_key)
|> Map.put("lineage_key", lineage_key)
end
defp sort_events_asc(events) do
Enum.sort_by(events, fn event ->
{Map.get(event, "captured_at", ""), Map.get(event, "event_id", "")}
end)
end
defp sort_events_desc(events) do
Enum.sort_by(
events,
fn event ->
{Map.get(event, "captured_at", ""), Map.get(event, "event_id", "")}
end,
:desc
)
end
defp history_signature do
history_dir = history_dir()
meta_path = Path.join(history_dir, "meta.json")
with true <- File.exists?(meta_path),
{:ok, meta_stat} <- File.stat(meta_path) do
event_stats =
history_dir
|> Path.join("events/*.ndjson")
|> Path.wildcard()
|> Enum.sort()
|> Enum.map(fn path ->
case File.stat(path) do
{:ok, stat} -> {path, stat.size, stat.mtime}
_ -> {path, 0, nil}
end
end)
snapshots_path = Path.join(history_dir, "snapshots.ndjson")
snapshots_stat =
case File.stat(snapshots_path) do
{:ok, stat} -> {stat.size, stat.mtime}
_ -> {0, nil}
end
{:ok, {meta_stat.size, meta_stat.mtime, snapshots_stat, event_stats}}
else
false -> {:error, :history_unavailable}
{:error, reason} -> {:error, reason}
end
end
defp history_dir do
Application.get_env(
:llm_db,
:history_dir,
Application.app_dir(:llm_db, "priv/llm_db/history")
)
end
end