Guided AI provider setup with model discovery
Task: Replace free-text model entry in Admin → Providers with a guided
flow (Name → API type → API endpoint → API token → Test connection) that
auto-discovers available models after a successful connection test and
presents them in a Select positioned right after the API endpoint field.
Model-independent connection test (key fix):
- The setup connection test no longer requires a model, removing the
chicken-and-egg where discovery could never run. test-connection's model
is now optional: when a model is supplied it does a full chat round-trip;
when omitted it verifies credentials via the provider's models endpoint and
reports how many models are available. The form sends no model on the
initial test, so a successful test now reliably triggers discovery.
Backend:
- aiAnalysis.ts: added listProviderModels(provider) — GETs {baseUrl}/models
using Bearer auth for openai/custom and x-api-key + anthropic-version for
anthropic. Normalizes data[].id (falls back to models[].id/.name),
dedupes + sorts, and redacts secrets in error messages via the existing
redactSecrets helper.
- providers.ts: added POST /providers/list-models accepting ad-hoc config
(apiType, baseUrl, optional apiToken, optional providerId). Falls back to
the stored token by providerId when token omitted; returns { ok, models,
message } and never leaks the token.
API contract:
- openapi.yaml: added /providers/list-models path, ProviderListModelsInput
and ProviderModelsResult schemas. Regenerated zod + react-query client via
the api-spec codegen workflow (orval).
Admin UI (admin.tsx):
- New ModelField component renders a loading state, a Select when models are
discovered, or a manual free-text input fallback (with hint) when discovery
returns nothing — so saving always works for custom endpoints.
- Field order follows the guided flow: Name → API type → API endpoint →
API token → Test connection, with the model selector appearing after the
token once discovery succeeds. A successful test automatically triggers
discovery; editing endpoint or token resets discovery state.
Verified: workspace typecheck passes, api-server tests 59/59 pass, live curl
of the new endpoint returns graceful errors without leaking the token.
Replit-Task-Id: 8d300a47-0b45-4677-9e9e-aa041bf03e98
This commit is contained in:
parent
b0af3c5c24
commit
f44c3ed247
12 changed files with 494 additions and 36 deletions
|
|
@ -175,6 +175,48 @@ export async function callProvider(
|
||||||
return callOpenAiCompatible(provider, system, user);
|
return callOpenAiCompatible(provider, system, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listProviderModels(
|
||||||
|
provider: AiProvider,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const base = provider.baseUrl.replace(/\/$/, "");
|
||||||
|
const url = `${base}/models`;
|
||||||
|
const headers: Record<string, string> =
|
||||||
|
provider.apiType === "anthropic"
|
||||||
|
? {
|
||||||
|
"x-api-key": provider.apiToken ?? "",
|
||||||
|
"anthropic-version": "2023-06-01",
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
Authorization: `Bearer ${provider.apiToken ?? ""}`,
|
||||||
|
};
|
||||||
|
const res = await fetchWithTimeout(url, { method: "GET", headers });
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text();
|
||||||
|
throw new Error(
|
||||||
|
`HTTP ${res.status}: ${redactSecrets(body.slice(0, 300), provider.apiToken)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const data = (await res.json()) as {
|
||||||
|
data?: { id?: unknown }[];
|
||||||
|
models?: { id?: unknown; name?: unknown }[];
|
||||||
|
};
|
||||||
|
const rows = Array.isArray(data.data)
|
||||||
|
? data.data
|
||||||
|
: Array.isArray(data.models)
|
||||||
|
? data.models
|
||||||
|
: [];
|
||||||
|
const ids = rows
|
||||||
|
.map((m) =>
|
||||||
|
typeof m.id === "string"
|
||||||
|
? m.id
|
||||||
|
: typeof (m as { name?: unknown }).name === "string"
|
||||||
|
? ((m as { name: string }).name)
|
||||||
|
: null,
|
||||||
|
)
|
||||||
|
.filter((id): id is string => !!id);
|
||||||
|
return Array.from(new Set(ids)).sort((a, b) => a.localeCompare(b));
|
||||||
|
}
|
||||||
|
|
||||||
function buildRuleMenu(aiRules: AiRuleConfig[]): string {
|
function buildRuleMenu(aiRules: AiRuleConfig[]): string {
|
||||||
const lines = aiRules.map(
|
const lines = aiRules.map(
|
||||||
(r) => `- ${r.ruleId} (${r.axis}): ${r.title} — ${r.description}`,
|
(r) => `- ${r.ruleId} (${r.axis}): ${r.title} — ${r.description}`,
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,10 @@ import {
|
||||||
TestProviderParams,
|
TestProviderParams,
|
||||||
TestProviderResponse,
|
TestProviderResponse,
|
||||||
TestProviderConnectionBody,
|
TestProviderConnectionBody,
|
||||||
|
ListProviderModelsBody,
|
||||||
|
ListProviderModelsResponse,
|
||||||
} from "@workspace/api-zod";
|
} from "@workspace/api-zod";
|
||||||
import { callProvider } from "../lib/aiAnalysis";
|
import { callProvider, listProviderModels } from "../lib/aiAnalysis";
|
||||||
|
|
||||||
const router: IRouter = Router();
|
const router: IRouter = Router();
|
||||||
|
|
||||||
|
|
@ -168,27 +170,41 @@ router.post("/providers/test-connection", async (req, res) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasModel = typeof d.model === "string" && d.model.trim() !== "";
|
||||||
|
|
||||||
const provider: AiProvider = {
|
const provider: AiProvider = {
|
||||||
id: d.providerId ?? 0,
|
id: d.providerId ?? 0,
|
||||||
name: "",
|
name: "",
|
||||||
apiType: d.apiType,
|
apiType: d.apiType,
|
||||||
baseUrl: d.baseUrl,
|
baseUrl: d.baseUrl,
|
||||||
model: d.model,
|
model: hasModel ? (d.model as string) : "",
|
||||||
apiToken: token,
|
apiToken: token,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const reply = await callProvider(
|
if (hasModel) {
|
||||||
provider,
|
const reply = await callProvider(
|
||||||
"Du bist ein Verbindungstest.",
|
provider,
|
||||||
'Antworte mit dem einzelnen Wort "OK".',
|
"Du bist ein Verbindungstest.",
|
||||||
);
|
'Antworte mit dem einzelnen Wort "OK".',
|
||||||
|
);
|
||||||
|
return res.json(
|
||||||
|
TestProviderResponse.parse({
|
||||||
|
ok: true,
|
||||||
|
message: `Verbindung erfolgreich. Antwort: ${reply.trim().slice(0, 80) || "(leer)"}`,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const models = await listProviderModels(provider);
|
||||||
return res.json(
|
return res.json(
|
||||||
TestProviderResponse.parse({
|
TestProviderResponse.parse({
|
||||||
ok: true,
|
ok: true,
|
||||||
message: `Verbindung erfolgreich. Antwort: ${reply.trim().slice(0, 80) || "(leer)"}`,
|
message:
|
||||||
|
models.length > 0
|
||||||
|
? `Verbindung erfolgreich. ${models.length} Modelle verfügbar.`
|
||||||
|
: "Verbindung erfolgreich. Es wurden keine Modelle gefunden – bitte das Modell manuell eingeben.",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -201,4 +217,61 @@ router.post("/providers/test-connection", async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post("/providers/list-models", async (req, res) => {
|
||||||
|
const parsed = ListProviderModelsBody.safeParse(req.body);
|
||||||
|
if (!parsed.success)
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ message: "Ungültige Eingabe", details: parsed.error.issues });
|
||||||
|
const d = parsed.data;
|
||||||
|
|
||||||
|
let token: string | null =
|
||||||
|
d.apiToken && d.apiToken !== "" ? d.apiToken : null;
|
||||||
|
if (!token && d.providerId !== undefined && d.providerId !== null) {
|
||||||
|
const [existing] = await db
|
||||||
|
.select()
|
||||||
|
.from(aiProvidersTable)
|
||||||
|
.where(eq(aiProvidersTable.id, d.providerId));
|
||||||
|
if (existing?.apiToken) token = existing.apiToken;
|
||||||
|
}
|
||||||
|
if (!token) {
|
||||||
|
return res.json(
|
||||||
|
ListProviderModelsResponse.parse({
|
||||||
|
ok: false,
|
||||||
|
models: [],
|
||||||
|
message: "Kein API-Token angegeben.",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider: AiProvider = {
|
||||||
|
id: d.providerId ?? 0,
|
||||||
|
name: "",
|
||||||
|
apiType: d.apiType,
|
||||||
|
baseUrl: d.baseUrl,
|
||||||
|
model: "",
|
||||||
|
apiToken: token,
|
||||||
|
enabled: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const models = await listProviderModels(provider);
|
||||||
|
return res.json(
|
||||||
|
ListProviderModelsResponse.parse({ ok: true, models }),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
return res.json(
|
||||||
|
ListProviderModelsResponse.parse({
|
||||||
|
ok: false,
|
||||||
|
models: [],
|
||||||
|
message:
|
||||||
|
err instanceof Error
|
||||||
|
? err.message
|
||||||
|
: "Modelle konnten nicht geladen werden.",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
useListProviders, getListProvidersQueryKey, useCreateProvider, useUpdateProvider, useDeleteProvider, useTestProvider, useTestProviderConnection,
|
useListProviders, getListProvidersQueryKey, useCreateProvider, useUpdateProvider, useDeleteProvider, useTestProvider, useTestProviderConnection, useListProviderModels,
|
||||||
useListPrompts, getListPromptsQueryKey, useUpdatePrompt,
|
useListPrompts, getListPromptsQueryKey, useUpdatePrompt,
|
||||||
useListRules, getListRulesQueryKey, useUpdateRule,
|
useListRules, getListRulesQueryKey, useUpdateRule,
|
||||||
AiProviderApiType, RuleUpdateSeverity
|
AiProviderApiType, RuleUpdateSeverity
|
||||||
|
|
@ -21,6 +21,50 @@ 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";
|
||||||
|
|
||||||
|
function ModelField({ models, loading, tried, value, onChange }: {
|
||||||
|
models: string[];
|
||||||
|
loading: boolean;
|
||||||
|
tried: boolean;
|
||||||
|
value: string;
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
}) {
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>Modell</Label>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground border rounded-md px-3 py-2">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" /> Modelle werden geladen…
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (models.length > 0) {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>Modell</Label>
|
||||||
|
<Select value={value} onValueChange={onChange}>
|
||||||
|
<SelectTrigger><SelectValue placeholder="Modell auswählen" /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{models.map(m => <SelectItem key={m} value={m}>{m}</SelectItem>)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">{models.length} Modelle gefunden.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>Modell</Label>
|
||||||
|
<Input value={value} onChange={e => onChange(e.target.value)} required placeholder="z.B. gpt-4o" />
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{tried
|
||||||
|
? "Keine Modelle gefunden – bitte das Modell manuell eingeben."
|
||||||
|
: "Testen Sie die Verbindung, um verfügbare Modelle automatisch zu laden, oder geben Sie das Modell manuell ein."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ProviderTab() {
|
function ProviderTab() {
|
||||||
const { data: providers, isLoading } = useListProviders();
|
const { data: providers, isLoading } = useListProviders();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
@ -31,19 +75,42 @@ function ProviderTab() {
|
||||||
const deleteProvider = useDeleteProvider();
|
const deleteProvider = useDeleteProvider();
|
||||||
const testProvider = useTestProvider();
|
const testProvider = useTestProvider();
|
||||||
const testConnection = useTestProviderConnection();
|
const testConnection = useTestProviderConnection();
|
||||||
|
const listModels = useListProviderModels();
|
||||||
|
|
||||||
const [isAddOpen, setIsAddOpen] = useState(false);
|
const [isAddOpen, setIsAddOpen] = useState(false);
|
||||||
const [addForm, setAddForm] = useState({ name: "", apiType: AiProviderApiType.openai as AiProviderApiType, baseUrl: "", model: "", apiToken: "", enabled: true });
|
const [addForm, setAddForm] = useState({ name: "", apiType: AiProviderApiType.openai as AiProviderApiType, baseUrl: "", model: "", apiToken: "", enabled: true });
|
||||||
const [addTesting, setAddTesting] = useState(false);
|
const [addTesting, setAddTesting] = useState(false);
|
||||||
const [addTestResult, setAddTestResult] = useState<{ ok: boolean; message: string } | null>(null);
|
const [addTestResult, setAddTestResult] = useState<{ ok: boolean; message: string } | null>(null);
|
||||||
|
const [addModels, setAddModels] = useState<string[]>([]);
|
||||||
|
const [addModelsLoading, setAddModelsLoading] = useState(false);
|
||||||
|
const [addModelsTried, setAddModelsTried] = useState(false);
|
||||||
|
|
||||||
const [editingId, setEditingId] = useState<number | null>(null);
|
const [editingId, setEditingId] = useState<number | null>(null);
|
||||||
const [editForm, setEditForm] = useState({ name: "", apiType: AiProviderApiType.openai as AiProviderApiType, baseUrl: "", model: "", apiToken: "", enabled: true });
|
const [editForm, setEditForm] = useState({ name: "", apiType: AiProviderApiType.openai as AiProviderApiType, baseUrl: "", model: "", apiToken: "", enabled: true });
|
||||||
const [editTesting, setEditTesting] = useState(false);
|
const [editTesting, setEditTesting] = useState(false);
|
||||||
const [editTestResult, setEditTestResult] = useState<{ ok: boolean; message: string } | null>(null);
|
const [editTestResult, setEditTestResult] = useState<{ ok: boolean; message: string } | null>(null);
|
||||||
|
const [editModels, setEditModels] = useState<string[]>([]);
|
||||||
|
const [editModelsLoading, setEditModelsLoading] = useState(false);
|
||||||
|
const [editModelsTried, setEditModelsTried] = useState(false);
|
||||||
|
|
||||||
const [testingId, setTestingId] = useState<number | null>(null);
|
const [testingId, setTestingId] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const resetAddDiscovery = () => {
|
||||||
|
setAddTestResult(null);
|
||||||
|
setAddTesting(false);
|
||||||
|
setAddModels([]);
|
||||||
|
setAddModelsLoading(false);
|
||||||
|
setAddModelsTried(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetEditDiscovery = () => {
|
||||||
|
setEditTestResult(null);
|
||||||
|
setEditTesting(false);
|
||||||
|
setEditModels([]);
|
||||||
|
setEditModelsLoading(false);
|
||||||
|
setEditModelsTried(false);
|
||||||
|
};
|
||||||
|
|
||||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: getListProvidersQueryKey() });
|
const invalidate = () => queryClient.invalidateQueries({ queryKey: getListProvidersQueryKey() });
|
||||||
|
|
||||||
const handleAddSubmit = (e: React.FormEvent) => {
|
const handleAddSubmit = (e: React.FormEvent) => {
|
||||||
|
|
@ -53,6 +120,7 @@ function ProviderTab() {
|
||||||
toast({ title: "Provider hinzugefügt" });
|
toast({ title: "Provider hinzugefügt" });
|
||||||
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 });
|
||||||
|
resetAddDiscovery();
|
||||||
invalidate();
|
invalidate();
|
||||||
},
|
},
|
||||||
onError: () => toast({ title: "Fehler beim Hinzufügen", variant: "destructive" })
|
onError: () => toast({ title: "Fehler beim Hinzufügen", variant: "destructive" })
|
||||||
|
|
@ -104,13 +172,56 @@ function ProviderTab() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const discoverAddModels = () => {
|
||||||
|
setAddModelsLoading(true);
|
||||||
|
setAddModelsTried(true);
|
||||||
|
setAddModels([]);
|
||||||
|
listModels.mutate({ data: { apiType: addForm.apiType, baseUrl: addForm.baseUrl, apiToken: addForm.apiToken } }, {
|
||||||
|
onSuccess: (res) => {
|
||||||
|
setAddModelsLoading(false);
|
||||||
|
const models = res.ok ? res.models : [];
|
||||||
|
setAddModels(models);
|
||||||
|
if (models.length > 0 && !models.includes(addForm.model)) {
|
||||||
|
setAddForm(f => ({ ...f, model: models[0] }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
setAddModelsLoading(false);
|
||||||
|
setAddModels([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const discoverEditModels = (providerId: number) => {
|
||||||
|
setEditModelsLoading(true);
|
||||||
|
setEditModelsTried(true);
|
||||||
|
setEditModels([]);
|
||||||
|
listModels.mutate({ data: { apiType: editForm.apiType, baseUrl: editForm.baseUrl, apiToken: editForm.apiToken, providerId } }, {
|
||||||
|
onSuccess: (res) => {
|
||||||
|
setEditModelsLoading(false);
|
||||||
|
const models = res.ok ? res.models : [];
|
||||||
|
setEditModels(models);
|
||||||
|
if (models.length > 0 && !models.includes(editForm.model)) {
|
||||||
|
setEditForm(f => ({ ...f, model: models[0] }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
setEditModelsLoading(false);
|
||||||
|
setEditModels([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddTest = () => {
|
const handleAddTest = () => {
|
||||||
setAddTestResult(null);
|
setAddTestResult(null);
|
||||||
setAddTesting(true);
|
setAddTesting(true);
|
||||||
testConnection.mutate({ data: { apiType: addForm.apiType, baseUrl: addForm.baseUrl, model: addForm.model, apiToken: addForm.apiToken } }, {
|
setAddModels([]);
|
||||||
|
setAddModelsTried(false);
|
||||||
|
testConnection.mutate({ data: { apiType: addForm.apiType, baseUrl: addForm.baseUrl, ...(addForm.model ? { model: addForm.model } : {}), apiToken: addForm.apiToken } }, {
|
||||||
onSuccess: (res) => {
|
onSuccess: (res) => {
|
||||||
setAddTesting(false);
|
setAddTesting(false);
|
||||||
setAddTestResult({ ok: res.ok, message: res.message || (res.ok ? "Der API-Aufruf war erfolgreich." : "Es gab ein Problem.") });
|
setAddTestResult({ ok: res.ok, message: res.message || (res.ok ? "Der API-Aufruf war erfolgreich." : "Es gab ein Problem.") });
|
||||||
|
if (res.ok) discoverAddModels();
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
setAddTesting(false);
|
setAddTesting(false);
|
||||||
|
|
@ -123,10 +234,13 @@ function ProviderTab() {
|
||||||
if (!editingId) return;
|
if (!editingId) return;
|
||||||
setEditTestResult(null);
|
setEditTestResult(null);
|
||||||
setEditTesting(true);
|
setEditTesting(true);
|
||||||
testConnection.mutate({ data: { apiType: editForm.apiType, baseUrl: editForm.baseUrl, model: editForm.model, apiToken: editForm.apiToken, providerId: editingId } }, {
|
setEditModels([]);
|
||||||
|
setEditModelsTried(false);
|
||||||
|
testConnection.mutate({ data: { apiType: editForm.apiType, baseUrl: editForm.baseUrl, ...(editForm.model ? { model: editForm.model } : {}), apiToken: editForm.apiToken, providerId: editingId } }, {
|
||||||
onSuccess: (res) => {
|
onSuccess: (res) => {
|
||||||
setEditTesting(false);
|
setEditTesting(false);
|
||||||
setEditTestResult({ ok: res.ok, message: res.message || (res.ok ? "Der API-Aufruf war erfolgreich." : "Es gab ein Problem.") });
|
setEditTestResult({ ok: res.ok, message: res.message || (res.ok ? "Der API-Aufruf war erfolgreich." : "Es gab ein Problem.") });
|
||||||
|
if (res.ok) discoverEditModels(editingId);
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
setEditTesting(false);
|
setEditTesting(false);
|
||||||
|
|
@ -144,8 +258,7 @@ function ProviderTab() {
|
||||||
apiToken: "",
|
apiToken: "",
|
||||||
enabled: provider.enabled
|
enabled: provider.enabled
|
||||||
});
|
});
|
||||||
setEditTestResult(null);
|
resetEditDiscovery();
|
||||||
setEditTesting(false);
|
|
||||||
setEditingId(provider.id);
|
setEditingId(provider.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -158,7 +271,7 @@ function ProviderTab() {
|
||||||
<h2 className="text-xl font-bold">KI-Provider</h2>
|
<h2 className="text-xl font-bold">KI-Provider</h2>
|
||||||
<p className="text-sm text-muted-foreground">Konfigurieren Sie externe LLM-Provider für die semantische Analyse.</p>
|
<p className="text-sm text-muted-foreground">Konfigurieren Sie externe LLM-Provider für die semantische Analyse.</p>
|
||||||
</div>
|
</div>
|
||||||
<Dialog open={isAddOpen} onOpenChange={(o) => { setIsAddOpen(o); if (!o) { setAddTestResult(null); setAddTesting(false); } }}>
|
<Dialog open={isAddOpen} onOpenChange={(o) => { setIsAddOpen(o); if (!o) resetAddDiscovery(); }}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button className="gap-2"><Plus className="w-4 h-4"/> Provider hinzufügen</Button>
|
<Button className="gap-2"><Plus className="w-4 h-4"/> Provider hinzufügen</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|
@ -187,18 +300,21 @@ function ProviderTab() {
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>Base URL</Label>
|
<Label>API-Endpunkt (Base URL)</Label>
|
||||||
<Input value={addForm.baseUrl} onChange={e => setAddForm({...addForm, baseUrl: e.target.value})} required placeholder="z.B. https://api.openai.com/v1" />
|
<Input value={addForm.baseUrl} onChange={e => { setAddForm({...addForm, baseUrl: e.target.value}); resetAddDiscovery(); }} required placeholder="z.B. https://api.openai.com/v1" />
|
||||||
<p className="text-xs text-muted-foreground">OpenAI-kompatibel: https://api.openai.com/v1 <br/> Anthropic: https://api.anthropic.com/v1</p>
|
<p className="text-xs text-muted-foreground">OpenAI-kompatibel: https://api.openai.com/v1 <br/> Anthropic: https://api.anthropic.com/v1</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label>Modell</Label>
|
|
||||||
<Input value={addForm.model} onChange={e => setAddForm({...addForm, model: e.target.value})} required placeholder="z.B. gpt-4o" />
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>API Token</Label>
|
<Label>API Token</Label>
|
||||||
<Input type="password" value={addForm.apiToken} onChange={e => setAddForm({...addForm, apiToken: e.target.value})} required />
|
<Input type="password" value={addForm.apiToken} onChange={e => { setAddForm({...addForm, apiToken: e.target.value}); resetAddDiscovery(); }} required />
|
||||||
</div>
|
</div>
|
||||||
|
<ModelField
|
||||||
|
models={addModels}
|
||||||
|
loading={addModelsLoading}
|
||||||
|
tried={addModelsTried}
|
||||||
|
value={addForm.model}
|
||||||
|
onChange={(v) => setAddForm(f => ({ ...f, model: v }))}
|
||||||
|
/>
|
||||||
<div className="flex items-center justify-between mt-2">
|
<div className="flex items-center justify-between mt-2">
|
||||||
<Label>Aktiviert</Label>
|
<Label>Aktiviert</Label>
|
||||||
<Switch checked={addForm.enabled} onCheckedChange={c => setAddForm({...addForm, enabled: c})} />
|
<Switch checked={addForm.enabled} onCheckedChange={c => setAddForm({...addForm, enabled: c})} />
|
||||||
|
|
@ -289,17 +405,20 @@ function ProviderTab() {
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>Base URL</Label>
|
<Label>API-Endpunkt (Base URL)</Label>
|
||||||
<Input value={editForm.baseUrl} onChange={e => setEditForm({...editForm, baseUrl: e.target.value})} required />
|
<Input value={editForm.baseUrl} onChange={e => { setEditForm({...editForm, baseUrl: e.target.value}); resetEditDiscovery(); }} required />
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label>Modell</Label>
|
|
||||||
<Input value={editForm.model} onChange={e => setEditForm({...editForm, model: e.target.value})} required />
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>API Token (leer lassen zum Beibehalten)</Label>
|
<Label>API Token (leer lassen zum Beibehalten)</Label>
|
||||||
<Input type="password" value={editForm.apiToken} onChange={e => setEditForm({...editForm, apiToken: e.target.value})} placeholder="Token beibehalten" />
|
<Input type="password" value={editForm.apiToken} onChange={e => { setEditForm({...editForm, apiToken: e.target.value}); resetEditDiscovery(); }} placeholder="Token beibehalten" />
|
||||||
</div>
|
</div>
|
||||||
|
<ModelField
|
||||||
|
models={editModels}
|
||||||
|
loading={editModelsLoading}
|
||||||
|
tried={editModelsTried}
|
||||||
|
value={editForm.model}
|
||||||
|
onChange={(v) => setEditForm(f => ({ ...f, model: v }))}
|
||||||
|
/>
|
||||||
{editTestResult && (
|
{editTestResult && (
|
||||||
<div className={`flex items-start gap-2 rounded-md border p-3 text-sm ${editTestResult.ok ? "border-green-500/50 bg-green-500/10 text-green-700 dark:text-green-400" : "border-destructive/50 bg-destructive/10 text-destructive"}`}>
|
<div className={`flex items-start gap-2 rounded-md border p-3 text-sm ${editTestResult.ok ? "border-green-500/50 bg-green-500/10 text-green-700 dark:text-green-400" : "border-destructive/50 bg-destructive/10 text-destructive"}`}>
|
||||||
{editTestResult.ok ? <CheckCircle2 className="w-4 h-4 mt-0.5 shrink-0" /> : <XCircle className="w-4 h-4 mt-0.5 shrink-0" />}
|
{editTestResult.ok ? <CheckCircle2 className="w-4 h-4 mt-0.5 shrink-0" /> : <XCircle className="w-4 h-4 mt-0.5 shrink-0" />}
|
||||||
|
|
|
||||||
|
|
@ -487,14 +487,40 @@ export interface ProviderTestConnectionInput {
|
||||||
apiType: ProviderTestConnectionInputApiType;
|
apiType: ProviderTestConnectionInputApiType;
|
||||||
/** @minLength 1 */
|
/** @minLength 1 */
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
/** @minLength 1 */
|
/** Optional model to exercise with a full request; when omitted the test verifies credentials via the models endpoint instead */
|
||||||
model: string;
|
model?: string;
|
||||||
/** Token to use for the test; omit or leave empty to fall back to the stored token of providerId */
|
/** Token to use for the test; omit or leave empty to fall back to the stored token of providerId */
|
||||||
apiToken?: string;
|
apiToken?: string;
|
||||||
/** When apiToken is empty, fall back to this saved provider's stored token */
|
/** When apiToken is empty, fall back to this saved provider's stored token */
|
||||||
providerId?: number;
|
providerId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ProviderListModelsInputApiType = typeof ProviderListModelsInputApiType[keyof typeof ProviderListModelsInputApiType];
|
||||||
|
|
||||||
|
|
||||||
|
export const ProviderListModelsInputApiType = {
|
||||||
|
openai: 'openai',
|
||||||
|
anthropic: 'anthropic',
|
||||||
|
custom: 'custom',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export interface ProviderListModelsInput {
|
||||||
|
apiType: ProviderListModelsInputApiType;
|
||||||
|
/** @minLength 1 */
|
||||||
|
baseUrl: string;
|
||||||
|
/** Token to use for the request; omit or leave empty to fall back to the stored token of providerId */
|
||||||
|
apiToken?: string;
|
||||||
|
/** When apiToken is empty, fall back to this saved provider's stored token */
|
||||||
|
providerId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProviderModelsResult {
|
||||||
|
ok: boolean;
|
||||||
|
models: string[];
|
||||||
|
/** @nullable */
|
||||||
|
message?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Prompt {
|
export interface Prompt {
|
||||||
id: number;
|
id: number;
|
||||||
key: string;
|
key: string;
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ import type {
|
||||||
HealthStatus,
|
HealthStatus,
|
||||||
Prompt,
|
Prompt,
|
||||||
PromptUpdate,
|
PromptUpdate,
|
||||||
|
ProviderListModelsInput,
|
||||||
|
ProviderModelsResult,
|
||||||
ProviderTestConnectionInput,
|
ProviderTestConnectionInput,
|
||||||
ProviderTestResult,
|
ProviderTestResult,
|
||||||
Rule,
|
Rule,
|
||||||
|
|
@ -1095,6 +1097,78 @@ export const useTestProviderConnection = <TError = ErrorType<unknown>,
|
||||||
return useMutation(getTestProviderConnectionMutationOptions(options));
|
return useMutation(getTestProviderConnectionMutationOptions(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getListProviderModelsUrl = () => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return `/api/providers/list-models`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries the provider's models endpoint with the supplied ad-hoc configuration (or the stored token of providerId when the token is omitted) and returns the discovered model IDs.
|
||||||
|
* @summary List the available models for a provider configuration
|
||||||
|
*/
|
||||||
|
export const listProviderModels = async (providerListModelsInput: ProviderListModelsInput, options?: RequestInit): Promise<ProviderModelsResult> => {
|
||||||
|
|
||||||
|
return customFetch<ProviderModelsResult>(getListProviderModelsUrl(),
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||||
|
body: JSON.stringify(
|
||||||
|
providerListModelsInput,)
|
||||||
|
}
|
||||||
|
);}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const getListProviderModelsMutationOptions = <TError = ErrorType<unknown>,
|
||||||
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof listProviderModels>>, TError,{data: BodyType<ProviderListModelsInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||||
|
): UseMutationOptions<Awaited<ReturnType<typeof listProviderModels>>, TError,{data: BodyType<ProviderListModelsInput>}, TContext> => {
|
||||||
|
|
||||||
|
const mutationKey = ['listProviderModels'];
|
||||||
|
const {mutation: mutationOptions, request: requestOptions} = options ?
|
||||||
|
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
||||||
|
options
|
||||||
|
: {...options, mutation: {...options.mutation, mutationKey}}
|
||||||
|
: {mutation: { mutationKey, }, request: undefined};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const mutationFn: MutationFunction<Awaited<ReturnType<typeof listProviderModels>>, {data: BodyType<ProviderListModelsInput>}> = (props) => {
|
||||||
|
const {data} = props ?? {};
|
||||||
|
|
||||||
|
return listProviderModels(data,requestOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return { mutationFn, ...mutationOptions }}
|
||||||
|
|
||||||
|
export type ListProviderModelsMutationResult = NonNullable<Awaited<ReturnType<typeof listProviderModels>>>
|
||||||
|
export type ListProviderModelsMutationBody = BodyType<ProviderListModelsInput>
|
||||||
|
export type ListProviderModelsMutationError = ErrorType<unknown>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary List the available models for a provider configuration
|
||||||
|
*/
|
||||||
|
export const useListProviderModels = <TError = ErrorType<unknown>,
|
||||||
|
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof listProviderModels>>, TError,{data: BodyType<ProviderListModelsInput>}, TContext>, request?: SecondParameter<typeof customFetch>}
|
||||||
|
): UseMutationResult<
|
||||||
|
Awaited<ReturnType<typeof listProviderModels>>,
|
||||||
|
TError,
|
||||||
|
{data: BodyType<ProviderListModelsInput>},
|
||||||
|
TContext
|
||||||
|
> => {
|
||||||
|
return useMutation(getListProviderModelsMutationOptions(options));
|
||||||
|
}
|
||||||
|
|
||||||
export const getListPromptsUrl = () => {
|
export const getListPromptsUrl = () => {
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -303,6 +303,29 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/ProviderTestResult"
|
$ref: "#/components/schemas/ProviderTestResult"
|
||||||
|
|
||||||
|
/providers/list-models:
|
||||||
|
post:
|
||||||
|
operationId: listProviderModels
|
||||||
|
tags: [providers]
|
||||||
|
summary: List the available models for a provider configuration
|
||||||
|
description: >-
|
||||||
|
Queries the provider's models endpoint with the supplied ad-hoc
|
||||||
|
configuration (or the stored token of providerId when the token is
|
||||||
|
omitted) and returns the discovered model IDs.
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ProviderListModelsInput"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Discovered models
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ProviderModelsResult"
|
||||||
|
|
||||||
/prompts:
|
/prompts:
|
||||||
get:
|
get:
|
||||||
operationId: listPrompts
|
operationId: listPrompts
|
||||||
|
|
@ -838,7 +861,7 @@ components:
|
||||||
|
|
||||||
ProviderTestConnectionInput:
|
ProviderTestConnectionInput:
|
||||||
type: object
|
type: object
|
||||||
required: [apiType, baseUrl, model]
|
required: [apiType, baseUrl]
|
||||||
properties:
|
properties:
|
||||||
apiType:
|
apiType:
|
||||||
type: string
|
type: string
|
||||||
|
|
@ -848,7 +871,7 @@ components:
|
||||||
minLength: 1
|
minLength: 1
|
||||||
model:
|
model:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
description: Optional model to exercise with a full request; when omitted the test verifies credentials via the models endpoint instead
|
||||||
apiToken:
|
apiToken:
|
||||||
type: string
|
type: string
|
||||||
description: Token to use for the test; omit or leave empty to fall back to the stored token of providerId
|
description: Token to use for the test; omit or leave empty to fall back to the stored token of providerId
|
||||||
|
|
@ -856,6 +879,36 @@ components:
|
||||||
type: integer
|
type: integer
|
||||||
description: When apiToken is empty, fall back to this saved provider's stored token
|
description: When apiToken is empty, fall back to this saved provider's stored token
|
||||||
|
|
||||||
|
ProviderListModelsInput:
|
||||||
|
type: object
|
||||||
|
required: [apiType, baseUrl]
|
||||||
|
properties:
|
||||||
|
apiType:
|
||||||
|
type: string
|
||||||
|
enum: [openai, anthropic, custom]
|
||||||
|
baseUrl:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
|
apiToken:
|
||||||
|
type: string
|
||||||
|
description: Token to use for the request; omit or leave empty to fall back to the stored token of providerId
|
||||||
|
providerId:
|
||||||
|
type: integer
|
||||||
|
description: When apiToken is empty, fall back to this saved provider's stored token
|
||||||
|
|
||||||
|
ProviderModelsResult:
|
||||||
|
type: object
|
||||||
|
required: [ok, models]
|
||||||
|
properties:
|
||||||
|
ok:
|
||||||
|
type: boolean
|
||||||
|
models:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: ["string", "null"]
|
||||||
|
|
||||||
Prompt:
|
Prompt:
|
||||||
type: object
|
type: object
|
||||||
required: [id, key, name, content, updatedAt]
|
required: [id, key, name, content, updatedAt]
|
||||||
|
|
|
||||||
|
|
@ -370,11 +370,10 @@ export const TestProviderResponse = zod.object({
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const TestProviderConnectionBody = zod.object({
|
export const TestProviderConnectionBody = zod.object({
|
||||||
"apiType": zod.enum(['openai', 'anthropic', 'custom']),
|
"apiType": zod.enum(['openai', 'anthropic', 'custom']),
|
||||||
"baseUrl": zod.string().min(1),
|
"baseUrl": zod.string().min(1),
|
||||||
"model": zod.string().min(1),
|
"model": zod.string().optional().describe('Optional model to exercise with a full request; when omitted the test verifies credentials via the models endpoint instead'),
|
||||||
"apiToken": zod.string().optional().describe('Token to use for the test; omit or leave empty to fall back to the stored token of providerId'),
|
"apiToken": zod.string().optional().describe('Token to use for the test; omit or leave empty to fall back to the stored token of providerId'),
|
||||||
"providerId": zod.number().optional().describe('When apiToken is empty, fall back to this saved provider\'s stored token')
|
"providerId": zod.number().optional().describe('When apiToken is empty, fall back to this saved provider\'s stored token')
|
||||||
})
|
})
|
||||||
|
|
@ -385,6 +384,27 @@ export const TestProviderConnectionResponse = zod.object({
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries the provider's models endpoint with the supplied ad-hoc configuration (or the stored token of providerId when the token is omitted) and returns the discovered model IDs.
|
||||||
|
* @summary List the available models for a provider configuration
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const ListProviderModelsBody = zod.object({
|
||||||
|
"apiType": zod.enum(['openai', 'anthropic', 'custom']),
|
||||||
|
"baseUrl": zod.string().min(1),
|
||||||
|
"apiToken": zod.string().optional().describe('Token to use for the request; omit or leave empty to fall back to the stored token of providerId'),
|
||||||
|
"providerId": zod.number().optional().describe('When apiToken is empty, fall back to this saved provider\'s stored token')
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ListProviderModelsResponse = zod.object({
|
||||||
|
"ok": zod.boolean(),
|
||||||
|
"models": zod.array(zod.string()),
|
||||||
|
"message": zod.string().nullish()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary List configurable AI prompts
|
* @summary List configurable AI prompts
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,9 @@ export * from './findingSeverity';
|
||||||
export * from './healthStatus';
|
export * from './healthStatus';
|
||||||
export * from './prompt';
|
export * from './prompt';
|
||||||
export * from './promptUpdate';
|
export * from './promptUpdate';
|
||||||
|
export * from './providerListModelsInput';
|
||||||
|
export * from './providerListModelsInputApiType';
|
||||||
|
export * from './providerModelsResult';
|
||||||
export * from './providerTestConnectionInput';
|
export * from './providerTestConnectionInput';
|
||||||
export * from './providerTestConnectionInputApiType';
|
export * from './providerTestConnectionInputApiType';
|
||||||
export * from './providerTestResult';
|
export * from './providerTestResult';
|
||||||
|
|
|
||||||
18
lib/api-zod/src/generated/types/providerListModelsInput.ts
Normal file
18
lib/api-zod/src/generated/types/providerListModelsInput.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
/**
|
||||||
|
* Generated by orval v8.9.1 🍺
|
||||||
|
* Do not edit manually.
|
||||||
|
* Api
|
||||||
|
* API specification
|
||||||
|
* OpenAPI spec version: 0.1.0
|
||||||
|
*/
|
||||||
|
import type { ProviderListModelsInputApiType } from './providerListModelsInputApiType';
|
||||||
|
|
||||||
|
export interface ProviderListModelsInput {
|
||||||
|
apiType: ProviderListModelsInputApiType;
|
||||||
|
/** @minLength 1 */
|
||||||
|
baseUrl: string;
|
||||||
|
/** Token to use for the request; omit or leave empty to fall back to the stored token of providerId */
|
||||||
|
apiToken?: string;
|
||||||
|
/** When apiToken is empty, fall back to this saved provider's stored token */
|
||||||
|
providerId?: number;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
/**
|
||||||
|
* Generated by orval v8.9.1 🍺
|
||||||
|
* Do not edit manually.
|
||||||
|
* Api
|
||||||
|
* API specification
|
||||||
|
* OpenAPI spec version: 0.1.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ProviderListModelsInputApiType = typeof ProviderListModelsInputApiType[keyof typeof ProviderListModelsInputApiType];
|
||||||
|
|
||||||
|
|
||||||
|
export const ProviderListModelsInputApiType = {
|
||||||
|
openai: 'openai',
|
||||||
|
anthropic: 'anthropic',
|
||||||
|
custom: 'custom',
|
||||||
|
} as const;
|
||||||
14
lib/api-zod/src/generated/types/providerModelsResult.ts
Normal file
14
lib/api-zod/src/generated/types/providerModelsResult.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
/**
|
||||||
|
* Generated by orval v8.9.1 🍺
|
||||||
|
* Do not edit manually.
|
||||||
|
* Api
|
||||||
|
* API specification
|
||||||
|
* OpenAPI spec version: 0.1.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ProviderModelsResult {
|
||||||
|
ok: boolean;
|
||||||
|
models: string[];
|
||||||
|
/** @nullable */
|
||||||
|
message?: string | null;
|
||||||
|
}
|
||||||
|
|
@ -11,8 +11,8 @@ export interface ProviderTestConnectionInput {
|
||||||
apiType: ProviderTestConnectionInputApiType;
|
apiType: ProviderTestConnectionInputApiType;
|
||||||
/** @minLength 1 */
|
/** @minLength 1 */
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
/** @minLength 1 */
|
/** Optional model to exercise with a full request; when omitted the test verifies credentials via the models endpoint instead */
|
||||||
model: string;
|
model?: string;
|
||||||
/** Token to use for the test; omit or leave empty to fall back to the stored token of providerId */
|
/** Token to use for the test; omit or leave empty to fall back to the stored token of providerId */
|
||||||
apiToken?: string;
|
apiToken?: string;
|
||||||
/** When apiToken is empty, fall back to this saved provider's stored token */
|
/** When apiToken is empty, fall back to this saved provider's stored token */
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue