superlocalmemory 3.4.23 → 3.4.24

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.
@@ -129,6 +129,11 @@ async def set_mode(request: Request):
129
129
  llm_model=old_config.llm.model,
130
130
  llm_api_key=old_config.llm.api_key,
131
131
  llm_api_base=old_config.llm.api_base,
132
+ embedding_provider=old_config.embedding.provider,
133
+ embedding_endpoint=old_config.embedding.api_endpoint,
134
+ embedding_key=old_config.embedding.api_key,
135
+ embedding_model_name=old_config.embedding.model_name,
136
+ embedding_dimension=old_config.embedding.dimension,
132
137
  )
133
138
  new_config.active_profile = old_config.active_profile
134
139
  new_config.save()
@@ -165,7 +170,10 @@ async def set_mode(request: Request):
165
170
 
166
171
  @router.post("/mode/set")
167
172
  async def set_full_config(request: Request):
168
- """Save mode + provider + model + API key together."""
173
+ """Save mode + provider + model + API key together.
174
+
175
+ V3.4.24: Also accepts embedding_* fields for custom embedding endpoints.
176
+ """
169
177
  try:
170
178
  body = await request.json()
171
179
  new_mode = body.get("mode", "a").lower()
@@ -187,6 +195,11 @@ async def set_full_config(request: Request):
187
195
  llm_model=model,
188
196
  llm_api_key=api_key,
189
197
  llm_api_base="http://localhost:11434" if provider == "ollama" else "",
198
+ embedding_provider=body.get("embedding_provider", ""),
199
+ embedding_endpoint=body.get("embedding_endpoint", ""),
200
+ embedding_key=body.get("embedding_key", ""),
201
+ embedding_model_name=body.get("embedding_model", ""),
202
+ embedding_dimension=int(body.get("embedding_dimension", 0) or 0),
190
203
  )
191
204
  config.active_profile = old.active_profile
192
205
  config.save()
@@ -213,11 +226,145 @@ async def set_full_config(request: Request):
213
226
  "mode": new_mode,
214
227
  "provider": provider,
215
228
  "model": model,
229
+ "embedding_provider": config.embedding.provider,
230
+ "embedding_model": config.embedding.model_name,
231
+ "embedding_dimension": config.embedding.dimension,
232
+ }
233
+ except Exception as e:
234
+ return JSONResponse({"error": str(e)}, status_code=500)
235
+
236
+
237
+ # ── V3.4.24: Embedding Configuration ────────────────────────────────
238
+
239
+ @router.get("/embedding/config")
240
+ async def get_embedding_config(request: Request):
241
+ """Return current embedding configuration."""
242
+ try:
243
+ from superlocalmemory.core.config import SLMConfig
244
+ config = SLMConfig.load()
245
+ emb = config.embedding
246
+ return {
247
+ "provider": emb.provider,
248
+ "model_name": emb.model_name,
249
+ "dimension": emb.dimension,
250
+ "api_endpoint": emb.api_endpoint,
251
+ "has_key": bool(emb.api_key),
252
+ "is_openai_compatible": emb.is_openai_compatible,
253
+ "mode": config.mode.value,
254
+ }
255
+ except Exception as e:
256
+ return JSONResponse({"error": str(e)}, status_code=500)
257
+
258
+
259
+ @router.put("/embedding/config")
260
+ async def set_embedding_config(request: Request):
261
+ """Update embedding configuration independently of mode switch."""
262
+ try:
263
+ body = await request.json()
264
+ from superlocalmemory.core.config import SLMConfig, EmbeddingConfig
265
+ config = SLMConfig.load()
266
+
267
+ new_provider = body.get("provider", config.embedding.provider)
268
+ new_model = body.get("model_name", config.embedding.model_name)
269
+ new_dim = int(body.get("dimension", config.embedding.dimension) or 768)
270
+ if not (64 <= new_dim <= 8192):
271
+ return JSONResponse({"error": f"Dimension must be 64-8192, got {new_dim}"}, status_code=400)
272
+ new_endpoint = body.get("api_endpoint", config.embedding.api_endpoint)
273
+ new_key = body.get("api_key", config.embedding.api_key)
274
+
275
+ old_emb = config.embedding
276
+ config.embedding = EmbeddingConfig(
277
+ model_name=new_model,
278
+ dimension=new_dim,
279
+ provider=new_provider,
280
+ api_endpoint=new_endpoint,
281
+ api_key=new_key,
282
+ ollama_model=old_emb.ollama_model,
283
+ ollama_base_url=old_emb.ollama_base_url,
284
+ api_version=old_emb.api_version,
285
+ deployment_name=old_emb.deployment_name,
286
+ )
287
+ config.save()
288
+
289
+ needs_reindex = (
290
+ old_emb.provider != new_provider
291
+ or old_emb.model_name != new_model
292
+ or old_emb.dimension != new_dim
293
+ )
294
+
295
+ # Kill workers so next request uses new config
296
+ try:
297
+ from superlocalmemory.core.worker_pool import WorkerPool
298
+ WorkerPool.shared().shutdown()
299
+ except Exception:
300
+ pass
301
+ if hasattr(request.app.state, "engine"):
302
+ request.app.state.engine = None
303
+
304
+ return {
305
+ "success": True,
306
+ "provider": new_provider,
307
+ "model_name": new_model,
308
+ "dimension": new_dim,
309
+ "needs_reindex": needs_reindex,
216
310
  }
