Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/memory/hindsight/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1172,7 +1172,7 @@ def get_config_schema(self):
{"key": "api_url", "description": "Hindsight API URL", "default": _DEFAULT_LOCAL_URL, "when": {"mode": "local_external"}},
{"key": "api_key", "description": "API key (optional)", "secret": True, "env_var": "HINDSIGHT_API_KEY", "when": {"mode": "local_external"}},
# Local embedded mode
{"key": "llm_provider", "description": "LLM provider", "default": "openai", "choices": ["openai", "anthropic", "gemini", "groq", "openrouter", "minimax", "ollama", "lmstudio", "openai_compatible"], "when": {"mode": "local_embedded"}},
{"key": "llm_provider", "description": "LLM provider", "default": "openai", "choices": list(_PROVIDER_DEFAULT_MODELS), "when": {"mode": "local_embedded"}},
{"key": "llm_base_url", "description": "Endpoint URL (e.g. http://192.168.1.10:8080/v1)", "default": "", "when": {"mode": "local_embedded", "llm_provider": "openai_compatible"}},
{"key": "llm_api_key", "description": "LLM API key (optional for openai_compatible)", "secret": True, "env_var": "HINDSIGHT_LLM_API_KEY", "when": {"mode": "local_embedded"}},
{"key": "llm_model", "description": "LLM model", "default": "gpt-4o-mini", "default_from": {"field": "llm_provider", "map": _PROVIDER_DEFAULT_MODELS}, "when": {"mode": "local_embedded"}},
Expand Down
55 changes: 52 additions & 3 deletions plugins/memory/hindsight/config_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
"Cloud",
"Hindsight Cloud API (lightweight, just needs an API key)",
),
ProviderFieldOption(
"local_embedded",
"Local Embedded",
"Run a managed local Hindsight daemon with embedded PostgreSQL (needs LLM API key)",
),
ProviderFieldOption(
"local_external",
"Local External",
Expand All @@ -35,11 +40,55 @@
),
ProviderField(
key="api_key",
label="API key",
label="Cloud API key",
kind=KIND_SECRET,
env_key="HINDSIGHT_API_KEY",
description="Used to authenticate with the Hindsight API.",
placeholder="Enter Hindsight API key",
description="Hindsight Cloud API key (cloud mode only).",
placeholder="Enter Hindsight Cloud API key",
inline=True,
),
ProviderField(
key="llm_api_key",
label="LLM API key",
kind=KIND_SECRET,
env_key="HINDSIGHT_LLM_API_KEY",
description="LLM API key for memory extraction (local embedded mode).",
placeholder="Enter LLM API key",
inline=True,
),
ProviderField(
key="llm_provider",
label="LLM provider",
kind=KIND_SELECT,
default="openai",
description="LLM provider for local embedded mode.",
options=(
ProviderFieldOption("openai", "openai"),
ProviderFieldOption("anthropic", "anthropic"),
ProviderFieldOption("gemini", "gemini"),
ProviderFieldOption("groq", "groq"),
ProviderFieldOption("openrouter", "openrouter"),
ProviderFieldOption("minimax", "minimax"),
ProviderFieldOption("ollama", "ollama"),
ProviderFieldOption("lmstudio", "lmstudio"),
ProviderFieldOption("openai_compatible", "openai_compatible"),
),
inline=True,
),
ProviderField(
key="llm_base_url",
label="LLM base URL",
kind=KIND_TEXT,
description="OpenAI-compatible endpoint URL (for openai_compatible provider).",
env_fallbacks=("HINDSIGHT_API_LLM_BASE_URL",),
inline=True,
),
ProviderField(
key="llm_model",
label="LLM model",
kind=KIND_TEXT,
default="gpt-4o-mini",
description="Model name for LLM calls (e.g. gpt-4o-mini, glm-5.2-heavy).",
inline=True,
),
ProviderField(
Expand Down
44 changes: 36 additions & 8 deletions tests/hermes_cli/test_web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -771,30 +771,58 @@ def test_openviking_dashboard_rejects_blocked_endpoint_before_saving(self):



def test_declared_surface_put_writes_config_and_secret(self):
def test_declared_surface_put_round_trips_supported_llm_provider(self):
from hermes_constants import get_hermes_home
from hermes_cli.config import load_env

resp = self.client.put(
"/api/memory/providers/hindsight/config?surface=declared",
json={
"values": {
"mode": "local_external",
"api_url": "http://localhost:8888",
"api_key": "hs-declared-key",
"mode": "local_embedded",
"llm_provider": "groq",
"llm_model": "openai/gpt-oss-120b",
"llm_api_key": "llm-declared-key",
}
},
)

assert resp.status_code == 200
assert resp.json() == {"ok": True}
assert load_env()["HINDSIGHT_API_KEY"] == "hs-declared-key"
assert load_env()["HINDSIGHT_LLM_API_KEY"] == "llm-declared-key"

config_path = get_hermes_home() / "hindsight" / "config.json"
provider_config = json.loads(config_path.read_text(encoding="utf-8"))
assert provider_config["mode"] == "local_external"
assert provider_config["api_url"] == "http://localhost:8888"
assert "api_key" not in provider_config
assert provider_config["mode"] == "local_embedded"
assert provider_config["llm_provider"] == "groq"
assert provider_config["llm_model"] == "openai/gpt-oss-120b"
assert "llm_api_key" not in provider_config

get_resp = self.client.get(
"/api/memory/providers/hindsight/config?surface=declared"
)
fields = self._provider_field_map(get_resp.json())
assert fields["llm_provider"]["value"] == "groq"
assert fields["llm_api_key"]["value"] == ""
assert fields["llm_api_key"]["is_set"] is True

def test_declared_surface_put_rejects_unsupported_llm_provider(self):
from hermes_constants import get_hermes_home

resp = self.client.put(
"/api/memory/providers/hindsight/config?surface=declared",
json={
"values": {
"mode": "local_embedded",
"llm_provider": "unsupported",
}
},
)

assert resp.status_code == 400
assert resp.json()["detail"] == "Invalid value for 'llm_provider'"
assert not (get_hermes_home() / "hindsight" / "config.json").exists()




Expand Down
34 changes: 31 additions & 3 deletions tests/plugins/memory/test_hindsight_config_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ def test_hindsight_is_declared():
assert {field.key for field in provider.fields} == {
"mode",
"api_key",
"llm_api_key",
"llm_provider",
"llm_base_url",
"llm_model",
"api_url",
"bank_id",
"recall_budget",
Expand All @@ -36,9 +40,7 @@ def test_mode_gating_is_expressed_as_select_options():

mode = next(field for field in provider.fields if field.key == "mode")
assert mode.kind == KIND_SELECT
assert mode.allowed_values() == {"cloud", "local_external"}
# local_embedded is intentionally unsupported on desktop.
assert "local_embedded" not in mode.allowed_values()
assert mode.allowed_values() == {"cloud", "local_embedded", "local_external"}


def test_api_key_is_a_secret_bound_to_env():
Expand All @@ -49,3 +51,29 @@ def test_api_key_is_a_secret_bound_to_env():
assert api_key.kind == KIND_SECRET
assert api_key.is_secret is True
assert api_key.env_key == "HINDSIGHT_API_KEY"


def test_local_embedded_llm_fields_are_declared():
provider = get_provider_config_schema("hindsight")
assert provider is not None

fields = {field.key: field for field in provider.fields}
llm_api_key = fields["llm_api_key"]
assert llm_api_key.kind == KIND_SECRET
assert llm_api_key.is_secret is True
assert llm_api_key.env_key == "HINDSIGHT_LLM_API_KEY"

llm_provider = fields["llm_provider"]
assert llm_provider.kind == KIND_SELECT
assert llm_provider.default == "openai"
assert llm_provider.allowed_values() == {
"openai",
"anthropic",
"gemini",
"groq",
"openrouter",
"minimax",
"ollama",
"lmstudio",
"openai_compatible",
}
7 changes: 7 additions & 0 deletions tests/plugins/memory/test_hindsight_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -1247,6 +1247,13 @@ def test_schema_has_all_new_fields(self, provider):
}
assert expected_keys.issubset(keys), f"Missing: {expected_keys - keys}"

def test_llm_provider_choices_match_default_model_providers(self, provider):
fields = {field["key"]: field for field in provider.get_config_schema()}

assert fields["llm_provider"]["choices"] == list(
fields["llm_model"]["default_from"]["map"]
)


# ---------------------------------------------------------------------------
# bank_id_template tests
Expand Down
Loading