Skip to content

fix(kilo-indexing): split oversized embedding batches on context overflow - #15456

Open
PeterHodges15 wants to merge 1 commit into
Kilo-Org:mainfrom
PeterHodges15:fix/15450-embedding-context-overflow
Open

PeterHodges15 wants to merge 1 commit into
Kilo-Org:mainfrom
PeterHodges15:fix/15450-embedding-context-overflow

Conversation

@PeterHodges15

Copy link
Copy Markdown

Summary

Fixes #15450 — the indexer silently skips files when an embedding batch exceeds the embedding model's maximum input context.

Problem

The indexer caps individual items at OpenAI-sized token limits (MAX_ITEM_TOKENS = 8191) and packs up to BATCH_SEGMENT_THRESHOLD = 60 blocks per batch. Small-context embedding models (e.g. Granite, ~512-token input) reject such batches with a deterministic 400:

400 input (517 tokens) is larger than the max context size (512 tokens)

The scanner treats this as transient and retries it three times, then skips the file — so files are silently dropped from the index (Indexing partially failed: Only 27 of 96 files were indexed).

Fix

  • Reactive batch split — embedBatches now detects a context-overflow 400 (isContextOverflowError) from the endpoint and binary-splits the batch, re-requesting the halves. This is provider-agnostic and lives in the shared path used by all five embedders (openai-compatible, openai, voyage, openrouter, bedrock). Item order is preserved and token usage is summed.
  • Fail fast on deterministic errors — the scanner no longer retries deterministic 4xx embedding responses (except 429), so an unsplittable single-item overflow fails fast instead of burning retries.
  • Preserved status — formatEmbeddingError now carries the HTTP status on the returned error so callers can distinguish retryable from deterministic failures.

Testing

  • New embedder-helpers.test.ts: split behaviour (order, usage summation, split-to-single, rethrow on single-item overflow, no split on non-overflow 4xx/5xx).
  • validation-helpers.test.ts: isContextOverflowError, isNonRetryableEmbeddingError, and status preservation.
  • scanner.test.ts: a deterministic 4xx embed failure is not retried.
  • openai-compatible.test.ts: end-to-end split through the OpenAI client.

All kilo-indexing unit tests pass; tsgo --noEmit clean.

…flow

When an embedding batch exceeds the model's maximum input context (e.g. small-context models such as Granite that reject batches over 512 tokens), the provider returns a deterministic 400 that the scanner retried three times before skipping the file, silently dropping files from the index.

- Split the batch reactively in embedBatches when the endpoint rejects it with a context-overflow error (isContextOverflowError), preserving order and summing token usage.

- Stop retrying deterministic 4xx embedding responses (except 429) in the scanner so unsplittable single-item overflows fail fast instead of burning retries.

- Preserve the HTTP status on formatted embedding errors so callers can distinguish retryable from deterministic failures.
@github-actions github-actions Bot added community Opened by someone outside the Kilo team needs-triage Waiting for a maintainer to look priority:p1 High: fixes and regressions. Set from the title. Change by hand. labels Oct 24, 2026
},
}
}
throw error

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: A single oversized item still fails the entire batch

When one block is itself over the model's limit (batch.length === 1), this rethrow aborts the whole batch, so every file represented in that batch is still dropped from the index — just faster than before. That case is reachable for small-context models because the batching guard is still MAX_ITEM_TOKENS = 8191 (the openai-compatible default), far above e.g. Granite's ~512 tokens, so any single block between those sizes triggers it. Consider truncating an over-limit item to the model limit, or skipping just that block with a warning, instead of failing its neighbours.

