akm-cli
Version:
akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur
114 lines (113 loc) • 5.18 kB
JavaScript
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
/**
* HTTP client for a standalone cross-encoder rerank endpoint (#951).
*
* `curate_rerank` was a dead `llm.features.*` key removed in 0.8.0 — no wire
* call was ever implemented under it. This is the first real implementation:
* a small, dedicated client (NOT routed through `llm/client.ts`'s chat-
* completions transport, since a reranker speaks a different, much smaller
* contract) for the request/response shape a TEI/Cohere-style `/rerank`
* endpoint accepts:
*
* POST <endpoint>
* { "model": "<name>", "query": "<text>", "documents": ["<text>", ...] }
*
* 200 OK
* { "results": [ { "index": 0, "relevance_score": 0.83 }, ... ] }
*
* `results` need not be sorted or complete — {@link rerankDocuments} sorts by
* `relevance_score` descending and callers treat a missing index as
* "unscored" (kept in its original relative position, after every scored
* document). Deliberately independent of `EngineConfigSchema`'s "llm"/"agent"
* kinds — see the comment on `CurateRerankConfigSchema` in
* `core/config/schema/search.ts` for why.
*/
import { fetchWithTimeout, readBodyWithByteCap } from "../core/common.js";
import { resolveSecret } from "../core/config/config.js";
import { isApiKeyReference } from "../core/config/schema/primitives.js";
import { redactErrorBody, redactSensitiveText } from "../core/redaction.js";
import { resolveSecretFromStore } from "../sources/snapshot-fetchers/secret-seam.js";
const DEFAULT_RERANK_TIMEOUT_MS = 10_000;
export class RerankCallError extends Error {
code;
constructor(message, code) {
super(message);
this.name = "RerankCallError";
this.code = code;
}
}
/**
* Call a configured rerank endpoint and return documents ordered by
* relevance to `query`, most relevant first. Any document the endpoint
* didn't return a score for keeps its original relative order, appended
* after every scored document (never dropped).
*
* Throws {@link RerankCallError} on any transport/parse failure — callers
* that want a graceful fallback should use `tryLlmFeature("curate_rerank", ...)`
* (`llm/feature-gate.ts`), matching every other bounded in-tree LLM/rerank
* call site.
*/
export async function rerankDocuments(config, query, documents) {
if (!config.endpoint) {
throw new RerankCallError("search.curateRerank.endpoint is not configured.", "provider_error");
}
if (documents.length === 0)
return [];
const resolvedKey = config.apiKey && isApiKeyReference(config.apiKey)
? resolveSecret(config.apiKey, resolveSecretFromStore)
: config.apiKey;
const headers = { "Content-Type": "application/json" };
if (resolvedKey)
headers.Authorization = `Bearer ${resolvedKey}`;
const timeoutMs = config.timeoutMs ?? DEFAULT_RERANK_TIMEOUT_MS;
const requestBody = JSON.stringify({
...(config.model ? { model: config.model } : {}),
query,
documents,
});
let response;
try {
response = await fetchWithTimeout(config.endpoint, { method: "POST", headers, body: requestBody }, timeoutMs);
}
catch (err) {
const msg = err instanceof Error ? err.message : String(err);
if (msg.includes("timed out")) {
throw new RerankCallError(`Rerank request timed out after ${timeoutMs}ms`, "timeout");
}
throw new RerankCallError(`Rerank network error: ${msg}`, "network_error");
}
if (!response.ok) {
const rawBody = await readBodyWithByteCap(response).catch(() => "");
const safeBody = redactSensitiveText(redactErrorBody(rawBody), resolvedKey ? [resolvedKey] : []);
throw new RerankCallError(`Rerank request failed (${response.status}) ${config.endpoint}: ${safeBody}`, "provider_error");
}
const rawBody = await readBodyWithByteCap(response);
let json;
try {
json = JSON.parse(rawBody);
}
catch {
throw new RerankCallError(`Rerank response was not valid JSON ${config.endpoint}: ${redactSensitiveText(redactErrorBody(rawBody), resolvedKey ? [resolvedKey] : [])}`, "parse_error");
}
if (!Array.isArray(json.results)) {
throw new RerankCallError(`Rerank response from ${config.endpoint} has no "results" array.`, "parse_error");
}
const scored = new Map();
for (const entry of json.results) {
if (typeof entry.index !== "number")
continue;
const score = typeof entry.relevance_score === "number" ? entry.relevance_score : entry.score;
if (typeof score === "number")
scored.set(entry.index, score);
}
const rankedScored = [...scored.entries()]
.map(([index, score]) => ({ index, score }))
.sort((a, b) => b.score - a.score);
const unscored = documents
.map((_, index) => index)
.filter((index) => !scored.has(index))
.map((index) => ({ index, score: Number.NEGATIVE_INFINITY }));
return [...rankedScored, ...unscored];
}