From f44c3ed247a71da1d8183553f915e35b5661a16f Mon Sep 17 00:00:00 2001 From: amertensreplit <49614208-amertensreplit@users.noreply.replit.com> Date: Wed, 10 Jun 2026 21:13:35 +0000 Subject: [PATCH] Guided AI provider setup with model discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- artifacts/api-server/src/lib/aiAnalysis.ts | 42 +++++ artifacts/api-server/src/routes/providers.ts | 89 +++++++++- artifacts/skillguard/src/pages/admin.tsx | 159 +++++++++++++++--- .../src/generated/api.schemas.ts | 30 +++- lib/api-client-react/src/generated/api.ts | 74 ++++++++ lib/api-spec/openapi.yaml | 57 ++++++- lib/api-zod/src/generated/api.ts | 24 ++- lib/api-zod/src/generated/types/index.ts | 3 + .../types/providerListModelsInput.ts | 18 ++ .../types/providerListModelsInputApiType.ts | 16 ++ .../generated/types/providerModelsResult.ts | 14 ++ .../types/providerTestConnectionInput.ts | 4 +- 12 files changed, 494 insertions(+), 36 deletions(-) create mode 100644 lib/api-zod/src/generated/types/providerListModelsInput.ts create mode 100644 lib/api-zod/src/generated/types/providerListModelsInputApiType.ts create mode 100644 lib/api-zod/src/generated/types/providerModelsResult.ts diff --git a/artifacts/api-server/src/lib/aiAnalysis.ts b/artifacts/api-server/src/lib/aiAnalysis.ts index 237f6e2..5caebcc 100644 --- a/artifacts/api-server/src/lib/aiAnalysis.ts +++ b/artifacts/api-server/src/lib/aiAnalysis.ts @@ -175,6 +175,48 @@ export async function callProvider( return callOpenAiCompatible(provider, system, user); } +export async function listProviderModels( + provider: AiProvider, +): Promise { + const base = provider.baseUrl.replace(/\/$/, ""); + const url = `${base}/models`; + const headers: Record = + 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 { const lines = aiRules.map( (r) => `- ${r.ruleId} (${r.axis}): ${r.title} — ${r.description}`, diff --git a/artifacts/api-server/src/routes/providers.ts b/artifacts/api-server/src/routes/providers.ts index 1e5a3ca..3219931 100644 --- a/artifacts/api-server/src/routes/providers.ts +++ b/artifacts/api-server/src/routes/providers.ts @@ -12,8 +12,10 @@ import { TestProviderParams, TestProviderResponse, TestProviderConnectionBody, + ListProviderModelsBody, + ListProviderModelsResponse, } from "@workspace/api-zod"; -import { callProvider } from "../lib/aiAnalysis"; +import { callProvider, listProviderModels } from "../lib/aiAnalysis"; 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 = { id: d.providerId ?? 0, name: "", apiType: d.apiType, baseUrl: d.baseUrl, - model: d.model, + model: hasModel ? (d.model as string) : "", apiToken: token, enabled: true, createdAt: new Date(), }; try { - const reply = await callProvider( - provider, - "Du bist ein Verbindungstest.", - 'Antworte mit dem einzelnen Wort "OK".', - ); + if (hasModel) { + const reply = await callProvider( + provider, + "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( TestProviderResponse.parse({ 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) { @@ -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; diff --git a/artifacts/skillguard/src/pages/admin.tsx b/artifacts/skillguard/src/pages/admin.tsx index 42f85c4..f558aa1 100644 --- a/artifacts/skillguard/src/pages/admin.tsx +++ b/artifacts/skillguard/src/pages/admin.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { - useListProviders, getListProvidersQueryKey, useCreateProvider, useUpdateProvider, useDeleteProvider, useTestProvider, useTestProviderConnection, + useListProviders, getListProvidersQueryKey, useCreateProvider, useUpdateProvider, useDeleteProvider, useTestProvider, useTestProviderConnection, useListProviderModels, useListPrompts, getListPromptsQueryKey, useUpdatePrompt, useListRules, getListRulesQueryKey, useUpdateRule, 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 { 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 ( +
+ +
+ Modelle werden geladen… +
+
+ ); + } + if (models.length > 0) { + return ( +
+ + +

{models.length} Modelle gefunden.

+
+ ); + } + return ( +
+ + onChange(e.target.value)} required placeholder="z.B. gpt-4o" /> +

+ {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."} +

+
+ ); +} + function ProviderTab() { const { data: providers, isLoading } = useListProviders(); const queryClient = useQueryClient(); @@ -31,19 +75,42 @@ function ProviderTab() { const deleteProvider = useDeleteProvider(); const testProvider = useTestProvider(); const testConnection = useTestProviderConnection(); + const listModels = useListProviderModels(); const [isAddOpen, setIsAddOpen] = useState(false); const [addForm, setAddForm] = useState({ name: "", apiType: AiProviderApiType.openai as AiProviderApiType, baseUrl: "", model: "", apiToken: "", enabled: true }); const [addTesting, setAddTesting] = useState(false); const [addTestResult, setAddTestResult] = useState<{ ok: boolean; message: string } | null>(null); + const [addModels, setAddModels] = useState([]); + const [addModelsLoading, setAddModelsLoading] = useState(false); + const [addModelsTried, setAddModelsTried] = useState(false); const [editingId, setEditingId] = useState(null); const [editForm, setEditForm] = useState({ name: "", apiType: AiProviderApiType.openai as AiProviderApiType, baseUrl: "", model: "", apiToken: "", enabled: true }); const [editTesting, setEditTesting] = useState(false); const [editTestResult, setEditTestResult] = useState<{ ok: boolean; message: string } | null>(null); + const [editModels, setEditModels] = useState([]); + const [editModelsLoading, setEditModelsLoading] = useState(false); + const [editModelsTried, setEditModelsTried] = useState(false); const [testingId, setTestingId] = useState(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 handleAddSubmit = (e: React.FormEvent) => { @@ -53,6 +120,7 @@ function ProviderTab() { toast({ title: "Provider hinzugefügt" }); setIsAddOpen(false); setAddForm({ name: "", apiType: AiProviderApiType.openai as AiProviderApiType, baseUrl: "", model: "", apiToken: "", enabled: true }); + resetAddDiscovery(); invalidate(); }, 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 = () => { setAddTestResult(null); 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) => { setAddTesting(false); setAddTestResult({ ok: res.ok, message: res.message || (res.ok ? "Der API-Aufruf war erfolgreich." : "Es gab ein Problem.") }); + if (res.ok) discoverAddModels(); }, onError: () => { setAddTesting(false); @@ -123,10 +234,13 @@ function ProviderTab() { if (!editingId) return; setEditTestResult(null); 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) => { setEditTesting(false); setEditTestResult({ ok: res.ok, message: res.message || (res.ok ? "Der API-Aufruf war erfolgreich." : "Es gab ein Problem.") }); + if (res.ok) discoverEditModels(editingId); }, onError: () => { setEditTesting(false); @@ -144,8 +258,7 @@ function ProviderTab() { apiToken: "", enabled: provider.enabled }); - setEditTestResult(null); - setEditTesting(false); + resetEditDiscovery(); setEditingId(provider.id); }; @@ -158,7 +271,7 @@ function ProviderTab() {

KI-Provider

Konfigurieren Sie externe LLM-Provider für die semantische Analyse.

- { setIsAddOpen(o); if (!o) { setAddTestResult(null); setAddTesting(false); } }}> + { setIsAddOpen(o); if (!o) resetAddDiscovery(); }}> @@ -187,18 +300,21 @@ function ProviderTab() {
- - setAddForm({...addForm, baseUrl: e.target.value})} required placeholder="z.B. https://api.openai.com/v1" /> + + { setAddForm({...addForm, baseUrl: e.target.value}); resetAddDiscovery(); }} required placeholder="z.B. https://api.openai.com/v1" />

OpenAI-kompatibel: https://api.openai.com/v1
Anthropic: https://api.anthropic.com/v1

-
- - setAddForm({...addForm, model: e.target.value})} required placeholder="z.B. gpt-4o" /> -
- setAddForm({...addForm, apiToken: e.target.value})} required /> + { setAddForm({...addForm, apiToken: e.target.value}); resetAddDiscovery(); }} required />
+ setAddForm(f => ({ ...f, model: v }))} + />
setAddForm({...addForm, enabled: c})} /> @@ -289,17 +405,20 @@ function ProviderTab() {
- - setEditForm({...editForm, baseUrl: e.target.value})} required /> -
-
- - setEditForm({...editForm, model: e.target.value})} required /> + + { setEditForm({...editForm, baseUrl: e.target.value}); resetEditDiscovery(); }} required />
- setEditForm({...editForm, apiToken: e.target.value})} placeholder="Token beibehalten" /> + { setEditForm({...editForm, apiToken: e.target.value}); resetEditDiscovery(); }} placeholder="Token beibehalten" />
+ setEditForm(f => ({ ...f, model: v }))} + /> {editTestResult && (
{editTestResult.ok ? : } diff --git a/lib/api-client-react/src/generated/api.schemas.ts b/lib/api-client-react/src/generated/api.schemas.ts index ff0cf02..d178789 100644 --- a/lib/api-client-react/src/generated/api.schemas.ts +++ b/lib/api-client-react/src/generated/api.schemas.ts @@ -487,14 +487,40 @@ export interface ProviderTestConnectionInput { apiType: ProviderTestConnectionInputApiType; /** @minLength 1 */ baseUrl: string; - /** @minLength 1 */ - model: string; + /** Optional model to exercise with a full request; when omitted the test verifies credentials via the models endpoint instead */ + model?: string; /** Token to use for the test; 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 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 { id: number; key: string; diff --git a/lib/api-client-react/src/generated/api.ts b/lib/api-client-react/src/generated/api.ts index 714aded..88713d8 100644 --- a/lib/api-client-react/src/generated/api.ts +++ b/lib/api-client-react/src/generated/api.ts @@ -28,6 +28,8 @@ import type { HealthStatus, Prompt, PromptUpdate, + ProviderListModelsInput, + ProviderModelsResult, ProviderTestConnectionInput, ProviderTestResult, Rule, @@ -1095,6 +1097,78 @@ export const useTestProviderConnection = , 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 => { + + return customFetch(getListProviderModelsUrl(), + { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify( + providerListModelsInput,) + } +);} + + + + +export const getListProviderModelsMutationOptions = , + TContext = unknown>(options?: { mutation?:UseMutationOptions>, TError,{data: BodyType}, TContext>, request?: SecondParameter} +): UseMutationOptions>, TError,{data: BodyType}, 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>, {data: BodyType}> = (props) => { + const {data} = props ?? {}; + + return listProviderModels(data,requestOptions) + } + + + + + + + return { mutationFn, ...mutationOptions }} + + export type ListProviderModelsMutationResult = NonNullable>> + export type ListProviderModelsMutationBody = BodyType + export type ListProviderModelsMutationError = ErrorType + + /** + * @summary List the available models for a provider configuration + */ +export const useListProviderModels = , + TContext = unknown>(options?: { mutation?:UseMutationOptions>, TError,{data: BodyType}, TContext>, request?: SecondParameter} + ): UseMutationResult< + Awaited>, + TError, + {data: BodyType}, + TContext + > => { + return useMutation(getListProviderModelsMutationOptions(options)); + } + export const getListPromptsUrl = () => { diff --git a/lib/api-spec/openapi.yaml b/lib/api-spec/openapi.yaml index 3462bfe..4945f90 100644 --- a/lib/api-spec/openapi.yaml +++ b/lib/api-spec/openapi.yaml @@ -303,6 +303,29 @@ paths: schema: $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: get: operationId: listPrompts @@ -838,7 +861,7 @@ components: ProviderTestConnectionInput: type: object - required: [apiType, baseUrl, model] + required: [apiType, baseUrl] properties: apiType: type: string @@ -848,7 +871,7 @@ components: minLength: 1 model: 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: type: string 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 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: type: object required: [id, key, name, content, updatedAt] diff --git a/lib/api-zod/src/generated/api.ts b/lib/api-zod/src/generated/api.ts index 860cc52..b3a4224 100644 --- a/lib/api-zod/src/generated/api.ts +++ b/lib/api-zod/src/generated/api.ts @@ -370,11 +370,10 @@ export const TestProviderResponse = zod.object({ - export const TestProviderConnectionBody = zod.object({ "apiType": zod.enum(['openai', 'anthropic', 'custom']), "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'), "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 */ diff --git a/lib/api-zod/src/generated/types/index.ts b/lib/api-zod/src/generated/types/index.ts index 160e209..206ab44 100644 --- a/lib/api-zod/src/generated/types/index.ts +++ b/lib/api-zod/src/generated/types/index.ts @@ -27,6 +27,9 @@ export * from './findingSeverity'; export * from './healthStatus'; export * from './prompt'; export * from './promptUpdate'; +export * from './providerListModelsInput'; +export * from './providerListModelsInputApiType'; +export * from './providerModelsResult'; export * from './providerTestConnectionInput'; export * from './providerTestConnectionInputApiType'; export * from './providerTestResult'; diff --git a/lib/api-zod/src/generated/types/providerListModelsInput.ts b/lib/api-zod/src/generated/types/providerListModelsInput.ts new file mode 100644 index 0000000..07fec82 --- /dev/null +++ b/lib/api-zod/src/generated/types/providerListModelsInput.ts @@ -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; +} diff --git a/lib/api-zod/src/generated/types/providerListModelsInputApiType.ts b/lib/api-zod/src/generated/types/providerListModelsInputApiType.ts new file mode 100644 index 0000000..2e41343 --- /dev/null +++ b/lib/api-zod/src/generated/types/providerListModelsInputApiType.ts @@ -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; diff --git a/lib/api-zod/src/generated/types/providerModelsResult.ts b/lib/api-zod/src/generated/types/providerModelsResult.ts new file mode 100644 index 0000000..0f6c8c7 --- /dev/null +++ b/lib/api-zod/src/generated/types/providerModelsResult.ts @@ -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; +} diff --git a/lib/api-zod/src/generated/types/providerTestConnectionInput.ts b/lib/api-zod/src/generated/types/providerTestConnectionInput.ts index 5104c98..29881e7 100644 --- a/lib/api-zod/src/generated/types/providerTestConnectionInput.ts +++ b/lib/api-zod/src/generated/types/providerTestConnectionInput.ts @@ -11,8 +11,8 @@ export interface ProviderTestConnectionInput { apiType: ProviderTestConnectionInputApiType; /** @minLength 1 */ baseUrl: string; - /** @minLength 1 */ - model: string; + /** Optional model to exercise with a full request; when omitted the test verifies credentials via the models endpoint instead */ + model?: string; /** Token to use for the test; 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 */