mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-15 00:17:09 +00:00
feat(proxy): complete openai routing scope
This commit is contained in:
1 parent
b346693c3a
commit
17187c4abd
24 files changed
+1819
-53
No files matched your search
@@ -0,0 +1,194 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { resolveProxyRequestRoute } from '../../../src/proxy/request-router';
|
||||
import { resolveOpenAICompatProfileConfig } from '../../../src/proxy/profile-router';
|
||||
import { loadSettings } from '../../../src/utils/config-manager';
|
||||
|
||||
let originalCcsHome: string | undefined;
|
||||
let tempDir: string;
|
||||
|
||||
function writeSettings(profileName: string, env: Record<string, string>): string {
|
||||
const settingsPath = path.join(tempDir, '.ccs', `${profileName}.settings.json`);
|
||||
fs.writeFileSync(settingsPath, JSON.stringify({ env }, null, 2), 'utf8');
|
||||
return settingsPath;
|
||||
}
|
||||
|
||||
function buildProfile(profileName: string) {
|
||||
const settingsPath = path.join(tempDir, '.ccs', `${profileName}.settings.json`);
|
||||
const profile = resolveOpenAICompatProfileConfig(
|
||||
profileName,
|
||||
settingsPath,
|
||||
loadSettings(settingsPath).env || {}
|
||||
);
|
||||
expect(profile).toBeTruthy();
|
||||
return profile!;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
originalCcsHome = process.env.CCS_HOME;
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-proxy-router-'));
|
||||
fs.mkdirSync(path.join(tempDir, '.ccs'), { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalCcsHome !== undefined) {
|
||||
process.env.CCS_HOME = originalCcsHome;
|
||||
} else {
|
||||
delete process.env.CCS_HOME;
|
||||
}
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('resolveProxyRequestRoute', () => {
|
||||
beforeEach(() => {
|
||||
process.env.CCS_HOME = tempDir;
|
||||
|
||||
const profiles = {
|
||||
hf: writeSettings('hf', {
|
||||
ANTHROPIC_BASE_URL: 'https://router.huggingface.co/v1',
|
||||
ANTHROPIC_AUTH_TOKEN: 'hf_token',
|
||||
ANTHROPIC_MODEL: 'hf-default',
|
||||
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
|
||||
}),
|
||||
deepseek: writeSettings('deepseek', {
|
||||
ANTHROPIC_BASE_URL: 'https://api.deepseek.com/v1',
|
||||
ANTHROPIC_AUTH_TOKEN: 'deepseek_token',
|
||||
ANTHROPIC_MODEL: 'deepseek-chat',
|
||||
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
|
||||
}),
|
||||
thinker: writeSettings('thinker', {
|
||||
ANTHROPIC_BASE_URL: 'https://thinking.example.com/v1',
|
||||
ANTHROPIC_AUTH_TOKEN: 'think_token',
|
||||
ANTHROPIC_MODEL: 'deepseek-reasoner',
|
||||
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
|
||||
}),
|
||||
search: writeSettings('search', {
|
||||
ANTHROPIC_BASE_URL: 'https://search.example.com/v1',
|
||||
ANTHROPIC_AUTH_TOKEN: 'search_token',
|
||||
ANTHROPIC_MODEL: 'sonar-pro',
|
||||
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
|
||||
}),
|
||||
background: writeSettings('background', {
|
||||
ANTHROPIC_BASE_URL: 'https://background.example.com/v1',
|
||||
ANTHROPIC_AUTH_TOKEN: 'background_token',
|
||||
ANTHROPIC_MODEL: 'qwen-small',
|
||||
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
|
||||
}),
|
||||
long: writeSettings('long', {
|
||||
ANTHROPIC_BASE_URL: 'https://long.example.com/v1',
|
||||
ANTHROPIC_AUTH_TOKEN: 'long_token',
|
||||
ANTHROPIC_MODEL: 'gemini-2.5-pro',
|
||||
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
|
||||
}),
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, '.ccs', 'config.json'),
|
||||
JSON.stringify(
|
||||
{
|
||||
profiles,
|
||||
proxy: {
|
||||
routing: {
|
||||
default: 'hf:hf-default',
|
||||
background: 'background:qwen-small',
|
||||
think: 'thinker:deepseek-reasoner',
|
||||
longContext: 'long:gemini-2.5-pro',
|
||||
longContextThreshold: 10,
|
||||
webSearch: 'search:sonar-pro',
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
'utf8'
|
||||
);
|
||||
});
|
||||
|
||||
it('uses explicit profile:model selectors', () => {
|
||||
const route = resolveProxyRequestRoute(buildProfile('hf'), {
|
||||
model: 'deepseek:deepseek-reasoner',
|
||||
stream: true,
|
||||
messages: [{ role: 'user', content: 'plan this' }],
|
||||
});
|
||||
|
||||
expect(route.profile.profileName).toBe('deepseek');
|
||||
expect(route.model).toBe('deepseek-reasoner');
|
||||
expect(route.source).toBe('explicit-profile');
|
||||
});
|
||||
|
||||
it('matches plain model ids to the profile that owns them', () => {
|
||||
const route = resolveProxyRequestRoute(buildProfile('hf'), {
|
||||
model: 'deepseek-chat',
|
||||
stream: true,
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
});
|
||||
|
||||
expect(route.profile.profileName).toBe('deepseek');
|
||||
expect(route.model).toBe('deepseek-chat');
|
||||
expect(route.source).toBe('profile-model-match');
|
||||
});
|
||||
|
||||
it('routes thinking requests through the configured think scenario', () => {
|
||||
const route = resolveProxyRequestRoute(buildProfile('hf'), {
|
||||
model: 'hf-default',
|
||||
stream: true,
|
||||
reasoning_effort: 'high',
|
||||
reasoning: { enabled: true, effort: 'high' },
|
||||
messages: [{ role: 'user', content: 'work this out carefully' }],
|
||||
});
|
||||
|
||||
expect(route.profile.profileName).toBe('thinker');
|
||||
expect(route.model).toBe('deepseek-reasoner');
|
||||
expect(route.scenario).toBe('think');
|
||||
expect(route.source).toBe('scenario');
|
||||
});
|
||||
|
||||
it('routes long-context requests through the configured longContext scenario', () => {
|
||||
const route = resolveProxyRequestRoute(buildProfile('hf'), {
|
||||
model: 'hf-default',
|
||||
stream: true,
|
||||
messages: [{ role: 'user', content: 'x'.repeat(120) }],
|
||||
});
|
||||
|
||||
expect(route.profile.profileName).toBe('long');
|
||||
expect(route.model).toBe('gemini-2.5-pro');
|
||||
expect(route.scenario).toBe('longContext');
|
||||
expect(route.estimatedTokens).toBeGreaterThan(10);
|
||||
});
|
||||
|
||||
it('routes web_search tool requests through the configured webSearch scenario', () => {
|
||||
const route = resolveProxyRequestRoute(buildProfile('hf'), {
|
||||
model: 'hf-default',
|
||||
stream: true,
|
||||
messages: [{ role: 'user', content: 'search the docs' }],
|
||||
tools: [
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'web_search',
|
||||
parameters: { type: 'object' },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(route.profile.profileName).toBe('search');
|
||||
expect(route.model).toBe('sonar-pro');
|
||||
expect(route.scenario).toBe('webSearch');
|
||||
});
|
||||
|
||||
it('routes haiku requests through the configured background scenario', () => {
|
||||
const route = resolveProxyRequestRoute(buildProfile('hf'), {
|
||||
model: 'claude-3-haiku',
|
||||
stream: true,
|
||||
messages: [{ role: 'user', content: 'run this in the background' }],
|
||||
});
|
||||
|
||||
expect(route.profile.profileName).toBe('background');
|
||||
expect(route.model).toBe('qwen-small');
|
||||
expect(route.scenario).toBe('background');
|
||||
});
|
||||
});
|
||||
@@ -28,6 +28,7 @@ describe('ProxyRequestTransformer', () => {
|
||||
|
||||
expect(result.stream).toBe(true);
|
||||
expect(result.reasoning_effort).toBe('high');
|
||||
expect(result.reasoning).toEqual({ enabled: true, effort: 'high' });
|
||||
expect(result.max_tokens).toBe(1024);
|
||||
expect(result.temperature).toBe(0.2);
|
||||
expect(result.top_p).toBe(0.9);
|
||||
@@ -42,6 +43,43 @@ describe('ProxyRequestTransformer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('translates base64 image blocks into OpenAI image_url parts', () => {
|
||||
const transformer = new ProxyRequestTransformer();
|
||||
const result = transformer.transform({
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: 'Describe this image' },
|
||||
{
|
||||
type: 'image',
|
||||
source: {
|
||||
type: 'base64',
|
||||
media_type: 'image/png',
|
||||
data: 'ZmFrZS1pbWFnZS1ieXRlcw==',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.messages).toEqual([
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: 'Describe this image' },
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: 'data:image/png;base64,ZmFrZS1pbWFnZS1ieXRlcw==',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('drops malformed optional fields but preserves the translated core request', () => {
|
||||
const transformer = new ProxyRequestTransformer();
|
||||
const result = transformer.transform({
|
||||
|
||||
Reference in new issue
Block a user