allow no provider on a resource

This commit is contained in:
miloschwartz
2026-08-05 15:43:42 -04:00
parent bcf6b86b84
commit 790daba796
11 changed files with 25 additions and 86 deletions
-1
View File
@@ -1739,7 +1739,6 @@
"aiResourceProvidersHelp": "Models must be defined on each provider. Model names cannot overlap across selected providers.", "aiResourceProvidersHelp": "Models must be defined on each provider. Model names cannot overlap across selected providers.",
"aiResourceProvidersSelect": "Select providers", "aiResourceProvidersSelect": "Select providers",
"aiResourceProvidersEmpty": "No AI providers found", "aiResourceProvidersEmpty": "No AI providers found",
"aiResourceProvidersRequired": "Select at least one AI provider",
"aiResourceProvidersUpdated": "Providers updated", "aiResourceProvidersUpdated": "Providers updated",
"aiResourceProvidersErrorUpdate": "Failed to update providers", "aiResourceProvidersErrorUpdate": "Failed to update providers",
"aiResourceAliasRequired": "Alias is required for inference resources", "aiResourceAliasRequired": "Alias is required for inference resources",
+1 -1
View File
@@ -394,7 +394,7 @@ async function createHttpResource(
const resolved = await resolveProviderAttachments({ const resolved = await resolveProviderAttachments({
orgId, orgId,
attachments: aiProviderInputs ?? [], attachments: aiProviderInputs ?? [],
requireAtLeastOne: true requireAtLeastOne: false
}); });
if (isInferenceFieldsError(resolved)) { if (isInferenceFieldsError(resolved)) {
return next(createHttpError(HttpCode.BAD_REQUEST, resolved.error)); return next(createHttpError(HttpCode.BAD_REQUEST, resolved.error));
@@ -77,8 +77,9 @@ export async function removeAiProviderFromResource(
const { providerId } = parsedBody.data; const { providerId } = parsedBody.data;
const parsedParams = const parsedParams = removeAiProviderFromResourceParamsSchema.safeParse(
removeAiProviderFromResourceParamsSchema.safeParse(req.params); req.params
);
if (!parsedParams.success) { if (!parsedParams.success) {
return next( return next(
createHttpError( createHttpError(
@@ -129,22 +130,15 @@ export async function removeAiProviderFromResource(
modelAccessMode: a.modelAccessMode modelAccessMode: a.modelAccessMode
})); }));
if (remaining.length === 0) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"At least one AI provider is required for inference-mode resources"
)
);
}
const attachments = await resolveProviderAttachments({ const attachments = await resolveProviderAttachments({
orgId: resource.orgId, orgId: resource.orgId,
attachments: remaining, attachments: remaining,
requireAtLeastOne: true requireAtLeastOne: false
}); });
if (isInferenceFieldsError(attachments)) { if (isInferenceFieldsError(attachments)) {
return next(createHttpError(HttpCode.BAD_REQUEST, attachments.error)); return next(
createHttpError(HttpCode.BAD_REQUEST, attachments.error)
);
} }
await setPublicResourceAiProviders(resourceId, attachments); await setPublicResourceAiProviders(resourceId, attachments);
@@ -27,7 +27,7 @@ registry.registerPath({
method: "post", method: "post",
path: "/resource/{resourceId}/ai-providers", path: "/resource/{resourceId}/ai-providers",
description: description:
"Replace the AI providers attached to an inference resource. At least one provider is required. Model keys must be unique across attached catalog providers.", "Replace the AI providers attached to an inference resource. An empty list clears all providers. Model keys must be unique across attached catalog providers.",
tags: [OpenAPITags.PublicResource], tags: [OpenAPITags.PublicResource],
request: { request: {
params: setResourceAiProvidersParamsSchema, params: setResourceAiProvidersParamsSchema,
@@ -113,7 +113,7 @@ export async function setResourceAiProviders(
const attachments = await resolveProviderAttachments({ const attachments = await resolveProviderAttachments({
orgId: resource.orgId, orgId: resource.orgId,
attachments: providers, attachments: providers,
requireAtLeastOne: true requireAtLeastOne: false
}); });
if (isInferenceFieldsError(attachments)) { if (isInferenceFieldsError(attachments)) {
return next( return next(
@@ -353,7 +353,7 @@ export async function createSiteResource(
const resolved = await resolveProviderAttachments({ const resolved = await resolveProviderAttachments({
orgId, orgId,
attachments: aiProviderInputs ?? [], attachments: aiProviderInputs ?? [],
requireAtLeastOne: true requireAtLeastOne: false
}); });
if (isInferenceFieldsError(resolved)) { if (isInferenceFieldsError(resolved)) {
return next( return next(
@@ -63,8 +63,9 @@ export async function removeAiProviderFromSiteResource(
next: NextFunction next: NextFunction
): Promise<any> { ): Promise<any> {
try { try {
const parsedBody = const parsedBody = removeAiProviderFromSiteResourceBodySchema.safeParse(
removeAiProviderFromSiteResourceBodySchema.safeParse(req.body); req.body
);
if (!parsedBody.success) { if (!parsedBody.success) {
return next( return next(
createHttpError( createHttpError(
@@ -128,22 +129,15 @@ export async function removeAiProviderFromSiteResource(
modelAccessMode: a.modelAccessMode modelAccessMode: a.modelAccessMode
})); }));
if (remaining.length === 0) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"At least one AI provider is required for inference-mode resources"
)
);
}
const attachments = await resolveProviderAttachments({ const attachments = await resolveProviderAttachments({
orgId: siteResource.orgId, orgId: siteResource.orgId,
attachments: remaining, attachments: remaining,
requireAtLeastOne: true requireAtLeastOne: false
}); });
if (isInferenceFieldsError(attachments)) { if (isInferenceFieldsError(attachments)) {
return next(createHttpError(HttpCode.BAD_REQUEST, attachments.error)); return next(
createHttpError(HttpCode.BAD_REQUEST, attachments.error)
);
} }
await setSiteResourceAiProviders(siteResourceId, attachments); await setSiteResourceAiProviders(siteResourceId, attachments);
@@ -27,7 +27,7 @@ registry.registerPath({
method: "post", method: "post",
path: "/site-resource/{siteResourceId}/ai-providers", path: "/site-resource/{siteResourceId}/ai-providers",
description: description:
"Replace the AI providers attached to an inference site resource. At least one provider is required. Model keys must be unique across attached catalog providers.", "Replace the AI providers attached to an inference site resource. An empty list clears all providers. Model keys must be unique across attached catalog providers.",
tags: [OpenAPITags.PrivateResource], tags: [OpenAPITags.PrivateResource],
request: { request: {
params: setSiteResourceAiProvidersParamsSchema, params: setSiteResourceAiProvidersParamsSchema,
@@ -115,7 +115,7 @@ export async function setSiteResourceAiProviders(
const attachments = await resolveProviderAttachments({ const attachments = await resolveProviderAttachments({
orgId: siteResource.orgId, orgId: siteResource.orgId,
attachments: providers, attachments: providers,
requireAtLeastOne: true requireAtLeastOne: false
}); });
if (isInferenceFieldsError(attachments)) { if (isInferenceFieldsError(attachments)) {
return next( return next(
@@ -58,11 +58,9 @@ export default function PrivateResourceProvidersPage() {
const formSchema = useMemo( const formSchema = useMemo(
() => () =>
z.object({ z.object({
providerIds: z providerIds: z.array(z.number().int().positive())
.array(z.number().int().positive())
.min(1, t("aiResourceProvidersRequired"))
}), }),
[t] []
); );
type FormValues = z.infer<typeof formSchema>; type FormValues = z.infer<typeof formSchema>;
@@ -58,11 +58,9 @@ export default function PublicResourceProvidersPage() {
const formSchema = useMemo( const formSchema = useMemo(
() => () =>
z.object({ z.object({
providerIds: z providerIds: z.array(z.number().int().positive())
.array(z.number().int().positive())
.min(1, t("aiResourceProvidersRequired"))
}), }),
[t] []
); );
type FormValues = z.infer<typeof formSchema>; type FormValues = z.infer<typeof formSchema>;
@@ -252,7 +252,6 @@ export default function Page() {
const [selectedProviders, setSelectedProviders] = useState< const [selectedProviders, setSelectedProviders] = useState<
SelectedAiProvider[] SelectedAiProvider[]
>([]); >([]);
const [showProvidersError, setShowProvidersError] = useState(false);
// SSH-specific state // SSH-specific state
const [sshServerMode, setSshServerMode] = useState<"standard" | "native">( const [sshServerMode, setSshServerMode] = useState<"standard" | "native">(
@@ -355,9 +354,6 @@ export default function Page() {
if (!availableTypes.includes(resourceType)) { if (!availableTypes.includes(resourceType)) {
setResourceType("http"); setResourceType("http");
} }
if (resourceType !== "inference") {
setShowProvidersError(false);
}
}, [availableTypes, resourceType]); }, [availableTypes, resourceType]);
const baseResourceFormSchema = useMemo( const baseResourceFormSchema = useMemo(
@@ -1440,14 +1436,6 @@ export default function Page() {
setSelectedProviders( setSelectedProviders(
providers providers
); );
if (
providers.length >
0
) {
setShowProvidersError(
false
);
}
}} }}
/> />
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
@@ -1455,13 +1443,6 @@ export default function Page() {
"aiResourceProvidersHelp" "aiResourceProvidersHelp"
)} )}
</p> </p>
{showProvidersError && (
<p className="text-sm text-destructive">
{t(
"aiResourceProvidersRequired"
)}
</p>
)}
</div> </div>
</SettingsFormCell> </SettingsFormCell>
</SettingsFormGrid> </SettingsFormGrid>
@@ -1493,16 +1474,6 @@ export default function Page() {
const tcpValid = !isHttpResource const tcpValid = !isHttpResource
? await tcpUdpForm.trigger() ? await tcpUdpForm.trigger()
: true; : true;
const providersValid =
resourceType !== "inference" ||
selectedProviders.length > 0;
if (
resourceType === "inference" &&
!providersValid
) {
setShowProvidersError(true);
}
if ( if (
resourceType === "ssh" && resourceType === "ssh" &&
@@ -1527,8 +1498,7 @@ export default function Page() {
baseValid && baseValid &&
domainValid && domainValid &&
tcpValid && tcpValid &&
bgValid && bgValid
providersValid
) { ) {
onSubmit(); onSubmit();
} }
-14
View File
@@ -467,13 +467,6 @@ export function createCreateFormSchema(t: TranslateFn) {
path: ["alias"] path: ["alias"]
}); });
} }
if (!data.providerIds || data.providerIds.length < 1) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("aiResourceProvidersRequired"),
path: ["providerIds"]
});
}
} }
if ( if (
data.mode !== "ssh" && data.mode !== "ssh" &&
@@ -639,13 +632,6 @@ export function createInferenceFormSchema(t: TranslateFn) {
path: ["alias"] path: ["alias"]
}); });
} }
if (!data.providerIds || data.providerIds.length < 1) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("aiResourceProvidersRequired"),
path: ["providerIds"]
});
}
}); });
} }