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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { GET } from '@/app/api/openrouter/models/[provider]/[model]/endpoints/route';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { GET } from '@/app/api/openrouter/models/[provider]/[model]/endpoints/route';
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { describe, expect, test } from '@jest/globals';
import { NextRequest } from 'next/server';
import { getOpenRouterModelsMetadataFromDatabase } from '@/lib/ai-gateway/providers/gateway-models-cache';
import { QWEN37_MAX_MODEL_ID } from '@/lib/ai-gateway/custom-pricing';
import { GET } from './route';
import { GET as openRouterV1GET } from '@/app/api/openrouter/v1/models/[provider]/[model]/endpoints/route';
import { GET as gatewayV1GET } from '@/app/api/gateway/v1/models/[provider]/[model]/endpoints/route';

jest.mock('@/lib/ai-gateway/providers/gateway-models-cache', () => ({
getOpenRouterModelsMetadataFromDatabase: jest.fn(),
}));

const mockedGetOpenRouterModelsMetadataFromDatabase = jest.mocked(
getOpenRouterModelsMetadataFromDatabase
);

function request(modelId: string) {
return new NextRequest(`http://localhost:3000/api/openrouter/models/${modelId}/endpoints`);
}

describe('GET /api/openrouter/models/[provider]/[model]/endpoints', () => {
test('uses the canonical handler for versioned routes', () => {
expect(openRouterV1GET).toBe(GET);
expect(gatewayV1GET).toBe(GET);
});

test('undoes discounts for every priced endpoint without adding missing fields', async () => {
const model = {
id: 'deepseek/deepseek-v4-pro',
name: 'DeepSeek: DeepSeek V4 Pro',
endpoints: [
{
provider_name: 'DeepSeek',
context_length: 1_048_576,
pricing: { prompt: '0.000000435', completion: '0.00000087', discount: 0.5 },
},
{
provider_name: 'Baidu',
pricing: { prompt: '0.0000006253', completion: '0.0000012506', discount: 0.63 },
},
{ provider_name: 'Unpriced' },
],
};
mockedGetOpenRouterModelsMetadataFromDatabase.mockResolvedValue({ [model.id]: model });

const response = await GET(request(model.id), {
params: Promise.resolve({ provider: 'deepseek', model: 'deepseek-v4-pro' }),
});

expect(response.status).toBe(200);
expect(response.headers.get('cache-control')).toBe(
'public, max-age=0, s-maxage=60, stale-while-revalidate=60'
);
await expect(response.json()).resolves.toEqual({
data: {
id: model.id,
name: model.name,
endpoints: [
{
provider_name: 'DeepSeek',
context_length: 1_048_576,
pricing: { prompt: '0.000000870000', completion: '0.000001740000' },
},
{
provider_name: 'Baidu',
pricing: { prompt: '0.000001690000', completion: '0.000003380000' },
},
{ provider_name: 'Unpriced' },
],
},
});
});

test('returns 404 when the model is absent from the cache', async () => {
mockedGetOpenRouterModelsMetadataFromDatabase.mockResolvedValue({});

const response = await GET(request('missing/model'), {
params: Promise.resolve({ provider: 'missing', model: 'model' }),
});

expect(response.status).toBe(404);
expect(response.headers.get('cache-control')).toBe(
'public, max-age=0, s-maxage=60, stale-while-revalidate=60'
);
await expect(response.json()).resolves.toEqual({
error: { message: 'Not Found', code: 404 },
});
});

test('returns 404 for forbidden free models without reading cached metadata', async () => {
mockedGetOpenRouterModelsMetadataFromDatabase.mockClear();
const modelId = 'openai/gpt-oss-20b:free';

const response = await GET(request(modelId), {
params: Promise.resolve({ provider: 'openai', model: 'gpt-oss-20b:free' }),
});

expect(response.status).toBe(404);
expect(response.headers.get('cache-control')).toBe(
'public, max-age=0, s-maxage=60, stale-while-revalidate=60'
);
await expect(response.json()).resolves.toEqual({
error: { message: 'Not Found', code: 404 },
});
expect(mockedGetOpenRouterModelsMetadataFromDatabase).not.toHaveBeenCalled();
});

test('applies custom pricing to every priced endpoint', async () => {
const model = {
id: QWEN37_MAX_MODEL_ID,
name: 'Qwen: Qwen3.7 Max',
endpoints: [
{
provider_name: 'Alibaba',
pricing: { prompt: '0.000001', completion: '0.000002' },
},
{
provider_name: 'Another provider',
pricing: { prompt: '0.000003', completion: '0.000004' },
},
{ provider_name: 'Unpriced' },
],
};
mockedGetOpenRouterModelsMetadataFromDatabase.mockResolvedValue({ [model.id]: model });
const [provider, modelName] = model.id.split('/');

const response = await GET(request(model.id), {
params: Promise.resolve({ provider, model: modelName }),
});

expect(response.status).toBe(200);
const data = (await response.json()).data;
expect(data.endpoints).toEqual([
{
provider_name: 'Alibaba',
pricing: {
prompt: '0.000001250000',
completion: '0.000003750000',
input_cache_read: '0.000000125000',
input_cache_write: '0.000001562500',
},
},
{
provider_name: 'Another provider',
pricing: {
prompt: '0.000001250000',
completion: '0.000003750000',
input_cache_read: '0.000000125000',
input_cache_write: '0.000001562500',
},
},
{ provider_name: 'Unpriced' },
]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { getOpenRouterModelsMetadataFromDatabase } from '@/lib/ai-gateway/providers/gateway-models-cache';
import { getModelDisplayPricing } from '@/lib/ai-gateway/providers/openrouter';
import { applyCustomPricingToPricing } from '@/lib/ai-gateway/custom-pricing';
import { isForbiddenFreeModel } from '@/lib/ai-gateway/forbidden-free-models';

const CACHE_CONTROL = 'public, max-age=0, s-maxage=60, stale-while-revalidate=60';

export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ provider: string; model: string }> }
) {
const { provider, model } = await params;
const modelId = `${provider}/${model}`;
if (isForbiddenFreeModel(modelId)) {
return NextResponse.json(
{ error: { message: 'Not Found', code: 404 } },
{ status: 404, headers: { 'Cache-Control': CACHE_CONTROL } }
);
}

const models = await getOpenRouterModelsMetadataFromDatabase();
const storedModel = models[modelId];

if (!storedModel) {
return NextResponse.json(
{ error: { message: 'Not Found', code: 404 } },
{ status: 404, headers: { 'Cache-Control': CACHE_CONTROL } }
);
}

return NextResponse.json(
{
data: {
...storedModel,
endpoints: storedModel.endpoints.map(endpoint => {
if (!endpoint.pricing) return endpoint;
const displayPricing = getModelDisplayPricing(storedModel.id, endpoint.pricing);
return {
...endpoint,
pricing: applyCustomPricingToPricing(
storedModel.id,
displayPricing ?? endpoint.pricing
),
};
}),
},
},
{ headers: { 'Cache-Control': CACHE_CONTROL } }
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { GET } from '@/app/api/openrouter/models/[provider]/[model]/endpoints/route';
8 changes: 8 additions & 0 deletions apps/web/src/lib/ai-gateway/custom-pricing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ function applyPricing(pricing: OpenRouterModel['pricing'], customPricing: Pricin
};
}

export function applyCustomPricingToPricing(
modelId: string,
pricing: OpenRouterModel['pricing']
): OpenRouterModel['pricing'] {
const customPricing = getCustomPricing(modelId);
return customPricing ? applyPricing(pricing, customPricing.pricing[0].pricing) : pricing;
}

export function applyCustomPricingToModel(model: OpenRouterModel): OpenRouterModel {
const customPricing = getCustomPricing(model.id);
if (!customPricing) return model;
Expand Down