Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +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

Expand Down Expand Up @@ -67,6 +68,9 @@ OpenAI API
Anthropic API
/anthropic/v1/messages — Messages
/anthropic/v1/models — List available models

Rerank API
/v1/rerank - Rerank
```

### Multi-Provider Support
Expand Down
33 changes: 33 additions & 0 deletions src/adapters/openai/v1/rerank/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// 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<string, new () => 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]();
}
}
63 changes: 63 additions & 0 deletions src/adapters/openai/v1/rerank/openai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// 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<RerankResponse> {
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,
});

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<AsyncGenerator<never>> {
throw new HTTPException(400, {
message: "Rerank API does not support streaming",
});
}
}
2 changes: 2 additions & 0 deletions src/routes/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ContextEnv>();

Expand All @@ -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);

Expand Down
45 changes: 45 additions & 0 deletions src/routes/v1/rerank.ts
Original file line number Diff line number Diff line change
@@ -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<ContextEnv>();

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;
1 change: 1 addition & 0 deletions src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export type LMRouterConfigModelType =
| "language"
| "image"
| "embedding"
| "rerank"
| "audio";

export interface LMRouterConfigModel {
Expand Down
114 changes: 114 additions & 0 deletions src/types/rerank.ts
Original file line number Diff line number Diff line change
@@ -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[];
};
}