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.
This commit is contained in:
Replit Agent 2026-06-16 17:46:59 +00:00
parent 50d2adb674
commit 2695883f0d
5 changed files with 89 additions and 5 deletions

View file

@ -32,6 +32,8 @@ export default {
fields: { fields: {
name: "Name", name: "Name",
apiType: "API-Typ", apiType: "API-Typ",
endpointPreset: "Anbieter-Voreinstellung",
endpointPresetPlaceholder: "Anbieter wählen …",
baseUrl: "API-Endpunkt (Base URL)", baseUrl: "API-Endpunkt (Base URL)",
baseUrlPlaceholder: "z.B. https://api.openai.com/v1", baseUrlPlaceholder: "z.B. https://api.openai.com/v1",
baseUrlHintOpenai: "OpenAI-kompatibel: https://api.openai.com/v1", baseUrlHintOpenai: "OpenAI-kompatibel: https://api.openai.com/v1",

View file

@ -32,6 +32,8 @@ export default {
fields: { fields: {
name: "Name", name: "Name",
apiType: "API type", apiType: "API type",
endpointPreset: "Provider preset",
endpointPresetPlaceholder: "Choose provider …",
baseUrl: "API endpoint (base URL)", baseUrl: "API endpoint (base URL)",
baseUrlPlaceholder: "e.g. https://api.openai.com/v1", baseUrlPlaceholder: "e.g. https://api.openai.com/v1",
baseUrlHintOpenai: "OpenAI-compatible: https://api.openai.com/v1", baseUrlHintOpenai: "OpenAI-compatible: https://api.openai.com/v1",

View file

@ -32,6 +32,8 @@ export default {
fields: { fields: {
name: "Nombre", name: "Nombre",
apiType: "Tipo de API", apiType: "Tipo de API",
endpointPreset: "Configuración de proveedor",
endpointPresetPlaceholder: "Elegir proveedor …",
baseUrl: "Endpoint de API (URL base)", baseUrl: "Endpoint de API (URL base)",
baseUrlPlaceholder: "p. ej. https://api.openai.com/v1", baseUrlPlaceholder: "p. ej. https://api.openai.com/v1",
baseUrlHintOpenai: "Compatible con OpenAI: https://api.openai.com/v1", baseUrlHintOpenai: "Compatible con OpenAI: https://api.openai.com/v1",

View file

@ -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 { Loader2, Plus, Trash2, CheckCircle2, XCircle, BrainCircuit, ShieldAlert, KeyRound, Server, Activity } from "lucide-react";
import { AxisBadge, SeverityBadge } from "@/components/ui-helpers"; import { AxisBadge, SeverityBadge } from "@/components/ui-helpers";
type ProviderPreset = { label: string; baseUrl: string; chatCompletion: string };
const PROVIDER_PRESETS: Partial<Record<AiProviderApiType, ProviderPreset[]>> = {
[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 }: { function ModelField({ models, loading, tried, value, onChange }: {
models: string[]; models: string[];
loading: boolean; loading: boolean;
@ -98,6 +117,11 @@ function ProviderTab() {
const [editModelsTried, setEditModelsTried] = useState(false); const [editModelsTried, setEditModelsTried] = useState(false);
const [testingId, setTestingId] = useState<number | null>(null); const [testingId, setTestingId] = useState<number | null>(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 = () => { const resetAddDiscovery = () => {
setAddTestResult(null); setAddTestResult(null);
@ -124,6 +148,7 @@ function ProviderTab() {
toast({ title: t("admin.providers.toasts.added") }); toast({ title: t("admin.providers.toasts.added") });
setIsAddOpen(false); setIsAddOpen(false);
setAddForm({ name: "", apiType: AiProviderApiType.openai as AiProviderApiType, baseUrl: "", model: "", apiToken: "", enabled: true }); setAddForm({ name: "", apiType: AiProviderApiType.openai as AiProviderApiType, baseUrl: "", model: "", apiToken: "", enabled: true });
setAddPreset("");
resetAddDiscovery(); resetAddDiscovery();
invalidate(); invalidate();
}, },
@ -262,6 +287,7 @@ function ProviderTab() {
apiToken: "", apiToken: "",
enabled: provider.enabled enabled: provider.enabled
}); });
setEditPreset("");
resetEditDiscovery(); resetEditDiscovery();
setEditingId(provider.id); setEditingId(provider.id);
}; };
@ -294,19 +320,45 @@ function ProviderTab() {
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label>{t("admin.providers.fields.apiType")}</Label> <Label>{t("admin.providers.fields.apiType")}</Label>
<Select value={addForm.apiType} onValueChange={(v: AiProviderApiType) => setAddForm({...addForm, apiType: v})}> <Select value={addForm.apiType} onValueChange={(v: AiProviderApiType) => {
setAddForm({ ...addForm, apiType: v, baseUrl: "" });
setAddPreset("");
resetAddDiscovery();
}}>
<SelectTrigger><SelectValue /></SelectTrigger> <SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="openai">OpenAI</SelectItem> <SelectItem value="openai">OpenAI-kompatibel</SelectItem>
<SelectItem value="anthropic">Anthropic</SelectItem> <SelectItem value="anthropic">Anthropic</SelectItem>
<SelectItem value="custom">Custom</SelectItem> <SelectItem value="custom">Custom</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{(PROVIDER_PRESETS[addForm.apiType]?.length ?? 0) > 0 && (
<div className="grid gap-2">
<Label>{t("admin.providers.fields.endpointPreset")}</Label>
<Select value={addPreset} onValueChange={(label) => {
const preset = PROVIDER_PRESETS[addForm.apiType]?.find(p => p.label === label);
setAddPreset(label);
if (preset) { setAddForm(f => ({ ...f, baseUrl: preset.baseUrl })); resetAddDiscovery(); }
}}>
<SelectTrigger><SelectValue placeholder={t("admin.providers.fields.endpointPresetPlaceholder")} /></SelectTrigger>
<SelectContent>
{PROVIDER_PRESETS[addForm.apiType]!.map(p => (
<SelectItem key={p.label} value={p.label}>{p.label}</SelectItem>
))}
</SelectContent>
</Select>
{addSelectedPreset && (
<div className="rounded-md bg-muted/60 px-3 py-2 text-xs font-mono space-y-1">
<div><span className="text-muted-foreground">Base URL: </span>{addSelectedPreset.baseUrl}</div>
<div><span className="text-muted-foreground">Chat Completions: </span>{addSelectedPreset.chatCompletion}</div>
</div>
)}
</div>
)}
<div className="grid gap-2"> <div className="grid gap-2">
<Label>{t("admin.providers.fields.baseUrl")}</Label> <Label>{t("admin.providers.fields.baseUrl")}</Label>
<Input value={addForm.baseUrl} onChange={e => { setAddForm({...addForm, baseUrl: e.target.value}); resetAddDiscovery(); }} required placeholder={t("admin.providers.fields.baseUrlPlaceholder")} /> <Input value={addForm.baseUrl} onChange={e => { setAddForm({...addForm, baseUrl: e.target.value}); resetAddDiscovery(); }} required placeholder={t("admin.providers.fields.baseUrlPlaceholder")} />
<p className="text-xs text-muted-foreground">{t("admin.providers.fields.baseUrlHintOpenai")} <br/> {t("admin.providers.fields.baseUrlHintAnthropic")}</p>
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label>{t("admin.providers.fields.apiToken")}</Label> <Label>{t("admin.providers.fields.apiToken")}</Label>
@ -399,15 +451,41 @@ function ProviderTab() {
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label>{t("admin.providers.fields.apiType")}</Label> <Label>{t("admin.providers.fields.apiType")}</Label>
<Select value={editForm.apiType} onValueChange={(v: AiProviderApiType) => setEditForm({...editForm, apiType: v})}> <Select value={editForm.apiType} onValueChange={(v: AiProviderApiType) => {
setEditForm({ ...editForm, apiType: v });
setEditPreset("");
}}>
<SelectTrigger><SelectValue /></SelectTrigger> <SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="openai">OpenAI</SelectItem> <SelectItem value="openai">OpenAI-kompatibel</SelectItem>
<SelectItem value="anthropic">Anthropic</SelectItem> <SelectItem value="anthropic">Anthropic</SelectItem>
<SelectItem value="custom">Custom</SelectItem> <SelectItem value="custom">Custom</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{(PROVIDER_PRESETS[editForm.apiType]?.length ?? 0) > 0 && (
<div className="grid gap-2">
<Label>{t("admin.providers.fields.endpointPreset")}</Label>
<Select value={editPreset} onValueChange={(label) => {
const preset = PROVIDER_PRESETS[editForm.apiType]?.find(p => p.label === label);
setEditPreset(label);
if (preset) { setEditForm(f => ({ ...f, baseUrl: preset.baseUrl })); resetEditDiscovery(); }
}}>
<SelectTrigger><SelectValue placeholder={t("admin.providers.fields.endpointPresetPlaceholder")} /></SelectTrigger>
<SelectContent>
{PROVIDER_PRESETS[editForm.apiType]!.map(p => (
<SelectItem key={p.label} value={p.label}>{p.label}</SelectItem>
))}
</SelectContent>
</Select>
{editSelectedPreset && (
<div className="rounded-md bg-muted/60 px-3 py-2 text-xs font-mono space-y-1">
<div><span className="text-muted-foreground">Base URL: </span>{editSelectedPreset.baseUrl}</div>
<div><span className="text-muted-foreground">Chat Completions: </span>{editSelectedPreset.chatCompletion}</div>
</div>
)}
</div>
)}
<div className="grid gap-2"> <div className="grid gap-2">
<Label>{t("admin.providers.fields.baseUrl")}</Label> <Label>{t("admin.providers.fields.baseUrl")}</Label>
<Input value={editForm.baseUrl} onChange={e => { setEditForm({...editForm, baseUrl: e.target.value}); resetEditDiscovery(); }} required /> <Input value={editForm.baseUrl} onChange={e => { setEditForm({...editForm, baseUrl: e.target.value}); resetEditDiscovery(); }} required />

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB