{"record":{"id":"de968880dbf4948f","repo":"abhigyanpatwari/GitNexus","slug":"embedding-endpoint-returned-an-unexpected-response","errorCode":null,"errorMessage":"Embedding endpoint returned an unexpected response shape (${safeUrl(url)}, batch ${batchIndex})","messagePattern":"Embedding endpoint returned an unexpected response shape \\((.+?), batch (.+?)\\)","errorType":"exception","errorClass":"RetryableEmbeddingBodyError","httpStatus":null,"severity":"error","filePath":"gitnexus/src/core/embeddings/http-client.ts","lineNumber":512,"sourceCode":"            // wired to the body stream, so a stalled body rejects with the abort\n            // reason. Re-raise AbortError (and TimeoutError when retry is off)\n            // untouched — `isTerminalNetworkError` is `resilientFetch`'s own\n            // predicate, so this test agrees with `classifyOutcome` by\n            // construction. Wrapping one would flip its verdict from\n            // `terminal-network` (returned without retry AND without touching\n            // the breaker, via `recordNeutral()`) to `retryable-network`\n            // (retried, then `breaker.recordFailure()`): the same timeout would\n            // take 3 attempts instead of 1, count toward the process-global\n            // `embeddings-http` breaker, and reach the operator as \"unparseable\n            // response\" so they never reach for the timeout knob.\n            // Opt-in `GITNEXUS_EMBEDDING_RETRY_TIMEOUTS=1` is the exception:\n            // TimeoutError is re-wrapped so the existing retry loop can retry it.\n            throwIfRetryableTimeout(err, retryTimeouts, requestOptions.signal?.aborted, timeoutMs);\n            if (isTerminalNetworkError(err)) throw err;\n            throw new RetryableEmbeddingBodyError(unparseableMessage(), { cause: err });\n          }\n          if (!Array.isArray(payload?.data) || !payload.data.every(isEmbeddingItem)) {\n            throw new RetryableEmbeddingBodyError(unexpectedShapeMessage());\n          }\n          // Cardinality belongs *inside* the retry loop. `every(isEmbeddingItem)`\n          // is vacuously true for `[]` and true for any array shorter than the\n          // request, so a 200 carrying `{\"data\": []}` — or half the vectors —\n          // used to be classified `success`, call `breaker.recordSuccess()`\n          // (erasing the outage signal), and only then fail terminally after a\n          // single attempt. A short body is a truncated body: same backoff, same\n          // breaker accounting as any other endpoint fault (#2790).\n          if (payload.data.length !== batch.length) {\n            throw new RetryableEmbeddingBodyError(\n              countMismatchMessage(payload.data.length, batch.length, safeUrl(url), batchIndex),\n            );\n          }\n          parsed = payload.data;\n          return attemptResp;\n        },\n        breakerKey: HTTP_BREAKER_KEY,\n        retry: {","sourceCodeStart":494,"sourceCodeEnd":530,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/ac9a4e9abd8fd3058c070b72c23402a4f887929a/gitnexus/src/core/embeddings/http-client.ts#L494-L530","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: custom server response\nres.json({ embeddings: vectors });\n// after: OpenAI-compatible shape\nres.json({ data: vectors.map((v, index) => ({ embedding: v, index })) });","handlingStrategy":"validation","validationCode":"const isEmbeddingItem = (x: any): boolean =>\n  !!x && Array.isArray(x.embedding) && typeof x.index === 'number';\n// validate once against your endpoint before adopting it:\nconst ok = Array.isArray(payload?.data) && payload.data.every(isEmbeddingItem);","typeGuard":"const isOpenAiShapeError = (e: unknown): boolean =>\n  e instanceof Error && e.message.includes('unexpected response shape');","tryCatchPattern":"try {\n  vectors = await httpEmbed(texts);\n} catch (e) {\n  if (isOpenAiShapeError(e)) {\n    // your endpoint is not OpenAI-compatible: adapt it or front it with a compatible gateway\n  } else throw e;\n}","preventionTips":["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."],"tags":["embeddings","http","response-shape","openai-compat","validation"],"backgroundTag":"api-response-schema-mismatch","analyzedSha":"ac9a4e9abd8fd3058c070b72c23402a4f887929a","analyzedAt":"2026-08-20T23:29:22.980Z","contentChangedAt":"2026-08-20T23:29:22.980Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}