local-embed 0.1.0__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.
- local_embed-0.1.0/.gitignore +246 -0
- local_embed-0.1.0/PKG-INFO +195 -0
- local_embed-0.1.0/README.md +170 -0
- local_embed-0.1.0/local_embed/__init__.py +29 -0
- local_embed-0.1.0/local_embed/__main__.py +8 -0
- local_embed-0.1.0/local_embed/cli.py +88 -0
- local_embed-0.1.0/local_embed/config.py +200 -0
- local_embed-0.1.0/local_embed/engine.py +555 -0
- local_embed-0.1.0/local_embed/logging.py +122 -0
- local_embed-0.1.0/local_embed/server.py +370 -0
- local_embed-0.1.0/local_embed/server_utils.py +197 -0
- local_embed-0.1.0/local_embed/threads.py +71 -0
- local_embed-0.1.0/pyproject.toml +62 -0
- local_embed-0.1.0/tests/test_client.py +61 -0
- local_embed-0.1.0/tests/test_config.py +42 -0
- local_embed-0.1.0/tests/test_engine.py +268 -0
- local_embed-0.1.0/tests/test_logging.py +82 -0
- local_embed-0.1.0/tests/test_server.py +135 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# Runtime config — personal per-user settings; template is slife.template.json5.
|
|
2
|
+
slife.json5
|
|
3
|
+
mcp-plugin.json5
|
|
4
|
+
local_embed.json5
|
|
5
|
+
|
|
6
|
+
# Playwright MCP browser traces (test artifacts)
|
|
7
|
+
.playwright-mcp/
|
|
8
|
+
|
|
9
|
+
# Credential store encrypted backup
|
|
10
|
+
credentials.crypt
|
|
11
|
+
|
|
12
|
+
# WeChat session files (contain auth tokens)
|
|
13
|
+
wechat_*.json5
|
|
14
|
+
|
|
15
|
+
# Runtime logs
|
|
16
|
+
logs/
|
|
17
|
+
|
|
18
|
+
# User-saved files (memfiles plugin storage — per-agent runtime data)
|
|
19
|
+
*.files/
|
|
20
|
+
|
|
21
|
+
# Byte-compiled / optimized / DLL files
|
|
22
|
+
__pycache__/
|
|
23
|
+
*.py[codz]
|
|
24
|
+
*$py.class
|
|
25
|
+
|
|
26
|
+
# C extensions
|
|
27
|
+
*.so
|
|
28
|
+
|
|
29
|
+
# Distribution / packaging
|
|
30
|
+
.Python
|
|
31
|
+
build/
|
|
32
|
+
develop-eggs/
|
|
33
|
+
dist/
|
|
34
|
+
downloads/
|
|
35
|
+
eggs/
|
|
36
|
+
.eggs/
|
|
37
|
+
lib/
|
|
38
|
+
lib64/
|
|
39
|
+
parts/
|
|
40
|
+
sdist/
|
|
41
|
+
var/
|
|
42
|
+
wheels/
|
|
43
|
+
share/python-wheels/
|
|
44
|
+
*.egg-info/
|
|
45
|
+
.installed.cfg
|
|
46
|
+
*.egg
|
|
47
|
+
MANIFEST
|
|
48
|
+
|
|
49
|
+
# PyInstaller
|
|
50
|
+
# Usually these files are written by a python script from a template
|
|
51
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
52
|
+
*.manifest
|
|
53
|
+
*.spec
|
|
54
|
+
|
|
55
|
+
# Installer logs
|
|
56
|
+
pip-log.txt
|
|
57
|
+
pip-delete-this-directory.txt
|
|
58
|
+
|
|
59
|
+
# Unit test / coverage reports
|
|
60
|
+
htmlcov/
|
|
61
|
+
.tox/
|
|
62
|
+
.nox/
|
|
63
|
+
.coverage
|
|
64
|
+
.coverage.*
|
|
65
|
+
.cache
|
|
66
|
+
nosetests.xml
|
|
67
|
+
coverage.xml
|
|
68
|
+
*.cover
|
|
69
|
+
*.py.cover
|
|
70
|
+
.hypothesis/
|
|
71
|
+
.pytest_cache/
|
|
72
|
+
cover/
|
|
73
|
+
|
|
74
|
+
# Translations
|
|
75
|
+
*.mo
|
|
76
|
+
*.pot
|
|
77
|
+
|
|
78
|
+
# Django stuff:
|
|
79
|
+
*.log
|
|
80
|
+
local_settings.py
|
|
81
|
+
db.sqlite3
|
|
82
|
+
db.sqlite3-journal
|
|
83
|
+
|
|
84
|
+
# Flask stuff:
|
|
85
|
+
instance/
|
|
86
|
+
.webassets-cache
|
|
87
|
+
|
|
88
|
+
# Scrapy stuff:
|
|
89
|
+
.scrapy
|
|
90
|
+
|
|
91
|
+
# Sphinx documentation
|
|
92
|
+
docs/_build/
|
|
93
|
+
|
|
94
|
+
# PyBuilder
|
|
95
|
+
.pybuilder/
|
|
96
|
+
target/
|
|
97
|
+
|
|
98
|
+
# Jupyter Notebook
|
|
99
|
+
.ipynb_checkpoints
|
|
100
|
+
|
|
101
|
+
# IPython
|
|
102
|
+
profile_default/
|
|
103
|
+
ipython_config.py
|
|
104
|
+
|
|
105
|
+
# pyenv
|
|
106
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
107
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
108
|
+
# .python-version
|
|
109
|
+
|
|
110
|
+
# pipenv
|
|
111
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
112
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
113
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
114
|
+
# install all needed dependencies.
|
|
115
|
+
# Pipfile.lock
|
|
116
|
+
|
|
117
|
+
# UV
|
|
118
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
119
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
120
|
+
# commonly ignored for libraries.
|
|
121
|
+
# uv.lock
|
|
122
|
+
|
|
123
|
+
# poetry
|
|
124
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
125
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
126
|
+
# commonly ignored for libraries.
|
|
127
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
128
|
+
# poetry.lock
|
|
129
|
+
# poetry.toml
|
|
130
|
+
|
|
131
|
+
# pdm
|
|
132
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
133
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
134
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
135
|
+
# pdm.lock
|
|
136
|
+
# pdm.toml
|
|
137
|
+
.pdm-python
|
|
138
|
+
.pdm-build/
|
|
139
|
+
|
|
140
|
+
# pixi
|
|
141
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
142
|
+
# pixi.lock
|
|
143
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
144
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
145
|
+
.pixi
|
|
146
|
+
|
|
147
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
148
|
+
__pypackages__/
|
|
149
|
+
|
|
150
|
+
# Celery stuff
|
|
151
|
+
celerybeat-schedule
|
|
152
|
+
celerybeat.pid
|
|
153
|
+
|
|
154
|
+
# Redis
|
|
155
|
+
*.rdb
|
|
156
|
+
*.aof
|
|
157
|
+
*.pid
|
|
158
|
+
|
|
159
|
+
# RabbitMQ
|
|
160
|
+
mnesia/
|
|
161
|
+
rabbitmq/
|
|
162
|
+
rabbitmq-data/
|
|
163
|
+
|
|
164
|
+
# ActiveMQ
|
|
165
|
+
activemq-data/
|
|
166
|
+
|
|
167
|
+
# SageMath parsed files
|
|
168
|
+
*.sage.py
|
|
169
|
+
|
|
170
|
+
# Environments
|
|
171
|
+
.env
|
|
172
|
+
.envrc
|
|
173
|
+
.venv
|
|
174
|
+
env/
|
|
175
|
+
venv/
|
|
176
|
+
ENV/
|
|
177
|
+
env.bak/
|
|
178
|
+
venv.bak/
|
|
179
|
+
|
|
180
|
+
# Spyder project settings
|
|
181
|
+
.spyderproject
|
|
182
|
+
.spyproject
|
|
183
|
+
|
|
184
|
+
# Rope project settings
|
|
185
|
+
.ropeproject
|
|
186
|
+
|
|
187
|
+
# mkdocs documentation
|
|
188
|
+
/site
|
|
189
|
+
|
|
190
|
+
# mypy
|
|
191
|
+
.mypy_cache/
|
|
192
|
+
.dmypy.json
|
|
193
|
+
dmypy.json
|
|
194
|
+
|
|
195
|
+
# Pyre type checker
|
|
196
|
+
.pyre/
|
|
197
|
+
|
|
198
|
+
# pytype static type analyzer
|
|
199
|
+
.pytype/
|
|
200
|
+
|
|
201
|
+
# Cython debug symbols
|
|
202
|
+
cython_debug/
|
|
203
|
+
|
|
204
|
+
# PyCharm
|
|
205
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
206
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
207
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
208
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
209
|
+
# .idea/
|
|
210
|
+
|
|
211
|
+
# Abstra
|
|
212
|
+
# Abstra is an AI-powered process automation framework.
|
|
213
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
214
|
+
# Learn more at https://abstra.io/docs
|
|
215
|
+
.abstra/
|
|
216
|
+
|
|
217
|
+
# Visual Studio Code
|
|
218
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
219
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
220
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
221
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
222
|
+
# .vscode/
|
|
223
|
+
# Temporary file for partial code execution
|
|
224
|
+
tempCodeRunnerFile.py
|
|
225
|
+
|
|
226
|
+
# Ruff stuff:
|
|
227
|
+
.ruff_cache/
|
|
228
|
+
|
|
229
|
+
# PyPI configuration file
|
|
230
|
+
.pypirc
|
|
231
|
+
|
|
232
|
+
# Marimo
|
|
233
|
+
marimo/_static/
|
|
234
|
+
marimo/_lsp/
|
|
235
|
+
__marimo__/
|
|
236
|
+
|
|
237
|
+
# Streamlit
|
|
238
|
+
.streamlit/secrets.toml
|
|
239
|
+
|
|
240
|
+
# SQLite database files (may appear anywhere in the project)
|
|
241
|
+
*.db
|
|
242
|
+
*.db-shm
|
|
243
|
+
*.db-wal
|
|
244
|
+
|
|
245
|
+
# local DB backups (never committed)
|
|
246
|
+
slife.db.bak*
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: local-embed
|
|
3
|
+
Version: 0.1.0
|
|
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: starlette>=0.37.0
|
|
20
|
+
Provides-Extra: gguf
|
|
21
|
+
Requires-Dist: llama-cpp-python; extra == 'gguf'
|
|
22
|
+
Provides-Extra: transformer
|
|
23
|
+
Requires-Dist: sentence-transformers; extra == 'transformer'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# local-embed
|
|
27
|
+
|
|
28
|
+
**Standalone local embedding server** — loads one GGUF (llama-cpp) or HF
|
|
29
|
+
transformer embedding model **once** and exposes it as an
|
|
30
|
+
[OpenAI-compatible](https://platform.openai.com/docs/api-reference/embeddings)
|
|
31
|
+
`/v1/embeddings` HTTP endpoint **plus** FastMCP tools, on a single port.
|
|
32
|
+
|
|
33
|
+
Built for **slife** (its `memdb` and `memfiles` plugins both call this
|
|
34
|
+
service over HTTP, so the model is never loaded twice in one process
|
|
35
|
+
tree), but it is a fully standalone package — any OpenAI-compatible
|
|
36
|
+
client can use it.
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
POST http://127.0.0.1:8000/v1/embeddings
|
|
40
|
+
slife memdb ─────────────────────────┐
|
|
41
|
+
slife memfiles ──────────────────────┤
|
|
42
|
+
any OpenAI client ───────────────────┤
|
|
43
|
+
▼
|
|
44
|
+
┌────────────────────────────┐
|
|
45
|
+
│ local-embed │
|
|
46
|
+
│ /v1/embeddings (HTTP) │
|
|
47
|
+
│ /v1/models (HTTP) │
|
|
48
|
+
│ /health (HTTP) │
|
|
49
|
+
│ /mcp (FastMCP tools) │
|
|
50
|
+
│ ONE loaded model │
|
|
51
|
+
└────────────────────────────┘
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# GGUF backend (llama-cpp-python)
|
|
58
|
+
uv tool install 'local-embed[gguf]' # or: uv pip install 'local-embed[gguf]'
|
|
59
|
+
# Transformer backend (sentence-transformers)
|
|
60
|
+
uv tool install 'local-embed[transformer]' # or: uv pip install 'local-embed[transformer]'
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Python 3.13+. The server core only depends on `fastmcp` + `starlette`; the
|
|
64
|
+
heavy model backends are optional extras.
|
|
65
|
+
|
|
66
|
+
## Run
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# GGUF model (recommended — offline, no HF download)
|
|
70
|
+
local-embed --backend gguf --model bge-m3 --gguf-path /path/to/bge-m3-q4_k_m.gguf --port 8000
|
|
71
|
+
|
|
72
|
+
# HF transformer model (downloads from HF hub on first load)
|
|
73
|
+
local-embed --backend transformer --model BAAI/bge-m3 --port 8000
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
By default it binds `127.0.0.1:8000` (local only — never exposed to the
|
|
77
|
+
network).
|
|
78
|
+
|
|
79
|
+
### Transformer models & the `env:` section
|
|
80
|
+
|
|
81
|
+
A `transformer` model is referenced by its HF *repo name* (`BAAI/bge-m3`).
|
|
82
|
+
The HuggingFace hub resolves that name against its cache (default
|
|
83
|
+
`~/.cache/huggingface`), downloading on first load if missing. To keep the
|
|
84
|
+
server self-contained — point it at an existing local cache, or force
|
|
85
|
+
offline — put the env vars in `local_embed.json5`'s `env:` section (injected
|
|
86
|
+
into this process before any model loads; an existing shell env var wins):
|
|
87
|
+
|
|
88
|
+
```json5
|
|
89
|
+
{
|
|
90
|
+
env: {
|
|
91
|
+
HF_HUB_CACHE: "C:\\Users\\me\\HuggingFace\\hub", // existing cache
|
|
92
|
+
HF_HUB_OFFLINE: "1" // never hit the network
|
|
93
|
+
},
|
|
94
|
+
models: {
|
|
95
|
+
"bge-m3-transformer": { backend: "transformer", model: "BAAI/bge-m3", device: "" }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Without `HF_HUB_CACHE` the model resolves against the default cache — a
|
|
101
|
+
model already downloaded elsewhere would be silently re-fetched.
|
|
102
|
+
|
|
103
|
+
## Use
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
curl http://127.0.0.1:8000/v1/embeddings \
|
|
107
|
+
-H 'Content-Type: application/json' \
|
|
108
|
+
-d '{"model": "bge-m3", "input": ["hello world", "another text"]}'
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Returns the standard OpenAI shape:
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"object": "list",
|
|
116
|
+
"data": [
|
|
117
|
+
{"object": "embedding", "index": 0, "embedding": [0.012, ...]},
|
|
118
|
+
{"object": "embedding", "index": 1, "embedding": [...]}
|
|
119
|
+
],
|
|
120
|
+
"model": "bge-m3",
|
|
121
|
+
"usage": {"prompt_tokens": 3, "total_tokens": 3}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Any OpenAI-compatible client works — point `base_url` at
|
|
126
|
+
`http://127.0.0.1:8000/v1` (e.g. slife's `memdb.embedding` api backend, or
|
|
127
|
+
the `openai` Python package):
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from openai import OpenAI
|
|
131
|
+
|
|
132
|
+
client = OpenAI(base_url="http://127.0.0.1:8000/v1", api_key="local")
|
|
133
|
+
vecs = client.embeddings.create(model="bge-m3", input=["hello"])
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Other endpoints:
|
|
137
|
+
|
|
138
|
+
- `GET /v1/models` — the loaded model + its real embedding dimension.
|
|
139
|
+
- `GET /health` — `{status, backend, model, dimension, loaded}`.
|
|
140
|
+
- `POST /mcp` — FastMCP streamable-HTTP endpoint (tools `embed_status` and
|
|
141
|
+
`embed`).
|
|
142
|
+
|
|
143
|
+
## Dimension
|
|
144
|
+
|
|
145
|
+
The real output width is only known once the model is loaded (`n_embd` /
|
|
146
|
+
`get_sentence_embedding_dimension`). `GET /v1/models` reports the real
|
|
147
|
+
dimension, so a client can size its vector table correctly — a wrong width
|
|
148
|
+
silently drops every mis-sized embedding.
|
|
149
|
+
|
|
150
|
+
## As a slife external plugin
|
|
151
|
+
|
|
152
|
+
slife treats every embedding model as a **remote OpenAI-compatible
|
|
153
|
+
endpoint** — local-embed is just one such endpoint. The model is
|
|
154
|
+
**determined by the plugin's active model**: slife discovers it from
|
|
155
|
+
`GET /v1/models` (the entry flagged `active: true`) on load.
|
|
156
|
+
|
|
157
|
+
Register local-embed as an external plugin (like `mcp-plugin`) so slife
|
|
158
|
+
manages the process, and point slife's embedding config at it with the
|
|
159
|
+
unified OpenAI format:
|
|
160
|
+
|
|
161
|
+
```json5
|
|
162
|
+
plugins: {
|
|
163
|
+
external: [
|
|
164
|
+
{ name: "local-embed", module: "local_embed.server" }
|
|
165
|
+
]
|
|
166
|
+
},
|
|
167
|
+
memdb: {
|
|
168
|
+
embedding: {
|
|
169
|
+
base_url: "http://127.0.0.1:8000/v1", // stable port from local_embed.json5
|
|
170
|
+
api_key: "local",
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The plugin binds the **stable port** from `local_embed.json5` (default
|
|
176
|
+
`8000`), so slife's `base_url` is fixed whether local-embed runs as the
|
|
177
|
+
plugin or standalone. When the service is unreachable, slife degrades
|
|
178
|
+
gracefully to keyword search.
|
|
179
|
+
|
|
180
|
+
## CLI
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
local-embed --help
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
- `--host` / `--port` — bind address (default `127.0.0.1:8000`)
|
|
187
|
+
- `--backend gguf|transformer` — model backend (default `gguf`)
|
|
188
|
+
- `--model` — model name/id (for metadata and dim guessing)
|
|
189
|
+
- `--gguf-path` — path to the GGUF file (required for `backend=gguf`)
|
|
190
|
+
- `--device cpu|cuda` — transformer device (default auto)
|
|
191
|
+
- `--log-level` — logging level (default `INFO`)
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
MIT — see the repository root `LICENSE`.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# local-embed
|
|
2
|
+
|
|
3
|
+
**Standalone local embedding server** — loads one GGUF (llama-cpp) or HF
|
|
4
|
+
transformer embedding model **once** and exposes it as an
|
|
5
|
+
[OpenAI-compatible](https://platform.openai.com/docs/api-reference/embeddings)
|
|
6
|
+
`/v1/embeddings` HTTP endpoint **plus** FastMCP tools, on a single port.
|
|
7
|
+
|
|
8
|
+
Built for **slife** (its `memdb` and `memfiles` plugins both call this
|
|
9
|
+
service over HTTP, so the model is never loaded twice in one process
|
|
10
|
+
tree), but it is a fully standalone package — any OpenAI-compatible
|
|
11
|
+
client can use it.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
POST http://127.0.0.1:8000/v1/embeddings
|
|
15
|
+
slife memdb ─────────────────────────┐
|
|
16
|
+
slife memfiles ──────────────────────┤
|
|
17
|
+
any OpenAI client ───────────────────┤
|
|
18
|
+
▼
|
|
19
|
+
┌────────────────────────────┐
|
|
20
|
+
│ local-embed │
|
|
21
|
+
│ /v1/embeddings (HTTP) │
|
|
22
|
+
│ /v1/models (HTTP) │
|
|
23
|
+
│ /health (HTTP) │
|
|
24
|
+
│ /mcp (FastMCP tools) │
|
|
25
|
+
│ ONE loaded model │
|
|
26
|
+
└────────────────────────────┘
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# GGUF backend (llama-cpp-python)
|
|
33
|
+
uv tool install 'local-embed[gguf]' # or: uv pip install 'local-embed[gguf]'
|
|
34
|
+
# Transformer backend (sentence-transformers)
|
|
35
|
+
uv tool install 'local-embed[transformer]' # or: uv pip install 'local-embed[transformer]'
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Python 3.13+. The server core only depends on `fastmcp` + `starlette`; the
|
|
39
|
+
heavy model backends are optional extras.
|
|
40
|
+
|
|
41
|
+
## Run
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# GGUF model (recommended — offline, no HF download)
|
|
45
|
+
local-embed --backend gguf --model bge-m3 --gguf-path /path/to/bge-m3-q4_k_m.gguf --port 8000
|
|
46
|
+
|
|
47
|
+
# HF transformer model (downloads from HF hub on first load)
|
|
48
|
+
local-embed --backend transformer --model BAAI/bge-m3 --port 8000
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
By default it binds `127.0.0.1:8000` (local only — never exposed to the
|
|
52
|
+
network).
|
|
53
|
+
|
|
54
|
+
### Transformer models & the `env:` section
|
|
55
|
+
|
|
56
|
+
A `transformer` model is referenced by its HF *repo name* (`BAAI/bge-m3`).
|
|
57
|
+
The HuggingFace hub resolves that name against its cache (default
|
|
58
|
+
`~/.cache/huggingface`), downloading on first load if missing. To keep the
|
|
59
|
+
server self-contained — point it at an existing local cache, or force
|
|
60
|
+
offline — put the env vars in `local_embed.json5`'s `env:` section (injected
|
|
61
|
+
into this process before any model loads; an existing shell env var wins):
|
|
62
|
+
|
|
63
|
+
```json5
|
|
64
|
+
{
|
|
65
|
+
env: {
|
|
66
|
+
HF_HUB_CACHE: "C:\\Users\\me\\HuggingFace\\hub", // existing cache
|
|
67
|
+
HF_HUB_OFFLINE: "1" // never hit the network
|
|
68
|
+
},
|
|
69
|
+
models: {
|
|
70
|
+
"bge-m3-transformer": { backend: "transformer", model: "BAAI/bge-m3", device: "" }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Without `HF_HUB_CACHE` the model resolves against the default cache — a
|
|
76
|
+
model already downloaded elsewhere would be silently re-fetched.
|
|
77
|
+
|
|
78
|
+
## Use
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
curl http://127.0.0.1:8000/v1/embeddings \
|
|
82
|
+
-H 'Content-Type: application/json' \
|
|
83
|
+
-d '{"model": "bge-m3", "input": ["hello world", "another text"]}'
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Returns the standard OpenAI shape:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"object": "list",
|
|
91
|
+
"data": [
|
|
92
|
+
{"object": "embedding", "index": 0, "embedding": [0.012, ...]},
|
|
93
|
+
{"object": "embedding", "index": 1, "embedding": [...]}
|
|
94
|
+
],
|
|
95
|
+
"model": "bge-m3",
|
|
96
|
+
"usage": {"prompt_tokens": 3, "total_tokens": 3}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Any OpenAI-compatible client works — point `base_url` at
|
|
101
|
+
`http://127.0.0.1:8000/v1` (e.g. slife's `memdb.embedding` api backend, or
|
|
102
|
+
the `openai` Python package):
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from openai import OpenAI
|
|
106
|
+
|
|
107
|
+
client = OpenAI(base_url="http://127.0.0.1:8000/v1", api_key="local")
|
|
108
|
+
vecs = client.embeddings.create(model="bge-m3", input=["hello"])
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Other endpoints:
|
|
112
|
+
|
|
113
|
+
- `GET /v1/models` — the loaded model + its real embedding dimension.
|
|
114
|
+
- `GET /health` — `{status, backend, model, dimension, loaded}`.
|
|
115
|
+
- `POST /mcp` — FastMCP streamable-HTTP endpoint (tools `embed_status` and
|
|
116
|
+
`embed`).
|
|
117
|
+
|
|
118
|
+
## Dimension
|
|
119
|
+
|
|
120
|
+
The real output width is only known once the model is loaded (`n_embd` /
|
|
121
|
+
`get_sentence_embedding_dimension`). `GET /v1/models` reports the real
|
|
122
|
+
dimension, so a client can size its vector table correctly — a wrong width
|
|
123
|
+
silently drops every mis-sized embedding.
|
|
124
|
+
|
|
125
|
+
## As a slife external plugin
|
|
126
|
+
|
|
127
|
+
slife treats every embedding model as a **remote OpenAI-compatible
|
|
128
|
+
endpoint** — local-embed is just one such endpoint. The model is
|
|
129
|
+
**determined by the plugin's active model**: slife discovers it from
|
|
130
|
+
`GET /v1/models` (the entry flagged `active: true`) on load.
|
|
131
|
+
|
|
132
|
+
Register local-embed as an external plugin (like `mcp-plugin`) so slife
|
|
133
|
+
manages the process, and point slife's embedding config at it with the
|
|
134
|
+
unified OpenAI format:
|
|
135
|
+
|
|
136
|
+
```json5
|
|
137
|
+
plugins: {
|
|
138
|
+
external: [
|
|
139
|
+
{ name: "local-embed", module: "local_embed.server" }
|
|
140
|
+
]
|
|
141
|
+
},
|
|
142
|
+
memdb: {
|
|
143
|
+
embedding: {
|
|
144
|
+
base_url: "http://127.0.0.1:8000/v1", // stable port from local_embed.json5
|
|
145
|
+
api_key: "local",
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The plugin binds the **stable port** from `local_embed.json5` (default
|
|
151
|
+
`8000`), so slife's `base_url` is fixed whether local-embed runs as the
|
|
152
|
+
plugin or standalone. When the service is unreachable, slife degrades
|
|
153
|
+
gracefully to keyword search.
|
|
154
|
+
|
|
155
|
+
## CLI
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
local-embed --help
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
- `--host` / `--port` — bind address (default `127.0.0.1:8000`)
|
|
162
|
+
- `--backend gguf|transformer` — model backend (default `gguf`)
|
|
163
|
+
- `--model` — model name/id (for metadata and dim guessing)
|
|
164
|
+
- `--gguf-path` — path to the GGUF file (required for `backend=gguf`)
|
|
165
|
+
- `--device cpu|cuda` — transformer device (default auto)
|
|
166
|
+
- `--log-level` — logging level (default `INFO`)
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT — see the repository root `LICENSE`.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""local-embed — standalone local embedding server.
|
|
2
|
+
|
|
3
|
+
Loads a local GGUF (llama-cpp) or HF transformer embedding model ONCE and
|
|
4
|
+
exposes it as an OpenAI-compatible ``/v1/embeddings`` HTTP endpoint plus
|
|
5
|
+
FastMCP tools. slife's memdb/memfiles plugins both call it over HTTP, so
|
|
6
|
+
the model is never loaded twice in one process tree.
|
|
7
|
+
|
|
8
|
+
Modules::
|
|
9
|
+
|
|
10
|
+
server.py FastMCP plugin — MCP tools + /v1/embeddings custom route
|
|
11
|
+
engine.py Model engine (gguf / transformer), lazy load + encode
|
|
12
|
+
cli.py Console entry point (``local-embed``)
|
|
13
|
+
threads.py run_daemon — daemon-thread offload for blocking model calls
|
|
14
|
+
logging.py Structured logging setup
|
|
15
|
+
|
|
16
|
+
Usage::
|
|
17
|
+
|
|
18
|
+
local-embed --backend gguf --gguf-path /path/to/model.gguf
|
|
19
|
+
local-embed --backend transformer --model BAAI/bge-m3
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from importlib.metadata import version as _version
|
|
24
|
+
|
|
25
|
+
__version__ = _version("local-embed")
|
|
26
|
+
except Exception:
|
|
27
|
+
__version__ = "0.0.0"
|
|
28
|
+
|
|
29
|
+
__all__ = ["__version__"]
|