Files
Dragan Spiridonov 94033e3500 feat(llm-router): Phase 1 — kernel singleton + persistence + Gemini key alias
Wires the foundation for ADR-043's HybridRouter into the kernel boot
sequence so domain plugins can finally receive an llmRouter dependency.
Phase 1 lands the kernel/CLI/provider changes; Phase 2 will plumb the
router through all 13 domains.

Background: the LLM-enhancement code paths added in ADR-051
(isLLMAnalysisAvailable() across 14 services) were unreachable because
no construction site ever passed an llmRouter — services accepted the
optional dependency but no caller injected it. ProviderManager +
HybridRouter were only instantiated from the `aqe llm advise` CLI
subcommand.

Changes
- src/shared/llm/providers/gemini.ts: read GOOGLE_API_KEY as a third
  fallback after GOOGLE_AI_API_KEY and GEMINI_API_KEY, so users with
  the conventional Google key name don't need to alias it themselves.
- src/shared/llm/router/config-store.ts (new): load/save
  .agentic-qe/llm-config.json with env-detection layering
  (defaults < disk < env < explicit override). Strips apiKey fields
  defensively before persisting. Filters out providers the
  ProviderManager doesn't yet construct (onnx) to keep boot logs
  clean.
- src/shared/llm/llm-router-service.ts (new): builds a HybridRouter
  from the loaded config, picks primary/fallbacks from the available
  set honoring the user's defaultProvider when possible. Accepts an
  injected ProviderManager for tests.
- src/kernel/interfaces.ts: extends QEKernel with llmRouter? and
  KernelConfig with llmRouter.enabled ('auto'|true|false) +
  configOverride + providerManager test-injection.
- src/kernel/kernel.ts: kernel-singleton llmRouter built during
  initialize(), exposed via kernel.llmRouter, forwarded to all 13
  DOMAIN_FACTORIES as a 4th arg. Phase 2 wires each factory through
  to its coordinator/services; for now the wrappers accept-and-discard.
- src/cli/commands/llm-router.ts: replaces the
  `// would be persisted in real implementation` stub with real
  read/write through the config-store. `aqe llm config --set` now
  survives a process restart. Adds AQE_CONFIG_ROOT env var as a test
  seam so tests don't mutate the real .agentic-qe/.

Tests (110 total, all passing)
- gemini.test.ts: +3 new tests for each env-var alias, existing
  tests updated to clear all three names
- config-store.test.ts (new, 29 tests): load/save/merge precedence,
  env detection, secret stripping, atomicity
- llm-router-service.test.ts (new, 10 tests): pickEnabledProviders,
  pickPrimaryAndFallbacks, end-to-end with injected mock provider
- kernel-llm-router.test.ts (new, 4 tests): kernel boot with router
  enabled=false / auto-no-providers / injected mock, chat() round-trip
- llm-router.test.ts (CLI): +2 persistence-to-disk tests, env-isolated
- tests/mocks/index.ts: shared createMockLLMProvider() helper

Verification
- npx tsc --noEmit: clean
- 110 tests pass; no regressions in existing kernel.test.ts (34/34)
- Stderr noise from PersistentSONAEngine init is pre-existing and
  unrelated to this change.

What this commit does NOT do
- Does NOT change behavior for any user yet — DOMAIN_FACTORIES
  wrappers still discard the llmRouter param. Phase 2 lights up the
  domain services.

Refs ADR-043 (vendor-independent LLM), ADR-051 (LLM-enhanced services)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 09:38:40 +00:00
..