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
5 changes: 5 additions & 0 deletions .changeset/reranking-runtime-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@openrouter/ai-sdk-provider": patch

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] Changeset is marked patch — a new public API method is conventionally a minor bump.

Suggested change
"@openrouter/ai-sdk-provider": patch
"@openrouter/ai-sdk-provider": minor
Details

Why: This PR adds a brand-new public surface — provider.rerankingModel() and the exported OpenRouterRerankingModel / OpenRouterRerankingSettings types. Per semver, new backwards-compatible API additions are minor, not patch. The most recent comparable change in this repo — #479, which added provider.videoModel() — shipped as a Minor changeset (see CHANGELOG.md). Marking this patch means the new method lands in a patch release, which under-signals the feature to consumers pinning on ~ ranges.

Non-blocking: if the maintainers intentionally batch feature additions into patch releases for this package, disregard.

Reviewed at bd948de

---

Add runtime support for `rerankingModel()` on the OpenRouter provider.
26 changes: 26 additions & 0 deletions src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import type {
OpenRouterImageModelId,
OpenRouterImageSettings,
} from './types/openrouter-image-settings';
import type {
OpenRouterRerankingModelId,
OpenRouterRerankingSettings,
} from './types/openrouter-reranking-settings';
import type {
OpenRouterVideoModelId,
OpenRouterVideoSettings,
Expand All @@ -27,6 +31,7 @@ import { OpenRouterChatLanguageModel } from './chat';
import { OpenRouterCompletionLanguageModel } from './completion';
import { OpenRouterEmbeddingModel } from './embedding';
import { OpenRouterImageModel } from './image';
import { OpenRouterRerankingModel } from './reranking';
import { webSearch } from './tool/web-search';
import { withUserAgentSuffix } from './utils/with-user-agent-suffix';
import { VERSION } from './version';
Expand Down Expand Up @@ -115,6 +120,14 @@ Creates an OpenRouter video model for video generation.
settings?: OpenRouterVideoSettings,
): OpenRouterVideoModel;

/**
Creates an OpenRouter reranking model.
*/
rerankingModel(
modelId: OpenRouterRerankingModelId,
settings?: OpenRouterRerankingSettings,
): OpenRouterRerankingModel;

/**
* Provider-defined tools for OpenRouter server tools.
*/
Expand Down Expand Up @@ -280,6 +293,18 @@ export function createOpenRouter(
extraBody: options.extraBody,
});

const createRerankingModel = (
modelId: OpenRouterRerankingModelId,
settings: OpenRouterRerankingSettings = {},
) =>
new OpenRouterRerankingModel(modelId, settings, {
provider: 'openrouter.reranking',
url: ({ path }) => `${baseURL}${path}`,
headers: getHeaders,
fetch: options.fetch,
extraBody: options.extraBody,
});

