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:
commit
327b18bf47
11
.env.example
11
.env.example
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
Loading…
Reference in New Issue