feat(model-connections): add test preview functionality for model connections

This commit is contained in:
Anish Sarkar 2026-06-13 00:12:04 +05:30
parent 55f004e1da
commit 9f6210ad08
12 changed files with 294 additions and 77 deletions

View file

@ -1,12 +1,14 @@
"use client";
import { useAtom, useAtomValue } from "jotai";
import { CheckCircle2, Trash2, XCircle } from "lucide-react";
import { Trash2 } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import {
createModelConnectionMutationAtom,
deleteModelConnectionMutationAtom,
previewConnectionModelsMutationAtom,
testPreviewModelMutationAtom,
updateModelRolesMutationAtom,
} from "@/atoms/model-connections/model-connections-mutation.atoms";
import {
@ -53,24 +55,6 @@ import {
providerIcon,
} from "./model-connections/provider-metadata";
function StatusBadge({ connection }: { connection: ConnectionRead }) {
if (connection.last_status === "OK") {
return (
<Badge variant="outline" className="gap-1 text-green-600">
<CheckCircle2 className="h-3 w-3" /> Healthy
</Badge>
);
}
if (connection.last_status) {
return (
<Badge variant="outline" className="gap-1 text-destructive">
<XCircle className="h-3 w-3" /> {connection.last_status}
</Badge>
);
}
return <Badge variant="secondary">Not tested</Badge>;
}
function flattenModels(connections: ConnectionRead[]) {
return connections.flatMap((connection) =>
connection.models.map((model) => ({
@ -110,7 +94,6 @@ function ConnectionCard({ connection }: { connection: ConnectionRead }) {
</div>
</div>
<div className="flex shrink-0 items-center gap-2">
<StatusBadge connection={connection} />
<ConnectionSettingsDialog connection={connection} providerLabel={providerLabel} />
<AlertDialog>
<AlertDialogTrigger asChild>
@ -156,6 +139,7 @@ export function ModelConnectionsSettings({ searchSpaceId }: { searchSpaceId: num
const [{ data: roles }] = useAtom(modelRolesAtom);
const createConnection = useAtomValue(createModelConnectionMutationAtom);
const previewModels = useAtomValue(previewConnectionModelsMutationAtom);
const testPreviewModel = useAtomValue(testPreviewModelMutationAtom);
const updateRoles = useAtomValue(updateModelRolesMutationAtom);
const [isAddProviderOpen, setIsAddProviderOpen] = useState(false);
@ -220,9 +204,7 @@ export function ModelConnectionsSettings({ searchSpaceId }: { searchSpaceId: num
});
}
// Each provider connect form builds its own credential payload; the backend
// resolver (`to_litellm`) forwards `extra.litellm_params` straight to LiteLLM.
function handleCreate(draft: ConnectionDraft) {
function connectionModelsForDraft(draft: ConnectionDraft) {
const models = [...connectModels];
if (draft.seedModelId && !models.some((model) => model.model_id === draft.seedModelId)) {
models.push({
@ -233,22 +215,46 @@ export function ModelConnectionsSettings({ searchSpaceId }: { searchSpaceId: num
metadata: {},
});
}
return models;
}
createConnection.mutate(
function representativeTestModel(models: ModelSelection[]) {
const enabledModels = models.filter((model) => model.enabled);
return enabledModels.find((model) => capability(model, "chat")) ?? enabledModels[0];
}
// Each provider connect form builds its own credential payload; the backend
// resolver (`to_litellm`) forwards `extra.litellm_params` straight to LiteLLM.
function handleCreate(draft: ConnectionDraft) {
const models = connectionModelsForDraft(draft);
const testModel = representativeTestModel(models);
if (!testModel) {
toast.error("Select at least one model before connecting");
return;
}
const request = {
provider,
base_url: draft.base_url,
api_key: draft.api_key,
scope: "SEARCH_SPACE" as const,
search_space_id: searchSpaceId,
extra: draft.extra,
enabled: true,
models,
};
testPreviewModel.mutate(
{ ...request, model_id: testModel.model_id },
{
provider,
base_url: draft.base_url,
api_key: draft.api_key,
scope: "SEARCH_SPACE",
search_space_id: searchSpaceId,
extra: draft.extra,
enabled: true,
models,
},
{
onSuccess: () => {
setIsAddProviderOpen(false);
resetConnectState();
onSuccess: (result) => {
if (!result.ok) return;
createConnection.mutate(request, {
onSuccess: () => {
setIsAddProviderOpen(false);
resetConnectState();
},
});
},
}
);
@ -380,7 +386,7 @@ export function ModelConnectionsSettings({ searchSpaceId }: { searchSpaceId: num
onOpenChange={handleConnectOpenChange}
provider={provider}
selectedProvider={selectedProvider}
isPending={createConnection.isPending}
isPending={createConnection.isPending || testPreviewModel.isPending}
onSubmit={handleCreate}
previewModels={connectModels}
isPreviewingModels={previewModels.isPending}

View file

@ -5,9 +5,9 @@ import {
addManualModelMutationAtom,
bulkUpdateModelsMutationAtom,
discoverConnectionModelsMutationAtom,
testPreviewModelMutationAtom,
updateModelConnectionMutationAtom,
updateModelMutationAtom,
verifyModelConnectionMutationAtom,
} from "@/atoms/model-connections/model-connections-mutation.atoms";
import { Button } from "@/components/ui/button";
import {
@ -26,7 +26,7 @@ import type {
ConnectionRead,
ConnectionUpdateRequest,
} from "@/contracts/types/model-connections.types";
import type { SelectableModel } from "./model-utils";
import { capability, type SelectableModel } from "./model-utils";
import { ModelsSelectionPanel } from "./models-selection-panel";
import { providerIcon } from "./provider-metadata";
@ -39,8 +39,8 @@ export function ConnectionSettingsDialog({
connection,
providerLabel,
}: ConnectionSettingsDialogProps) {
const verifyConnection = useAtomValue(verifyModelConnectionMutationAtom);
const discoverModels = useAtomValue(discoverConnectionModelsMutationAtom);
const testPreviewModel = useAtomValue(testPreviewModelMutationAtom);
const updateConnection = useAtomValue(updateModelConnectionMutationAtom);
const addManualModel = useAtomValue(addManualModelMutationAtom);
const updateModel = useAtomValue(updateModelMutationAtom);
@ -81,11 +81,45 @@ export function ConnectionSettingsDialog({
if (apiKeyDraft.trim() !== (connection.api_key ?? "")) {
data.api_key = apiKeyDraft.trim() || null;
}
const apiKeyForTest = Object.hasOwn(data, "api_key")
? (data.api_key ?? null)
: (connection.api_key ?? null);
updateConnection.mutate(
{ id: connection.id, data },
const enabledModels = connection.models.filter((model) => model.enabled);
const testModel =
enabledModels.find((model) => capability(model, "chat")) ?? enabledModels[0];
if (!testModel) {
updateConnection.mutate(
{ id: connection.id, data },
{
onSuccess: () => setApiKeyDraft(""),
}
);
return;
}
testPreviewModel.mutate(
{
onSuccess: () => setApiKeyDraft(""),
provider: connection.provider,
base_url: data.base_url,
api_key: apiKeyForTest,
scope: "SEARCH_SPACE",
search_space_id: connection.search_space_id,
extra: connection.extra ?? {},
enabled: connection.enabled,
models: [],
model_id: testModel.model_id,
},
{
onSuccess: (result) => {
if (!result.ok) return;
updateConnection.mutate(
{ id: connection.id, data },
{
onSuccess: () => setApiKeyDraft(""),
}
);
},
}
);
}
@ -219,26 +253,15 @@ export function ConnectionSettingsDialog({
onBulkToggle={handleBulkToggle}
/>
{connection.last_status && connection.last_status !== "OK" ? (
<p className="rounded-lg bg-amber-500/10 px-3 py-2 text-sm text-amber-600 dark:text-amber-500">
{connection.last_error || "Could not list models."} Chat may still work; add model
IDs manually if discovery is unavailable.
</p>
) : null}
</div>
</div>
<DialogFooter className="shrink-0 border-t bg-popover px-6 py-4">
<Button
variant="secondary"
onClick={() => verifyConnection.mutate(connection.id)}
disabled={verifyConnection.isPending}
>
Test
</Button>
<Button
onClick={saveConnectionSettings}
disabled={updateConnection.isPending || !hasConnectionChanges}
disabled={
updateConnection.isPending || testPreviewModel.isPending || !hasConnectionChanges
}
>
Update
</Button>

View file

@ -94,6 +94,8 @@ export function ProviderConnectDialog({
})();
const canRefreshModels = !isAzure && !isVertex && (!isBedrock || canSubmit);
const hasEnabledModel = previewModels.some((model) => model.enabled) || Boolean(currentDraft.seedModelId);
const canConnect = canSubmit && hasEnabledModel;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@ -134,7 +136,7 @@ export function ProviderConnectDialog({
<ConnectFormFooter
onCancel={() => onOpenChange(false)}
onSubmit={() => onSubmit(currentDraft)}
canSubmit={canSubmit}
canSubmit={canConnect}
isPending={isPending}
/>
</DialogContent>