217
311
  except Exception as e:
218
312
  return JSONResponse({"error": str(e)}, status_code=500)
219
313
 
220
314
 
315
+ @router.post("/embedding/test")
316
+ async def test_embedding_endpoint(request: Request):
317
+ """Test connectivity to a custom embedding endpoint."""
318
+ try:
319
+ import httpx
320
+ from urllib.parse import urlparse
321
+ body = await request.json()
322
+ endpoint = body.get("api_endpoint", "").rstrip("/")
323
+ model = body.get("model_name", "test")
324
+ api_key = body.get("api_key", "")
325
+
326
+ if not endpoint:
327
+ return JSONResponse({"error": "No endpoint provided"}, status_code=400)
328
+
329
+ parsed = urlparse(endpoint)
330
+ if parsed.scheme not in ("http", "https"):
331
+ return JSONResponse({"error": "Only http/https endpoints supported"}, status_code=400)
332
+ host = parsed.hostname or ""
333
+ if host in ("169.254.169.254", "metadata.google.internal"):
334
+ return JSONResponse({"error": "Cloud metadata endpoints not allowed"}, status_code=400)
335
+
336
+ if not endpoint.endswith("/embeddings"):
337
+ endpoint = f"{endpoint}/embeddings"
338
+
339
+ headers = {"Content-Type": "application/json"}
340
+ if api_key:
341
+ headers["Authorization"] = f"Bearer {api_key}"
342
+
343
+ payload = {"input": ["test embedding connection"], "model": model}
344
+
345
+ with httpx.Client(timeout=httpx.Timeout(15.0)) as client:
346
+ resp = client.post(endpoint, headers=headers, json=payload)
347
+ resp.raise_for_status()
348
+ data = resp.json()
349
+ emb_data = data.get("data", [])
350
+ if emb_data:
351
+ dim = len(emb_data[0].get("embedding", []))
352
+ return {
353
+ "success": True,
354
+ "message": f"Connected! Dimension: {dim}",
355
+ "dimension": dim,
356
+ }
357
+ return {"success": False, "error": "No embedding data returned"}
358
+ except httpx.HTTPStatusError as e:
359
+ return {"success": False, "error": f"HTTP {e.response.status_code}"}
360
+ except httpx.ConnectError:
361
+ return {"success": False, "error": "Cannot reach the embedding server. Is it running?"}
362
+ except httpx.TimeoutException:
363
+ return {"success": False, "error": "Connection timed out after 15 seconds."}
364
+ except Exception as e:
365
+ return {"success": False, "error": type(e).__name__}
366
+
367
+
221
368
  @router.post("/provider/test")
222
369
  async def test_provider(request: Request):
223
370
  """Test connectivity to an LLM provider."""
@@ -1593,13 +1740,8 @@ async def process_health(request: Request):
1593
1740
  processes["worker_pool"] = {"status": worker_status}
1594
1741
 
1595
1742
  # Memory usage of current process (approximate)
