import { createServer, type Server } from 'node:http'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { baseUrlHint, describeDroppedKeys, normalizeBaseUrl, probeResponsesEndpoint, toProviderSpec, } from './providerService'; import type { ApplyProviderInput } from './types'; let server: Server; let port = 0; let nextStatus = 404; let lastRequest: { url: string | null; auth: string | null } = { url: null, auth: null }; beforeAll(async () => { server = createServer((req, res) => { lastRequest = { url: req.url ?? null, auth: req.headers.authorization ?? null }; req.resume(); res.writeHead(nextStatus, { 'content-type': 'application/json' }); res.end(JSON.stringify({ status: nextStatus })); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const address = server.address(); port = typeof address === 'object' && address ? address.port : 0; }); afterAll(async () => { await new Promise((resolve) => server.close(() => resolve())); }); const openaiLike: ApplyProviderInput = { modelId: 'qwen3-max', name: '通义千问', baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1/', apiKey: 'sk-test-key', providerType: 'DASHSCOPE', headersJson: JSON.stringify({ 'X-DashScope-WorkSpace': 'ws-1' }), config: JSON.stringify({ request_max_retries: 3, temperature: 0.2 }), }; describe('normalizeBaseUrl / baseUrlHint', () => { it('去掉尾部斜杠,空值返回 null', () => { expect(normalizeBaseUrl('https://x.com/v1///')).toBe('https://x.com/v1'); expect(normalizeBaseUrl(' ')).toBeNull(); expect(normalizeBaseUrl(null)).toBeNull(); }); it('缺版本段时给非阻断提示', () => { expect(baseUrlHint('https://x.com')).toMatch(/v1/); expect(baseUrlHint('https://x.com/v1')).toBeNull(); expect(baseUrlHint('ftp://x.com/v1')).toMatch(/http/); expect(baseUrlHint(null)).toMatch(/未配置/); }); }); describe('toProviderSpec', () => { it('OpenAI 兼容端点:写 model_providers.zsjz,key 只留给 env', () => { const spec = toProviderSpec(openaiLike); expect(spec).toMatchObject({ id: 'zsjz', model: 'qwen3-max', baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1', apiKey: 'sk-test-key', envKey: 'ZSJZ_CODEX_API_KEY', }); expect(spec.builtinProvider ?? null).toBeNull(); expect(spec.httpHeaders).toEqual({ 'X-DashScope-WorkSpace': 'ws-1' }); // config 里只放行白名单键,temperature 必须被丢掉 expect(spec.extra).toEqual({ request_max_retries: 3 }); }); it('OLLAMA 走 Codex 内置 provider,不需要 baseUrl 与 key', () => { const spec = toProviderSpec({ modelId: 'qwen3:8b', providerType: 'OLLAMA' }); expect(spec).toMatchObject({ id: null, builtinProvider: 'ollama', model: 'qwen3:8b' }); expect(spec.baseUrl).toBeNull(); }); it('远端 Ollama 必须保留并补齐 base_url,不能退回 localhost:11434', () => { expect( toProviderSpec({ modelId: 'qwen', providerType: 'OLLAMA', baseUrl: 'http://10.66.66.66:8080' }).baseUrl, ).toBe('http://10.66.66.66:8080/v1'); expect( toProviderSpec({ modelId: 'qwen', providerType: 'OLLAMA', baseUrl: 'http://host:11434/v1/' }).baseUrl, ).toBe('http://host:11434/v1'); }); it('可显式指定内置 provider(lmstudio)', () => { const spec = toProviderSpec({ modelId: 'local-model', builtinProvider: 'lmstudio' }); expect(spec.builtinProvider).toBe('lmstudio'); }); it('缺 modelId 或非内置 provider 缺 baseUrl 时报错', () => { expect(() => toProviderSpec({ modelId: ' ' })).toThrow(/modelId/); expect(() => toProviderSpec({ modelId: 'gpt', providerType: 'OPENAI' })).toThrow(/baseUrl/); }); it('headersJson 与 config 支持对象或 JSON 字符串,非法 JSON 忽略', () => { expect(toProviderSpec({ ...openaiLike, headersJson: { A: '1' } }).httpHeaders).toEqual({ A: '1' }); expect(toProviderSpec({ ...openaiLike, headersJson: '{bad json' }).httpHeaders).toBeNull(); expect(toProviderSpec({ ...openaiLike, config: { stream_max_retries: 5 } }).extra).toEqual({ stream_max_retries: 5, }); }); it('describeDroppedKeys 报告被丢弃的非白名单键', () => { expect(describeDroppedKeys(openaiLike)).toEqual(['temperature']); expect(describeDroppedKeys({ modelId: 'x' })).toEqual([]); }); }); describe('probeResponsesEndpoint', () => { it('打到 base_url 下的 /responses', async () => { nextStatus = 401; await probeResponsesEndpoint(`http://127.0.0.1:${port}/v1/`, 'sk-test-key'); expect(lastRequest.url).toBe('/v1/responses'); expect(lastRequest.auth).toBe('Bearer sk-test-key'); }); it('404/405 判为未实现 Responses API', async () => { nextStatus = 404; const notFound = await probeResponsesEndpoint(`http://127.0.0.1:${port}/v1`); expect(notFound).toMatchObject({ compatible: false, status: 404 }); expect(notFound.detail).toMatch(/未实现 Responses API/); nextStatus = 405; expect((await probeResponsesEndpoint(`http://127.0.0.1:${port}/v1`)).compatible).toBe(false); }); it('401/400/2xx 判为路由存在(兼容)', async () => { for (const status of [401, 400, 200]) { nextStatus = status; const result = await probeResponsesEndpoint(`http://127.0.0.1:${port}/v1`); expect(result).toMatchObject({ compatible: true, status }); } }); it('端点不可达时 status 为 null', async () => { const result = await probeResponsesEndpoint('http://127.0.0.1:1/v1'); expect(result.compatible).toBe(false); expect(result.status).toBeNull(); expect(result.detail).toMatch(/不可达/); }); });