Skip to content

Commit 2a00fe0

Browse files
committed
Initial commit: rerank client on the Interchange inference primitives
`rerankDocuments` posts a query and candidate set to a reranker, maps the reply back to caller-supplied document ids, and returns {id, score} sorted descending. Three wire formats, because reranking has no OpenAI-compatible standard and is not converging on one — TEI does not serve an OpenAI-shaped rerank route at all (huggingface/text-embeddings-inference#683): tei /rerank {query, texts} -> [{index, score}] cohere /v2/rerank {query, documents} -> {results: [{index, relevance_score}]} voyage /v1/rerank {query, documents} -> {data: [{index, relevance_score}]} Jina's /rerank uses Cohere's shape and is served by that adapter. This is a real adapter boundary rather than a knob, which is the opposite conclusion from @corbits/embedding — there, every provider serves one /v1/embeddings shape and only the parameters differ. Cohere and Voyage require a model and fail by name at request-build time rather than as a 400 in production; TEI serves whatever model it was started with. Scores are mapped through docs[index], never by reply position. Every protocol addresses documents by their offset in the request array and TEI's reply is explicitly unordered, so trusting position would attach the top score to the wrong document. An out-of-range index raises. RerankDoc.id exists for this reason: the wire formats only speak in offsets, so translating back to a stable identifier belongs here rather than in every caller. Built on @intx/inference rather than reimplementing it. Requests are BuiltRequests sent through the single deps.fetch path; deps.scheduler drives elapsed time and backoff so virtual-clock tests run the retry loop with no wall time; failures classify through the shared taxonomy, including a 200 whose body is not JSON (classifyProtocolMismatch), which a local model server behind a proxy will produce; createDefaultRetryPolicy decides back-off vs abort, and an abort mid-delay wakes immediately as the harness documents. The client this replaces had no retry at all. RerankAdapter mirrors ProviderAdapter — same buildRequest/parseResponse members, same optional extractRetryAfterMs. Styles resolve through a Map-backed registry mirroring createAdapterRegistry, so an apiStyle from config cannot reach an inherited Object.prototype member and fail later as an opaque TypeError. deps is Pick<Dependencies, "fetch" | "scheduler">: callers should not have to assemble a registry of inference providers in order to rerank a list. Failures raise rather than degrading soft. A reranker outage should be a policy decision at the call site — a retrieval pipeline usually wants to fall back to fused ordering — and it cannot make that choice if the failure is hidden here. Naming follows the published @intX surface rather than assumption: baseURL (used 18 times upstream, baseUrl zero), uppercase acronyms in type names, lowercase leading acronym in apiKey/apiStyle to match upstream's own apiKey. Tests: 21 pass. Typecheck, prettier and build clean, all four gated in CI. The index-mapping and retry paths were regression-verified by reintroducing each bug and confirming the tests fail. Refs CL-4539, CL-4549, CL-4550 Claude-Session: https://claude.ai/code/session_017GTgGzn5xAwvkU2GAPAHpF
0 parents  commit 2a00fe0

14 files changed

Lines changed: 1609 additions & 0 deletions

‎.github/workflows/ci.yml‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
check:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: oven-sh/setup-bun@v2
14+
with:
15+
bun-version: latest
16+
- run: bun install --frozen-lockfile
17+
- run: bunx tsc --noEmit
18+
- run: bun test ./src
19+
- run: bunx prettier --check src
20+
- run: bun run build

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
dist
3+
*.tsbuildinfo

‎.prettierrc.json‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"trailingComma": "all",
3+
"tabWidth": 2,
4+
"useTabs": false,
5+
"semi": true,
6+
"singleQuote": false
7+
}

‎LICENSE‎

Lines changed: 501 additions & 0 deletions
Large diffs are not rendered by default.

