mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-04 21:45:26 +02:00
- Corrected the API URL for Z.AI by removing the trailing slash. - Modified the AI router mutation to include context and added access control to ensure users can only access their organization's AI settings. These changes improve the accuracy of the API integration and enhance security by enforcing organizational access restrictions.
381 lines
9.6 KiB
TypeScript
381 lines
9.6 KiB
TypeScript
import { IS_CLOUD } from "@dokploy/server/constants";
|
|
import {
|
|
apiCreateAi,
|
|
apiUpdateAi,
|
|
deploySuggestionSchema,
|
|
} from "@dokploy/server/db/schema/ai";
|
|
import {
|
|
createDomain,
|
|
createMount,
|
|
findEnvironmentById,
|
|
} from "@dokploy/server/index";
|
|
import {
|
|
deleteAiSettings,
|
|
getAiSettingById,
|
|
getAiSettingsByOrganizationId,
|
|
saveAiSettings,
|
|
suggestVariants,
|
|
} from "@dokploy/server/services/ai";
|
|
import { createComposeByTemplate } from "@dokploy/server/services/compose";
|
|
import {
|
|
addNewService,
|
|
checkServiceAccess,
|
|
} from "@dokploy/server/services/permission";
|
|
import { findProjectById } from "@dokploy/server/services/project";
|
|
import {
|
|
getProviderHeaders,
|
|
getProviderName,
|
|
selectAIProvider,
|
|
type Model,
|
|
} from "@dokploy/server/utils/ai/select-ai-provider";
|
|
import { TRPCError } from "@trpc/server";
|
|
import { generateText } from "ai";
|
|
import { z } from "zod";
|
|
import { slugify } from "@/lib/slug";
|
|
import {
|
|
adminProcedure,
|
|
createTRPCRouter,
|
|
protectedProcedure,
|
|
} from "@/server/api/trpc";
|
|
import { generatePassword } from "@/templates/utils";
|
|
|
|
export const aiRouter = createTRPCRouter({
|
|
one: adminProcedure
|
|
.input(z.object({ aiId: z.string() }))
|
|
.query(async ({ input }) => {
|
|
return await getAiSettingById(input.aiId);
|
|
}),
|
|
|
|
getModels: protectedProcedure
|
|
.input(z.object({ apiUrl: z.string().min(1), apiKey: z.string() }))
|
|
.query(async ({ input }) => {
|
|
try {
|
|
const providerName = getProviderName(input.apiUrl);
|
|
const headers = getProviderHeaders(input.apiUrl, input.apiKey);
|
|
let response = null;
|
|
switch (providerName) {
|
|
case "ollama":
|
|
response = await fetch(`${input.apiUrl}/api/tags`, { headers });
|
|
break;
|
|
case "gemini":
|
|
response = await fetch(
|
|
`${input.apiUrl}/models?key=${encodeURIComponent(input.apiKey)}`,
|
|
{ headers: {} },
|
|
);
|
|
break;
|
|
case "perplexity":
|
|
// Perplexity doesn't have a /models endpoint, return hardcoded list
|
|
return [
|
|
{
|
|
id: "sonar-deep-research",
|
|
object: "model",
|
|
created: Date.now(),
|
|
owned_by: "perplexity",
|
|
},
|
|
{
|
|
id: "sonar-reasoning-pro",
|
|
object: "model",
|
|
created: Date.now(),
|
|
owned_by: "perplexity",
|
|
},
|
|
{
|
|
id: "sonar-reasoning",
|
|
object: "model",
|
|
created: Date.now(),
|
|
owned_by: "perplexity",
|
|
},
|
|
{
|
|
id: "sonar-pro",
|
|
object: "model",
|
|
created: Date.now(),
|
|
owned_by: "perplexity",
|
|
},
|
|
{
|
|
id: "sonar",
|
|
object: "model",
|
|
created: Date.now(),
|
|
owned_by: "perplexity",
|
|
},
|
|
] as Model[];
|
|
case "zai":
|
|
return [
|
|
{
|
|
id: "glm-5",
|
|
object: "model",
|
|
created: Date.now(),
|
|
owned_by: "zai",
|
|
},
|
|
{
|
|
id: "glm-4.7",
|
|
object: "model",
|
|
created: Date.now(),
|
|
owned_by: "zai",
|
|
},
|
|
] as Model[];
|
|
case "minimax":
|
|
return [
|
|
{
|
|
id: "MiniMax-M2.7",
|
|
object: "model",
|
|
created: Date.now(),
|
|
owned_by: "minimax",
|
|
},
|
|
] as Model[];
|
|
default:
|
|
if (!input.apiKey)
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "API key must contain at least 1 character(s)",
|
|
});
|
|
response = await fetch(`${input.apiUrl}/models`, { headers });
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
throw new Error(`Failed to fetch models: ${errorText}`);
|
|
}
|
|
|
|
const res = await response.json();
|
|
|
|
if (Array.isArray(res)) {
|
|
return res.map((model) => ({
|
|
id: model.id || model.name,
|
|
object: "model",
|
|
created: Date.now(),
|
|
owned_by: "provider",
|
|
}));
|
|
}
|
|
|
|
if (res.models) {
|
|
return res.models.map((model: any) => ({
|
|
id: model.id || model.name,
|
|
object: "model",
|
|
created: Date.now(),
|
|
owned_by: "provider",
|
|
})) as Model[];
|
|
}
|
|
|
|
if (res.data) {
|
|
return res.data as Model[];
|
|
}
|
|
|
|
const possibleModels =
|
|
(Object.values(res).find(Array.isArray) as any[]) || [];
|
|
return possibleModels.map((model) => ({
|
|
id: model.id || model.name,
|
|
object: "model",
|
|
created: Date.now(),
|
|
owned_by: "provider",
|
|
})) as Model[];
|
|
} catch (error) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: error instanceof Error ? error?.message : `Error: ${error}`,
|
|
});
|
|
}
|
|
}),
|
|
create: adminProcedure.input(apiCreateAi).mutation(async ({ ctx, input }) => {
|
|
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
|
}),
|
|
|
|
update: adminProcedure.input(apiUpdateAi).mutation(async ({ ctx, input }) => {
|
|
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
|
}),
|
|
|
|
getAll: adminProcedure.query(async ({ ctx }) => {
|
|
return await getAiSettingsByOrganizationId(
|
|
ctx.session.activeOrganizationId,
|
|
);
|
|
}),
|
|
|
|
get: adminProcedure
|
|
.input(z.object({ aiId: z.string() }))
|
|
.query(async ({ input }) => {
|
|
return await getAiSettingById(input.aiId);
|
|
}),
|
|
|
|
delete: adminProcedure
|
|
.input(z.object({ aiId: z.string() }))
|
|
.mutation(async ({ input }) => {
|
|
return await deleteAiSettings(input.aiId);
|
|
}),
|
|
|
|
getEnabledProviders: protectedProcedure.query(async ({ ctx }) => {
|
|
const settings = await getAiSettingsByOrganizationId(
|
|
ctx.session.activeOrganizationId,
|
|
);
|
|
return settings
|
|
.filter((s) => s.isEnabled)
|
|
.map((s) => ({ aiId: s.aiId, name: s.name, model: s.model }));
|
|
}),
|
|
|
|
analyzeLogs: protectedProcedure
|
|
.input(
|
|
z.object({
|
|
aiId: z.string().min(1),
|
|
logs: z.string().min(1),
|
|
context: z.enum(["build", "runtime"]),
|
|
}),
|
|
)
|
|
.mutation(async ({ input, ctx }) => {
|
|
try {
|
|
const aiSettings = await getAiSettingById(input.aiId);
|
|
if (!aiSettings?.isEnabled) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "AI provider is not enabled",
|
|
});
|
|
}
|
|
|
|
if (aiSettings.organizationId !== ctx.session.activeOrganizationId) {
|
|
throw new TRPCError({
|
|
code: "FORBIDDEN",
|
|
message: "Access denied",
|
|
});
|
|
}
|
|
|
|
const provider = selectAIProvider(aiSettings);
|
|
const model = provider(aiSettings.model);
|
|
|
|
const contextLabel =
|
|
input.context === "build" ? "build/deployment" : "runtime/container";
|
|
|
|
const result = await generateText({
|
|
model,
|
|
prompt: `You are a DevOps engineer analyzing ${contextLabel} logs. Analyze the following logs and provide:
|
|
|
|
1. **Summary**: A brief summary of what's happening
|
|
2. **Issues Found**: Any errors, warnings, or problems detected
|
|
3. **Root Cause**: The most likely root cause if there are errors
|
|
4. **Suggested Fix**: Actionable steps to resolve the issues
|
|
|
|
Be concise and practical. Focus on the most important issues. If the logs look healthy, say so briefly.
|
|
|
|
Logs:
|
|
${input.logs}`,
|
|
});
|
|
|
|
return { analysis: result.text };
|
|
} catch (error) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message:
|
|
error instanceof Error
|
|
? error.message
|
|
: `Analysis failed: ${error}`,
|
|
});
|
|
}
|
|
}),
|
|
|
|
testConnection: protectedProcedure
|
|
.input(
|
|
z.object({
|
|
apiUrl: z.string().min(1),
|
|
apiKey: z.string(),
|
|
model: z.string().min(1),
|
|
}),
|
|
)
|
|
.mutation(async ({ input }) => {
|
|
try {
|
|
const provider = selectAIProvider({
|
|
apiUrl: input.apiUrl,
|
|
apiKey: input.apiKey,
|
|
});
|
|
const model = provider(input.model);
|
|
const result = await generateText({
|
|
model,
|
|
prompt: "Reply with 'ok'",
|
|
});
|
|
if (!result.text) {
|
|
throw new Error("No response received from the model");
|
|
}
|
|
return { success: true, message: "Connection successful" };
|
|
} catch (error) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message:
|
|
error instanceof Error
|
|
? error.message
|
|
: `Connection failed: ${error}`,
|
|
});
|
|
}
|
|
}),
|
|
|
|
suggest: protectedProcedure
|
|
.input(
|
|
z.object({
|
|
aiId: z.string(),
|
|
input: z.string(),
|
|
serverId: z.string().optional(),
|
|
}),
|
|
)
|
|
.mutation(async ({ ctx, input }) => {
|
|
try {
|
|
return await suggestVariants({
|
|
...input,
|
|
organizationId: ctx.session.activeOrganizationId,
|
|
});
|
|
} catch (error) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: error instanceof Error ? error?.message : `Error: ${error}`,
|
|
});
|
|
}
|
|
}),
|
|
deploy: protectedProcedure
|
|
.input(deploySuggestionSchema)
|
|
.mutation(async ({ ctx, input }) => {
|
|
const environment = await findEnvironmentById(input.environmentId);
|
|
const project = await findProjectById(environment.projectId);
|
|
await checkServiceAccess(ctx, environment.projectId, "create");
|
|
|
|
if (IS_CLOUD && !input.serverId) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "You need to use a server to create a compose",
|
|
});
|
|
}
|
|
|
|
const projectName = slugify(`${project.name} ${input.id}`);
|
|
|
|
const compose = await createComposeByTemplate({
|
|
...input,
|
|
composeFile: input.dockerCompose,
|
|
env: input.envVariables,
|
|
serverId: input.serverId,
|
|
name: input.name,
|
|
sourceType: "raw",
|
|
appName: `${projectName}-${generatePassword(6)}`,
|
|
isolatedDeployment: true,
|
|
environmentId: input.environmentId,
|
|
});
|
|
|
|
if (input.domains && input.domains?.length > 0) {
|
|
for (const domain of input.domains) {
|
|
await createDomain({
|
|
...domain,
|
|
domainType: "compose",
|
|
certificateType: "none",
|
|
composeId: compose.composeId,
|
|
});
|
|
}
|
|
}
|
|
if (input.configFiles && input.configFiles?.length > 0) {
|
|
for (const mount of input.configFiles) {
|
|
await createMount({
|
|
filePath: mount.filePath,
|
|
mountPath: "",
|
|
content: mount.content,
|
|
serviceId: compose.composeId,
|
|
serviceType: "compose",
|
|
type: "file",
|
|
});
|
|
}
|
|
}
|
|
|
|
await addNewService(ctx, compose.composeId);
|
|
|
|
return null;
|
|
}),
|
|
});
|