diff --git a/packages/memory-host-sdk/src/host/batch-output.test.ts b/packages/memory-host-sdk/src/host/batch-output.test.ts index 339142322cc8..0653551b82b9 100644 --- a/packages/memory-host-sdk/src/host/batch-output.test.ts +++ b/packages/memory-host-sdk/src/host/batch-output.test.ts @@ -173,6 +173,39 @@ describe("applyEmbeddingBatchOutputLine", () => { expect(byCustomId.get("req-1")).toEqual([0.1, 0.2]); }); + it.each([ + { name: "non-array", embedding: "poison" }, + { name: "array-like object", embedding: { length: 1 } }, + { name: "boolean", embedding: false }, + { name: "string coordinate", embedding: ["poison"] }, + { name: "null coordinate", embedding: [null] }, + { name: "NaN coordinate", embedding: [Number.NaN] }, + { name: "positive infinity coordinate", embedding: [Number.POSITIVE_INFINITY] }, + { name: "negative infinity coordinate", embedding: [Number.NEGATIVE_INFINITY] }, + ])("rejects a $name instead of storing it as a valid vector", ({ embedding }) => { + const remaining = new Set(["req-invalid"]); + const errors: string[] = []; + const byCustomId = new Map(); + + applyEmbeddingBatchOutputLine({ + line: { + custom_id: "req-invalid", + response: { + status_code: 200, + body: { data: [{ embedding: embedding as unknown as number[] }] }, + }, + }, + remaining, + errors, + byCustomId, + }); + + expect(remaining.has("req-invalid")).toBe(false); + expect(errors).toHaveLength(1); + expect(errors[0]).toMatch(/^req-invalid: (?:empty|invalid) embedding$/); + expect(byCustomId.size).toBe(0); + }); + it("records provider error from line.error", () => { const remaining = new Set(["req-2"]); const errors: string[] = []; diff --git a/packages/memory-host-sdk/src/host/batch-output.ts b/packages/memory-host-sdk/src/host/batch-output.ts index 3083fa43824f..76fb39b0f462 100644 --- a/packages/memory-host-sdk/src/host/batch-output.ts +++ b/packages/memory-host-sdk/src/host/batch-output.ts @@ -174,8 +174,8 @@ export function applyEmbeddingBatchOutputLine(params: { const data = response?.body && typeof response.body === "object" ? (response.body.data ?? []) : []; const embedding = data[0]?.embedding ?? []; - if (embedding.length === 0) { - params.errors.push(`${customId}: empty embedding`); + if (!Array.isArray(embedding) || embedding.length === 0 || !embedding.every(Number.isFinite)) { + params.errors.push(`${customId}: ${embedding?.length ? "invalid" : "empty"} embedding`); return; } params.byCustomId.set(customId, embedding); diff --git a/packages/memory-host-sdk/src/host/embeddings-remote-fetch.test.ts b/packages/memory-host-sdk/src/host/embeddings-remote-fetch.test.ts index 62c3e2edd24b..77411793446c 100644 --- a/packages/memory-host-sdk/src/host/embeddings-remote-fetch.test.ts +++ b/packages/memory-host-sdk/src/host/embeddings-remote-fetch.test.ts @@ -124,6 +124,39 @@ describe("fetchRemoteEmbeddingVectors", () => { ).rejects.toThrow("embedding fetch failed: malformed JSON response"); }); + it.each([ + { name: "query", input: ["query"], data: [{ embedding: [] }] }, + { + name: "document batch", + input: ["first", "second"], + data: [{ embedding: [0.1] }, { embedding: [] }], + }, + ])("rejects empty vectors in a $name response", async ({ input, data }) => { + postJsonMock.mockImplementationOnce(async (params) => await params.parse({ data })); + + await expect( + fetchRemoteEmbeddingVectors({ + url: "https://memory.example/v1/embeddings", + headers: {}, + body: { input }, + errorPrefix: "embedding fetch failed", + }), + ).rejects.toThrow("embedding fetch failed: malformed JSON response"); + }); + + it("preserves an empty response for an empty submitted input batch", async () => { + postJsonMock.mockImplementationOnce(async (params) => await params.parse({ data: [] })); + + await expect( + fetchRemoteEmbeddingVectors({ + url: "https://memory.example/v1/embeddings", + headers: {}, + body: { input: [] }, + errorPrefix: "embedding fetch failed", + }), + ).resolves.toEqual([]); + }); + it("rejects wrong nested embedding vector types", async () => { postJsonMock.mockImplementationOnce(async (params) => { return await params.parse({ data: [{ embedding: [0.1, "bad"] }] }); diff --git a/packages/memory-host-sdk/src/host/embeddings-remote-fetch.ts b/packages/memory-host-sdk/src/host/embeddings-remote-fetch.ts index 24072776f1be..d259e65568fe 100644 --- a/packages/memory-host-sdk/src/host/embeddings-remote-fetch.ts +++ b/packages/memory-host-sdk/src/host/embeddings-remote-fetch.ts @@ -18,7 +18,7 @@ function malformedEmbeddingResponse(errorPrefix: string): Error { /** Validate and return one finite embedding vector. */ function readEmbeddingVector(value: unknown, errorPrefix: string): number[] { - if (!Array.isArray(value)) { + if (!Array.isArray(value) || value.length === 0) { throw malformedEmbeddingResponse(errorPrefix); } for (const entry of value) {