From c34689eef0157a15af1ab366e137ca6385713b30 Mon Sep 17 00:00:00 2001 From: bhushan4work Date: Fri, 6 Nov 2026 00:01:14 +0530 Subject: [PATCH 1/3] fix(core): validate topN in cohere and voyage rerankers (#582) --- packages/core/src/retrieval/cohere-reranker.ts | 4 ++-- packages/core/src/retrieval/voyage-reranker.ts | 4 ++-- packages/core/tests/retrieval/cohere-reranker.test.ts | 9 +++++++++ packages/core/tests/retrieval/voyage-reranker.test.ts | 9 +++++++++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/core/src/retrieval/cohere-reranker.ts b/packages/core/src/retrieval/cohere-reranker.ts index b48f8eca..4babdf34 100644 --- a/packages/core/src/retrieval/cohere-reranker.ts +++ b/packages/core/src/retrieval/cohere-reranker.ts @@ -23,8 +23,8 @@ 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 []; + const limit = Number.isFinite(topN) && topN > 0 ? Math.floor(topN) : 0; + if (!chunks.length || 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..1f7b07c9 100644 --- a/packages/core/src/retrieval/voyage-reranker.ts +++ b/packages/core/src/retrieval/voyage-reranker.ts @@ -23,8 +23,8 @@ 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 []; + const limit = Number.isFinite(topN) && topN > 0 ? Math.floor(topN) : 0; + if (!chunks.length || 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..2f7bd50c 100644 --- a/packages/core/tests/retrieval/cohere-reranker.test.ts +++ b/packages/core/tests/retrieval/cohere-reranker.test.ts @@ -45,6 +45,15 @@ describe('cohereReranker', () => { expect(send).not.toHaveBeenCalled(); }); + it('returns no results and makes no request for non-finite 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(await reranker.rerank('q', chunks, -Infinity)).toEqual([]); + expect(send).not.toHaveBeenCalled(); + }); + 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..21737819 100644 --- a/packages/core/tests/retrieval/voyage-reranker.test.ts +++ b/packages/core/tests/retrieval/voyage-reranker.test.ts @@ -45,6 +45,15 @@ describe('voyageReranker', () => { expect(send).not.toHaveBeenCalled(); }); + it('returns no results and makes no request for non-finite 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(await reranker.rerank('q', chunks, -Infinity)).toEqual([]); + expect(send).not.toHaveBeenCalled(); + }); + 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 }); From 4b4d6a0e5f42dff71e1494e9b7142794fdbe1ec7 Mon Sep 17 00:00:00 2001 From: bhushan4work Date: Fri, 6 Nov 2026 00:28:23 +0530 Subject: [PATCH 2/3] chore: add changeset for reranker topN validation (#582) --- .changeset/reranker-topn-validation.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/reranker-topn-validation.md diff --git a/.changeset/reranker-topn-validation.md b/.changeset/reranker-topn-validation.md new file mode 100644 index 00000000..ee35ed58 --- /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 non-finite or invalid inputs. From c0f579916baf4b3e7deac60d807bd4b9e20cbab0 Mon Sep 17 00:00:00 2001 From: bhushan4work Date: Fri, 6 Nov 2026 16:09:09 +0530 Subject: [PATCH 3/3] fix(core): allow Infinity topN in cohere and voyage rerankers (#582) --- .changeset/reranker-topn-validation.md | 2 +- packages/core/src/retrieval/cohere-reranker.ts | 4 ++-- packages/core/src/retrieval/voyage-reranker.ts | 4 ++-- packages/core/tests/retrieval/cohere-reranker.test.ts | 10 ++++++++-- packages/core/tests/retrieval/voyage-reranker.test.ts | 10 ++++++++-- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/.changeset/reranker-topn-validation.md b/.changeset/reranker-topn-validation.md index ee35ed58..d3cdf702 100644 --- a/.changeset/reranker-topn-validation.md +++ b/.changeset/reranker-topn-validation.md @@ -2,4 +2,4 @@ "@chatcops/core": patch --- -Validate topN in cohereReranker and voyageReranker to immediately return an empty array for non-finite or invalid inputs. +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 4babdf34..b5412fdc 100644 --- a/packages/core/src/retrieval/cohere-reranker.ts +++ b/packages/core/src/retrieval/cohere-reranker.ts @@ -23,8 +23,8 @@ export function cohereReranker(options: CohereRerankerOptions): Reranker { return { async rerank(query, chunks, topN, rerankOptions = {}) { - const limit = Number.isFinite(topN) && topN > 0 ? Math.floor(topN) : 0; - if (!chunks.length || limit <= 0) return []; + const limit = Math.floor(topN); + 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 1f7b07c9..b6c6b34d 100644 --- a/packages/core/src/retrieval/voyage-reranker.ts +++ b/packages/core/src/retrieval/voyage-reranker.ts @@ -23,8 +23,8 @@ export function voyageReranker(options: VoyageRerankerOptions): Reranker { return { async rerank(query, chunks, topN, rerankOptions = {}) { - const limit = Number.isFinite(topN) && topN > 0 ? Math.floor(topN) : 0; - if (!chunks.length || limit <= 0) return []; + const limit = Math.floor(topN); + 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 2f7bd50c..c96597aa 100644 --- a/packages/core/tests/retrieval/cohere-reranker.test.ts +++ b/packages/core/tests/retrieval/cohere-reranker.test.ts @@ -45,15 +45,21 @@ describe('cohereReranker', () => { expect(send).not.toHaveBeenCalled(); }); - it('returns no results and makes no request for non-finite topN values', async () => { + 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(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 21737819..220305ac 100644 --- a/packages/core/tests/retrieval/voyage-reranker.test.ts +++ b/packages/core/tests/retrieval/voyage-reranker.test.ts @@ -45,15 +45,21 @@ describe('voyageReranker', () => { expect(send).not.toHaveBeenCalled(); }); - it('returns no results and makes no request for non-finite topN values', async () => { + 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(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 });