local-embed 0.2.2__tar.gz → 0.2.4__tar.gz
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.
Potentially problematic release.
This version of local-embed might be problematic. Click here for more details.
- {local_embed-0.2.2 → local_embed-0.2.4}/.gitignore +4 -4
- local_embed-0.2.4/PKG-INFO +470 -0
- local_embed-0.2.4/README.md +444 -0
- local_embed-0.2.4/install.ps1 +85 -0
- local_embed-0.2.4/install.sh +73 -0
- {local_embed-0.2.2 → local_embed-0.2.4}/local_embed/cli.py +28 -48
- {local_embed-0.2.2 → local_embed-0.2.4}/local_embed/cmd_set.py +42 -31
- {local_embed-0.2.2 → local_embed-0.2.4}/local_embed/config.py +88 -29
- {local_embed-0.2.2 → local_embed-0.2.4}/local_embed/engine.py +195 -140
- {local_embed-0.2.2 → local_embed-0.2.4}/local_embed/logging.py +49 -2
- local_embed-0.2.4/local_embed/server.py +498 -0
- {local_embed-0.2.2 → local_embed-0.2.4}/local_embed/server_utils.py +41 -57
- {local_embed-0.2.2 → local_embed-0.2.4}/pyproject.toml +3 -3
- {local_embed-0.2.2 → local_embed-0.2.4}/tests/test_client.py +13 -1
- local_embed-0.2.4/tests/test_config.py +231 -0
- local_embed-0.2.4/tests/test_engine.py +429 -0
- {local_embed-0.2.2 → local_embed-0.2.4}/tests/test_logging.py +10 -0
- local_embed-0.2.4/tests/test_server.py +326 -0
- {local_embed-0.2.2 → local_embed-0.2.4}/tests/test_set.py +39 -56
- local_embed-0.2.2/PKG-INFO +0 -316
- local_embed-0.2.2/README.md +0 -290
- local_embed-0.2.2/local_embed/server.py +0 -379
- local_embed-0.2.2/tests/test_config.py +0 -42
- local_embed-0.2.2/tests/test_engine.py +0 -268
- local_embed-0.2.2/tests/test_server.py +0 -152
- {local_embed-0.2.2 → local_embed-0.2.4}/local_embed/__init__.py +0 -0
- {local_embed-0.2.2 → local_embed-0.2.4}/local_embed/__main__.py +0 -0
- {local_embed-0.2.2 → local_embed-0.2.4}/local_embed/threads.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# Runtime config — personal per-user settings
|
|
2
|
-
slife.json5
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
# Runtime config — personal per-user settings. The three initial configs
|
|
2
|
+
# (slife.json5 / local_embed.json5 / tools.json5) are git-tracked so the
|
|
3
|
+
# one-click installers can seed them as out-of-the-box defaults. Copies in a
|
|
4
|
+
# user's data dir (~/.slife/) remain personal and are NOT tracked.
|
|
5
5
|
|
|
6
6
|
# Playwright MCP browser traces (test artifacts)
|
|
7
7
|
.playwright-mcp/
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: local-embed
|
|
3
|
+
Version: 0.2.4
|
|
4
|
+
Summary: Standalone local embedding server — GGUF / transformer models as an OpenAI-compatible /v1/embeddings service
|
|
5
|
+
Project-URL: Homepage, https://github.com/juzcn/slife
|
|
6
|
+
Project-URL: Repository, https://github.com/juzcn/slife
|
|
7
|
+
Project-URL: Documentation, https://github.com/juzcn/slife/blob/main/local-embed/README.md
|
|
8
|
+
Author-email: juzcn <zhangjun@cueb.edu.cn>
|
|
9
|
+
License: MIT
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Terminals
|
|
17
|
+
Requires-Python: >=3.13
|
|
18
|
+
Requires-Dist: fastmcp>=2.0.0
|
|
19
|
+
Requires-Dist: json-five>=1.1.2
|
|
20
|
+
Requires-Dist: starlette>=0.37.0
|
|
21
|
+
Provides-Extra: gguf
|
|
22
|
+
Requires-Dist: llama-cpp-python==0.3.34; extra == 'gguf'
|
|
23
|
+
Provides-Extra: transformer
|
|
24
|
+
Requires-Dist: sentence-transformers; extra == 'transformer'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# local-embed
|
|
28
|
+
|
|
29
|
+
**A standalone local embedding service.** It loads one local GGUF
|
|
30
|
+
(llama-cpp) or HF transformer embedding model and exposes it as an
|
|
31
|
+
[OpenAI-compatible](https://platform.openai.com/docs/api-reference/embeddings)
|
|
32
|
+
`/v1/embeddings` URL — any OpenAI-compatible client can embed text through it.
|
|
33
|
+
|
|
34
|
+
On one port it serves two surfaces: an OpenAI-compatible `/v1/*` API for
|
|
35
|
+
embedding, and a Streamable HTTP MCP endpoint at `/mcp`.
|
|
36
|
+
|
|
37
|
+
local-embed is three things at once: an independent app, a Streamable HTTP
|
|
38
|
+
**MCP server** (see [As an MCP server](#as-an-mcp-server)), and a conforming
|
|
39
|
+
**slife plugin** (see [Loading as a slife plugin](#loading-as-a-slife-plugin)).
|
|
40
|
+
The product is the embedding URL; the MCP and plugin surfaces are how hosts
|
|
41
|
+
manage the same standalone service.
|
|
42
|
+
|
|
43
|
+
Two backends: **`gguf`** loads a local `.gguf` file (llama-cpp-python);
|
|
44
|
+
**`transformer`** loads an HF repo id from the local hub cache
|
|
45
|
+
(sentence-transformers). The server never downloads weights — you pre-download
|
|
46
|
+
them ([Model weights](#model-weights)).
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
any OpenAI-compatible client ──┐
|
|
50
|
+
slife (memdb / memfiles) ──────┤── POST /v1/embeddings ──┐
|
|
51
|
+
vector DB / RAG pipeline ──────┘ ▼
|
|
52
|
+
┌───────────────────────┐
|
|
53
|
+
slife (plugin host) ─── /mcp ────────────> │ local-embed │
|
|
54
|
+
│ models load on demand │
|
|
55
|
+
└───────────────────────┘
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Features
|
|
59
|
+
|
|
60
|
+
- **OpenAI-compatible** `/v1/embeddings` + Models API + `/health`.
|
|
61
|
+
- Two backends (GGUF / transformer), **many models as peers — each request
|
|
62
|
+
names the one it wants** (standard OpenAI semantics, no "active model").
|
|
63
|
+
- **Lazy load** — startup is fast; each model materialises on the first
|
|
64
|
+
request that names it (per-model `autoload: true` preloads a specific
|
|
65
|
+
model at startup).
|
|
66
|
+
- **Real dimension** reported after load (a guessed width is never served).
|
|
67
|
+
- Runs standalone, and can also be **loaded as a slife plugin** (see
|
|
68
|
+
[Loading as a slife plugin](#loading-as-a-slife-plugin)).
|
|
69
|
+
|
|
70
|
+
Requires Python ≥ 3.13.
|
|
71
|
+
|
|
72
|
+
## Install
|
|
73
|
+
|
|
74
|
+
The core package is `fastmcp` + `starlette` + `json-five` (the JSON5 parser,
|
|
75
|
+
imported as `json5`); the model backends are
|
|
76
|
+
optional extras.
|
|
77
|
+
|
|
78
|
+
One-click installers (install `uv` if needed, then `uv tool install --force
|
|
79
|
+
local-embed` — the backend is **not** included, see below):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# macOS / Linux / WSL
|
|
83
|
+
curl -fsSL https://raw.githubusercontent.com/juzcn/slife/main/local-embed/install.sh | bash
|
|
84
|
+
|
|
85
|
+
# Windows PowerShell
|
|
86
|
+
powershell -ExecutionPolicy Bypass -Command "irm https://raw.githubusercontent.com/juzcn/slife/main/local-embed/install.ps1 | iex"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Manual installs are equivalent:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# the app + CLI (standalone tool)
|
|
93
|
+
uv tool install --force local-embed
|
|
94
|
+
|
|
95
|
+
# with a backend:
|
|
96
|
+
uv tool install --force "local-embed[transformer]"
|
|
97
|
+
uv tool install --force "local-embed[gguf]"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`--force` (used by the installers too) is **idempotent** — the first run
|
|
101
|
+
installs, re-running **updates** to the latest PyPI release. A plain
|
|
102
|
+
`uv tool install local-embed` no-ops when the tool is already installed.
|
|
103
|
+
|
|
104
|
+
If you run local-embed inside an *existing* environment (alongside another
|
|
105
|
+
tool), install the backend into **that** environment's interpreter instead of
|
|
106
|
+
creating a separate one:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
uv pip install --python <venv-python> llama-cpp-python==0.3.34
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
where `<venv-python>` is the interpreter that runs `local-embed`.
|
|
113
|
+
|
|
114
|
+
### `gguf` backend — platform matrix
|
|
115
|
+
|
|
116
|
+
`gguf` (llama-cpp-python) is platform-sensitive: PyPI ships **only the sdist**
|
|
117
|
+
(no prebuilt wheels), so on Linux / WSL / macOS a plain install **compiles from
|
|
118
|
+
source** (needs a C compiler + CMake ≥ 3.21). **Windows has no default C
|
|
119
|
+
toolchain** (no MSVC), so it uses the upstream prebuilt CPU wheel — the one
|
|
120
|
+
workaround. GPU variants pass `CMAKE_ARGS`:
|
|
121
|
+
|
|
122
|
+
| Platform | Command |
|
|
123
|
+
|---|---|
|
|
124
|
+
| Linux / WSL / macOS, CPU | `uv pip install --python <venv-python> llama-cpp-python==0.3.34` (compiles from source) |
|
|
125
|
+
| macOS arm64 (Metal) | `CMAKE_ARGS="-DGGML_METAL=on" uv pip install --python <venv-python> llama-cpp-python==0.3.34` |
|
|
126
|
+
| NVIDIA CUDA (Linux) | `CMAKE_ARGS="-DGGML_CUDA=on" uv pip install --python <venv-python> llama-cpp-python==0.3.34` (needs the CUDA toolkit) |
|
|
127
|
+
| **Windows, CPU** | `uv pip install --python <venv-python> --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu llama-cpp-python==0.3.34` (prebuilt wheel — no MSVC) |
|
|
128
|
+
|
|
129
|
+
The Windows CPU row is the only workaround — everywhere else uses the standard
|
|
130
|
+
source build from PyPI. If the `gguf` backend is missing, the server errors at
|
|
131
|
+
startup with the exact command for your platform.
|
|
132
|
+
|
|
133
|
+
**Both backends at once** — one `uv pip install`:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
uv pip install --python <venv-python> sentence-transformers llama-cpp-python==0.3.34
|
|
137
|
+
# Windows CPU: add the upstream wheel index (see the matrix above)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Installing does **not** fetch a model — get the weights first.
|
|
141
|
+
|
|
142
|
+
## Quick start
|
|
143
|
+
|
|
144
|
+
1. **Install** the app and a backend (above).
|
|
145
|
+
2. **Download weights** — see [Model weights](#model-weights).
|
|
146
|
+
3. **Configure a model** — the CLI helper writes `local_embed.json5`:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
local-embed set BAAI/bge-m3 --HF_HUB_CACHE <dir> # transformer
|
|
150
|
+
local-embed set-gguf bge-m3 --path D:\models\bge.gguf # gguf
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
4. **Start the service**:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
local-embed # serve on http://127.0.0.1:17347
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
5. **Embed text** — point any OpenAI-compatible client at
|
|
160
|
+
`http://127.0.0.1:17347/v1` (see [HTTP API](#http-api-openai-compatible)).
|
|
161
|
+
|
|
162
|
+
## Configuration: `local_embed.json5`
|
|
163
|
+
|
|
164
|
+
Everything — host, port, models, backend — comes from
|
|
165
|
+
`local_embed.json5`, written by the CLI helpers or by hand. Path resolution:
|
|
166
|
+
`$LOCAL_EMBED_FILE` > slife project root (dev) > `~/.local-embed/local_embed.json5`.
|
|
167
|
+
|
|
168
|
+
```json5
|
|
169
|
+
{
|
|
170
|
+
models: {
|
|
171
|
+
"bge-m3": { backend: "gguf", gguf_path: "…", device: "", autoload: false },
|
|
172
|
+
"bge-m3-transformer": { backend: "transformer", model: "BAAI/bge-m3", device: "" }
|
|
173
|
+
},
|
|
174
|
+
env: { // injected into this process before any model loads
|
|
175
|
+
HF_HUB_CACHE: "C:\\…\\HuggingFace\\hub", // where transformer repos resolve
|
|
176
|
+
HF_HUB_OFFLINE: "1" // force offline
|
|
177
|
+
},
|
|
178
|
+
host: "127.0.0.1", // standalone only
|
|
179
|
+
port: 17347 // standalone only
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
- `models` — map of name → `{backend, gguf_path | model, device, max_tokens, autoload}`.
|
|
184
|
+
Every configured model is a peer; there is **no `active_model`** — each
|
|
185
|
+
request names the model it wants (standard OpenAI semantics).
|
|
186
|
+
- `autoload` (per model, default `false`) — a model's weights are large and
|
|
187
|
+
memory-hungry, so nothing loads until a request names it (**lazy**).
|
|
188
|
+
`autoload: true` on one model eager-loads just that model in the
|
|
189
|
+
background at startup (memory paid up front for a warm first embed); every
|
|
190
|
+
unflagged model stays lazy.
|
|
191
|
+
- `env:` — injected before any backend loads; an existing shell env var wins.
|
|
192
|
+
Without `HF_HUB_CACHE`, transformer repos resolve against the default cache
|
|
193
|
+
and a model downloaded elsewhere is silently re-fetched.
|
|
194
|
+
- Single-model convenience — top-level `backend`/`model`/`gguf_path`/`device`
|
|
195
|
+
— still works.
|
|
196
|
+
|
|
197
|
+
Reads are read-only at runtime — the server has no config-mutating tools.
|
|
198
|
+
|
|
199
|
+
## Model weights
|
|
200
|
+
|
|
201
|
+
The server never downloads models; both backends fail at load time when their
|
|
202
|
+
weights are missing (`gguf` needs the `gguf_path` file, `transformer` needs the
|
|
203
|
+
repo in the HF cache).
|
|
204
|
+
|
|
205
|
+
### GGUF
|
|
206
|
+
|
|
207
|
+
`.gguf` files are, in general, **community conversions** — upstream releases
|
|
208
|
+
are PyTorch weights, not GGUF — so there is **no single authoritative source**:
|
|
209
|
+
conversions are scattered across Hugging Face, ModelScope, Ollama, llama.cpp
|
|
210
|
+
community uploads and various project sites. Pick any source you trust; the
|
|
211
|
+
transport doesn't matter, local-embed only needs the file on disk.
|
|
212
|
+
|
|
213
|
+
HF is the most common host and its CLI pulls a single file:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
uv tool install "huggingface-hub[cli]" # provides `hf`
|
|
217
|
+
hf download <owner>/<repo> <model>.gguf --local-dir D:\models\bge-m3
|
|
218
|
+
# one-off, no permanent tool install:
|
|
219
|
+
uvx --from huggingface-hub hf download <owner>/<repo> <model>.gguf --local-dir D:\models\bge-m3
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Any source also works from a browser or `wget`/`curl` — on HF, every file is
|
|
223
|
+
fetchable from `https://huggingface.co/<owner>/<repo>/resolve/main/<model>.gguf`.
|
|
224
|
+
|
|
225
|
+
Prefer a high-fidelity quant such as **Q8_0** (~99 % of the original accuracy at
|
|
226
|
+
roughly a third of the size). Point `gguf_path` at the file:
|
|
227
|
+
|
|
228
|
+
```json5
|
|
229
|
+
models: {
|
|
230
|
+
"bge-m3": { backend: "gguf", gguf_path: "D:\\models\\bge-m3\\bge-m3-q8_0.gguf" }
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Transformer
|
|
235
|
+
|
|
236
|
+
`sentence-transformers` resolves an HF repo id against the local hub cache:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
hf download BAAI/bge-m3 # -> ~/.cache/huggingface/hub/models--BAAI--bge-m3
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
```json5
|
|
243
|
+
models: {
|
|
244
|
+
"bge-m3": { backend: "transformer", model: "BAAI/bge-m3" }
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## HTTP API (OpenAI-compatible)
|
|
249
|
+
|
|
250
|
+
The endpoint surface is a superset of the OpenAI Embeddings + Models APIs, all
|
|
251
|
+
on one port.
|
|
252
|
+
|
|
253
|
+
### `POST /v1/embeddings`
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
curl http://127.0.0.1:17347/v1/embeddings \
|
|
257
|
+
-H 'Content-Type: application/json' \
|
|
258
|
+
-d '{"model": "bge-m3", "input": ["hello world", "another text"]}'
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`input` is a string or a list of strings; `model` is **required** and names
|
|
262
|
+
any configured model. Returns the standard shape:
|
|
263
|
+
|
|
264
|
+
```json
|
|
265
|
+
{
|
|
266
|
+
"object": "list",
|
|
267
|
+
"data": [
|
|
268
|
+
{"object": "embedding", "index": 0, "embedding": [0.012, ...]},
|
|
269
|
+
{"object": "embedding", "index": 1, "embedding": [...]}
|
|
270
|
+
],
|
|
271
|
+
"model": "bge-m3",
|
|
272
|
+
"usage": {"prompt_tokens": 3, "total_tokens": 3}
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
OpenAI forbids empty-string input, so an empty/whitespace string in the
|
|
277
|
+
batch is rejected with a strict `400` — there is no zero-vector row
|
|
278
|
+
alignment on the wire; filter blanks before batching.
|
|
279
|
+
|
|
280
|
+
#### Errors — the OpenAI contract
|
|
281
|
+
|
|
282
|
+
Every error uses the standard envelope
|
|
283
|
+
`{"error": {"message", "type", "param", "code"}}` (`param` / `code` are
|
|
284
|
+
`null` when not applicable):
|
|
285
|
+
|
|
286
|
+
| Status | `type` | When | `param` / `code` |
|
|
287
|
+
|---|---|---|---|
|
|
288
|
+
| `400` | `invalid_request_error` | unparseable JSON body; `input` not a string/array of strings; missing `model`; empty / whitespace-only input | `input` / `model` |
|
|
289
|
+
| `400` | `invalid_request_error` | input exceeds the model's context length (`max_tokens` — never silently truncated) | `input` / `context_length_exceeded` |
|
|
290
|
+
| `404` | `invalid_request_error` | unknown `model` ("does not exist or you do not have access to it") | — / `model_not_found` |
|
|
291
|
+
| `503` | `server_error` | the model's engine is **still loading** (is_loading — retry shortly) or unavailable (backend dependency missing, load failed) | — |
|
|
292
|
+
| `500` | `server_error` | unexpected internal failure | — |
|
|
293
|
+
|
|
294
|
+
Split long documents into pieces of at most `max_tokens` tokens client-side
|
|
295
|
+
(slife's semantic drainer does this automatically).
|
|
296
|
+
|
|
297
|
+
### `GET /v1/models`
|
|
298
|
+
|
|
299
|
+
Every configured model, each with its real embedding dimension
|
|
300
|
+
(`dimension` / `dimension_known`), backend, and load state. The listing is
|
|
301
|
+
standard — no `active` marker, all models are peers.
|
|
302
|
+
|
|
303
|
+
### `GET /v1/models/{id}`
|
|
304
|
+
|
|
305
|
+
One model's detail (the OpenAI `retrieve` endpoint); 404 with the standard
|
|
306
|
+
error envelope when the id is unknown.
|
|
307
|
+
|
|
308
|
+
### `GET /health`
|
|
309
|
+
|
|
310
|
+
Liveness + engine state: `{status, models: [{name, backend, model,
|
|
311
|
+
dimension, dimension_known, loaded, available, max_tokens}]}` — `status`
|
|
312
|
+
is `ok` when any configured model's backend is usable.
|
|
313
|
+
|
|
314
|
+
### Any OpenAI client
|
|
315
|
+
|
|
316
|
+
Point `base_url` at `http://127.0.0.1:17347/v1` — the `openai` package, or any
|
|
317
|
+
OpenAI-compatible SDK:
|
|
318
|
+
|
|
319
|
+
```python
|
|
320
|
+
from openai import OpenAI
|
|
321
|
+
|
|
322
|
+
client = OpenAI(base_url="http://127.0.0.1:17347/v1", api_key="local")
|
|
323
|
+
vecs = client.embeddings.create(model="bge-m3", input=["hello"])
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## CLI (auxiliary tools)
|
|
327
|
+
|
|
328
|
+
The `local-embed` CLI is a **helper**, not the product: it starts the service
|
|
329
|
+
and writes the config file. The service is configured by `local_embed.json5`
|
|
330
|
+
and consumed over the HTTP API — the CLI only makes both steps convenient.
|
|
331
|
+
|
|
332
|
+
### `local-embed` — run the service
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
local-embed # binds 127.0.0.1:17347 by default
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
The CLI takes no model/endpoint flags — the config is the only source of truth.
|
|
339
|
+
A port already in use is a hard error with one actionable line (no silent
|
|
340
|
+
fallback to a free port): stop the other instance or change `port` in the
|
|
341
|
+
config.
|
|
342
|
+
|
|
343
|
+
### `local-embed set` / `set-gguf` — write the config
|
|
344
|
+
|
|
345
|
+
`set` (transformer) and `set-gguf` (gguf) upsert a model in the config and
|
|
346
|
+
pin port (and, for `set`, the HF cache + offline flag). Both are
|
|
347
|
+
**idempotent** — re-running yields the same config — and leave other models
|
|
348
|
+
untouched. There is no `active_model`: every configured model is a peer
|
|
349
|
+
and requests name the one they want.
|
|
350
|
+
|
|
351
|
+
| | `set` (transformer) | `set-gguf` |
|
|
352
|
+
|---|---|---|
|
|
353
|
+
| `<model_name>` | required — HF repo id (key + repo loaded) | required — config key |
|
|
354
|
+
| weight ref | `--HF_HUB_CACHE <dir>` — cache must contain the repo | `--path <PATH>` required — existing `.gguf` file |
|
|
355
|
+
| cache fallback | env `HF_HUB_CACHE`, else error | — |
|
|
356
|
+
| env written | `HF_HUB_CACHE` + `HF_HUB_OFFLINE: "1"` (offline server) | — |
|
|
357
|
+
| `--port <n>` | default `17347` | default `17347` |
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
local-embed set BAAI/bge-m3 [--HF_HUB_CACHE <dir>] [--port 17347]
|
|
361
|
+
local-embed set-gguf bge-m3 --path D:\models\bge-m3\bge-m3-q8_0.gguf [--port 17347]
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Any error — cache unset, repo not in cache, file missing — exits non-zero and
|
|
365
|
+
writes nothing. Changes apply on the next server start.
|
|
366
|
+
|
|
367
|
+
### Model download — offline by default
|
|
368
|
+
|
|
369
|
+
`HF_HUB_OFFLINE=1` by default, so the server **never downloads** a model (it's
|
|
370
|
+
too slow). Make the model available yourself — see
|
|
371
|
+
[Model weights](#model-weights): `hf download BAAI/bge-m3` for the transformer
|
|
372
|
+
route (into the HF cache), or drop a GGUF file and point `gguf_path` at it.
|
|
373
|
+
|
|
374
|
+
## Dimension
|
|
375
|
+
|
|
376
|
+
The real output width is only known once the model is loaded (`n_embd` /
|
|
377
|
+
`get_sentence_embedding_dimension`). `GET /v1/models` (or `/v1/models/{id}`)
|
|
378
|
+
reports the real dimension, so a client can size its vector table correctly —
|
|
379
|
+
a wrong width silently drops every mis-sized embedding. A guessed dimension is
|
|
380
|
+
never served as authoritative.
|
|
381
|
+
|
|
382
|
+
## As an MCP server
|
|
383
|
+
|
|
384
|
+
`/mcp` is a standard **MCP Streamable HTTP** endpoint, so any MCP client that
|
|
385
|
+
supports Streamable HTTP can register local-embed as an MCP server. Start it
|
|
386
|
+
first:
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
local-embed # serve on http://127.0.0.1:17347
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
then register the URL. The MCP client config shape is `{"type": "http",
|
|
393
|
+
"url": …}` — for example, Claude Code's `.mcp.json` (or `~/.claude.json`):
|
|
394
|
+
|
|
395
|
+
```json
|
|
396
|
+
{
|
|
397
|
+
"mcpServers": {
|
|
398
|
+
"local-embed": {
|
|
399
|
+
"type": "http",
|
|
400
|
+
"url": "http://127.0.0.1:17347/mcp"
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Or with the Claude Code CLI:
|
|
407
|
+
|
|
408
|
+
```bash
|
|
409
|
+
claude mcp add --transport http local-embed http://127.0.0.1:17347/mcp
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
> **Claude Desktop** registers remote HTTP MCP servers through its **connectors
|
|
413
|
+
> UI** (Settings → Connectors → Add connector → Remote), not
|
|
414
|
+
> `claude_desktop_config.json` — and a remote connector expects a hosted HTTPS
|
|
415
|
+
> URL, so a `127.0.0.1` server needs a tunnel or a stdio→HTTP bridge to be
|
|
416
|
+
> reachable from the desktop app.
|
|
417
|
+
|
|
418
|
+
**Over MCP you get status, not embeddings.** The only MCP tool is the internal
|
|
419
|
+
`__check` status probe (model list, dimensions, load state) — local-embed is
|
|
420
|
+
a *service-provider* MCP server, not a tool provider. To embed text, point
|
|
421
|
+
your consumer at the OpenAI-compatible `/v1/*` API instead. A client that only
|
|
422
|
+
speaks stdio can reach it through a stdio→HTTP bridge (e.g. `mcp-remote`).
|
|
423
|
+
|
|
424
|
+
## Running as a slife embedding backend
|
|
425
|
+
|
|
426
|
+
local-embed is a **plugin** — one row in slife's central plugin spec, so
|
|
427
|
+
slife spawns it, watches it, and reports it like every other child plugin. It
|
|
428
|
+
is also runnable standalone (`local-embed` on PATH), which is what the CLI is
|
|
429
|
+
for; slife still consumes the MODEL SERVICE purely as an OpenAI-compatible
|
|
430
|
+
HTTP endpoint (never through MCP tools).
|
|
431
|
+
|
|
432
|
+
Because its config pins a port that a static embeddings `base_url` points at,
|
|
433
|
+
it is the one plugin with a **fixed** port: it binds that port itself and a
|
|
434
|
+
taken port is a hard error (no fallback — a second instance would double the
|
|
435
|
+
model in memory and serve nothing new). The host then reports the plugin as
|
|
436
|
+
failed to load, with the reason, and the running instance keeps serving.
|
|
437
|
+
|
|
438
|
+
Point slife's embedding config at the daemon's stable port:
|
|
439
|
+
|
|
440
|
+
```json5
|
|
441
|
+
embeddings: {
|
|
442
|
+
providers: {
|
|
443
|
+
local: {
|
|
444
|
+
base_url: "http://127.0.0.1:17347/v1", // stable port from local_embed.json5
|
|
445
|
+
api_key: "local",
|
|
446
|
+
model: "bge-m3", // id POSTed on /v1/embeddings
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
active_model: "local", // provider-id only, or "local/<model>"
|
|
450
|
+
enabled: true
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
slife treats every embedding model as a remote OpenAI-compatible endpoint —
|
|
455
|
+
local-embed is one such endpoint. The model a request hits is the one the
|
|
456
|
+
caller names; when the config names no model, slife discovers it from
|
|
457
|
+
`GET /v1/models` (the first entry) on load. When the daemon is unreachable,
|
|
458
|
+
slife degrades gracefully to keyword search (`check_embeddings` in
|
|
459
|
+
`system_health` probes the **active** provider's HTTP endpoint and reports it
|
|
460
|
+
down — it never assumes that provider is local-embed).
|
|
461
|
+
|
|
462
|
+
The MCP surface it still serves is the internal **`__check`** (engine status:
|
|
463
|
+
model list, dimensions, load state) — a service-provider facade
|
|
464
|
+
for direct probing, not a slife plugin contract. It also serves plain OpenAPI
|
|
465
|
+
routes (`/v1/embeddings`, `/v1/models`, `/health`) on the same port via
|
|
466
|
+
`@mcp.custom_route` — one port, two protocols, no slife involvement.
|
|
467
|
+
|
|
468
|
+
## License
|
|
469
|
+
|
|
470
|
+
MIT — see the repository root `LICENSE`.
|