FreeLLMAPI / server /src /__tests__ /routes /responses.test.ts
Nryn215's picture
Upload folder using huggingface_hub
077865a verified
Raw
History Blame Contribute Delete
6.9 kB
import { describe, it, expect, beforeAll, vi } from 'vitest';
// Mock only routeRequest so we don't need real provider keys; keep the rest of
// the router module (recordSuccess / recordRateLimitHit) intact.
const { mockRouteRequest } = vi.hoisted(() => ({ mockRouteRequest: vi.fn() }));
vi.mock('../../services/router.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../services/router.js')>();
return { ...actual, routeRequest: mockRouteRequest };
});
import type { Express } from 'express';
import { createApp } from '../../app.js';
import { initDb, getUnifiedApiKey } from '../../db/index.js';
function fakeRoute(provider: any) {
return { provider, modelId: 'fake-model', modelDbId: 9999, apiKey: 'k', keyId: 1, platform: 'fake', displayName: 'Fake Model' };
}
async function post(app: Express, path: string, body: any, key?: string) {
const server = app.listen(0);
const addr = server.address() as any;
const res = await fetch(`http://127.0.0.1:${addr.port}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...(key ? { Authorization: `Bearer ${key}` } : {}) },
body: JSON.stringify(body),
});
const text = await res.text();
server.close();
return { status: res.status, text, contentType: res.headers.get('content-type') ?? '' };
}
describe('POST /v1/responses (#96)', () => {
let app: Express;
let key: string;
beforeAll(() => {
process.env.ENCRYPTION_KEY = '0'.repeat(64);
initDb(':memory:');
app = createApp();
key = getUnifiedApiKey();
});
it('rejects requests without a valid unified key (401)', async () => {
expect((await post(app, '/v1/responses', { input: 'hi' })).status).toBe(401);
expect((await post(app, '/v1/responses', { input: 'hi' }, 'wrong')).status).toBe(401);
});
it('rejects an invalid body (missing input) with 400', async () => {
expect((await post(app, '/v1/responses', { model: 'auto' }, key)).status).toBe(400);
});
// #118: image input isn't carried through the Responses translation yet, so
// it must hard-fail clearly rather than silently answer blind to the image.
it('rejects image input with a clear 422 pointing at /v1/chat/completions', async () => {
const { status, text } = await post(app, '/v1/responses', {
input: [{
role: 'user',
content: [
{ type: 'input_text', text: 'what is this?' },
{ type: 'input_image', image_url: 'data:image/png;base64,iVBORw0KGgo=' },
],
}],
}, key);
expect(status).toBe(422);
expect(JSON.parse(text).error.code).toBe('no_vision_model');
});
// #103: the x-api-key header (Anthropic wire format) must authenticate here
// too, not just on /v1/chat/completions.
it('accepts the unified key via the x-api-key header', async () => {
const server = app.listen(0);
const addr = server.address() as any;
const res = await fetch(`http://127.0.0.1:${addr.port}/v1/responses`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': key },
body: JSON.stringify({ input: 'hi' }),
});
server.close();
// Auth passes (not 401); body validity / routing is covered elsewhere.
expect(res.status).not.toBe(401);
});
it('non-stream: returns a completed Responses object with usage', async () => {
mockRouteRequest.mockReturnValue(fakeRoute({
async chatCompletion() {
return {
id: 'c', object: 'chat.completion', created: 0, model: 'fake-model',
choices: [{ index: 0, message: { role: 'assistant', content: 'Hello from fake' }, finish_reason: 'stop' }],
usage: { prompt_tokens: 3, completion_tokens: 4, total_tokens: 7 },
};
},
async *streamChatCompletion() { /* unused */ },
}));
const { status, text } = await post(app, '/v1/responses', { input: 'hi', stream: false }, key);
expect(status).toBe(200);
const body = JSON.parse(text);
expect(body.object).toBe('response');
expect(body.status).toBe('completed');
expect(body.output_text).toBe('Hello from fake');
expect(body.output[0]).toMatchObject({ type: 'message', role: 'assistant' });
expect(body.usage.total_tokens).toBe(7);
});
it('stream: emits the Responses SSE event sequence', async () => {
mockRouteRequest.mockReturnValue(fakeRoute({
async chatCompletion() { throw new Error('should not be called'); },
async *streamChatCompletion() {
yield { id: 'c', object: 'chat.completion.chunk', created: 0, model: 'fake-model', choices: [{ index: 0, delta: { role: 'assistant', content: 'Hel' }, finish_reason: null }] };
yield { id: 'c', object: 'chat.completion.chunk', created: 0, model: 'fake-model', choices: [{ index: 0, delta: { content: 'lo' }, finish_reason: null }] };
yield { id: 'c', object: 'chat.completion.chunk', created: 0, model: 'fake-model', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] };
},
}));
const { status, text, contentType } = await post(app, '/v1/responses', { input: 'hi', stream: true }, key);
expect(status).toBe(200);
expect(contentType).toContain('text/event-stream');
for (const ev of ['response.created', 'response.output_item.added', 'response.content_part.added',
'response.output_text.delta', 'response.output_text.done', 'response.output_item.done', 'response.completed']) {
expect(text).toContain(`event: ${ev}`);
}
expect(text).toContain('"delta":"Hel"');
expect(text).toContain('"delta":"lo"');
// the terminal event carries the assembled text
const completed = text.split('event: response.completed')[1];
expect(completed).toContain('"output_text":"Hello"');
});
it('stream: tool-call deltas produce function_call events with assembled arguments', async () => {
mockRouteRequest.mockReturnValue(fakeRoute({
async chatCompletion() { throw new Error('nope'); },
async *streamChatCompletion() {
yield { id: 'c', object: 'chat.completion.chunk', created: 0, model: 'fake-model', choices: [{ index: 0, delta: { tool_calls: [{ index: 0, id: 'call_1', type: 'function', function: { name: 'get_weather', arguments: '{"ci' } }] }, finish_reason: null }] };
yield { id: 'c', object: 'chat.completion.chunk', created: 0, model: 'fake-model', choices: [{ index: 0, delta: { tool_calls: [{ index: 0, type: 'function', function: { arguments: 'ty":"SF"}' } }] }, finish_reason: 'tool_calls' }] };
},
}));
const { text } = await post(app, '/v1/responses', { input: 'weather?', stream: true }, key);
expect(text).toContain('"type":"function_call"');
expect(text).toContain('event: response.function_call_arguments.delta');
expect(text).toContain('event: response.function_call_arguments.done');
expect(text).toContain('"arguments":"{\\"city\\":\\"SF\\"}"');
});
});