diff --git a/.changeset/reranker-topn-validation.md b/.changeset/reranker-topn-validation.md new file mode 100644 index 00000000..d3cdf702 --- /dev/null +++ b/.changeset/reranker-topn-validation.md @@ -0,0 +1,5 @@ +--- +"@chatcops/core": patch +--- + +Validate topN in cohereReranker and voyageReranker to immediately return an empty array for NaN. diff --git a/packages/core/src/retrieval/cohere-reranker.ts b/packages/core/src/retrieval/cohere-reranker.ts index b48f8eca..b5412fdc 100644 --- a/packages/core/src/retrieval/cohere-reranker.ts +++ b/packages/core/src/retrieval/cohere-reranker.ts @@ -24,7 +24,7 @@ export function cohereReranker(options: CohereRerankerOptions): Reranker { return { async rerank(query, chunks, topN, rerankOptions = {}) { const limit = Math.floor(topN); - if (chunks.length === 0 || limit <= 0) return []; + if (chunks.length === 0 || !(limit > 0)) return []; const response = await postWithRetries( 'Cohere', diff --git a/packages/core/src/retrieval/voyage-reranker.ts b/packages/core/src/retrieval/voyage-reranker.ts index 657e3e79..b6c6b34d 100644 --- a/packages/core/src/retrieval/voyage-reranker.ts +++ b/packages/core/src/retrieval/voyage-reranker.ts @@ -24,7 +24,7 @@ export function voyageReranker(options: VoyageRerankerOptions): Reranker { return { async rerank(query, chunks, topN, rerankOptions = {}) { const limit = Math.floor(topN); - if (chunks.length === 0 || limit <= 0) return []; + if (chunks.length === 0 || !(limit > 0)) return []; const response = await postWithRetries( 'Voyage', diff --git a/packages/core/tests/retrieval/cohere-reranker.test.ts b/packages/core/tests/retrieval/cohere-reranker.test.ts index f72a8c84..c96597aa 100644 --- a/packages/core/tests/retrieval/cohere-reranker.test.ts +++ b/packages/core/tests/retrieval/cohere-reranker.test.ts @@ -45,6 +45,21 @@ describe('cohereReranker', () => { expect(send).not.toHaveBeenCalled(); }); + it('returns no results and makes no request for NaN and -Infinity topN values', async () => { + const send = vi.fn(async () => json({ results: [] })); + const reranker = cohereReranker({ apiKey: 'k', fetch: send }); + expect(await reranker.rerank('q', chunks, Number.NaN)).toEqual([]); + expect(await reranker.rerank('q', chunks, -Infinity)).toEqual([]); + expect(send).not.toHaveBeenCalled(); + }); + + it('asks for no more than available documents when topN is Infinity', async () => { + const send = vi.fn(async () => json({ results: [] })); + const reranker = cohereReranker({ apiKey: 'k', fetch: send }); + await reranker.rerank('q', chunks, Infinity); + expect(JSON.parse(String((send.mock.calls[0] as unknown as [string, RequestInit])[1].body))).toMatchObject({ top_n: 3 }); + }); + it('surfaces client API errors without retrying', async () => { const send = vi.fn(async () => json({ message: 'Invalid model' }, 400)); const reranker = cohereReranker({ apiKey: 'k', fetch: send }); diff --git a/packages/core/tests/retrieval/voyage-reranker.test.ts b/packages/core/tests/retrieval/voyage-reranker.test.ts index aff13df0..220305ac 100644 --- a/packages/core/tests/retrieval/voyage-reranker.test.ts +++ b/packages/core/tests/retrieval/voyage-reranker.test.ts @@ -45,6 +45,21 @@ describe('voyageReranker', () => { expect(send).not.toHaveBeenCalled(); }); + it('returns no results and makes no request for NaN and -Infinity topN values', async () => { + const send = vi.fn(async () => json({ object: 'list', data: [] })); + const reranker = voyageReranker({ apiKey: 'k', fetch: send }); + expect(await reranker.rerank('q', chunks, Number.NaN)).toEqual([]); + expect(await reranker.rerank('q', chunks, -Infinity)).toEqual([]); + expect(send).not.toHaveBeenCalled(); + }); + + it('caps top_k to available documents when topN is Infinity', async () => { + const send = vi.fn(async () => json({ object: 'list', data: [] })); + const reranker = voyageReranker({ apiKey: 'k', fetch: send }); + await reranker.rerank('q', chunks, Infinity); + expect(JSON.parse(String((send.mock.calls[0] as unknown as [string, RequestInit])[1].body))).toMatchObject({ top_k: 3 }); + }); + it('surfaces client API errors without retrying', async () => { const send = vi.fn(async () => json({ detail: 'Provided model is not supported.' }, 400)); const reranker = voyageReranker({ apiKey: 'k', fetch: send });