Skip to content

fix(openai-embedding): add dimensions request parameter toggle#8526

Open
kawayiYokami wants to merge 1 commit into
AstrBotDevs:masterfrom
kawayiYokami:fix/embedding-dimensions-param
Open

fix(openai-embedding): add dimensions request parameter toggle#8526
kawayiYokami wants to merge 1 commit into
AstrBotDevs:masterfrom
kawayiYokami:fix/embedding-dimensions-param

Conversation

@kawayiYokami
Copy link
Copy Markdown
Contributor

@kawayiYokami kawayiYokami commented Jun 2, 2026

修复 #8506

本 PR 为 OpenAI-compatible embedding provider 新增一个嵌入维度请求参数开关,默认关闭。

当前配置项 embedding_dimensions 的主要语义是本地向量维度元数据:它用于 get_dim(),并供 FAISS / 数据库 / UI 等本地逻辑判断向量长度。它不应该在默认情况下被自动转换成 API 请求参数 dimensions 发送给上游。

dimensions 对 OpenAI-compatible embedding 接口来说不是通用参数。很多 embedding 模型只接受 inputmodel,额外发送 dimensions 会导致 400 invalid parameter。硅基流动的 BAAI/bge-m3 就是典型例子。

同时,OpenAI text-embedding-3 系列等模型确实支持通过 dimensions 请求降维。因此本 PR 不彻底删除该能力,而是把它改成显式开关:

  • 默认关闭:不发送 dimensions,避免误伤不支持该参数的模型。
  • 用户显式开启:将 embedding_dimensions 作为 API dimensions 参数发送,用于支持明确支持降维的模型。

Modifications / 改动点

  • 新增 OpenAI Embedding 配置项 embedding_dimensions_as_request_param,默认 false

  • 在 provider 编辑界面的“嵌入维度 / 自动检测”旁边增加“请求参数”开关。

  • 修改 astrbot/core/provider/sources/openai_embedding_source.py:只有开关开启时才发送 dimensions

  • 保留 embedding_dimensionsget_dim() 中作为本地向量维度元数据使用。

  • 新增回归测试,覆盖:

    • 默认关闭时不发送 dimensions
    • 显式开启时发送 dimensions
    • 未配置 embedding_dimensions 时即使开关打开也不会发送无效参数。
  • This is NOT a breaking change. / 这不是一个破坏性变更。

Screenshots or Test Results / 运行截图或测试结果

uv run --dev pytest tests\test_openai_embedding_source.py -q --basetemp .tmp\pytest
# 3 passed in 10.07s

uv run --dev ruff check astrbot\core\provider\sources\openai_embedding_source.py tests\test_openai_embedding_source.py
# All checks passed!

uv run --dev python -m py_compile astrbot\core\provider\sources\openai_embedding_source.py tests\test_openai_embedding_source.py
# passed

cd dashboard
pnpm typecheck
# vue-tsc --noEmit passed

运行 uv run 时,本地 .venv 中存在旧包 metadata 警告,uv 随后自动重新安装/修复了包环境;测试本身通过。

关于知识库多嵌入模型兼容性的说明

本 PR 只修复当前 embedding 请求参数问题,不直接改造知识库架构。但这里必须强调:使用向量维度来标记或判断知识库索引兼容性,是原理上错误的实现。

向量维度只能说明向量数组长度一致,最多只能判断它们能否被写入同一个 FAISS index。它不能说明两个 embedding 模型产生的向量处在同一个语义空间中。

两个不同 embedding 模型即使输出完全相同的维度,也可能有完全不同的坐标系、向量分布和相似度语义。把它们混用到同一个知识库向量索引中,通常不会报维度错误,但检索结果会失真。这类错误不会以“插入失败”的形式暴露,而是以更隐蔽的检索质量退化形式出现。

因此,知识库的多 embedding 模型兼容不能只依赖 embedding_dimensions。正确方向应该是按 embedding provider / model 等向量空间身份隔离索引,或者在切换 embedding 模型时触发索引重建。此前 #6394 中曾提交过更完整的 RAG / 知识库改造方案,其中包含 provider 隔离的 FAISS 索引与在线重建能力,可作为后续实现方向的参考。本 PR 先保持较小范围,只处理 dimensions 请求参数开关问题。


