feat(provider): mainly support moonshot / siliconflow on setup (#43)

This commit is contained in:
DigHuang
2026-02-10 19:33:33 -08:00
committed by GitHub
Unverified
parent 563fcd2f24
commit 1b508d5bde
16 changed files with 1305 additions and 634 deletions

View File

@@ -22,20 +22,14 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { Switch } from '@/components/ui/switch';
import { useProviderStore, type ProviderWithKeyInfo } from '@/stores/providers';
import { useProviderStore, type ProviderConfig, type ProviderWithKeyInfo } from '@/stores/providers';
import {
PROVIDER_TYPE_INFO,
type ProviderType,
} from '@/lib/providers';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
// Provider type definitions
const providerTypes = [
{ id: 'anthropic', name: 'Anthropic', icon: '🤖', placeholder: 'sk-ant-api03-...' },
{ id: 'openai', name: 'OpenAI', icon: '💚', placeholder: 'sk-proj-...' },
{ id: 'google', name: 'Google', icon: '🔷', placeholder: 'AIza...' },
{ id: 'openrouter', name: 'OpenRouter', icon: '🌐', placeholder: 'sk-or-v1-...' },
{ id: 'ollama', name: 'Ollama', icon: '🦙', placeholder: 'Not required' },
{ id: 'custom', name: 'Custom', icon: '⚙️', placeholder: 'API key...' },
];
export function ProvidersSettings() {
const {
providers,
@@ -45,7 +39,7 @@ export function ProvidersSettings() {
addProvider,
updateProvider,
deleteProvider,
setApiKey,
updateProviderWithKey,
setDefaultProvider,
validateApiKey,
} = useProviderStore();
@@ -58,15 +52,33 @@ export function ProvidersSettings() {
fetchProviders();
}, [fetchProviders]);
const handleAddProvider = async (type: string, name: string, apiKey: string) => {
const handleAddProvider = async (
type: ProviderType,
name: string,
apiKey: string,
options?: { baseUrl?: string; model?: string }
) => {
// Only custom supports multiple instances.
// Built-in providers remain singleton by type.
const id = type === 'custom' ? `custom-${crypto.randomUUID()}` : type;
try {
await addProvider({
id: `${type}-${Date.now()}`,
type: type as 'anthropic' | 'openai' | 'google' | 'ollama' | 'custom',
name,
enabled: true,
}, apiKey || undefined);
await addProvider(
{
id,
type,
name,
baseUrl: options?.baseUrl,
model: options?.model,
enabled: true,
},
apiKey.trim() || undefined
);
// Auto-set as default if this is the first provider
if (providers.length === 0) {
await setDefaultProvider(id);
}
setShowAddDialog(false);
toast.success('Provider added successfully');
} catch (error) {
@@ -140,8 +152,12 @@ export function ProvidersSettings() {
onDelete={() => handleDeleteProvider(provider.id)}
onSetDefault={() => handleSetDefault(provider.id)}
onToggleEnabled={() => handleToggleEnabled(provider)}
onUpdateKey={async (key) => {
await setApiKey(provider.id, key);
onSaveEdits={async (payload) => {
await updateProviderWithKey(
provider.id,
payload.updates || {},
payload.newApiKey
);
setEditingProvider(null);
}}
onValidateKey={(key) => validateApiKey(provider.id, key)}
@@ -153,8 +169,10 @@ export function ProvidersSettings() {
{/* Add Provider Dialog */}
{showAddDialog && (
<AddProviderDialog
existingTypes={new Set(providers.map((p) => p.type))}
onClose={() => setShowAddDialog(false)}
onAdd={handleAddProvider}
onValidateKey={(type, key) => validateApiKey(type, key)}
/>
)}
</div>
@@ -170,7 +188,7 @@ interface ProviderCardProps {
onDelete: () => void;
onSetDefault: () => void;
onToggleEnabled: () => void;
onUpdateKey: (key: string) => Promise<void>;
onSaveEdits: (payload: { newApiKey?: string; updates?: Partial<ProviderConfig> }) => Promise<void>;
onValidateKey: (key: string) => Promise<{ valid: boolean; error?: string }>;
}
@@ -198,37 +216,78 @@ function ProviderCard({
onDelete,
onSetDefault,
onToggleEnabled,
onUpdateKey,
onSaveEdits,
onValidateKey,
}: ProviderCardProps) {
const [newKey, setNewKey] = useState('');
const [baseUrl, setBaseUrl] = useState(provider.baseUrl || '');
const [modelId, setModelId] = useState(provider.model || '');
const [showKey, setShowKey] = useState(false);
const [validating, setValidating] = useState(false);
const [saving, setSaving] = useState(false);
const typeInfo = providerTypes.find((t) => t.id === provider.type);
const handleSaveKey = async () => {
if (!newKey) return;
setValidating(true);
const result = await onValidateKey(newKey);
setValidating(false);
if (!result.valid) {
toast.error(result.error || 'Invalid API key');
return;
const typeInfo = PROVIDER_TYPE_INFO.find((t) => t.id === provider.type);
const canEditConfig = Boolean(typeInfo?.showBaseUrl || typeInfo?.showModelId);
useEffect(() => {
if (isEditing) {
setNewKey('');
setShowKey(false);
setBaseUrl(provider.baseUrl || '');
setModelId(provider.model || '');
}
}, [isEditing, provider.baseUrl, provider.model]);
const handleSaveEdits = async () => {
setSaving(true);
try {
await onUpdateKey(newKey);
const payload: { newApiKey?: string; updates?: Partial<ProviderConfig> } = {};
if (newKey.trim()) {
setValidating(true);
const result = await onValidateKey(newKey);
setValidating(false);
if (!result.valid) {
toast.error(result.error || 'Invalid API key');
setSaving(false);
return;
}
payload.newApiKey = newKey.trim();
}
if (canEditConfig) {
if (typeInfo?.showModelId && !modelId.trim()) {
toast.error('Model ID is required');
setSaving(false);
return;
}
const updates: Partial<ProviderConfig> = {};
if ((baseUrl.trim() || undefined) !== (provider.baseUrl || undefined)) {
updates.baseUrl = baseUrl.trim() || undefined;
}
if ((modelId.trim() || undefined) !== (provider.model || undefined)) {
updates.model = modelId.trim() || undefined;
}
if (Object.keys(updates).length > 0) {
payload.updates = updates;
}
}
if (!payload.newApiKey && !payload.updates) {
onCancelEdit();
setSaving(false);
return;
}
await onSaveEdits(payload);
setNewKey('');
toast.success('API key updated');
toast.success('Provider updated');
} catch (error) {
toast.error(`Failed to save key: ${error}`);
toast.error(`Failed to save provider: ${error}`);
} finally {
setSaving(false);
setValidating(false);
}
};
@@ -258,11 +317,37 @@ function ProviderCard({
{/* Key row */}
{isEditing ? (
<div className="space-y-2">
{canEditConfig && (
<>
{typeInfo?.showBaseUrl && (
<div className="space-y-1">
<Label className="text-xs">Base URL</Label>
<Input
value={baseUrl}
onChange={(e) => setBaseUrl(e.target.value)}
placeholder="https://api.example.com/v1"
className="h-9 text-sm"
/>
</div>
)}
{typeInfo?.showModelId && (
<div className="space-y-1">
<Label className="text-xs">Model ID</Label>
<Input
value={modelId}
onChange={(e) => setModelId(e.target.value)}
placeholder={typeInfo.modelIdPlaceholder || 'provider/model-id'}
className="h-9 text-sm"
/>
</div>
)}
</>
)}
<div className="flex gap-2">
<div className="relative flex-1">
<Input
type={showKey ? 'text' : 'password'}
placeholder={typeInfo?.placeholder}
placeholder={typeInfo?.requiresApiKey ? typeInfo?.placeholder : 'Optional: update API key'}
value={newKey}
onChange={(e) => setNewKey(e.target.value)}
className="pr-10 h-9 text-sm"
@@ -278,8 +363,17 @@ function ProviderCard({
<Button
variant="outline"
size="sm"
onClick={handleSaveKey}
disabled={!newKey || validating || saving}
onClick={handleSaveEdits}
disabled={
validating
|| saving
|| (
!newKey.trim()
&& (baseUrl.trim() || undefined) === (provider.baseUrl || undefined)
&& (modelId.trim() || undefined) === (provider.model || undefined)
)
|| Boolean(typeInfo?.showModelId && !modelId.trim())
}
>
{validating || saving ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
@@ -324,25 +418,75 @@ function ProviderCard({
}
interface AddProviderDialogProps {
existingTypes: Set<string>;
onClose: () => void;
onAdd: (type: string, name: string, apiKey: string) => Promise<void>;
onAdd: (
type: ProviderType,
name: string,
apiKey: string,
options?: { baseUrl?: string; model?: string }
) => Promise<void>;
onValidateKey: (type: string, apiKey: string) => Promise<{ valid: boolean; error?: string }>;
}
function AddProviderDialog({ onClose, onAdd }: AddProviderDialogProps) {
const [selectedType, setSelectedType] = useState<string | null>(null);
function AddProviderDialog({ existingTypes, onClose, onAdd, onValidateKey }: AddProviderDialogProps) {
const [selectedType, setSelectedType] = useState<ProviderType | null>(null);
const [name, setName] = useState('');
const [apiKey, setApiKey] = useState('');
const [baseUrl, setBaseUrl] = useState('');
const [modelId, setModelId] = useState('');
const [showKey, setShowKey] = useState(false);
const [saving, setSaving] = useState(false);
const [validationError, setValidationError] = useState<string | null>(null);
const typeInfo = providerTypes.find((t) => t.id === selectedType);
const typeInfo = PROVIDER_TYPE_INFO.find((t) => t.id === selectedType);
// Only custom can be added multiple times.
const availableTypes = PROVIDER_TYPE_INFO.filter(
(t) => t.id === 'custom' || !existingTypes.has(t.id),
);
const handleAdd = async () => {
if (!selectedType) return;
setSaving(true);
setValidationError(null);
try {
await onAdd(selectedType, name || typeInfo?.name || selectedType, apiKey);
// Validate key first if the provider requires one and a key was entered
const requiresKey = typeInfo?.requiresApiKey ?? false;
if (requiresKey && !apiKey.trim()) {
setValidationError('API key is required');
setSaving(false);
return;
}
if (requiresKey && apiKey) {
const result = await onValidateKey(selectedType, apiKey);
if (!result.valid) {
setValidationError(result.error || 'Invalid API key');
setSaving(false);
return;
}
}
const requiresModel = typeInfo?.showModelId ?? false;
if (requiresModel && !modelId.trim()) {
setValidationError('Model ID is required');
setSaving(false);
return;
}
await onAdd(
selectedType,
name || typeInfo?.name || selectedType,
apiKey.trim(),
{
baseUrl: baseUrl.trim() || undefined,
model: (typeInfo?.defaultModelId || modelId.trim()) || undefined,
}
);
} catch {
// error already handled via toast in parent
} finally {
setSaving(false);
}
@@ -360,12 +504,14 @@ function AddProviderDialog({ onClose, onAdd }: AddProviderDialogProps) {
<CardContent className="space-y-4">
{!selectedType ? (
<div className="grid grid-cols-2 gap-3">
{providerTypes.map((type) => (
{availableTypes.map((type) => (
<button
key={type.id}
onClick={() => {
setSelectedType(type.id);
setName(type.name);
setBaseUrl(type.defaultBaseUrl || '');
setModelId(type.defaultModelId || '');
}}
className="p-4 rounded-lg border hover:bg-accent transition-colors text-center"
>
@@ -381,7 +527,12 @@ function AddProviderDialog({ onClose, onAdd }: AddProviderDialogProps) {
<div>
<p className="font-medium">{typeInfo?.name}</p>
<button
onClick={() => setSelectedType(null)}
onClick={() => {
setSelectedType(null);
setValidationError(null);
setBaseUrl('');
setModelId('');
}}
className="text-sm text-muted-foreground hover:text-foreground"
>
Change provider
@@ -407,7 +558,10 @@ function AddProviderDialog({ onClose, onAdd }: AddProviderDialogProps) {
type={showKey ? 'text' : 'password'}
placeholder={typeInfo?.placeholder}
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
onChange={(e) => {
setApiKey(e.target.value);
setValidationError(null);
}}
className="pr-10"
/>
<button
@@ -418,10 +572,40 @@ function AddProviderDialog({ onClose, onAdd }: AddProviderDialogProps) {
{showKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
{validationError && (
<p className="text-xs text-destructive">{validationError}</p>
)}
<p className="text-xs text-muted-foreground">
Your API key will be securely encrypted and stored locally.
Your API key is stored locally on your machine.
</p>
</div>
{typeInfo?.showBaseUrl && (
<div className="space-y-2">
<Label htmlFor="baseUrl">Base URL</Label>
<Input
id="baseUrl"
placeholder="https://api.example.com/v1"
value={baseUrl}
onChange={(e) => setBaseUrl(e.target.value)}
/>
</div>
)}
{typeInfo?.showModelId && (
<div className="space-y-2">
<Label htmlFor="modelId">Model ID</Label>
<Input
id="modelId"
placeholder={typeInfo.modelIdPlaceholder || 'provider/model-id'}
value={modelId}
onChange={(e) => {
setModelId(e.target.value);
setValidationError(null);
}}
/>
</div>
)}
</div>
)}
@@ -433,7 +617,7 @@ function AddProviderDialog({ onClose, onAdd }: AddProviderDialogProps) {
</Button>
<Button
onClick={handleAdd}
disabled={!selectedType || saving}
disabled={!selectedType || saving || ((typeInfo?.showModelId ?? false) && modelId.trim().length === 0)}
>
{saving ? (
<Loader2 className="h-4 w-4 animate-spin mr-2" />