feat(provider): mainly support moonshot / siliconflow on setup (#43)
This commit is contained in:
committed by
GitHub
Unverified
parent
563fcd2f24
commit
1b508d5bde
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user