Spaces:
Running
Running
Deploy vectorization service from GitHub 3eb9f3be642221ba8ff6c4accaec7a9da8978534
Browse files- README.md +11 -2
- VERSION +1 -1
- app/bge_m3.py +13 -10
- app/models.py +37 -1
- app/openai_api.py +13 -4
README.md
CHANGED
|
@@ -113,13 +113,22 @@ to the base URL ending in `/openai/v1`, not to the Voyage `/v1` URL.
|
|
| 113 |
`usage.total_tokens` format. Only fixed native dimensions are supported.
|
| 114 |
- `GET /openai/v1/models`: dense model aliases for provider selection.
|
| 115 |
- Bearer authentication uses the **same** `API_KEY` as Voyage; no new secrets.
|
| 116 |
-
- Dense E5
|
|
|
|
|
|
|
|
|
|
| 117 |
- **Crucial:** this endpoint forwards raw input to the tokenizer **without adding any
|
| 118 |
query/document instructions**. Supply prefixes in the client configuration.
|
| 119 |
For **E5 only**, inputs exceeding 512 tokens are truncated from the right
|
| 120 |
to 512 tokens, including the prefix and special tokens. Short inputs are
|
| 121 |
unchanged; truncation is logged with the input index and original token count.
|
| 122 |
-
Qwen inputs are not truncated
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
For E5 configure in Dify:
|
| 125 |
- Document prefix: `passage:`
|
|
|
|
| 113 |
`usage.total_tokens` format. Only fixed native dimensions are supported.
|
| 114 |
- `GET /openai/v1/models`: dense model aliases for provider selection.
|
| 115 |
- Bearer authentication uses the **same** `API_KEY` as Voyage; no new secrets.
|
| 116 |
+
- Dense E5, Qwen3 and BGE-M3 (`bge-m3` or `BAAI/bge-m3`). BGE-M3
|
| 117 |
+
returns only its 1024D dense embedding; sparse and ColBERT are not
|
| 118 |
+
computed on this route. Use `/bge-m3/encode` for the hybrid API.
|
| 119 |
+
mLateOn ColBERT remains on `/colbert/encode` and Voyage.
|
| 120 |
- **Crucial:** this endpoint forwards raw input to the tokenizer **without adding any
|
| 121 |
query/document instructions**. Supply prefixes in the client configuration.
|
| 122 |
For **E5 only**, inputs exceeding 512 tokens are truncated from the right
|
| 123 |
to 512 tokens, including the prefix and special tokens. Short inputs are
|
| 124 |
unchanged; truncation is logged with the input index and original token count.
|
| 125 |
+
Qwen and BGE-M3 inputs are not truncated (BGE-M3 uses
|
| 126 |
+
`BGE_M3_MAX_TOKENS=2048` by default). Unsupported dimensions return an error.
|
| 127 |
+
|
| 128 |
+
For BGE-M3 in Dify: use model `bge-m3`, embedding dimensions `1024`,
|
| 129 |
+
and leave query/document prefixes empty. Only dense vectors are returned
|
| 130 |
+
to standard OpenAI-compatible clients; the native BGE-M3 endpoint keeps
|
| 131 |
+
dense + sparse and optional ColBERT.
|
| 132 |
|
| 133 |
For E5 configure in Dify:
|
| 134 |
- Document prefix: `passage:`
|
VERSION
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
|
|
|
|
| 1 |
+
3eb9f3be642221ba8ff6c4accaec7a9da8978534
|
app/bge_m3.py
CHANGED
|
@@ -55,7 +55,8 @@ class BgeM3Engine:
|
|
| 55 |
)
|
| 56 |
return cls(model, max_tokens=max_tokens)
|
| 57 |
|
| 58 |
-
def encode(self, texts: list[str], return_colbert: bool = False
|
|
|
|
| 59 |
if not texts or len(texts) > 8:
|
| 60 |
raise ValueError("BGE-M3 accepts between 1 and 8 texts per request")
|
| 61 |
|
|
@@ -80,20 +81,22 @@ class BgeM3Engine:
|
|
| 80 |
batch_size=1,
|
| 81 |
max_length=self.max_tokens,
|
| 82 |
return_dense=True,
|
| 83 |
-
return_sparse=
|
| 84 |
return_colbert_vecs=return_colbert,
|
| 85 |
)
|
| 86 |
dense = [float(x) for x in outputs["dense_vecs"][0]]
|
| 87 |
validate_vector(dense, self.dimensions)
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
|
|
|
|
|
|
| 97 |
|
| 98 |
colbert = None
|
| 99 |
if return_colbert:
|
|
|
|
| 55 |
)
|
| 56 |
return cls(model, max_tokens=max_tokens)
|
| 57 |
|
| 58 |
+
def encode(self, texts: list[str], return_colbert: bool = False,
|
| 59 |
+
return_sparse: bool = True) -> list[BgeM3Embedding]:
|
| 60 |
if not texts or len(texts) > 8:
|
| 61 |
raise ValueError("BGE-M3 accepts between 1 and 8 texts per request")
|
| 62 |
|
|
|
|
| 81 |
batch_size=1,
|
| 82 |
max_length=self.max_tokens,
|
| 83 |
return_dense=True,
|
| 84 |
+
return_sparse=return_sparse,
|
| 85 |
return_colbert_vecs=return_colbert,
|
| 86 |
)
|
| 87 |
dense = [float(x) for x in outputs["dense_vecs"][0]]
|
| 88 |
validate_vector(dense, self.dimensions)
|
| 89 |
|
| 90 |
+
sparse_items = []
|
| 91 |
+
if return_sparse:
|
| 92 |
+
weights = outputs["lexical_weights"][0]
|
| 93 |
+
sparse_items = sorted(
|
| 94 |
+
(int(token_id), float(weight))
|
| 95 |
+
for token_id, weight in weights.items()
|
| 96 |
+
)
|
| 97 |
+
if any(index < 0 or not math.isfinite(weight) or weight < 0
|
| 98 |
+
for index, weight in sparse_items):
|
| 99 |
+
raise RuntimeError("Invalid BGE-M3 sparse weights")
|
| 100 |
|
| 101 |
colbert = None
|
| 102 |
if return_colbert:
|
app/models.py
CHANGED
|
@@ -186,8 +186,44 @@ class ModelRegistry:
|
|
| 186 |
return (QWEN_ID if engine is self.qwen else E5_ID), engine.dimensions, result
|
| 187 |
|
| 188 |
def openai_encode(self, texts, canonical_model):
|
| 189 |
-
"""
|
| 190 |
from app.batching import batch_encode
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
if canonical_model == E5_ID:
|
| 192 |
engine, is_qwen = self.e5, False
|
| 193 |
elif canonical_model == QWEN_ID:
|
|
|
|
| 186 |
return (QWEN_ID if engine is self.qwen else E5_ID), engine.dimensions, result
|
| 187 |
|
| 188 |
def openai_encode(self, texts, canonical_model):
|
| 189 |
+
"""OpenAI-compatible output is always dense only."""
|
| 190 |
from app.batching import batch_encode
|
| 191 |
+
from app.bge_m3 import BGE_M3_ID
|
| 192 |
+
if canonical_model == BGE_M3_ID:
|
| 193 |
+
if self.bge_m3 is None:
|
| 194 |
+
raise ValueError("BGE-M3 is disabled")
|
| 195 |
+
|
| 196 |
+
def encode_bge_dense():
|
| 197 |
+
engine = self.bge_m3
|
| 198 |
+
counts = [
|
| 199 |
+
len(engine.model.tokenizer(text, add_special_tokens=True,
|
| 200 |
+
truncation=False)["input_ids"])
|
| 201 |
+
for text in texts
|
| 202 |
+
]
|
| 203 |
+
for index, count in enumerate(counts):
|
| 204 |
+
if count > engine.max_tokens:
|
| 205 |
+
raise TooManyTokens(count, engine.max_tokens,
|
| 206 |
+
input_index=index)
|
| 207 |
+
if sum(counts) > 65536:
|
| 208 |
+
raise ValueError("Batch exceeds 65536 total input tokens")
|
| 209 |
+
results = []
|
| 210 |
+
group, group_tokens = [], 0
|
| 211 |
+
# Dify may send 16+ texts; internal native BGE max is 8.
|
| 212 |
+
for text, count in zip(texts, counts):
|
| 213 |
+
if group and (len(group) == 8 or group_tokens + count > 8192):
|
| 214 |
+
results.extend(engine.encode(
|
| 215 |
+
group, return_sparse=False, return_colbert=False))
|
| 216 |
+
group, group_tokens = [], 0
|
| 217 |
+
group.append(text)
|
| 218 |
+
group_tokens += count
|
| 219 |
+
if group:
|
| 220 |
+
results.extend(engine.encode(
|
| 221 |
+
group, return_sparse=False, return_colbert=False))
|
| 222 |
+
return [EmbeddingResult(
|
| 223 |
+
tokens_count=item.tokens_count, embedding=item.dense)
|
| 224 |
+
for item in results]
|
| 225 |
+
|
| 226 |
+
return self._run(encode_bge_dense)
|
| 227 |
if canonical_model == E5_ID:
|
| 228 |
engine, is_qwen = self.e5, False
|
| 229 |
elif canonical_model == QWEN_ID:
|
app/openai_api.py
CHANGED
|
@@ -15,13 +15,15 @@ from fastapi.responses import JSONResponse
|
|
| 15 |
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
| 16 |
|
| 17 |
from app.embedding import MODEL_ID as E5_ID, TooManyTokens
|
|
|
|
| 18 |
from app.models import ModelRegistry, QWEN_ID, ServiceBusy
|
| 19 |
from app.voyage_api import authenticate, resident_models
|
| 20 |
|
| 21 |
router = APIRouter(prefix="/openai/v1", tags=["OpenAI-compatible"])
|
| 22 |
log = logging.getLogger(__name__)
|
| 23 |
DenseModelName = Literal[
|
| 24 |
-
"e5", "qwen", "
|
|
|
|
| 25 |
]
|
| 26 |
|
| 27 |
|
|
@@ -61,8 +63,14 @@ def create_embedding(
|
|
| 61 |
registry: ModelRegistry = Depends(resident_models),
|
| 62 |
):
|
| 63 |
model = request.model
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
if request.dimensions is not None and request.dimensions != engine.dimensions:
|
| 67 |
log.warning(
|
| 68 |
"OpenAI embeddings rejected: reason=unsupported_dimensions model=%s "
|
|
@@ -131,6 +139,7 @@ def list_models():
|
|
| 131 |
"object": "list",
|
| 132 |
"data": [
|
| 133 |
{"id": mid, "object": "model", "created": 0, "owned_by": "local"}
|
| 134 |
-
for mid in ("e5", "qwen", E5_ID, QWEN_ID
|
|
|
|
| 135 |
],
|
| 136 |
}
|
|
|
|
| 15 |
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
| 16 |
|
| 17 |
from app.embedding import MODEL_ID as E5_ID, TooManyTokens
|
| 18 |
+
from app.bge_m3 import BGE_M3_ID
|
| 19 |
from app.models import ModelRegistry, QWEN_ID, ServiceBusy
|
| 20 |
from app.voyage_api import authenticate, resident_models
|
| 21 |
|
| 22 |
router = APIRouter(prefix="/openai/v1", tags=["OpenAI-compatible"])
|
| 23 |
log = logging.getLogger(__name__)
|
| 24 |
DenseModelName = Literal[
|
| 25 |
+
"e5", "qwen", "bge-m3", "intfloat/multilingual-e5-small",
|
| 26 |
+
"Qwen/Qwen3-Embedding-0.6B", "BAAI/bge-m3"
|
| 27 |
]
|
| 28 |
|
| 29 |
|
|
|
|
| 63 |
registry: ModelRegistry = Depends(resident_models),
|
| 64 |
):
|
| 65 |
model = request.model
|
| 66 |
+
if model in ("bge-m3", BGE_M3_ID):
|
| 67 |
+
canonical, engine = BGE_M3_ID, registry.bge_m3
|
| 68 |
+
elif model in ("qwen", QWEN_ID):
|
| 69 |
+
canonical, engine = QWEN_ID, registry.qwen
|
| 70 |
+
else:
|
| 71 |
+
canonical, engine = E5_ID, registry.e5
|
| 72 |
+
if engine is None:
|
| 73 |
+
return api_error(503, "BGE-M3 model is disabled", "server_error")
|
| 74 |
if request.dimensions is not None and request.dimensions != engine.dimensions:
|
| 75 |
log.warning(
|
| 76 |
"OpenAI embeddings rejected: reason=unsupported_dimensions model=%s "
|
|
|
|
| 139 |
"object": "list",
|
| 140 |
"data": [
|
| 141 |
{"id": mid, "object": "model", "created": 0, "owned_by": "local"}
|
| 142 |
+
for mid in ("e5", "qwen", E5_ID, QWEN_ID,
|
| 143 |
+
"bge-m3", BGE_M3_ID)
|
| 144 |
],
|
| 145 |
}
|