Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 58 additions & 6 deletions astrbot/core/config/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -1621,17 +1621,63 @@ 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": "",
},
"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": "",
},
Expand Down Expand Up @@ -1937,6 +1983,12 @@ class ChatProviderTemplate(TypedDict):
"description": "API Base URL",
"type": "string",
},
"send_dimensions_param": {
"description": "透传 dimensions 参数",
"type": "bool",
"hint": "启用后,将 embedding_dimensions 作为 dimensions 参数发送给上游 API。支持自定义维度的服务(OpenAI、智谱、火山等)可开启此项以实现降维;若上游不支持自定义维度则关闭。",
"condition": {"type": "openai_compatible_embedding"},
},
"volcengine_cluster": {
"type": "string",
"description": "火山引擎集群",
Expand Down Expand Up @@ -2357,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",
Expand Down
4 changes: 4 additions & 0 deletions astrbot/core/provider/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
137 changes: 137 additions & 0 deletions astrbot/core/provider/sources/openai_compatible_embedding_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
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.
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

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:
"""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 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(
"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
self._http_client = None

proxy = provider_config.get("proxy", "")
if proxy:
logger.info(f"[OpenAI Compatible Embedding] 使用代理: {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=timeout,
http_client=self._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()
if self._http_client:
await self._http_client.aclose()
31 changes: 3 additions & 28 deletions dashboard/src/components/shared/AstrBotConfig.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 18 additions & 2 deletions dashboard/src/i18n/locales/en-US/features/config-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -1188,8 +1188,24 @@
"embedding_api_base": {
"description": "API Base URL"
},
"send_dimensions_param": {
"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 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": "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."
},
"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."
Expand Down Expand Up @@ -1518,4 +1534,4 @@
"helpMiddle": "or",
"helpSuffix": "."
}
}
}
20 changes: 18 additions & 2 deletions dashboard/src/i18n/locales/ru-RU/features/config-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -1193,8 +1193,24 @@
"embedding_api_base": {
"description": "Адрес прокси-сервера"
},
"send_dimensions_param": {
"description": "Передавать параметр dimensions",
"hint": "Если включено, отправляет embedding_dimensions в upstream API как параметр dimensions. Это позволяет уменьшить размерность для сервисов, которые это поддерживают (OpenAI, Zhipu, Volcengine). Отключите, если upstream не поддерживает пользовательскую размерность."
},
"openai_embedding": {
"hint": "OpenAI Embedding автоматически добавляет /v1 при запросе."
"hint": "OpenAI Embedding сохранён для существующих конфигураций и стандартных /v1 endpoint-ов. Он автоматически добавляет /v1 и по-прежнему отправляет dimensions, если задан embedding_dimensions."
},
"openai_compatible_embedding": {
"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 без изменений."
},
"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."
Expand Down Expand Up @@ -1523,4 +1539,4 @@
"helpMiddle": "или",
"helpSuffix": "."
}
}
}
20 changes: 18 additions & 2 deletions dashboard/src/i18n/locales/zh-CN/features/config-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -1190,8 +1190,24 @@
"embedding_api_base": {
"description": "API Base URL"
},
"send_dimensions_param": {
"description": "透传 dimensions 参数",
"hint": "启用后,将 embedding_dimensions 作为 dimensions 参数发送给上游 API。支持自定义维度的服务(OpenAI、智谱、火山等)可开启此项以实现降维;若上游不支持自定义维度则关闭。"
},
"openai_embedding": {
"hint": "OpenAI Embedding 会在请求时自动补上 /v1。"
"hint": "OpenAI Embedding 保留给已有配置和标准 /v1 接口使用,会在请求时自动补上 /v1,并在填写 embedding_dimensions 时继续直接发送 dimensions。"
},
"openai_compatible_embedding": {
"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 路径不变。"
},
"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。"
Expand Down Expand Up @@ -1520,4 +1536,4 @@
"helpMiddle": "或",
"helpSuffix": "。"
}
}
}
Loading
Loading