dcd004289f
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>
61 lines
2.6 KiB
Markdown
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.
|