Skip to content

fix: handle Gemini batch embedding count mismatch (avoid zip crash)#926

Open
Stromweld wants to merge 2 commits into
plastic-labs:mainfrom
Stromweld:fix/gemini-batch-embedding-zip-crash
Open

fix: handle Gemini batch embedding count mismatch (avoid zip crash)#926
Stromweld wants to merge 2 commits into
plastic-labs:mainfrom
Stromweld:fix/gemini-batch-embedding-zip-crash

Conversation

@Stromweld

@Stromweld Stromweld commented Jul 22, 2026

Copy link
Copy Markdown

Problem

Batch Gemini embedding calls in src/embedding_client.py did:

contents=[item.text for item in batch]
...
for item, embedding in zip(batch, response.embeddings, strict=True):

This assumed Gemini always returns exactly one embedding per input item, in order. While the SDK docstring claims this, it does not always hold in practice (possible causes: empty/whitespace items dropped, batch size >100, other API quirks). When the counts didn't match, zip(..., strict=True) raised ValueError: zip() argument 2 is shorter than argument 1, crashing the whole batch.

Fix

  • Keep the efficient multi-item batch call as the normal path.
  • Explicitly check len(response.embeddings) != len(batch) before zipping.
  • On a mismatch, fall back to embedding each item in that batch individually (one call per item, preserving original order) instead of guessing at alignment.
  • Each individual fallback call must return exactly one embedding; if it returns zero or multiple, raise ValueError with a clear diagnostic (text_id/chunk_index) instead of silently dropping or misaligning data.

Tests

Added to tests/llm/test_embedding_client.py:

  • Normal single-item batch response
  • Normal multi-item batch response, verifying item-to-embedding order
  • Mismatched (short) multi-item batch response triggering per-item fallback, verifying correct mapping
  • Fallback individual call returning zero embeddings raises a controlled ValueError
20 passed, 17 warnings in 10.93s

