Files
majordomo/docs/adr/0004-env-dsn-providers.md
T
steve dcd004289f feat: foundations — canonical types, Parse grammar, env DSNs, health, chains
Phase 1 of the majordomo build:
- llm/ canonical contract (messages, parts, tools, capabilities, streaming,
  Model/Provider, error classification)
- health/ clock-injected tracker (threshold bench, exponential capped
  cooldown, reset-on-success)
- root Registry + Parse (verbatim model ids, inline recursive alias
  expansion with cycle detection, chain dedup), LLM_* env-DSN providers
  (go-llm parity: lazy fallback + eager LoadEnv), health-aware chain
  executor behind the Model interface
- provider/fake scriptable test provider; hermetic test suite incl. the
  trailing-thinking chain and foreman:// env loading
- ADRs 0001-0008, CLAUDE.md, README (honest matrix), CI workflow,
  docs/phase-1-design.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 12:35:34 +02:00

61 lines
2.6 KiB
Markdown

# 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.orgrimmar.dudenhoeffer.casa
LLM_M5=foreman://token@foreman-m5.orgrimmar.dudenhoeffer.casa
```
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_<NAME>` and
registers each as provider `lower(<NAME>)` (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.