From 0e2fdcec8d8b5b457d45b70b38dc63d0ade3f6e0 Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Fri, 11 Sep 2026 16:42:24 -0700 Subject: [PATCH] Embed passages through a verified llama endpoint Passages can be embedded by a local llama.cpp server serving the same model, configured from Settings -> Advanced. The server is trusted only because its vectors match the local model's, never by name: - Zotero.Embeddings.Endpoint: verify() embeds calibration passages locally and remotely (plus one chunk-length text) and stores a typed verdict (ok, unreachable, unauthorized, not-embeddings, width-mismatch, low-agreement, context-too-small) keyed to the URL and model version. Passages route to the endpoint only while a matching verdict says ok; queries always embed locally. - Every batch carries a sentinel text whose local vector is cached, so a server switched to another model or pooling is caught on that batch and nothing wrong is stored. Three consecutive failures skip the endpoint for the rest of the run; a run start rechecks a verified endpoint. - MODELS gains `serving` (GGUF repo and quant) for both bekko models; getCommand() renders the llama.cpp command with a 4096 context. - Preferences: status line and Configure Endpoint dialog with the URL, the facts the server must match, a copyable command, typed errors, and a Stop Using Endpoint button. Also fixes two tokenizer defects in the runtime's transformers.js that the endpoint comparison exposed, both affecting every locally embedded bekko vector: - Metaspace's leading word marker was never prepended (the runtime needs the legacy add_prefix_space flag). `leadingSpace: true` in MODELS adds it in embedMany(). Short texts had drifted to cosine 0.83-0.95 against the reference. - Newlines and repeated spaces were tokenized differently from the reference. Whitespace runs now collapse to one space in both embedding paths. --- .../preferences/embeddingsEndpoint.xhtml | 179 +++++++ .../preferences/preferences_advanced.js | 44 ++ .../preferences/preferences_advanced.xhtml | 5 + chrome/content/zotero/xpcom/embeddings.js | 474 +++++++++++++++++- chrome/locale/en-US/zotero/preferences.ftl | 45 ++ defaults/preferences/zotero.js | 3 + scss/preferences/_advanced.scss | 31 +- test/tests/embeddingsTest.js | 255 ++++++++++ 8 files changed, 1020 insertions(+), 16 deletions(-) create mode 100644 chrome/content/zotero/preferences/embeddingsEndpoint.xhtml diff --git a/chrome/content/zotero/preferences/embeddingsEndpoint.xhtml b/chrome/content/zotero/preferences/embeddingsEndpoint.xhtml new file mode 100644 index 0000000000..012a322794 --- /dev/null +++ b/chrome/content/zotero/preferences/embeddingsEndpoint.xhtml @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + diff --git a/chrome/content/zotero/preferences/preferences_advanced.js b/chrome/content/zotero/preferences/preferences_advanced.js index a659c16f8d..2e0f74b54e 100644 --- a/chrome/content/zotero/preferences/preferences_advanced.js +++ b/chrome/content/zotero/preferences/preferences_advanced.js @@ -110,6 +110,15 @@ Zotero_Preferences.Advanced = { Zotero.Embeddings.Indexing.stopIndexing(); }); + document.getElementById('semantic-search-endpoint-configure').addEventListener('command', () => { + this.openSemanticSearchEndpointDialog(); + }); + // The endpoint's stored verdict is read lazily; have it in memory + // before the status line first renders + Zotero.Embeddings.Endpoint.load().then(() => { + this.updateSemanticSearchUI(Zotero.Embeddings.Indexing.getStatus()); + }); + // Render current state, then compute up-to-date per-library counts this.updateSemanticSearchUI(Zotero.Embeddings.Indexing.getStatus()); Zotero.Embeddings.Indexing.refreshStatus(); @@ -161,6 +170,8 @@ Zotero_Preferences.Advanced = { return; } + this.updateSemanticSearchEndpointUI(status.endpoint); + // Phase / status message let phaseLabel = document.getElementById('semantic-search-phase'); let hasRemaining = status.items.done < status.items.total @@ -235,6 +246,39 @@ Zotero_Preferences.Advanced = { }, + // Whether the active model can be served at all, and how the configured + // server stands (see Zotero.Embeddings.Endpoint.getStatus()) + updateSemanticSearchEndpointUI: function (endpoint) { + let row = document.getElementById('semantic-search-endpoint-row'); + row.hidden = !Zotero.Embeddings.Endpoint.isSupported(); + if (row.hidden) { + return; + } + let label = document.getElementById('semantic-search-endpoint-status'); + let id = { + off: 'off', + unknown: null, + unverified: 'unverified', + ok: 'valid', + unreachable: 'unreachable' + }[endpoint.state]; + if (id === null) { + label.removeAttribute('data-l10n-id'); + label.value = ''; + return; + } + document.l10n.setAttributes(label, `preferences-advanced-semantic-search-endpoint-${id || 'invalid'}`); + }, + + + openSemanticSearchEndpointDialog: function () { + let io = { ok: false }; + window.openDialog('chrome://zotero/content/preferences/embeddingsEndpoint.xhtml', + 'zotero-preferences-embeddingsEndpoint', 'chrome,modal,centerscreen', io); + this.updateSemanticSearchUI(Zotero.Embeddings.Indexing.getStatus()); + }, + + // Key/value rows of pipeline diagnostics for developers, so the labels // are plain English rather than localized _updateSemanticSearchDiagnostics: function (diagnostics, eta) { diff --git a/chrome/content/zotero/preferences/preferences_advanced.xhtml b/chrome/content/zotero/preferences/preferences_advanced.xhtml index fb2bc89d49..46104c9c1f 100644 --- a/chrome/content/zotero/preferences/preferences_advanced.xhtml +++ b/chrome/content/zotero/preferences/preferences_advanced.xhtml @@ -343,6 +343,11 @@ data-l10n-id="preferences-advanced-semantic-search-stop" hidden="true"/> +