# ADR-0004: LLM_* env-DSN provider definitions (go-llm parity, plus eager load) **Status:** Accepted — 2026-06-10 ## Context Steve's deployments define providers via env vars that must keep working unchanged: ``` LLM_M1=foreman://token@foreman-m1.example.com LLM_M5=foreman://token@foreman-m5.example.com ``` go-llm (v2/parse.go) implements this **lazily only**: `Parse("m5/x")` misses the registry, computes `LLM_` + UPPER(name) with `-`→`_`, reads exactly that var, parses `scheme://[token@]host[/path]` by plain string splits, requires the scheme to be a registered provider, and dials `https://` + host. There is no environment scan. The kickoff additionally requires `New()` to load LLM_* providers eagerly and a testable `LoadEnv(map)`. ## Decision Implement **both** paths over one DSN parser (byte-for-byte go-llm semantics — `://` split, first-`@` split, trailing-`/` trim, ErrInvalidDSN on missing scheme/host, base URL always `https://host[/path]`): - **Eager:** `New()` scans the process environment for `LLM_` and registers each as provider `lower()` (underscores preserved: `LLM_MY_BOX` → `my_box`). `LoadEnv(map[string]string)` is the explicit, testable entry. Malformed entries never fail construction: they are recorded per-name, returned joined from LoadEnv, and surface from Parse only when that name is actually referenced (matching go-llm's fail-on-use behavior). - **Lazy (go-llm parity):** an unknown provider name in Parse falls back to `LLM_{UPPER(name, - → _)}`, so hyphenated spec names (`my-prov/x` → `LLM_MY_PROV`) work exactly as in go-llm. Lazily resolved providers are cached in the registry. - The DSN **scheme** selects a `SchemeFactory` (foreman, ollama, ollama-cloud, openai, anthropic, google, gemini; extensible via `RegisterScheme`). The factory receives the registry name and the parsed DSN (token = credential, `https://host` = base URL). ## Consequences - Existing muscle memory carries over: every go-llm-resolvable LLM_* var resolves identically here. - Eager loading additionally makes env providers visible to discovery (`Provider(name)`) before first use. - An env DSN cannot express plain-http endpoints (https is forced) — same limitation as go-llm, kept deliberately for parity; local Ollama uses the `ollama` provider's own default (`http://localhost:11434`) rather than a DSN. ## Alternatives considered - `url.Parse`-based DSN parsing: subtly different (percent-decoding, userinfo passwords). Parity wins. Rejected. - Failing New() on malformed LLM_* vars: one stray var would break every consumer at startup. Rejected.