Packages
llm_db
2026.7.2
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 one immutable index published atomically through
`:persistent_term` for fast timeline/recent lookups without a cache process.
The cache uses one bounded key. Reloading replaces that key and therefore has
the normal `:persistent_term` global-GC update cost; reads remain lock-free.
"""
@cache_key {__MODULE__, :index}
@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, index} <- ensure_loaded() do
{:ok, index.meta}
else
{:error, _reason} -> {: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, index} <- ensure_loaded() do
model_key = provider_str <> ":" <> model_id
lineage_key = Map.get(index.lineage_by_model, model_key, model_key)
events =
Map.get(index.lineage_index, lineage_key) ||
Map.get(index.model_index, model_key, [])
{:ok, events}
else
{:error, reason} -> {:error, reason}
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, index} <- ensure_loaded() do
{:ok, Enum.take(index.recent_events, min(limit, @max_limit))}
else
{:error, reason} -> {:error, reason}
end
end
def recent(_), do: {:error, :invalid_limit}
@doc false
@spec clear_cache() :: :ok
def clear_cache do
with_load_lock(fn ->
:persistent_term.erase(@cache_key)
:ok
end)
end
@doc false
@spec reload() :: {:ok, :loaded} | {:error, term()}
def reload do
with_load_lock(fn ->
with {:ok, signature} <- history_signature(),
{:ok, index} <- load_index() do
publish_index(signature, index)
{:ok, :loaded}
end
end)
end
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
with {:ok, signature} <- history_signature() do
case cached_index(signature) do
{:ok, index} ->
{:ok, index}
:stale ->
with_load_lock(fn -> load_and_publish_if_stale(signature) end)
end
end
end
defp with_load_lock(fun), do: :global.trans(@load_lock, fun)
defp cached_index(signature) do
case :persistent_term.get(@cache_key, :missing) do
%{signature: ^signature, index: index} -> {:ok, index}
_other -> :stale
end
end
defp load_and_publish_if_stale(signature) do
case cached_index(signature) do
{:ok, index} ->
{:ok, index}
:stale ->
with {:ok, index} <- load_index() do
publish_index(signature, index)
{:ok, index}
end
end
end
defp publish_index(signature, index) do
:persistent_term.put(@cache_key, %{signature: signature, index: index})
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, {history_dir, 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