Skip to content
Merged
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
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,64 @@ This list is not a definitive list of models supported by OpenRouter, as it cons

You can find the latest list of tool-supported models supported by OpenRouter [here](https://openrouter.ai/models?order=newest&supported_parameters=tools). (Note: This list may contain models that are not compatible with the AI SDK.)

## Embeddings

OpenRouter supports embedding models for semantic search, RAG pipelines, and vector-native features. The provider exposes embeddings compatible with both AI SDK v5 and v4.

### AI SDK v5 (Recommended)

```ts
import { embed } from 'ai';
import { openrouter } from '@openrouter/ai-sdk-provider';

const { embedding } = await embed({
model: openrouter.textEmbeddingModel('openai/text-embedding-3-small'),
value: 'sunny day at the beach',
});

console.log(embedding); // Array of numbers representing the embedding
```

### Batch Embeddings

```ts
import { embedMany } from 'ai';
import { openrouter } from '@openrouter/ai-sdk-provider';

const { embeddings } = await embedMany({
model: openrouter.textEmbeddingModel('openai/text-embedding-3-small'),
values: [
'sunny day at the beach',
'rainy day in the city',
'snowy mountain peak',
],
});

console.log(embeddings); // Array of embedding arrays
```

### AI SDK v4 (Deprecated)

For backwards compatibility, the `embedding` method is also available:

```ts
import { embed } from 'ai';
import { openrouter } from '@openrouter/ai-sdk-provider';

const { embedding } = await embed({
model: openrouter.embedding('openai/text-embedding-3-small'),
value: 'sunny day at the beach',
});
```

### Supported Embedding Models

OpenRouter supports various embedding models including:
- `openai/text-embedding-3-small`
- `openai/text-embedding-3-large`
- `openai/text-embedding-ada-002`
- And more available on [OpenRouter](https://openrouter.ai/models?output_modalities=embeddings)

## Passing Extra Body to OpenRouter

There are 3 ways to pass extra body to OpenRouter:
Expand Down
253 changes: 253 additions & 0 deletions src/embedding/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
import { describe, expect, it } from 'vitest';
import { createOpenRouter } from '../provider';
import { OpenRouterEmbeddingModel } from './index';

describe('OpenRouterEmbeddingModel', () => {
const mockFetch = async (
_url: URL | RequestInfo,
_init?: RequestInit,
): Promise<Response> => {
return new Response(
JSON.stringify({
id: 'test-id',
object: 'list',
data: [
{
object: 'embedding',
embedding: new Array(1536).fill(0.1),
index: 0,
},
],
model: 'openai/text-embedding-3-small',
usage: {
prompt_tokens: 5,
total_tokens: 5,
cost: 0.00001,
},
}),
{
status: 200,
headers: {
'content-type': 'application/json',
},
},
);
};

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

it('should expose embedding method (deprecated)', () => {
const provider = createOpenRouter({ apiKey: 'test-key' });
expect(provider.embedding).toBeDefined();
expect(typeof provider.embedding).toBe('function');
});

it('should create an embedding model instance', () => {
const provider = createOpenRouter({ apiKey: 'test-key' });
const model = provider.textEmbeddingModel(
'openai/text-embedding-3-small',
);
expect(model).toBeInstanceOf(OpenRouterEmbeddingModel);
expect(model.modelId).toBe('openai/text-embedding-3-small');
expect(model.provider).toBe('openrouter');
expect(model.specificationVersion).toBe('v2');
});
});

describe('doEmbed', () => {
it('should embed a single value', async () => {
const provider = createOpenRouter({
apiKey: 'test-key',
fetch: mockFetch,
});
const model = provider.textEmbeddingModel(
'openai/text-embedding-3-small',
);

const result = await model.doEmbed({
values: ['sunny day at the beach'],
});

expect(result.embeddings).toHaveLength(1);
expect(result.embeddings[0]).toHaveLength(1536);
expect(result.usage).toEqual({ tokens: 5 });
expect(
(result.providerMetadata?.openrouter as { usage?: { cost?: number } })
?.usage?.cost,
).toBe(0.00001);
});

it('should embed multiple values', async () => {
const mockFetchMultiple = async (
_url: URL | RequestInfo,
_init?: RequestInit,
): Promise<Response> => {
return new Response(
JSON.stringify({
object: 'list',
data: [
{
object: 'embedding',
embedding: new Array(1536).fill(0.1),
index: 0,
},
{
object: 'embedding',
embedding: new Array(1536).fill(0.2),
index: 1,
},
{
object: 'embedding',
embedding: new Array(1536).fill(0.3),
index: 2,
},
],
model: 'openai/text-embedding-3-small',
usage: {
prompt_tokens: 15,
total_tokens: 15,
},
}),
{
status: 200,
headers: {
'content-type': 'application/json',
},
},
);
};

const provider = createOpenRouter({
apiKey: 'test-key',
fetch: mockFetchMultiple,
});
const model = provider.textEmbeddingModel(
'openai/text-embedding-3-small',
);

const result = await model.doEmbed({
values: [
'sunny day at the beach',
'rainy day in the city',
'snowy mountain peak',
],
});

expect(result.embeddings).toHaveLength(3);
expect(result.embeddings[0]).toHaveLength(1536);
expect(result.embeddings[1]).toHaveLength(1536);
expect(result.embeddings[2]).toHaveLength(1536);
expect(result.usage).toEqual({ tokens: 15 });
});

it('should pass custom settings to API', async () => {
let capturedRequest: Record<string, unknown> | undefined;

const mockFetchWithCapture = async (
_url: URL | RequestInfo,
init?: RequestInit,
): Promise<Response> => {
capturedRequest = JSON.parse(init?.body as string);
return new Response(
JSON.stringify({
object: 'list',
data: [
{
object: 'embedding',
embedding: new Array(1536).fill(0.1),
index: 0,
},
],
model: 'openai/text-embedding-3-small',
usage: {
prompt_tokens: 5,
total_tokens: 5,
},
}),
{
status: 200,
headers: {
'content-type': 'application/json',
},
},
);
};

const provider = createOpenRouter({
apiKey: 'test-key',
fetch: mockFetchWithCapture,
});

const model = provider.textEmbeddingModel(
'openai/text-embedding-3-small',
{
user: 'test-user-123',
provider: {
order: ['openai'],
allow_fallbacks: false,
},
},
);

await model.doEmbed({
values: ['test input'],
});

expect(capturedRequest?.user).toBe('test-user-123');
expect(capturedRequest?.provider).toEqual({
order: ['openai'],
allow_fallbacks: false,
});
expect(capturedRequest?.model).toBe('openai/text-embedding-3-small');
expect(capturedRequest?.input).toEqual(['test input']);
});

it('should handle response without usage information', async () => {
const mockFetchNoUsage = async (
_url: URL | RequestInfo,
_init?: RequestInit,
): Promise<Response> => {
return new Response(
JSON.stringify({
object: 'list',
data: [
{
object: 'embedding',
embedding: new Array(1536).fill(0.1),
index: 0,
},
],
model: 'openai/text-embedding-3-small',
}),
{
status: 200,
headers: {
'content-type': 'application/json',
},
},
);
};

const provider = createOpenRouter({
apiKey: 'test-key',
fetch: mockFetchNoUsage,
});
const model = provider.textEmbeddingModel(
'openai/text-embedding-3-small',
);

const result = await model.doEmbed({
values: ['test'],
});

expect(result.embeddings).toHaveLength(1);
expect(result.usage).toBeUndefined();
expect(result.providerMetadata).toBeUndefined();
});
});
});
Loading