Compare commits

...
Author SHA1 Message Date
Nicolò Boschi e03af83dc2 chore: regenerate docs skill and openapi spec 2026-03-31 17:44:59 +02:00
Nicolò Boschi cd71cf9105 refactor: replace LLMProvider classmethods with from_env() and document missing config fields
Remove for_memory(), for_answer_generation(), and for_judge() classmethods
that used hardcoded env var strings. Replace with a single from_env() that
uses config.py constants. for_answer_generation and for_judge were dead code
(only called from benchmarks). Benchmark files now construct LLMProvider
directly with their own env var fallback logic.

Also document 13 config fields that were missing from configuration.md:
LLM_EXTRA_BODY, LLM_GEMINI_SAFETY_SETTINGS, EMBEDDINGS_LOCAL_FORCE_CPU,
RERANKER_LOCAL_FORCE_CPU, RETAIN_BATCH_TOKENS, RETAIN_ENTITY_LOOKUP,
RETAIN_DEFAULT_STRATEGY, webhook settings, and audit log settings.
2026-03-31 17:30:21 +02:00
14 changed files with 183 additions and 115 deletions
@@ -715,25 +715,32 @@ class LLMProvider:
pass
@classmethod
def for_memory(cls) -> "LLMProvider":
"""Create provider for memory operations from environment variables."""
provider = os.getenv("HINDSIGHT_API_LLM_PROVIDER", "groq")
api_key = os.getenv("HINDSIGHT_API_LLM_API_KEY", "")
def from_env(cls) -> "LLMProvider":
"""Create provider from environment variables using config.py constants."""
from ..config import (
DEFAULT_LLM_MODEL,
DEFAULT_LLM_PROVIDER,
ENV_LLM_API_KEY,
ENV_LLM_BASE_URL,
ENV_LLM_EXTRA_BODY,
ENV_LLM_MODEL,
ENV_LLM_PROVIDER,
)
provider = os.getenv(ENV_LLM_PROVIDER, DEFAULT_LLM_PROVIDER)
api_key = os.getenv(ENV_LLM_API_KEY, "")
# API key not needed for openai-codex (uses OAuth), claude-code (uses Keychain OAuth),
# ollama (local), vertexai (uses GCP service account credentials),
# or litellm (uses provider-specific auth, e.g. AWS credentials for Bedrock)
if not api_key and not requires_api_key(provider):
pass # Provider handles its own auth
elif not api_key:
raise ValueError(
"HINDSIGHT_API_LLM_API_KEY environment variable is required (unless using openai-codex, claude-code, or litellm)"
f"{ENV_LLM_API_KEY} environment variable is required (unless using openai-codex, claude-code, or litellm)"
)
base_url = os.getenv("HINDSIGHT_API_LLM_BASE_URL", "")
model = os.getenv("HINDSIGHT_API_LLM_MODEL", "openai/gpt-oss-120b")
base_url = os.getenv(ENV_LLM_BASE_URL, "")
model = os.getenv(ENV_LLM_MODEL, DEFAULT_LLM_MODEL)
extra_body = json.loads(os.getenv(ENV_LLM_EXTRA_BODY, "null"))
extra_body = json.loads(os.getenv("HINDSIGHT_API_LLM_EXTRA_BODY", "null"))
return cls(
provider=provider,
api_key=api_key,
@@ -743,62 +750,6 @@ class LLMProvider:
extra_body=extra_body,
)
@classmethod
def for_answer_generation(cls) -> "LLMProvider":
"""Create provider for answer generation. Falls back to memory config if not set."""
provider = os.getenv("HINDSIGHT_API_ANSWER_LLM_PROVIDER", os.getenv("HINDSIGHT_API_LLM_PROVIDER", "groq"))
api_key = os.getenv("HINDSIGHT_API_ANSWER_LLM_API_KEY", os.getenv("HINDSIGHT_API_LLM_API_KEY", ""))
# API key not needed for providers with their own auth mechanisms
if not api_key and not requires_api_key(provider):
pass # Provider handles its own auth
elif not api_key:
raise ValueError(
"HINDSIGHT_API_LLM_API_KEY or HINDSIGHT_API_ANSWER_LLM_API_KEY environment variable is required "
"(unless using openai-codex, claude-code, or litellm)"
)
base_url = os.getenv("HINDSIGHT_API_ANSWER_LLM_BASE_URL", os.getenv("HINDSIGHT_API_LLM_BASE_URL", ""))
model = os.getenv("HINDSIGHT_API_ANSWER_LLM_MODEL", os.getenv("HINDSIGHT_API_LLM_MODEL", "openai/gpt-oss-120b"))
extra_body = json.loads(os.getenv("HINDSIGHT_API_LLM_EXTRA_BODY", "null"))
return cls(
provider=provider,
api_key=api_key,
base_url=base_url,
model=model,
reasoning_effort="high",
extra_body=extra_body,
)
@classmethod
def for_judge(cls) -> "LLMProvider":
"""Create provider for judge/evaluator operations. Falls back to memory config if not set."""
provider = os.getenv("HINDSIGHT_API_JUDGE_LLM_PROVIDER", os.getenv("HINDSIGHT_API_LLM_PROVIDER", "groq"))
api_key = os.getenv("HINDSIGHT_API_JUDGE_LLM_API_KEY", os.getenv("HINDSIGHT_API_LLM_API_KEY", ""))
# API key not needed for providers with their own auth mechanisms
if not api_key and not requires_api_key(provider):
pass # Provider handles its own auth
elif not api_key:
raise ValueError(
"HINDSIGHT_API_LLM_API_KEY or HINDSIGHT_API_JUDGE_LLM_API_KEY environment variable is required "
"(unless using openai-codex, claude-code, or litellm)"
)
base_url = os.getenv("HINDSIGHT_API_JUDGE_LLM_BASE_URL", os.getenv("HINDSIGHT_API_LLM_BASE_URL", ""))
model = os.getenv("HINDSIGHT_API_JUDGE_LLM_MODEL", os.getenv("HINDSIGHT_API_LLM_MODEL", "openai/gpt-oss-120b"))
extra_body = json.loads(os.getenv("HINDSIGHT_API_LLM_EXTRA_BODY", "null"))
return cls(
provider=provider,
api_key=api_key,
base_url=base_url,
model=model,
reasoning_effort="high",
extra_body=extra_body,
)
class ConfiguredLLMProvider:
"""
+1 -1
View File
@@ -126,7 +126,7 @@ def llm_config():
Provide LLM configuration for tests.
This can be used by tests that need to call LLM directly without memory system.
"""
return LLMConfig.for_memory()
return LLMConfig.from_env()
@pytest.fixture(scope="session")
@@ -36,7 +36,7 @@ class TestCausalRelationsValidation:
"""
context = "Personal life update"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
event_date = datetime(2024, 3, 15)
facts, _, usage = await extract_facts_from_text(
@@ -81,7 +81,7 @@ class TestCausalRelationsValidation:
"""
context = "Project update"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
event_date = datetime(2024, 6, 1)
facts, _, _ = await extract_facts_from_text(
@@ -118,7 +118,7 @@ class TestCausalRelationsValidation:
"""
context = "Personal achievement story"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
event_date = datetime(2024, 7, 15)
facts, _, _ = await extract_facts_from_text(
@@ -168,7 +168,7 @@ class TestCausalRelationsValidation:
"""
context = "Business impact analysis"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
event_date = datetime(2024, 4, 1)
facts, _, usage = await extract_facts_from_text(
@@ -205,7 +205,7 @@ class TestCausalRelationsValidation:
"""
context = "Career progression"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
event_date = datetime(2024, 5, 1)
facts, _, _ = await extract_facts_from_text(
@@ -35,7 +35,7 @@ After searching for weeks, I finally found a cheaper apartment in Brooklyn.
"""
context = "Personal story about housing change"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=text, event_date=datetime(2024, 3, 15), context=context, llm_config=llm_config, agent_name="TestUser",
@@ -105,7 +105,7 @@ The renovation took three months and cost $15,000.
"""
context = "Home repair story"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=text, event_date=datetime(2024, 6, 1), context=context, llm_config=llm_config, agent_name="TestUser",
@@ -136,7 +136,7 @@ Machine learning fascinated me so much that I changed my career to data science.
"""
context = "Career change story"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=text, event_date=datetime(2024, 1, 1), context=context, llm_config=llm_config, agent_name="TestUser",
@@ -164,7 +164,7 @@ The new role enabled me to lead a team of engineers.
"""
context = "Work promotion story"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=text, event_date=datetime(2024, 2, 15), context=context, llm_config=llm_config, agent_name="TestUser",
@@ -192,7 +192,7 @@ Reduced spending somewhat affected local businesses.
"""
context = "Economic impact story"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=text, event_date=datetime(2024, 4, 1), context=context, llm_config=llm_config, agent_name="TestUser",
@@ -23,7 +23,7 @@ I changed the return type of the `process_request` function from `dict` to `Resp
After that, I updated the three callers in `api/handlers.py` to destructure the new model fields.
The type checker was happy after the change but I noticed one test was still using the old dict keys.
"""
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=text,
event_date=datetime(2025, 3, 28),
@@ -50,7 +50,7 @@ The tests were failing with a ConnectionRefusedError on the Redis integration su
I traced it to the connection pool not being initialized before the first test ran.
I added a setup fixture that ensures the pool is warmed up, and all 47 tests pass now.
"""
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=text,
event_date=datetime(2025, 3, 28),
@@ -78,7 +78,7 @@ I proposed splitting it into two modules: token_validation.py and session_manage
The user approved my approach and I started with the token validation logic.
I discovered that the existing tests were mocking the wrong interface, so I had to rewrite them first.
"""
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=text,
event_date=datetime(2025, 3, 28),
@@ -106,7 +106,7 @@ I migrated our codebase from the old TypeVar approach to the new syntax.
The migration touched 23 files but was mostly mechanical.
PEP 695 defines the new type statement that makes generics more readable.
"""
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=text,
event_date=datetime(2025, 3, 28),
@@ -38,7 +38,7 @@ I ran into my neighbor Sarah who mentioned she's planning a trip to Italy next m
"""
context = "Personal diary entry"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=text,
@@ -83,7 +83,7 @@ User: Perfect, I'll make a reservation for Saturday at 7pm.
"""
context = "Restaurant recommendation conversation"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=text,
@@ -140,7 +140,7 @@ I edited about 20 photos from my recent trip to the mountains.
"""
context = "Personal blog post"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=text,
@@ -205,7 +205,7 @@ I edited about 20 photos from my recent trip to the mountains.
text = "\n".join([f"{turn['speaker']}: {turn['text']}" for turn in session])
context = f"Conversation between {data['conversation']['speaker_a']} and {data['conversation']['speaker_b']}"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=text,
@@ -267,7 +267,7 @@ I'm planning to visit Japan next year.
"""
context = "Personal info"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=text,
@@ -42,7 +42,7 @@ Marcus felt anxious about the upcoming interview.
"""
context = "Personal journal entry"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=text,
@@ -75,7 +75,7 @@ The music was so loud I could barely hear myself think.
"""
context = "Personal experience"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=text,
@@ -109,7 +109,7 @@ Maybe we should reconsider the timeline.
"""
context = "Team discussion"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=text,
@@ -143,7 +143,7 @@ I'm unable to attend the conference due to scheduling conflicts.
"""
context = "Personal profile discussion"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=text,
@@ -176,7 +176,7 @@ Unlike last year, we're ahead of schedule.
"""
context = "Project review"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=text,
@@ -210,7 +210,7 @@ She's enthusiastic about the opportunity.
"""
context = "Team meeting"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=text,
@@ -244,7 +244,7 @@ I'm planning to switch careers because I'm not fulfilled in my current role.
"""
context = "Personal goals discussion"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=text,
@@ -282,7 +282,7 @@ Family is the most important thing to her.
"""
context = "Personal values discussion"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=text,
@@ -315,7 +315,7 @@ I prefer presenting in person rather than virtually because I can read the room
"""
context = "Personal reflection"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
event_date = datetime(2024, 11, 13)
@@ -373,7 +373,7 @@ I'm planning to visit Tokyo next month.
"""
context = "Personal conversation"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
event_date = datetime(2024, 11, 13)
@@ -421,7 +421,7 @@ with a concert surrounded by music, joy and the warm summer breeze.
"""
context = "Conversation between Melanie and Caroline"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
event_date = datetime(2023, 8, 14, 14, 24)
last_error = None
@@ -496,7 +496,7 @@ It was a beautiful day and I plan to make this a regular habit.
"""
context = "Personal diary"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
event_date = datetime(2024, 11, 13)
@@ -547,7 +547,7 @@ It was a beautiful day and I plan to make this a regular habit.
"""Test that relative dates are converted to absolute dates."""
reference_date = datetime(2024, 3, 20, 14, 0, 0, tzinfo=UTC)
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
text = """
Yesterday I went hiking in Yosemite.
@@ -582,7 +582,7 @@ It was a beautiful day and I plan to make this a regular habit.
"""Test that facts without temporal info are still extracted."""
reference_date = datetime(2024, 3, 20, 14, 0, 0, tzinfo=UTC)
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
text = "Alice works at Google. She loves Python programming."
@@ -607,7 +607,7 @@ It was a beautiful day and I plan to make this a regular habit.
"""Test that absolute dates in text are preserved."""
reference_date = datetime(2024, 3, 20, 14, 0, 0, tzinfo=UTC)
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
text = """
On March 15, 2024, Alice joined Google.
@@ -662,7 +662,7 @@ great time! Every time I see it, I can't help but smile.
"""
context = "Conversation between Deborah and Jolene"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
event_date = datetime(2023, 2, 23)
@@ -715,7 +715,7 @@ I've learned so much from it.
"""
context = "Personal update"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=text,
@@ -785,7 +785,7 @@ Jamie: Congratulations! I'd love to read it.
context = "Podcast episode between you (Marcus) and Jamie discussing AI research"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=transcript,
@@ -831,7 +831,7 @@ We presented our findings to the team yesterday.
context = "Personal work log"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=text,
@@ -867,7 +867,7 @@ Jamie: [teasing] We'll see who's right, my Niners pick is solid.
context = "podcast episode on match prediction of week 10 - Marcus (you) and Jamie - 14 nov"
agent_name = "Marcus"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=transcript,
@@ -929,7 +929,7 @@ so the algorithm learns to box out. See you next week!
context = "Podcast episode between you (Marcus) and Jamie about AI"
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
max_retries = 3
last_error = None
+2 -2
View File
@@ -2256,7 +2256,7 @@ If the text contains both Italian and English content, extract ONLY the Italian
Il sistema di autenticazione è stato migrato a OAuth 2.0.
"""
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
facts, _, _ = await extract_facts_from_text(
text=text,
@@ -2491,7 +2491,7 @@ async def test_verbatim_extraction_mode():
"She holds a CKA certification and has 5 years of Kubernetes experience."
)
llm_config = LLMConfig.for_memory()
llm_config = LLMConfig.from_env()
contents = [RetainContent(content=text, event_date=datetime(2024, 3, 10, tzinfo=timezone.utc), context="onboarding notes")]
facts, chunks, _ = await extract_facts_from_contents(
contents=contents,
@@ -212,10 +212,22 @@ class LLMAnswerEvaluator:
"""LLM-based answer evaluator with configurable provider."""
def __init__(self):
"""Initialize with LLM configuration for judge/evaluator."""
"""Initialize with LLM configuration for judge/evaluator.
Uses HINDSIGHT_API_JUDGE_LLM_* env vars with fallback to HINDSIGHT_API_LLM_* for
benchmark-specific LLM configuration (separate from the API config system).
"""
import os
from hindsight_api.engine.llm_wrapper import LLMConfig
self.llm_config = LLMConfig.for_judge()
self.llm_config = LLMConfig(
provider=os.getenv("HINDSIGHT_API_JUDGE_LLM_PROVIDER", os.getenv("HINDSIGHT_API_LLM_PROVIDER", "openai")),
api_key=os.getenv("HINDSIGHT_API_JUDGE_LLM_API_KEY", os.getenv("HINDSIGHT_API_LLM_API_KEY", "")),
base_url=os.getenv("HINDSIGHT_API_JUDGE_LLM_BASE_URL", os.getenv("HINDSIGHT_API_LLM_BASE_URL", "")),
model=os.getenv("HINDSIGHT_API_JUDGE_LLM_MODEL", os.getenv("HINDSIGHT_API_LLM_MODEL", "gpt-4o-mini")),
reasoning_effort="high",
)
self.client = self.llm_config._client
self.model = self.llm_config.model
@@ -111,8 +111,18 @@ class LoComoAnswerGenerator(LLMAnswerGenerator):
"""LoComo-specific answer generator using configurable LLM provider."""
def __init__(self):
"""Initialize with LLM configuration for answer generation."""
self.llm_config = LLMConfig.for_answer_generation()
"""Initialize with LLM configuration for answer generation.
Uses HINDSIGHT_API_ANSWER_LLM_* env vars with fallback to HINDSIGHT_API_LLM_* for
benchmark-specific LLM configuration (separate from the API config system).
"""
self.llm_config = LLMConfig(
provider=os.getenv("HINDSIGHT_API_ANSWER_LLM_PROVIDER", os.getenv("HINDSIGHT_API_LLM_PROVIDER", "openai")),
api_key=os.getenv("HINDSIGHT_API_ANSWER_LLM_API_KEY", os.getenv("HINDSIGHT_API_LLM_API_KEY", "")),
base_url=os.getenv("HINDSIGHT_API_ANSWER_LLM_BASE_URL", os.getenv("HINDSIGHT_API_LLM_BASE_URL", "")),
model=os.getenv("HINDSIGHT_API_ANSWER_LLM_MODEL", os.getenv("HINDSIGHT_API_LLM_MODEL", "gpt-4o-mini")),
reasoning_effort="high",
)
self.client = self.llm_config._client
self.model = self.llm_config.model
@@ -150,7 +150,15 @@ class LongMemEvalAnswerGenerator(LLMAnswerGenerator):
- "json": Raw JSON dump of recall_result (original behavior)
- "structured": Human-readable format with facts grouped with source chunks
"""
self.llm_config = LLMConfig.for_answer_generation()
# Uses HINDSIGHT_API_ANSWER_LLM_* env vars with fallback to HINDSIGHT_API_LLM_* for
# benchmark-specific LLM configuration (separate from the API config system).
self.llm_config = LLMConfig(
provider=os.getenv("HINDSIGHT_API_ANSWER_LLM_PROVIDER", os.getenv("HINDSIGHT_API_LLM_PROVIDER", "openai")),
api_key=os.getenv("HINDSIGHT_API_ANSWER_LLM_API_KEY", os.getenv("HINDSIGHT_API_LLM_API_KEY", "")),
base_url=os.getenv("HINDSIGHT_API_ANSWER_LLM_BASE_URL", os.getenv("HINDSIGHT_API_LLM_BASE_URL", "")),
model=os.getenv("HINDSIGHT_API_ANSWER_LLM_MODEL", os.getenv("HINDSIGHT_API_LLM_MODEL", "gpt-4o-mini")),
reasoning_effort="high",
)
self.client = self.llm_config._client
self.model = self.llm_config.model
self.context_format = context_format
@@ -172,6 +172,8 @@ To switch between backends:
| `HINDSIGHT_API_LLM_TIMEOUT` | LLM request timeout in seconds | `120` |
| `HINDSIGHT_API_LLM_GROQ_SERVICE_TIER` | Groq service tier: `on_demand`, `flex`, `auto` | `auto` |
| `HINDSIGHT_API_LLM_OPENAI_SERVICE_TIER` | OpenAI service tier: `flex` for 50% cost savings (OpenAI Flex Processing) | None (default) |
| `HINDSIGHT_API_LLM_EXTRA_BODY` | JSON dict merged into `extra_body` for all OpenAI-compatible API calls. Useful for custom model servers (e.g., vLLM `chat_template_kwargs`). | `null` |
| `HINDSIGHT_API_LLM_GEMINI_SAFETY_SETTINGS` | JSON-encoded list of `{category, threshold}` dicts for Gemini/VertexAI content safety filtering | `null` |
**Provider Examples**
@@ -353,6 +355,7 @@ export HINDSIGHT_API_RETAIN_LLM_MAX_BACKOFF=120.0 # Cap at 2min instead of 1m
| `HINDSIGHT_API_EMBEDDINGS_PROVIDER` | Provider: `local`, `tei`, `openai`, `cohere`, `litellm`, or `litellm-sdk` | `local` |
| `HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL` | Model for local provider | `BAAI/bge-small-en-v1.5` |
| `HINDSIGHT_API_EMBEDDINGS_LOCAL_TRUST_REMOTE_CODE` | Allow loading models with custom code (security risk, disabled by default) | `false` |
| `HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU` | Force CPU mode for local embeddings (avoids MPS/XPC issues on macOS) | `false` |
| `HINDSIGHT_API_EMBEDDINGS_TEI_URL` | TEI server URL | - |
| `HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY` | OpenAI API key (falls back to `HINDSIGHT_API_LLM_API_KEY`) | - |
| `HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL` | OpenAI embedding model | `text-embedding-3-small` |
@@ -451,6 +454,7 @@ Supported OpenAI embedding dimensions:
| `HINDSIGHT_API_RERANKER_LOCAL_MODEL` | Model for local provider | `cross-encoder/ms-marco-MiniLM-L-6-v2` |
| `HINDSIGHT_API_RERANKER_LOCAL_MAX_CONCURRENT` | Max concurrent local reranking (prevents CPU thrashing under load) | `4` |
| `HINDSIGHT_API_RERANKER_LOCAL_TRUST_REMOTE_CODE` | Allow loading models with custom code (security risk, disabled by default) | `false` |
| `HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU` | Force CPU mode for local reranker (avoids MPS/XPC issues on macOS) | `false` |
| `HINDSIGHT_API_RERANKER_LOCAL_FP16` | Half-precision (FP16) inference for the local reranker. 2736% faster on MPS; quality-identical. Disabled by default to avoid regressions on non-MPS deployments — some CPUs lack native FP16 support. | `false` |
| `HINDSIGHT_API_RERANKER_LOCAL_BUCKET_BATCHING` | Sort pairs by token length before batching to reduce padding waste. 3654% faster across models; quality-identical by construction. | `false` |
| `HINDSIGHT_API_RERANKER_LOCAL_BATCH_SIZE` | Batch size for local reranker `predict()`. Optimal value varies by hardware and model (smaller batches can outperform larger ones on MPS). | `32` |
@@ -611,6 +615,9 @@ Controls the retain (memory ingestion) pipeline.
| `HINDSIGHT_API_RETAIN_CUSTOM_INSTRUCTIONS` | Full prompt override for fact extraction (only used when mode is `custom`). Replaces built-in extraction rules entirely. | - |
| `HINDSIGHT_API_RETAIN_EXTRACT_CAUSAL_LINKS` | Extract causal relationships between facts | `true` |
| `HINDSIGHT_API_RETAIN_BATCH_ENABLED` | Use LLM Batch API for fact extraction (50% cost savings, only with async operations) | `false` |
| `HINDSIGHT_API_RETAIN_BATCH_TOKENS` | Max characters per sub-batch for async retain auto-splitting | `10000` |
| `HINDSIGHT_API_RETAIN_ENTITY_LOOKUP` | Entity lookup method during retain: `full` (exact match) or `trigram` (fuzzy trigram matching) | `trigram` |
| `HINDSIGHT_API_RETAIN_DEFAULT_STRATEGY` | Default retain strategy name. When set, all retain calls without an explicit `strategy` parameter use this strategy. | - |
| `HINDSIGHT_API_RETAIN_BATCH_POLL_INTERVAL_SECONDS` | Batch API polling interval in seconds | `60` |
> **Entity labels** (`entity_labels`) and **free-form entity extraction** (`entities_allow_free_form`) are configured per bank via the [bank config API](/developer/api/memory-banks#retain-configuration), not as global environment variables — each bank can have its own controlled vocabulary. See [Entity Labels](/developer/retain#entity-labels) for details.
@@ -1022,6 +1029,23 @@ Configuration for background task processing. By default, the API processes task
| `HINDSIGHT_API_SKIP_LLM_VERIFICATION` | Skip LLM connection check on startup | `false` |
| `HINDSIGHT_API_LAZY_RERANKER` | Lazy-load reranker model (faster startup) | `false` |
### Webhooks
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_WEBHOOK_URL` | Global webhook URL for event delivery | - (disabled) |
| `HINDSIGHT_API_WEBHOOK_SECRET` | HMAC signing secret for webhook payloads | - (unsigned) |
| `HINDSIGHT_API_WEBHOOK_EVENT_TYPES` | Comma-separated list of event types to deliver via webhook | `consolidation.completed` |
| `HINDSIGHT_API_WEBHOOK_DELIVERY_POLL_INTERVAL_SECONDS` | How often the webhook delivery worker polls for pending deliveries (seconds) | `30` |
### Audit Logging
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_AUDIT_LOG_ENABLED` | Master switch for audit logging | `false` |
| `HINDSIGHT_API_AUDIT_LOG_ACTIONS` | Comma-separated allowlist of action types to audit (empty = all eligible actions) | `""` |
| `HINDSIGHT_API_AUDIT_LOG_RETENTION_DAYS` | Number of days to retain audit log entries. `-1` = keep forever. | `-1` |
### Programmatic Configuration
You can also configure the API programmatically using `MemoryEngine.from_env()`:
@@ -172,6 +172,8 @@ To switch between backends:
| `HINDSIGHT_API_LLM_TIMEOUT` | LLM request timeout in seconds | `120` |
| `HINDSIGHT_API_LLM_GROQ_SERVICE_TIER` | Groq service tier: `on_demand`, `flex`, `auto` | `auto` |
| `HINDSIGHT_API_LLM_OPENAI_SERVICE_TIER` | OpenAI service tier: `flex` for 50% cost savings (OpenAI Flex Processing) | None (default) |
| `HINDSIGHT_API_LLM_EXTRA_BODY` | JSON dict merged into `extra_body` for all OpenAI-compatible API calls. Useful for custom model servers (e.g., vLLM `chat_template_kwargs`). | `null` |
| `HINDSIGHT_API_LLM_GEMINI_SAFETY_SETTINGS` | JSON-encoded list of `{category, threshold}` dicts for Gemini/VertexAI content safety filtering | `null` |
**Provider Examples**
@@ -353,6 +355,7 @@ export HINDSIGHT_API_RETAIN_LLM_MAX_BACKOFF=120.0 # Cap at 2min instead of 1m
| `HINDSIGHT_API_EMBEDDINGS_PROVIDER` | Provider: `local`, `tei`, `openai`, `cohere`, `litellm`, or `litellm-sdk` | `local` |
| `HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL` | Model for local provider | `BAAI/bge-small-en-v1.5` |
| `HINDSIGHT_API_EMBEDDINGS_LOCAL_TRUST_REMOTE_CODE` | Allow loading models with custom code (security risk, disabled by default) | `false` |
| `HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU` | Force CPU mode for local embeddings (avoids MPS/XPC issues on macOS) | `false` |
| `HINDSIGHT_API_EMBEDDINGS_TEI_URL` | TEI server URL | - |
| `HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY` | OpenAI API key (falls back to `HINDSIGHT_API_LLM_API_KEY`) | - |
| `HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL` | OpenAI embedding model | `text-embedding-3-small` |
@@ -366,6 +369,7 @@ export HINDSIGHT_API_RETAIN_LLM_MAX_BACKOFF=120.0 # Cap at 2min instead of 1m
| `HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_API_KEY` | LiteLLM SDK API key for direct embedding provider access | - |
| `HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_MODEL` | LiteLLM SDK embedding model (use provider prefix, e.g., `cohere/embed-english-v3.0`) | `cohere/embed-english-v3.0` |
| `HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_API_BASE` | Custom base URL for LiteLLM SDK embeddings (optional) | - |
| `HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_OUTPUT_DIMENSIONS` | Optional output embedding dimensions (provider-dependent, e.g., `768` for Gemini embedding models) | - |
```bash
# Local (default) - uses SentenceTransformers
@@ -413,6 +417,8 @@ export HINDSIGHT_API_EMBEDDINGS_LITELLM_MODEL=text-embedding-3-small # or coher
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=litellm-sdk
export HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_API_KEY=your-provider-api-key
export HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_MODEL=cohere/embed-english-v3.0
# Optional: request a specific output dimension when the provider supports it
# export HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_OUTPUT_DIMENSIONS=768
# Supported LiteLLM SDK embedding providers:
# - cohere/embed-english-v3.0 (1024 dimensions)
@@ -426,6 +432,8 @@ export HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_MODEL=cohere/embed-english-v3.0
Hindsight automatically detects the embedding dimension from the model at startup and adjusts the database schema accordingly. The default model (`BAAI/bge-small-en-v1.5`) produces 384-dimensional vectors, while OpenAI models produce 1536 or 3072 dimensions.
For `litellm-sdk`, if you set `HINDSIGHT_API_EMBEDDINGS_LITELLM_SDK_OUTPUT_DIMENSIONS`, startup uses that output size when the underlying provider supports LiteLLM's `dimensions` parameter (otherwise behavior is unchanged). The same dimension-change rules below apply.
:::warning Dimension Changes
Once memories are stored, you cannot change the embedding dimension without losing data. If you need to switch to a model with different dimensions:
@@ -446,6 +454,7 @@ Supported OpenAI embedding dimensions:
| `HINDSIGHT_API_RERANKER_LOCAL_MODEL` | Model for local provider | `cross-encoder/ms-marco-MiniLM-L-6-v2` |
| `HINDSIGHT_API_RERANKER_LOCAL_MAX_CONCURRENT` | Max concurrent local reranking (prevents CPU thrashing under load) | `4` |
| `HINDSIGHT_API_RERANKER_LOCAL_TRUST_REMOTE_CODE` | Allow loading models with custom code (security risk, disabled by default) | `false` |
| `HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU` | Force CPU mode for local reranker (avoids MPS/XPC issues on macOS) | `false` |
| `HINDSIGHT_API_RERANKER_LOCAL_FP16` | Half-precision (FP16) inference for the local reranker. 2736% faster on MPS; quality-identical. Disabled by default to avoid regressions on non-MPS deployments — some CPUs lack native FP16 support. | `false` |
| `HINDSIGHT_API_RERANKER_LOCAL_BUCKET_BATCHING` | Sort pairs by token length before batching to reduce padding waste. 3654% faster across models; quality-identical by construction. | `false` |
| `HINDSIGHT_API_RERANKER_LOCAL_BATCH_SIZE` | Batch size for local reranker `predict()`. Optimal value varies by hardware and model (smaller batches can outperform larger ones on MPS). | `32` |
@@ -606,6 +615,9 @@ Controls the retain (memory ingestion) pipeline.
| `HINDSIGHT_API_RETAIN_CUSTOM_INSTRUCTIONS` | Full prompt override for fact extraction (only used when mode is `custom`). Replaces built-in extraction rules entirely. | - |
| `HINDSIGHT_API_RETAIN_EXTRACT_CAUSAL_LINKS` | Extract causal relationships between facts | `true` |
| `HINDSIGHT_API_RETAIN_BATCH_ENABLED` | Use LLM Batch API for fact extraction (50% cost savings, only with async operations) | `false` |
| `HINDSIGHT_API_RETAIN_BATCH_TOKENS` | Max characters per sub-batch for async retain auto-splitting | `10000` |
| `HINDSIGHT_API_RETAIN_ENTITY_LOOKUP` | Entity lookup method during retain: `full` (exact match) or `trigram` (fuzzy trigram matching) | `trigram` |
| `HINDSIGHT_API_RETAIN_DEFAULT_STRATEGY` | Default retain strategy name. When set, all retain calls without an explicit `strategy` parameter use this strategy. | - |
| `HINDSIGHT_API_RETAIN_BATCH_POLL_INTERVAL_SECONDS` | Batch API polling interval in seconds | `60` |
> **Entity labels** (`entity_labels`) and **free-form entity extraction** (`entities_allow_free_form`) are configured per bank via the [bank config API](api/memory-banks.md#retain-configuration), not as global environment variables — each bank can have its own controlled vocabulary. See [Entity Labels](retain.md#entity-labels) for details.
@@ -1017,6 +1029,23 @@ Configuration for background task processing. By default, the API processes task
| `HINDSIGHT_API_SKIP_LLM_VERIFICATION` | Skip LLM connection check on startup | `false` |
| `HINDSIGHT_API_LAZY_RERANKER` | Lazy-load reranker model (faster startup) | `false` |
### Webhooks
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_WEBHOOK_URL` | Global webhook URL for event delivery | - (disabled) |
| `HINDSIGHT_API_WEBHOOK_SECRET` | HMAC signing secret for webhook payloads | - (unsigned) |
| `HINDSIGHT_API_WEBHOOK_EVENT_TYPES` | Comma-separated list of event types to deliver via webhook | `consolidation.completed` |
| `HINDSIGHT_API_WEBHOOK_DELIVERY_POLL_INTERVAL_SECONDS` | How often the webhook delivery worker polls for pending deliveries (seconds) | `30` |
### Audit Logging
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_AUDIT_LOG_ENABLED` | Master switch for audit logging | `false` |
| `HINDSIGHT_API_AUDIT_LOG_ACTIONS` | Comma-separated allowlist of action types to audit (empty = all eligible actions) | `""` |
| `HINDSIGHT_API_AUDIT_LOG_RETENTION_DAYS` | Number of days to retain audit log entries. `-1` = keep forever. | `-1` |
### Programmatic Configuration
You can also configure the API programmatically using `MemoryEngine.from_env()`:
@@ -5815,6 +5815,32 @@
"title": "Tags",
"description": "Tags associated with this document",
"default": []
},
"document_metadata": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"title": "Document Metadata",
"description": "Document metadata"
},
"retain_params": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"title": "Retain Params",
"description": "Parameters used during retain"
}
},
"type": "object",
@@ -5833,9 +5859,17 @@
"bank_id": "user123",
"content_hash": "abc123",
"created_at": "2024-01-15T10:30:00Z",
"document_metadata": {
"channel": "#general",
"source": "slack"
},
"id": "session_1",
"memory_unit_count": 15,
"original_text": "Full document text here...",
"retain_params": {
"context": "Team meeting notes",
"event_date": "2024-01-15"
},
"tags": [
"user_a",
"session_123"