Merge PR #13: provider-agnostic LLM extraction (brian-doherty)

Combines HERMES_AGENT_NAME + DeepSeek/custom endpoint vars in
.env.example Strongly Recommended section.
This commit is contained in:
ClaudioDrews 2026-06-03 16:37:38 -03:00
commit 327b18bf47
2 changed files with 90 additions and 11 deletions

View File

@ -68,10 +68,19 @@ OLLAMA_EMBEDDING_MODEL=nomic-embed-text
# multi-agent deployments cannot tell agents apart.
HERMES_AGENT_NAME=hermes
# Alternative LLM API keys — the extraction pipeline is provider-agnostic.
# Priority: ICARUS_ENDPOINT + ICARUS_API_KEY_ENV → DEEPSEEK_API_KEY → OPENROUTER_API_KEY
# Set DEEPSEEK_API_KEY to route directly to api.deepseek.com (no middleman):
# DEEPSEEK_API_KEY=sk-...
# Fully custom endpoint override (base URL for /v1/chat/completions)
# ICARUS_ENDPOINT=https://my-custom-api.example.com/v1/chat/completions
# ICARUS_API_KEY_ENV=CUSTOM_API_KEY
# LLM extraction token limit — 1024 is too small, causes fabric truncation
ICARUS_EXTRACTION_MAX_TOKENS=4096
# LLM extraction model (any OpenRouter chat model)
# LLM extraction model (any OpenRouter chat model, or bare name for direct APIs)
ICARUS_EXTRACTION_MODEL=deepseek/deepseek-v4-flash
# Embedding dimensions — must match Qdrant collection schema

View File

@ -17,9 +17,81 @@ _OPENROUTER_KEY = (
or os.environ.get("OPENROUTER_DS_API_KEY", "")
or os.environ.get("OPENROUTER_API_KEY", "")
)
_DEEPSEEK_KEY = os.environ.get("DEEPSEEK_API_KEY", "")
# Provider-agnostic endpoint resolution.
# Consumers call _resolve_llm_endpoint() and _resolve_llm_headers()
# instead of reading the globals or hardcoding URLs.
# Priority: 1) ICARUS_ENDPOINT + ICARUS_API_KEY_ENV override,
# 2) DEEPSEEK_API_KEY → api.deepseek.com,
# 3) OPENROUTER_API_KEY → openrouter.ai (existing behaviour).
_ICARUS_ENDPOINT = os.environ.get("ICARUS_ENDPOINT", "").strip().rstrip("/")
_ICARUS_API_KEY_ENV = os.environ.get("ICARUS_API_KEY_ENV", "").strip()
_EXTRACTION_MODEL = os.environ.get("ICARUS_EXTRACTION_MODEL", "deepseek/deepseek-v4-flash")
_EXTRACTION_MAX_TOKENS = int(os.environ.get("ICARUS_EXTRACTION_MAX_TOKENS", "1024"))
def _resolve_llm_endpoint() -> str:
"""Return the base URL for LLM chat completions.
Priority:
1. ``ICARUS_ENDPOINT`` env var (fully custom).
2. ``api.deepseek.com/v1/chat/completions`` when ``DEEPSEEK_API_KEY`` is set.
3. ``https://openrouter.ai/api/v1/chat/completions`` (default / legacy).
"""
if _ICARUS_ENDPOINT:
return _ICARUS_ENDPOINT
if _DEEPSEEK_KEY:
return "https://api.deepseek.com/v1/chat/completions"
return "https://openrouter.ai/api/v1/chat/completions"
def _resolve_llm_api_key() -> str:
"""Return the API key for the resolved endpoint.
Priority (mirrors _resolve_llm_endpoint):
1. The env var named by ``ICARUS_API_KEY_ENV``, if set.
2. ``DEEPSEEK_API_KEY`` when available.
3. ``OPENROUTER_API_KEY`` (default).
"""
if _ICARUS_API_KEY_ENV:
custom_key = os.environ.get(_ICARUS_API_KEY_ENV, "")
if custom_key:
return custom_key
if _DEEPSEEK_KEY:
return _DEEPSEEK_KEY
return _OPENROUTER_KEY
def _resolve_llm_headers(api_key: str) -> dict:
"""Return the HTTP headers for the resolved endpoint."""
endpoint = _resolve_llm_endpoint()
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
# OpenRouter requires referer + title headers
if "openrouter.ai" in endpoint:
headers["HTTP-Referer"] = "https://hermes-agent.local"
headers["X-Title"] = "Icarus Session Extraction"
return headers
def _resolve_llm_model() -> str:
"""Return the model name compatible with the resolved endpoint.
OpenRouter uses ``provider/model`` slugs (e.g. ``deepseek/deepseek-v4-flash``).
Direct API endpoints (DeepSeek, custom) use bare model names.
"""
model = _EXTRACTION_MODEL
endpoint = _resolve_llm_endpoint()
if "openrouter.ai" not in endpoint and "/" in model:
# Strip the provider prefix for direct API calls
_, bare = model.split("/", 1)
return bare
return model
logger = logging.getLogger(__name__)
# ── Truncation limits (env-configurable) ──
@ -676,8 +748,10 @@ def _llm_extract_entries(transcript):
Returns list of dicts: {type, summary, content, training_value}
Returns empty list on failure or if nothing worth preserving.
"""
if not _OPENROUTER_KEY:
logger.warning("icarus: no OpenRouter key — skipping LLM extraction")
api_key = _resolve_llm_api_key()
if not api_key:
logger.warning("icarus: no LLM API key found (checked ICARUS_API_KEY_ENV, "
"DEEPSEEK_API_KEY, OPENROUTER_API_KEY) — skipping LLM extraction")
return []
prompt = (
@ -701,7 +775,7 @@ def _llm_extract_entries(transcript):
)
payload = json.dumps({
"model": _EXTRACTION_MODEL,
"model": _resolve_llm_model(),
"messages": [
{"role": "system", "content": prompt},
{"role": "user", "content": transcript[:8000]}
@ -711,15 +785,11 @@ def _llm_extract_entries(transcript):
}).encode("utf-8")
try:
endpoint = _resolve_llm_endpoint()
req = urllib.request.Request(
"https://openrouter.ai/api/v1/chat/completions",
endpoint,
data=payload,
headers={
"Authorization": f"Bearer {_OPENROUTER_KEY}",
"Content-Type": "application/json",
"HTTP-Referer": "https://hermes-agent.local",
"X-Title": "Icarus Session Extraction"
}
headers=_resolve_llm_headers(api_key)
)
resp = urllib.request.urlopen(req, timeout=45)
body = json.loads(resp.read().decode("utf-8"))