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
84 changes: 59 additions & 25 deletions apps/web/src/app/api/gateway/embedding-models/route.test.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,68 @@
import { describe, expect, test } from '@jest/globals';
import { describe, expect, test, beforeEach } from '@jest/globals';
import { GET } from './route';
import {
KILO_DEFAULT_EMBEDDING_MODEL,
KILO_EMBEDDING_MODEL_CATALOG,
getKiloEmbeddingModel,
normalizeKiloEmbeddingModelId,
} from '@/lib/ai-gateway/embeddings/kilo-embedding-models';
import { getOpenRouterEmbeddingModels } from '@/lib/ai-gateway/providers/openrouter';
import type {
OpenRouterModel,
OpenRouterModelsResponse,
} from '@/lib/organizations/organization-types';

jest.mock('@/lib/ai-gateway/providers/openrouter');

const mockedGetOpenRouterEmbeddingModels = jest.mocked(getOpenRouterEmbeddingModels);

function makeEmbeddingModel(id: string): OpenRouterModel {
return {
id,
name: id,
created: 0,
description: '',
architecture: {
input_modalities: ['text'],
output_modalities: ['embeddings'],
tokenizer: 'Other',
},
top_provider: {
is_moderated: false,
context_length: 8192,
max_completion_tokens: null,
},
pricing: {
prompt: '0.0000001',
completion: '0',
},
context_length: 8192,
};
}

describe('GET /api/gateway/embedding-models', () => {
test('returns the Kilo embedding model catalog', async () => {
const response = await GET();
beforeEach(() => {
mockedGetOpenRouterEmbeddingModels.mockReset();
});

test('returns the embedding models from OpenRouter', async () => {
const response: OpenRouterModelsResponse = {
data: [
makeEmbeddingModel('mistralai/mistral-embed-2312'),
makeEmbeddingModel('openai/text-embedding-3-small'),
],
};
mockedGetOpenRouterEmbeddingModels.mockResolvedValue(response);

expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual(KILO_EMBEDDING_MODEL_CATALOG);
const result = await GET();

expect(result.status).toBe(200);
await expect(result.json()).resolves.toEqual(response);
});

test('catalog includes default model metadata and aliases', () => {
expect(KILO_EMBEDDING_MODEL_CATALOG.defaultModel).toBe(KILO_DEFAULT_EMBEDDING_MODEL);
expect(getKiloEmbeddingModel(KILO_DEFAULT_EMBEDDING_MODEL)).toMatchObject({
id: KILO_DEFAULT_EMBEDDING_MODEL,
dimension: 1024,
scoreThreshold: 0.35,
});
expect(getKiloEmbeddingModel('codestral-embed-2505')).toMatchObject({
id: 'mistralai/codestral-embed-2505',
dimension: 256,
scoreThreshold: 0.35,
test('returns 500 when OpenRouter fetch fails', async () => {
mockedGetOpenRouterEmbeddingModels.mockRejectedValue(new Error('boom'));

const result = await GET();

expect(result.status).toBe(500);
await expect(result.json()).resolves.toEqual({
error: 'Failed to fetch embedding models',
message: 'Error from OpenRouter API',
});
expect(normalizeKiloEmbeddingModelId('text-embedding-3-small')).toBe(
'openai/text-embedding-3-small'
);
});
});
26 changes: 23 additions & 3 deletions apps/web/src/app/api/gateway/embedding-models/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
import { NextResponse } from 'next/server';
import { KILO_EMBEDDING_MODEL_CATALOG } from '@/lib/ai-gateway/embeddings/kilo-embedding-models';
import { captureException } from '@sentry/nextjs';
import type { OpenRouterModelsResponse } from '@/lib/organizations/organization-types';
import { getOpenRouterEmbeddingModels } from '@/lib/ai-gateway/providers/openrouter';

export async function GET(): Promise<NextResponse> {
return NextResponse.json(KILO_EMBEDDING_MODEL_CATALOG);
/**
* Test using:
* curl -vvv 'http://localhost:3000/api/gateway/embedding-models'
*/
export async function GET(): Promise<
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Will this affect the new semantic search feature in the extnesion?

Copy link
Copy Markdown
Contributor

@chrarnoldus chrarnoldus May 8, 2026

Choose a reason for hiding this comment

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

does it call this endpoint? api/gateway/embedding-models then yes

NextResponse<{ error: string; message?: string } | OpenRouterModelsResponse>
> {
try {
const data = await getOpenRouterEmbeddingModels();
return NextResponse.json(data);
} catch (error) {
captureException(error, {
tags: { endpoint: 'gateway/embedding-models' },
extra: { action: 'fetching_embedding_models' },
});
return NextResponse.json(
{ error: 'Failed to fetch embedding models', message: 'Error from OpenRouter API' },
{ status: 500 }
);
}
}
52 changes: 52 additions & 0 deletions apps/web/src/lib/ai-gateway/providers/openrouter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,55 @@ export async function getEnhancedOpenRouterModels(): Promise<OpenRouterModelsRes

return { data: enhancedModelList(rawResponse.data) };
}

/**
* Fetch embedding models from the OpenRouter API.
* Mirrors `getRawOpenRouterModels` but filters to models that emit embeddings.
*/
export async function getOpenRouterEmbeddingModels(): Promise<OpenRouterModelsResponse> {
const response = await fetch(
`${PROVIDERS.OPENROUTER.apiUrl}/models?output_modalities=embeddings`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${PROVIDERS.OPENROUTER.apiKey}`,
...ATTRIBUTION_HEADERS,
},
next: { revalidate: 60 },
}
);

if (!response.ok) {
const errorMessage = `Failed to fetch OpenRouter embedding models: ${response.status} ${response.statusText}`;
captureException(new Error(errorMessage), {
tags: { endpoint: 'openrouter/embedding-models', source: 'openrouter_api' },
extra: {
status: response.status,
statusText: response.statusText,
},
});
throw new Error('Failed to fetch embedding models from OpenRouter API');
}

const data = await response.json();

const parseResult = OpenRouterModelsResponseSchema.safeParse(data);

if (!parseResult.success) {
errorExceptInTest(
'OpenRouter embedding models response not in expected format:',
parseResult.error
);

captureMessage('openrouter embedding models not in expected format!', {
level: 'error',
extra: {
data,
zodError: parseResult.error.issues,
},
});
return data as OpenRouterModelsResponse;
}

return parseResult.data;
}