defmodule LLMDB do @moduledoc """ Fast, persistent_term-backed LLM model metadata catalog. Provides a simple, capability-aware API for querying LLM model metadata. All queries are backed by `:persistent_term` for O(1), lock-free access. ## Model Specs Model specifications can be expressed in multiple formats: - `"provider:model"` (e.g., `"openai:gpt-4o-mini"`) - Traditional colon format - `"model@provider"` (e.g., `"gpt-4o-mini@openai"`) - Filesystem-safe @ format - `{:provider, "model"}` (e.g., `{:openai, "gpt-4o-mini"}`) - Tuple format See the [Model Spec Formats](guides/model-spec-formats.md) guide for detailed information on when to use each format. ## Two Phases **Phase 1 - Build Time** (Mix tasks): - `mix llm_db.pull` - Pull remote sources into local caches - `mix llm_db.build` - Run ETL and build a canonical snapshot artifact - This is a development/CI operation that builds the complete catalog **Phase 2 - Runtime** (Consumer library): - `load/1` - Load packaged snapshot into Store with optional filtering - Query functions to select models by capabilities - All queries operate on the filtered catalog loaded in Store ## Providers - `providers/0` - Get all providers as list of Provider structs - `provider/1` - Get a specific provider by ID ## Models - `models/0` - Get all models as list of Model structs - `models/1` - Get all models for a provider - `model/1` - Resolve a colon, @, or tuple model spec - `model/2` - Get a specific model by provider and ID ## Selection and Policy - `select/1` - Select first model matching capability requirements - `candidates/1` - Get all models matching capability requirements - `allowed?/1` - Check if a model is in the filtered catalog - `capabilities/1` - Get capabilities map for a model ## Utilities - `parse/1,2` - Parse a model spec string (colon or @ format) into {provider, model_id} tuple - `parse!/1,2` - Parse a model spec string, raising on error - `format/1,2` - Format a {provider, model_id} tuple as a string - `build/1,2` - Build a spec string from various inputs, converting between formats ## Examples # Get all providers providers = LLMDB.providers() # Get a specific provider {:ok, provider} = LLMDB.provider(:openai) # Get all models for a provider models = LLMDB.models(:openai) # Get a specific model {:ok, model} = LLMDB.model(:openai, "gpt-4o-mini") # Parse spec and get model {:ok, model} = LLMDB.model("openai:gpt-4o-mini") # Select a model matching requirements {:ok, {:openai, "gpt-4o-mini"}} = LLMDB.select( require: [chat: true, tools: true, json_native: true], prefer: [:openai, :anthropic] ) # Check if a model is allowed true = LLMDB.allowed?({:openai, "gpt-4o-mini"}) """ alias LLMDB.{Catalog, Loader, Model, Provider, Query, Spec, Store} @type provider :: atom() @type provider_input :: provider() | String.t() @type model_id :: String.t() @type model_spec :: {provider(), model_id()} | String.t() | Model.t() # Lifecycle @doc """ Loads or reloads the LLM model catalog. Phase 2 operation: Loads the packaged snapshot into runtime Store with optional filtering and customization based on consumer configuration. This function is idempotent - calling it multiple times with the same configuration will not reload the catalog unnecessarily. ## Options Consumer configuration options (override `config :llm_db, ...` settings): - `:allow` - `:all`, list of providers `[:openai]`, or map `%{openai: :all | [patterns]}` - `:deny` - List of providers `[:provider]` or map `%{provider: [patterns]}` - `:prefer` - List of provider atoms in preference order - `:custom` - Map with provider IDs as keys, provider configs (with models) as values ## Returns - `{:ok, snapshot}` - Successfully loaded the catalog - `{:error, :no_snapshot}` - No packaged snapshot available - `{:error, term}` - Other loading errors ## Examples # Load with default configuration from app env {:ok, _snapshot} = LLMDB.load() # Load with provider filter {:ok, _snapshot} = LLMDB.load(allow: [:openai, :anthropic]) # Load with model pattern filters {:ok, _snapshot} = LLMDB.load( allow: %{openai: ["gpt-4*"], anthropic: :all}, deny: %{openai: ["gpt-4-0613"]}, prefer: [:anthropic, :openai] ) # Load with custom providers/models {:ok, _snapshot} = LLMDB.load( custom: %{ local: [ name: "Local Provider", base_url: "http://localhost:8080", models: %{ "llama-3" => %{capabilities: %{chat: true}}, "mistral-7b" => %{capabilities: %{chat: true, tools: %{enabled: true}}} } ] } ) """ @spec load(keyword()) :: {:ok, map()} | {:error, term()} def load(opts \\ []) do Catalog.load(fn -> __lazy_load__(opts) end) end @doc false @spec __lazy_load__(keyword()) :: {:ok, map()} | {:error, term()} def __lazy_load__(opts) do case Loader.load(opts) do {:ok, snapshot} -> maybe_store_snapshot(snapshot, opts) {:error, :no_snapshot} -> load_empty(opts) {:error, _} = error -> error end end @doc """ Loads an empty catalog with no providers or models. Used as a fallback when no packaged snapshot is available, allowing the application to start successfully. The catalog can later be populated via `load/1` once a snapshot is available. ## Examples LLMDB.load_empty() #=> {:ok, %{providers: [], models: %{}, ...}} """ @spec load_empty(keyword()) :: {:ok, map()} def load_empty(opts \\ []) do {:ok, snapshot} = Loader.load_empty(opts) Store.put!(snapshot, opts) {:ok, snapshot} end # Providers @doc """ Returns all providers as a list of Provider structs. ## Examples providers = LLMDB.providers() #=> [%LLMDB.Provider{id: :anthropic, ...}, ...] """ @spec providers() :: [Provider.t()] defdelegate providers(), to: Store @doc """ Returns a specific provider by ID. ## Parameters - `provider` - Provider atom (e.g., `:openai`, `:anthropic`) ## Returns - `{:ok, provider}` - Provider found - `{:error, term}` - Provider not found ## Examples {:ok, provider} = LLMDB.provider(:openai) """ @spec provider(provider()) :: {:ok, Provider.t()} | {:error, term()} defdelegate provider(provider), to: Store # Models @doc """ Returns all models across all providers (filtered). ## Examples models = LLMDB.models() #=> [%LLMDB.Model{}, ...] """ @spec models() :: [Model.t()] def models do providers() |> Enum.flat_map(fn p -> Store.models(p.id) end) end @doc """ Returns all models for a specific provider (filtered). ## Parameters - `provider` - Provider atom (e.g., `:openai`, `:anthropic`) ## Returns List of Model structs for the provider, or empty list if provider not found. ## Examples models = LLMDB.models(:openai) #=> [%LLMDB.Model{id: "gpt-4o", ...}, ...] """ @spec models(provider()) :: [Model.t()] defdelegate models(provider), to: Store @doc """ Parses a model spec and returns the model. Supports "provider:model", "model@provider", and tuple formats. Tuple provider identifiers may be atoms or strings and are normalized through the same provider contract as string specs. ## Parameters - `spec` - A string spec or `{provider, model_id}` tuple ## Returns - `{:ok, model}` - Model found - `{:error, term}` - Parse error or model not found ## Examples {:ok, model} = LLMDB.model("openai:gpt-4o-mini") {:ok, model} = LLMDB.model("gpt-4o-mini@openai") {:ok, model} = LLMDB.model({:openai, "gpt-4o-mini"}) {:ok, model} = LLMDB.model({"openai", "gpt-4o-mini"}) {:ok, model} = LLMDB.model("anthropic:claude-3-5-sonnet-20241022") """ @spec model(String.t() | {provider_input(), model_id()}) :: {:ok, Model.t()} | {:error, term()} def model({provider, model_id}) when (is_atom(provider) or is_binary(provider)) and is_binary(model_id) do with {:ok, {normalized_provider, normalized_model_id}} <- Spec.parse_spec({provider, model_id}) do model(normalized_provider, normalized_model_id) end end def model(spec) when is_binary(spec) do with {:ok, {p, id}} <- Spec.parse_spec(spec) do model(p, id) end end @doc """ Returns a specific model by provider and ID (filtered). ## Parameters - `provider` - Provider atom (e.g., `:openai`) - `model_id` - Model ID string (e.g., `"gpt-4o-mini"`) ## Returns - `{:ok, model}` - Model found - `{:error, term}` - Model not found ## Examples {:ok, model} = LLMDB.model(:openai, "gpt-4o-mini") """ @spec model(provider(), model_id()) :: {:ok, Model.t()} | {:error, term()} defdelegate model(provider, model_id), to: Store # Selection (delegated to Query) @doc """ Selects the first model matching capability requirements. Delegates to `LLMDB.Query.select/1`. ## Options - `:require` - Keyword list of required capabilities - `:forbid` - Keyword list of forbidden capabilities - `:prefer` - List of provider atoms in preference order - `:scope` - Either `:all` (default) or a specific provider atom ## Returns - `{:ok, {provider, model_id}}` - First matching model - `{:error, :no_match}` - No models match the criteria ## Examples {:ok, {provider, model_id}} = LLMDB.select( require: [chat: true, tools: true], prefer: [:openai, :anthropic] ) """ @spec select(keyword()) :: {:ok, {provider(), model_id()}} | {:error, :no_match} def select(), do: Query.select([]) defdelegate select(opts), to: Query @doc """ Returns all models matching capability requirements. Delegates to `LLMDB.Query.candidates/1`. ## Options - `:require` - Keyword list of required capabilities - `:forbid` - Keyword list of forbidden capabilities - `:prefer` - List of provider atoms in preference order - `:scope` - Either `:all` (default) or a specific provider atom ## Returns List of `{provider, model_id}` tuples matching the criteria. ## Examples candidates = LLMDB.candidates( require: [chat: true, tools: true], prefer: [:openai, :anthropic] ) """ @spec candidates(keyword()) :: [{provider(), model_id()}] def candidates(), do: Query.candidates([]) defdelegate candidates(opts), to: Query @doc """ Gets capabilities for a model spec. Delegates to `LLMDB.Query.capabilities/1`. ## Parameters - `spec` - Either `{provider, model_id}` tuple, `"provider:model"` string, or `%Model{}` struct ## Examples caps = LLMDB.capabilities({:openai, "gpt-4o-mini"}) #=> %{chat: true, tools: %{enabled: true, ...}, ...} """ @spec capabilities(model_spec()) :: map() | nil defdelegate capabilities(spec), to: Query # Policy @doc """ Returns true if the model is allowed by current filters. Checks if the model is present in the filtered snapshot loaded in Store. ## Parameters - `spec` - Either `%Model{}`, `{provider, model_id}` tuple, or `"provider:model"` string ## Returns `true` if model is in filtered catalog, `false` otherwise ## Examples true = LLMDB.allowed?({:openai, "gpt-4o-mini"}) true = LLMDB.allowed?("openai:gpt-4o-mini") {:ok, model} = LLMDB.model(:openai, "gpt-4o-mini") true = LLMDB.allowed?(model) """ @spec allowed?(model_spec()) :: boolean() def allowed?(%Model{provider: p, id: id}), do: allowed?({p, id}) def allowed?({provider, model_id}) when is_atom(provider) and is_binary(model_id) do case Store.model(provider, model_id) do {:ok, _} -> true _ -> false end end def allowed?(spec) when is_binary(spec) do case Spec.parse_spec(spec) do {:ok, tuple} -> allowed?(tuple) _ -> false end end # Utilities @doc """ Parses a model spec string into a {provider, model_id} tuple. Supports both "provider:model" (default) and "model@provider" (filename-safe) formats. Automatically detects the format based on separator present. ## Parameters - `spec` - String like `"openai:gpt-4o-mini"`, `"gpt-4o-mini@openai"`, or a tuple such as `{:openai, "gpt-4o-mini"}` or `{"openai", "gpt-4o-mini"}` - `opts` - Keyword list with optional `:format` to explicitly specify `:colon` or `:at` ## Returns - `{:ok, {provider, model_id}}` - Successfully parsed spec - `{:error, term}` - Invalid spec format ## Examples {:ok, {:openai, "gpt-4o-mini"}} = LLMDB.parse("openai:gpt-4o-mini") {:ok, {:openai, "gpt-4o-mini"}} = LLMDB.parse("gpt-4o-mini@openai") {:ok, {:anthropic, "claude-3-5-sonnet-20241022"}} = LLMDB.parse("anthropic:claude-3-5-sonnet-20241022") {:ok, {:openai, "gpt-4o"}} = LLMDB.parse({:openai, "gpt-4o"}) {:ok, {:openai, "gpt-4o"}} = LLMDB.parse({"openai", "gpt-4o"}) # With explicit format when ambiguous {:ok, {:openai, "model@test"}} = LLMDB.parse("openai:model@test", format: :colon) """ @spec parse(String.t() | {provider_input(), model_id()}, keyword()) :: {:ok, {provider(), model_id()}} | {:error, term()} def parse(spec, opts \\ []), do: Spec.parse_spec(spec, opts) @doc """ Parses a model spec string, raising on error. Same as `parse/2` but raises `ArgumentError` instead of returning error tuple. ## Examples {:openai, "gpt-4o-mini"} = LLMDB.parse!("openai:gpt-4o-mini") {:openai, "gpt-4o-mini"} = LLMDB.parse!("gpt-4o-mini@openai") """ @spec parse!(String.t() | {provider_input(), model_id()}, keyword()) :: {provider(), model_id()} def parse!(spec, opts \\ []), do: Spec.parse_spec!(spec, opts) @doc """ Formats a model spec tuple as a string. Converts a {provider, model_id} tuple to string format. The output format can be controlled via the `format` parameter or falls back to the application config `:llm_db, :model_spec_format` (default: `:provider_colon_model`). ## Parameters - `spec` - {provider, model_id} tuple - `format` - Optional format override (atom) ## Supported Formats - `:provider_colon_model` - "provider:model" (default) - `:model_at_provider` - "model@provider" (filename-safe) - `:filename_safe` - alias for `:model_at_provider` ## Examples "openai:gpt-4o-mini" = LLMDB.format({:openai, "gpt-4o-mini"}) "gpt-4o-mini@openai" = LLMDB.format({:openai, "gpt-4o-mini"}, :filename_safe) "gpt-4o-mini@openai" = LLMDB.format({:openai, "gpt-4o-mini"}, :model_at_provider) """ @spec format({provider(), model_id()}, atom() | nil) :: String.t() def format(spec, format \\ nil), do: Spec.format_spec(spec, format) @doc """ Builds a model specification string from various inputs. Accepts strings (in any supported format) or tuples and outputs a string in the desired format. Useful for converting between formats. ## Parameters - `input` - Model spec as string or tuple - `opts` - Keyword list with optional `:format` for output format ## Examples "gpt-4@openai" = LLMDB.build("openai:gpt-4", format: :filename_safe) "openai:gpt-4" = LLMDB.build("gpt-4@openai", format: :provider_colon_model) "gpt-4@openai" = LLMDB.build({:openai, "gpt-4"}, format: :model_at_provider) """ @spec build(String.t() | {provider(), model_id()}, keyword()) :: String.t() def build(input, opts \\ []), do: Spec.build_spec(input, opts) # Internal (Store access) @doc false @spec snapshot() :: map() defdelegate snapshot(), to: Store @doc false @spec epoch() :: non_neg_integer() defdelegate epoch(), to: Store # Private: idempotent storage - only update Store if snapshot changed defp maybe_store_snapshot(snapshot, opts) do prev = Store.snapshot() prev_digest = get_in(prev, [:meta, :digest]) new_digest = get_in(snapshot, [:meta, :digest]) if prev_digest == new_digest do {:ok, prev} else Store.put!(snapshot, opts) {:ok, snapshot} end end end