superlocalmemory 3.4.53 → 3.4.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superlocalmemory",
3
- "version": "3.4.53",
3
+ "version": "3.4.55",
4
4
  "description": "Information-geometric agent memory with mathematical guarantees. 4-channel retrieval, Fisher-Rao similarity, zero-LLM mode, EU AI Act compliant. Works with Claude, Cursor, Windsurf, and 17+ AI tools.",
5
5
  "keywords": [
6
6
  "ai-memory",
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "superlocalmemory"
3
- version = "3.4.53"
3
+ version = "3.4.55"
4
4
  description = "Information-geometric agent memory with mathematical guarantees"
5
5
  readme = "README.md"
6
6
  license = {text = "AGPL-3.0-or-later"}
@@ -239,11 +239,63 @@ if [ -f "${REPO_DIR}/mcp_server.py" ]; then
239
239
  echo "✓ MCP server copied"
240
240
  fi
241
241
 
242
- # Copy config if not exists
243
- if [ ! -f "${INSTALL_DIR}/config.json" ]; then
244
- echo "Creating default config..."
245
- cp "${REPO_DIR}/config.json" "${INSTALL_DIR}/config.json"
246
- echo " Config created"
242
+ # Interactive mode selection (v3.4.55)
243
+ if [ ! -f "${INSTALL_DIR}/current_mode" ] && [ "${NON_INTERACTIVE}" != "true" ]; then
244
+ echo ""
245
+ echo "┌─────────────────────────────────────────────┐"
246
+ echo "│ SuperLocalMemory V3 — Setup │"
247
+ echo "└─────────────────────────────────────────────┘"
248
+ echo ""
249
+ echo " Choose operating mode:"
250
+ echo ""
251
+ echo " [A] Zero-Cloud — Pure local, no API keys needed"
252
+ echo " • Embedding: sentence-transformers (local)"
253
+ echo " • LLM: none"
254
+ echo " • Best for: privacy, air-gapped, EU AI Act"
255
+ echo ""
256
+ echo " [B] Local AI — Ollama-powered (recommended)"
257
+ echo " • Embedding: ollama / nomic-embed-text"
258
+ echo " • LLM: ollama / llama3.2"
259
+ echo " • Best for: full offline AI, zero cost"
260
+ echo ""
261
+ echo " [C] Cloud Power — OpenRouter / OpenAI API"
262
+ echo " • Embedding: text-embedding-3-large"
263
+ echo " • LLM: claude-sonnet-4 / gpt-4.1-mini"
264
+ echo " • Best for: max quality, API required"
265
+ echo ""
266
+ read -r -p " Enter mode [A/B/C] (default: B): " MODE_CHOICE
267
+ MODE_CHOICE=${MODE_CHOICE:-b}
268
+ case "${MODE_CHOICE,,}" in
269
+ a) SELECTED_MODE="a" ;;
270
+ c) SELECTED_MODE="c" ;;
271
+ *) SELECTED_MODE="b" ;;
272
+ esac
273
+ echo ""
274
+ echo " → Selected Mode ${SELECTED_MODE^^}"
275
+ echo ""
276
+ # Generate initial 3-mode config via Python
277
+ python3 -c "
278
+ from superlocalmemory.core.config import SLMConfig
279
+ SLMConfig.migrate_to_3mode()
280
+ SLMConfig.switch_mode('${SELECTED_MODE}')
281
+ print('✓ Mode ${SELECTED_MODE^^} configured')
282
+ print(' Config files created: mode_a.json, mode_b.json, mode_c.json')
283
+ print(' Run slm mode a/b/c to switch modes later')
284
+ "
285
+ elif [ ! -f "${INSTALL_DIR}/config.json" ] && [ "${NON_INTERACTIVE}" = "true" ]; then
286
+ echo "Creating default config (non-interactive, Mode B)..."
287
+ python3 -c "
288
+ from superlocalmemory.core.config import SLMConfig
289
+ SLMConfig.migrate_to_3mode()
290
+ print('✓ Default config created (Mode B)')
291
+ "
292
+ elif [ -f "${INSTALL_DIR}/config.json" ] && [ ! -f "${INSTALL_DIR}/current_mode" ]; then
293
+ echo "Migrating existing config to 3-mode system..."
294
+ python3 -c "
295
+ from superlocalmemory.core.config import SLMConfig
296
+ SLMConfig.migrate_to_3mode()
297
+ print('✓ Config migrated — your settings preserved')
298
+ "
247
299
  else
