From 07c23292da58375cac553f83e63ccee2beb570b6 Mon Sep 17 00:00:00 2001 From: Vyacheslav Scherbinin Date: Tue, 19 Aug 2025 10:28:56 +0700 Subject: [PATCH] fix(ai): ollama fetch models --- .../dashboard/settings/handle-ai.tsx | 14 ++++++++++---- apps/dokploy/server/api/routers/ai.ts | 18 ++++++++++++++++-- packages/server/src/db/schema/ai.ts | 2 +- .../server/src/utils/ai/select-ai-provider.ts | 2 +- 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/handle-ai.tsx b/apps/dokploy/components/dashboard/settings/handle-ai.tsx index 2c4e7847f..a8041d71e 100644 --- a/apps/dokploy/components/dashboard/settings/handle-ai.tsx +++ b/apps/dokploy/components/dashboard/settings/handle-ai.tsx @@ -38,7 +38,7 @@ import { api } from "@/utils/api"; const Schema = z.object({ name: z.string().min(1, { message: "Name is required" }), apiUrl: z.string().url({ message: "Please enter a valid URL" }), - apiKey: z.string().min(1, { message: "API Key is required" }), + apiKey: z.string(), model: z.string().min(1, { message: "Model is required" }), isEnabled: z.boolean(), }); @@ -53,6 +53,7 @@ export const HandleAi = ({ aiId }: Props) => { const utils = api.useUtils(); const [error, setError] = useState(null); const [open, setOpen] = useState(false); + const [fetchModels, setFetchModels] = useState(false); const { data, refetch } = api.ai.one.useQuery( { aiId: aiId || "", @@ -71,7 +72,7 @@ export const HandleAi = ({ aiId }: Props) => { name: "", apiUrl: "", apiKey: "", - model: "gpt-3.5-turbo", + model: "", isEnabled: true, }, }); @@ -81,7 +82,7 @@ export const HandleAi = ({ aiId }: Props) => { name: data?.name ?? "", apiUrl: data?.apiUrl ?? "https://api.openai.com/v1", apiKey: data?.apiKey ?? "", - model: data?.model ?? "gpt-3.5-turbo", + model: data?.model ?? "", isEnabled: data?.isEnabled ?? true, }); }, [aiId, form, data]); @@ -96,12 +97,17 @@ export const HandleAi = ({ aiId }: Props) => { apiKey: apiKey ?? "", }, { - enabled: !!apiUrl && !!apiKey, + enabled: fetchModels, onError: (error) => { setError(`Failed to fetch models: ${error.message}`); }, }, ); + + useEffect(() => { + const isOllama = apiUrl.includes(":11434") || apiUrl.includes("ollama"); + setFetchModels(!!apiUrl && isOllama || !!apiKey); + }, [apiUrl, apiKey]); useEffect(() => { const apiUrl = form.watch("apiUrl"); diff --git a/apps/dokploy/server/api/routers/ai.ts b/apps/dokploy/server/api/routers/ai.ts index ed4277c1d..51b9ba95c 100644 --- a/apps/dokploy/server/api/routers/ai.ts +++ b/apps/dokploy/server/api/routers/ai.ts @@ -20,6 +20,7 @@ import { } from "@dokploy/server/services/user"; import { getProviderHeaders, + getProviderName, type Model, } from "@dokploy/server/utils/ai/select-ai-provider"; import { TRPCError } from "@trpc/server"; @@ -47,11 +48,24 @@ export const aiRouter = createTRPCRouter({ }), getModels: protectedProcedure - .input(z.object({ apiUrl: z.string().min(1), apiKey: z.string().min(1) })) + .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); - const response = await fetch(`${input.apiUrl}/models`, { headers }); + let response = null; + switch (providerName) { + case "ollama": + response = await fetch(`${input.apiUrl}/api/tags`, { headers }); + break; + 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(); diff --git a/packages/server/src/db/schema/ai.ts b/packages/server/src/db/schema/ai.ts index 3704c3dd9..0ee5af4bd 100644 --- a/packages/server/src/db/schema/ai.ts +++ b/packages/server/src/db/schema/ai.ts @@ -32,7 +32,7 @@ export const aiRelations = relations(ai, ({ one }) => ({ const createSchema = createInsertSchema(ai, { name: z.string().min(1, { message: "Name is required" }), apiUrl: z.string().url({ message: "Please enter a valid URL" }), - apiKey: z.string().min(1, { message: "API Key is required" }), + apiKey: z.string(), model: z.string().min(1, { message: "Model is required" }), isEnabled: z.boolean().optional(), }); diff --git a/packages/server/src/utils/ai/select-ai-provider.ts b/packages/server/src/utils/ai/select-ai-provider.ts index b1a741138..4f3f28eb3 100644 --- a/packages/server/src/utils/ai/select-ai-provider.ts +++ b/packages/server/src/utils/ai/select-ai-provider.ts @@ -7,7 +7,7 @@ import { createOpenAI } from "@ai-sdk/openai"; import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; import { createOllama } from "ollama-ai-provider"; -function getProviderName(apiUrl: string) { +export function getProviderName(apiUrl: string) { if (apiUrl.includes("api.openai.com")) return "openai"; if (apiUrl.includes("azure.com")) return "azure"; if (apiUrl.includes("api.anthropic.com")) return "anthropic";