ALLM.Keys (allm v0.3.0)

Copy Markdown View Source

API key resolution per spec §6.4. Keys never appear on the engine.

Five-level resolution chain — the first level that yields a non-empty string wins:

  1. opts[:api_key] — explicit per-call override
  2. ALLM.Keys.Store — in-process Agent set via put/2
  3. Application.get_env(:allm, :keys, %{})[provider]
  4. System.get_env(env_var_for(provider))
  5. .env file at config :allm, :dotenv_path (default: File.cwd!() <> "/.env") — only consulted when config :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).

get/1 and get/2 return {:ok, key, source} on hit or {:error, :missing} on miss (spec §6.4 — Non-obvious decision #9 preserves this shape despite the project's "no atom-tuple errors" rule). fetch!/2 raises %ALLM.Error.EngineError{reason: :missing_key} on miss (Non-obvious decision #8 — the only function in the library that raises).

.env parser limitations

ALLM ships a built-in .env parser (Non-obvious decision #2) to avoid 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.

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

provider()

@type provider() :: atom()

source()

@type source() :: :opts | :runtime | :app_config | :env | :dotenv

Functions

delete(provider)

@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.

env_var_for(provider)

@spec env_var_for(provider()) :: String.t()

Translate a provider atom to its env-var name.

Known providers use the spec §6.4 table (:openai → "OPENAI_API_KEY", etc.). Unknown providers fall back to String.upcase("#{provider}") <> "_API_KEY".

Public because ALLM.Keys.Dotenv.lookup/1 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"

fetch!(provider, opts \\ [])

@spec fetch!(
  provider(),
  keyword()
) :: String.t()

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 (Non-obvious decision #8). 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

get(provider)

@spec get(provider()) :: {:ok, String.t(), source()} | {:error, :missing}

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

get(provider, opts)

@spec get(
  provider(),
  keyword()
) :: {:ok, String.t(), source()} | {:error, :missing}

Resolve the API key for provider, honoring opts[:api_key] as the highest-precedence source.

See the module docs for the full chain.

put(provider, key)

@spec put(provider(), String.t()) :: :ok

Install key for provider in the in-process runtime store.

Cleared by ALLM.Keys.delete/1 or ALLM.Keys.Store.clear/0. Persists for the lifetime of the ALLM.Keys.Store 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