248
300
  echo "○ Config exists (keeping existing)"
249
301
  fi
@@ -3,7 +3,7 @@
3
3
  import os
4
4
  os.environ.setdefault("KMP_DUPLICATE_LIB_OK", "TRUE")
5
5
 
6
- __version__ = "3.4.53"
6
+ __version__ = "3.4.55"
7
7
 
8
8
  _REQUIRED_VERSIONS = {
9
9
  "sentence_transformers": "5.3.0",
@@ -652,30 +652,11 @@ def cmd_mode(args: Namespace) -> None:
652
652
 
653
653
  config = SLMConfig.load()
654
654
 
655
- def _apply_mode_change(new_value: str) -> tuple[SLMConfig, bool]:
656
- """Mutate-in-place mode switch. Returns (updated_config, llm_was_set).
657
-
658
- Only changes ``config.mode``. If the user has no LLM provider
659
- configured AND is moving to Mode B or C, populates the mode's
660
- default LLM block so the daemon has something to talk to.
661
- Everything else (embedding, retrieval, evolution, forgetting,
662
- math, profile) is preserved byte-for-byte.
663
- """
664
- new_mode = Mode(new_value)
665
- llm_was_set = False
666
- if new_mode != Mode.A and not config.llm.provider:
667
- defaults = SLMConfig.for_mode(new_mode)
668
- config.llm = defaults.llm
669
- llm_was_set = True
670
- config.mode = new_mode
671
- config.save(mode_change=True)
672
- return config, llm_was_set
673
-
674
655
  if getattr(args, 'json', False):
675
656
  from superlocalmemory.cli.json_output import json_print
676
657
  if args.value:
677
658
  old_mode = config.mode.value.upper()
678
- updated, _ = _apply_mode_change(args.value)
659
+ updated = SLMConfig.switch_mode(args.value)
679
660
  json_print("mode", data={
680
661
  "previous_mode": old_mode, "current_mode": args.value.upper(),
681
662
  }, next_actions=[
@@ -690,22 +671,17 @@ def cmd_mode(args: Namespace) -> None:
690
671
  return
691
672
 
692
673
  if args.value:
693
- updated, llm_was_set = _apply_mode_change(args.value)
674
+ updated = SLMConfig.switch_mode(args.value)
694
675
  print(f"Mode set to: {args.value.upper()}")
695
-
696
- # v3.4.43: embedding/retrieval are now preserved, so the old
697
- # "Embedding model changed. Re-indexing will run on next recall."
698
- # warning no longer fires from a CLI mode switch — that was the
699
- # symptom of the bug. The warning is retained ONLY as an
700
- # informational note when LLM defaults were freshly populated.
701
- if llm_was_set:
702
- print(f" ℹ LLM provider populated from mode defaults: "
703
- f"{updated.llm.provider}/{updated.llm.model}. "
704
- f"Run `slm provider set` to customize.")
676
+ print(f" Embedding: {updated.embedding.provider}/{updated.embedding.model_name}")
677
+ if args.value.lower() != "a":
678
+ print(f" LLM: {updated.llm.provider}/{updated.llm.model}")
679
+ print(f" Reranker: ONNX cross-encoder (enabled)")
705
680
 
706
681
  # V3.3.4: Warn if Mode C lacks cloud API key
707
682
  if args.value == "c" and not updated.llm.api_key:
708
683
  print(" ⚠ Mode C requires a cloud API key. Run: slm provider set")
684
+ print(" ℹ Run `slm restart` to apply the new mode.")
709
685
  else:
710
686
  print(f"Current mode: {config.mode.value.upper()}")
711
687
 
@@ -12,9 +12,12 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
12
12
 
13
13
  from __future__ import annotations
14
14
 
15
+ import logging
15
16
  from dataclasses import dataclass, field
16
17
  from pathlib import Path
17
18
 
19
+ logger = logging.getLogger(__name__)
20
+
18
21
  from superlocalmemory.storage.models import Mode
19
22
 
20
23
 
@@ -25,6 +28,9 @@ from superlocalmemory.storage.models import Mode
25
28
  DEFAULT_BASE_DIR = Path.home() / ".superlocalmemory"
26
29
  DEFAULT_DB_NAME = "memory.db"
27
30
  DEFAULT_PROFILES_FILE = "profiles.json"
31
+ CURRENT_MODE_FILE = "current_mode"
32
+ # Populated lazily in _get_mode_config_path() to avoid circular imports
33
+ _MODE_CONFIG_NAMES: dict | None = None
28
34
 
29
35
 
30
36
  # ---------------------------------------------------------------------------
@@ -950,3 +956,180 @@ class SLMConfig:
950
956
  sheaf_contradiction_threshold=0.65, # Higher for 3072d embeddings
951
957
  ),
952
958
  )
959
+
960
+ # ------------------------------------------------------------------
961
+ # 3-Mode config system (v3.4.54)
962
+ # ------------------------------------------------------------------
963
+
964
+ @staticmethod
965
+ def _mode_config_path(base_dir: Path, mode: "Mode") -> Path:
966
+ """Return the per-mode config file path."""
967
+ from superlocalmemory.storage.models import Mode as _M
968
+ _names = {
969
+ _M.A: "mode_a.json",
970
+ _M.B: "mode_b.json",
971
+ _M.C: "mode_c.json",
972
+ }
973
+ return base_dir / _names.get(mode, "mode_a.json")
974
+
975
+ @staticmethod
976
+ def read_current_mode(base_dir: Path | None = None) -> str:
977
+ """Read the current mode from the ``current_mode`` file.
978
+
979
+ Returns ``\"b\"`` (the default) if the file doesn't exist.
980
+ """
981
+ _base = base_dir or DEFAULT_BASE_DIR
982
+ _f = _base / CURRENT_MODE_FILE
983
+ try:
984
+ return _f.read_text(encoding="utf-8").strip().lower() or "b"
985
+ except (OSError, FileNotFoundError):
986
+ return "b"
987
+
988
+ @staticmethod
989
+ def write_current_mode(mode: str, base_dir: Path | None = None) -> None:
990
+ """Write the current mode letter to ``current_mode``."""
991
+ _base = base_dir or DEFAULT_BASE_DIR
992
+ _base.mkdir(parents=True, exist_ok=True)
993
+ (_base / CURRENT_MODE_FILE).write_text(
994
+ mode.lower().strip(), encoding="utf-8",
995
+ )
996
+
997
+ @classmethod
998
+ def switch_mode(
999
+ cls,
1000
+ new_mode: str,
1001
+ base_dir: Path | None = None,
1002
+ ) -> "SLMConfig":
1003
+ """Switch to a different mode, preserving the old mode's config.
1004
+
1005
+ v3.4.54: Saves the current config as ``mode_{old}.json``, then
1006
+ loads ``mode_{new}.json`` (or creates it from defaults if it
1007
+ doesn't exist). Writes ``current_mode`` and persists the new
1008
+ active config as ``config.json`` for backward compatibility
1009
+ with tooling that reads it directly.
1010
+
1011
+ Returns the new active config.
1012
+ """
1013
+ from superlocalmemory.storage.models import Mode as _M
1014
+ _base = base_dir or DEFAULT_BASE_DIR
1015
+ _base.mkdir(parents=True, exist_ok=True)
1016
+
1017
+ old_config = cls.load(_base / "config.json")
1018
+ old_mode = old_config.mode.value.lower()
1019
+
1020
+ new_mode_val = _M(new_mode.lower())
1021
+
1022
+ # 1. Save current config to its per-mode file (unless already
1023
+ # matched — prevents overwriting user customizations)
1024
+ if old_mode != new_mode.lower():
1025
+ old_path = cls._mode_config_path(_base, old_config.mode)
1026
+ old_config.save(old_path)
1027
+
1028
+ # 2. Try to load the target mode's saved config.
1029
+ new_path = cls._mode_config_path(_base, new_mode_val)
1030
+ need_migration = False
1031
+ if new_path.exists():
1032
+ try:
1033
+ new_config = cls.load(new_path)
1034
+ # Ensure the loaded config actually has the right mode
1035
+ if new_config.mode != new_mode_val:
1036
+ new_config = cls.for_mode(new_mode_val, base_dir=_base)
1037
+ except Exception:
1038
+ new_config = cls.for_mode(new_mode_val, base_dir=_base)
1039
+ else:
1040
+ # First time switching to this mode: try migrating from
1041
+ # legacy config.json if it was already in this mode
1042
+ legacy = _base / "config.json"
1043
+ if legacy.exists():
1044
+ try:
1045
+ import json
1046
+ _data = json.loads(legacy.read_text(encoding="utf-8"))
1047
+ _legacy_mode = _data.get("mode", "").lower()
1048
+ if _legacy_mode == new_mode.lower():
1049
+ # config.json IS this mode — use it directly
1050
+ new_config = cls.load(legacy)
1051
+ need_migration = True
1052
+ else:
1053
+ new_config = cls.for_mode(new_mode_val, base_dir=_base)
1054
+ except Exception:
1055
+ new_config = cls.for_mode(new_mode_val, base_dir=_base)
1056
+ else:
1057
+ new_config = cls.for_mode(new_mode_val, base_dir=_base)
1058
+
1059
+ # 3. Save as active config.json (backward compat)
1060
+ new_config.save(_base / "config.json", mode_change=True)
1061
+
1062
+ # 4. Write current_mode
1063
+ cls.write_current_mode(new_mode, _base)
1064
+
1065
+ # 5. On first migration, also save mode_a.json and mode_c.json
1066
+ # so users have a complete set
1067
+ if need_migration:
1068
+ for _m in (_M.A, _M.B, _M.C):
1069
+ _mp = cls._mode_config_path(_base, _m)
1070
+ if not _mp.exists():
1071
+ try:
1072
+ _mc = cls.for_mode(_m, base_dir=_base)
1073
+ _mc.save(_mp)
1074
+ except Exception:
1075
+ pass
1076
+
1077
+ return new_config
1078
+
1079
+ @classmethod
1080
+ def migrate_to_3mode(cls, base_dir: Path | None = None) -> bool:
1081
+ """One-time migration: config.json → 3-mode system.
1082
+
1083
+ Called on daemon boot. Idempotent — if ``current_mode`` already
1084
+ exists, this is a no-op. Returns True if migration was performed.
1085
+ """
1086
+ _base = base_dir or DEFAULT_BASE_DIR
1087
+ _current = _base / CURRENT_MODE_FILE
1088
+ if _current.exists():
1089
+ return False # already migrated
1090
+
1091
+ legacy = _base / "config.json"
1092
+ if not legacy.exists():
1093
+ # No config at all — write defaults and current_mode
1094
+ from superlocalmemory.storage.models import Mode as _M
1095
+ _def = cls.for_mode(_M.B, base_dir=_base)
1096
+ _def.save(legacy)
1097
+ cls.write_current_mode("b", _base)
1098
+ for _m in (_M.A, _M.B, _M.C):
1099
+ _mp = cls._mode_config_path(_base, _m)
1100
+ if not _mp.exists():
1101
+ try:
1102
+ _mc = cls.for_mode(_m, base_dir=_base)
1103
+ _mc.save(_mp)
1104
+ except Exception:
1105
+ pass
1106
+ return True
1107
+
1108
+ # Migrate existing config.json → mode_{current}.json
1109
+ try:
1110
+ config = cls.load(legacy)
1111
+ current = config.mode.value.lower()
1112
+ cls.write_current_mode(current, _base)
1113
+
1114
+ # Save current config to its mode file
1115
+ mode_path = cls._mode_config_path(_base, config.mode)
1116
+ config.save(mode_path)
1117
+
1118
+ # Generate other mode files from defaults
1119
+ from superlocalmemory.storage.models import Mode as _M
1120
+ for _m in (_M.A, _M.B, _M.C):
1121
+ _mp = cls._mode_config_path(_base, _m)
1122
+ if not _mp.exists():
1123
+ try:
1124
+ _mc = cls.for_mode(_m, base_dir=_base)
1125
+ _mc.save(_mp)
1126
+ except Exception:
1127
+ pass
1128
+
1129
+ logger.info(
1130
+ "3-mode config system migrated: config.json → mode_%s.json. "
1131
+ "All three mode configs generated.", current,
1132
+ )
1133
+ return True
1134
+ except Exception:
1135
+ return False
@@ -446,6 +446,9 @@ async def lifespan(application: FastAPI):
446
446
  from superlocalmemory.core.config import SLMConfig
447
447
  from superlocalmemory.core.engine import MemoryEngine
448
448
 
449
+ # v3.4.54: one-time migration config.json → 3-mode system
450
+ SLMConfig.migrate_to_3mode()
451
+
449
452
  config = SLMConfig.load()
450
453
  engine = MemoryEngine(config)
451
454
  engine.initialize()
@@ -1095,6 +1098,192 @@ def _register_dashboard_routes(application: FastAPI) -> None:
1095
1098
  async def api_version():
1096
1099
  return JSONResponse({"version": _SLM_VERSION})
1097
1100
 
1101
+ # v3.4.55: Mode switching & config API for the dashboard UI.
1102
+ # The auto-settings.js expects /api/v3/* endpoints. These routes
1103
+ # bridge the 3-mode config system to the existing settings page.
1104
+
1105
+ @application.get("/api/v3/auto")
1106
+ async def v3_auto_detect():
1107
+ """Auto-detect available providers from environment."""
1108
+ import os as _os
1109
+ providers = []
1110
+ if _os.environ.get("OPENROUTER_API_KEY"):
1111
+ providers.append({"id": "openrouter", "name": "OpenRouter", "has_key": True})
1112
+ if _os.environ.get("OPENAI_API_KEY"):
1113
+ providers.append({"id": "openai", "name": "OpenAI", "has_key": True})
1114
+ if _os.environ.get("ANTHROPIC_API_KEY"):
1115
+ providers.append({"id": "anthropic", "name": "Anthropic", "has_key": True})
1116
+ # Ollama is always available as a local option if the server is reachable
1117
+ try:
1118
+ import httpx as _hx
1119
+ _r = _hx.get("http://localhost:11434/api/tags", timeout=2.0)
1120
+ ollama_models = []
1121
+ if _r.status_code == 200:
1122
+ ollama_models = [m["name"] for m in _r.json().get("models", [])]
1123
+ providers.append({
1124
+ "id": "ollama", "name": "Ollama (local)",
1125
+ "has_key": False, "running": True,
1126
+ "models": ollama_models,
1127
+ })
1128
+ except Exception:
1129
+ providers.append({
1130
+ "id": "ollama", "name": "Ollama (local)",
1131
+ "has_key": False, "running": False, "models": [],
1132
+ })
1133
+ return {"providers": providers}
1134
+
1135
+ @application.get("/api/v3/mode")
1136
+ async def v3_get_mode():
1137
+ """Get current mode and available modes."""
1138
+ from superlocalmemory.core.config import SLMConfig
1139
+ from superlocalmemory.storage.models import Mode as _M
1140
+ _base = Path.home() / ".superlocalmemory"
1141
+ current = SLMConfig.read_current_mode(_base)
1142
+ modes = {}
1143
+ for _m in (_M.A, _M.B, _M.C):
1144
+ _name = _m.value.lower()
1145
+ _path = SLMConfig._mode_config_path(_base, _m)
1146
+ _cfg = None
1147
+ if _path.exists():
1148
+ try:
1149
+ _cfg = SLMConfig.load(_path)
1150
+ except Exception:
1151
+ pass
1152
+ modes[_name] = {
1153
+ "label": {"a": "Zero-Cloud", "b": "Local AI", "c": "Cloud Power"}[_name],
1154
+ "config_exists": _path.exists(),
1155
+ "embedding_provider": getattr(_cfg.embedding, "provider", "") if _cfg else "",
1156
+ "embedding_model": getattr(_cfg.embedding, "model_name", "") if _cfg else "",
1157
+ "llm_provider": getattr(_cfg.llm, "provider", "") if _cfg else "",
1158
+ "llm_model": getattr(_cfg.llm, "model", "") if _cfg else "",
1159
+ "reranker": _cfg.retrieval.use_cross_encoder if _cfg else True,
1160
+ }
1161
+ return {"current_mode": current, "modes": modes}
1162
+
1163
+ @application.post("/api/v3/mode/set")
1164
+ async def v3_set_mode(request: Request):
1165
+ """Switch mode and optionally update provider/model. Body matches
1166
+ the auto-settings.js saveSettings() payload."""
1167
+ from superlocalmemory.core.config import SLMConfig
1168
+ try:
1169
+ body = await request.json()
1170
+ new_mode = (body.get("mode") or body.get("settings_mode") or "").lower().strip()
1171
+ if new_mode not in ("a", "b", "c"):
1172
+ return JSONResponse(
1173
+ {"ok": False, "error": "mode must be a, b, or c"},
1174
+ status_code=400,
1175
+ )
1176
+ config = SLMConfig.switch_mode(new_mode)
1177
+
1178
+ # If provider/model were sent, update the saved config
1179
+ provider = body.get("provider", "").strip()
1180
+ if provider and new_mode != "a":
1181
+ _base = Path.home() / ".superlocalmemory"
1182
+ from superlocalmemory.core.config import LLMConfig, EmbeddingConfig
1183
+ # Update LLM
1184
+ model = body.get("model", "").strip()
1185
+ api_key = body.get("api_key", "").strip()
1186
+ endpoint = body.get("endpoint", "").strip()
1187
+ if provider or model:
1188
+ config.llm = LLMConfig(
1189
+ provider=provider or config.llm.provider,
1190
+ model=model or config.llm.model,
1191
+ api_key=api_key or config.llm.api_key,
1192
+ api_base=endpoint or config.llm.api_base,
1193
+ )
1194
+ # Update embedding
1195
+ emb_provider = body.get("embedding_provider", "").strip()
1196
+ emb_model = body.get("embedding_model", "").strip()
1197
+ emb_key = body.get("embedding_key", "").strip()
1198
+ if emb_provider or emb_model:
1199
+ config.embedding = EmbeddingConfig(
1200
+ provider=emb_provider or config.embedding.provider,
1201
+ model_name=emb_model or config.embedding.model_name,
1202
+ dimension=config.embedding.dimension,
1203
+ api_key=emb_key or config.embedding.api_key,
1204
+ )
1205
+ config.save(mode_change=True)
1206
+
1207
+ return {
1208
+ "ok": True, "mode": new_mode,
1209
+ "embedding": f"{config.embedding.provider}/{config.embedding.model_name}",
1210
+ "llm": f"{config.llm.provider}/{config.llm.model}",
1211
+ "message": f"Switched to Mode {new_mode.upper()}. Run slm restart to apply.",
1212
+ }
1213
+ except Exception as exc:
1214
+ return JSONResponse({"ok": False, "error": str(exc)}, status_code=500)
1215
+
1216
+ @application.get("/api/v3/ollama/status")
1217
+ async def v3_ollama_status():
1218
+ """Check if Ollama is running and list available models."""
1219
+ try:
1220
+ import httpx as _hx
1221
+ _r = _hx.get("http://localhost:11434/api/tags", timeout=3.0)
1222
+ if _r.status_code == 200:
1223
+ _data = _r.json()
1224
+ return {
1225
+ "running": True,
1226
+ "models": [{"name": m["name"], "size": m.get("size", 0)}
1227
+ for m in _data.get("models", [])],
1228
+ }
1229
+ except Exception:
1230
+ pass
1231
+ return {"running": False, "models": []}
1232
+
1233
+ @application.post("/api/v3/provider/test")
1234
+ async def v3_provider_test(request: Request):
1235
+ """Test a provider connection. Body: {provider, api_key, endpoint}."""
1236
+ try:
1237
+ body = await request.json()
1238
+ provider = body.get("provider", "")
1239
+ api_key = body.get("api_key", "")
1240
+ endpoint = body.get("endpoint", "")
1241
+ if provider == "ollama":
1242
+ import httpx as _hx
1243
+ _r = _hx.get(f"{endpoint or 'http://localhost:11434'}/api/tags", timeout=3.0)
1244
+ return {"ok": _r.status_code == 200, "message": "Ollama reachable" if _r.status_code == 200 else f"HTTP {_r.status_code}"}
1245
+ if provider in ("openai", "openrouter"):
1246
+ import httpx as _hx
1247
+ _url = f"{endpoint or 'https://api.openai.com/v1'}/models"
1248
+ _headers = {"Authorization": f"Bearer {api_key}"}
1249
+ _r = _hx.get(_url, headers=_headers, timeout=5.0)
1250
+ return {"ok": _r.status_code == 200, "message": "API key valid" if _r.status_code == 200 else f"HTTP {_r.status_code}: {_r.text[:200]}"}
1251
+ return {"ok": False, "message": f"Unknown provider: {provider}"}
1252
+ except Exception as exc:
1253
+ return {"ok": False, "message": str(exc)}
1254
+
1255
+ @application.get("/api/v3/embedding/config")
1256
+ async def v3_get_embedding_config():
1257
+ """Get current embedding configuration."""
1258
+ engine = getattr(application.state, "engine", None)
1259
+ if engine is None:
1260
+ return JSONResponse({"ok": False, "error": "engine not initialized"}, status_code=503)
1261
+ config = getattr(engine, "_config", None)
1262
+ if config is None:
1263
+ return JSONResponse({"ok": False, "error": "no config loaded"}, status_code=503)
1264
+ return {
1265
+ "provider": getattr(config.embedding, "provider", ""),
1266
+ "model_name": getattr(config.embedding, "model_name", ""),
1267
+ "dimension": getattr(config.embedding, "dimension", 0),
1268
+ }
1269
+
1270
+ @application.post("/api/v3/embedding/test")
1271
+ async def v3_embedding_test(request: Request):
1272
+ """Test embedding with current config. Body: {text: \"test\"}."""
1273
+ try:
1274
+ body = await request.json()
1275
+ text = body.get("text", "test embedding")
1276
+ engine = getattr(application.state, "engine", None)
1277
+ if engine is None:
1278
+ return {"ok": False, "error": "engine not initialized"}
1279
+ embedder = getattr(engine, "_embedder", None)
1280
+ if embedder is None:
1281
+ return {"ok": False, "error": "embedder not available"}
1282
+ vec = embedder.embed(text)
1283
+ return {"ok": True, "dimensions": len(vec) if vec else 0}
1284
+ except Exception as exc:
1285
+ return {"ok": False, "error": str(exc)}
1286
+
1098
1287
  @application.get("/", response_class=HTMLResponse)
1099
1288
  async def root():
1100
1289
  index_path = UI_DIR / "index.html"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: superlocalmemory
3
- Version: 3.4.53
3
+ Version: 3.4.55
4
4
  Summary: Information-geometric agent memory with mathematical guarantees
5
5
  Author-email: Varun Pratap Bhardwaj <admin@superlocalmemory.com>
6
6
  License: AGPL-3.0-or-later