Skip to content

fix: UniversalAPIEmbedder now passes embedding_dims to API calls - #2180

Merged
endxxxx merged 7 commits into
MemTensor:dev-v2.0.30from
RerankerGuo:fix/issue-2177-embedding-dims
Aug 13, 2026
Merged

fix: UniversalAPIEmbedder now passes embedding_dims to API calls#2180
endxxxx merged 7 commits into
MemTensor:dev-v2.0.30from
RerankerGuo:fix/issue-2177-embedding-dims

Conversation

@RerankerGuo

Copy link
Copy Markdown
Contributor

Description

Fixes #2177

The UniversalAPIEmbedder previously silently ignored the embedding_dims config field when making embeddings.create() calls. This caused models like text-embedding-3-large to always return the full default dimension embedding (e.g. 3072), making it impossible to use the dimensions parameter for reduced-dimensional embeddings.

Changes

  1. Added _build_embedding_kwargs() helper — conditionally includes the dimensions parameter when embedding_dims is set in config
  2. Extracted _call_embeddings_api() method — handles both primary and backup client paths with unified dimension support
  3. Added graceful fallback — if the API rejects the dimensions parameter (e.g. older model versions or non-Ollama providers), automatically retries without it
  4. Both primary and backup client paths now use the same dimensions-aware calling logic
  5. Added comprehensive unit tests in tests/embedders/test_universal_api.py

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Refactor (improved code structure via helper extraction)

How Has This Been Tested?

  • python3 -m py_compile src/memos/embedders/universal_api.py passes
  • python3 -m py_compile tests/embedders/test_universal_api.py passes
  • 5 logic tests for _build_embedding_kwargs (no dims / with dims / zero dims / empty list / batch)
  • Fallback behavior verified: when dimensions not supported, auto-retry without
  • No behavior change when embedding_dims=None (backward compatible)

Checklist

@Memtensor-AI Memtensor-AI added area:model llm + embedder + reranker status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Jul 28, 2026
@Memtensor-AI
Memtensor-AI requested a review from endxxxx July 28, 2026 15:07
@Memtensor-AI

Memtensor-AI commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

🤖 Open Code Review

Target: PR #2180
Task: 889b4a327d218f71
Base: dev-v2.0.30
Head: fix/issue-2177-embedding-dims

🔍 OpenCodeReview found 3 issue(s) in this PR.


1. src/memos/api/config.py (L625)

Two issues here:

  1. Behavioral regression — wrong default value: BaseEmbedderConfig.embedding_dims has a schema default of None, which tells the embedder not to pass a dimensions parameter and to use the model's native output size. Hard-coding 1024 as the fallback means that every deployment that does not explicitly set EMBEDDING_DIMENSION will silently request 1024-dimensional embeddings, regardless of whether the chosen model supports Matryoshka truncation. This will cause a BadRequestError for models that don't support it, or silently change the embedding space for models that do — both are breaking for existing deployments. Use None (or omit the key) when the env var is absent so the schema default is respected.

  2. Inconsistent env-var naming: All other env vars in this file follow the MOS_ prefix convention (e.g. MOS_EMBEDDER_MODEL, MOS_EMBEDDER_API_KEY). EMBEDDING_DIMENSION breaks that convention and makes the variable harder to discover. Rename it to MOS_EMBEDDER_EMBEDDING_DIMS (or similar).

💡 Suggested Change

Before:

                    "embedding_dims": int(os.getenv("EMBEDDING_DIMENSION", "1024")),

After:

                    "embedding_dims": int(os.getenv("MOS_EMBEDDER_EMBEDDING_DIMS")) if os.getenv("MOS_EMBEDDER_EMBEDDING_DIMS") else None,

2. src/memos/embedders/universal_api.py (L90)

This marker is an incomplete phrase that could match benign provider messages such as "changing output dimensions will lead to different results" or "changing output dimensions will lead to better performance", causing a false positive that silently drops the user-configured dimensions parameter.

Either use the full, unambiguous phrase that actually appears in known provider error responses, or remove this marker if no concrete provider response has been confirmed.

💡 Suggested Change

Before:

            "changing output dimensions will lead",

After:

            "changing output dimensions is unsupported",

3. src/memos/embedders/universal_api.py (L126-L131)

The broad except Exception catches every exception from _call_embeddings_api, including non-dimensions-related BadRequestErrors (e.g., invalid model name, malformed input). This means a misconfiguration error on the primary client silently triggers the backup client path, potentially masking real errors and causing confusing behavior.

