FreeLLMAPI / server /src /__tests__ /routes /proxy-tools.test.ts
Nryn215's picture
Upload folder using huggingface_hub
077865a verified
Raw
History Blame Contribute Delete
7.63 kB
import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest';
import type { Express } from 'express';
import { createApp } from '../../app.js';
import { initDb, getDb, getUnifiedApiKey } from '../../db/index.js';
import { mintDashboardToken, isGatedApiPath } from '../helpers/auth.js';
let dashToken = '';
async function request(app: Express, method: string, path: string, body?: any, headers: Record<string, string> = {}) {
const server = app.listen(0);
const addr = server.address() as any;
const url = `http://127.0.0.1:${addr.port}${path}`;
const res = await fetch(url, {
method,
headers: { ...(body ? { 'Content-Type': 'application/json' } : {}), ...(isGatedApiPath(path) && !('Authorization' in headers) ? { Authorization: `Bearer ${dashToken}` } : {}), ...headers },
body: body ? JSON.stringify(body) : undefined,
});
const data = await res.text();
server.close();
let json: any = null;
try { json = JSON.parse(data); } catch {}
return { status: res.status, body: json, headers: res.headers, raw: data };
}
function authHeaders() {
return { Authorization: `Bearer ${getUnifiedApiKey()}` };
}
describe('Proxy tool-calling support', () => {
let app: Express;
beforeAll(() => {
process.env.ENCRYPTION_KEY = '0'.repeat(64);
initDb(':memory:');
app = createApp();
dashToken = mintDashboardToken();
});
beforeEach(async () => {
const db = getDb();
db.prepare('DELETE FROM api_keys').run();
db.prepare('DELETE FROM requests').run();
const addKey = await request(app, 'POST', '/api/keys', {
platform: 'groq',
key: 'gsk_proxy_tool_test',
label: 'proxy-tools',
});
expect(addKey.status).toBe(201);
});
afterEach(() => {
vi.restoreAllMocks();
});
it('passes tools/tool_choice to provider and returns tool_calls', async () => {
const origFetch = global.fetch;
let providerBody: any = null;
vi.spyOn(global, 'fetch').mockImplementation(async (url, init) => {
const urlStr = typeof url === 'string' ? url : url.toString();
if (urlStr.includes('api.groq.com/openai/v1/chat/completions')) {
providerBody = JSON.parse((init as any).body);
return {
ok: true,
json: () => Promise.resolve({
id: 'chatcmpl-tool',
object: 'chat.completion',
created: 123,
model: 'openai/gpt-oss-120b',
choices: [{
index: 0,
message: {
role: 'assistant',
content: null,
tool_calls: [{
id: 'call_weather',
type: 'function',
function: {
name: 'get_weather',
arguments: '{"city":"Karachi"}',
},
}],
},
finish_reason: 'tool_calls',
}],
usage: { prompt_tokens: 12, completion_tokens: 4, total_tokens: 16 },
}),
} as any;
}
return origFetch(url, init);
});
const { status, body } = await request(app, 'POST', '/v1/chat/completions', {
// No `model` → auto-route via fallback chain.
messages: [{ role: 'user', content: 'What is the weather in Karachi?' }],
tools: [{
type: 'function',
function: {
name: 'get_weather',
description: 'Get current weather',
parameters: {
type: 'object',
properties: { city: { type: 'string' } },
required: ['city'],
},
},
}],
tool_choice: 'required',
}, authHeaders());
expect(status).toBe(200);
expect(providerBody.tools).toHaveLength(1);
expect(providerBody.tool_choice).toBe('required');
expect(body.choices[0].finish_reason).toBe('tool_calls');
expect(body.choices[0].message.tool_calls[0].function.name).toBe('get_weather');
});
it('accepts assistant tool_calls + tool messages in follow-up turns', async () => {
const origFetch = global.fetch;
let providerBody: any = null;
vi.spyOn(global, 'fetch').mockImplementation(async (url, init) => {
const urlStr = typeof url === 'string' ? url : url.toString();
if (urlStr.includes('api.groq.com/openai/v1/chat/completions')) {
providerBody = JSON.parse((init as any).body);
return {
ok: true,
json: () => Promise.resolve({
id: 'chatcmpl-final',
object: 'chat.completion',
created: 123,
model: 'openai/gpt-oss-120b',
choices: [{
index: 0,
message: {
role: 'assistant',
content: 'It is 30C in Karachi.',
},
finish_reason: 'stop',
}],
usage: { prompt_tokens: 18, completion_tokens: 6, total_tokens: 24 },
}),
} as any;
}
return origFetch(url, init);
});
const { status, body } = await request(app, 'POST', '/v1/chat/completions', {
messages: [
{ role: 'user', content: 'Weather in Karachi?' },
{
role: 'assistant',
content: null,
tool_calls: [{
id: 'call_weather_1',
type: 'function',
function: {
name: 'get_weather',
arguments: '{"city":"Karachi"}',
},
}],
},
{
role: 'tool',
tool_call_id: 'call_weather_1',
content: '{"temp_c":30}',
},
],
}, authHeaders());
expect(status).toBe(200);
expect(providerBody.messages[1].role).toBe('assistant');
expect(providerBody.messages[1].content).toBeNull();
expect(providerBody.messages[1].tool_calls).toHaveLength(1);
expect(providerBody.messages[2].role).toBe('tool');
expect(providerBody.messages[2].tool_call_id).toBe('call_weather_1');
expect(body.choices[0].message.content).toContain('30C');
});
it('round-trips assistant reasoning_content on follow-up turns (DeepSeek thinking — #255)', async () => {
const origFetch = global.fetch;
let providerBody: any = null;
vi.spyOn(global, 'fetch').mockImplementation(async (url, init) => {
const urlStr = typeof url === 'string' ? url : url.toString();
if (urlStr.includes('api.groq.com/openai/v1/chat/completions')) {
providerBody = JSON.parse((init as any).body);
return {
ok: true,
json: () => Promise.resolve({
id: 'chatcmpl-r', object: 'chat.completion', created: 1, model: 'm',
choices: [{ index: 0, message: { role: 'assistant', content: 'done' }, finish_reason: 'stop' }],
usage: { prompt_tokens: 5, completion_tokens: 2, total_tokens: 7 },
}),
} as any;
}
return origFetch(url, init);
});
const { status } = await request(app, 'POST', '/v1/chat/completions', {
messages: [
{ role: 'user', content: 'think then answer' },
{
role: 'assistant',
content: 'partial',
// What a DeepSeek thinking model returned last turn and the client
// replayed. Stripping it makes OpenCode Zen 400 on this request.
reasoning_content: 'Let me reason about this step by step...',
},
{ role: 'user', content: 'continue' },
],
}, authHeaders());
expect(status).toBe(200);
expect(providerBody.messages[1].role).toBe('assistant');
expect(providerBody.messages[1].reasoning_content).toBe('Let me reason about this step by step...');
});
});