Add platform adapters and tests for CLIProxyAPI, OneHub, Sub2API, and title hint detection
- Implemented `CliProxyApiAdapter` with detection logic for CLIProxyAPI. - Added tests for `CliProxyApiAdapter` to verify detection and response handling. - Created `OneHubAdapter` with methods for fetching models and user groups. - Added tests for `OneHubAdapter` to ensure correct fallback and response parsing. - Developed `Sub2ApiAdapter` with detection and user info fetching capabilities. - Added comprehensive tests for `Sub2ApiAdapter` covering various scenarios. - Introduced `TitleHintPlatform` type and detection logic based on HTML title. - Implemented tests for title hint detection to identify platforms from URLs.
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const fetchModelPricingCatalogMock = vi.fn(async (_arg?: unknown) => null);
|
||||
|
||||
vi.mock('../../services/modelPricingService.js', () => ({
|
||||
fetchModelPricingCatalog: (arg: unknown) => fetchModelPricingCatalogMock(arg),
|
||||
}));
|
||||
|
||||
import { resolveUpstreamEndpointCandidates } from './upstreamEndpoint.js';
|
||||
|
||||
const baseContext = {
|
||||
site: {
|
||||
id: 1,
|
||||
url: 'https://upstream.example.com',
|
||||
platform: '',
|
||||
apiKey: null,
|
||||
},
|
||||
account: {
|
||||
id: 2,
|
||||
accessToken: 'token-demo',
|
||||
apiToken: null,
|
||||
},
|
||||
};
|
||||
|
||||
describe('resolveUpstreamEndpointCandidates', () => {
|
||||
beforeEach(() => {
|
||||
fetchModelPricingCatalogMock.mockReset();
|
||||
fetchModelPricingCatalogMock.mockResolvedValue(null);
|
||||
});
|
||||
|
||||
it('uses responses -> messages -> chat for unknown platforms', async () => {
|
||||
const openaiOrder = await resolveUpstreamEndpointCandidates(
|
||||
{
|
||||
...baseContext,
|
||||
site: { ...baseContext.site, platform: 'new-api' },
|
||||
},
|
||||
'gpt-5.3',
|
||||
'openai',
|
||||
);
|
||||
expect(openaiOrder).toEqual(['responses', 'messages', 'chat']);
|
||||
|
||||
const claudeOrder = await resolveUpstreamEndpointCandidates(
|
||||
{
|
||||
...baseContext,
|
||||
site: { ...baseContext.site, platform: 'new-api' },
|
||||
},
|
||||
'gpt-5.3',
|
||||
'claude',
|
||||
);
|
||||
expect(claudeOrder).toEqual(['responses', 'messages', 'chat']);
|
||||
|
||||
const responsesOrder = await resolveUpstreamEndpointCandidates(
|
||||
{
|
||||
...baseContext,
|
||||
site: { ...baseContext.site, platform: 'new-api' },
|
||||
},
|
||||
'gpt-5.3',
|
||||
'responses',
|
||||
);
|
||||
expect(responsesOrder).toEqual(['responses', 'messages', 'chat']);
|
||||
});
|
||||
|
||||
it('keeps explicit platform priority rules', async () => {
|
||||
const openaiOrder = await resolveUpstreamEndpointCandidates(
|
||||
{
|
||||
...baseContext,
|
||||
site: { ...baseContext.site, platform: 'openai' },
|
||||
},
|
||||
'gpt-5.3',
|
||||
'openai',
|
||||
);
|
||||
expect(openaiOrder).toEqual(['chat', 'responses']);
|
||||
|
||||
const openaiResponsesOrder = await resolveUpstreamEndpointCandidates(
|
||||
{
|
||||
...baseContext,
|
||||
site: { ...baseContext.site, platform: 'openai' },
|
||||
},
|
||||
'gpt-5.3',
|
||||
'responses',
|
||||
);
|
||||
expect(openaiResponsesOrder).toEqual(['responses', 'chat']);
|
||||
|
||||
const claudeOrder = await resolveUpstreamEndpointCandidates(
|
||||
{
|
||||
...baseContext,
|
||||
site: { ...baseContext.site, platform: 'claude' },
|
||||
},
|
||||
'claude-opus-4-6',
|
||||
'claude',
|
||||
);
|
||||
expect(claudeOrder).toEqual(['messages']);
|
||||
});
|
||||
|
||||
it('keeps anyrouter messages-first special case', async () => {
|
||||
const openaiOrder = await resolveUpstreamEndpointCandidates(
|
||||
{
|
||||
...baseContext,
|
||||
site: { ...baseContext.site, platform: 'anyrouter' },
|
||||
},
|
||||
'claude-opus-4-6',
|
||||
'openai',
|
||||
);
|
||||
expect(openaiOrder).toEqual(['messages', 'chat']);
|
||||
|
||||
const responsesOrder = await resolveUpstreamEndpointCandidates(
|
||||
{
|
||||
...baseContext,
|
||||
site: { ...baseContext.site, platform: 'anyrouter' },
|
||||
},
|
||||
'claude-opus-4-6',
|
||||
'responses',
|
||||
);
|
||||
expect(responsesOrder).toEqual(['responses', 'messages', 'chat']);
|
||||
});
|
||||
});
|
||||
@@ -381,13 +381,9 @@ function preferredEndpointOrder(downstreamFormat: EndpointPreference, sitePlatfo
|
||||
return ['messages'];
|
||||
}
|
||||
|
||||
if (downstreamFormat === 'responses') {
|
||||
return ['responses', 'chat', 'messages'];
|
||||
}
|
||||
|
||||
return downstreamFormat === 'claude'
|
||||
? ['messages', 'chat']
|
||||
: ['chat', 'messages'];
|
||||
// Unknown/generic upstreams: try the most expressive protocol first,
|
||||
// then degrade to anthropic messages, and finally chat completions.
|
||||
return ['responses', 'messages', 'chat'];
|
||||
}
|
||||
|
||||
export async function resolveUpstreamEndpointCandidates(
|
||||
|
||||
Reference in New Issue
Block a user