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

2.6 KiB

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_BOXmy_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/xLLM_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.