Files
evomap__evolver/test/proxyModelsE2E.test.js
evolver-publish 8e2e91d456 Release v1.89.18
2026-06-28 22:47:18 +08:00

107 lines
5.3 KiB
JavaScript

'use strict';
// Real-socket e2e for GET /v1/models routing. The probe is routed by the `anthropic-version` header to the
// Anthropic or OpenAI upstream's model list (so codex/opencode/cursor/SDK startup probes never 404). Mocks the
// provider callables to assert: correct provider chosen, GET method + no body, native path (/v1/models vs
// /models), and the model list forwarded back.
const test = require('node:test');
const assert = require('node:assert');
const http = require('node:http');
const { ProxyHttpServer } = require('../src/proxy/server/http.js');
const { buildModelsHandler } = require('../src/proxy/router/models_route.js');
let calls;
function mockAnthropic(reqPath, body, opts) {
calls.push({ provider: 'anthropic', reqPath, body, method: opts && opts.method });
return Promise.resolve({ status: 200, headers: { 'content-type': 'application/json' }, stream: null,
text: () => JSON.stringify({ data: [{ id: 'claude-opus-4-8', type: 'model' }] }) });
}
function mockOpenAI(reqPath, body, opts) {
calls.push({ provider: 'openai', reqPath, body, method: opts && opts.method });
return Promise.resolve({ status: 200, headers: { 'content-type': 'application/json' }, stream: null,
text: () => JSON.stringify({ object: 'list', data: [{ id: 'gpt-4o', object: 'model' }] }) });
}
async function withServer(run) {
calls = [];
const silent = { log() {}, warn() {}, error() {} };
const handler = buildModelsHandler({ anthropicProxy: mockAnthropic, openAIProxy: mockOpenAI, logger: silent });
const srv = new ProxyHttpServer({ 'GET /v1/models': handler }, { port: 0, logger: silent });
srv.token = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
srv.server = http.createServer((req, res) => srv._handleRequest(req, res));
const port = await new Promise((resolve) => srv.server.listen(0, '127.0.0.1', () => resolve(srv.server.address().port)));
srv.actualPort = port;
const get = (headers) => new Promise((resolve, reject) => {
const req = http.request({ host: '127.0.0.1', port, path: '/v1/models', method: 'GET',
headers: { authorization: 'Bearer aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', ...headers } }, (res) => {
const chunks = [];
res.on('data', (d) => chunks.push(d));
res.on('end', () => resolve({ status: res.statusCode, bytes: Buffer.concat(chunks) }));
});
req.on('error', reject);
req.end();
});
try {
await run({ get });
} finally {
await new Promise((r) => srv.server.close(r));
}
}
test('GET /v1/models with anthropic-version header → Anthropic /v1/models (GET, no body)', async () => {
await withServer(async ({ get }) => {
const res = await get({ 'anthropic-version': '2023-06-01' });
assert.equal(res.status, 200);
const body = JSON.parse(res.bytes.toString('utf8'));
assert.equal(body.data[0].id, 'claude-opus-4-8'); // anthropic list forwarded
assert.equal(calls.length, 1);
assert.equal(calls[0].provider, 'anthropic');
assert.equal(calls[0].reqPath, '/v1/models');
assert.equal(calls[0].method, 'GET');
assert.equal(calls[0].body, null); // GET sends no body
});
});
test('GET /v1/models without anthropic-version → OpenAI /models (default; codex/opencode/cursor)', async () => {
await withServer(async ({ get }) => {
const res = await get({ 'user-agent': 'opencode/1.0' });
assert.equal(res.status, 200);
const body = JSON.parse(res.bytes.toString('utf8'));
assert.equal(body.data[0].id, 'gpt-4o'); // openai list forwarded
assert.equal(calls[0].provider, 'openai');
assert.equal(calls[0].reqPath, '/models'); // OpenAI base already ends in /v1 → /models
assert.equal(calls[0].method, 'GET');
assert.equal(calls[0].body, null);
});
});
test('GET /v1/models surfaces an upstream 401 (no key) instead of 404', async () => {
await withServer(async ({ get }) => {
// openAIProxy that throws like _proxyOpenAIResponses does with no key
const handler = buildModelsHandler({
anthropicProxy: mockAnthropic,
openAIProxy: () => Promise.reject(Object.assign(new Error('openai api key required'), { statusCode: 401 })),
logger: { log() {}, warn() {}, error() {} },
});
// swap in a one-off server for this case
const srv = new ProxyHttpServer({ 'GET /v1/models': handler }, { port: 0, logger: { log() {}, warn() {}, error() {} } });
srv.token = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
srv.server = http.createServer((req, res) => srv._handleRequest(req, res));
const port = await new Promise((resolve) => srv.server.listen(0, '127.0.0.1', () => resolve(srv.server.address().port)));
srv.actualPort = port;
const res = await new Promise((resolve, reject) => {
const req = http.request({ host: '127.0.0.1', port, path: '/v1/models', method: 'GET', headers: { authorization: 'Bearer aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' } }, (r) => {
const ch = []; r.on('data', (d) => ch.push(d)); r.on('end', () => resolve({ status: r.statusCode, bytes: Buffer.concat(ch) }));
});
req.on('error', reject); req.end();
});
await new Promise((r) => srv.server.close(r));
assert.equal(res.status, 401);
assert.match(res.bytes.toString('utf8'), /api key required/);
});
});