SmartDataAlex commited on
Commit
c130de9
·
verified ·
1 Parent(s): 350ad7e

Deploy vectorization service from GitHub 3eb9f3be642221ba8ff6c4accaec7a9da8978534

Browse files
Files changed (5) hide show
  1. README.md +11 -2
  2. VERSION +1 -1
  3. app/bge_m3.py +13 -10
  4. app/models.py +37 -1
  5. 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 and Qwen3 only. ColBERT retains `/colbert/encode` and Voyage extension.
 
 
 
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. Unsupported dimensions return an error.
 
 
 
 
 
 
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
- f2352e8140c520d4480226b9332c05011709ed54
 
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) -> list[BgeM3Embedding]:
 
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=True,
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
- weights = outputs["lexical_weights"][0]
90
- sparse_items = sorted(
91
- (int(token_id), float(weight))
92
- for token_id, weight in weights.items()
93
- )
94
- if any(index < 0 or not math.isfinite(weight) or weight < 0
95
- for index, weight in sparse_items):
96
- raise RuntimeError("Invalid BGE-M3 sparse weights")
 
 
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
- """Raw OpenAI-compatible embeddings: client supplies any query/document prefix."""
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", "intfloat/multilingual-e5-small", "Qwen/Qwen3-Embedding-0.6B"
 
25
  ]
26
 
27
 
@@ -61,8 +63,14 @@ def create_embedding(
61
  registry: ModelRegistry = Depends(resident_models),
62
  ):
63
  model = request.model
64
- canonical = QWEN_ID if model in ("qwen", QWEN_ID) else E5_ID
65
- engine = registry.qwen if canonical == QWEN_ID else registry.e5
 
 
 
 
 
 
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
  }