fix: keep knowledge base uploads working when the embedding service caps inputs - #10110
Mola-maker wants to merge 2 commits into
Conversation
…aps inputs
Uploading a document could fail with
存储失败:文本块已生成,但写入知识库索引时出错。
even though nothing was wrong with storage. The dashboard always requests
`batch_size=32`, and DashScope text-embedding-v3/v4 reject any request carrying
more than 10 inputs with a deterministic HTTP 400 ("batch size is invalid, it
should not be larger than 10."). Retrying the identical request can never
succeed, so every batch burned its 3 retries and the whole upload aborted with
0 documents stored.
Three changes:
- `EmbeddingProvider.get_max_batch_size()` declares the per-request input limit
of a provider. `get_embeddings_batch` clamps the caller's `batch_size` to it,
so the doomed request is never built. The DashScope adapter declares the
per-model limit (v1/v2: 25, v3/v4: 10, multimodal: conservative 10); the
OpenAI-compatible adapter infers it from the base URL host, the same way it
already infers `embedding_dimensions`, because DashScope is commonly reached
through its OpenAI-compatible endpoint. Any other gateway can declare
`embedding_max_batch_items` in its provider config.
- A declared limit cannot cover providers whose limit the adapter cannot know,
so `get_embeddings_batch` also recovers at runtime: a rejection that blames
the number of inputs splits the batch and retries the pieces (honouring a
limit the provider stated in the message, halving otherwise) instead of
re-sending the same oversized request. Only narrow, status-code-gated
patterns qualify, so token limits, rate limits and auth failures keep their
existing behaviour. A limit learned from a stated value is reused for the
rest of the upload and for later ones.
- `FaissVecDB.insert_batch` now wraps the provider call as
`KnowledgeBaseUploadError(stage="embedding", ...)`. Previously the raw
provider error escaped unlabelled and `kb_helper` relabelled it as a storage
failure, which is where the misleading message in the report came from.
Fixes AstrBotDevs#10109
Co-Authored-By: Claude Code <noreply@anthropic.com>
There was a problem hiding this comment.
Hey - I've found 1 issue
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="astrbot/core/db/vec_db/faiss_impl/vec_db.py" line_range="142-149" />
<code_context>
+ # Attach the embedding stage here: otherwise the raw provider error
+ # escapes unlabelled and gets reported downstream as a storage
+ # failure, hiding the real cause (see kb_helper's stage mapping).
+ cause = str(exc).strip() or type(exc).__name__
+ if len(cause) > 300:
+ cause = cause[:300] + "…"
+ raise KnowledgeBaseUploadError(
+ stage="embedding",
+ user_message=f"向量化失败:调用嵌入模型时出错。原因:{cause}",
+ details={
+ "cause": str(exc),
+ "error_type": type(exc).__name__,
+ "provider": type(self.embedding_provider).__name__,
</code_context>
<issue_to_address>
**🚨 issue (security):** The raw embedding-provider exception is copied into both the user-facing `user_message` and `details["cause"]` without redaction. Provider authentication errors can include the API key, as shown by the reported `Incorrect API key provided: sk-xxx` output, so credentials are exposed in upload logs or returned error data.
**Triggers:** When an embedding service includes credentials or other sensitive request data in its exception text.
**Suggested fix:** Sanitize provider exception text before putting it in `user_message` or `details`, and preserve the full exception only in a properly protected diagnostic log.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 1 finding to address first, and if the new batching or error-classification logic is wrong, an upload could fail after some vectors or documents have already been persisted, leaving a bounded partial knowledge base that needs cleanup or a rerun; reverting will not remove those records. The change does not move money or expose access, and the remaining impact is normally repairable.
Blocking findings: astrbot/core/db/vec_db/faiss_impl/vec_db.py:149
The embedding failure path copies the provider's error text into the
`user_message` and `details` of the raised `KnowledgeBaseUploadError`, and both
reach the upload log and the dashboard's failure list. Providers quote the
request they refused, API key included:
向量化失败:调用嵌入模型时出错。原因:HTTP 401: Incorrect API key provided: sk-proj-...
Run the text through the existing `redact_sensitive_text` before attaching it,
in `FaissVecDB.insert_batch` and in kb_helper's storage-stage handler, which
carries the same `cause` through the same log line. Non-secret context is kept,
so the message stays actionable.
`redact_sensitive_text` could not catch the reported shape: its `sk-` pattern
only accepted an alphanumeric body, so dash-segmented vendor keys
(`sk-proj-...`, `sk-ant-api03-...`) passed through untouched. Extend it, and let
the named-credential pattern accept a quoted value (`api_key="..."`), which the
`=`-style form previously missed.
Tests: `tests/unit/test_error_redaction.py` for the redactor, plus assertions
that a key-bearing provider error is redacted through `insert_batch` and through
`upload_document`'s storage stage.
Co-Authored-By: Claude Code <noreply@anthropic.com>
|
Valid — and the fix uncovered a gap in the redactor we already have for this.
Both sites that carry the provider's text are now redacted:
The full exception still travels on the Verified end to end — the same script that previously printed the raw key now prints:
|
Problem
Uploading a document to a knowledge base could fail with the message quoted in #10109:
Nothing is wrong with storage. The dashboard always sends
batch_size=32(DocumentsTab.vue→knowledge_base_service.py), while DashScopetext-embedding-v3/v4— one of AstrBot's default embedding templates — reject any request carrying more than 10 inputs:That error is deterministic, so the retry loop in
EmbeddingProvider.get_embeddings_batchcan never recover: each batch spends its 3 retries, the upload aborts, and the knowledge base ends up withdoc_count=0 chunk_count=0.Because the upload runs as a background task, the browser never sees an HTTP error — only the misleading log line, which is what made this hard to diagnose.
Reproduction (before)
Driving the real
KBHelper.upload_documentpath with a stub embedding backend that emulates DashScope's 10-input cap, at the dashboard'sbatch_size=32:Fix
Three layers, all on the provider request boundary.
1. Declared per-request input limit.
EmbeddingProvider.get_max_batch_size()(defaultNone= unknown) is consulted once inget_embeddings_batch, and the caller'sbatch_sizeis clamped to it — the doomed request is never built, at zero cost.DashScopeEmbeddingProviderdeclares the per-model limit (v1/v2 → 25, v3/v4 → 10, multimodal → a conservative 10).OpenAIEmbeddingProviderinfers it from the configured base URL host, the same way it already infersembedding_dimensions, since DashScope is commonly reached through its OpenAI-compatible endpoint. Any other gateway whose limit the adapter cannot know can setembedding_max_batch_itemsin its provider config (schema entry added; combined withmin(), so config can only lower a known limit).2. Adaptive splitting as the runtime safety net. A declared limit is only as good as the table behind it, so a rejection that actually blames the number of inputs is handled where it happens: the batch is split and its pieces retried, instead of re-sending the same oversized request. When the provider states a limit (
should not be larger than 10), that number is used exactly — 32 inputs become10/10/10/2rather than repeated halving — and cached on the provider for the rest of the upload and later ones. When it does not, the batch halves. Splitting is triggered only when an HTTP status in{400, 413, 414, 422}(or no status at all) agrees with a narrow size-semantics pattern and no token/context/rate-limit pattern is present, so unrelated failures keep their existing behaviour. Deterministic 4xx failures also no longer burn three exponential-backoff retries. Result ordering, the concurrency semaphore, the progress callback and the aggregate有 N 个批次处理失败error are unchanged.3. Honest error attribution.
FaissVecDB.insert_batchcalled the provider outside anytry, so the raw error escaped unlabelled andkb_helperrelabelled it as a storage failure — the source of the reported message. That call is now wrapped asKnowledgeBaseUploadError(stage="embedding", ...)with the real cause, so any residual embedding failure (bad key, unknown model, network) is legible instead of being blamed on storage.Reproduction (after)
Same script, same
batch_size=32, same 10-input cap:And a provider that fails for an unrelated reason now reports the real one instead of the storage message:
Credential redaction
Carrying the provider's text through the new
stage="embedding"error also meant carrying whatever the provider quoted from the request, API key included — into the upload log (kb_helperlogsdetails) and into the dashboard's failure list. The text now goes through the existingredact_sensitive_textat both sites that carry it (vec_db.py, and kb_helper's storage-stage handler, which passes the samecausethrough the same log line). Non-secret context is kept, so the message stays actionable;details["cause"]stays complete, onlyuser_messageis bounded to 300 chars.That redactor had a gap this exposed: its
sk-pattern only accepted an alphanumeric body, so dash-segmented vendor keys (sk-proj-...,sk-ant-api03-...) passed through untouched. It is widened here, and its named-credential pattern now also accepts a quoted value (api_key="..."), which the=-style form previously missed. Short stand-ins such assk-testare still left readable on purpose.Tests
New
tests/unit/test_embedding_batch_split.py(37 cases): declared-clamp and config-declared limits, split-and-retry with order preservation, exact use of a stated limit, halving when none is stated, learned-limit reuse across calls, a limit learned without a stated value not being persisted, retryable failures still retried, a single rejected text surfacing the provider error, classifier false-positive exclusions (context length,max_tokens, rate limit, auth, 5xx), a realopenai.BadRequestErrorshape, per-model DashScope limits, host sniffing, the OpenAI-compatible DashScope endpoint end-to-end, an end-to-endFaissVecDB.insert_batchof 40 chunks against a 10-input provider, and redaction of a key-bearing provider error.New
tests/unit/test_error_redaction.py(18 cases) covers the redactor: the vendor key formats, named credentials in JSON/query/header form,safe_error, and short placeholders deliberately left alone.Verification:
pytest tests/unitandpytest tests --ignore=tests/unit --ignore=tests/integration— 1325 + 2041 passed. The 11 failures in those runs (grep tool limits, dashboard config update, KOOK client, file-URI helpers, ...) reproduce identically on a cleanHEADworktree and are unrelated to this change.ruff format --check .andruff check .— clean.Not in this PR
The dashboard discards the per-file reason the server already returns in
result.failed[].error(DocumentsTab.vue→ only counts are shown), so a failed upload still surfaces no reason in the UI. Making that visible means a new UI surface plus new locale keys in four languages, so it belongs in a separate UX PR.Fixes #10109
🤖 Generated with Claude Code
Summary by Sourcery
Prevent knowledge-base upload failures caused by embedding providers that cap the number of inputs per request.
New Features:
embedding_max_batch_itemsconfiguration option.Bug Fixes:
Enhancements:
Tests: