Merge pull request #1551 from AnishSarkar22/fix/models

feat(model connection): remove manual model creation
This commit is contained in:
Rohan Verma 2026-07-24 13:39:01 -07:00 committed by GitHub
commit 041dfaf5f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 74 additions and 244 deletions

View file

@ -24,7 +24,6 @@ from app.schemas import (
ConnectionRead, ConnectionRead,
ConnectionUpdate, ConnectionUpdate,
LlmSetupStatusRead, LlmSetupStatusRead,
ModelCreate,
ModelPreviewRead, ModelPreviewRead,
ModelProviderRead, ModelProviderRead,
ModelRead, ModelRead,
@ -619,49 +618,6 @@ async def discover_connection_models(
return [_model_read(model) for model in conn.models] return [_model_read(model) for model in conn.models]
@router.post("/model-connections/{connection_id}/models", response_model=ModelRead)
async def add_manual_model(
connection_id: int,
data: ModelCreate,
session: AsyncSession = Depends(get_async_session),
auth: AuthContext = Depends(get_auth_context),
):
conn = await _load_connection(session, connection_id)
await _assert_connection_access(
session, auth, conn, Permission.LLM_CONFIGS_UPDATE.value
)
model_id = data.model_id.strip()
if not model_id:
raise HTTPException(status_code=400, detail="model_id is required")
if any(existing.model_id == model_id for existing in conn.models):
raise HTTPException(
status_code=400, detail="Model already exists on this connection"
)
capabilities = derive_capabilities(conn, model_id)
model = Model(
connection_id=conn.id,
model_id=model_id,
display_name=data.display_name or None,
source=ModelSource.MANUAL,
capabilities_override={},
enabled=True,
catalog={},
)
_apply_model_facts(model, capabilities)
session.add(model)
await session.commit()
await session.refresh(model)
conn = await _load_connection(session, connection_id)
await _default_unset_roles(session, conn, list(conn.models))
if conn.workspace_id is not None:
await _clear_invalid_roles(session, conn.workspace_id)
await session.commit()
await session.refresh(model)
return _model_read(model)
@router.patch( @router.patch(
"/model-connections/{connection_id}/models", response_model=list[ModelRead] "/model-connections/{connection_id}/models", response_model=list[ModelRead]
) )

View file

@ -44,7 +44,6 @@ from .model_connections import (
ConnectionRead, ConnectionRead,
ConnectionUpdate, ConnectionUpdate,
LlmSetupStatusRead, LlmSetupStatusRead,
ModelCreate,
ModelPreviewRead, ModelPreviewRead,
ModelProviderRead, ModelProviderRead,
ModelRead, ModelRead,
@ -205,7 +204,6 @@ __all__ = [
"MembershipRead", "MembershipRead",
"MembershipReadWithUser", "MembershipReadWithUser",
"MembershipUpdate", "MembershipUpdate",
"ModelCreate",
"ModelPreviewRead", "ModelPreviewRead",
"ModelProviderRead", "ModelProviderRead",
"ModelRead", "ModelRead",

View file

@ -93,17 +93,6 @@ class ConnectionUpdate(BaseModel):
enabled: bool | None = None enabled: bool | None = None
class ModelCreate(BaseModel):
"""Manually register a model id on a connection.
For providers without a usable ``/models`` endpoint (Perplexity, MiniMax,
Azure deployments, etc.) or to pin a single model from a noisy provider.
"""
model_id: str = Field(..., max_length=255)
display_name: str | None = Field(None, max_length=255)
class ModelUpdate(BaseModel): class ModelUpdate(BaseModel):
display_name: str | None = Field(None, max_length=255) display_name: str | None = Field(None, max_length=255)
enabled: bool | None = None enabled: bool | None = None

View file

@ -203,11 +203,6 @@ def _discovery_error_message(conn: Connection, exc: httpx.HTTPError) -> str:
return _docker_hint(base_url, exc) return _docker_hint(base_url, exc)
def _allowlist(conn: Connection) -> set[str]:
raw = (conn.extra or {}).get("model_ids") or []
return {str(item).strip() for item in raw if str(item).strip()}
def _litellm_info(model_string: str, model_id: str) -> dict[str, Any]: def _litellm_info(model_string: str, model_id: str) -> dict[str, Any]:
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
info = litellm.get_model_info(model=model_string) info = litellm.get_model_info(model=model_string)
@ -449,7 +444,6 @@ async def _discover_bedrock_models(conn: Connection) -> list[dict[str, Any]]:
async def discover_models(conn: Connection) -> list[dict[str, Any]]: async def discover_models(conn: Connection) -> list[dict[str, Any]]:
allowlist = _allowlist(conn)
spec = spec_for(conn.provider) spec = spec_for(conn.provider)
try: try:
@ -472,8 +466,6 @@ async def discover_models(conn: Connection) -> list[dict[str, Any]]:
except httpx.HTTPError as exc: except httpx.HTTPError as exc:
raise ModelDiscoveryError(_discovery_error_message(conn, exc)) from exc raise ModelDiscoveryError(_discovery_error_message(conn, exc)) from exc
if allowlist:
results = [item for item in results if item["model_id"] in allowlist]
return results return results

View file

@ -59,6 +59,10 @@ def to_litellm(
kwargs: dict[str, Any] = {} kwargs: dict[str, Any] = {}
if api_key: if api_key:
kwargs["api_key"] = api_key kwargs["api_key"] = api_key
elif provider == "lm_studio":
# LiteLLM's OpenAI-compatible adapter expects an api_key value even
# when LM Studio accepts unauthenticated local requests.
kwargs["api_key"] = "not-needed"
prefix = spec.litellm_prefix or str(provider) prefix = spec.litellm_prefix or str(provider)
model_string = f"{prefix}/{model_id}" if prefix else model_id model_string = f"{prefix}/{model_id}" if prefix else model_id

View file

@ -64,6 +64,22 @@ def test_openai_compatible_resolver_uses_explicit_api_base() -> None:
assert ensure_v1("http://example.com/v1") == "http://example.com/v1" assert ensure_v1("http://example.com/v1") == "http://example.com/v1"
def test_lm_studio_resolver_supplies_dummy_api_key_when_empty() -> None:
model, kwargs = to_litellm(
{
"provider": "lm_studio",
"base_url": "http://host.docker.internal:1234/v1",
"api_key": None,
"extra": {},
},
"tinyllama-1.1b-chat-v0.6",
)
assert model == "openai/tinyllama-1.1b-chat-v0.6"
assert kwargs["api_base"] == "http://host.docker.internal:1234/v1"
assert kwargs["api_key"] == "not-needed"
def test_openai_compatible_raw_resolver_does_not_append_v1() -> None: def test_openai_compatible_raw_resolver_does_not_append_v1() -> None:
model, kwargs = to_litellm( model, kwargs = to_litellm(
{ {

View file

@ -54,7 +54,7 @@
--sidebar-ring: oklch(0.708 0 0); --sidebar-ring: oklch(0.708 0 0);
--main-panel: var(--panel); --main-panel: var(--panel);
--syntax-bg: #f5f5f5; --syntax-bg: #f5f5f5;
--brand: oklch(0.623 0.214 259.815); --brand: oklch(0.546 0.245 262.881);
--highlight: oklch(0.852 0.199 91.936); --highlight: oklch(0.852 0.199 91.936);
} }
@ -103,7 +103,7 @@ html[data-surfsense-auth-type="LOCAL"] .runtime-auth-google {
--sidebar-ring: oklch(0.439 0 0); --sidebar-ring: oklch(0.439 0 0);
--main-panel: var(--panel); --main-panel: var(--panel);
--syntax-bg: #1e1e1e; --syntax-bg: #1e1e1e;
--brand: oklch(0.707 0.165 254.624); --brand: oklch(0.546 0.245 262.881);
--highlight: oklch(0.852 0.199 91.936); --highlight: oklch(0.852 0.199 91.936);
} }

View file

@ -4,7 +4,6 @@ import type {
ConnectionCreateRequest, ConnectionCreateRequest,
ConnectionRead, ConnectionRead,
ConnectionUpdateRequest, ConnectionUpdateRequest,
ModelCreateRequest,
ModelPreviewRead, ModelPreviewRead,
ModelRead, ModelRead,
ModelRoles, ModelRoles,
@ -117,13 +116,7 @@ export const verifyModelConnectionMutationAtom = atomWithMutation((get) => {
if (result.ok) { if (result.ok) {
toast.success("Connection verified"); toast.success("Connection verified");
} else { } else {
// Non-fatal: many providers lack a /models endpoint yet still serve toast.warning(result.message || "Couldn't verify this connection.");
// chat. Guide the user to add model IDs manually instead of alarming.
toast.warning(
result.message
? `${result.message} Chat may still work — add model IDs manually.`
: "Couldn't list models. Chat may still work — add model IDs manually."
);
} }
invalidateModelConnections(workspaceId); invalidateModelConnections(workspaceId);
}, },
@ -170,20 +163,6 @@ export const testPreviewModelMutationAtom = atomWithMutation(() => {
}; };
}); });
export const addManualModelMutationAtom = atomWithMutation((get) => {
const workspaceId = Number(get(activeWorkspaceIdAtom));
return {
mutationKey: ["models", "add-manual"],
mutationFn: ({ connectionId, data }: { connectionId: number; data: ModelCreateRequest }) =>
modelConnectionsApiService.addManualModel(connectionId, data),
onSuccess: () => {
toast.success("Model added");
invalidateModelConnections(workspaceId);
},
onError: (error: Error) => toast.error(error.message || "Failed to add model"),
};
});
export const updateModelMutationAtom = atomWithMutation((get) => { export const updateModelMutationAtom = atomWithMutation((get) => {
const workspaceId = Number(get(activeWorkspaceIdAtom)); const workspaceId = Number(get(activeWorkspaceIdAtom));
return { return {

View file

@ -113,7 +113,7 @@ export function BedrockConnectForm({ onDraftChange }: ProviderConnectFormProps)
</p> </p>
) : null} ) : null}
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Add Bedrock model IDs from the provider&apos;s settings after connecting. After entering credentials, refresh models to discover the Bedrock catalog for this region.
</p> </p>
</div> </div>
); );

View file

@ -2,7 +2,6 @@ import { useAtomValue } from "jotai";
import { Eye, EyeOff, Settings } from "lucide-react"; import { Eye, EyeOff, Settings } from "lucide-react";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { import {
addManualModelMutationAtom,
bulkUpdateModelsMutationAtom, bulkUpdateModelsMutationAtom,
discoverConnectionModelsMutationAtom, discoverConnectionModelsMutationAtom,
testPreviewModelMutationAtom, testPreviewModelMutationAtom,
@ -50,26 +49,17 @@ export function ConnectionSettingsDialog({
const discoverModels = useAtomValue(discoverConnectionModelsMutationAtom); const discoverModels = useAtomValue(discoverConnectionModelsMutationAtom);
const testPreviewModel = useAtomValue(testPreviewModelMutationAtom); const testPreviewModel = useAtomValue(testPreviewModelMutationAtom);
const updateConnection = useAtomValue(updateModelConnectionMutationAtom); const updateConnection = useAtomValue(updateModelConnectionMutationAtom);
const addManualModel = useAtomValue(addManualModelMutationAtom);
const bulkUpdateModels = useAtomValue(bulkUpdateModelsMutationAtom); const bulkUpdateModels = useAtomValue(bulkUpdateModelsMutationAtom);
const allowlist = Array.isArray(connection.extra?.model_ids)
? (connection.extra.model_ids as string[])
: [];
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [baseUrlDraft, setBaseUrlDraft] = useState(connection.base_url ?? ""); const [baseUrlDraft, setBaseUrlDraft] = useState(connection.base_url ?? "");
const [apiKeyDraft, setApiKeyDraft] = useState(""); const [apiKeyDraft, setApiKeyDraft] = useState("");
const [showApiKey, setShowApiKey] = useState(false); const [showApiKey, setShowApiKey] = useState(false);
const [allowlistText, setAllowlistText] = useState(allowlist.join(", "));
const [isSavingConnectionSettings, setIsSavingConnectionSettings] = useState(false); const [isSavingConnectionSettings, setIsSavingConnectionSettings] = useState(false);
const [draftEnabledModelIds, setDraftEnabledModelIds] = useState(() => const [draftEnabledModelIds, setDraftEnabledModelIds] = useState(() =>
enabledModelIds(connection.models) enabledModelIds(connection.models)
); );
const isLocal =
connection.provider === "ollama_chat" ||
connection.provider === "lm_studio" ||
!connection.base_url?.startsWith("https");
const hasConnectionChanges = const hasConnectionChanges =
baseUrlDraft.trim() !== (connection.base_url ?? "") || baseUrlDraft.trim() !== (connection.base_url ?? "") ||
apiKeyDraft.trim() !== (connection.api_key ?? ""); apiKeyDraft.trim() !== (connection.api_key ?? "");
@ -93,7 +83,6 @@ export function ConnectionSettingsDialog({
setBaseUrlDraft(connection.base_url ?? ""); setBaseUrlDraft(connection.base_url ?? "");
setApiKeyDraft(connection.api_key ?? ""); setApiKeyDraft(connection.api_key ?? "");
setShowApiKey(false); setShowApiKey(false);
setAllowlistText(allowlist.join(", "));
setIsSavingConnectionSettings(false); setIsSavingConnectionSettings(false);
setDraftEnabledModelIds(enabledModelIds(connection.models)); setDraftEnabledModelIds(enabledModelIds(connection.models));
} }
@ -168,17 +157,6 @@ export function ConnectionSettingsDialog({
} }
} }
function saveAllowlist() {
const ids = allowlistText
.split(",")
.map((value) => value.trim())
.filter(Boolean);
updateConnection.mutate({
id: connection.id,
data: { extra: { ...(connection.extra ?? {}), model_ids: ids } },
});
}
function handleToggleModel(model: SelectableModel, enabled: boolean) { function handleToggleModel(model: SelectableModel, enabled: boolean) {
if (typeof model.id !== "number") return; if (typeof model.id !== "number") return;
const modelId = model.id; const modelId = model.id;
@ -276,41 +254,15 @@ export function ConnectionSettingsDialog({
</div> </div>
</div> </div>
{!isLocal ? (
<div className="space-y-2">
<Label className="text-xs">Model IDs filter (optional)</Label>
<div className="flex gap-2">
<Input
value={allowlistText}
onChange={(event) => setAllowlistText(event.target.value)}
placeholder="Comma-separated, e.g. anthropic/claude-sonnet-4-5, google/gemini-2.5-pro"
/>
<Button size="sm" onClick={saveAllowlist} disabled={updateConnection.isPending}>
Save filter
</Button>
</div>
<p className="text-xs text-muted-foreground">
Leave empty to discover all models. Recommended for providers with large catalogs.
</p>
</div>
) : null}
<Separator className="bg-muted-foreground/20" /> <Separator className="bg-muted-foreground/20" />
<ModelsSelectionPanel <ModelsSelectionPanel
models={draftModels} models={draftModels}
isRefreshing={discoverModels.isPending} isRefreshing={discoverModels.isPending}
isAddingManual={addManualModel.isPending}
isUpdatingModel={isSavingConnectionSettings} isUpdatingModel={isSavingConnectionSettings}
isBulkUpdating={isSavingConnectionSettings || bulkUpdateModels.isPending} isBulkUpdating={isSavingConnectionSettings || bulkUpdateModels.isPending}
refreshLabel={`Refresh ${providerLabel} models`} refreshLabel={`Refresh ${providerLabel} models`}
onRefresh={() => discoverModels.mutate(connection.id)} onRefresh={() => discoverModels.mutate(connection.id)}
onAddManual={(modelId) =>
addManualModel.mutate({
connectionId: connection.id,
data: { model_id: modelId },
})
}
onToggleModel={handleToggleModel} onToggleModel={handleToggleModel}
onBulkToggle={handleBulkToggle} onBulkToggle={handleBulkToggle}
/> />

View file

@ -216,22 +216,6 @@ export function ModelProviderConnectionsPanel({
); );
} }
function addConnectModel(modelId: string) {
setConnectModels((current) => {
if (current.some((model) => model.model_id === modelId)) return current;
return [
...current,
{
model_id: modelId,
display_name: modelId,
source: "MANUAL",
enabled: true,
metadata: {},
},
];
});
}
function toggleConnectModel(model: SelectableModel, enabled: boolean) { function toggleConnectModel(model: SelectableModel, enabled: boolean) {
setConnectModels((current) => setConnectModels((current) =>
current.map((item) => (item.model_id === model.model_id ? { ...item, enabled } : item)) current.map((item) => (item.model_id === model.model_id ? { ...item, enabled } : item))
@ -292,7 +276,6 @@ export function ModelProviderConnectionsPanel({
previewModels={connectModels} previewModels={connectModels}
isPreviewingModels={previewModels.isPending} isPreviewingModels={previewModels.isPending}
onPreviewModels={refreshConnectModels} onPreviewModels={refreshConnectModels}
onAddPreviewModel={addConnectModel}
onTogglePreviewModel={toggleConnectModel} onTogglePreviewModel={toggleConnectModel}
onBulkTogglePreviewModels={bulkToggleConnectModels} onBulkTogglePreviewModels={bulkToggleConnectModels}
/> />

View file

@ -1,9 +1,7 @@
import { RefreshCw } from "lucide-react"; import { RefreshCw } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { import {
capability, capability,
@ -18,14 +16,12 @@ interface ModelsSelectionPanelProps {
models: SelectableModel[]; models: SelectableModel[];
description?: string; description?: string;
emptyMessage?: string; emptyMessage?: string;
manualInputPlaceholder?: string;
refreshLabel?: string; refreshLabel?: string;
isRefreshing?: boolean; isRefreshing?: boolean;
isAddingManual?: boolean; isRefreshDisabled?: boolean;
isUpdatingModel?: boolean; isUpdatingModel?: boolean;
isBulkUpdating?: boolean; isBulkUpdating?: boolean;
onRefresh?: () => void; onRefresh?: () => void;
onAddManual?: (modelId: string) => void;
onToggleModel?: (model: SelectableModel, enabled: boolean) => void; onToggleModel?: (model: SelectableModel, enabled: boolean) => void;
onBulkToggle?: (models: SelectableModel[], enabled: boolean) => void; onBulkToggle?: (models: SelectableModel[], enabled: boolean) => void;
} }
@ -34,18 +30,15 @@ export function ModelsSelectionPanel({
models, models,
description = "Select models to make available for this provider.", description = "Select models to make available for this provider.",
emptyMessage = "No models available.", emptyMessage = "No models available.",
manualInputPlaceholder = "Add a model ID manually",
refreshLabel = "Refresh models", refreshLabel = "Refresh models",
isRefreshing = false, isRefreshing = false,
isAddingManual = false, isRefreshDisabled = false,
isUpdatingModel = false, isUpdatingModel = false,
isBulkUpdating = false, isBulkUpdating = false,
onRefresh, onRefresh,
onAddManual,
onToggleModel, onToggleModel,
onBulkToggle, onBulkToggle,
}: ModelsSelectionPanelProps) { }: ModelsSelectionPanelProps) {
const [manualModelId, setManualModelId] = useState("");
const [modelFilter, setModelFilter] = useState<ModelCapabilityFilter | null>(null); const [modelFilter, setModelFilter] = useState<ModelCapabilityFilter | null>(null);
const filteredModels = modelFilter const filteredModels = modelFilter
@ -54,13 +47,6 @@ export function ModelsSelectionPanel({
const allFilteredModelsEnabled = const allFilteredModelsEnabled =
filteredModels.length > 0 && filteredModels.every((model) => model.enabled); filteredModels.length > 0 && filteredModels.every((model) => model.enabled);
function addModel() {
const modelId = manualModelId.trim();
if (!modelId || !onAddManual) return;
onAddManual(modelId);
setManualModelId("");
}
function toggleFilteredModels() { function toggleFilteredModels() {
const nextEnabled = !allFilteredModelsEnabled; const nextEnabled = !allFilteredModelsEnabled;
const changedModels = filteredModels.filter((model) => model.enabled !== nextEnabled); const changedModels = filteredModels.filter((model) => model.enabled !== nextEnabled);
@ -91,7 +77,7 @@ export function ModelsSelectionPanel({
size="icon" size="icon"
type="button" type="button"
onClick={onRefresh} onClick={onRefresh}
disabled={isRefreshing} disabled={isRefreshing || isRefreshDisabled}
aria-label={refreshLabel} aria-label={refreshLabel}
> >
<RefreshCw className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} /> <RefreshCw className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
@ -100,32 +86,6 @@ export function ModelsSelectionPanel({
</div> </div>
</div> </div>
{onAddManual ? (
<div className="flex gap-2">
<Input
value={manualModelId}
onChange={(event) => setManualModelId(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
addModel();
}
}}
placeholder={manualInputPlaceholder}
/>
<Button
size="sm"
type="button"
onClick={addModel}
disabled={isAddingManual || !manualModelId.trim()}
className="relative min-w-[88px]"
>
<span className={isAddingManual ? "opacity-0" : ""}>Add model</span>
{isAddingManual ? <Spinner size="xs" className="absolute" /> : null}
</Button>
</div>
) : null}
{models.length > 0 ? ( {models.length > 0 ? (
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<span className="text-xs font-medium text-muted-foreground">Filter models</span> <span className="text-xs font-medium text-muted-foreground">Filter models</span>
@ -139,21 +99,44 @@ export function ModelsSelectionPanel({
type="button" type="button"
variant="secondary" variant="secondary"
size="sm" size="sm"
className={`h-7 rounded-full px-3 text-xs ${isActive ? "" : "opacity-80"}`} className={`h-7 rounded-full px-3 text-xs ${
isActive
? "bg-brand text-white hover:bg-brand/90"
: "opacity-80"
}`}
onClick={() => setModelFilter(isActive ? null : filter.key)} onClick={() => setModelFilter(isActive ? null : filter.key)}
> >
{filter.label} {filter.label}
<span className="ml-1 text-muted-foreground">{count}</span> <span className={`ml-1 ${isActive ? "text-white/80" : "text-muted-foreground"}`}>
{count}
</span>
</Button> </Button>
); );
})} })}
</div> </div>
) : null} ) : null}
<div className="h-80 overflow-y-auto rounded-xl border bg-muted/20 p-2"> <div
className={`overflow-y-auto rounded-xl border bg-muted/20 p-2 ${
models.length === 0 ? "h-auto border-dashed border-border/100" : "h-80"
}`}
>
{models.length === 0 ? ( {models.length === 0 ? (
<div className="rounded-lg px-3 py-6 text-center text-sm text-muted-foreground"> <div className="flex flex-col items-center gap-3 rounded-lg px-3 py-6 text-center text-sm text-muted-foreground">
{emptyMessage} {emptyMessage}
{onRefresh ? (
<Button
variant="secondary"
size="sm"
type="button"
onClick={onRefresh}
disabled={isRefreshing || isRefreshDisabled}
className="relative"
>
<span className={isRefreshing ? "opacity-0" : ""}>Reload models</span>
{isRefreshing ? <Spinner size="sm" className="absolute" /> : null}
</Button>
) : null}
</div> </div>
) : null} ) : null}
{filteredModels.length === 0 && modelFilter ? ( {filteredModels.length === 0 && modelFilter ? (
@ -169,21 +152,17 @@ export function ModelsSelectionPanel({
{filteredModels.map((model) => ( {filteredModels.map((model) => (
<div <div
key={model.id ?? model.model_id} key={model.id ?? model.model_id}
className="flex items-center gap-3 rounded-lg px-3 py-2 transition-colors hover:bg-background" className="flex items-center gap-3 rounded-lg px-3 py-2 transition-colors hover:bg-popover"
> >
<Checkbox <Checkbox
checked={model.enabled} checked={model.enabled}
onCheckedChange={(checked) => onToggleModel?.(model, checked === true)} onCheckedChange={(checked) => onToggleModel?.(model, checked === true)}
disabled={!onToggleModel || isUpdatingModel} disabled={!onToggleModel || isUpdatingModel}
className="border-muted-foreground/20"
/> />
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-2 text-sm font-medium"> <div className="flex items-center gap-2 text-sm font-medium">
<span className="truncate">{modelLabel(model)}</span> <span className="truncate">{modelLabel(model)}</span>
{model.source === "MANUAL" ? (
<Badge variant="outline" className="text-[10px]">
manual
</Badge>
) : null}
</div> </div>
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
{capabilityLabels(model) || "No discovered capabilities"} {capabilityLabels(model) || "No discovered capabilities"}

View file

@ -33,7 +33,6 @@ interface ProviderConnectDialogProps {
previewModels?: SelectableModel[]; previewModels?: SelectableModel[];
isPreviewingModels?: boolean; isPreviewingModels?: boolean;
onPreviewModels?: (draft: ConnectionDraft) => void; onPreviewModels?: (draft: ConnectionDraft) => void;
onAddPreviewModel?: (modelId: string) => void;
onTogglePreviewModel?: (model: SelectableModel, enabled: boolean) => void; onTogglePreviewModel?: (model: SelectableModel, enabled: boolean) => void;
onBulkTogglePreviewModels?: (models: SelectableModel[], enabled: boolean) => void; onBulkTogglePreviewModels?: (models: SelectableModel[], enabled: boolean) => void;
} }
@ -53,7 +52,6 @@ export function ProviderConnectDialog({
previewModels = [], previewModels = [],
isPreviewingModels = false, isPreviewingModels = false,
onPreviewModels, onPreviewModels,
onAddPreviewModel,
onTogglePreviewModel, onTogglePreviewModel,
onBulkTogglePreviewModels, onBulkTogglePreviewModels,
}: ProviderConnectDialogProps) { }: ProviderConnectDialogProps) {
@ -94,7 +92,8 @@ export function ProviderConnectDialog({
return "Select the models to enable for this provider"; return "Select the models to enable for this provider";
})(); })();
const canRefreshModels = !isAzure && !isVertex && (!isBedrock || canSubmit); const supportsModelRefresh = !isAzure && !isVertex;
const canRefreshModels = supportsModelRefresh && canSubmit;
const hasEnabledModel = const hasEnabledModel =
previewModels.some((model) => model.enabled) || Boolean(currentDraft.seedModelId); previewModels.some((model) => model.enabled) || Boolean(currentDraft.seedModelId);
const canConnect = canSubmit && hasEnabledModel; const canConnect = canSubmit && hasEnabledModel;
@ -130,18 +129,22 @@ export function ProviderConnectDialog({
<DefaultConnectForm {...formProps} /> <DefaultConnectForm {...formProps} />
)} )}
<Separator className="bg-muted-foreground/20" /> {!isAzure ? (
<>
<Separator className="bg-muted-foreground/20" />
<ModelsSelectionPanel <ModelsSelectionPanel
models={previewModels} models={previewModels}
description={modelDescription} description={modelDescription}
isRefreshing={isPreviewingModels} isRefreshing={isPreviewingModels}
refreshLabel={`Refresh ${meta.name} models`} isRefreshDisabled={!canRefreshModels}
onRefresh={canRefreshModels ? () => onPreviewModels?.(currentDraft) : undefined} refreshLabel={`Refresh ${meta.name} models`}
onAddManual={onAddPreviewModel} onRefresh={supportsModelRefresh ? () => onPreviewModels?.(currentDraft) : undefined}
onToggleModel={onTogglePreviewModel} onToggleModel={onTogglePreviewModel}
onBulkToggle={onBulkTogglePreviewModels} onBulkToggle={onBulkTogglePreviewModels}
/> />
</>
) : null}
</div> </div>
<ConnectFormFooter <ConnectFormFooter
onCancel={() => onOpenChange(false)} onCancel={() => onOpenChange(false)}

View file

@ -111,7 +111,7 @@ export function VertexConnectForm({ onDraftChange }: ProviderConnectFormProps) {
</div> </div>
)} )}
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Add Vertex AI model IDs from the provider&apos;s settings after connecting. SurfSense will show supported Vertex AI models from the provider catalog.
</p> </p>
</div> </div>
); );

View file

@ -74,11 +74,6 @@ export const connectionUpdateRequest = z.object({
enabled: z.boolean().optional(), enabled: z.boolean().optional(),
}); });
export const modelCreateRequest = z.object({
model_id: z.string().min(1),
display_name: z.string().nullable().optional(),
});
export const modelUpdateRequest = z.object({ export const modelUpdateRequest = z.object({
display_name: z.string().nullable().optional(), display_name: z.string().nullable().optional(),
enabled: z.boolean().optional(), enabled: z.boolean().optional(),
@ -142,7 +137,6 @@ export type ConnectionRead = z.infer<typeof connectionRead>;
export type ConnectionCreateRequest = z.infer<typeof connectionCreateRequest>; export type ConnectionCreateRequest = z.infer<typeof connectionCreateRequest>;
export type ModelTestPreviewRequest = z.infer<typeof modelTestPreviewRequest>; export type ModelTestPreviewRequest = z.infer<typeof modelTestPreviewRequest>;
export type ConnectionUpdateRequest = z.infer<typeof connectionUpdateRequest>; export type ConnectionUpdateRequest = z.infer<typeof connectionUpdateRequest>;
export type ModelCreateRequest = z.infer<typeof modelCreateRequest>;
export type ModelUpdateRequest = z.infer<typeof modelUpdateRequest>; export type ModelUpdateRequest = z.infer<typeof modelUpdateRequest>;
export type ModelsBulkUpdateRequest = z.infer<typeof modelsBulkUpdateRequest>; export type ModelsBulkUpdateRequest = z.infer<typeof modelsBulkUpdateRequest>;
export type ModelRoles = z.infer<typeof modelRoles>; export type ModelRoles = z.infer<typeof modelRoles>;

View file

@ -10,7 +10,6 @@ import {
globalLlmConfigStatus, globalLlmConfigStatus,
type LlmSetupStatus, type LlmSetupStatus,
llmSetupStatus, llmSetupStatus,
type ModelCreateRequest,
type ModelPreviewRead, type ModelPreviewRead,
type ModelProviderRead, type ModelProviderRead,
type ModelRead, type ModelRead,
@ -18,7 +17,6 @@ import {
type ModelsBulkUpdateRequest, type ModelsBulkUpdateRequest,
type ModelTestPreviewRequest, type ModelTestPreviewRequest,
type ModelUpdateRequest, type ModelUpdateRequest,
modelCreateRequest,
modelListResponse, modelListResponse,
modelPreviewListResponse, modelPreviewListResponse,
modelProviderListResponse, modelProviderListResponse,
@ -139,19 +137,6 @@ class ModelConnectionsApiService {
}); });
}; };
addManualModel = async (
connectionId: number,
request: ModelCreateRequest
): Promise<ModelRead> => {
const parsed = modelCreateRequest.safeParse(request);
if (!parsed.success) {
throw new ValidationError(parsed.error.issues.map((issue) => issue.message).join(", "));
}
return baseApiService.post(`/api/v1/model-connections/${connectionId}/models`, modelRead, {
body: parsed.data,
});
};
updateModel = async (id: number, request: ModelUpdateRequest): Promise<ModelRead> => { updateModel = async (id: number, request: ModelUpdateRequest): Promise<ModelRead> => {
const parsed = modelUpdateRequest.safeParse(request); const parsed = modelUpdateRequest.safeParse(request);
if (!parsed.success) { if (!parsed.success) {