Consider excluding BadRequestError from the outer handler (let it propagate directly), since _call_embeddings_api already handles the dimensions-related BadRequestError case internally. Only transient/network failures should trigger the backup client.

💡 Suggested Change

Before:

            except Exception as e:
                if self.use_backup_client:
                    logger.warning(
                        "Embedding request failed error_type=%s; trying backup client",
                        type(e).__name__,
                    )

After:

            except (BadRequestError, Exception) as e:
                # Only fall back for non-BadRequestError failures; BadRequestError
                # indicates a client configuration problem that the backup is unlikely to fix.
                if isinstance(e, BadRequestError):
                    raise ValueError(f"Embeddings request ended with error: {e}") from e
                if self.use_backup_client:
                    logger.warning(
                        "Embedding request failed error_type=%s; trying backup client",
                        type(e).__name__,
                    )

Generated by cloud-assistant via Open Code Review.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

⚠️ Automated Test Results: INCONCLUSIVE

Automated tests inconclusive (auto-generated test defect); treated as non-blocking. Manual review recommended. Details: The AI-generated tests mock client.embeddings.create with a synchronous function/exception, but the SUT wraps the call in asyncio.run(asyncio.wait_for(client.embeddings.create(...), ...)), which requires create to return an awaitable coroutine. [advisory, non-gating] AI-generated tests on branch test/auto-gen-f4fd7aa1f5c1ae47-20260728232248: 66/67 passed, 1 failed — these do NOT affect the PR verdict; review the branch manually.

Branch: fix/issue-2177-embedding-dims

@RerankerGuo
RerankerGuo force-pushed the fix/issue-2177-embedding-dims branch from 6bc3968 to 9f25415 Compare July 30, 2026 00:54
@Memtensor-AI

Copy link
Copy Markdown
Collaborator

⚠️ Automated Test Results: INCONCLUSIVE

Automated tests inconclusive (auto-generated test defect); treated as non-blocking. Manual review recommended. Details: The tests mock client.embeddings.create with a synchronous function, but the code under test wraps the call in asyncio.run(asyncio.wait_for(...)), which requires an awaitable/coroutine. The tests fail to model the async contract that _call_embeddings_api expects. [advisory, non-gating] AI-generated tests on branch test/auto-gen-863b005ada4494c7-20260730092619: 67/92 passed, 25 failed — these do NOT affect the PR verdict; review the branch manually.

Branch: fix/issue-2177-embedding-dims

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

⚠️ Automated Test Results: INCONCLUSIVE

Automated tests inconclusive (auto-generated test defect); treated as non-blocking. Manual review recommended. Details: All three failing tests crash inside asyncio.wait_for/ensure_future because the mocked client.embeddings.create(...) returns a plain MagicMock instead of an awaitable, so the asyncio machinery cannot schedule it as a future. [advisory, non-gating] AI-generated tests on branch test/auto-gen-1a47268566676cdb-20260731102051: 85/86 passed, 1 failed — these do NOT affect the PR verdict; review the branch manually.

Branch: fix/issue-2177-embedding-dims

Closes MemTensor#2177

The UniversalAPIEmbedder previously silently ignored the
embedding_dims config field when making embeddings.create()
calls. This caused models like text-embedding-3-large to always
return the full default dimension embedding, making it impossible
to use the dimensions parameter for reduced-dimensional embeddings.

Changes:
- Added _build_embedding_kwargs() helper that conditionally
  includes the 'dimensions' parameter when embedding_dims is set
- Extracted _call_embeddings_api() method that handles both
  primary and backup client paths with unified dimension support
- Added graceful fallback: if the API rejects the dimensions
  parameter (e.g. older model versions), automatically retries
  without it
- Both primary and backup client paths now use the same
  dimensions-aware calling logic
- Added comprehensive unit tests in test_universal_api.py

Test: python3 -m py_compile src/memos/embedders/universal_api.py
Test: python3 -m py_compile tests/embedders/test_universal_api.py
…or can await it

UniversalAPIEmbedder now awaits client.embeddings.create() via asyncio.wait_for,
so the mock must be an async function (AsyncMock) rather than a plain
MagicMock. Introduce _awaitable_response/_mock_embedding_response helpers
and patch embeddings.create at the instance level instead of relying on
the default MagicMock return-value.
@RerankerGuo
RerankerGuo force-pushed the fix/issue-2177-embedding-dims branch from dc3f4f4 to b98b8e0 Compare August 5, 2026 05:26
@Memtensor-AI

Copy link
Copy Markdown
Collaborator

