mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 20:15:40 +00:00
feat(api): add Hugging Face provider preset
- add the huggingface preset and hf alias to the shared preset catalog - default the preset to Factory Droid because the documented path is generic chat completions - cover the CLI and dialog behavior with preset metadata and UI tests
This commit is contained in:
1 parent
270810c06e
commit
83255f4771
7 files changed
+79
-2
No files matched your search
@@ -265,12 +265,17 @@ async function resolveModelConfiguration(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function resolveDefaultTarget(
|
async function resolveDefaultTarget(
|
||||||
|
preset: ProviderPreset | null,
|
||||||
providedTarget: TargetType | undefined,
|
providedTarget: TargetType | undefined,
|
||||||
yes: boolean | undefined
|
yes: boolean | undefined
|
||||||
): Promise<TargetType> {
|
): Promise<TargetType> {
|
||||||
if (providedTarget) {
|
if (providedTarget) {
|
||||||
return providedTarget;
|
return providedTarget;
|
||||||
}
|
}
|
||||||
|
if (preset?.defaultTarget) {
|
||||||
|
console.log(info(`Using preset default target: ${preset.defaultTarget}`));
|
||||||
|
return preset.defaultTarget;
|
||||||
|
}
|
||||||
if (yes) {
|
if (yes) {
|
||||||
return 'claude';
|
return 'claude';
|
||||||
}
|
}
|
||||||
@@ -349,7 +354,7 @@ export async function handleApiCreateCommand(args: string[]): Promise<void> {
|
|||||||
parsedArgs.name,
|
parsedArgs.name,
|
||||||
parsedArgs.yes
|
parsedArgs.yes
|
||||||
);
|
);
|
||||||
const target = await resolveDefaultTarget(parsedArgs.target, parsedArgs.yes);
|
const target = await resolveDefaultTarget(null, parsedArgs.target, parsedArgs.yes);
|
||||||
|
|
||||||
if (name && apiProfileExists(name) && !parsedArgs.force) {
|
if (name && apiProfileExists(name) && !parsedArgs.force) {
|
||||||
console.log(fail(`API '${name}' already exists`));
|
console.log(fail(`API '${name}' already exists`));
|
||||||
@@ -464,7 +469,7 @@ export async function handleApiCreateCommand(args: string[]): Promise<void> {
|
|||||||
const finalModels = hasClaudeMappings
|
const finalModels = hasClaudeMappings
|
||||||
? applyClaudeExtendedContextPreference(models, shouldEnableClaudeLongContext)
|
? applyClaudeExtendedContextPreference(models, shouldEnableClaudeLongContext)
|
||||||
: models;
|
: models;
|
||||||
const target = await resolveDefaultTarget(parsedArgs.target, parsedArgs.yes);
|
const target = await resolveDefaultTarget(preset, parsedArgs.target, parsedArgs.yes);
|
||||||
|
|
||||||
if (parsedArgs.extendedContext !== undefined && !hasClaudeMappings) {
|
if (parsedArgs.extendedContext !== undefined && !hasClaudeMappings) {
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|||||||
@@ -91,6 +91,9 @@ export async function showApiCommandHelp(writeLine: HelpWriter = console.log): P
|
|||||||
writeLine(` ${color('ccs api create --preset openrouter', 'command')}`);
|
writeLine(` ${color('ccs api create --preset openrouter', 'command')}`);
|
||||||
writeLine(` ${color('ccs api create --preset alibaba-coding-plan', 'command')}`);
|
writeLine(` ${color('ccs api create --preset alibaba-coding-plan', 'command')}`);
|
||||||
writeLine(` ${color('ccs api create --preset alibaba', 'command')} ${dim('# alias')}`);
|
writeLine(` ${color('ccs api create --preset alibaba', 'command')} ${dim('# alias')}`);
|
||||||
|
writeLine(
|
||||||
|
` ${color('ccs api create hf-router --preset hf', 'command')} ${dim('# defaults to droid for generic chat completions')}`
|
||||||
|
);
|
||||||
writeLine(` ${color('ccs api create --preset glm', 'command')}`);
|
writeLine(` ${color('ccs api create --preset glm', 'command')}`);
|
||||||
writeLine('');
|
writeLine('');
|
||||||
writeLine(subheader('Claude Long Context'));
|
writeLine(subheader('Claude Long Context'));
|
||||||
|
|||||||
@@ -6,10 +6,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export type PresetCategory = 'recommended' | 'alternative';
|
export type PresetCategory = 'recommended' | 'alternative';
|
||||||
|
export type ProviderPresetTarget = 'claude' | 'droid' | 'codex';
|
||||||
|
|
||||||
export const PROVIDER_PRESET_IDS = [
|
export const PROVIDER_PRESET_IDS = [
|
||||||
'openrouter',
|
'openrouter',
|
||||||
'alibaba-coding-plan',
|
'alibaba-coding-plan',
|
||||||
|
'huggingface',
|
||||||
'ollama',
|
'ollama',
|
||||||
'llamacpp',
|
'llamacpp',
|
||||||
'anthropic',
|
'anthropic',
|
||||||
@@ -36,6 +38,7 @@ export interface ProviderPresetDefinition {
|
|||||||
apiKeyHint: string;
|
apiKeyHint: string;
|
||||||
category: PresetCategory;
|
category: PresetCategory;
|
||||||
requiresApiKey: boolean;
|
requiresApiKey: boolean;
|
||||||
|
defaultTarget?: ProviderPresetTarget;
|
||||||
/** Additional env vars for thinking mode, etc. */
|
/** Additional env vars for thinking mode, etc. */
|
||||||
extraEnv?: Record<string, string>;
|
extraEnv?: Record<string, string>;
|
||||||
/** Enable always thinking mode. */
|
/** Enable always thinking mode. */
|
||||||
@@ -57,6 +60,7 @@ export const PROVIDER_PRESET_ALIASES: Readonly<Record<string, ProviderPresetId>>
|
|||||||
kimi: 'km',
|
kimi: 'km',
|
||||||
alibaba: 'alibaba-coding-plan',
|
alibaba: 'alibaba-coding-plan',
|
||||||
acp: 'alibaba-coding-plan',
|
acp: 'alibaba-coding-plan',
|
||||||
|
hf: 'huggingface',
|
||||||
});
|
});
|
||||||
|
|
||||||
const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [
|
const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [
|
||||||
@@ -135,6 +139,20 @@ const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [
|
|||||||
featured: true,
|
featured: true,
|
||||||
icon: '/assets/providers/claude.svg',
|
icon: '/assets/providers/claude.svg',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'huggingface',
|
||||||
|
name: 'Hugging Face',
|
||||||
|
description: 'Inference Providers router via OpenAI-compatible chat completions',
|
||||||
|
baseUrl: 'https://router.huggingface.co/v1',
|
||||||
|
defaultProfileName: 'hf',
|
||||||
|
defaultModel: 'openai/gpt-oss-120b:fastest',
|
||||||
|
apiKeyPlaceholder: 'hf_...',
|
||||||
|
apiKeyHint: 'Create a User Access Token at hf.co/settings/tokens',
|
||||||
|
category: 'alternative',
|
||||||
|
requiresApiKey: true,
|
||||||
|
defaultTarget: 'droid',
|
||||||
|
badge: 'Router',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'glm',
|
id: 'glm',
|
||||||
name: 'GLM',
|
name: 'GLM',
|
||||||
|
|||||||
@@ -73,6 +73,25 @@ describe('provider-presets', () => {
|
|||||||
expect(preset?.defaultProfileName).toBe('qwen-api');
|
expect(preset?.defaultProfileName).toBe('qwen-api');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('resolves Hugging Face preset metadata', () => {
|
||||||
|
const preset = getPresetById('huggingface');
|
||||||
|
expect(preset?.id).toBe('huggingface');
|
||||||
|
expect(preset?.baseUrl).toBe('https://router.huggingface.co/v1');
|
||||||
|
expect(preset?.defaultProfileName).toBe('hf');
|
||||||
|
expect(preset?.defaultModel).toBe('openai/gpt-oss-120b:fastest');
|
||||||
|
expect(preset?.defaultTarget).toBe('droid');
|
||||||
|
expect(preset?.apiKeyPlaceholder).toBe('hf_...');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves hf alias to the Hugging Face preset', () => {
|
||||||
|
const preset = getPresetById('hf');
|
||||||
|
expect(preset?.id).toBe('huggingface');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats hf alias as a valid preset id', () => {
|
||||||
|
expect(isValidPresetId('hf')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps Anthropic direct last in the recommended preset order and reuses the Claude logo', () => {
|
it('keeps Anthropic direct last in the recommended preset order and reuses the Claude logo', () => {
|
||||||
const recommendedPresetIds = PROVIDER_PRESETS.filter(
|
const recommendedPresetIds = PROVIDER_PRESETS.filter(
|
||||||
(preset) => preset.category === 'recommended'
|
(preset) => preset.category === 'recommended'
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ export function ProfileCreateDialog({
|
|||||||
opusModel: preset.defaultModel,
|
opusModel: preset.defaultModel,
|
||||||
sonnetModel: preset.defaultModel,
|
sonnetModel: preset.defaultModel,
|
||||||
haikuModel: preset.defaultModel,
|
haikuModel: preset.defaultModel,
|
||||||
|
target: preset.defaultTarget ?? 'claude',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[reset]
|
[reset]
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ describe('ProfileCreateDialog', () => {
|
|||||||
expect(screen.getByText('More Presets')).toBeInTheDocument();
|
expect(screen.getByText('More Presets')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Local Runtimes')).toBeInTheDocument();
|
expect(screen.getByText('Local Runtimes')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Alibaba Coding Plan')).toBeVisible();
|
expect(screen.getByText('Alibaba Coding Plan')).toBeVisible();
|
||||||
|
expect(screen.getByText('Hugging Face')).toBeVisible();
|
||||||
expect(document.body.querySelectorAll('.overflow-x-auto')).toHaveLength(2);
|
expect(document.body.querySelectorAll('.overflow-x-auto')).toHaveLength(2);
|
||||||
|
|
||||||
const customButton = screen.getByRole('button', { name: /Custom Endpoint/i });
|
const customButton = screen.getByRole('button', { name: /Custom Endpoint/i });
|
||||||
@@ -74,4 +75,27 @@ describe('ProfileCreateDialog', () => {
|
|||||||
expect(screen.getByDisplayValue('http://localhost:11434')).toBeInTheDocument();
|
expect(screen.getByDisplayValue('http://localhost:11434')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Local Runtimes')).toBeInTheDocument();
|
expect(screen.getByText('Local Runtimes')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('steers the Hugging Face preset to the droid target by default', async () => {
|
||||||
|
render(
|
||||||
|
<ProfileCreateDialog
|
||||||
|
open
|
||||||
|
onOpenChange={vi.fn()}
|
||||||
|
onSuccess={vi.fn()}
|
||||||
|
initialMode="openrouter"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const huggingFaceButton = screen.getByText('Hugging Face').closest('button');
|
||||||
|
expect(huggingFaceButton).not.toBeNull();
|
||||||
|
if (!huggingFaceButton) {
|
||||||
|
throw new Error('Hugging Face preset button not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await userEvent.click(huggingFaceButton);
|
||||||
|
|
||||||
|
expect(screen.getByDisplayValue('hf')).toBeInTheDocument();
|
||||||
|
expect(screen.getByDisplayValue('https://router.huggingface.co/v1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('combobox')).toHaveTextContent('Factory Droid');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -32,6 +32,13 @@ describe('provider preset metadata', () => {
|
|||||||
expect(getPresetById('glmt')?.id).toBe('glm');
|
expect(getPresetById('glmt')?.id).toBe('glm');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('maps hf alias to the Hugging Face preset', () => {
|
||||||
|
const preset = getPresetById('hf');
|
||||||
|
expect(preset?.id).toBe('huggingface');
|
||||||
|
expect(preset?.baseUrl).toBe('https://router.huggingface.co/v1');
|
||||||
|
expect(preset?.defaultTarget).toBe('droid');
|
||||||
|
});
|
||||||
|
|
||||||
it('uses the llama.cpp provider logo asset for the local llama.cpp preset', () => {
|
it('uses the llama.cpp provider logo asset for the local llama.cpp preset', () => {
|
||||||
expect(getPresetById('llamacpp')?.icon).toBe('/assets/providers/llama-cpp.svg');
|
expect(getPresetById('llamacpp')?.icon).toBe('/assets/providers/llama-cpp.svg');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in new issue
Block a user