abhigyanpatwari/GitNexus · error · RetryableEmbeddingBodyError

Embedding endpoint returned an unexpected response shape

Error message

Embedding endpoint returned an unexpected response shape (${safeUrl(url)}, batch ${batchIndex})

What it means

httpEmbedBatch's shape check: the parsed 2xx body is not { data: Array<{ embedding, index }> }. Each payload.data element must pass isEmbeddingItem; a missing data array or wrong element shape throws RetryableEmbeddingBodyError (retried, then terminal with this message). The client speaks the OpenAI embeddings response schema, nothing else.

Solutions

  1. Verify the response with curl and compare against {"data":[{"embedding":[...],"index":0}]} shape.
  2. Put an OpenAI-compatible adapter (LiteLLM, gateway) in front of a non-conforming backend.
  3. Fix your server to emit the OpenAI embeddings schema including per-item index.
  4. Confirm GITNEXUS_EMBEDDING_MODEL names a model the endpoint actually serves.

Example fix

// before: custom server response
res.json({ embeddings: vectors });
// after: OpenAI-compatible shape
res.json({ data: vectors.map((v, index) => ({ embedding: v, index })) });
Defensive patterns

Strategy: validation

Validate before calling

const isEmbeddingItem = (x: any): boolean =>
  !!x && Array.isArray(x.embedding) && typeof x.index === 'number';
// validate once against your endpoint before adopting it:
const ok = Array.isArray(payload?.data) && payload.data.every(isEmbeddingItem);

Type guard

const isOpenAiShapeError = (e: unknown): boolean =>
  e instanceof Error && e.message.includes('unexpected response shape');

Try / catch

try {
  vectors = await httpEmbed(texts);
} catch (e) {
  if (isOpenAiShapeError(e)) {
    // your endpoint is not OpenAI-compatible: adapt it or front it with a compatible gateway
  } else throw e;
}

Prevention

When it happens

Trigger: An endpoint returning a non-OpenAI schema — { embeddings: [...] }, { vectors: [...] }, a plain array, or data items whose embedding field is named differently or absent — consistently for a batch.

Common situations: Homegrown embedding servers, gRPC-gateway JSON translations, or a /embeddings route on a different API version whose item shape changed; also data: null when the model errors quietly.

Related errors


AI-assisted analysis of abhigyanpatwari/GitNexus@ac9a4e9abd (2026-08-20). Data as JSON: /api/errors/de968880dbf4948f. Report an issue: GitHub.

Appendix: source

Thrown at gitnexus/src/core/embeddings/http-client.ts:512

            // wired to the body stream, so a stalled body rejects with the abort
            // reason. Re-raise AbortError (and TimeoutError when retry is off)
            // untouched — `isTerminalNetworkError` is `resilientFetch`'s own
            // predicate, so this test agrees with `classifyOutcome` by
            // construction. Wrapping one would flip its verdict from
            // `terminal-network` (returned without retry AND without touching
            // the breaker, via `recordNeutral()`) to `retryable-network`
            // (retried, then `breaker.recordFailure()`): the same timeout would
            // take 3 attempts instead of 1, count toward the process-global
            // `embeddings-http` breaker, and reach the operator as "unparseable
            // response" so they never reach for the timeout knob.
            // Opt-in `GITNEXUS_EMBEDDING_RETRY_TIMEOUTS=1` is the exception:
            // TimeoutError is re-wrapped so the existing retry loop can retry it.
            throwIfRetryableTimeout(err, retryTimeouts, requestOptions.signal?.aborted, timeoutMs);
            if (isTerminalNetworkError(err)) throw err;
            throw new RetryableEmbeddingBodyError(unparseableMessage(), { cause: err });
          }
          if (!Array.isArray(payload?.data) || !payload.data.every(isEmbeddingItem)) {
            throw new RetryableEmbeddingBodyError(unexpectedShapeMessage());
          }
          // Cardinality belongs *inside* the retry loop. `every(isEmbeddingItem)`
          // is vacuously true for `[]` and true for any array shorter than the
          // request, so a 200 carrying `{"data": []}` — or half the vectors —
          // used to be classified `success`, call `breaker.recordSuccess()`
          // (erasing the outage signal), and only then fail terminally after a
          // single attempt. A short body is a truncated body: same backoff, same
          // breaker accounting as any other endpoint fault (#2790).
          if (payload.data.length !== batch.length) {
            throw new RetryableEmbeddingBodyError(
              countMismatchMessage(payload.data.length, batch.length, safeUrl(url), batchIndex),
            );
          }
          parsed = payload.data;
          return attemptResp;
        },
        breakerKey: HTTP_BREAKER_KEY,
        retry: {

View on GitHub (pinned to ac9a4e9abd)