From f890f15bbc7f1a415dd0b367e4d40d8825259728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E6=B8=B8?= Date: Mon, 6 Oct 2025 10:38:59 +0800 Subject: [PATCH 1/4] Add initial implementation for OpenAI-based rerank adapter and API integration --- README.md | 5 +- src/adapters/openai/v1/rerank/adapter.ts | 31 ++++++ src/adapters/openai/v1/rerank/openai.ts | 64 +++++++++++++ src/routes/v1/openai/v1.ts | 2 + src/routes/v1/openai/v1/rerank.ts | 45 +++++++++ src/types/config.ts | 1 + src/types/rerank.ts | 114 +++++++++++++++++++++++ 7 files changed, 260 insertions(+), 2 deletions(-) create mode 100644 src/adapters/openai/v1/rerank/adapter.ts create mode 100644 src/adapters/openai/v1/rerank/openai.ts create mode 100644 src/routes/v1/openai/v1/rerank.ts create mode 100644 src/types/rerank.ts diff --git a/README.md b/README.md index e51f079..4015385 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,9 @@ It’s an **open-source alternative to OpenRouter**, but goes far beyond languag - **Audio**: Speech-to-text and audio models - **Video**: Video generation (coming soon) - **Embeddings**: Semantic search and RAG +- **Rerank**: Semantic search and RAG - **Search**: Jina, Exa, and other web search APIs (coming soon) -- **Code**: Execution with interpreters such as e2b (coming soon) - +- **Code**: Execution with interpreters such as e2b (coming soon)´ ## Quick Start LMRouter service is available at [lmrouter.com](https://lmrouter.com). Please refer to the [documentation](https://docs.lmrouter.com/) for more information. @@ -58,6 +58,7 @@ OpenAI API /openai/v1/images/generations — Image generation /openai/v1/images/edits — Image editing /openai/v1/embeddings — Embeddings + /openai/v1/rerank - Rarank /openai/v1/responses — Responses /openai/v1/audio/speech — Audio speech /openai/v1/audio/transcriptions — Audio transcriptions diff --git a/src/adapters/openai/v1/rerank/adapter.ts b/src/adapters/openai/v1/rerank/adapter.ts new file mode 100644 index 0000000..785715c --- /dev/null +++ b/src/adapters/openai/v1/rerank/adapter.ts @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 LMRouter Contributors + +import type { + RerankRequest, + RerankResponse, +} from "../../../../types/rerank.js"; + +import { LMRouterAdapter } from "../../../adapter.js"; +import { OpenAIRerankOpenAIAdapter } from "./openai.js"; +import type { LMRouterConfigProvider } from "../../../../types/config.js"; + +export type OpenAIRerankAdapter = LMRouterAdapter< + RerankRequest, + {}, + RerankResponse, + never +>; + +const adapters: Record OpenAIRerankOpenAIAdapter> = { + others: OpenAIRerankOpenAIAdapter, +}; + +export class OpenAIRerankAdapterFactory { + static getAdapter(provider: LMRouterConfigProvider): OpenAIRerankOpenAIAdapter { + if (!Object.keys(adapters).includes(provider.type)) { + return new adapters.others(); + } + return new adapters[provider.type](); + } +} diff --git a/src/adapters/openai/v1/rerank/openai.ts b/src/adapters/openai/v1/rerank/openai.ts new file mode 100644 index 0000000..65de47c --- /dev/null +++ b/src/adapters/openai/v1/rerank/openai.ts @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 LMRouter Contributors + +import { HTTPException } from "hono/http-exception"; +import OpenAI from "openai"; +import type { + RerankRequest, + RerankResponse, +} from "../../../../types/rerank.js"; + +import type { OpenAIRerankAdapter } from "./adapter.js"; +import type { LMRouterApiCallUsage } from "../../../../types/billing.js"; +import type { LMRouterConfigProvider } from "../../../../types/config.js"; + +export class OpenAIRerankOpenAIAdapter implements OpenAIRerankAdapter { + usage?: LMRouterApiCallUsage; + + async sendRequest( + provider: LMRouterConfigProvider, + request: RerankRequest, + options?: {}, + ): Promise { + const openai = new OpenAI({ + baseURL: provider.base_url, + apiKey: provider.api_key, + defaultHeaders: { + "HTTP-Referer": "https://lmrouter.com/", + "X-Title": "LMRouter", + }, + }); + const rerank: RerankResponse = await openai.request({ + method: "post", + path: "/rerank", + body: request, + }); + console.log(rerank); + + if (rerank && "usage" in rerank) { + this.usage = { + input: rerank.usage.total_tokens, + request: 1, + }; + } + + if (rerank && "meta" in rerank) { + this.usage = { + input: rerank.meta?.tokens?.input_tokens, + output: rerank.meta?.tokens?.output_tokens, + request: 1, + }; + } + return rerank; + } + + async sendRequestStreaming( + provider: LMRouterConfigProvider, + request: RerankRequest, + options?: {}, + ): Promise> { + throw new HTTPException(400, { + message: "Rerank API does not support streaming", + }); + } +} diff --git a/src/routes/v1/openai/v1.ts b/src/routes/v1/openai/v1.ts index 3a2b3c1..982c91a 100644 --- a/src/routes/v1/openai/v1.ts +++ b/src/routes/v1/openai/v1.ts @@ -9,6 +9,7 @@ import chatRouter from "./v1/chat.js"; import embeddingsRouter from "./v1/embeddings.js"; import imagesRouter from "./v1/images.js"; import modelsRouter from "./v1/models.js"; +import rerankRouter from "./v1/rerank.js" import responsesRouter from "./v1/responses.js"; const openaiV1Router = new Hono(); @@ -18,6 +19,7 @@ openaiV1Router.route("/chat", chatRouter); openaiV1Router.route("/embeddings", embeddingsRouter); openaiV1Router.route("/images", imagesRouter); openaiV1Router.route("/models", modelsRouter); +openaiV1Router.route("/rerank", rerankRouter); openaiV1Router.route("/responses", responsesRouter); export default openaiV1Router; diff --git a/src/routes/v1/openai/v1/rerank.ts b/src/routes/v1/openai/v1/rerank.ts new file mode 100644 index 0000000..1dd6448 --- /dev/null +++ b/src/routes/v1/openai/v1/rerank.ts @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 LMRouter Contributors + +import { Hono } from "hono"; + +import { OpenAIRerankAdapterFactory } from "../../../../adapters/openai/v1/rerank/adapter.js"; +import { requireAuth } from "../../../../middlewares/auth.js"; +import { ensureBalance } from "../../../../middlewares/billing.js"; +import { parseModel } from "../../../../middlewares/model.js"; +import type { ContextEnv } from "../../../../types/hono.js"; +import { recordApiCall } from "../../../../utils/billing.js"; +import { TimeKeeper } from "../../../../utils/chrono.js"; +import { iterateModelProviders } from "../../../../utils/utils.js"; +import type { RerankRequest } from "../../../../types/rerank.js"; + +const rerankRouter = new Hono(); + +rerankRouter.use(requireAuth(), ensureBalance, parseModel); + +rerankRouter.post("/", async (c) => { + const body = await c.req.json(); + return await iterateModelProviders(c, async (providerCfg, provider) => { + const reqBody = { ...body } as RerankRequest; + reqBody.model = providerCfg.model; + + const adapter = OpenAIRerankAdapterFactory.getAdapter(provider); + const timeKeeper = new TimeKeeper(); + timeKeeper.record(); + const response = await adapter.sendRequest(provider, reqBody); + timeKeeper.record(); + await recordApiCall( + c, + providerCfg.provider, + 200, + timeKeeper.timestamps(), + adapter.usage, + providerCfg.pricing, + undefined, + false, + ); + return c.json(response); + }); +}); + +export default rerankRouter; diff --git a/src/types/config.ts b/src/types/config.ts index bfaf928..71c0197 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -123,6 +123,7 @@ export type LMRouterConfigModelType = | "language" | "image" | "embedding" + | "rerank" | "audio"; export interface LMRouterConfigModel { diff --git a/src/types/rerank.ts b/src/types/rerank.ts new file mode 100644 index 0000000..d968f2c --- /dev/null +++ b/src/types/rerank.ts @@ -0,0 +1,114 @@ +export type RerankRequest = + | JinaRerankRequest + | CohereRerankV1Request + | CohereRerankV2Request; + +export type RerankResponse = + | JinaRerankResponse + | CohereRerankV1Response + | CohereRerankV2Response; + +export interface JinaRerankRequest { + model: string; + query: + | string + | { + text?: string; + image?: string; + }; + documents: + | string[] + | { + image?: string; + text?: string; + }[]; + top_k?: number; + return_documents: boolean; +} + +export interface JinaRerankResponse { + model: string; + object: string; + usage: { + total_tokens: number; + }; + results: { + index: number; + relevance_score: number; + document?: string; + }; +} + +export interface CohereRerankV1Request { + model?: string; + query: string; + documents: string[]; + top_k?: number; + rank_fields?: string[]; + return_documents: boolean; + max_chunks_per_doc?: number; +} + +export interface CohereRerankV2Request { + model: string; + query: string; + documents: string[]; + top_k?: number; + max_tokens_per_doc?: number; +} + +export interface CohereRerankV1Response { + id?: string; + results: { + index: number; + relevance_score: number; + document?: { text: string }; + }[]; + meta?: { + api_version?: { + version: string; + is_deprecated?: boolean; + is_experimental?: boolean; + }; + billed_units?: { + images?: number; + input_tokens?: number; + output_tokens?: number; + search_units?: number; + classifications?: number; + }; + tokens?: { + input_tokens?: number; + output_tokens?: number; + }; + cached_tokens?: number; + warnings?: string[]; + }; +} +export interface CohereRerankV2Response { + id?: string; + results: { + index: number; + relevance_score: number; + }[]; + meta?: { + api_version?: { + version: string; + is_deprecated?: boolean; + is_experimental?: boolean; + }; + billed_units?: { + images?: number; + input_tokens?: number; + output_tokens?: number; + search_units?: number; + classifications?: number; + }; + tokens?: { + input_tokens?: number; + output_tokens?: number; + }; + cached_tokens?: number; + warnings?: string[]; + }; +} From f2489f89f5910a476f4f4f8d5242d092b68c89ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E6=B8=B8?= Date: Mon, 6 Oct 2025 11:09:04 +0800 Subject: [PATCH 2/4] Refactor rerank router --- README.md | 4 +++- src/routes/v1.ts | 2 ++ src/routes/v1/openai/v1.ts | 2 -- src/routes/v1/{openai/v1 => }/rerank.ts | 18 +++++++++--------- 4 files changed, 14 insertions(+), 12 deletions(-) rename src/routes/v1/{openai/v1 => }/rerank.ts (60%) diff --git a/README.md b/README.md index 4015385..1848b9a 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,6 @@ OpenAI API /openai/v1/images/generations — Image generation /openai/v1/images/edits — Image editing /openai/v1/embeddings — Embeddings - /openai/v1/rerank - Rarank /openai/v1/responses — Responses /openai/v1/audio/speech — Audio speech /openai/v1/audio/transcriptions — Audio transcriptions @@ -68,6 +67,9 @@ OpenAI API Anthropic API /anthropic/v1/messages — Messages /anthropic/v1/models — List available models + +Rerank API + /v1/rerank - Rerank ``` ### Multi-Provider Support diff --git a/src/routes/v1.ts b/src/routes/v1.ts index b5cfade..e35de50 100644 --- a/src/routes/v1.ts +++ b/src/routes/v1.ts @@ -14,6 +14,7 @@ import billingRouter from "./v1/billing.js"; import modelsRouter from "./v1/models.js"; import openaiRouter from "./v1/openai.js"; import providersRouter from "./v1/providers.js"; +import rerankRouter from "./v1/rerank.js"; const v1Router = new Hono(); @@ -37,6 +38,7 @@ v1Router.on(["GET", "POST"], "/auth/**", (c) => { v1Router.route("/billing", billingRouter); v1Router.route("/models", modelsRouter); +v1Router.route("/rerank", rerankRouter); v1Router.route("/openai", openaiRouter); v1Router.route("/providers", providersRouter); diff --git a/src/routes/v1/openai/v1.ts b/src/routes/v1/openai/v1.ts index 982c91a..3a2b3c1 100644 --- a/src/routes/v1/openai/v1.ts +++ b/src/routes/v1/openai/v1.ts @@ -9,7 +9,6 @@ import chatRouter from "./v1/chat.js"; import embeddingsRouter from "./v1/embeddings.js"; import imagesRouter from "./v1/images.js"; import modelsRouter from "./v1/models.js"; -import rerankRouter from "./v1/rerank.js" import responsesRouter from "./v1/responses.js"; const openaiV1Router = new Hono(); @@ -19,7 +18,6 @@ openaiV1Router.route("/chat", chatRouter); openaiV1Router.route("/embeddings", embeddingsRouter); openaiV1Router.route("/images", imagesRouter); openaiV1Router.route("/models", modelsRouter); -openaiV1Router.route("/rerank", rerankRouter); openaiV1Router.route("/responses", responsesRouter); export default openaiV1Router; diff --git a/src/routes/v1/openai/v1/rerank.ts b/src/routes/v1/rerank.ts similarity index 60% rename from src/routes/v1/openai/v1/rerank.ts rename to src/routes/v1/rerank.ts index 1dd6448..62fa5c7 100644 --- a/src/routes/v1/openai/v1/rerank.ts +++ b/src/routes/v1/rerank.ts @@ -3,15 +3,15 @@ import { Hono } from "hono"; -import { OpenAIRerankAdapterFactory } from "../../../../adapters/openai/v1/rerank/adapter.js"; -import { requireAuth } from "../../../../middlewares/auth.js"; -import { ensureBalance } from "../../../../middlewares/billing.js"; -import { parseModel } from "../../../../middlewares/model.js"; -import type { ContextEnv } from "../../../../types/hono.js"; -import { recordApiCall } from "../../../../utils/billing.js"; -import { TimeKeeper } from "../../../../utils/chrono.js"; -import { iterateModelProviders } from "../../../../utils/utils.js"; -import type { RerankRequest } from "../../../../types/rerank.js"; +import { OpenAIRerankAdapterFactory } from "../../adapters/openai/v1/rerank/adapter.js"; +import { requireAuth } from "../../middlewares/auth.js"; +import { ensureBalance } from "../../middlewares/billing.js"; +import { parseModel } from "../../middlewares/model.js"; +import type { ContextEnv } from "../../types/hono.js"; +import { recordApiCall } from "../../utils/billing.js"; +import { TimeKeeper } from "../../utils/chrono.js"; +import { iterateModelProviders } from "../../utils/utils.js"; +import type { RerankRequest } from "../../types/rerank.js"; const rerankRouter = new Hono(); From 843e64607f8e54d7877730ee96f0dc9b6dbf081f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E6=B8=B8?= Date: Mon, 6 Oct 2025 11:10:28 +0800 Subject: [PATCH 3/4] fix lint --- README.md | 1 + src/adapters/openai/v1/rerank/adapter.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1848b9a..104f8b8 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ It’s an **open-source alternative to OpenRouter**, but goes far beyond languag - **Rerank**: Semantic search and RAG - **Search**: Jina, Exa, and other web search APIs (coming soon) - **Code**: Execution with interpreters such as e2b (coming soon)´ + ## Quick Start LMRouter service is available at [lmrouter.com](https://lmrouter.com). Please refer to the [documentation](https://docs.lmrouter.com/) for more information. diff --git a/src/adapters/openai/v1/rerank/adapter.ts b/src/adapters/openai/v1/rerank/adapter.ts index 785715c..447ebbf 100644 --- a/src/adapters/openai/v1/rerank/adapter.ts +++ b/src/adapters/openai/v1/rerank/adapter.ts @@ -22,7 +22,9 @@ const adapters: Record OpenAIRerankOpenAIAdapter> = { }; export class OpenAIRerankAdapterFactory { - static getAdapter(provider: LMRouterConfigProvider): OpenAIRerankOpenAIAdapter { + static getAdapter( + provider: LMRouterConfigProvider, + ): OpenAIRerankOpenAIAdapter { if (!Object.keys(adapters).includes(provider.type)) { return new adapters.others(); } From 49d54f4f889fb0b4717fc4ae1020daec8468410e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E6=B8=B8?= Date: Mon, 6 Oct 2025 13:22:07 +0800 Subject: [PATCH 4/4] Remove leftover console.log from OpenAI rerank adapter --- src/adapters/openai/v1/rerank/openai.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/adapters/openai/v1/rerank/openai.ts b/src/adapters/openai/v1/rerank/openai.ts index 65de47c..315f025 100644 --- a/src/adapters/openai/v1/rerank/openai.ts +++ b/src/adapters/openai/v1/rerank/openai.ts @@ -33,7 +33,6 @@ export class OpenAIRerankOpenAIAdapter implements OpenAIRerankAdapter { path: "/rerank", body: request, }); - console.log(rerank); if (rerank && "usage" in rerank) { this.usage = {