Skip to content

fix(provider): salvage partial batches when an endpoint rejects one item - #11

Open
dearbld wants to merge 3 commits into
JohnXu22786:mainfrom
dearbld:pr-4
Open

dearbld wants to merge 3 commits into
JohnXu22786:mainfrom
dearbld:pr-4

Conversation

@dearbld

@dearbld dearbld commented Sep 9, 2026

Copy link
Copy Markdown

Salvage partial batches when an endpoint rejects one item

Base: on top of pr-2 · stacked series: pr-1 → pr-2 → this PR → pr-5 → pr-3

Motivation

OpenAICompatProvider.embed sends chunks in batches of 32. When the endpoint answers HTTP 400 with a parameter-error body, the whole embed call throws — but such a 400 is frequently caused by one rejected item (an oversized or otherwise invalid chunk), not the batch shape. The blast radius is worst at boot: the first failed build plus allowFallback permanently pins the index to the local lexical provider, even though 31/32 items were perfectly embeddable.

What changes

On HTTP 400 with a multi-item batch:

  1. Retry the batch item by item (input: [one]).
  2. Good items keep their embeddings; the dimension is learned from the first good single response when dimension: 0 (inferred).
  3. Rejected items become zero-vector Float32Array placeholders so row alignment is preserved — search still ranks them through the lexical channel. Float32Array specifically: the persistence layer calls vec.subarray, which a plain Array does not implement (a plain-Array placeholder crashed persistence and lost the whole build in production).
  4. If every item fails, the original batch error is kept (no behavior change for genuinely bad requests).

Per-item retries share the batch's timeoutMs budget via AbortSignal.any — without a per-item cap, a slow endpoint could hang each of the 32 retries indefinitely.

Before / after

// before: one bad chunk kills the batch and (with allowFallback) the index quality forever
if (!response.ok) throw new EmbeddingError(`... HTTP ${response.status} ...`)

// after: 400 on a multi-item batch → per-item salvage, zero-vector placeholders, original error only if all fail

Testing

  • tsc --noEmit clean; full suite 82/82 pass, including two new mock-server tests:
    • batch of 3 with 1 rejected item → 3 rows returned, placeholder is [0,0,0], good rows non-zero, request sequence 3 → 1 → 1 → 1;
    • every item rejected → original HTTP 400 error preserved.
  • Suggested: replay against a real endpoint that returns code 1210 on token-limit overruns.

…ction tool

Collapse the three flat tool registrations into a single
sema(action=search|stats|reindex) tool. The execute and render logic of
the three former tools is kept verbatim; only the tool surface changes,
so three per-round schema payloads become one. The open output schema
root records the producing sub-tool via __act so render dispatches; an
explicit additionalProperties value is required by the dsh schema
compiler.

【中文】将 sema_search/sema_reindex/sema_stats 三件平铺工具聚合为
sema(action=search|stats|reindex) 单件。三件原工具的 execute 与
render 逻辑逐字保留,仅收敛工具面——每轮注入 schema 从三件降为
一件。输出 schema 根保持开放(additionalProperties 显式声明——
dsh schema 编译器拒省略态),内部 __act 字段记录产出子工具供
render 分发。
The 16000-char default assumes roughly 0.25 tokens per character, which
holds for English but not for CJK text (close to one token per
character). A default-sized CJK chunk exceeds embedding endpoints'
per-item token limit, every batch is rejected with HTTP 400, and the
index silently degrades to the local lexical provider. Lower the
default to 3000 chars, which keeps CJK chunks within typical per-item
limits at a small cost for ASCII-heavy corpora.

【中文】默认 maxCharsPerText=16000 按英文 token 比标定;中文接近
1 字≈1 token,默认尺寸中文 chunk 超出 embedding 端点单条 token
上限——整批 400、allowFallback 下静默永久降级为本地 lexical。
默认降为 3000 字符:中文安全,英文语料损失小。
A batch-level HTTP 400 (parameter-error family) can be triggered by a
single rejected chunk. Previously the whole embed call failed, and after
the first boot-build failure the index silently degraded to the local
lexical provider forever. Retry a rejected batch item-by-item: good
items keep their vectors, rejected items become zero-vector
Float32Array placeholders so row alignment survives persistence, and if
every item fails the original batch error is kept. Per-item retries
share the batch timeout budget through AbortSignal.any so a slow
endpoint cannot hang the salvage loop.

【中文】批级 HTTP 400(参数误族)常由单条坏 chunk 触发——原先整批
embed 失败,boot 首败+allowFallback 即永久降级 lexical。改为逐条
重试:好条保留向量,坏条以零向量 Float32Array 占位保行对齐
(persist 层依赖 subarray,普通 Array 会炸持久化致整次构建丢失);
全批皆坏时保留原批错误语义。单条重试经 AbortSignal.any 共享
timeoutMs 预算,防慢端点无限挂起。
@JohnXu22786

Copy link
Copy Markdown
Owner

To clarify the intended behavior: after a batch embedding request fails, retry each text individually. Keep vectors for the texts that succeed and use a zero vector for a bad item, so one bad text does not fail the whole batch. Please keep that feature, but use one shared batch deadline: each individual retry should receive only the remaining time, rather than a fresh full timeout. Please add a timeout-boundary test and make sure cancellation is handled correctly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants