Add LLM client interface with LiteLLM adapter (A5/A11)
CI / checks (pull_request) Successful in 10s
CI / checks (push) Successful in 9s

PR 2 of the steps-3/4 plan.

- llm.py: LLMClient protocol — complete(events, tools, model) ->
  canonical events — present from the first real LLM call.
  LiteLLMClient renders requests via the A10 renderer, targets the
  proxy's OpenAI-compatible route, and maps provider tool-call IDs
  to locally-minted canonical IDs at the boundary
- RoleMap (A11): executor/planner/delegate/consultant resolved from
  env; pinned() returns the mapping to record at task start
- Config strictly from env: NISSE_LLM_BASE_URL, NISSE_LLM_TOKEN;
  envfile.py is a minimal .env loader where real env wins; .env now
  gitignored with a committed .env.example documenting all variables
  (Matrix ones included for the upcoming transport PR)
- Live smoke test against the proxy is integration-marked and skips
  without credentials — CI stays credential-free per operator
  decision; the test runs locally once .env exists
- httpx added as nisse-dev's HTTP client

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
dongarandClaude Fable 5 committed 2026-09-06 20:57:51 +00:00
1 parent e185bb7c88
commit 0c2021fced
8 files changed
+462 -2

No files matched your search

+22
View File
@@ -0,0 +1,22 @@
"""Minimal .env loader (spec A5: config from env/.env, nothing hardcoded).
Deliberately tiny: KEY=VALUE lines, comments and blanks ignored, existing
environment variables never overridden (real env wins over the file).
"""
import os
from collections.abc import MutableMapping
from pathlib import Path
def load_env_file(path: Path, env: MutableMapping[str, str] = os.environ) -> None:
if not path.exists():
return
for line in path.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
key = key.strip()
if key and key not in env:
env[key] = value.strip()