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.

Files changed (28) hide show
  1. {local_embed-0.2.2 → local_embed-0.2.4}/.gitignore +4 -4
  2. local_embed-0.2.4/PKG-INFO +470 -0
  3. local_embed-0.2.4/README.md +444 -0
  4. local_embed-0.2.4/install.ps1 +85 -0
  5. local_embed-0.2.4/install.sh +73 -0
  6. {local_embed-0.2.2 → local_embed-0.2.4}/local_embed/cli.py +28 -48
  7. {local_embed-0.2.2 → local_embed-0.2.4}/local_embed/cmd_set.py +42 -31
  8. {local_embed-0.2.2 → local_embed-0.2.4}/local_embed/config.py +88 -29
  9. {local_embed-0.2.2 → local_embed-0.2.4}/local_embed/engine.py +195 -140
  10. {local_embed-0.2.2 → local_embed-0.2.4}/local_embed/logging.py +49 -2
  11. local_embed-0.2.4/local_embed/server.py +498 -0
  12. {local_embed-0.2.2 → local_embed-0.2.4}/local_embed/server_utils.py +41 -57
  13. {local_embed-0.2.2 → local_embed-0.2.4}/pyproject.toml +3 -3
  14. {local_embed-0.2.2 → local_embed-0.2.4}/tests/test_client.py +13 -1
  15. local_embed-0.2.4/tests/test_config.py +231 -0
  16. local_embed-0.2.4/tests/test_engine.py +429 -0
  17. {local_embed-0.2.2 → local_embed-0.2.4}/tests/test_logging.py +10 -0
  18. local_embed-0.2.4/tests/test_server.py +326 -0
  19. {local_embed-0.2.2 → local_embed-0.2.4}/tests/test_set.py +39 -56
  20. local_embed-0.2.2/PKG-INFO +0 -316
  21. local_embed-0.2.2/README.md +0 -290
  22. local_embed-0.2.2/local_embed/server.py +0 -379
  23. local_embed-0.2.2/tests/test_config.py +0 -42
  24. local_embed-0.2.2/tests/test_engine.py +0 -268
  25. local_embed-0.2.2/tests/test_server.py +0 -152
  26. {local_embed-0.2.2 → local_embed-0.2.4}/local_embed/__init__.py +0 -0
  27. {local_embed-0.2.2 → local_embed-0.2.4}/local_embed/__main__.py +0 -0
  28. {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; template is slife.template.json5.
2
- slife.json5
3
- mcp-plugin.json5
4
- local_embed.json5
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`.