Current section

Files

Jump to
llm_db lib llm_db provider.ex
Raw

lib/llm_db/provider.ex

defmodule LLMDB.Provider do
@moduledoc """
Provider struct with Zoi schema validation.
Represents an LLM provider with metadata including identity, base URL,
environment variables, documentation, and pricing defaults.
## Fields
- `:id` - Unique provider identifier atom (e.g., `:openai`)
- `:name` - Display name
- `:base_url` - Base API URL (supports template variables like `{region}`)
- `:env` - List of environment variable names for credentials
- `:config_schema` - Runtime configuration field definitions
- `:doc` - Documentation URL
- `:pricing_defaults` - Default pricing components applied to all models (see below)
- `:exclude_models` - Model IDs to exclude from upstream sources
- `:extra` - Additional provider-specific data
- `:alias_of` - Primary provider ID if this is an alias
## Pricing Defaults
The `:pricing_defaults` field defines default pricing for tools and features
that apply to all models from this provider. This avoids duplicating tool
pricing across every model definition.
%{
currency: "USD",
components: [
%{id: "tool.web_search", kind: "tool", tool: "web_search", unit: "call", per: 1000, rate: 10.0},
%{id: "storage.vectors", kind: "storage", unit: "gb_day", per: 1, rate: 0.10}
]
}
Provider defaults are merged with model-specific pricing at load time.
See `LLMDB.Pricing` and the [Pricing and Billing guide](pricing-and-billing.md).
"""
@config_field_schema Zoi.object(%{
name: Zoi.string(),
type: Zoi.string(),
required: Zoi.boolean() |> Zoi.default(false),
default: Zoi.any() |> Zoi.nullish(),
doc: Zoi.string() |> Zoi.nullish()
})
@pricing_component_schema Zoi.object(%{
id: Zoi.string(),
kind:
Zoi.enum([
"token",
"tool",
"image",
"storage",
"request",
"other"
])
|> Zoi.nullish(),
unit:
Zoi.enum([
"token",
"call",
"query",
"session",
"gb_day",
"image",
"source",
"other"
])
|> Zoi.nullish(),
per: Zoi.integer() |> Zoi.min(1) |> Zoi.nullish(),
rate: Zoi.number() |> Zoi.nullish(),
meter: Zoi.string() |> Zoi.nullish(),
tool: Zoi.union([Zoi.atom(), Zoi.string()]) |> Zoi.nullish(),
size_class: Zoi.string() |> Zoi.nullish(),
notes: Zoi.string() |> Zoi.nullish()
})
@pricing_defaults_schema Zoi.object(%{
currency: Zoi.string() |> Zoi.nullish(),
components: Zoi.array(@pricing_component_schema) |> Zoi.default([])
})
@schema Zoi.struct(
__MODULE__,
%{
id: Zoi.atom(),
name: Zoi.string() |> Zoi.nullish(),
base_url: Zoi.string() |> Zoi.nullish(),
env: Zoi.array(Zoi.string()) |> Zoi.nullish(),
config_schema: Zoi.array(@config_field_schema) |> Zoi.nullish(),
doc: Zoi.string() |> Zoi.nullish(),
exclude_models: Zoi.array(Zoi.string()) |> Zoi.default([]) |> Zoi.nullish(),
pricing_defaults: @pricing_defaults_schema |> Zoi.nullish(),
extra: Zoi.map() |> Zoi.nullish(),
alias_of: Zoi.atom() |> Zoi.nullish()
},
coerce: true
)
@type t :: unquote(Zoi.type_spec(@schema))
@enforce_keys Zoi.Struct.enforce_keys(@schema)
defstruct Zoi.Struct.struct_fields(@schema)
@doc "Returns the Zoi schema for Provider"
def schema, do: @schema
@doc """
Creates a new Provider struct from a map, validating with Zoi schema.
## Examples
iex> LLMDB.Provider.new(%{id: :openai, name: "OpenAI"})
{:ok, %LLMDB.Provider{id: :openai, name: "OpenAI"}}
iex> LLMDB.Provider.new(%{})
{:error, _validation_errors}
"""
@spec new(map()) :: {:ok, t()} | {:error, term()}
def new(attrs) when is_map(attrs) do
Zoi.parse(@schema, attrs)
end
@doc """
Creates a new Provider struct from a map, raising on validation errors.
## Examples
iex> LLMDB.Provider.new!(%{id: :openai, name: "OpenAI"})
%LLMDB.Provider{id: :openai, name: "OpenAI"}
"""
@spec new!(map()) :: t()
def new!(attrs) when is_map(attrs) do
case new(attrs) do
{:ok, provider} -> provider
{:error, reason} -> raise ArgumentError, "Invalid provider: #{inspect(reason)}"
end
end
end
defimpl DeepMerge.Resolver, for: LLMDB.Provider do
@moduledoc false
def resolve(original, override = %LLMDB.Provider{}, resolver) do
cleaned_override =
override
|> Map.from_struct()
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
|> Map.new()
Map.merge(original, cleaned_override, resolver)
end
def resolve(original, override, resolver) when is_map(override) do
Map.merge(original, override, resolver)
end
def resolve(_original, override, _resolver), do: override
end