From fb912c470cc54062327d4b27a7a27c4d72c99c22 Mon Sep 17 00:00:00 2001 From: whatevertogo Date: Thu, 19 Mar 2026 22:09:01 +0800 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20OpenAI=20?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=E5=B5=8C=E5=85=A5=E6=8F=90=E4=BE=9B=E5=95=86?= =?UTF-8?q?=E5=8F=8A=E7=9B=B8=E5=85=B3=E9=85=8D=E7=BD=AE=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=99=BA=E8=B0=B1=E5=92=8C=E7=81=AB=E5=B1=B1=E5=BC=95?= =?UTF-8?q?=E6=93=8E=E6=8F=90=E4=BE=9B=E5=95=86=E5=B5=8C=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/config/default.py | 45 +++++- astrbot/core/provider/manager.py | 4 + .../openai_compatible_embedding_source.py | 111 ++++++++++++++ .../src/components/shared/AstrBotConfig.vue | 31 +--- .../en-US/features/config-metadata.json | 15 +- .../ru-RU/features/config-metadata.json | 15 +- .../zh-CN/features/config-metadata.json | 15 +- docs/en/use/knowledge-base.md | 10 +- docs/zh/use/knowledge-base.md | 10 +- tests/test_dashboard.py | 80 ++++++++++ ...test_openai_compatible_embedding_source.py | 138 ++++++++++++++++++ 11 files changed, 437 insertions(+), 37 deletions(-) create mode 100644 astrbot/core/provider/sources/openai_compatible_embedding_source.py create mode 100644 tests/test_openai_compatible_embedding_source.py diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 5b4ea7686a..98cc1a2740 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -1621,17 +1621,48 @@ class ChatProviderTemplate(TypedDict): "gemini_tts_voice_name": "Leda", "proxy": "", }, - "OpenAI Embedding": { - "id": "openai_embedding", - "type": "openai_embedding", + "OpenAI Compatible Embedding": { + "id": "openai_compatible_embedding", + "type": "openai_compatible_embedding", "provider": "openai", "provider_type": "embedding", - "hint": "provider_group.provider.openai_embedding.hint", + "hint": "provider_group.provider.openai_compatible_embedding.hint", "enable": True, "embedding_api_key": "", "embedding_api_base": "", "embedding_model": "", "embedding_dimensions": 1024, + "send_dimensions_param": False, + "timeout": 20, + "proxy": "", + }, + "Zhipu Embedding": { + "id": "zhipu_embedding", + "type": "openai_compatible_embedding", + "provider": "zhipu", + "provider_type": "embedding", + "hint": "provider_group.provider.zhipu_embedding.hint", + "enable": True, + "embedding_api_key": "", + "embedding_api_base": "https://open.bigmodel.cn/api/paas/v4", + "embedding_model": "embedding-3", + "embedding_dimensions": 2048, + "send_dimensions_param": True, + "timeout": 20, + "proxy": "", + }, + "Volcengine Embedding": { + "id": "volcengine_embedding", + "type": "openai_compatible_embedding", + "provider": "volcengine", + "provider_type": "embedding", + "hint": "provider_group.provider.volcengine_embedding.hint", + "enable": True, + "embedding_api_key": "", + "embedding_api_base": "https://ark.cn-beijing.volces.com/api/v3", + "embedding_model": "doubao-embedding-vision", + "embedding_dimensions": 2048, + "send_dimensions_param": True, "timeout": 20, "proxy": "", }, @@ -1937,6 +1968,12 @@ class ChatProviderTemplate(TypedDict): "description": "API Base URL", "type": "string", }, + "send_dimensions_param": { + "description": "透传 dimensions 参数", + "type": "bool", + "hint": "启用后,会把 embedding_dimensions 作为 dimensions 参数发送给上游嵌入接口。对于只需要本地向量维度、但不支持 dimensions 参数的 OpenAI 兼容服务,请关闭此项。", + "condition": {"type": "openai_compatible_embedding"}, + }, "volcengine_cluster": { "type": "string", "description": "火山引擎集群", diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py index 0df9f791ae..5336ff8263 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -443,6 +443,10 @@ def dynamic_import_provider(self, type: str) -> None: from .sources.openai_embedding_source import ( OpenAIEmbeddingProvider as OpenAIEmbeddingProvider, ) + case "openai_compatible_embedding": + from .sources.openai_compatible_embedding_source import ( + OpenAICompatibleEmbeddingProvider as OpenAICompatibleEmbeddingProvider, + ) case "gemini_embedding": from .sources.gemini_embedding_source import ( GeminiEmbeddingProvider as GeminiEmbeddingProvider, diff --git a/astrbot/core/provider/sources/openai_compatible_embedding_source.py b/astrbot/core/provider/sources/openai_compatible_embedding_source.py new file mode 100644 index 0000000000..182f7cd33a --- /dev/null +++ b/astrbot/core/provider/sources/openai_compatible_embedding_source.py @@ -0,0 +1,111 @@ +from urllib.parse import urlsplit + +import httpx +from openai import AsyncOpenAI + +from astrbot import logger + +from ..entities import ProviderType +from ..provider import EmbeddingProvider +from ..register import register_provider_adapter + + +def normalize_openai_compatible_embedding_api_base(api_base: str) -> str: + """Normalize API base while preserving provider-specific path prefixes.""" + cleaned_api_base = api_base.strip().removesuffix("/") + if not cleaned_api_base: + return "https://api.openai.com/v1" + + parsed_api_base = urlsplit(cleaned_api_base) + if parsed_api_base.path and parsed_api_base.path != "/": + return cleaned_api_base + + return f"{cleaned_api_base}/v1" + + +def parse_embedding_dimensions(provider_config: dict) -> int: + """Return the configured local vector size, or 0 when unset/invalid.""" + raw_dimensions = provider_config.get("embedding_dimensions") + if raw_dimensions in (None, ""): + return 0 + + try: + return int(raw_dimensions) + except (ValueError, TypeError): + logger.warning( + "embedding_dimensions in embedding configs is not a valid integer: '%s', ignored.", + raw_dimensions, + ) + return 0 + + +def should_send_dimensions_param(provider_config: dict) -> bool: + """Keep dimensions opt-in for generic OpenAI-compatible services.""" + raw_value = provider_config.get("send_dimensions_param", False) + if isinstance(raw_value, bool): + return raw_value + if isinstance(raw_value, str): + return raw_value.strip().lower() in {"1", "true", "yes", "on"} + return bool(raw_value) + + +@register_provider_adapter( + "openai_compatible_embedding", + "OpenAI Compatible Embedding 提供商适配器", + provider_type=ProviderType.EMBEDDING, +) +class OpenAICompatibleEmbeddingProvider(EmbeddingProvider): + def __init__(self, provider_config: dict, provider_settings: dict) -> None: + super().__init__(provider_config, provider_settings) + self.provider_config = provider_config + self.provider_settings = provider_settings + proxy = provider_config.get("proxy", "") + http_client = None + if proxy: + logger.info(f"[OpenAI Compatible Embedding] 使用代理: {proxy}") + http_client = httpx.AsyncClient(proxy=proxy) + + self.client = AsyncOpenAI( + api_key=provider_config.get("embedding_api_key"), + base_url=normalize_openai_compatible_embedding_api_base( + provider_config.get("embedding_api_base", "") + ), + timeout=int(provider_config.get("timeout", 20)), + http_client=http_client, + ) + self.model = provider_config.get("embedding_model", "text-embedding-3-small") + + async def get_embedding(self, text: str) -> list[float]: + """获取文本的嵌入。""" + kwargs = self._embedding_kwargs() + embedding = await self.client.embeddings.create( + input=text, + model=self.model, + **kwargs, + ) + return embedding.data[0].embedding + + async def get_embeddings(self, text: list[str]) -> list[list[float]]: + """批量获取文本的嵌入。""" + kwargs = self._embedding_kwargs() + embeddings = await self.client.embeddings.create( + input=text, + model=self.model, + **kwargs, + ) + return [item.embedding for item in embeddings.data] + + def _embedding_kwargs(self) -> dict: + """Only send optional parameters the upstream explicitly needs.""" + dimensions = parse_embedding_dimensions(self.provider_config) + if should_send_dimensions_param(self.provider_config) and dimensions > 0: + return {"dimensions": dimensions} + return {} + + def get_dim(self) -> int: + """获取向量的维度。""" + return parse_embedding_dimensions(self.provider_config) + + async def terminate(self): + if self.client: + await self.client.close() diff --git a/dashboard/src/components/shared/AstrBotConfig.vue b/dashboard/src/components/shared/AstrBotConfig.vue index bc1c86bdfc..254ceb9f62 100644 --- a/dashboard/src/components/shared/AstrBotConfig.vue +++ b/dashboard/src/components/shared/AstrBotConfig.vue @@ -50,36 +50,11 @@ const filteredIterable = computed(() => { const providerHint = computed(() => { const hint = props.iterable?.hint - if (typeof hint !== 'string' || !hint) return '' - - if ( - hint === 'provider_group.provider.openai_embedding.hint' - || hint === 'provider_group.provider.gemini_embedding.hint' - ) { - return '' - } - - return hint + return typeof hint === 'string' ? hint : '' }) -const getItemHint = (itemKey, itemMeta) => { - if (itemMeta?.hint) return itemMeta.hint - - if (itemKey !== 'embedding_api_base') return '' - - const providerType = props.iterable?.type - if (providerType === 'openai_embedding') { - return getRaw('provider_group.provider.openai_embedding.hint') - ? 'provider_group.provider.openai_embedding.hint' - : '' - } - if (providerType === 'gemini_embedding') { - return getRaw('provider_group.provider.gemini_embedding.hint') - ? 'provider_group.provider.gemini_embedding.hint' - : '' - } - - return '' +const getItemHint = (_itemKey, itemMeta) => { + return itemMeta?.hint || '' } const dialog = ref(false) diff --git a/dashboard/src/i18n/locales/en-US/features/config-metadata.json b/dashboard/src/i18n/locales/en-US/features/config-metadata.json index 2e12143725..fcfb35f94e 100644 --- a/dashboard/src/i18n/locales/en-US/features/config-metadata.json +++ b/dashboard/src/i18n/locales/en-US/features/config-metadata.json @@ -1188,9 +1188,22 @@ "embedding_api_base": { "description": "API Base URL" }, + "send_dimensions_param": { + "description": "Send dimensions parameter", + "hint": "When enabled, AstrBot sends embedding_dimensions to the upstream embedding API as dimensions. Disable this for OpenAI-compatible services that only need the local vector size and do not support the dimensions parameter." + }, "openai_embedding": { "hint": "OpenAI Embedding automatically appends /v1 at request time." }, + "openai_compatible_embedding": { + "hint": "AstrBot appends /v1 only when the API Base URL has no path. Existing paths such as /api/paas/v4 or /api/v3 are preserved as-is." + }, + "zhipu_embedding": { + "hint": "The Zhipu preset defaults to https://open.bigmodel.cn/api/paas/v4 and embedding-3, and keeps the /api/paas/v4 path unchanged." + }, + "volcengine_embedding": { + "hint": "The Volcengine preset defaults to https://ark.cn-beijing.volces.com/api/v3 and doubao-embedding-vision, and keeps the /api/v3 path unchanged. AstrBot still uses this model for text input only." + }, "gemini_embedding": { "hint": "Gemini Embedding does not require manually adding /v1beta." }, @@ -1518,4 +1531,4 @@ "helpMiddle": "or", "helpSuffix": "." } -} \ No newline at end of file +} diff --git a/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json b/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json index 56d12c9838..49f372f81e 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json +++ b/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json @@ -1193,9 +1193,22 @@ "embedding_api_base": { "description": "Адрес прокси-сервера" }, + "send_dimensions_param": { + "description": "Передавать параметр dimensions", + "hint": "Если включено, AstrBot отправляет embedding_dimensions в upstream embedding API как параметр dimensions. Отключите это для OpenAI-совместимых сервисов, которым нужен только локальный размер вектора и которые не поддерживают параметр dimensions." + }, "openai_embedding": { "hint": "OpenAI Embedding автоматически добавляет /v1 при запросе." }, + "openai_compatible_embedding": { + "hint": "AstrBot добавляет /v1 только если в API Base URL нет пути. Уже заданные пути, такие как /api/paas/v4 или /api/v3, сохраняются без изменений." + }, + "zhipu_embedding": { + "hint": "Пресет Zhipu по умолчанию использует https://open.bigmodel.cn/api/paas/v4 и embedding-3, сохраняя путь /api/paas/v4 без изменений." + }, + "volcengine_embedding": { + "hint": "Пресет Volcengine по умолчанию использует https://ark.cn-beijing.volces.com/api/v3 и doubao-embedding-vision, сохраняя путь /api/v3 без изменений. Сейчас AstrBot использует эту модель только для текстового ввода." + }, "gemini_embedding": { "hint": "Gemini Embedding не требует ручного добавления /v1beta." }, @@ -1523,4 +1536,4 @@ "helpMiddle": "или", "helpSuffix": "." } -} \ No newline at end of file +} diff --git a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json index 0c9148bd0b..428a148f6f 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json +++ b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json @@ -1190,9 +1190,22 @@ "embedding_api_base": { "description": "API Base URL" }, + "send_dimensions_param": { + "description": "透传 dimensions 参数", + "hint": "启用后,会把 embedding_dimensions 作为 dimensions 参数发送给上游嵌入接口。对于只需要本地向量维度、但不支持 dimensions 参数的 OpenAI 兼容服务,请关闭此项。" + }, "openai_embedding": { "hint": "OpenAI Embedding 会在请求时自动补上 /v1。" }, + "openai_compatible_embedding": { + "hint": "如果 API Base URL 不带路径,AstrBot 会自动补上 /v1;如果已经带了 /api/paas/v4 或 /api/v3 这类路径,则保持原样。" + }, + "zhipu_embedding": { + "hint": "智谱预设默认使用 https://open.bigmodel.cn/api/paas/v4 和 embedding-3,并保留 /api/paas/v4 路径不变。" + }, + "volcengine_embedding": { + "hint": "火山预设默认使用 https://ark.cn-beijing.volces.com/api/v3 和 doubao-embedding-vision,并保留 /api/v3 路径不变。AstrBot 当前仍只按文本输入使用该模型。" + }, "gemini_embedding": { "hint": "Gemini Embedding 无需手动添加 /v1beta。" }, @@ -1520,4 +1533,4 @@ "helpMiddle": "或", "helpSuffix": "。" } -} \ No newline at end of file +} diff --git a/docs/en/use/knowledge-base.md b/docs/en/use/knowledge-base.md index b1f9e1dc12..7a5a4703ee 100644 --- a/docs/en/use/knowledge-base.md +++ b/docs/en/use/knowledge-base.md @@ -10,12 +10,20 @@ Open the service provider page, click "Add Service Provider", and select Embedding. -Currently, AstrBot supports embedding vector services compatible with OpenAI API and Gemini API. +AstrBot now includes built-in presets for OpenAI-compatible Embedding, Zhipu Embedding, Volcengine Embedding, and Gemini Embedding. + +If you want to connect another OpenAI-compatible embedding service, use `OpenAI Compatible Embedding` first. When `embedding api base` only contains the host, AstrBot automatically appends `/v1`. If the URL already contains a path such as Zhipu `/api/paas/v4` or Volcengine Ark `/api/v3`, AstrBot preserves that path as-is. Click on the provider card above to enter the configuration page and fill in the configuration. After completing the configuration, click Save. +> [!NOTE] +> `OpenAI Compatible Embedding` includes a `send_dimensions_param` switch. When enabled, AstrBot sends `embedding_dimensions` to the upstream embedding API as the `dimensions` parameter. Disable it for OpenAI-compatible services that only need the local vector size and do not support `dimensions`. + +> [!NOTE] +> The Volcengine preset defaults to `doubao-embedding-vision`. AstrBot's knowledge-base pipeline is still text chunking plus text embedding only, so this integration uses the model with text input only and does not add multimodal knowledge-base support yet,although it is a multimodal embedding model. + ## Configuring Reranker Model (Optional) A reranker model can improve the precision of final retrieval results to some extent. diff --git a/docs/zh/use/knowledge-base.md b/docs/zh/use/knowledge-base.md index d79336c251..0a8b79247c 100644 --- a/docs/zh/use/knowledge-base.md +++ b/docs/zh/use/knowledge-base.md @@ -11,12 +11,20 @@ 打开服务提供商页面,点击新增服务提供商,选择 Embedding。 -目前 AstrBot 支持兼容 OpenAI API 和 Gemini API 的嵌入向量服务。 +目前 AstrBot 内置了通用 OpenAI-compatible Embedding、智谱 Embedding、火山 Embedding 和 Gemini Embedding。 + +如果你要接入其他兼容 OpenAI API 的嵌入服务,优先选择 `OpenAI Compatible Embedding`。当 `embedding api base` 只填写域名时,AstrBot 会自动补上 `/v1`;如果你填写的是带路径的地址,例如智谱的 `/api/paas/v4` 或火山 Ark 的 `/api/v3`,AstrBot 会保持原样,不会额外拼接 `/v1`。 点击上面的提供商卡片进入配置页面,填写配置。 配置完成后,点击保存。 +> [!NOTE] +> `OpenAI Compatible Embedding` 提供了 `send_dimensions_param` 开关。开启后,AstrBot 会把 `embedding_dimensions` 作为 `dimensions` 参数发送给上游接口;如果你的兼容服务只需要本地向量维度、但不支持 `dimensions` 参数,请关闭它。 + +> [!NOTE] +> 火山预设默认模型为 `doubao-embedding-vision`。AstrBot 当前的知识库链路仍然只按文本分块和文本 embedding 工作,所以本次接入只会按文本输入使用该模型,不代表已经支持多模态知识库。 + ## 配置重排序模型(可选) 重排序模型可以一定程度上提高最终召回结果的精度。 diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index bf14aa4c72..600dfb2fa1 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -15,6 +15,7 @@ from astrbot.core import LogBroker from astrbot.core.core_lifecycle import AstrBotCoreLifecycle from astrbot.core.db.sqlite import SQLiteDatabase +from astrbot.core.provider.sources import openai_compatible_embedding_source from astrbot.core.star.star import star_registry from astrbot.core.star.star_handler import star_handlers_registry from astrbot.core.utils.pip_installer import PipInstallError @@ -107,6 +108,85 @@ async def test_get_stat(app: Quart, authenticated_header: dict): assert data["status"] == "ok" and "platform" in data["data"] +@pytest.mark.asyncio +async def test_provider_template_exposes_openai_compatible_embedding_presets( + app: Quart, + authenticated_header: dict, +): + test_client = app.test_client() + response = await test_client.get( + "/api/config/provider/template", + headers=authenticated_header, + ) + + assert response.status_code == 200 + data = await response.get_json() + assert data["status"] == "ok" + + templates = data["data"]["config_schema"]["provider"]["config_template"] + assert "OpenAI Compatible Embedding" in templates + assert "Zhipu Embedding" in templates + assert "Volcengine Embedding" in templates + assert templates["OpenAI Compatible Embedding"]["type"] == ( + "openai_compatible_embedding" + ) + + +class _FakeDashboardEmbeddingsAPI: + async def create(self, **kwargs): + return SimpleNamespace( + data=[SimpleNamespace(embedding=[0.1, 0.2, 0.3, 0.4])], + ) + + +class _FakeDashboardAsyncOpenAI: + def __init__(self, **kwargs) -> None: + self.kwargs = kwargs + self.embeddings = _FakeDashboardEmbeddingsAPI() + + async def close(self): + return None + + +@pytest.mark.asyncio +async def test_get_embedding_dim_supports_openai_compatible_embedding( + app: Quart, + authenticated_header: dict, + monkeypatch: pytest.MonkeyPatch, +): + test_client = app.test_client() + monkeypatch.setattr( + openai_compatible_embedding_source, + "AsyncOpenAI", + _FakeDashboardAsyncOpenAI, + ) + + response = await test_client.post( + "/api/config/provider/get_embedding_dim", + json={ + "provider_config": { + "id": "dashboard-openai-compatible-embedding", + "type": "openai_compatible_embedding", + "provider_type": "embedding", + "embedding_api_key": "test-key", + "embedding_api_base": "https://example.com", + "embedding_model": "text-embedding-3-small", + "embedding_dimensions": 2048, + "send_dimensions_param": False, + "timeout": 20, + "proxy": "", + "enable": True, + } + }, + headers=authenticated_header, + ) + + assert response.status_code == 200 + data = await response.get_json() + assert data["status"] == "ok" + assert data["data"]["embedding_dimensions"] == 4 + + @pytest.mark.asyncio async def test_subagent_config_accepts_default_persona( app: Quart, diff --git a/tests/test_openai_compatible_embedding_source.py b/tests/test_openai_compatible_embedding_source.py new file mode 100644 index 0000000000..781d664e60 --- /dev/null +++ b/tests/test_openai_compatible_embedding_source.py @@ -0,0 +1,138 @@ +from types import SimpleNamespace + +import pytest + +from astrbot.core.provider.sources import openai_compatible_embedding_source as source + + +class _FakeEmbeddingsAPI: + def __init__(self, create_calls: list[dict]) -> None: + self._create_calls = create_calls + + async def create(self, **kwargs): + self._create_calls.append(kwargs) + return SimpleNamespace( + data=[SimpleNamespace(embedding=[0.1, 0.2, 0.3])], + ) + + +class _FakeAsyncOpenAI: + instances: list["_FakeAsyncOpenAI"] = [] + + def __init__(self, **kwargs) -> None: + self.kwargs = kwargs + self.create_calls: list[dict] = [] + self.embeddings = _FakeEmbeddingsAPI(self.create_calls) + self.closed = False + self.__class__.instances.append(self) + + async def close(self): + self.closed = True + + +def _make_provider_config(**overrides) -> dict: + provider_config = { + "id": "test-openai-compatible-embedding", + "type": "openai_compatible_embedding", + "embedding_api_key": "test-key", + "embedding_api_base": "", + "embedding_model": "text-embedding-3-small", + "embedding_dimensions": 1024, + "send_dimensions_param": False, + "timeout": 20, + "proxy": "", + } + provider_config.update(overrides) + return provider_config + + +@pytest.mark.parametrize( + ("api_base", "expected_base_url"), + [ + ("", "https://api.openai.com/v1"), + ("https://example.com", "https://example.com/v1"), + ("https://example.com/", "https://example.com/v1"), + ( + "https://open.bigmodel.cn/api/paas/v4", + "https://open.bigmodel.cn/api/paas/v4", + ), + ( + "https://ark.cn-beijing.volces.com/api/v3", + "https://ark.cn-beijing.volces.com/api/v3", + ), + ], +) +def test_normalize_openai_compatible_embedding_api_base(api_base, expected_base_url): + assert ( + source.normalize_openai_compatible_embedding_api_base(api_base) + == expected_base_url + ) + + +@pytest.mark.asyncio +async def test_openai_compatible_embedding_provider_appends_v1_only_for_host_url( + monkeypatch: pytest.MonkeyPatch, +): + _FakeAsyncOpenAI.instances.clear() + monkeypatch.setattr(source, "AsyncOpenAI", _FakeAsyncOpenAI) + + provider = source.OpenAICompatibleEmbeddingProvider( + _make_provider_config(embedding_api_base="https://example.com"), + {}, + ) + + try: + assert _FakeAsyncOpenAI.instances[-1].kwargs["base_url"] == ( + "https://example.com/v1" + ) + finally: + await provider.terminate() + + +@pytest.mark.asyncio +async def test_openai_compatible_embedding_provider_preserves_existing_api_path( + monkeypatch: pytest.MonkeyPatch, +): + _FakeAsyncOpenAI.instances.clear() + monkeypatch.setattr(source, "AsyncOpenAI", _FakeAsyncOpenAI) + + provider = source.OpenAICompatibleEmbeddingProvider( + _make_provider_config( + embedding_api_base="https://open.bigmodel.cn/api/paas/v4", + ), + {}, + ) + + try: + assert _FakeAsyncOpenAI.instances[-1].kwargs["base_url"] == ( + "https://open.bigmodel.cn/api/paas/v4" + ) + finally: + await provider.terminate() + + +@pytest.mark.asyncio +async def test_openai_compatible_embedding_provider_sends_dimensions_only_when_enabled( + monkeypatch: pytest.MonkeyPatch, +): + _FakeAsyncOpenAI.instances.clear() + monkeypatch.setattr(source, "AsyncOpenAI", _FakeAsyncOpenAI) + + provider_without_dimensions = source.OpenAICompatibleEmbeddingProvider( + _make_provider_config(send_dimensions_param=False), + {}, + ) + provider_with_dimensions = source.OpenAICompatibleEmbeddingProvider( + _make_provider_config(send_dimensions_param=True, embedding_dimensions=2048), + {}, + ) + + try: + await provider_without_dimensions.get_embedding("hello") + await provider_with_dimensions.get_embedding("hello") + + assert "dimensions" not in _FakeAsyncOpenAI.instances[0].create_calls[0] + assert _FakeAsyncOpenAI.instances[1].create_calls[0]["dimensions"] == 2048 + finally: + await provider_without_dimensions.terminate() + await provider_with_dimensions.terminate() From 61b3e795294f68d386441032451ed4d7a8f0bda2 Mon Sep 17 00:00:00 2001 From: whatevertogo Date: Thu, 19 Mar 2026 22:13:45 +0800 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20Ollama=20?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=E5=B5=8C=E5=85=A5=E6=8F=90=E4=BE=9B=E5=95=86?= =?UTF-8?q?=E5=8F=8A=E7=9B=B8=E5=85=B3=E9=85=8D=E7=BD=AE=EF=BC=8C=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E6=96=87=E6=A1=A3=E5=92=8C=E6=B5=8B=E8=AF=95=E7=94=A8?= =?UTF-8?q?=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/config/default.py | 15 +++++++++++++++ .../locales/en-US/features/config-metadata.json | 3 +++ .../locales/ru-RU/features/config-metadata.json | 3 +++ .../locales/zh-CN/features/config-metadata.json | 3 +++ docs/en/use/knowledge-base.md | 5 ++++- docs/zh/use/knowledge-base.md | 5 ++++- tests/test_dashboard.py | 2 ++ 7 files changed, 34 insertions(+), 2 deletions(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 98cc1a2740..cf633a2d96 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -1666,6 +1666,21 @@ class ChatProviderTemplate(TypedDict): "timeout": 20, "proxy": "", }, + "Ollama Embedding": { + "id": "ollama_embedding", + "type": "openai_compatible_embedding", + "provider": "ollama", + "provider_type": "embedding", + "hint": "provider_group.provider.ollama_embedding.hint", + "enable": True, + "embedding_api_key": "ollama", + "embedding_api_base": "http://127.0.0.1:11434", + "embedding_model": "embeddinggemma", + "embedding_dimensions": 768, + "send_dimensions_param": False, + "timeout": 20, + "proxy": "", + }, "Gemini Embedding": { "id": "gemini_embedding", "type": "gemini_embedding", diff --git a/dashboard/src/i18n/locales/en-US/features/config-metadata.json b/dashboard/src/i18n/locales/en-US/features/config-metadata.json index fcfb35f94e..2ecd657956 100644 --- a/dashboard/src/i18n/locales/en-US/features/config-metadata.json +++ b/dashboard/src/i18n/locales/en-US/features/config-metadata.json @@ -1204,6 +1204,9 @@ "volcengine_embedding": { "hint": "The Volcengine preset defaults to https://ark.cn-beijing.volces.com/api/v3 and doubao-embedding-vision, and keeps the /api/v3 path unchanged. AstrBot still uses this model for text input only." }, + "ollama_embedding": { + "hint": "The Ollama preset defaults to local http://127.0.0.1:11434, model embeddinggemma, and 768 dimensions. The API key defaults to ollama only for OpenAI SDK compatibility, and Ollama ignores it." + }, "gemini_embedding": { "hint": "Gemini Embedding does not require manually adding /v1beta." }, diff --git a/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json b/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json index 49f372f81e..550d580903 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json +++ b/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json @@ -1209,6 +1209,9 @@ "volcengine_embedding": { "hint": "Пресет Volcengine по умолчанию использует https://ark.cn-beijing.volces.com/api/v3 и doubao-embedding-vision, сохраняя путь /api/v3 без изменений. Сейчас AstrBot использует эту модель только для текстового ввода." }, + "ollama_embedding": { + "hint": "Пресет Ollama по умолчанию использует локальный http://127.0.0.1:11434, модель embeddinggemma и размерность 768. API key по умолчанию равен ollama только для совместимости с OpenAI SDK; сам Ollama его игнорирует." + }, "gemini_embedding": { "hint": "Gemini Embedding не требует ручного добавления /v1beta." }, diff --git a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json index 428a148f6f..b0185ccea9 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json +++ b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json @@ -1206,6 +1206,9 @@ "volcengine_embedding": { "hint": "火山预设默认使用 https://ark.cn-beijing.volces.com/api/v3 和 doubao-embedding-vision,并保留 /api/v3 路径不变。AstrBot 当前仍只按文本输入使用该模型。" }, + "ollama_embedding": { + "hint": "Ollama 预设默认使用本地 http://127.0.0.1:11434、模型 embeddinggemma、维度 768。API Key 默认填 ollama,仅用于兼容 OpenAI SDK,Ollama 会忽略它。" + }, "gemini_embedding": { "hint": "Gemini Embedding 无需手动添加 /v1beta。" }, diff --git a/docs/en/use/knowledge-base.md b/docs/en/use/knowledge-base.md index 7a5a4703ee..3c1e118722 100644 --- a/docs/en/use/knowledge-base.md +++ b/docs/en/use/knowledge-base.md @@ -10,7 +10,7 @@ Open the service provider page, click "Add Service Provider", and select Embedding. -AstrBot now includes built-in presets for OpenAI-compatible Embedding, Zhipu Embedding, Volcengine Embedding, and Gemini Embedding. +AstrBot now includes built-in presets for OpenAI-compatible Embedding, Zhipu Embedding, Volcengine Embedding, Ollama Embedding, and Gemini Embedding. If you want to connect another OpenAI-compatible embedding service, use `OpenAI Compatible Embedding` first. When `embedding api base` only contains the host, AstrBot automatically appends `/v1`. If the URL already contains a path such as Zhipu `/api/paas/v4` or Volcengine Ark `/api/v3`, AstrBot preserves that path as-is. @@ -24,6 +24,9 @@ After completing the configuration, click Save. > [!NOTE] > The Volcengine preset defaults to `doubao-embedding-vision`. AstrBot's knowledge-base pipeline is still text chunking plus text embedding only, so this integration uses the model with text input only and does not add multimodal knowledge-base support yet,although it is a multimodal embedding model. +> [!NOTE] +> The Ollama preset defaults to local `http://127.0.0.1:11434`, model `embeddinggemma`, and 768 dimensions. Before using it, run `ollama pull embeddinggemma` locally and make sure the Ollama service is running. + ## Configuring Reranker Model (Optional) A reranker model can improve the precision of final retrieval results to some extent. diff --git a/docs/zh/use/knowledge-base.md b/docs/zh/use/knowledge-base.md index 0a8b79247c..a52caf103d 100644 --- a/docs/zh/use/knowledge-base.md +++ b/docs/zh/use/knowledge-base.md @@ -11,7 +11,7 @@ 打开服务提供商页面,点击新增服务提供商,选择 Embedding。 -目前 AstrBot 内置了通用 OpenAI-compatible Embedding、智谱 Embedding、火山 Embedding 和 Gemini Embedding。 +目前 AstrBot 内置了通用 OpenAI-compatible Embedding、智谱 Embedding、火山 Embedding、Ollama Embedding 和 Gemini Embedding。 如果你要接入其他兼容 OpenAI API 的嵌入服务,优先选择 `OpenAI Compatible Embedding`。当 `embedding api base` 只填写域名时,AstrBot 会自动补上 `/v1`;如果你填写的是带路径的地址,例如智谱的 `/api/paas/v4` 或火山 Ark 的 `/api/v3`,AstrBot 会保持原样,不会额外拼接 `/v1`。 @@ -25,6 +25,9 @@ > [!NOTE] > 火山预设默认模型为 `doubao-embedding-vision`。AstrBot 当前的知识库链路仍然只按文本分块和文本 embedding 工作,所以本次接入只会按文本输入使用该模型,不代表已经支持多模态知识库。 +> [!NOTE] +> Ollama 预设默认指向本地 `http://127.0.0.1:11434`,模型为 `embeddinggemma`,默认维度为 768。开始使用前请先在本机执行 `ollama pull embeddinggemma`,并确保 Ollama 服务已经启动。 + ## 配置重排序模型(可选) 重排序模型可以一定程度上提高最终召回结果的精度。 diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 600dfb2fa1..f00da85539 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -127,9 +127,11 @@ async def test_provider_template_exposes_openai_compatible_embedding_presets( assert "OpenAI Compatible Embedding" in templates assert "Zhipu Embedding" in templates assert "Volcengine Embedding" in templates + assert "Ollama Embedding" in templates assert templates["OpenAI Compatible Embedding"]["type"] == ( "openai_compatible_embedding" ) + assert templates["Ollama Embedding"]["provider"] == "ollama" class _FakeDashboardEmbeddingsAPI: From 2aaad51e32de5b76e423418c23d0c69255cb4e12 Mon Sep 17 00:00:00 2001 From: whatevertogo Date: Thu, 19 Mar 2026 22:32:44 +0800 Subject: [PATCH 3/8] fix: address PR review feedback for embedding provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix typo in docs: "yet,although" → "yet, although" - Fix resource leak: store httpx.AsyncClient as self._http_client and close in terminate() - Add timeout parsing with exception handling for invalid values - Handle API base URL without scheme (e.g., "api.openai.com") - Add test assertions to verify terminate() properly closes client --- .../openai_compatible_embedding_source.py | 33 ++++++++++++++++--- docs/en/use/knowledge-base.md | 2 +- ...test_openai_compatible_embedding_source.py | 5 +++ 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/astrbot/core/provider/sources/openai_compatible_embedding_source.py b/astrbot/core/provider/sources/openai_compatible_embedding_source.py index 182f7cd33a..5c89b42e07 100644 --- a/astrbot/core/provider/sources/openai_compatible_embedding_source.py +++ b/astrbot/core/provider/sources/openai_compatible_embedding_source.py @@ -11,12 +11,23 @@ def normalize_openai_compatible_embedding_api_base(api_base: str) -> str: - """Normalize API base while preserving provider-specific path prefixes.""" + """Normalize API base while preserving provider-specific path prefixes. + + Handles URLs with or without scheme: + - Empty/whitespace → https://api.openai.com/v1 + - Host only (api.openai.com) → https://api.openai.com/v1 + - Full URL with path (https://example.com/api/v3) → preserved as-is + """ cleaned_api_base = api_base.strip().removesuffix("/") if not cleaned_api_base: return "https://api.openai.com/v1" parsed_api_base = urlsplit(cleaned_api_base) + # If no scheme, the URL is parsed incorrectly (host becomes path) + if not parsed_api_base.scheme: + cleaned_api_base = f"https://{cleaned_api_base}" + parsed_api_base = urlsplit(cleaned_api_base) + if parsed_api_base.path and parsed_api_base.path != "/": return cleaned_api_base @@ -59,19 +70,29 @@ def __init__(self, provider_config: dict, provider_settings: dict) -> None: super().__init__(provider_config, provider_settings) self.provider_config = provider_config self.provider_settings = provider_settings + self._http_client = None + proxy = provider_config.get("proxy", "") - http_client = None if proxy: logger.info(f"[OpenAI Compatible Embedding] 使用代理: {proxy}") - http_client = httpx.AsyncClient(proxy=proxy) + self._http_client = httpx.AsyncClient(proxy=proxy) + + try: + timeout = int(provider_config.get("timeout", 20)) + except (ValueError, TypeError): + logger.warning( + "Invalid timeout value in provider config: '%s'. Using default 20s.", + provider_config.get("timeout"), + ) + timeout = 20 self.client = AsyncOpenAI( api_key=provider_config.get("embedding_api_key"), base_url=normalize_openai_compatible_embedding_api_base( provider_config.get("embedding_api_base", "") ), - timeout=int(provider_config.get("timeout", 20)), - http_client=http_client, + timeout=timeout, + http_client=self._http_client, ) self.model = provider_config.get("embedding_model", "text-embedding-3-small") @@ -109,3 +130,5 @@ def get_dim(self) -> int: async def terminate(self): if self.client: await self.client.close() + if self._http_client: + await self._http_client.aclose() diff --git a/docs/en/use/knowledge-base.md b/docs/en/use/knowledge-base.md index 3c1e118722..33b5fc7736 100644 --- a/docs/en/use/knowledge-base.md +++ b/docs/en/use/knowledge-base.md @@ -22,7 +22,7 @@ After completing the configuration, click Save. > `OpenAI Compatible Embedding` includes a `send_dimensions_param` switch. When enabled, AstrBot sends `embedding_dimensions` to the upstream embedding API as the `dimensions` parameter. Disable it for OpenAI-compatible services that only need the local vector size and do not support `dimensions`. > [!NOTE] -> The Volcengine preset defaults to `doubao-embedding-vision`. AstrBot's knowledge-base pipeline is still text chunking plus text embedding only, so this integration uses the model with text input only and does not add multimodal knowledge-base support yet,although it is a multimodal embedding model. +> The Volcengine preset defaults to `doubao-embedding-vision`. AstrBot's knowledge-base pipeline is still text chunking plus text embedding only, so this integration uses the model with text input only and does not add multimodal knowledge-base support yet, although it is a multimodal embedding model. > [!NOTE] > The Ollama preset defaults to local `http://127.0.0.1:11434`, model `embeddinggemma`, and 768 dimensions. Before using it, run `ollama pull embeddinggemma` locally and make sure the Ollama service is running. diff --git a/tests/test_openai_compatible_embedding_source.py b/tests/test_openai_compatible_embedding_source.py index 781d664e60..5f8d365169 100644 --- a/tests/test_openai_compatible_embedding_source.py +++ b/tests/test_openai_compatible_embedding_source.py @@ -50,6 +50,7 @@ def _make_provider_config(**overrides) -> dict: ("api_base", "expected_base_url"), [ ("", "https://api.openai.com/v1"), + ("api.openai.com", "https://api.openai.com/v1"), ("https://example.com", "https://example.com/v1"), ("https://example.com/", "https://example.com/v1"), ( @@ -87,6 +88,7 @@ async def test_openai_compatible_embedding_provider_appends_v1_only_for_host_url ) finally: await provider.terminate() + assert provider.client.closed is True @pytest.mark.asyncio @@ -109,6 +111,7 @@ async def test_openai_compatible_embedding_provider_preserves_existing_api_path( ) finally: await provider.terminate() + assert provider.client.closed is True @pytest.mark.asyncio @@ -136,3 +139,5 @@ async def test_openai_compatible_embedding_provider_sends_dimensions_only_when_e finally: await provider_without_dimensions.terminate() await provider_with_dimensions.terminate() + assert provider_without_dimensions.client.closed is True + assert provider_with_dimensions.client.closed is True From 448b5e41f6c176ee068820cfc88d8be90a2b1d21 Mon Sep 17 00:00:00 2001 From: whatevertogo Date: Thu, 19 Mar 2026 22:59:04 +0800 Subject: [PATCH 4/8] docs: improve embedding provider hints for clarity - Clarify that openai_embedding is kept for backward compatibility - Explain dimensions forwarding behavior difference between the two providers - Update hints in en-US, zh-CN, and ru-RU locales --- .../src/i18n/locales/en-US/features/config-metadata.json | 4 ++-- .../src/i18n/locales/ru-RU/features/config-metadata.json | 4 ++-- .../src/i18n/locales/zh-CN/features/config-metadata.json | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dashboard/src/i18n/locales/en-US/features/config-metadata.json b/dashboard/src/i18n/locales/en-US/features/config-metadata.json index 2ecd657956..48154af23a 100644 --- a/dashboard/src/i18n/locales/en-US/features/config-metadata.json +++ b/dashboard/src/i18n/locales/en-US/features/config-metadata.json @@ -1193,10 +1193,10 @@ "hint": "When enabled, AstrBot sends embedding_dimensions to the upstream embedding API as dimensions. Disable this for OpenAI-compatible services that only need the local vector size and do not support the dimensions parameter." }, "openai_embedding": { - "hint": "OpenAI Embedding automatically appends /v1 at request time." + "hint": "OpenAI Embedding is kept for existing configs and standard /v1 endpoints. It automatically appends /v1 and still forwards dimensions whenever embedding_dimensions is configured." }, "openai_compatible_embedding": { - "hint": "AstrBot appends /v1 only when the API Base URL has no path. Existing paths such as /api/paas/v4 or /api/v3 are preserved as-is." + "hint": "Use this for broader OpenAI-compatible embedding services. AstrBot appends /v1 only when the API Base URL has no path, and preserves existing paths such as /api/paas/v4 or /api/v3. Unlike the legacy OpenAI Embedding preset, dimensions forwarding is explicitly configurable here." }, "zhipu_embedding": { "hint": "The Zhipu preset defaults to https://open.bigmodel.cn/api/paas/v4 and embedding-3, and keeps the /api/paas/v4 path unchanged." diff --git a/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json b/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json index 550d580903..cce274c265 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json +++ b/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json @@ -1198,10 +1198,10 @@ "hint": "Если включено, AstrBot отправляет embedding_dimensions в upstream embedding API как параметр dimensions. Отключите это для OpenAI-совместимых сервисов, которым нужен только локальный размер вектора и которые не поддерживают параметр dimensions." }, "openai_embedding": { - "hint": "OpenAI Embedding автоматически добавляет /v1 при запросе." + "hint": "OpenAI Embedding сохранён для существующих конфигураций и стандартных /v1 endpoint-ов. Он автоматически добавляет /v1 и по-прежнему отправляет dimensions, если задан embedding_dimensions." }, "openai_compatible_embedding": { - "hint": "AstrBot добавляет /v1 только если в API Base URL нет пути. Уже заданные пути, такие как /api/paas/v4 или /api/v3, сохраняются без изменений." + "hint": "Используйте этот вариант для более широкого круга OpenAI-совместимых embedding-сервисов. AstrBot добавляет /v1 только если в API Base URL нет пути, а уже заданные пути, такие как /api/paas/v4 или /api/v3, сохраняются без изменений. В отличие от старого OpenAI Embedding, здесь можно явно управлять передачей dimensions." }, "zhipu_embedding": { "hint": "Пресет Zhipu по умолчанию использует https://open.bigmodel.cn/api/paas/v4 и embedding-3, сохраняя путь /api/paas/v4 без изменений." diff --git a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json index b0185ccea9..c0c260e567 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json +++ b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json @@ -1195,10 +1195,10 @@ "hint": "启用后,会把 embedding_dimensions 作为 dimensions 参数发送给上游嵌入接口。对于只需要本地向量维度、但不支持 dimensions 参数的 OpenAI 兼容服务,请关闭此项。" }, "openai_embedding": { - "hint": "OpenAI Embedding 会在请求时自动补上 /v1。" + "hint": "OpenAI Embedding 保留给已有配置和标准 /v1 接口使用,会在请求时自动补上 /v1,并在填写 embedding_dimensions 时继续直接发送 dimensions。" }, "openai_compatible_embedding": { - "hint": "如果 API Base URL 不带路径,AstrBot 会自动补上 /v1;如果已经带了 /api/paas/v4 或 /api/v3 这类路径,则保持原样。" + "hint": "用于更广义的 OpenAI 兼容 embedding 服务。如果 API Base URL 不带路径,AstrBot 会自动补上 /v1;如果已经带了 /api/paas/v4 或 /api/v3 这类路径,则保持原样。相比旧的 OpenAI Embedding,这里还可以显式控制是否透传 dimensions。" }, "zhipu_embedding": { "hint": "智谱预设默认使用 https://open.bigmodel.cn/api/paas/v4 和 embedding-3,并保留 /api/paas/v4 路径不变。" From f9e29acdbc190b990db74ee14c2326fc39dff411 Mon Sep 17 00:00:00 2001 From: whatevertogo Date: Thu, 19 Mar 2026 23:01:36 +0800 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20OpenAI=20?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=E5=B5=8C=E5=85=A5=E6=8F=90=E4=BE=9B=E5=95=86?= =?UTF-8?q?=E7=9A=84=E6=96=87=E6=A1=A3=E5=92=8C=E6=B5=8B=E8=AF=95=EF=BC=8C?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E5=85=BC=E5=AE=B9=E6=80=A7=E5=92=8C=E8=AD=A6?= =?UTF-8?q?=E5=91=8A=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../openai_compatible_embedding_source.py | 11 ++-- docs/en/use/knowledge-base.md | 4 +- docs/zh/use/knowledge-base.md | 4 +- ...test_openai_compatible_embedding_source.py | 50 +++++++++++++++++++ 4 files changed, 63 insertions(+), 6 deletions(-) diff --git a/astrbot/core/provider/sources/openai_compatible_embedding_source.py b/astrbot/core/provider/sources/openai_compatible_embedding_source.py index 5c89b42e07..024ae3fda1 100644 --- a/astrbot/core/provider/sources/openai_compatible_embedding_source.py +++ b/astrbot/core/provider/sources/openai_compatible_embedding_source.py @@ -51,13 +51,16 @@ def parse_embedding_dimensions(provider_config: dict) -> int: def should_send_dimensions_param(provider_config: dict) -> bool: - """Keep dimensions opt-in for generic OpenAI-compatible services.""" + """Read the explicit bool switch used by OpenAI-compatible presets.""" raw_value = provider_config.get("send_dimensions_param", False) if isinstance(raw_value, bool): return raw_value - if isinstance(raw_value, str): - return raw_value.strip().lower() in {"1", "true", "yes", "on"} - return bool(raw_value) + if raw_value not in (None, ""): + logger.warning( + "send_dimensions_param should be a boolean in embedding configs: '%s', treated as disabled.", + raw_value, + ) + return False @register_provider_adapter( diff --git a/docs/en/use/knowledge-base.md b/docs/en/use/knowledge-base.md index 33b5fc7736..9b1a0152ed 100644 --- a/docs/en/use/knowledge-base.md +++ b/docs/en/use/knowledge-base.md @@ -14,6 +14,8 @@ AstrBot now includes built-in presets for OpenAI-compatible Embedding, Zhipu Emb If you want to connect another OpenAI-compatible embedding service, use `OpenAI Compatible Embedding` first. When `embedding api base` only contains the host, AstrBot automatically appends `/v1`. If the URL already contains a path such as Zhipu `/api/paas/v4` or Volcengine Ark `/api/v3`, AstrBot preserves that path as-is. +The legacy `OpenAI Embedding` preset is still kept for backward compatibility with existing configurations. It remains a good fit for standard OpenAI-style `/v1` endpoints and keeps the previous behavior of forwarding `dimensions` whenever `embedding_dimensions` is configured. `OpenAI Compatible Embedding` targets broader compatibility by preserving provider-specific path prefixes and making `dimensions` forwarding opt-in, so the two presets are intentionally not interchangeable. + Click on the provider card above to enter the configuration page and fill in the configuration. After completing the configuration, click Save. @@ -64,7 +66,7 @@ In the configuration file, you can specify different knowledge bases for differe 2. Go to the [Model Marketplace](https://ppio.cn/model-api/console) and click on Embedding Models. 3. Click on BAAI:BGE-M3 (as of 2025-06-02, this model is free on this platform). 4. Find the API integration guide and apply for a Key. -5. Fill in the AstrBot OpenAI Embedding model provider configuration: +5. Fill in the AstrBot `OpenAI Compatible Embedding` model provider configuration: 1. API Key is the PPIO API Key you just applied for 2. embedding api base: enter `https://api.ppinfra.com/v3/openai` 3. model: enter the model you selected, in this example `baai/bge-m3`. diff --git a/docs/zh/use/knowledge-base.md b/docs/zh/use/knowledge-base.md index a52caf103d..77fc06f44e 100644 --- a/docs/zh/use/knowledge-base.md +++ b/docs/zh/use/knowledge-base.md @@ -15,6 +15,8 @@ 如果你要接入其他兼容 OpenAI API 的嵌入服务,优先选择 `OpenAI Compatible Embedding`。当 `embedding api base` 只填写域名时,AstrBot 会自动补上 `/v1`;如果你填写的是带路径的地址,例如智谱的 `/api/paas/v4` 或火山 Ark 的 `/api/v3`,AstrBot 会保持原样,不会额外拼接 `/v1`。 +保留旧的 `OpenAI Embedding` 主要是为了兼容已有配置。它仍然适合标准 OpenAI 风格的 `/v1` 接口,并会继续沿用原来的行为:当你配置了 `embedding_dimensions` 时,请求里会直接发送 `dimensions`。`OpenAI Compatible Embedding` 则用于更广义的兼容服务,重点解决自定义路径前缀和是否透传 `dimensions` 这两个兼容性问题,因此两者不是简单重复。 + 点击上面的提供商卡片进入配置页面,填写配置。 配置完成后,点击保存。 @@ -65,7 +67,7 @@ AstrBot 支持多知识库管理。在聊天时,您可以**自由指定知识 2. 进入 [模型广场](https://ppio.cn/model-api/console),点击嵌入模型 3. 点击 BAAI:BGE-M3 (截止至 2025-06-02,该模型在该平台免费)。 4. 找到 API 接入指南,申请 Key。 -5. 填写 AstrBot OpenAI Embedding 模型提供商配置: +5. 填写 AstrBot `OpenAI Compatible Embedding` 模型提供商配置: 1. API Key 为刚刚申请的 PPIO 的 API Key 2. embedding api base 填写 `https://api.ppinfra.com/v3/openai` 3. model 填写你选择的模型,此例子中为 `baai/bge-m3`。 diff --git a/tests/test_openai_compatible_embedding_source.py b/tests/test_openai_compatible_embedding_source.py index 5f8d365169..4fad605cc1 100644 --- a/tests/test_openai_compatible_embedding_source.py +++ b/tests/test_openai_compatible_embedding_source.py @@ -30,6 +30,18 @@ async def close(self): self.closed = True +class _FakeHTTPClient: + instances: list["_FakeHTTPClient"] = [] + + def __init__(self, **kwargs) -> None: + self.kwargs = kwargs + self.closed = False + self.__class__.instances.append(self) + + async def aclose(self): + self.closed = True + + def _make_provider_config(**overrides) -> dict: provider_config = { "id": "test-openai-compatible-embedding", @@ -141,3 +153,41 @@ async def test_openai_compatible_embedding_provider_sends_dimensions_only_when_e await provider_with_dimensions.terminate() assert provider_without_dimensions.client.closed is True assert provider_with_dimensions.client.closed is True + + +@pytest.mark.asyncio +async def test_openai_compatible_embedding_provider_closes_proxy_http_client( + monkeypatch: pytest.MonkeyPatch, +): + _FakeAsyncOpenAI.instances.clear() + _FakeHTTPClient.instances.clear() + monkeypatch.setattr(source, "AsyncOpenAI", _FakeAsyncOpenAI) + monkeypatch.setattr(source.httpx, "AsyncClient", _FakeHTTPClient) + + provider = source.OpenAICompatibleEmbeddingProvider( + _make_provider_config(proxy="http://127.0.0.1:7890"), + {}, + ) + + try: + assert _FakeHTTPClient.instances[-1].kwargs["proxy"] == "http://127.0.0.1:7890" + finally: + await provider.terminate() + assert provider.client.closed is True + assert provider._http_client.closed is True + + +def test_should_send_dimensions_param_requires_boolean( + monkeypatch: pytest.MonkeyPatch, +): + warnings: list[str] = [] + + def _capture_warning(message, *args): + warnings.append(message % args) + + monkeypatch.setattr(source.logger, "warning", _capture_warning) + + assert ( + source.should_send_dimensions_param({"send_dimensions_param": "true"}) is False + ) + assert "send_dimensions_param should be a boolean" in warnings[0] From 6db3a861e4a90369c9a93991f1cad4263da734e0 Mon Sep 17 00:00:00 2001 From: whatevertogo Date: Thu, 19 Mar 2026 23:16:56 +0800 Subject: [PATCH 6/8] docs: clarify send_dimensions_param purpose in i18n - Explain that dimensions enables dimension reduction (not just forwarding) - List services that support it: OpenAI, Zhipu, Volcengine - Update all three locales: en-US, zh-CN, ru-RU --- .../src/i18n/locales/en-US/features/config-metadata.json | 4 ++-- .../src/i18n/locales/ru-RU/features/config-metadata.json | 2 +- .../src/i18n/locales/zh-CN/features/config-metadata.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dashboard/src/i18n/locales/en-US/features/config-metadata.json b/dashboard/src/i18n/locales/en-US/features/config-metadata.json index 48154af23a..089007fe4e 100644 --- a/dashboard/src/i18n/locales/en-US/features/config-metadata.json +++ b/dashboard/src/i18n/locales/en-US/features/config-metadata.json @@ -1189,8 +1189,8 @@ "description": "API Base URL" }, "send_dimensions_param": { - "description": "Send dimensions parameter", - "hint": "When enabled, AstrBot sends embedding_dimensions to the upstream embedding API as dimensions. Disable this for OpenAI-compatible services that only need the local vector size and do not support the dimensions parameter." + "description": "Forward dimensions parameter", + "hint": "When enabled, sends embedding_dimensions to the upstream API as the dimensions parameter. This allows dimension reduction on services that support it (OpenAI, Zhipu, Volcengine). Leave disabled if the upstream does not support custom dimensions." }, "openai_embedding": { "hint": "OpenAI Embedding is kept for existing configs and standard /v1 endpoints. It automatically appends /v1 and still forwards dimensions whenever embedding_dimensions is configured." diff --git a/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json b/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json index cce274c265..478f3c4c68 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json +++ b/dashboard/src/i18n/locales/ru-RU/features/config-metadata.json @@ -1195,7 +1195,7 @@ }, "send_dimensions_param": { "description": "Передавать параметр dimensions", - "hint": "Если включено, AstrBot отправляет embedding_dimensions в upstream embedding API как параметр dimensions. Отключите это для OpenAI-совместимых сервисов, которым нужен только локальный размер вектора и которые не поддерживают параметр dimensions." + "hint": "Если включено, отправляет embedding_dimensions в upstream API как параметр dimensions. Это позволяет уменьшить размерность для сервисов, которые это поддерживают (OpenAI, Zhipu, Volcengine). Отключите, если upstream не поддерживает пользовательскую размерность." }, "openai_embedding": { "hint": "OpenAI Embedding сохранён для существующих конфигураций и стандартных /v1 endpoint-ов. Он автоматически добавляет /v1 и по-прежнему отправляет dimensions, если задан embedding_dimensions." diff --git a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json index c0c260e567..caa865bfe4 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json +++ b/dashboard/src/i18n/locales/zh-CN/features/config-metadata.json @@ -1192,7 +1192,7 @@ }, "send_dimensions_param": { "description": "透传 dimensions 参数", - "hint": "启用后,会把 embedding_dimensions 作为 dimensions 参数发送给上游嵌入接口。对于只需要本地向量维度、但不支持 dimensions 参数的 OpenAI 兼容服务,请关闭此项。" + "hint": "启用后,将 embedding_dimensions 作为 dimensions 参数发送给上游 API。支持自定义维度的服务(OpenAI、智谱、火山等)可开启此项以实现降维;若上游不支持自定义维度则关闭。" }, "openai_embedding": { "hint": "OpenAI Embedding 保留给已有配置和标准 /v1 接口使用,会在请求时自动补上 /v1,并在填写 embedding_dimensions 时继续直接发送 dimensions。" From 04e23fab3b9f331db4776f9616c8f985d57d9759 Mon Sep 17 00:00:00 2001 From: whatevertogo Date: Thu, 19 Mar 2026 23:21:10 +0800 Subject: [PATCH 7/8] fix: use i18n keys for send_dimensions_param in default config Replace hardcoded strings with i18n key paths to enable proper translation in the frontend for send_dimensions_param field. --- astrbot/core/config/default.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index cf633a2d96..459db37f75 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -1984,9 +1984,9 @@ class ChatProviderTemplate(TypedDict): "type": "string", }, "send_dimensions_param": { - "description": "透传 dimensions 参数", + "description": "provider_group.provider.send_dimensions_param.description", "type": "bool", - "hint": "启用后,会把 embedding_dimensions 作为 dimensions 参数发送给上游嵌入接口。对于只需要本地向量维度、但不支持 dimensions 参数的 OpenAI 兼容服务,请关闭此项。", + "hint": "provider_group.provider.send_dimensions_param.hint", "condition": {"type": "openai_compatible_embedding"}, }, "volcengine_cluster": { From 856f13c693aaffb107efb50e927811892bcced77 Mon Sep 17 00:00:00 2001 From: whatevertogo Date: Thu, 19 Mar 2026 23:40:32 +0800 Subject: [PATCH 8/8] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20send=5Fdimensi?= =?UTF-8?q?ons=5Fparam=20=E5=92=8C=20proxy=20=E7=9A=84=E6=8F=8F=E8=BF=B0?= =?UTF-8?q?=E5=92=8C=E6=8F=90=E7=A4=BA=E4=BF=A1=E6=81=AF=EF=BC=8C=E4=BB=A5?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E7=94=A8=E6=88=B7=E7=90=86=E8=A7=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/config/default.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 459db37f75..d933dfa89b 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -1984,9 +1984,9 @@ class ChatProviderTemplate(TypedDict): "type": "string", }, "send_dimensions_param": { - "description": "provider_group.provider.send_dimensions_param.description", + "description": "透传 dimensions 参数", "type": "bool", - "hint": "provider_group.provider.send_dimensions_param.hint", + "hint": "启用后,将 embedding_dimensions 作为 dimensions 参数发送给上游 API。支持自定义维度的服务(OpenAI、智谱、火山等)可开启此项以实现降维;若上游不支持自定义维度则关闭。", "condition": {"type": "openai_compatible_embedding"}, }, "volcengine_cluster": { @@ -2409,9 +2409,9 @@ class ChatProviderTemplate(TypedDict): "type": "string", }, "proxy": { - "description": "provider_group.provider.proxy.description", + "description": "代理地址", "type": "string", - "hint": "provider_group.provider.proxy.hint", + "hint": "HTTP/HTTPS 代理地址,格式如 http://127.0.0.1:7890。仅对该提供商的 API 请求生效,不影响 Docker 内网通信。", }, "model": { "description": "模型 ID",