refactor(sse): type the rerank response adapter's options parameter (#8528)

`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<string | { text?: string }>`
— 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 <busan011@ormbiz.co.kr>
This commit is contained in:
backryun
2026-07-26 15:51:56 +09:00
committed by GitHub
parent 0312fe2a40
commit 2cf462c1a1
2 changed files with 108 additions and 1 deletions

View File

@@ -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()}`,

View File

@@ -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");
});