* 5xx (server) errors are still considered retryable.
*/
export function isNonRetryableEmbeddingError(error: unknown): boolean {
const status = extractStatusCode(error)

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: extractStatusCode misses AWS SDK (Bedrock) errors

extractStatusCode only reads error.status, error.response.status, and HTTP <n>: in the message. AWS SDK v3 errors (Bedrock) expose the HTTP status via $metadata.httpStatusCode, so here status is undefined: deterministic Bedrock 4xx responses stay retryable at the scanner, and isContextOverflowError falls back to message-only matching. Adding error.$metadata?.httpStatusCode to extractStatusCode would make the new fast-fail behaviour match the PR's provider-agnostic intent.

const status = extractStatusCode(error)
if (status === undefined) return false
if (status === 429) return false
return status >= 400 && status < 500

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: Transient 4xx statuses are now treated as non-retryable

408 Request Timeout and 425 Too Early are retryable 4xx responses. Classifying every 4xx except 429 as deterministic means a genuine timeout now fails the batch on the first attempt. Consider excluding 408 and 425 alongside 429 here.

// burning the remaining retries — this is what produced the "400 after 3
// retries" symptom in #15450. Transient errors (429, 5xx, network) still retry.
if (isNonRetryableEmbeddingError(error)) {
break

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: Failure reporting still claims every retry was used

After this early break, the code below still logs Failed to process batch after ${this.maxBatchRetries} attempts and calls emitError(mode, "scanner:processBatch", lastError, this.maxBatchRetries), so a failure that happened on the first attempt is reported and telemetried as if three retries were exhausted. Passing the actual attempts value would keep the log line, the error telemetry retryCount, and the onError message accurate.

["Rate limit exceeded", 429],
["Internal server error", 500],
["Input had invalid characters", 400],
])("does not treat a non-overflow %d as a context overflow: %s", (message, status) => {

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: Format specifiers do not match the tuple order

Each row is [message, status], so %d is substituted with a string and renders NaN in the test name.

Suggested change
])("does not treat a non-overflow %d as a context overflow: %s", (message, status) => {
])("does not treat a non-overflow %s as a context overflow: %d", (message, status) => {

@kilo-code-bot

kilo-code-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 5 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 5
Issue Details (click to expand)

SUGGESTION

File Line Issue
packages/kilo-indexing/src/indexing/shared/embedder-helpers.ts 42 A batch containing one item that is itself over the model limit rethrows and fails the whole batch, so those files are still dropped; the batching guard remains MAX_ITEM_TOKENS = 8191, so items above a small model's limit (e.g. 512) are reachable. Consider truncating or skipping only that item.
packages/kilo-indexing/src/indexing/shared/validation-helpers.ts 265 extractStatusCode does not read $metadata.httpStatusCode, so deterministic Bedrock (AWS SDK) 4xx errors still retry and overflow detection falls back to message matching for that provider.
packages/kilo-indexing/src/indexing/shared/validation-helpers.ts 268 408 and 425 are retryable 4xx statuses but are now classified as deterministic, so a real timeout fails the batch on the first attempt.
packages/kilo-indexing/src/indexing/processors/scanner.ts 660 After the early break, logs, telemetry retryCount, and the onError message still claim maxBatchRetries attempts occurred.
packages/kilo-indexing/test/kilocode/indexing/shared/validation-helpers.test.ts 127 test.each title has %d for the message and %s for the status, rendering NaN in the test name.
Files Reviewed (8 files)
  • .changeset/fix-indexing-embedding-context-overflow.md
  • packages/kilo-indexing/src/indexing/processors/scanner.ts - 1 issue
  • packages/kilo-indexing/src/indexing/shared/embedder-helpers.ts - 1 issue
  • packages/kilo-indexing/src/indexing/shared/validation-helpers.ts - 2 issues
  • packages/kilo-indexing/test/kilocode/indexing/embedders/openai-compatible.test.ts
  • packages/kilo-indexing/test/kilocode/indexing/processors/scanner.test.ts
  • packages/kilo-indexing/test/kilocode/indexing/shared/embedder-helpers.test.ts
  • packages/kilo-indexing/test/kilocode/indexing/shared/validation-helpers.test.ts - 1 issue

Fix these issues in Kilo Cloud


Reviewed by deepseek-flash-latest · Input: 63.5K · Output: 18.2K · Cached: 615.3K

Review guidance: REVIEW.md from base branch main

This branch has not been deployed

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

Labels

community Opened by someone outside the Kilo team needs-triage Waiting for a maintainer to look priority:p1 High: fixes and regressions. Set from the title. Change by hand.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Indexer silently skips files when an embedding batch exceeds the model's max input tokens (400 after 3 retries)

1 participant