✅ Automated Test Results: PASSED

All tests passed (9/9 executed). memos_python_core/changed-repo-python: 9/9. Duration: 5s

Branch: fix/issue-2177-embedding-dims

@Memtensor-AI Memtensor-AI added status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 and removed status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Aug 5, 2026
@endxxxx

endxxxx commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Hi @RerankerGuo , thanks for working on the embedding_dims issue—the intention of this PR is correct, but I found a blocking regression during local end-to-end testing.

UniversalAPIEmbedder imports the synchronous openai.OpenAI and AzureOpenAI clients. Therefore, client.embeddings.create() returns a normal CreateEmbeddingResponse, not a coroutine. The new implementation passes that synchronous response into asyncio.wait_for(), which causes every successful real request to fail with:

TypeError: An asyncio.Future, a coroutine or an awaitable is required

The current unit tests do not expose this because they replace embeddings.create() with AsyncMock, which does not match the actual SDK contract. In a local MemOS API test, /product/search still returned HTTP 200, but embedding retrieval failed internally and the API silently returned empty results.

There are two additional concerns:

The fallback currently catches every Exception. Timeouts, authentication failures, rate limits, and unrelated bad requests will all trigger another request without dimensions. The fallback should only handle a BadRequestError that explicitly indicates the provider/model does not support the dimensions parameter.

APIConfig.get_embedder_config() does not map EMBEDDING_DIMENSION to UniversalAPIEmbedderConfig.embedding_dims. As a result, the normal API service path configures the vector database dimension but does not pass the same dimension to the embedder.

I suggest keeping the client synchronous and using the OpenAI SDK’s request-level timeout:

response = client.embeddings.create(
    **kwargs,
    timeout=timeout,
)

Then catch only the relevant BadRequestError, retry without dimensions only when the error clearly says the parameter or matryoshka dimensions are unsupported, and update the tests to use synchronous MagicMock. It would also be helpful to add embedding_dims=int(os.getenv("EMBEDDING_DIMENSION", "1024")) to the API embedder configuration.

After applying these changes locally, I verified the complete flow with an OpenAI-compatible test service: the first request included dimensions=1024, received an explicit unsupported-dimensions 400, retried once without dimensions, returned a 1024-dimensional vector, and /product/search completed without the previous awaitable error.

@Memtensor-AI Memtensor-AI added area:api 云服务 / FastAPI / OpenAPI / MCP status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 and removed status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 labels Aug 11, 2026
@Memtensor-AI

Copy link
Copy Markdown
Collaborator

✅ Automated Test Results: PASSED

All tests passed (12/12 executed). memos_python_core/changed-repo-python: 12/12. Duration: 5s [advisory, non-gating] AI-generated tests on branch test/auto-gen-026d301585fcc0e3-20260811145635: 118/119 passed, 1 failed — these do NOT affect the PR verdict; review the branch manually.

Branch: fix/issue-2177-embedding-dims

Recognize provider errors that identify an unsupported dimensions parameter through structured param/code fields while preserving invalid-value failures. Add regression coverage for structured fallback and API dimension configuration.

Test: .venv/bin/python -m pytest tests/embedders/test_universal_api.py -q\nTest: .venv/bin/python -m pytest tests/api -q\nTest: .venv/bin/python -m pytest tests/embedders -q --ignore=tests/embedders/test_ark.py
@Memtensor-AI Memtensor-AI added area:core MOS 编排层 / 框架底座 / 跨模块问题 and removed area:api 云服务 / FastAPI / OpenAPI / MCP labels Aug 12, 2026
@Memtensor-AI Memtensor-AI removed the area:model llm + embedder + reranker label Aug 12, 2026
@Memtensor-AI
Memtensor-AI requested a review from WeiminLee August 12, 2026 02:00
@RerankerGuo

RerankerGuo commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @endxxxx for the concrete reproduction and for pushing 0f5ed1b.

I kept that synchronous-client fix and added the remaining structured-error coverage in 88a515aa:

  • OpenAI / AzureOpenAI embedding calls stay synchronous and use the SDK request-level timeout;
  • retry without dimensions remains limited to BadRequestError;
  • structured flat or nested error bodies now recognize param=dimensions with unsupported/unknown codes even when the message is only Bad request;
  • invalid dimension values still propagate without a fallback request;
  • the API configuration mapping from EMBEDDING_DIMENSION to embedding_dims now has regression coverage.

