From 2695883f0d5477e6b16a9e6b2ec37475714d09c6 Mon Sep 17 00:00:00 2001 From: Replit Agent Date: Tue, 16 Jun 2026 17:46:59 +0000 Subject: [PATCH] Add provider endpoint preset dropdown to admin AI provider forms User request: implement selection of default endpoints for known AI providers in the "New AI provider" dropdown, including chat completion endpoints. Changes (artifacts/skillguard/src/pages/admin.tsx): - Added PROVIDER_PRESETS constant with 9 OpenAI-compatible presets (OpenAI, Groq, OpenRouter, Mistral AI, DeepSeek, Together AI, Perplexity AI, Ollama lokal, LM Studio lokal) and 1 Anthropic preset. - Added addPreset / editPreset state + addSelectedPreset / editSelectedPreset derived values to ProviderTab. - Both Add and Edit dialogs now show an "Anbieter-Voreinstellung" dropdown (hidden for apiType=custom) between the API-type and Base-URL fields. Selecting a preset auto-fills the Base URL and resets discovery state. - On selection, a mono info panel shows both the Base URL and the full Chat Completions endpoint for the chosen preset. - apiType change clears the preset and (in Add form) also clears the baseUrl so no stale URL from a different provider type persists. - Removed the old static hint text (baseUrlHintOpenai / baseUrlHintAnthropic) since the dropdown replaces it. i18n (locales/de/en/es/admin.ts): - Added endpointPreset and endpointPresetPlaceholder keys to all three language files. Also fixed in this session: ran orval codegen after the i18n task merge left the generated types stale (language/lang fields missing from SkillScanInput, ScanDetail, useListRules); typecheck now fully green. --- .../skillguard/src/i18n/locales/de/admin.ts | 2 + .../skillguard/src/i18n/locales/en/admin.ts | 2 + .../skillguard/src/i18n/locales/es/admin.ts | 2 + artifacts/skillguard/src/pages/admin.tsx | 88 +++++++++++++++++- attached_assets/image_1781631809853.png | Bin 0 -> 10018 bytes 5 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 attached_assets/image_1781631809853.png diff --git a/artifacts/skillguard/src/i18n/locales/de/admin.ts b/artifacts/skillguard/src/i18n/locales/de/admin.ts index 265dbdd..3b4509c 100644 --- a/artifacts/skillguard/src/i18n/locales/de/admin.ts +++ b/artifacts/skillguard/src/i18n/locales/de/admin.ts @@ -32,6 +32,8 @@ export default { fields: { name: "Name", apiType: "API-Typ", + endpointPreset: "Anbieter-Voreinstellung", + endpointPresetPlaceholder: "Anbieter wählen …", baseUrl: "API-Endpunkt (Base URL)", baseUrlPlaceholder: "z.B. https://api.openai.com/v1", baseUrlHintOpenai: "OpenAI-kompatibel: https://api.openai.com/v1", diff --git a/artifacts/skillguard/src/i18n/locales/en/admin.ts b/artifacts/skillguard/src/i18n/locales/en/admin.ts index 3652111..ba2edb3 100644 --- a/artifacts/skillguard/src/i18n/locales/en/admin.ts +++ b/artifacts/skillguard/src/i18n/locales/en/admin.ts @@ -32,6 +32,8 @@ export default { fields: { name: "Name", apiType: "API type", + endpointPreset: "Provider preset", + endpointPresetPlaceholder: "Choose provider …", baseUrl: "API endpoint (base URL)", baseUrlPlaceholder: "e.g. https://api.openai.com/v1", baseUrlHintOpenai: "OpenAI-compatible: https://api.openai.com/v1", diff --git a/artifacts/skillguard/src/i18n/locales/es/admin.ts b/artifacts/skillguard/src/i18n/locales/es/admin.ts index ffb3e00..3dbc7c0 100644 --- a/artifacts/skillguard/src/i18n/locales/es/admin.ts +++ b/artifacts/skillguard/src/i18n/locales/es/admin.ts @@ -32,6 +32,8 @@ export default { fields: { name: "Nombre", apiType: "Tipo de API", + endpointPreset: "Configuración de proveedor", + endpointPresetPlaceholder: "Elegir proveedor …", baseUrl: "Endpoint de API (URL base)", baseUrlPlaceholder: "p. ej. https://api.openai.com/v1", baseUrlHintOpenai: "Compatible con OpenAI: https://api.openai.com/v1", diff --git a/artifacts/skillguard/src/pages/admin.tsx b/artifacts/skillguard/src/pages/admin.tsx index bcffebd..a955611 100644 --- a/artifacts/skillguard/src/pages/admin.tsx +++ b/artifacts/skillguard/src/pages/admin.tsx @@ -23,6 +23,25 @@ import { useToast } from "@/hooks/use-toast"; import { Loader2, Plus, Trash2, CheckCircle2, XCircle, BrainCircuit, ShieldAlert, KeyRound, Server, Activity } from "lucide-react"; import { AxisBadge, SeverityBadge } from "@/components/ui-helpers"; +type ProviderPreset = { label: string; baseUrl: string; chatCompletion: string }; + +const PROVIDER_PRESETS: Partial> = { + [AiProviderApiType.openai]: [ + { label: "OpenAI", baseUrl: "https://api.openai.com/v1", chatCompletion: "https://api.openai.com/v1/chat/completions" }, + { label: "Groq", baseUrl: "https://api.groq.com/openai/v1", chatCompletion: "https://api.groq.com/openai/v1/chat/completions" }, + { label: "OpenRouter", baseUrl: "https://openrouter.ai/api/v1", chatCompletion: "https://openrouter.ai/api/v1/chat/completions" }, + { label: "Mistral AI", baseUrl: "https://api.mistral.ai/v1", chatCompletion: "https://api.mistral.ai/v1/chat/completions" }, + { label: "DeepSeek", baseUrl: "https://api.deepseek.com/v1", chatCompletion: "https://api.deepseek.com/v1/chat/completions" }, + { label: "Together AI", baseUrl: "https://api.together.xyz/v1", chatCompletion: "https://api.together.xyz/v1/chat/completions" }, + { label: "Perplexity AI", baseUrl: "https://api.perplexity.ai", chatCompletion: "https://api.perplexity.ai/chat/completions" }, + { label: "Ollama (lokal)", baseUrl: "http://localhost:11434/v1", chatCompletion: "http://localhost:11434/v1/chat/completions" }, + { label: "LM Studio (lokal)", baseUrl: "http://localhost:1234/v1", chatCompletion: "http://localhost:1234/v1/chat/completions" }, + ], + [AiProviderApiType.anthropic]: [ + { label: "Anthropic", baseUrl: "https://api.anthropic.com", chatCompletion: "https://api.anthropic.com/v1/messages" }, + ], +}; + function ModelField({ models, loading, tried, value, onChange }: { models: string[]; loading: boolean; @@ -98,6 +117,11 @@ function ProviderTab() { const [editModelsTried, setEditModelsTried] = useState(false); const [testingId, setTestingId] = useState(null); + const [addPreset, setAddPreset] = useState(""); + const [editPreset, setEditPreset] = useState(""); + + const addSelectedPreset = PROVIDER_PRESETS[addForm.apiType]?.find(p => p.label === addPreset) ?? null; + const editSelectedPreset = PROVIDER_PRESETS[editForm.apiType]?.find(p => p.label === editPreset) ?? null; const resetAddDiscovery = () => { setAddTestResult(null); @@ -124,6 +148,7 @@ function ProviderTab() { toast({ title: t("admin.providers.toasts.added") }); setIsAddOpen(false); setAddForm({ name: "", apiType: AiProviderApiType.openai as AiProviderApiType, baseUrl: "", model: "", apiToken: "", enabled: true }); + setAddPreset(""); resetAddDiscovery(); invalidate(); }, @@ -262,6 +287,7 @@ function ProviderTab() { apiToken: "", enabled: provider.enabled }); + setEditPreset(""); resetEditDiscovery(); setEditingId(provider.id); }; @@ -294,19 +320,45 @@ function ProviderTab() {
- { + setAddForm({ ...addForm, apiType: v, baseUrl: "" }); + setAddPreset(""); + resetAddDiscovery(); + }}> - OpenAI + OpenAI-kompatibel Anthropic Custom
+ {(PROVIDER_PRESETS[addForm.apiType]?.length ?? 0) > 0 && ( +
+ + + {addSelectedPreset && ( +
+
Base URL: {addSelectedPreset.baseUrl}
+
Chat Completions: {addSelectedPreset.chatCompletion}
+
+ )} +
+ )}
{ setAddForm({...addForm, baseUrl: e.target.value}); resetAddDiscovery(); }} required placeholder={t("admin.providers.fields.baseUrlPlaceholder")} /> -

{t("admin.providers.fields.baseUrlHintOpenai")}
{t("admin.providers.fields.baseUrlHintAnthropic")}

@@ -399,15 +451,41 @@ function ProviderTab() {
- { + setEditForm({ ...editForm, apiType: v }); + setEditPreset(""); + }}> - OpenAI + OpenAI-kompatibel Anthropic Custom
+ {(PROVIDER_PRESETS[editForm.apiType]?.length ?? 0) > 0 && ( +
+ + + {editSelectedPreset && ( +
+
Base URL: {editSelectedPreset.baseUrl}
+
Chat Completions: {editSelectedPreset.chatCompletion}
+
+ )} +
+ )}
{ setEditForm({...editForm, baseUrl: e.target.value}); resetEditDiscovery(); }} required /> diff --git a/attached_assets/image_1781631809853.png b/attached_assets/image_1781631809853.png new file mode 100644 index 0000000000000000000000000000000000000000..3fb2ca9980a97b6b92a0cefc8a546645b41b89cb GIT binary patch literal 10018 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