@@ -29,11 +35,27 @@ export function ClientRoutesPage() {
+
+
+
+ {t('modelMappings.title')}
+
+
+
+
+ {formData.modelMappings && formData.modelMappings.length > 0 ? (
+
+ {formData.modelMappings.map((mapping, index) => (
+
+
+
+ {
+ const newMappings = [...(formData.modelMappings || [])];
+ newMappings[index] = { ...newMappings[index], pattern: e.target.value };
+ updateFormData({ modelMappings: newMappings });
+ }}
+ placeholder="*claude*, *sonnet*, *"
+ className="font-mono text-sm"
+ />
+
+
+
+
+ {
+ const newMappings = [...(formData.modelMappings || [])];
+ newMappings[index] = { ...newMappings[index], target: value };
+ updateFormData({ modelMappings: newMappings });
+ }}
+ placeholder={t('modelInput.selectOrEnter')}
+ />
+
+
+
+ ))}
+
+ ) : (
+
+ {t('modelMappings.noMappings')}
+
+ )}
+
+
{saveStatus === 'error' && (
diff --git a/web/src/pages/providers/components/select-type-step.tsx b/web/src/pages/providers/components/select-type-step.tsx
index e07129a1..c8575399 100644
--- a/web/src/pages/providers/components/select-type-step.tsx
+++ b/web/src/pages/providers/components/select-type-step.tsx
@@ -41,6 +41,8 @@ export function SelectTypeStep() {
selectedTemplate: templateId,
name: template.name,
clients: updatedClients,
+ modelMappings: template.modelMappings,
+ logo: template.logoUrl,
});
goToCustomConfig();
diff --git a/web/src/pages/providers/index.tsx b/web/src/pages/providers/index.tsx
index b9328648..46aa5a82 100644
--- a/web/src/pages/providers/index.tsx
+++ b/web/src/pages/providers/index.tsx
@@ -1,5 +1,5 @@
-import { useMemo, useRef } from 'react';
-import { Plus, Layers, Download, Upload } from 'lucide-react';
+import { useMemo, useRef, useState } from 'react';
+import { Plus, Layers, Download, Upload, Search } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { useProviders, useAllProviderStats } from '@/hooks/queries';
@@ -9,9 +9,9 @@ import { getTransport } from '@/lib/transport';
import { ProviderRow } from './components/provider-row';
import { useQueryClient } from '@tanstack/react-query';
import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
import { PageHeader } from '@/components/layout/page-header';
import { PROVIDER_TYPE_CONFIGS, type ProviderTypeKey } from './types';
-import { useState } from 'react';
import { AntigravityQuotasProvider } from '@/contexts/antigravity-quotas-context';
export function ProvidersPage() {
@@ -21,6 +21,7 @@ export function ProvidersPage() {
const { data: providerStats = {} } = useAllProviderStats();
const { countsByProvider } = useStreamingRequests();
const [importStatus, setImportStatus] = useState
(null);
+ const [searchQuery, setSearchQuery] = useState('');
const fileInputRef = useRef(null);
const queryClient = useQueryClient();
@@ -32,7 +33,19 @@ export function ProvidersPage() {
custom: [],
};
- providers?.forEach((p) => {
+ // Filter providers by search query
+ const filteredProviders = providers?.filter((p) => {
+ if (!searchQuery.trim()) return true;
+ const query = searchQuery.toLowerCase();
+ const config = PROVIDER_TYPE_CONFIGS[p.type as ProviderTypeKey];
+ const displayInfo = config?.getDisplayInfo(p) || '';
+ return (
+ p.name.toLowerCase().includes(query) ||
+ displayInfo.toLowerCase().includes(query)
+ );
+ });
+
+ filteredProviders?.forEach((p) => {
const type = p.type as ProviderTypeKey;
if (groups[type]) {
groups[type].push(p);
@@ -43,7 +56,7 @@ export function ProvidersPage() {
});
return groups;
- }, [providers]);
+ }, [providers, searchQuery]);
// Export providers as JSON file
const handleExport = async () => {
@@ -107,6 +120,18 @@ export function ProvidersPage() {
count: providers?.length || 0,
})}
>
+
+
+ setSearchQuery(e.target.value)}
+ className="pl-9 w-48"
+ />
+
>;
+ modelMappings?: TemplateModelMapping[]; // 可选的模型映射
};
export const quickTemplates: QuickTemplate[] = [
@@ -95,6 +105,7 @@ export const quickTemplates: QuickTemplate[] = [
nameKey: 'addProvider.templates.88code.name',
descriptionKey: 'addProvider.templates.88code.description',
icon: 'grid',
+ logoUrl: logo88code,
supportedClients: ['claude', 'codex', 'gemini'],
clientBaseURLs: {
claude: 'https://www.88code.ai/api',
@@ -109,6 +120,7 @@ export const quickTemplates: QuickTemplate[] = [
nameKey: 'addProvider.templates.aicodemirror.name',
descriptionKey: 'addProvider.templates.aicodemirror.description',
icon: 'layers',
+ logoUrl: aicodemirrorLogo,
supportedClients: ['claude', 'codex', 'gemini'],
clientBaseURLs: {
claude: 'https://api.aicodemirror.com/api/claudecode',
@@ -144,6 +156,20 @@ export const quickTemplates: QuickTemplate[] = [
claude: 'https://free.duckcoding.com',
},
},
+ {
+ id: 'nvidia',
+ name: 'NVIDIA',
+ description: 'NVIDIA NIM · OpenAI 兼容',
+ nameKey: 'addProvider.templates.nvidia.name',
+ descriptionKey: 'addProvider.templates.nvidia.description',
+ icon: 'layers',
+ logoUrl: nvidiaLogo,
+ supportedClients: ['openai'],
+ clientBaseURLs: {
+ openai: 'https://integrate.api.nvidia.com',
+ },
+ modelMappings: [{ pattern: '*', target: 'minimaxai/minimax-m2.1' }],
+ },
];
// Client config
@@ -169,6 +195,8 @@ export type ProviderFormData = {
baseURL: string;
apiKey: string;
clients: ClientConfig[];
+ modelMappings?: TemplateModelMapping[]; // 模型映射
+ logo?: string; // Logo URL
};
// Create step type