mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 12:19:35 +00:00
feat(ui): add Ollama provider presets to dashboard
- Add ollama (local) and ollama-cloud presets to UI provider-presets.ts - Update profile-create-dialog to handle requiresApiKey: false - Show "No API Key Required" info box for local Ollama - Add --api-key ignored warning to CLI for local Ollama preset - Add ollama-cloud to CLI help text preset list Fixes edge cases identified in PR review: - UI dashboard now has parity with CLI Ollama support - Conditional API key validation in profile creation form
This commit is contained in:
@@ -186,6 +186,9 @@ async function handleCreate(args: string[]): Promise<void> {
|
|||||||
|
|
||||||
if (preset?.noApiKey) {
|
if (preset?.noApiKey) {
|
||||||
// Preset doesn't require API key (e.g., local Ollama)
|
// Preset doesn't require API key (e.g., local Ollama)
|
||||||
|
if (parsedArgs.apiKey) {
|
||||||
|
console.log(dim('Note: API key ignored for local Ollama (not required)'));
|
||||||
|
}
|
||||||
console.log(info('No API key required for local Ollama'));
|
console.log(info('No API key required for local Ollama'));
|
||||||
apiKey = ''; // Empty string for local providers
|
apiKey = ''; // Empty string for local providers
|
||||||
} else if (!apiKey) {
|
} else if (!apiKey) {
|
||||||
@@ -431,7 +434,7 @@ async function showHelp(): Promise<void> {
|
|||||||
console.log('');
|
console.log('');
|
||||||
console.log(subheader('Options'));
|
console.log(subheader('Options'));
|
||||||
console.log(
|
console.log(
|
||||||
` ${color('--preset <id>', 'command')} Use provider preset (openrouter, ollama, glm, glmt, kimi, foundry, mm, deepseek, qwen)`
|
` ${color('--preset <id>', 'command')} Use provider preset (openrouter, ollama, ollama-cloud, glm, glmt, kimi, foundry, mm, deepseek, qwen)`
|
||||||
);
|
);
|
||||||
console.log(` ${color('--base-url <url>', 'command')} API base URL (create)`);
|
console.log(` ${color('--base-url <url>', 'command')} API base URL (create)`);
|
||||||
console.log(` ${color('--api-key <key>', 'command')} API key (create)`);
|
console.log(` ${color('--api-key <key>', 'command')} API key (create)`);
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const schema = z.object({
|
|||||||
.min(1, 'Name is required')
|
.min(1, 'Name is required')
|
||||||
.regex(/^[a-zA-Z][a-zA-Z0-9._-]*$/, 'Must start with letter, only letters/numbers/.-_'),
|
.regex(/^[a-zA-Z][a-zA-Z0-9._-]*$/, 'Must start with letter, only letters/numbers/.-_'),
|
||||||
baseUrl: z.string().url('Invalid URL format'),
|
baseUrl: z.string().url('Invalid URL format'),
|
||||||
apiKey: z.string().min(1, 'API key is required'),
|
apiKey: z.string(), // Validation handled conditionally in onSubmit
|
||||||
model: z.string().optional(),
|
model: z.string().optional(),
|
||||||
opusModel: z.string().optional(),
|
opusModel: z.string().optional(),
|
||||||
sonnetModel: z.string().optional(),
|
sonnetModel: z.string().optional(),
|
||||||
@@ -207,9 +207,16 @@ export function ProfileCreateDialog({
|
|||||||
}, [baseUrlValue, selectedPreset]);
|
}, [baseUrlValue, selectedPreset]);
|
||||||
|
|
||||||
const onSubmit = async (data: FormData) => {
|
const onSubmit = async (data: FormData) => {
|
||||||
|
// Validate API key - required unless preset has requiresApiKey: false
|
||||||
|
if (currentPreset?.requiresApiKey !== false && !data.apiKey) {
|
||||||
|
toast.error('API key is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Use user-provided baseUrl (allows customization of preset URLs)
|
// Use user-provided baseUrl (allows customization of preset URLs)
|
||||||
const finalData = {
|
const finalData = {
|
||||||
...data,
|
...data,
|
||||||
|
// For presets that don't require API key, use empty string
|
||||||
|
apiKey: currentPreset?.requiresApiKey === false ? '' : data.apiKey,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await createMutation.mutateAsync(finalData);
|
await createMutation.mutateAsync(finalData);
|
||||||
@@ -367,38 +374,51 @@ export function ProfileCreateDialog({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* API Key */}
|
{/* API Key - hidden for presets that don't require it */}
|
||||||
<div className="space-y-1.5">
|
{currentPreset?.requiresApiKey !== false ? (
|
||||||
<Label htmlFor="apiKey">
|
<div className="space-y-1.5">
|
||||||
API Key <span className="text-destructive">*</span>
|
<Label htmlFor="apiKey">
|
||||||
</Label>
|
API Key <span className="text-destructive">*</span>
|
||||||
<div className="relative">
|
</Label>
|
||||||
<Input
|
<div className="relative">
|
||||||
id="apiKey"
|
<Input
|
||||||
type={showApiKey ? 'text' : 'password'}
|
id="apiKey"
|
||||||
{...register('apiKey')}
|
type={showApiKey ? 'text' : 'password'}
|
||||||
placeholder={currentPreset?.apiKeyPlaceholder ?? 'sk-...'}
|
{...register('apiKey')}
|
||||||
className="pr-10"
|
placeholder={currentPreset?.apiKeyPlaceholder ?? 'sk-...'}
|
||||||
/>
|
className="pr-10"
|
||||||
<Button
|
/>
|
||||||
type="button"
|
<Button
|
||||||
variant="ghost"
|
type="button"
|
||||||
size="icon"
|
variant="ghost"
|
||||||
className="absolute right-0 top-0 h-9 w-9 text-muted-foreground hover:text-foreground"
|
size="icon"
|
||||||
onClick={() => setShowApiKey(!showApiKey)}
|
className="absolute right-0 top-0 h-9 w-9 text-muted-foreground hover:text-foreground"
|
||||||
tabIndex={-1}
|
onClick={() => setShowApiKey(!showApiKey)}
|
||||||
>
|
tabIndex={-1}
|
||||||
{showApiKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
>
|
||||||
</Button>
|
{showApiKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{errors.apiKey ? (
|
||||||
|
<p className="text-xs text-destructive">{errors.apiKey.message}</p>
|
||||||
|
) : (
|
||||||
|
currentPreset?.apiKeyHint && (
|
||||||
|
<p className="text-xs text-muted-foreground">{currentPreset.apiKeyHint}</p>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{errors.apiKey ? (
|
) : (
|
||||||
<p className="text-xs text-destructive">{errors.apiKey.message}</p>
|
<div className="flex items-start gap-3 p-3 bg-green-50 dark:bg-green-950/20 text-green-800 dark:text-green-300 rounded-md text-sm border border-green-100 dark:border-green-900/30">
|
||||||
) : (
|
<Info className="w-5 h-5 shrink-0 mt-0.5" />
|
||||||
currentPreset?.apiKeyHint && (
|
<div>
|
||||||
<p className="text-xs text-muted-foreground">{currentPreset.apiKeyHint}</p>
|
<p className="font-medium">No API Key Required</p>
|
||||||
)
|
<p className="text-xs opacity-90">
|
||||||
)}
|
{currentPreset?.apiKeyHint ||
|
||||||
</div>
|
'This provider runs locally and does not require authentication.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="models" className="p-6 mt-0 space-y-4">
|
<TabsContent value="models" className="p-6 mt-0 space-y-4">
|
||||||
|
|||||||
@@ -44,6 +44,21 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [
|
|||||||
apiKeyHint: 'Get your API key at openrouter.ai/keys',
|
apiKeyHint: 'Get your API key at openrouter.ai/keys',
|
||||||
category: 'recommended',
|
category: 'recommended',
|
||||||
},
|
},
|
||||||
|
// Recommended - Ollama (Local)
|
||||||
|
{
|
||||||
|
id: 'ollama',
|
||||||
|
name: 'Ollama (Local)',
|
||||||
|
description: 'Local open-source models via Ollama (32K+ context)',
|
||||||
|
baseUrl: 'http://localhost:11434',
|
||||||
|
defaultProfileName: 'ollama',
|
||||||
|
badge: 'Local',
|
||||||
|
featured: true,
|
||||||
|
defaultModel: 'qwen3-coder',
|
||||||
|
requiresApiKey: false,
|
||||||
|
apiKeyPlaceholder: '',
|
||||||
|
apiKeyHint: 'No API key required for local Ollama',
|
||||||
|
category: 'recommended',
|
||||||
|
},
|
||||||
// Alternative providers - GLM/GLMT/Kimi
|
// Alternative providers - GLM/GLMT/Kimi
|
||||||
{
|
{
|
||||||
id: 'glm',
|
id: 'glm',
|
||||||
@@ -136,6 +151,19 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [
|
|||||||
apiKeyHint: 'Get your API key from Alibaba Cloud Model Studio',
|
apiKeyHint: 'Get your API key from Alibaba Cloud Model Studio',
|
||||||
category: 'alternative',
|
category: 'alternative',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'ollama-cloud',
|
||||||
|
name: 'Ollama Cloud',
|
||||||
|
description: 'Ollama.com cloud models (glm-4.7:cloud, qwen3-coder:480b)',
|
||||||
|
baseUrl: 'https://ollama.com',
|
||||||
|
defaultProfileName: 'ollama-cloud',
|
||||||
|
badge: 'Cloud',
|
||||||
|
defaultModel: 'glm-4.7:cloud',
|
||||||
|
requiresApiKey: true,
|
||||||
|
apiKeyPlaceholder: 'YOUR_OLLAMA_CLOUD_API_KEY',
|
||||||
|
apiKeyHint: 'Get your API key at ollama.com',
|
||||||
|
category: 'alternative',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Get presets by category */
|
/** Get presets by category */
|
||||||
|
|||||||
Reference in New Issue
Block a user