API key resolution. Keys never appear on the engine — they resolve at adapter-call time, so a serialized engine is safe to persist.
Resolution chain
Five levels — the first level that yields a non-empty string wins:
opts[:api_key]— explicit per-call override- The runtime store — an in-process Agent set via
put/2 Application.get_env(:allm, :keys, %{})[provider]System.get_env(env_var_for(provider)).envfile atconfig :allm, :dotenv_path(default:File.cwd! <> "/.env") — only consulted whenconfig :allm, load_dotenv: true.
Empty-string values at every level are treated as missing (defensive
an unset-looking env var like OPENAI_API_KEY= must not satisfy
resolution).
Return shapes
get/1 and get/2 return {:ok, key, source} on hit or
{:error, :missing} on miss. fetch!/2 raises
%ALLM.Error.EngineError{reason: :missing_key} on miss — it's the only
function in the library that raises rather than returning an
{:error, _} tuple, justified because adapters look up keys at call
time deep inside their implementation and bubbling {:error, _}
through every with chain is untenable.
.env parser limitations
ALLM ships a built-in .env parser to avoid pulling in a dotenvy
dependency for a level-5 fallback most users won't even enable. The
supported subset is strict: KEY=VALUE lines, # comment lines,
blank lines, export KEY=VALUE (the leading export is stripped),
and surrounding double-quote stripping. No variable interpolation,
no multi-line values, no escape sequences, no single-quote
stripping. Users with complex .env files should either
System.put_env/2 at boot or depend on dotenvy themselves.
See also guides/multi_tenant_keys.md.
Summary
Functions
Remove provider's key from the in-process runtime store.
Translate a provider atom to its env-var name.
Like get/2, but raises %ALLM.Error.EngineError{reason: :missing_key}
when no source yields a non-empty key.
Resolve the API key for provider via the five-level chain.
Resolve the API key for provider, honoring opts[:api_key] as the
highest-precedence source.
Install key for provider in the in-process runtime store.
Types
@type provider() :: atom()
@type source() :: :opts | :runtime | :app_config | :env | :dotenv
Functions
@spec delete(provider()) :: :ok
Remove provider's key from the in-process runtime store.
Does not touch Application env, system env, or the .env cache.
Translate a provider atom to its env-var name.
Known providers use a fixed table (:openai → "OPENAI_API_KEY",
:anthropic → "ANTHROPIC_API_KEY", :google → "GOOGLE_API_KEY",
:cohere → "COHERE_API_KEY", :mistral → "MISTRAL_API_KEY",
:fake → "FAKE_API_KEY"). Unknown providers fall back to
String.upcase("#{provider}") <> "_API_KEY".
Public because the dotenv-loader delegates through it for
consistent provider→env-var translation: the .env source looks up the
same env-var name the System.get_env source does, so configuring one
(OPENAI_API_KEY=… in the shell) and the other (OPENAI_API_KEY=… in
.env) uses an identical key name. This module is the single source
of truth for that mapping.
Examples
iex> ALLM.Keys.env_var_for(:openai)
"OPENAI_API_KEY"
iex> ALLM.Keys.env_var_for(:anthropic)
"ANTHROPIC_API_KEY"
iex> ALLM.Keys.env_var_for(:some_new_provider)
"SOME_NEW_PROVIDER_API_KEY"
Like get/2, but raises %ALLM.Error.EngineError{reason: :missing_key}
when no source yields a non-empty key.
This is the ONLY function in the library that raises
ALLM.Error.EngineError rather than returning it in an {:error, _}
tuple. Justified because adapters look up keys at call time deep
inside their implementation, and bubbling {:error, _} through every
with chain is untenable.
The raised error's :metadata carries :checked_sources — a list of
the source atoms that were actually consulted (:dotenv is included
only when config :allm, load_dotenv: true).
Note: :checked_sources records which levels were configured/enabled
for this call (i.e., which chain links were walked), not which levels
actually had a value supplied. An :opts entry appearing in the list
means the opts keyword was inspected — it does not distinguish
"api_key: omitted" from "api_key: present but empty/nil".
Examples
iex> ALLM.Keys.Store.clear()
iex> ALLM.Keys.put(:my_test_provider, "sk-doctest")
iex> ALLM.Keys.fetch!(:my_test_provider)
"sk-doctest"
iex> ALLM.Keys.Store.clear()
iex> try do
...> ALLM.Keys.fetch!(:my_test_provider)
...> rescue
...> e in ALLM.Error.EngineError -> e.reason
...> end
:missing_key
Resolve the API key for provider via the five-level chain.
Returns {:ok, key, source} on hit (where source identifies which
level of the chain provided the key) or {:error, :missing} on miss.
Shorthand for get(provider, []).
Examples
iex> ALLM.Keys.Store.clear()
iex> ALLM.Keys.put(:my_test_provider, "sk-doctest")
iex> ALLM.Keys.get(:my_test_provider)
{:ok, "sk-doctest", :runtime}
iex> ALLM.Keys.Store.clear()
:ok
Resolve the API key for provider, honoring opts[:api_key] as the
highest-precedence source.
See the module docs for the full chain.
Install key for provider in the in-process runtime store.
Cleared by ALLM.Keys.delete/1 or by clearing the runtime store
directly. Persists for the lifetime of the in-process Agent.
Examples
iex> ALLM.Keys.Store.clear()
iex> ALLM.Keys.put(:my_test_provider, "sk-doctest")
:ok
iex> ALLM.Keys.get(:my_test_provider)
{:ok, "sk-doctest", :runtime}
iex> ALLM.Keys.Store.clear()
:ok