# `ALLM.Keys`
[🔗](https://github.com/cykod/ALLM/blob/v0.3.0/lib/allm/keys.ex#L1)

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.

# `provider`

```elixir
@type provider() :: atom()
```

# `source`

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

# `delete`

```elixir
@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`

```elixir
@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!`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
