| import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; |
| import { POST } from '@/app/api/models/route'; |
| import type { NextRequest } from 'next/server'; |
|
|
| |
| |
| |
| |
| |
| |
|
|
| function makeReq(body: unknown): NextRequest { |
| return { json: async () => body, headers: { get: () => null } } as unknown as NextRequest; |
| } |
|
|
| function jsonResponse(obj: unknown): Response { |
| const text = JSON.stringify(obj); |
| return { ok: true, status: 200, json: async () => obj, text: async () => text } as unknown as Response; |
| } |
|
|
| let fetchMock: ReturnType<typeof vi.fn>; |
|
|
| const MODELS_DEV_FIXTURE = { |
| 'opencode-go': { |
| models: { |
| 'glm-5.2': { |
| id: 'glm-5.2', |
| limit: { context: 1000000, output: 131072 }, |
| modalities: { input: ['text'] }, |
| status: 'active', |
| }, |
| 'minimax-m3': { |
| id: 'minimax-m3', |
| limit: { context: 1000000, output: 131072 }, |
| modalities: { input: ['text', 'image', 'video'] }, |
| status: 'active', |
| }, |
| 'glm-5': { |
| id: 'glm-5', |
| limit: { context: 202752 }, |
| modalities: { input: ['text'] }, |
| status: 'deprecated', |
| }, |
| }, |
| }, |
| }; |
|
|
| beforeEach(() => { |
| fetchMock = vi.fn(async (url: unknown) => { |
| const u = String(url); |
| if (u.includes('127.0.0.1:11434')) return jsonResponse({ models: [{ name: 'llama3' }] }); |
| if (u.includes('api.example.com')) return jsonResponse({ data: [{ id: 'gpt-x' }] }); |
| if (u.includes('models.dev')) return jsonResponse(MODELS_DEV_FIXTURE); |
| return jsonResponse({}); |
| }); |
| vi.stubGlobal('fetch', fetchMock); |
| }); |
| afterEach(() => vi.unstubAllGlobals()); |
|
|
| describe('/api/models SSRF guard wiring', () => { |
| it('rejects a custom provider on a private/loopback URL — no outbound fetch is made', async () => { |
| const res = await POST(makeReq({ provider: 'my-cloud', apiKey: 'sk-x', baseUrl: 'http://169.254.169.254/v1' })); |
| const data = await res.json(); |
| expect(data.models).toEqual([]); |
| expect(fetchMock).not.toHaveBeenCalled(); |
| }); |
|
|
| it('allows a custom provider on a public URL — fetches that endpoint', async () => { |
| const res = await POST(makeReq({ provider: 'my-cloud', apiKey: 'sk-x', baseUrl: 'https://api.example.com/v1' })); |
| const data = await res.json(); |
| expect(fetchMock).toHaveBeenCalledWith('https://api.example.com/v1/models', expect.anything()); |
| expect(data.models).toContain('gpt-x'); |
| }); |
|
|
| it('does NOT guard built-in local providers — Ollama on localhost still works (regression)', async () => { |
| const res = await POST(makeReq({ provider: 'ollama', baseUrl: 'http://localhost:11434/v1' })); |
| const data = await res.json(); |
| expect(fetchMock).toHaveBeenCalledWith('http://127.0.0.1:11434/api/tags'); |
| expect(data.models).toContain('llama3'); |
| }); |
| }); |
|
|
| describe('/api/models opencode-go discovery', () => { |
| it('returns active models from models.dev and excludes deprecated ones', async () => { |
| const res = await POST(makeReq({ provider: 'opencode-go', apiKey: 'sk-x' })); |
| const data = await res.json(); |
| expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('models.dev')); |
| const ids = data.models.map((m: { id: string }) => m.id); |
| expect(ids).toContain('glm-5.2'); |
| expect(ids).toContain('minimax-m3'); |
| expect(ids).not.toContain('glm-5'); |
| }); |
|
|
| it('maps contextLength and inputModalities from models.dev fixture', async () => { |
| const res = await POST(makeReq({ provider: 'opencode-go', apiKey: 'sk-x' })); |
| const data = await res.json(); |
| const glm = data.models.find((m: { id: string }) => m.id === 'glm-5.2'); |
| expect(glm).toBeDefined(); |
| expect(glm.contextLength).toBe(1000000); |
| expect(glm.inputModalities).toEqual(['text']); |
| const minimax = data.models.find((m: { id: string }) => m.id === 'minimax-m3'); |
| expect(minimax).toBeDefined(); |
| expect(minimax.contextLength).toBe(1000000); |
| expect(minimax.inputModalities).toEqual(['text', 'image', 'video']); |
| }); |
|
|
| it('returns empty models array gracefully when models.dev fetch fails', async () => { |
| fetchMock.mockImplementationOnce(async () => ({ ok: false, status: 503 } as unknown as Response)); |
| const res = await POST(makeReq({ provider: 'opencode-go', apiKey: 'sk-x' })); |
| const data = await res.json(); |
| expect(data.models).toEqual([]); |
| }); |
| }); |
|
|