mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-23 06:45:27 +02:00
feat(ai): allow organizations to define custom AI provider presets
Organization admins can define their own AI providers (name + API URL) from the AI settings section. When at least one custom provider is defined, it replaces the built-in provider list in the Add AI form, the API URL is auto-filled and locked, and the backend rejects any configuration whose URL is not in the allowed list.
This commit is contained in:
@@ -54,6 +54,15 @@ export const apiUpdateAi = createSchema
|
||||
})
|
||||
.omit({ organizationId: true });
|
||||
|
||||
export const aiCustomProviderSchema = z.object({
|
||||
name: z.string().min(1, { message: "Name is required" }),
|
||||
apiUrl: z.string().url({ message: "Please enter a valid URL" }),
|
||||
});
|
||||
|
||||
export const apiSaveAiCustomProviders = z.object({
|
||||
providers: z.array(aiCustomProviderSchema),
|
||||
});
|
||||
|
||||
export const deploySuggestionSchema = z.object({
|
||||
environmentId: z.string().min(1),
|
||||
id: z.string().min(1),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { ai } from "@dokploy/server/db/schema";
|
||||
import { ai, organization } from "@dokploy/server/db/schema";
|
||||
import { aiCustomProviderSchema } from "@dokploy/server/db/schema/ai";
|
||||
import { selectAIProvider } from "@dokploy/server/utils/ai/select-ai-provider";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { generateText, Output } from "ai";
|
||||
@@ -48,9 +49,74 @@ export const getAiSettingById = async (aiId: string) => {
|
||||
return aiSetting;
|
||||
};
|
||||
|
||||
type AiCustomProvider = z.infer<typeof aiCustomProviderSchema>;
|
||||
|
||||
const parseOrgMetadata = (metadata: string | null) => {
|
||||
try {
|
||||
const parsed = JSON.parse(metadata || "{}");
|
||||
return typeof parsed === "object" && parsed !== null
|
||||
? (parsed as Record<string, unknown>)
|
||||
: {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const getCustomAiProviders = async (organizationId: string) => {
|
||||
const org = await db.query.organization.findFirst({
|
||||
where: eq(organization.id, organizationId),
|
||||
});
|
||||
const metadata = parseOrgMetadata(org?.metadata ?? null);
|
||||
const result = z
|
||||
.array(aiCustomProviderSchema)
|
||||
.safeParse(metadata.aiProviders);
|
||||
return result.success ? result.data : [];
|
||||
};
|
||||
|
||||
export const saveCustomAiProviders = async (
|
||||
organizationId: string,
|
||||
providers: AiCustomProvider[],
|
||||
) => {
|
||||
const org = await db.query.organization.findFirst({
|
||||
where: eq(organization.id, organizationId),
|
||||
});
|
||||
if (!org) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Organization not found",
|
||||
});
|
||||
}
|
||||
const metadata = parseOrgMetadata(org.metadata);
|
||||
metadata.aiProviders = providers;
|
||||
await db
|
||||
.update(organization)
|
||||
.set({ metadata: JSON.stringify(metadata) })
|
||||
.where(eq(organization.id, organizationId));
|
||||
return providers;
|
||||
};
|
||||
|
||||
const normalizeApiUrl = (url: string) => url.trim().replace(/\/+$/, "");
|
||||
|
||||
export const saveAiSettings = async (organizationId: string, settings: any) => {
|
||||
const aiId = settings.aiId;
|
||||
|
||||
if (settings.apiUrl) {
|
||||
const customProviders = await getCustomAiProviders(organizationId);
|
||||
if (customProviders.length > 0) {
|
||||
const isAllowed = customProviders.some(
|
||||
(provider) =>
|
||||
normalizeApiUrl(provider.apiUrl) === normalizeApiUrl(settings.apiUrl),
|
||||
);
|
||||
if (!isAllowed) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message:
|
||||
"This API URL is not in your organization's allowed AI providers",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return db
|
||||
.insert(ai)
|
||||
.values({
|
||||
|
||||
Reference in New Issue
Block a user