1596
- memory_mb = 0.0
1597
- try:
1598
- import resource
1599
- usage = resource.getrusage(resource.RUSAGE_SELF)
1600
- memory_mb = round(usage.ru_maxrss / (1024 * 1024), 1)
1601
- except Exception:
1602
- pass
1743
+ from superlocalmemory.core.platform_utils import get_rss_mb
1744
+ memory_mb = round(get_rss_mb(), 1)
1603
1745
 
1604
1746
  return {
1605
1747
  "processes": processes,
@@ -1007,8 +1007,53 @@
1007
1007
  </div>
1008
1008
  </div>
1009
1009
 
1010
+ <!-- Step 3: Embedding Configuration (V3.4.24) -->
1011
+ <div class="mt-3 pt-3 border-top" id="settings-embedding-panel">
1012
+ <h6 class="text-muted"><i class="bi bi-cpu"></i> Step 3: Embedding Model</h6>
1013
+ <p class="small text-muted mb-2">
1014
+ Controls how text is converted to vectors for semantic search.
1015
+ Default: local model (768d). Custom: any OpenAI-compatible endpoint.
1016
+ </p>
1017
+ <div class="row g-2 mb-2">
1018
+ <div class="col-md-4">
1019
+ <label class="form-label small">Embedding Provider</label>
1020
+ <select class="form-select form-select-sm" id="settings-emb-provider">
1021
+ <option value="default">Default (Local Model)</option>
1022
+ <option value="openai">Custom Endpoint (OpenAI-compatible)</option>
1023
+ </select>
1024
+ </div>
1025
+ <div class="col-md-4" id="settings-emb-model-col" style="display:none;">
1026
+ <label class="form-label small">Model Name</label>
1027
+ <input type="text" id="settings-emb-model" class="form-control form-control-sm" placeholder="e.g. Qwen3-Embedding">
1028
+ </div>
1029
+ <div class="col-md-4" id="settings-emb-dim-col" style="display:none;">
1030
+ <label class="form-label small">Dimension</label>
1031
+ <input type="number" id="settings-emb-dimension" class="form-control form-control-sm" placeholder="e.g. 1024" min="64" max="8192">
1032
+ </div>
1033
+ </div>
1034
+ <div class="row g-2 mb-2" id="settings-emb-endpoint-row" style="display:none;">
1035
+ <div class="col-md-8">
1036
+ <label class="form-label small">Embedding Endpoint</label>
1037
+ <input type="text" id="settings-emb-endpoint" class="form-control form-control-sm" placeholder="http://localhost:8045/v1/embeddings">
1038
+ </div>
1039
+ <div class="col-md-4">
1040
+ <label class="form-label small">API Key (optional)</label>
1041
+ <input type="password" id="settings-emb-key" class="form-control form-control-sm" placeholder="not-needed">
1042
+ </div>
1043
+ </div>
1044
+ <div id="settings-emb-test-row" style="display:none;">
1045
+ <button class="btn btn-sm btn-outline-info" id="settings-emb-test-btn">
1046
+ <i class="bi bi-lightning"></i> Test Embedding
1047
+ </button>
1048
+ <span id="settings-emb-test-result" class="ms-2 small"></span>
1049
+ </div>
1050
+ <div id="settings-emb-info" class="small text-muted mt-1">
1051
+ Using local <strong>nomic-embed-text-v1.5</strong> (768d)
1052
+ </div>
1053
+ </div>
1054
+
1010
1055
  <!-- Save button -->
1011
- <div class="mt-2">
1056
+ <div class="mt-3">
1012
1057
  <button class="btn btn-primary" id="settings-save-all">
1013
1058
  <i class="bi bi-check-circle"></i> Save Configuration
1014
1059
  </button>
@@ -353,20 +353,28 @@ async function saveAllSettings() {
353
353
  if (statusEl) { statusEl.textContent = 'Saving...'; statusEl.style.display = 'inline'; statusEl.className = 'ms-2 text-muted'; }
354
354
 
355
355
  try {
356
- // Save mode
356
+ // V3.4.24: Include embedding params in save payload
357
+ var embParams = getEmbeddingParams();
358
+ var payload = Object.assign({mode: mode, provider: provider, model: model, api_key: apiKey}, embParams);
357
359
  var modeResp = await fetch('/api/v3/mode/set', {
358
360
  method: 'POST',
359
361
  headers: {'Content-Type': 'application/json'},
360
- body: JSON.stringify({mode: mode, provider: provider, model: model, api_key: apiKey})
362
+ body: JSON.stringify(payload)
361
363
  });
362
364
 
363
365
  if (modeResp.ok) {
366
+ var modeData = await modeResp.json();
367
+ var msg = 'Configuration saved! Mode: ' + mode.toUpperCase() +
368
+ (provider !== 'none' ? ' | Provider: ' + provider : '');
369
+ if (modeData.needs_reindex) {
370
+ msg += ' | Embeddings will be re-indexed on next use (may take several minutes).';
371
+ }
364
372
  if (statusEl) {
365
- statusEl.textContent = 'Configuration saved! Mode: ' + mode.toUpperCase() +
366
- (provider !== 'none' ? ' | Provider: ' + provider : '');
367
- statusEl.className = 'ms-2 text-success fw-bold';
373
+ statusEl.textContent = msg;
374
+ statusEl.className = modeData.needs_reindex ? 'ms-2 text-warning fw-bold' : 'ms-2 text-success fw-bold';
368
375
  }
369
376
  loadModeSettings();
377
+ loadEmbeddingSettings();
370
378
  } else {
371
379
  if (statusEl) { statusEl.textContent = 'Save failed'; statusEl.className = 'ms-2 text-danger'; }
372
380
  }
@@ -381,10 +389,127 @@ async function saveAllSettings() {
381
389
  }, 5000);
382
390
  }
383
391
 
392
+ // ============================================================================
393
+ // Embedding Configuration (V3.4.24 — Custom OpenAI-compatible endpoints)
394
+ // ============================================================================
395
+
396
+ async function loadEmbeddingSettings() {
397
+ try {
398
+ var resp = await fetch('/api/v3/embedding/config');
399
+ if (!resp.ok) return;
400
+ var data = await resp.json();
401
+
402
+ var provEl = document.getElementById('settings-emb-provider');
403
+ if (provEl) {
404
+ provEl.value = data.is_openai_compatible ? 'openai' : 'default';
405
+ }
406
+
407
+ if (data.is_openai_compatible) {
408
+ var modelEl = document.getElementById('settings-emb-model');
409
+ if (modelEl) modelEl.value = data.model_name || '';
410
+ var dimEl = document.getElementById('settings-emb-dimension');
411
+ if (dimEl) dimEl.value = data.dimension || '';
412
+ var epEl = document.getElementById('settings-emb-endpoint');
413
+ if (epEl) epEl.value = data.api_endpoint || '';
414
+ }
415
+
416
+ updateEmbeddingUI();
417
+
418
+ var info = document.getElementById('settings-emb-info');
419
+ if (info) {
420
+ var _name = (data.model_name || 'unknown').replace(/[<>&"']/g, function(c) {
421
+ return {'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;',"'":'&#39;'}[c];
422
+ });
423
+ if (data.is_openai_compatible) {
424
+ info.innerHTML = 'Using custom endpoint: <strong>' + _name + '</strong> (' + data.dimension + 'd)';
425
+ } else {
426
+ info.innerHTML = 'Using local <strong>' + _name + '</strong> (' + data.dimension + 'd)';
427
+ }
428
+ }
429
+ } catch (e) {
430
+ console.log('Load embedding settings error:', e);
431
+ }
432
+ }
433
+
434
+ function updateEmbeddingUI() {
435
+ var provider = document.getElementById('settings-emb-provider')?.value || 'default';
436
+ var isCustom = provider === 'openai';
437
+
438
+ var modelCol = document.getElementById('settings-emb-model-col');
439
+ var dimCol = document.getElementById('settings-emb-dim-col');
440
+ var endpointRow = document.getElementById('settings-emb-endpoint-row');
441
+ var testRow = document.getElementById('settings-emb-test-row');
442
+
443
+ if (modelCol) modelCol.style.display = isCustom ? 'block' : 'none';
444
+ if (dimCol) dimCol.style.display = isCustom ? 'block' : 'none';
445
+ if (endpointRow) endpointRow.style.display = isCustom ? 'flex' : 'none';
446
+ if (testRow) testRow.style.display = isCustom ? 'block' : 'none';
447
+
448
+ var info = document.getElementById('settings-emb-info');
449
+ if (info && !isCustom) {
450
+ info.innerHTML = 'Using local <strong>nomic-embed-text-v1.5</strong> (768d)';
451
+ }
452
+ }
453
+
454
+ async function testEmbeddingEndpoint() {
455
+ var endpoint = document.getElementById('settings-emb-endpoint')?.value || '';
456
+ var model = document.getElementById('settings-emb-model')?.value || '';
457
+ var key = document.getElementById('settings-emb-key')?.value || '';
458
+ var resultEl = document.getElementById('settings-emb-test-result');
459
+
460
+ if (!endpoint) {
461
+ if (resultEl) { resultEl.textContent = 'Enter an endpoint first'; resultEl.className = 'ms-2 small text-danger'; }
462
+ return;
463
+ }
464
+
465
+ if (resultEl) { resultEl.textContent = 'Testing...'; resultEl.className = 'ms-2 small text-muted'; }
466
+
467
+ try {
468
+ var resp = await fetch('/api/v3/embedding/test', {
469
+ method: 'POST',
470
+ headers: {'Content-Type': 'application/json'},
471
+ body: JSON.stringify({api_endpoint: endpoint, model_name: model, api_key: key})
472
+ });
473
+ var data = await resp.json();
474
+ if (data.success) {
475
+ if (resultEl) { resultEl.textContent = data.message; resultEl.className = 'ms-2 small text-success fw-bold'; }
476
+ var dimEl = document.getElementById('settings-emb-dimension');
477
+ if (dimEl && data.dimension) {
478
+ if (!dimEl.value) {
479
+ dimEl.value = data.dimension;
480
+ } else if (parseInt(dimEl.value) !== data.dimension) {
481
+ if (resultEl) {
482
+ resultEl.textContent = 'Connected! Warning: endpoint returns ' + data.dimension + 'd but you entered ' + dimEl.value + 'd';
483
+ resultEl.className = 'ms-2 small text-warning fw-bold';
484
+ }
485
+ }
486
+ }
487
+ } else {
488
+ if (resultEl) { resultEl.textContent = 'Failed: ' + (data.error || 'Unknown'); resultEl.className = 'ms-2 small text-danger'; }
489
+ }
490
+ } catch (e) {
491
+ if (resultEl) { resultEl.textContent = 'Error: ' + e.message; resultEl.className = 'ms-2 small text-danger'; }
492
+ }
493
+ }
494
+
495
+ function getEmbeddingParams() {
496
+ var provider = document.getElementById('settings-emb-provider')?.value || 'default';
497
+ if (provider !== 'openai') return {};
498
+ return {
499
+ embedding_provider: 'openai',
500
+ embedding_endpoint: document.getElementById('settings-emb-endpoint')?.value || '',
501
+ embedding_model: document.getElementById('settings-emb-model')?.value || '',
502
+ embedding_dimension: parseInt(document.getElementById('settings-emb-dimension')?.value) || 0,
503
+ embedding_key: document.getElementById('settings-emb-key')?.value || '',
504
+ };
505
+ }
506
+
384
507
  // Bind events
385
508
  document.getElementById('settings-provider')?.addEventListener('change', updateProviderUI);
386
509
  document.getElementById('settings-save-all')?.addEventListener('click', saveAllSettings);
387
510
  document.getElementById('settings-test-btn')?.addEventListener('click', testConnection);
511
+ document.getElementById('settings-emb-provider')?.addEventListener('change', updateEmbeddingUI);
512
+ document.getElementById('settings-emb-test-btn')?.addEventListener('click', testEmbeddingEndpoint);
388
513
 
389
514
  // Mode radio buttons
390
515
  document.querySelectorAll('input[name="settings-mode-radio"]').forEach(function(radio) {
@@ -395,5 +520,6 @@ document.querySelectorAll('input[name="settings-mode-radio"]').forEach(function(
395
520
  document.getElementById('settings-tab')?.addEventListener('shown.bs.tab', function() {
396
521
  loadAutoSettings();
397
522
  loadModeSettings();
523
+ loadEmbeddingSettings();
398
524
  updateModeUI();
399
525
  });