Current section

Files

Jump to
llm_db lib mix tasks llm_db.build.ex
Raw

lib/mix/tasks/llm_db.build.ex

defmodule Mix.Tasks.LlmDb.Build do
use Mix.Task
@shortdoc "Build snapshot.json from sources using the ETL pipeline"
@moduledoc """
Builds snapshot.json from configured sources using the Engine ETL pipeline.
Runs the complete ETL pipeline (Ingest → Normalize → Validate → Merge →
Enrich → Filter → Index) on configured sources to generate a fresh
snapshot.json file.
## Usage
mix llm_db.build
## Configuration
Configure sources in your application config:
config :llm_db,
sources: [
{LLMDB.Sources.Packaged, %{}},
{LLMDB.Sources.ModelsDev, %{url: "https://models.dev/api.json"}},
{LLMDB.Sources.JSONFile, %{paths: ["priv/custom.json"]}}
],
allow: :all,
deny: %{},
prefer: [:openai, :anthropic]
"""
@manifest_path "priv/llm_db/manifest.json"
@providers_dir "priv/llm_db/providers"
@impl Mix.Task
def run(_args) do
ensure_llm_db_project!()
Mix.Task.run("app.start")
Mix.shell().info("Building snapshot from configured sources...\n")
{:ok, snapshot} = build_snapshot()
save_snapshot(snapshot)
print_summary(snapshot)
end
defp build_snapshot do
config = LLMDB.Config.get()
sources = LLMDB.Config.sources!()
if sources == [] do
Mix.shell().info("Warning: No sources configured - snapshot will be empty\n")
end
LLMDB.Engine.run(
sources: sources,
allow: config.allow,
deny: config.deny,
prefer: config.prefer
)
end
defp save_snapshot(snapshot) do
# Create providers directory
File.mkdir_p!(@providers_dir)
# Write each provider to its own file
provider_ids =
snapshot.providers
|> Enum.map(fn {provider_id, provider_data} ->
filename = "#{provider_id}.json"
path = Path.join(@providers_dir, filename)
provider_output = map_with_string_keys(provider_data)
json = Jason.encode!(provider_output, pretty: true)
File.write!(path, json)
Atom.to_string(provider_id)
end)
|> Enum.sort()
# Write manifest with metadata
File.mkdir_p!(Path.dirname(@manifest_path))
manifest = %{
"version" => snapshot.version,
"generated_at" => snapshot.generated_at,
"providers" => provider_ids
}
json = Jason.encode!(manifest, pretty: true)
File.write!(@manifest_path, json)
Mix.shell().info("✓ Manifest written to #{@manifest_path} (v#{snapshot.version})")
Mix.shell().info("✓ #{length(provider_ids)} provider files written to #{@providers_dir}/")
# Generate ValidProviders module from normalized snapshot data
generate_valid_providers(snapshot)
end
defp print_summary(snapshot) do
provider_count = map_size(snapshot.providers)
model_count =
snapshot.providers
|> Map.values()
|> Enum.map(fn provider -> map_size(provider.models) end)
|> Enum.sum()
Mix.shell().info("")
Mix.shell().info("Summary:")
Mix.shell().info(" Providers: #{provider_count}")
Mix.shell().info(" Models: #{model_count}")
end
defp map_with_string_keys(map) when is_map(map) do
# Convert struct to plain map first
plain_map =
if Map.has_key?(map, :__struct__) do
Map.from_struct(map)
else
map
end
# Convert to sorted keyword list for deterministic JSON output
plain_map
|> Enum.map(fn
{k, v} when is_atom(k) -> {Atom.to_string(k), map_with_string_keys(v)}
{k, v} -> {to_string(k), map_with_string_keys(v)}
end)
|> Enum.sort_by(fn {k, _v} -> k end)
|> Jason.OrderedObject.new()
end
defp map_with_string_keys(list) when is_list(list) do
Enum.map(list, &map_with_string_keys/1)
end
defp map_with_string_keys(value), do: value
# Generate ValidProviders module from normalized snapshot data
defp generate_valid_providers(snapshot) do
# Extract provider atoms from nested providers map
provider_atoms =
snapshot.providers
|> Map.keys()
|> Enum.sort()
|> Enum.uniq()
write_valid_providers_module(provider_atoms)
Mix.shell().info("✓ Generated valid_providers.ex with #{length(provider_atoms)} providers")
end
# Write the ValidProviders module to disk
defp write_valid_providers_module(provider_atoms) do
module_code = """
defmodule LLMDB.Generated.ValidProviders do
@moduledoc \"\"\"
Auto-generated module containing all valid provider atoms.
This module is generated by `mix llm_db.build` to prevent atom leaking.
By pre-generating all provider atoms at build time, we ensure that runtime
code can only use existing atoms via `String.to_existing_atom/1`.
DO NOT EDIT THIS FILE MANUALLY - it will be overwritten.
\"\"\"
@providers #{inspect(provider_atoms, limit: :infinity)}
@doc \"\"\"
Returns the list of all valid provider atoms.
\"\"\"
@spec list() :: [atom()]
def list, do: @providers
@doc \"\"\"
Checks if the given atom is a valid provider.
\"\"\"
@spec member?(atom()) :: boolean()
def member?(atom), do: atom in @providers
end
"""
module_path = "lib/llm_db/generated/valid_providers.ex"
File.mkdir_p!(Path.dirname(module_path))
formatted = Code.format_string!(module_code) |> IO.iodata_to_binary()
# Ensure file ends with newline (Elixir convention)
content = if String.ends_with?(formatted, "\n"), do: formatted, else: formatted <> "\n"
File.write!(module_path, content)
end
defp ensure_llm_db_project! do
app = Mix.Project.config()[:app]
if app != :llm_db do
Mix.raise("""
mix llm_db.build can only be run inside the llm_db project itself.
This task generates lib/llm_db/generated/valid_providers.ex. Running it from
a downstream application would create a duplicate LLMDB.Generated.ValidProviders
module that conflicts with the one shipped in the :llm_db Hex package.
If you need to regenerate the snapshot (maintainers only):
cd path/to/llm_db
mix llm_db.build
For downstream applications, use the data and modules shipped with :llm_db.
""")
end
end
end