‎README.md‎

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# @corbits/reranking
2+
3+
Cross-encoder rerank client. Given a query and a candidate set, `rerankDocuments`
4+
posts them to a reranker, maps the reply back to your own document ids, and
5+
returns `{id, score}` sorted descending.
6+
7+
Unlike embeddings, reranking has **no OpenAI-compatible standard** and is not
8+
converging on one — TEI does not serve an OpenAI-shaped rerank route at all
9+
([text-embeddings-inference#683](https://github.com/huggingface/text-embeddings-inference/issues/683)).
10+
So this is a real adapter boundary, not a knob:
11+
12+
| `apiStyle` | endpoint | request | response |
13+
| ---------- | ------------ | -------------------- | --------------------------------------- |
14+
| `tei` | `/rerank` | `{query, texts}` | `[{index, score}]` |
15+
| `cohere` | `/v2/rerank` | `{query, documents}` | `{results: [{index, relevance_score}]}` |
16+
| `voyage` | `/v1/rerank` | `{query, documents}` | `{data: [{index, relevance_score}]}` |
17+
18+
Jina's `/rerank` uses Cohere's shape and is served by the `cohere` adapter.
19+
20+
Transport, error classification and retry come from `@intx/inference` rather
21+
than being reimplemented: requests are `BuiltRequest`s sent through the single
22+
`deps.fetch` path, failures become an `InferenceError` through the same
23+
classifiers a chat call uses, and `createDefaultRetryPolicy` decides whether to
24+
back off or abort.
25+
26+
`RerankAdapter` deliberately mirrors inference's `ProviderAdapter` — same
27+
`buildRequest` / `parseResponse` members, same optional `extractRetryAfterMs`
28+
for a provider that signals pacing its own way. Styles resolve through
29+
`createRerankAdapterRegistry`, which closes over a private `Map` copy so an
30+
`apiStyle` arriving from config or an environment variable cannot reach an
31+
inherited member of `Object.prototype` and fail later as an opaque `TypeError`.
32+
Pass your own `registry` to add a house format without forking the package.
33+
34+
```ts
35+
import { createDefaultScheduler } from "@intx/inference";
36+
import { rerankDocuments } from "@corbits/reranking";
37+
38+
// Only `fetch` and `scheduler` are needed — a full harness `Dependencies`
39+
// also satisfies this if you already have one.
40+
const deps = { fetch, scheduler: createDefaultScheduler() };
41+
42+
const ranked = await rerankDocuments(
43+
"how do I deploy to staging?",
44+
[
45+
{ id: "doc-1", text: "Staging deploys run from main…" },
46+
{ id: "doc-2", text: "On-call rotation is weekly…" },
47+
],
48+
{ baseURL: "http://localhost:8085", apiStyle: "tei" },
49+
{ deps },
50+
);
51+
```
52+
53+
## Scores are mapped by index, never by position
54+
55+
Every one of these protocols addresses documents by their position in the
56+
_request_ array, and TEI's reply is explicitly unordered. Results are therefore
57+
mapped back through `docs[index]`, and an out-of-range index raises rather than
58+
silently attaching a score to the wrong document.
59+
60+
`RerankDoc.id` exists for this reason: the wire formats only speak in array
61+
offsets, so translating back to a stable identifier is this package's job rather
62+
than the caller's.
63+
64+
## Degrading soft is the caller's decision
65+
66+
This package raises on failure. It does not swallow errors, because a reranker
67+
outage should be a policy decision at the call site — a retrieval pipeline
68+
typically wants to fall back to its fused ordering and carry on, which it cannot
69+
do if the failure is hidden here.
70+
71+
## Errors
72+
73+
Every failure — transport, HTTP status, or a 200 whose body is not JSON —
74+
raises `ModelRequestError`, carrying the classified `InferenceError` as `reason`
75+
and the URL.
76+
77+
Note that `@corbits/embedding` and `@corbits/reranking` each carry their own
78+
copy of this class until the shared transport is upstreamed, so `instanceof`
79+
does not hold across the two. Catching both? Discriminate on
80+
`error.name === "ModelRequestError"`.
81+
82+
## Versioning
83+
84+
Semver. Released manually with `bun run build && npm publish` once CI is green.
85+
86+
## Development
87+
88+
```bash
89+
bun install
90+
bun test ./src
91+
bunx tsc --noEmit
92+
```
93+
94+
## License
95+
96+
LGPL-2.1-only — see [`LICENSE`](LICENSE).

‎bun.lock‎

Lines changed: 60 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"name": "@corbits/reranking",
3+
"description": "Cross-encoder rerank client with TEI, Cohere/Jina and Voyage adapters, built on the Interchange inference primitives",
4+
"version": "0.1.0",
5+
"license": "LGPL-2.1-only",
6+
"type": "module",
7+
"exports": {
8+
".": {
9+
"intx-src": "./src/index.ts",
10+
"types": "./dist/index.d.ts",
11+
"default": "./dist/index.js"
12+
}
13+
},
14+
"scripts": {
15+
"build": "tsc -p tsconfig.build.json",
16+
"typecheck": "tsc --noEmit",
17+
"test": "bun test ./src",
18+
"format": "prettier -w ."
19+
},
20+
"dependencies": {
21+
"@intx/inference": "0.2.2",
22+
"@intx/log": "0.2.2",
23+
"@intx/types": "0.2.2",
24+
"arktype": "^2.1.29"
25+
},
26+
"devDependencies": {
27+
"@intx/inference-testing": "0.2.2",
28+
"@types/bun": "1.3.9",
29+
"prettier": "3.6.2",
30+
"typescript": "5.9.3"
31+
},
32+
"files": ["dist", "README.md", "LICENSE"],
33+
"sideEffects": false,
34+
"publishConfig": {
35+
"access": "public"
36+
},
37+
"repository": {
38+
"type": "git",
39+
"url": "git+https://github.com/corbitsdev/corbits-reranking.git"
40+
},
41+
"engines": {
42+
"bun": ">=1.2.0"
43+
}
44+
}

0 commit comments

Comments
 (0)