Files
metapi/src/server/routes/proxy/models.test.ts
T

380 lines
10 KiB
TypeScript

import Fastify, { type FastifyInstance } from 'fastify';
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
type DbModule = typeof import('../../db/index.js');
type ProxyRouterModule = typeof import('./router.js');
type TokenRouterModule = typeof import('../../services/tokenRouter.js');
describe('/v1/models route', () => {
let db: DbModule['db'];
let schema: DbModule['schema'];
let proxyRoutes: ProxyRouterModule['proxyRoutes'];
let invalidateTokenRouterCache: TokenRouterModule['invalidateTokenRouterCache'];
let app: FastifyInstance;
let dataDir = '';
beforeAll(async () => {
dataDir = mkdtempSync(join(tmpdir(), 'metapi-models-route-'));
process.env.DATA_DIR = dataDir;
await import('../../db/migrate.js');
const dbModule = await import('../../db/index.js');
const proxyRouterModule = await import('./router.js');
const tokenRouterModule = await import('../../services/tokenRouter.js');
db = dbModule.db;
schema = dbModule.schema;
proxyRoutes = proxyRouterModule.proxyRoutes;
invalidateTokenRouterCache = tokenRouterModule.invalidateTokenRouterCache;
app = Fastify();
await app.register(proxyRoutes);
});
beforeEach(async () => {
invalidateTokenRouterCache();
await db.delete(schema.routeChannels).run();
await db.delete(schema.tokenRoutes).run();
await db.delete(schema.tokenModelAvailability).run();
await db.delete(schema.modelAvailability).run();
await db.delete(schema.accountTokens).run();
await db.delete(schema.accounts).run();
await db.delete(schema.sites).run();
await db.delete(schema.downstreamApiKeys).run();
});
afterAll(async () => {
await app.close();
delete process.env.DATA_DIR;
});
it('hides models that have no routable channel even if model availability contains them', async () => {
const site = await db.insert(schema.sites).values({
name: 'test-site',
url: 'https://upstream.example.com',
platform: 'openai',
status: 'active',
}).returning().get();
const account = await db.insert(schema.accounts).values({
siteId: site.id,
accessToken: 'account-access-token',
status: 'active',
}).returning().get();
const token = await db.insert(schema.accountTokens).values({
accountId: account.id,
name: 'default',
token: 'account-api-token',
enabled: true,
isDefault: true,
}).returning().get();
await db.insert(schema.modelAvailability).values([
{
accountId: account.id,
modelName: 'routable-model',
available: true,
},
{
accountId: account.id,
modelName: 'orphan-model',
available: true,
},
]).run();
const route = await db.insert(schema.tokenRoutes).values({
modelPattern: 'routable-model',
enabled: true,
}).returning().get();
await db.insert(schema.routeChannels).values({
routeId: route.id,
accountId: account.id,
tokenId: token.id,
sourceModel: 'routable-model',
enabled: true,
}).run();
await db.insert(schema.downstreamApiKeys).values({
name: 'managed-key',
key: 'sk-managed-models',
enabled: true,
}).run();
const response = await app.inject({
method: 'GET',
url: '/v1/models',
headers: {
authorization: 'Bearer sk-managed-models',
},
});
expect(response.statusCode).toBe(200);
const body = response.json() as {
object: 'list';
data: Array<{ id: string }>;
};
const ids = body.data.map((item) => item.id);
expect(ids).toContain('routable-model');
expect(ids).not.toContain('orphan-model');
});
it('returns only whitelist models for managed key with supportedModels policy', async () => {
const site = await db.insert(schema.sites).values({
name: 'test-site',
url: 'https://upstream.example.com',
platform: 'openai',
status: 'active',
}).returning().get();
const account = await db.insert(schema.accounts).values({
siteId: site.id,
accessToken: 'account-access-token',
status: 'active',
}).returning().get();
const token = await db.insert(schema.accountTokens).values({
accountId: account.id,
name: 'default',
token: 'account-api-token',
enabled: true,
isDefault: true,
}).returning().get();
await db.insert(schema.modelAvailability).values([
{
accountId: account.id,
modelName: 'allowed-model',
available: true,
},
{
accountId: account.id,
modelName: 'blocked-model',
available: true,
},
]).run();
const allowedRoute = await db.insert(schema.tokenRoutes).values({
modelPattern: 'allowed-model',
enabled: true,
}).returning().get();
const blockedRoute = await db.insert(schema.tokenRoutes).values({
modelPattern: 'blocked-model',
enabled: true,
}).returning().get();
await db.insert(schema.routeChannels).values([
{
routeId: allowedRoute.id,
accountId: account.id,
tokenId: token.id,
sourceModel: 'allowed-model',
enabled: true,
},
{
routeId: blockedRoute.id,
accountId: account.id,
tokenId: token.id,
sourceModel: 'blocked-model',
enabled: true,
},
]).run();
await db.insert(schema.downstreamApiKeys).values({
name: 'managed-key',
key: 'sk-managed-whitelist',
enabled: true,
supportedModels: JSON.stringify(['allowed-model']),
}).run();
const response = await app.inject({
method: 'GET',
url: '/v1/models',
headers: {
authorization: 'Bearer sk-managed-whitelist',
},
});
expect(response.statusCode).toBe(200);
const body = response.json() as {
object: 'list';
data: Array<{ id: string }>;
};
const ids = body.data.map((item) => item.id);
expect(ids).toContain('allowed-model');
expect(ids).not.toContain('blocked-model');
});
it('returns only selected group route alias for managed key with allowedRouteIds policy', async () => {
const site = await db.insert(schema.sites).values({
name: 'test-site',
url: 'https://upstream.example.com',
platform: 'openai',
status: 'active',
}).returning().get();
const account = await db.insert(schema.accounts).values({
siteId: site.id,
accessToken: 'account-access-token',
status: 'active',
}).returning().get();
const token = await db.insert(schema.accountTokens).values({
accountId: account.id,
name: 'default',
token: 'account-api-token',
enabled: true,
isDefault: true,
}).returning().get();
await db.insert(schema.modelAvailability).values([
{
accountId: account.id,
modelName: 'claude-opus-4-5',
available: true,
},
{
accountId: account.id,
modelName: 'claude-sonnet-4-5',
available: true,
},
]).run();
const groupRoute = await db.insert(schema.tokenRoutes).values({
modelPattern: 're:^claude-(opus|sonnet)-4-5$',
displayName: 'claude-opus-4-6',
enabled: true,
}).returning().get();
await db.insert(schema.routeChannels).values({
routeId: groupRoute.id,
accountId: account.id,
tokenId: token.id,
enabled: true,
}).run();
await db.insert(schema.downstreamApiKeys).values({
name: 'managed-key',
key: 'sk-managed-group-only',
enabled: true,
allowedRouteIds: JSON.stringify([groupRoute.id]),
}).run();
const response = await app.inject({
method: 'GET',
url: '/v1/models',
headers: {
authorization: 'Bearer sk-managed-group-only',
},
});
expect(response.statusCode).toBe(200);
const body = response.json() as {
object: 'list';
data: Array<{ id: string }>;
};
const ids = body.data.map((item) => item.id);
expect(ids).toContain('claude-opus-4-6');
expect(ids).not.toContain('claude-opus-4-5');
expect(ids).not.toContain('claude-sonnet-4-5');
});
it('filters search pseudo models out of /v1/models', async () => {
const site = await db.insert(schema.sites).values({
name: 'search-site',
url: 'https://search.example.com',
platform: 'openai',
status: 'active',
}).returning().get();
const account = await db.insert(schema.accounts).values({
siteId: site.id,
accessToken: 'search-access-token',
status: 'active',
}).returning().get();
const token = await db.insert(schema.accountTokens).values({
accountId: account.id,
name: 'default',
token: 'search-api-token',
enabled: true,
isDefault: true,
}).returning().get();
await db.insert(schema.modelAvailability).values([
{
accountId: account.id,
modelName: '__search',
available: true,
},
{
accountId: account.id,
modelName: '__tavily_search',
available: true,
},
{
accountId: account.id,
modelName: 'gpt-4.1',
available: true,
},
]).run();
const searchRoute = await db.insert(schema.tokenRoutes).values({
modelPattern: '__search',
enabled: true,
}).returning().get();
const llmRoute = await db.insert(schema.tokenRoutes).values({
modelPattern: 'gpt-4.1',
enabled: true,
}).returning().get();
await db.insert(schema.routeChannels).values([
{
routeId: searchRoute.id,
accountId: account.id,
tokenId: token.id,
sourceModel: '__search',
enabled: true,
},
{
routeId: llmRoute.id,
accountId: account.id,
tokenId: token.id,
sourceModel: 'gpt-4.1',
enabled: true,
},
]).run();
await db.insert(schema.downstreamApiKeys).values({
name: 'search-key',
key: 'change-me-proxy-sk-token',
enabled: true,
}).run();
const response = await app.inject({
method: 'GET',
url: '/v1/models',
headers: {
authorization: 'Bearer change-me-proxy-sk-token',
},
});
expect(response.statusCode).toBe(200);
const body = response.json() as {
object: 'list';
data: Array<{ id: string }>;
};
const ids = body.data.map((item) => item.id);
expect(ids).toContain('gpt-4.1');
expect(ids).not.toContain('__search');
expect(ids).not.toContain('__tavily_search');
});
});