Checklist / 检查清单

  • 😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
    / 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。

  • 👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
    / 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”

  • 🤓 I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
    / 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到 requirements.txtpyproject.toml 文件相应位置。

  • 😮 My changes do not introduce malicious code.
    / 我的更改没有引入恶意代码。

@dosubot dosubot Bot added size:S This PR changes 10-29 lines, ignoring generated files. area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. labels Jun 2, 2026
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="tests/test_openai_embedding_source.py" line_range="27-28" />
<code_context>
+        return SimpleNamespace(data=data)
+
+
+@pytest.mark.asyncio
+async def test_openai_embedding_does_not_send_configured_dimensions():
+    provider = OpenAIEmbeddingProvider(
+        {
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test case for when `embedding_dimensions` is not configured at all.

In addition to the configured case, please add an async test where `embedding_dimensions` is omitted from `provider_config`. That test should assert that `get_dim()` uses the default dimension and that `embeddings.create` is called only with `input` and `model` (no `dimensions` or extra kwargs).

Suggested implementation:

```python
    fake_embeddings = FakeEmbeddingsClient()
    provider.client = SimpleNamespace(embeddings=fake_embeddings)


@pytest.mark.asyncio
async def test_openai_embedding_uses_default_dimensions_when_not_configured(mocker):
    provider = OpenAIEmbeddingProvider(
        {
            "id": "openai-compatible-embedding",
            "embedding_api_key": "test-key",
            "embedding_api_base": "https://example.com/v1",
            "embedding_model": "BAAI/bge-m3",
        },
        {},
    )

    # Use a mock embeddings client so we can inspect the call signature
    fake_embeddings = SimpleNamespace()
    fake_embeddings.create = mocker.AsyncMock(
        return_value=SimpleNamespace(
            data=[SimpleNamespace(embedding=[1.0, 2.0, 3.0])]
        )
    )
    provider.client = SimpleNamespace(embeddings=fake_embeddings)

    # get_dim() should fall back to the provider's default dimension
    default_dim = provider.get_dim()
    assert isinstance(default_dim, int)
    assert default_dim > 0

    # Trigger an embedding call; dimensions should not be passed explicitly
    inputs = ["hello", "world"]
    await provider.get_embeddings(inputs)

    assert fake_embeddings.create.call_count == 1
    call_args, call_kwargs = fake_embeddings.create.call_args

    # OpenAI embeddings API should only receive input and model
    # (no `dimensions` or other extra kwargs)
    assert "input" in call_kwargs
    assert "model" in call_kwargs
    assert call_kwargs["input"] == inputs
    assert call_kwargs["model"] == provider.config["embedding_model"]
    assert "dimensions" not in call_kwargs
    assert len(call_kwargs) == 2

```

To make this test compile and pass, you may need to:
1. Ensure `pytest-mock` is available so that the `mocker` fixture works, or replace `mocker.AsyncMock` with `unittest.mock.AsyncMock` and import it at the top of the file: `from unittest.mock import AsyncMock`.
2. Confirm that `OpenAIEmbeddingProvider` exposes:
   - a `get_dim()` method returning the effective embedding dimension (using a default when `embedding_dimensions` is not configured),
   - a `get_embeddings(inputs)` coroutine that ultimately calls `provider.client.embeddings.create(input=inputs, model=...)`,
   - a `config` (or similarly named) attribute containing `"embedding_model"`.
3. If the provider uses different method names (e.g. `embed_documents` or `aembed` instead of `get_embeddings`), update the test to call the correct coroutine and adjust the final assertions accordingly.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread tests/test_openai_embedding_source.py Outdated
Comment on lines +27 to +28
@pytest.mark.asyncio
async def test_openai_embedding_does_not_send_configured_dimensions():
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add a test case for when embedding_dimensions is not configured at all.

In addition to the configured case, please add an async test where embedding_dimensions is omitted from provider_config. That test should assert that get_dim() uses the default dimension and that embeddings.create is called only with input and model (no dimensions or extra kwargs).

Suggested implementation:

    fake_embeddings = FakeEmbeddingsClient()
    provider.client = SimpleNamespace(embeddings=fake_embeddings)


@pytest.mark.asyncio
async def test_openai_embedding_uses_default_dimensions_when_not_configured(mocker):
    provider = OpenAIEmbeddingProvider(
        {
            "id": "openai-compatible-embedding",
            "embedding_api_key": "test-key",
            "embedding_api_base": "https://example.com/v1",
            "embedding_model": "BAAI/bge-m3",
        },
        {},
    )

    # Use a mock embeddings client so we can inspect the call signature
    fake_embeddings = SimpleNamespace()
    fake_embeddings.create = mocker.AsyncMock(
        return_value=SimpleNamespace(
            data=[SimpleNamespace(embedding=[1.0, 2.0, 3.0])]
        )
    )
    provider.client = SimpleNamespace(embeddings=fake_embeddings)

    # get_dim() should fall back to the provider's default dimension
    default_dim = provider.get_dim()
    assert isinstance(default_dim, int)
    assert default_dim > 0

    # Trigger an embedding call; dimensions should not be passed explicitly
    inputs = ["hello", "world"]
    await provider.get_embeddings(inputs)

    assert fake_embeddings.create.call_count == 1
    call_args, call_kwargs = fake_embeddings.create.call_args

    # OpenAI embeddings API should only receive input and model
    # (no `dimensions` or other extra kwargs)
    assert "input" in call_kwargs
    assert "model" in call_kwargs
    assert call_kwargs["input"] == inputs
    assert call_kwargs["model"] == provider.config["embedding_model"]
    assert "dimensions" not in call_kwargs
    assert len(call_kwargs) == 2

To make this test compile and pass, you may need to:

  1. Ensure pytest-mock is available so that the mocker fixture works, or replace mocker.AsyncMock with unittest.mock.AsyncMock and import it at the top of the file: from unittest.mock import AsyncMock.
  2. Confirm that OpenAIEmbeddingProvider exposes:
    • a get_dim() method returning the effective embedding dimension (using a default when embedding_dimensions is not configured),
    • a get_embeddings(inputs) coroutine that ultimately calls provider.client.embeddings.create(input=inputs, model=...),
    • a config (or similarly named) attribute containing "embedding_model".
  3. If the provider uses different method names (e.g. embed_documents or aembed instead of get_embeddings), update the test to call the correct coroutine and adjust the final assertions accordingly.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request removes the dimensions parameter from OpenAI embedding requests to prevent errors with providers that do not support it, and adds a test suite for this behavior. However, completely removing this parameter breaks support for shortened embeddings in OpenAI's text-embedding-3 models, which can cause dimension mismatches and crashes in vector databases. The feedback suggests conditionally sending the dimensions parameter only for text-embedding-3 models when a shortened dimension is configured, and updating the tests to cover both scenarios.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines 45 to 60
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]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Completely removing the dimensions parameter breaks the ability to use shortened embeddings for OpenAI's text-embedding-3 models (e.g., requesting 512 dimensions instead of 1536).

If a user configures embedding_dimensions to 512 for text-embedding-3-small, the API will now return the default 1536 dimensions because the parameter is not sent. However, get_dim() will still return 512. This mismatch will cause a crash (e.g., dimension mismatch) when inserting the 1536-dimensional vector into a FAISS index or vector database configured for 512 dimensions.

To fix this, we should keep the dimensions parameter but only send it when the model is text-embedding-3 and the configured dimension is explicitly smaller than the default (e.g., < 1536 for small, < 3072 for large). For other models/providers (like bge-m3 or text-embedding-ada-002), the configured dimension matches the model's natural dimension, so we do not send the dimensions parameter, avoiding the 400 errors.

    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:
        """构建嵌入请求的可选参数"""
        kwargs = {}
        if "embedding_dimensions" in self.provider_config:
            try:
                dims = int(self.provider_config["embedding_dimensions"])
                if "text-embedding-3-small" in self.model and dims < 1536:
                    kwargs["dimensions"] = dims
                elif "text-embedding-3-large" in self.model and dims < 3072:
                    kwargs["dimensions"] = dims
            except (ValueError, TypeError):
                pass
        return kwargs

Comment on lines +27 to +51
@pytest.mark.asyncio
async def test_openai_embedding_does_not_send_configured_dimensions():
provider = OpenAIEmbeddingProvider(
{
"id": "openai-compatible-embedding",
"embedding_api_key": "test-key",
"embedding_api_base": "https://example.com/v1",
"embedding_model": "BAAI/bge-m3",
"embedding_dimensions": 1024,
},
{},
)
fake_embeddings = FakeEmbeddingsClient()
provider.client = SimpleNamespace(embeddings=fake_embeddings)

embedding = await provider.get_embedding("hello")
embeddings = await provider.get_embeddings(["hello", "world"])

assert embedding == [1.0, 2.0, 3.0]
assert embeddings == [[0.0, 0.0, 1.0], [1.0, 0.0, 1.0]]
assert provider.get_dim() == 1024
assert fake_embeddings.calls == [
{"input": "hello", "model": "BAAI/bge-m3"},
{"input": ["hello", "world"], "model": "BAAI/bge-m3"},
]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Let's update the test to cover both cases:

  1. Non-OpenAI models or default dimensions (where dimensions should NOT be sent).
  2. OpenAI text-embedding-3 models with shortened dimensions (where dimensions SHOULD be sent).
@pytest.mark.asyncio
async def test_openai_embedding_dimensions_handling():
    # Case 1: Non-OpenAI model or default dimension -> should NOT send dimensions parameter
    provider_non_openai = OpenAIEmbeddingProvider(
        {
            "id": "openai-compatible-embedding",
            "embedding_api_key": "test-key",
            "embedding_api_base": "https://example.com/v1",
            "embedding_model": "BAAI/bge-m3",
            "embedding_dimensions": 1024,
        },
        {},
    )
    fake_embeddings_1 = FakeEmbeddingsClient()
    provider_non_openai.client = SimpleNamespace(embeddings=fake_embeddings_1)

    embedding = await provider_non_openai.get_embedding("hello")
    embeddings = await provider_non_openai.get_embeddings(["hello", "world"])

    assert embedding == [1.0, 2.0, 3.0]
    assert embeddings == [[0.0, 0.0, 1.0], [1.0, 0.0, 1.0]]
    assert provider_non_openai.get_dim() == 1024
    assert fake_embeddings_1.calls == [
        {"input": "hello", "model": "BAAI/bge-m3"},
        {"input": ["hello", "world"], "model": "BAAI/bge-m3"},
    ]

    # Case 2: OpenAI text-embedding-3 model with shortened dimension -> SHOULD send dimensions parameter
    provider_openai = OpenAIEmbeddingProvider(
        {
            "id": "openai-embedding",
            "embedding_api_key": "test-key",
            "embedding_api_base": "https://api.openai.com/v1",
            "embedding_model": "text-embedding-3-small",
            "embedding_dimensions": 512,
        },
        {},
    )
    fake_embeddings_2 = FakeEmbeddingsClient()
    provider_openai.client = SimpleNamespace(embeddings=fake_embeddings_2)

    await provider_openai.get_embedding("hello")
    assert fake_embeddings_2.calls == [
        {"input": "hello", "model": "text-embedding-3-small", "dimensions": 512},
    ]

@kawayiYokami kawayiYokami force-pushed the fix/embedding-dimensions-param branch from 60cbeaf to 857dc7f Compare June 2, 2026 17:40
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:S This PR changes 10-29 lines, ignoring generated files. labels Jun 2, 2026
@kawayiYokami kawayiYokami changed the title fix(openai-embedding): stop sending dimensions parameter fix(openai-embedding): add dimensions request parameter toggle Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant