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
- Verify the response with curl and compare against {"data":[{"embedding":[...],"index":0}]} shape.
- Put an OpenAI-compatible adapter (LiteLLM, gateway) in front of a non-conforming backend.
- Fix your server to emit the OpenAI embeddings schema including per-item index.
- 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
- Standardize on OpenAI-compatible /embeddings backends or put an adapter (LiteLLM etc.) in front.
- Pin gateway/API versions so response schemas don't drift under you.
- Include a contract test for the embeddings endpoint in CI.
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
- GITNEXUS_EMBEDDING_REQUEST_DIMS must be a positive integer…
- must be a positive integer, got
- Embedding endpoint circuit open
- Embedding endpoint returned an unparseable response
- Embedding endpoint returned
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)