diff --git a/src/resources/embeddings.ts b/src/resources/embeddings.ts index 7a66904f7..cf1b2dbff 100644 --- a/src/resources/embeddings.ts +++ b/src/resources/embeddings.ts @@ -4,6 +4,7 @@ import { APIResource } from '../core/resource'; import { APIPromise } from '../core/api-promise'; import { RequestOptions } from '../internal/request-options'; import { loggerFor, toFloat32Array } from '../internal/utils'; +import { OpenAIError } from '../error'; export class Embeddings extends APIResource { /** @@ -51,8 +52,13 @@ export class Embeddings extends APIResource { return (response as APIPromise)._thenUnwrap((response) => { if (response && response.data) { response.data.forEach((embeddingBase64Obj) => { - const embeddingBase64Str = embeddingBase64Obj.embedding as unknown as string; - embeddingBase64Obj.embedding = toFloat32Array(embeddingBase64Str); + if (!embeddingBase64Obj) return; // Skip null/undefined items + + const embeddingBase64Str = embeddingBase64Obj.embedding as unknown; + if (embeddingBase64Str == null) { + throw new OpenAIError(`Missing embedding data for item at index ${embeddingBase64Obj.index ?? 'unknown'}`); + } + embeddingBase64Obj.embedding = toFloat32Array(embeddingBase64Str as string); }); } diff --git a/tests/api-resources/embeddings.test.ts b/tests/api-resources/embeddings.test.ts index 2ba17dbf1..aaf2e8f81 100644 --- a/tests/api-resources/embeddings.test.ts +++ b/tests/api-resources/embeddings.test.ts @@ -68,6 +68,112 @@ describe('resource embeddings', () => { expect(typeof response.data?.at(0)?.embedding).toBe('string'); }); + + test('create: should handle null embedding objects gracefully', async () => { + const client = makeClientWithCustomResponse({ + object: 'list', + data: [ + { object: 'embedding', index: 0, embedding: [-0.1, 0.2, 0.3] }, + null as any, // null embedding object + { object: 'embedding', index: 2, embedding: [0.4, 0.5, 0.6] }, + ], + model: 'test-model', + usage: { prompt_tokens: 1, total_tokens: 1 }, + }); + + const response = await client.embeddings.create({ + input: 'test', + model: 'test-model', + }); + + // Should skip null items and process valid ones + expect(response.data.length).toBe(3); + expect(Array.isArray(response.data[0]?.embedding)).toBe(true); + expect(response.data[1]).toBe(null); + expect(Array.isArray(response.data[2]?.embedding)).toBe(true); + }); + + test('create: should throw error for missing embedding data', async () => { + const client = makeClientWithCustomResponse({ + object: 'list', + data: [ + { object: 'embedding', index: 0, embedding: null as any }, // missing embedding data + ], + model: 'test-model', + usage: { prompt_tokens: 1, total_tokens: 1 }, + }); + + await expect( + client.embeddings.create({ + input: 'test', + model: 'test-model', + }), + ).rejects.toThrow('Missing embedding data for item at index 0'); + }); + + test('create: should throw error for undefined embedding data', async () => { + const client = makeClientWithCustomResponse({ + object: 'list', + data: [ + { object: 'embedding', index: 1, embedding: undefined as any }, // undefined embedding data + ], + model: 'test-model', + usage: { prompt_tokens: 1, total_tokens: 1 }, + }); + + await expect( + client.embeddings.create({ + input: 'test', + model: 'test-model', + }), + ).rejects.toThrow('Missing embedding data for item at index 1'); + }); + + test('create: should throw error for missing embedding data without index', async () => { + const client = makeClientWithCustomResponse({ + object: 'list', + data: [ + { object: 'embedding', embedding: null as any }, // missing embedding data and index + ], + model: 'test-model', + usage: { prompt_tokens: 1, total_tokens: 1 }, + }); + + await expect( + client.embeddings.create({ + input: 'test', + model: 'test-model', + }), + ).rejects.toThrow('Missing embedding data for item at index unknown'); + }); + + test('create: should handle mixed valid and invalid embedding objects', async () => { + const client = makeClientWithCustomResponse({ + object: 'list', + data: [ + { object: 'embedding', index: 0, embedding: [0.1, 0.2] }, // valid + null as any, // null object + { object: 'embedding', index: 2, embedding: [0.3, 0.4] }, // valid + undefined as any, // undefined object + { object: 'embedding', index: 4, embedding: [0.5, 0.6] }, // valid + ], + model: 'test-model', + usage: { prompt_tokens: 1, total_tokens: 1 }, + }); + + const response = await client.embeddings.create({ + input: ['test1', 'test2', 'test3', 'test4', 'test5'], + model: 'test-model', + }); + + // Should process valid items and skip null/undefined ones + expect(response.data.length).toBe(5); + expect(Array.isArray(response.data[0]?.embedding)).toBe(true); + expect(response.data[1]).toBe(null); + expect(Array.isArray(response.data[2]?.embedding)).toBe(true); + expect(response.data[3]).toBe(null); // undefined becomes null when serialized to JSON + expect(Array.isArray(response.data[4]?.embedding)).toBe(true); + }); }); function makeClient(): OpenAI { @@ -104,3 +210,22 @@ function makeClient(): OpenAI { baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', }); } + +function makeClientWithCustomResponse(responseBody: any): OpenAI { + const { fetch, handleRequest } = mockFetch(); + + handleRequest(async () => { + return new Response(JSON.stringify(responseBody), { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + + return new OpenAI({ + fetch, + apiKey: 'My API Key', + baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', + }); +}