This is a separate, independent fix from BUG1 (PR #925) — no overlap with that branch/commit.

Created with AI assistance — Hermes Agent v0.17.0, model Claude Sonnet 4.6.

Summary by CodeRabbit

  • Bug Fixes
    • Improved reliability of Gemini batch embeddings by validating response cardinality against the requested batch size.
    • Ensures embeddings are correctly mapped back to items; falls back to per-item embedding when batch alignment isn’t safe.
    • Added strict error handling for embeddings missing required values and for invalid per-item response sizes.
  • Tests
    • Extended Gemini embedding tests to cover matched batch success, order preservation, fallback behavior, and multiple error scenarios.

Batch Gemini embedding calls previously assumed the SDK always returns
exactly one embedding per input item in order, then used
zip(batch, response.embeddings, strict=True). When Gemini returns
fewer embeddings than input items (e.g. empty/whitespace items dropped,
API quirks), this crashed with 'zip() argument 2 is shorter than
argument 1'.

Now the batch call verifies embeddings/inputs cardinality explicitly.
On a match, the efficient batch path is used as before. On a mismatch,
we fall back to embedding each item individually (preserving order),
requiring exactly one embedding per individual call. If an individual
call itself returns zero or multiple embeddings, we raise ValueError
with a clear diagnostic instead of silently misaligning or dropping
items.

Signed-off-by: Corey Hemminger <hemminger@hotmail.com>
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 488afe57-1546-4064-9460-21f7150e32c1

📥 Commits

Reviewing files that changed from the base of the PR and between 95f6ae8 and 153f55a.

📒 Files selected for processing (2)
  • src/embedding_client.py
  • tests/llm/test_embedding_client.py

Walkthrough

Gemini batch embedding now validates that response cardinality matches the input batch, preserves item order, and falls back to per-item calls when it does not. Invalid counts or missing embedding values raise ValueError. Tests cover aligned, fallback, and failure paths.

Changes

Gemini batch alignment

Layer / File(s) Summary
Embedding cardinality fallback
src/embedding_client.py
Validates one-to-one Gemini batch responses, preserves ordered mappings, retries items individually on mismatches, and raises errors for invalid counts or missing values.
Embedding behavior validation
tests/llm/test_embedding_client.py
Adds configurable fake Gemini behavior and tests aligned responses, ordering, fallback remapping, invalid counts, and missing values.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: rajat-ahuja1997

Sequence Diagram(s)

sequenceDiagram
  participant EmbeddingClient
  participant GeminiAPI
  participant ItemEmbeddingCalls
  EmbeddingClient->>GeminiAPI: request batch embeddings
  GeminiAPI-->>EmbeddingClient: return batch embeddings
  EmbeddingClient->>ItemEmbeddingCalls: retry items when cardinality mismatches
  ItemEmbeddingCalls->>GeminiAPI: request single-item embeddings
  GeminiAPI-->>ItemEmbeddingCalls: return one embedding per item
  ItemEmbeddingCalls-->>EmbeddingClient: return aligned mappings
Loading

Poem

A bunny counts vectors in a row,
One for each input, neatly in tow.
If Gemini skips one in the stack,
I hop through each item and bring alignment back.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: fixing Gemini batch embedding count mismatches and the zip-related failure.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/embedding_client.py (1)

484-489: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Sequential per-item fallback calls add up to N round-trips.

The fallback loops over every item in batch (up to max_batch_size=100) with sequential await calls. Since this comment block itself notes the mismatch "has been observed" in practice (not purely theoretical), this sequential path can add meaningful latency when triggered. Consider asyncio.gather to issue the per-item calls concurrently, preserving output order (gather returns results in input order regardless of completion order) — but note the test double's single_calls list order is completion-order dependent, so tests/llm/test_embedding_client.py's assertions on call order may need to account for that if this is changed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/embedding_client.py` around lines 484 - 489, Update the per-item fallback
in the batch embedding flow to issue Gemini requests concurrently with
asyncio.gather while preserving batch output order. Keep each item mapped to its
corresponding response, and adjust affected tests such as single_calls order
assertions to account for completion-order variation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/embedding_client.py`:
- Around line 462-466: Update both the happy-path loop using zip and the
per-item fallback to raise ValueError when embedding.values is None or empty,
matching embed()'s “No embedding returned from Gemini API” behavior. Do not
silently skip the item; only validate and store embeddings when values are
present.

---

Nitpick comments:
In `@src/embedding_client.py`:
- Around line 484-489: Update the per-item fallback in the batch embedding flow
to issue Gemini requests concurrently with asyncio.gather while preserving batch
output order. Keep each item mapped to its corresponding response, and adjust
affected tests such as single_calls order assertions to account for
completion-order variation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d957eaef-21ef-4f0e-898e-ddfe4e2f05bd

📥 Commits

Reviewing files that changed from the base of the PR and between 4f9a413 and 95f6ae8.

📒 Files selected for processing (2)
  • src/embedding_client.py
  • tests/llm/test_embedding_client.py

Comment thread src/embedding_client.py
…ently dropping

CodeRabbit flagged that both the cardinality-matched batch (zip) path
and the per-item fallback path silently omitted an item from the
result dict when embedding.values was falsy (None or empty list).
This reintroduced the exact silent-misalignment risk this PR was
created to eliminate.

Now both paths raise a ValueError with diagnostic context (text_id,
chunk_index, and which path) when a returned embedding has no values,
matching the diagnostic style already used for the wrong-count case.

Also adds regression test coverage:
- fallback call returning multiple embeddings for a single item raises
- batch path with one falsy-values embedding in an otherwise
  cardinality-matched response raises
- fallback path with a falsy-values single embedding raises

Signed-off-by: Corey Hemminger <hemminger@hotmail.com>
@karaokedjodua

Copy link
Copy Markdown

Finn-loop review of 153f55a

CI: not configured (mergeStateStatus=BLOCKED, mergeable=MERGEABLE, but gh pr checks --required reports "no required checks configured" — stale branch-protection rule)
Mergeability: clean (structurally mergeable)

Review

PR #926 fixes a zip(strict=True) ValueError crash when Gemini batch embedding calls return fewer embeddings than input items. The fix adds a cardinality check before zipping and falls back to per-item embedding calls on mismatch, preserving correct item-to-embedding alignment. Eight new deterministic tests cover normal batch, order preservation, mismatch fallback, and all error paths.

1. Must fix before merge

  • [CI] No required CI checks are configured on this branch. gh pr checks 926 --required reports "no required checks reported" and the only running check is CodeRabbit (passed). The repository's CLAUDE.md documents uv run ruff check src/, uv run basedpyright, and uv run pytest tests/ as required quality gates, but none are enforced via branch protection. Additionally, mergeStateStatus is BLOCKED while mergeable is MERGEABLE, which indicates a stale branch-protection rule referencing a check that no longer exists. An admin must either update the branch-protection rules to match current checks or remove the stale rule.

2. Should fix soon

None. The code changes are well-scoped:

  • The fast path (len(embeddings) == len(batch)) preserves existing behavior with an added embedding.values emptiness check (previously this would silently produce a falsy result; now it raises a controlled ValueError).
  • The mismatch fallback iterates batch sequentially with per-item embedding calls, preserving order without guessing alignment.
  • Per-item fallback calls validate exactly one embedding returned and non-empty values, with diagnostic text_id/chunk_index in error messages.
  • Tests cover: single-item normal, multi-item order preservation, mismatch triggering fallback, zero-embedding fallback error, multi-embedding fallback error, empty-values-in-matched-path error, and empty-values-in-fallback error.

3. Safe to merge

No — CI gate is missing. Automated review of the code itself finds no defects, security issues, or AC gaps. A human must verify lint/typecheck/tests pass and resolve the stale branch-protection rule before merging.

Notes

  • No formal issue (Paperclip KAR-NNN) was linked in the PR body; the PR body itself served as the contract.
  • Labels (needs-human-review) could not be applied — this reviewer's account lacks admin rights on the repository.

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