From 2cf462c1a1a947cb11ecb982fc852e917afe9ed9 Mon Sep 17 00:00:00 2001 From: backryun Date: Sun, 26 Jul 2026 15:51:56 +0900 Subject: [PATCH] refactor(sse): type the rerank response adapter's options parameter (#8528) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `transformResponseFromProvider(providerConfig, data, options = {})` annotated nothing, so TS inferred `options` as `{}` from its default value and rejected every read of it: `top_n` x6, `documents` x4, `return_documents` x2 across the DeepInfra and Voyage adapters. All 12 of the file's diagnostics, one cause. Declared `RerankResponseOptions` from the JSDoc that already documents these fields on handleRerank, with `documents` as `Array` — the two forms the Cohere-compatible API accepts and the adapters already branch on. `providerConfig` and `data` stay unannotated; only `options` was producing errors and only `options` is touched. 280 -> 268, zero new, on a line-number-agnostic diff of the full tsc error set. Tests: `{ text }` documents were only ever exercised through the *request* adapter (#5332, #7809) — every response-adapter test passed plain strings, so the `typeof doc === "string" ? doc : doc?.text` branch on the response side was uncovered, and that branch is exactly what gives the declared type its union. Added rerank-object-documents-response-path.test.ts (5 tests): object-form documents resolved through both adapters, a mixed string/object array, the `{}`-without-text fallback, and Voyage's index remap across an empty `{ text: "" }` document. They pass against the parent commit's rerank.ts too — no behavior change. 43/43 across the 6 rerank/media-cost suites. Co-authored-by: backryun --- open-sse/handlers/rerank.ts | 25 +++++- ...ank-object-documents-response-path.test.ts | 84 +++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 tests/unit/rerank-object-documents-response-path.test.ts diff --git a/open-sse/handlers/rerank.ts b/open-sse/handlers/rerank.ts index 719459d555..175116e65d 100644 --- a/open-sse/handlers/rerank.ts +++ b/open-sse/handlers/rerank.ts @@ -16,6 +16,25 @@ import { resolveProxyForConnection } from "@/lib/db/settings"; import { runWithProxyContext } from "../utils/proxyFetch.ts"; import * as log from "@/sse/utils/logger"; +/** A document as the Cohere-compatible rerank API accepts it: a bare string or `{ text }`. */ +type RerankDocument = string | { text?: string }; + +/** + * The caller-side request fields the response adapters need to rebuild Cohere's + * `results[]`. Upstreams either omit the documents entirely (DeepInfra returns + * bare scores) or echo them in their own shape (Voyage returns plain strings), + * so document text is always synthesized from the caller's originals — and + * `top_n` / `return_documents` are honored here rather than upstream. + * + * Every field is optional: the parameter defaults to `{}` and the unit suites + * call it with subsets (e.g. `{ documents: ["a", "b"] }`). + */ +interface RerankResponseOptions { + documents?: RerankDocument[]; + return_documents?: boolean; + top_n?: number; +} + /** * Build authorization header for a rerank provider */ @@ -76,7 +95,11 @@ function buildAuthHeader(providerConfig, token) { /** * Transform response from provider-specific formats back to Cohere format */ -/* @testonly */ export function transformResponseFromProvider(providerConfig, data, options = {}) { +/* @testonly */ export function transformResponseFromProvider( + providerConfig, + data, + options: RerankResponseOptions = {} +) { if (providerConfig.format === "nvidia") { return { id: data.id != null ? String(data.id) : `rerank-${Date.now()}`, diff --git a/tests/unit/rerank-object-documents-response-path.test.ts b/tests/unit/rerank-object-documents-response-path.test.ts new file mode 100644 index 0000000000..6ae64afdac --- /dev/null +++ b/tests/unit/rerank-object-documents-response-path.test.ts @@ -0,0 +1,84 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { getRerankProvider } = await import("../../open-sse/config/rerankRegistry.ts"); +const { transformResponseFromProvider } = await import("../../open-sse/handlers/rerank.ts"); + +/** + * The Cohere-compatible rerank API accepts each document as either a bare string + * or `{ text }`, and the DeepInfra/Voyage **response** adapters synthesize + * `document.text` from the caller's originals (upstreams either omit documents + * or echo them in their own shape — #7809/#7811). + * + * The `{ text }` form was only ever exercised through the *request* adapter + * (`#5332 deepinfra request adapter`, `#7809 voyage request adapter handles + * {text} object documents`). Every response-adapter test passed plain strings, + * so the `typeof doc === "string" ? doc : doc?.text` branch on the response side + * was uncovered — which is also the branch that gives + * `RerankResponseOptions.documents` its union type. + */ + +test("deepinfra response adapter resolves document text from {text} objects", () => { + const cfg = getRerankProvider("deepinfra"); + const out = transformResponseFromProvider( + cfg, + { scores: [0.1, 0.9] }, + { documents: [{ text: "Washington DC" }, { text: "Paris" }], return_documents: true } + ); + + assert.equal(out.results[0].index, 1, "0.9 ranks first"); + assert.equal(out.results[0].document.text, "Paris"); + assert.equal(out.results[1].document.text, "Washington DC"); +}); + +test("deepinfra response adapter handles a mixed string/{text} document array", () => { + const cfg = getRerankProvider("deepinfra"); + const out = transformResponseFromProvider( + cfg, + { scores: [0.2, 0.8] }, + { documents: ["plain string", { text: "object form" }], return_documents: true } + ); + + assert.equal(out.results[0].document.text, "object form"); + assert.equal(out.results[1].document.text, "plain string"); +}); + +test("deepinfra response adapter falls back to empty text for a {text}-less object", () => { + const cfg = getRerankProvider("deepinfra"); + const out = transformResponseFromProvider( + cfg, + { scores: [0.5] }, + { documents: [{} as { text?: string }], return_documents: true } + ); + + assert.equal(out.results[0].document.text, "", "a document with no text must not yield undefined"); +}); + +test("voyage response adapter resolves document text from {text} objects", () => { + const cfg = getRerankProvider("voyage-ai"); + const out = transformResponseFromProvider( + cfg, + { data: [{ index: 0, relevance_score: 0.4 }, { index: 1, relevance_score: 0.7 }] }, + { documents: [{ text: "alpha" }, { text: "beta" }], return_documents: true } + ); + + assert.equal(out.results[0].index, 1, "0.7 ranks first"); + assert.equal(out.results[0].document.text, "beta"); + assert.equal(out.results[1].document.text, "alpha"); +}); + +test("voyage response adapter remaps indices past an empty {text} document", () => { + // The request adapter drops exact-empty documents before sending, so the + // upstream indices refer to the filtered array. The response adapter rebuilds + // that filter to map back — this must work for the object form too, not just + // strings. + const cfg = getRerankProvider("voyage-ai"); + const out = transformResponseFromProvider( + cfg, + { data: [{ index: 1, relevance_score: 0.9 }] }, + { documents: [{ text: "kept" }, { text: "" }, { text: "also kept" }], return_documents: true } + ); + + assert.equal(out.results[0].index, 2, "filtered index 1 maps back to original index 2"); + assert.equal(out.results[0].document.text, "also kept"); +});