From 6b87b9f5e3ed631fa6d6f1c8d3223d0d066a41f4 Mon Sep 17 00:00:00 2001 From: kilo-agent Date: Thu, 7 May 2026 14:38:44 +0000 Subject: [PATCH 1/2] feat(gateway): serve embedding models from OpenRouter Make /api/gateway/embedding-models proxy the live OpenRouter catalog (output_modalities=embeddings) so clients see all supported embedding models, mirroring the existing /api/gateway/models endpoint for language models. --- .../gateway/embedding-models/route.test.ts | 84 +++++++++++++------ .../app/api/gateway/embedding-models/route.ts | 26 +++++- .../ai-gateway/providers/openrouter/index.ts | 52 ++++++++++++ 3 files changed, 134 insertions(+), 28 deletions(-) diff --git a/apps/web/src/app/api/gateway/embedding-models/route.test.ts b/apps/web/src/app/api/gateway/embedding-models/route.test.ts index cf376d467..0d19d9f47 100644 --- a/apps/web/src/app/api/gateway/embedding-models/route.test.ts +++ b/apps/web/src/app/api/gateway/embedding-models/route.test.ts @@ -1,34 +1,68 @@ -import { describe, expect, test } from '@jest/globals'; +import { describe, expect, jest, 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' - ); }); }); diff --git a/apps/web/src/app/api/gateway/embedding-models/route.ts b/apps/web/src/app/api/gateway/embedding-models/route.ts index c45691b75..65e3b5f38 100644 --- a/apps/web/src/app/api/gateway/embedding-models/route.ts +++ b/apps/web/src/app/api/gateway/embedding-models/route.ts @@ -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 { - return NextResponse.json(KILO_EMBEDDING_MODEL_CATALOG); +/** + * Test using: + * curl -vvv 'http://localhost:3000/api/gateway/embedding-models' + */ +export async function GET(): Promise< + 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 } + ); + } } diff --git a/apps/web/src/lib/ai-gateway/providers/openrouter/index.ts b/apps/web/src/lib/ai-gateway/providers/openrouter/index.ts index 34de88c61..faac579ef 100644 --- a/apps/web/src/lib/ai-gateway/providers/openrouter/index.ts +++ b/apps/web/src/lib/ai-gateway/providers/openrouter/index.ts @@ -191,3 +191,55 @@ export async function getEnhancedOpenRouterModels(): Promise { + 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; +} From 75137e8dc4101ab2a8a16d9fa6610d9ef4e94720 Mon Sep 17 00:00:00 2001 From: kilo-agent Date: Thu, 7 May 2026 14:46:25 +0000 Subject: [PATCH 2/2] test: use global jest in embedding-models route test Importing jest from @jest/globals interferes with module auto-mocking under SWC, leaving the mocked function as the real implementation. --- apps/web/src/app/api/gateway/embedding-models/route.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/api/gateway/embedding-models/route.test.ts b/apps/web/src/app/api/gateway/embedding-models/route.test.ts index 0d19d9f47..f6db68c74 100644 --- a/apps/web/src/app/api/gateway/embedding-models/route.test.ts +++ b/apps/web/src/app/api/gateway/embedding-models/route.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, jest, test, beforeEach } from '@jest/globals'; +import { describe, expect, test, beforeEach } from '@jest/globals'; import { GET } from './route'; import { getOpenRouterEmbeddingModels } from '@/lib/ai-gateway/providers/openrouter'; import type {