| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145 |
- 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<void>((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<void>((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(/不可达/);
- });
- });
|