Precedence-aware merging with exclude handling for LLM model data.
Provides functions to merge providers, models, and arbitrary maps with configurable precedence rules. Handles excludes via exact match or glob patterns.
Summary
Functions
Compiles exclude patterns to regex for performance.
Converts a glob pattern to an anchored regex.
Checks if a model_id matches any exclude pattern.
Merges two maps with precedence rules.
Merges two lists of maps by a shared ID key.
Merges two model lists by {provider, id} identity, applying excludes.
Merges two provider lists by :id key.
Creates a configurable deep merge resolver function.
Functions
Compiles exclude patterns to regex for performance.
Converts a map of %{provider => [patterns]} to %{provider => [compiled_patterns]} where each pattern is either kept as a string (for exact match) or compiled to regex (for globs).
Examples
iex> result = LLMDB.Merge.compile_excludes(%{openai: ["gpt-3", "gpt-5-*"]})
iex> [exact, pattern] = result.openai
iex> exact
"gpt-3"
iex> Regex.match?(pattern, "gpt-5-pro")
true
Converts a glob pattern to an anchored regex.
- "" becomes "."
- Escape other regex special chars
- Anchor with ^ and $
Examples
iex> pattern = LLMDB.Merge.compile_pattern("gpt-*")
iex> Regex.match?(pattern, "gpt-4")
true
iex> pattern = LLMDB.Merge.compile_pattern("gpt-5-*-mini")
iex> Regex.match?(pattern, "gpt-5-turbo-mini")
true
Checks if a model_id matches any exclude pattern.
Patterns can be exact strings or compiled regexes.
Examples
iex> LLMDB.Merge.matches_exclude?("gpt-4", ["gpt-3", "gpt-5"])
false
iex> LLMDB.Merge.matches_exclude?("gpt-3", ["gpt-3", "gpt-5"])
true
iex> LLMDB.Merge.matches_exclude?("gpt-5-pro", [~r/^gpt-5-.*$/])
true
iex> LLMDB.Merge.matches_exclude?("gpt-4", [~r/^gpt-5-.*$/])
false
Merges two maps with precedence rules.
- Scalar values: higher precedence wins
- Maps: deep merge recursively
- Lists: concat and de-dup by value
- Higher precedence source always wins on scalars
Examples
iex> LLMDB.Merge.merge(%{a: 1}, %{b: 2}, :higher)
%{a: 1, b: 2}
iex> LLMDB.Merge.merge(%{a: 1}, %{a: 2}, :higher)
%{a: 2}
iex> LLMDB.Merge.merge(%{a: 1}, %{a: 2}, :lower)
%{a: 1}
iex> LLMDB.Merge.merge(%{a: %{b: 1}}, %{a: %{c: 2}}, :higher)
%{a: %{b: 1, c: 2}}
iex> LLMDB.Merge.merge(%{a: [1, 2]}, %{a: [2, 3]}, :higher)
%{a: [1, 2, 3]}
Merges two lists of maps by a shared ID key.
Keeps the base list order, overrides items with matching IDs from the override list, and appends override-only items in their original order.
Used by LLMDB.Pricing to merge pricing components from provider defaults
with model-specific overrides.
Parameters
base_list- The base list of maps (order preserved)override_list- Maps that override or extend the base listid_key- The key to match on (default::id). Supports both atom and string keys.
Examples
# Override matching items, preserve order
iex> base = [%{id: "a", value: 1}, %{id: "b", value: 2}]
iex> override = [%{id: "b", value: 20}]
iex> LLMDB.Merge.merge_list_by_id(base, override)
[%{id: "a", value: 1}, %{id: "b", value: 20}]
# Append new items from override
iex> base = [%{id: "a", value: 1}]
iex> override = [%{id: "b", value: 2}, %{id: "c", value: 3}]
iex> LLMDB.Merge.merge_list_by_id(base, override)
[%{id: "a", value: 1}, %{id: "b", value: 2}, %{id: "c", value: 3}]
# Pricing component merge example
iex> defaults = [%{id: "tool.search", rate: 10.0}, %{id: "tool.code", rate: 5.0}]
iex> overrides = [%{id: "tool.search", rate: 0.0}] # Free search
iex> LLMDB.Merge.merge_list_by_id(defaults, overrides)
[%{id: "tool.search", rate: 0.0}, %{id: "tool.code", rate: 5.0}]
Merges two model lists by {provider, id} identity, applying excludes.
- Merge models by {provider, id} identity
- Apply excludes: %{provider_atom => [patterns]} where patterns can be exact strings or globs with *
- Compile glob patterns to regex once for performance
- Higher precedence (override) wins on conflicts
Examples
iex> base = [%{id: "gpt-4", provider: :openai}]
iex> override = [%{id: "gpt-4", provider: :openai, capabilities: %{tools: true}}]
iex> LLMDB.Merge.merge_models(base, override, %{})
[%{id: "gpt-4", provider: :openai, capabilities: %{tools: true}}]
iex> base = [%{id: "gpt-4", provider: :openai}, %{id: "gpt-3", provider: :openai}]
iex> excludes = %{openai: ["gpt-3"]}
iex> LLMDB.Merge.merge_models(base, [], excludes)
[%{id: "gpt-4", provider: :openai}]
iex> base = [%{id: "gpt-4o-mini", provider: :openai}, %{id: "gpt-5-pro", provider: :openai}]
iex> excludes = %{openai: ["gpt-5-*"]}
iex> LLMDB.Merge.merge_models(base, [], excludes)
[%{id: "gpt-4o-mini", provider: :openai}]
Merges two provider lists by :id key.
Higher precedence (override) wins on conflicts.
Examples
iex> base = [%{id: :openai, name: "OpenAI"}]
iex> override = [%{id: :openai, name: "OpenAI Updated"}, %{id: :anthropic, name: "Anthropic"}]
iex> result = LLMDB.Merge.merge_providers(base, override)
iex> Enum.sort_by(result, & &1.id)
[%{id: :anthropic, name: "Anthropic"}, %{id: :openai, name: "OpenAI Updated"}]
Creates a configurable deep merge resolver function.
Returns a 3-arity function that can be passed to DeepMerge.deep_merge/3 with customizable behavior for list and special key handling.
Options
:union_list_keys- List of keys whose list values should be unioned (default: []):preserve_empty_list_keys- List of keys where empty list on right preserves left (default: [])
Examples
# Model merging with list unions
resolver = Merge.resolver(union_list_keys: [:aliases, :tags])
DeepMerge.deep_merge(base, override, resolver)
# Provider merging preserving exclude_models
resolver = Merge.resolver(preserve_empty_list_keys: [:exclude_models])
DeepMerge.deep_merge(base, override, resolver)