fix: add embedding dimensions send modes (#9245)

* fix: make embedding dimensions request optional

* fix: add embedding dimensions send modes

* fix: keep siliconflow qwen dimensions in auto mode

* fix: harden embedding dimensions auto mode
This commit is contained in:
lxfight
2026-07-15 16:12:16 +08:00
committed by GitHub
parent 296fcfb54a
commit 635124be32
6 changed files with 176 additions and 20 deletions

View File

@@ -1839,6 +1839,7 @@ CONFIG_METADATA_2 = {
"embedding_api_base": "",
"embedding_model": "",
"embedding_dimensions": 1024,
"embedding_dimensions_mode": "auto",
"timeout": 20,
"proxy": "",
},
@@ -2246,6 +2247,12 @@ CONFIG_METADATA_2 = {
"hint": "嵌入向量的维度。根据模型不同,可能需要调整,请参考具体模型的文档。此配置项请务必填写正确,否则将导致向量数据库无法正常工作。",
"_special": "get_embedding_dim",
},
"embedding_dimensions_mode": {
"description": "嵌入维度参数发送模式",
"type": "string",
"options": ["auto", "always", "never"],
"hint": "控制是否在 OpenAI 兼容 Embedding 请求中发送 dimensions 参数。auto 会仅对官方 OpenAI embedding-3 模型自动发送;第三方兼容 API 如需该参数可改为 always报错时改为 never。",
},
"embedding_model": {
"description": "嵌入模型",
"type": "string",

View File

@@ -1,4 +1,5 @@
import re
from urllib.parse import urlparse
import httpx
from openai import AsyncOpenAI
@@ -66,32 +67,47 @@ class OpenAIEmbeddingProvider(EmbeddingProvider):
return [item.embedding for item in embeddings.data]
def _embedding_kwargs(self) -> dict:
"""构建嵌入请求的可选参数"""
"""Build optional embedding request parameters."""
kwargs = {}
if "embedding_dimensions" in self.provider_config:
dimensions_mode = self.provider_config.get("embedding_dimensions_mode", "auto")
if dimensions_mode not in {"auto", "always", "never"}:
logger.warning(
f"Unknown embedding_dimensions_mode in embedding configs: '{dimensions_mode}', fallback to 'auto'."
)
dimensions_mode = "auto"
send_dimensions = dimensions_mode == "always"
if dimensions_mode == "auto":
api_base = _normalize_api_base(
self.provider_config.get(
"embedding_api_base", "https://api.openai.com/v1"
)
or "https://api.openai.com/v1"
)
parsed_api_base = urlparse(api_base)
model = (
getattr(self, "model", None)
or self.provider_config.get("embedding_model")
or "text-embedding-3-small"
)
model_lower = str(model).lower()
model_name = model_lower.rsplit("/", 1)[-1]
send_dimensions = (
parsed_api_base.scheme == "https"
and parsed_api_base.hostname == "api.openai.com"
and parsed_api_base.path.rstrip("/") == "/v1"
and model_name.startswith("text-embedding-3")
) or (
parsed_api_base.scheme == "https"
and parsed_api_base.hostname == "api.siliconflow.cn"
and model_name.startswith("qwen")
)
if send_dimensions and "embedding_dimensions" in self.provider_config:
try:
kwargs["dimensions"] = int(self.provider_config["embedding_dimensions"])
except (ValueError, TypeError):
logger.warning(
f"embedding_dimensions in embedding configs is not a valid integer: '{self.provider_config['embedding_dimensions']}', ignored."
)
# Fix: SiliconFlow provider does not support dimensions parameter, except for Qwen models.
provider_api_base = self.provider_config.get("embedding_api_base")
provider_id = self.provider_config.get("id", "unknown_id")
if (
provider_api_base
# Hard-code SiliconFlow API Base Prefix and Model Name, as it's just a temporary workaround.
and provider_api_base.strip().startswith("https://api.siliconflow.cn")
and not self.model.lower().startswith("qwen")
):
# For SiliconFlow and Non-Qwen models, dimensions parameter is not supported. so remove it.
removed_dimensions = kwargs.pop("dimensions", None)
if removed_dimensions is not None:
# Log a warning message if dimensions parameter is removed.
logger.warning(
f"dimensions not supported for model '{self.model}' of provider '{provider_id}' as SiliconFlow does not support this parameter for non-Qwen models: '{removed_dimensions}'."
)
return kwargs
def get_dim(self) -> int:

View File

@@ -1393,6 +1393,10 @@
"description": "Embedding dimensions",
"hint": "Embedding vector dimensions. May need adjustment per model; see model documentation. This must be correct or the vector database will not work."
},
"embedding_dimensions_mode": {
"description": "Embedding dimensions mode",
"hint": "Controls whether to send the dimensions parameter in OpenAI-compatible embedding requests. auto sends it only for official OpenAI embedding-3 models; use always when a compatible API supports it, or never when it rejects the parameter."
},
"embedding_model": {
"description": "Embedding model",
"hint": "Embedding model name."

View File

@@ -1390,6 +1390,10 @@
"description": "Размерность эмбеддингов",
"hint": "Размерность векторов эмбеддингов. Зависит от модели. Должно быть указано верно для работы векторной базы данных."
},
"embedding_dimensions_mode": {
"description": "Режим параметра dimensions",
"hint": "Управляет отправкой параметра dimensions в OpenAI-совместимых запросах Embedding. auto отправляет его только для официальных моделей OpenAI embedding-3; используйте always, если совместимый API поддерживает параметр, или never, если он отклоняет параметр."
},
"embedding_model": {
"description": "Модель эмбеддингов",
"hint": "Имя модели эмбеддингов."

View File

@@ -1395,6 +1395,10 @@
"description": "嵌入维度",
"hint": "嵌入向量的维度。根据模型不同,可能需要调整,请参考具体模型的文档。此配置项请务必填写正确,否则将导致向量数据库无法正常工作。"
},
"embedding_dimensions_mode": {
"description": "嵌入维度参数发送模式",
"hint": "控制是否在 OpenAI 兼容 Embedding 请求中发送 dimensions 参数。auto 会仅对官方 OpenAI embedding-3 模型自动发送;第三方兼容 API 如需该参数可改为 always报错时改为 never。"
},
"embedding_model": {
"description": "嵌入模型",
"hint": "嵌入模型名称。"

View File

@@ -1,4 +1,7 @@
from astrbot.core.provider.sources.openai_embedding_source import _normalize_api_base
from astrbot.core.provider.sources.openai_embedding_source import (
OpenAIEmbeddingProvider,
_normalize_api_base,
)
def test_openai_embedding_api_base_keeps_version_suffixes():
@@ -16,3 +19,121 @@ def test_openai_embedding_api_base_adds_default_version():
assert _normalize_api_base("https://example.test/v1/embeddings") == (
"https://example.test/v1"
)
def test_openai_embedding_dimensions_auto_sends_for_official_openai_embedding_3():
provider = OpenAIEmbeddingProvider.__new__(OpenAIEmbeddingProvider)
provider.provider_config = {"embedding_dimensions": 1024}
provider.model = "text-embedding-3-small"
assert provider.get_dim() == 1024
assert provider._embedding_kwargs() == {"dimensions": 1024}
def test_openai_embedding_dimensions_invalid_mode_falls_back_to_auto():
provider = OpenAIEmbeddingProvider.__new__(OpenAIEmbeddingProvider)
provider.provider_config = {
"embedding_dimensions": 1024,
"embedding_dimensions_mode": "foo",
}
provider.model = "text-embedding-3-small"
assert provider.get_dim() == 1024
assert provider._embedding_kwargs() == {"dimensions": 1024}
def test_openai_embedding_dimensions_auto_skips_for_official_openai_non_3_model():
provider = OpenAIEmbeddingProvider.__new__(OpenAIEmbeddingProvider)
provider.provider_config = {
"embedding_api_base": "https://api.openai.com/v1",
"embedding_dimensions": 1024,
"embedding_dimensions_mode": "auto",
}
provider.model = "text-embedding-ada-002"
assert provider._embedding_kwargs() == {}
def test_openai_embedding_dimensions_auto_skips_custom_api_base():
provider = OpenAIEmbeddingProvider.__new__(OpenAIEmbeddingProvider)
provider.provider_config = {
"embedding_api_base": "https://api.siliconflow.cn/v1",
"embedding_dimensions": 1024,
"embedding_dimensions_mode": "auto",
}
provider.model = "BAAI/bge-m3"
assert provider._embedding_kwargs() == {}
def test_openai_embedding_dimensions_auto_sends_for_siliconflow_qwen():
provider = OpenAIEmbeddingProvider.__new__(OpenAIEmbeddingProvider)
provider.provider_config = {
"embedding_api_base": "https://api.siliconflow.cn/v1",
"embedding_dimensions": 1024,
"embedding_dimensions_mode": "auto",
}
provider.model = "Qwen/Qwen3-Embedding-4B"
assert provider._embedding_kwargs() == {"dimensions": 1024}
def test_openai_embedding_dimensions_auto_skips_siliconflow_lookalike_host():
provider = OpenAIEmbeddingProvider.__new__(OpenAIEmbeddingProvider)
provider.provider_config = {
"embedding_api_base": "https://api.siliconflow.cn.evil.test/v1",
"embedding_dimensions": 1024,
"embedding_dimensions_mode": "auto",
}
provider.model = "Qwen/Qwen3-Embedding-4B"
assert provider._embedding_kwargs() == {}
def test_openai_embedding_dimensions_auto_handles_empty_model():
provider = OpenAIEmbeddingProvider.__new__(OpenAIEmbeddingProvider)
provider.provider_config = {"embedding_dimensions": 1024}
provider.model = None
assert provider._embedding_kwargs() == {"dimensions": 1024}
def test_openai_embedding_dimensions_are_sent_when_mode_is_always():
provider = OpenAIEmbeddingProvider.__new__(OpenAIEmbeddingProvider)
provider.provider_config = {
"embedding_dimensions": 1024,
"embedding_dimensions_mode": "always",
}
assert provider.get_dim() == 1024
assert provider._embedding_kwargs() == {"dimensions": 1024}
def test_openai_embedding_dimensions_always_mode_without_dimensions_sends_nothing():
provider = OpenAIEmbeddingProvider.__new__(OpenAIEmbeddingProvider)
provider.provider_config = {"embedding_dimensions_mode": "always"}
assert provider._embedding_kwargs() == {}
def test_openai_embedding_dimensions_invalid_value_is_ignored():
provider = OpenAIEmbeddingProvider.__new__(OpenAIEmbeddingProvider)
provider.provider_config = {
"embedding_dimensions": "not-a-number",
"embedding_dimensions_mode": "always",
}
assert provider.get_dim() == 0
assert provider._embedding_kwargs() == {}
def test_openai_embedding_dimensions_are_local_when_mode_is_never():
provider = OpenAIEmbeddingProvider.__new__(OpenAIEmbeddingProvider)
provider.provider_config = {
"embedding_dimensions": 1024,
"embedding_dimensions_mode": "never",
}
provider.model = "text-embedding-3-small"
assert provider.get_dim() == 1024
assert provider._embedding_kwargs() == {}