Verification:

  • .venv/bin/python -m pytest tests/embedders/test_universal_api.py -q -> 14 passed
  • .venv/bin/python -m pytest tests/api -q -> 112 passed
  • .venv/bin/python -m pytest tests/embedders -q --ignore=tests/embedders/test_ark.py -> 30 passed
  • Ruff check and format check pass for all three PR files
  • structured probe: unsupported_parameter=True, invalid_value=False

CI update for run 31555490763:

  • all 8 macOS and all 4 Windows jobs passed;
  • all 4 Ubuntu jobs stopped before build, Ruff, or tests in scripts/check_dependencies.py on the unchanged src/memos/embedders/cache.py top-level cachetools import;
  • this PR still changes only src/memos/api/config.py, src/memos/embedders/universal_api.py, and tests/embedders/test_universal_api.py; cache.py, pyproject.toml, poetry.lock, and the dependency checker are unchanged from main;
  • Open Code Review passed with one non-blocking finding.

I have kept the unrelated core/optional dependency issue out of this PR.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

✅ Automated Test Results: PASSED

All tests passed (14/14 executed). memos_python_core/changed-repo-python: 14/14. Duration: 6s

Branch: fix/issue-2177-embedding-dims

endxxxx added a commit that referenced this pull request Aug 13, 2026
## Description

This PR promotes `cachetools` from the `all` optional extra to a core
MemoryOS dependency.

The embedding cache implementation imports `cachetools` at module scope
and is part of the core embedder workflow. However, `cachetools` was
previously installed only through the `all` extra. As a result, minimal
installations using only the main dependency group failed the dependency
validation step with:

```text
src/memos/embedders/cache.py: Top-level import of unavailable module 'cachetools'
```

This change:

- Adds cachetools>=6.0.0 to the main project dependencies.
- Removes the duplicate declaration from the all optional dependencies.
- Updates poetry.lock without upgrading the existing locked cachetools
version (6.2.1).
- Ensures standard MemoryOS installations include the dependency
required by the embedding cache.

No runtime source code or public API behavior is changed.

Related context: CI failure discovered while validating #2180 

## Type of change

Please delete options that are not relevant.

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Refactor (does not change functionality, e.g. code style
improvements, linting)
- [ ] Documentation update

## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration

- [x] Unit Test
- [ ] Test Script Or Test Steps (please provide)
- [ ] Pipeline Automated API Test (please provide)

## Checklist

- [x] I have performed a self-review of my own code | 我已自行检查了自己的代码
- [x] I have commented my code in hard-to-understand areas |
我已在难以理解的地方对代码进行了注释
- [x] I have added tests that prove my fix is effective or that my
feature works | 我已添加测试以证明我的修复有效或功能正常
- [ ] I have created related documentation issue/PR in
[MemOS-Docs](https://github.com/MemTensor/MemOS-Docs) (if applicable) |
我已在 [MemOS-Docs](https://github.com/MemTensor/MemOS-Docs) 中创建了相关的文档
issue/PR(如果适用)
- [ ] I have linked the issue to this PR (if applicable) | 我已将 issue
链接到此 PR(如果适用)
- [ ] I have mentioned the person who will review this PR | 我已提及将审查此 PR
的人

## Reviewer Checklist
- [ ] closes #xxxx (Replace xxxx with the GitHub issue number)
- [ ] Made sure Checks passed
- [x] Tests have been provided
@endxxxx
endxxxx changed the base branch from main to dev-v2.0.30 August 13, 2026 08:34
@Memtensor-AI Memtensor-AI added area:api 云服务 / FastAPI / OpenAPI / MCP area:model llm + embedder + reranker and removed area:core MOS 编排层 / 框架底座 / 跨模块问题 labels Aug 13, 2026
@Memtensor-AI

Copy link
Copy Markdown
Collaborator

✅ Automated Test Results: PASSED

All tests passed (14/14 executed). memos_python_core/changed-repo-python: 14/14. Duration: 5s [advisory, non-gating] AI-generated tests on branch test/auto-gen-889b4a327d218f71-20260813163910: 344/345 passed, 1 failed — these do NOT affect the PR verdict; review the branch manually.

Branch: fix/issue-2177-embedding-dims

@Memtensor-AI Memtensor-AI added status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 and removed status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Aug 13, 2026
@endxxxx
endxxxx merged commit 25d2c9d into MemTensor:dev-v2.0.30 Aug 13, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:api 云服务 / FastAPI / OpenAPI / MCP area:model llm + embedder + reranker status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: UniversalAPIEmbedder silently ignores embedding_dims and never passes dimensions to the OpenAI API

6 participants