const createLanguageModel = (
modelId: OpenRouterChatModelId | OpenRouterCompletionModelId,
settings?: OpenRouterChatSettings | OpenRouterCompletionSettings,
Expand Down Expand Up @@ -312,6 +337,7 @@ export function createOpenRouter(
provider.embedding = createEmbeddingModel; // deprecated alias for v4 compatibility
provider.imageModel = createImageModel;
provider.videoModel = createVideoModel;
provider.rerankingModel = createRerankingModel;
provider.tools = {
webSearch: webSearch,
};
Expand Down
91 changes: 91 additions & 0 deletions src/reranking/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { describe, expect, it } from 'vitest';
import { createOpenRouter } from '../provider';
import { OpenRouterRerankingModel } from './index';

describe('OpenRouterRerankingModel', () => {
describe('provider methods', () => {
it('should expose rerankingModel method', () => {
const provider = createOpenRouter({ apiKey: 'test-api-key' });
expect(provider.rerankingModel).toBeDefined();
expect(typeof provider.rerankingModel).toBe('function');
});

it('should create a reranking model instance', () => {
const provider = createOpenRouter({ apiKey: 'test-api-key' });
const model = provider.rerankingModel('cohere/rerank-v3.5');
expect(model).toBeInstanceOf(OpenRouterRerankingModel);
expect(model.modelId).toBe('cohere/rerank-v3.5');
expect(model.provider).toBe('openrouter');
expect(model.specificationVersion).toBe('v3');
});
});

describe('doRerank', () => {
it('should rerank text documents', async () => {
let capturedUrl: string | undefined;
let capturedRequest: Record<string, unknown> | undefined;

const mockFetch = async (
url: URL | RequestInfo,
init?: RequestInit,
): Promise<Response> => {
capturedUrl = url.toString();
capturedRequest = JSON.parse(init?.body as string);
return new Response(
JSON.stringify({
id: 'rerank-test-id',
model: 'cohere/rerank-v3.5',
results: [
{ index: 1, relevance_score: 0.98 },
{ index: 0, relevance_score: 0.12 },
],
usage: {
prompt_tokens: 12,
total_tokens: 12,
cost: 0.00002,
},
}),
{
status: 200,
headers: {
'content-type': 'application/json',
},
},
);
};

const provider = createOpenRouter({
apiKey: 'test-api-key',
fetch: mockFetch,
});
const model = provider.rerankingModel('cohere/rerank-v3.5');

const result = await model.doRerank({
query: 'capital of France',
documents: {
type: 'text',
values: ['Berlin is in Germany', 'Paris is in France'],
},
topN: 2,
});

expect(capturedUrl).toBe('https://openrouter.ai/api/v1/rerank');
expect(capturedRequest).toMatchObject({
model: 'cohere/rerank-v3.5',
query: 'capital of France',
documents: ['Berlin is in Germany', 'Paris is in France'],
top_n: 2,
});
expect(result.ranking).toEqual([
{ index: 1, relevanceScore: 0.98 },
{ index: 0, relevanceScore: 0.12 },
]);
expect(result.response?.id).toBe('rerank-test-id');
expect(result.response?.modelId).toBe('cohere/rerank-v3.5');
expect(
(result.providerMetadata?.openrouter as { usage?: { cost?: number } })
?.usage?.cost,
).toBe(0.00002);
});
});
});
110 changes: 110 additions & 0 deletions src/reranking/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import type {
JSONObject,
RerankingModelV3,
SharedV3Headers,
SharedV3ProviderMetadata,
} from '@ai-sdk/provider';
import type {
OpenRouterRerankingModelId,
OpenRouterRerankingSettings,
} from '../types/openrouter-reranking-settings';

import {
combineHeaders,
createJsonResponseHandler,
postJsonToApi,
} from '@ai-sdk/provider-utils';
import { openrouterFailedResponseHandler } from '../schemas/error-response';
import { OpenRouterProviderMetadataSchema } from '../schemas/provider-metadata';
import { OpenRouterRerankingResponseSchema } from './schemas';

type OpenRouterRerankingConfig = {
provider: string;
headers: () => Record<string, string | undefined>;
url: (options: { modelId: string; path: string }) => string;
fetch?: typeof fetch;
extraBody?: Record<string, unknown>;
};

export class OpenRouterRerankingModel implements RerankingModelV3 {
readonly specificationVersion = 'v3' as const;
readonly provider = 'openrouter';
readonly modelId: OpenRouterRerankingModelId;
readonly settings: OpenRouterRerankingSettings;

private readonly config: OpenRouterRerankingConfig;

constructor(
modelId: OpenRouterRerankingModelId,
settings: OpenRouterRerankingSettings,
config: OpenRouterRerankingConfig,
) {
this.modelId = modelId;
this.settings = settings;
this.config = config;
}

async doRerank({
documents,
query,
topN,
abortSignal,
headers,
}: Parameters<RerankingModelV3['doRerank']>[0]): Promise<
Awaited<ReturnType<RerankingModelV3['doRerank']>>
> {
const documentValues: string[] | JSONObject[] = documents.values;
const args = {
model: this.modelId,
query,
documents: documentValues,
top_n: topN,
user: this.settings.user,
provider: this.settings.provider,
...this.config.extraBody,
...this.settings.extraBody,
};

const { value: responseValue, responseHeaders } = await postJsonToApi({
url: this.config.url({
path: '/rerank',
modelId: this.modelId,
}),
headers: combineHeaders(this.config.headers(), headers),
body: args,
failedResponseHandler: openrouterFailedResponseHandler,
successfulResponseHandler: createJsonResponseHandler(
OpenRouterRerankingResponseSchema,
),
abortSignal,
fetch: this.config.fetch,
});

return {
ranking: responseValue.results.map((result) => ({
index: result.index,
relevanceScore: result.relevance_score,
})),
providerMetadata: {
openrouter: OpenRouterProviderMetadataSchema.parse({
provider: '',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] provider: '' hardcodes the provider name to empty — the sibling embedding model reads it from the response instead.

Suggested change
provider: '',
provider: responseValue.provider ?? '',
Details

Why: Every other model in this provider surfaces the upstream provider slug in providerMetadata.openrouter.provider (e.g. src/embedding/index.ts:95 does provider: responseValue.provider ?? ''). Here it is pinned to '', so callers can never tell which upstream provider (cohere, jina, etc.) actually served the rerank — useful signal that the rest of the SDK exposes consistently.

The rerank response schema (src/reranking/schemas.ts) uses .passthrough(), so a provider field returned by the API is already preserved on responseValue at runtime; the only missing piece is surfacing it. If the API does not return provider on /rerank, the ?? '' fallback keeps current behaviour, so the change is safe either way. Optionally add provider: z.string().optional() to the schema to make the field explicit.

Reviewed at bd948de

usage: {
promptTokens: responseValue.usage?.prompt_tokens ?? 0,
completionTokens: 0,
totalTokens: responseValue.usage?.total_tokens ?? 0,
...(responseValue.usage?.cost != null
? { cost: responseValue.usage.cost }
: {}),
},
}),
} satisfies SharedV3ProviderMetadata,
response: {
id: responseValue.id,
modelId: responseValue.model,
headers: responseHeaders as SharedV3Headers,
body: responseValue,
},
warnings: [],
};
}
}
28 changes: 28 additions & 0 deletions src/reranking/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { z } from 'zod/v4';

export const OpenRouterRerankingResponseSchema = z
.object({
id: z.string().optional(),
model: z.string().optional(),
results: z.array(
z
.object({
index: z.number(),
relevance_score: z.number(),
})
.passthrough(),
),
usage: z
.object({
prompt_tokens: z.number().optional(),
total_tokens: z.number().optional(),
cost: z.number().optional(),
})
.passthrough()
.optional(),
})
.passthrough();

export type OpenRouterRerankingResponse = z.infer<
typeof OpenRouterRerankingResponseSchema
>;
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type { LanguageModelV3, LanguageModelV3Prompt };

export * from './openrouter-embedding-settings';
export * from './openrouter-image-settings';
export * from './openrouter-reranking-settings';
export * from './openrouter-video-settings';

export type OpenRouterProviderOptions = {
Expand Down
40 changes: 40 additions & 0 deletions src/types/openrouter-reranking-settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { OpenRouterSharedSettings } from '..';

// https://openrouter.ai/models?fmt=cards&supported_parameters=rerank
export type OpenRouterRerankingModelId = string;

export type OpenRouterRerankingSettings = {
/**
* Provider routing preferences to control request routing behavior.
*/
provider?: {
/**
* List of provider slugs to try in order.
*/
order?: string[];
/**
* Whether to allow backup providers when primary is unavailable.
*/
allow_fallbacks?: boolean;
/**
* Only use providers that support all parameters in your request.
*/
require_parameters?: boolean;
/**
* Control whether to use providers that may store data.
*/
data_collection?: 'allow' | 'deny';
/**
* List of provider slugs to allow for this request.
*/
only?: string[];
/**
* List of provider slugs to skip for this request.
*/
ignore?: string[];
/**
* Sort providers by price, throughput, or latency.
*/
sort?: 'price' | 'throughput' | 'latency';
};
} & Pick<OpenRouterSharedSettings, 'extraBody' | 'user'>;