Skip to content

Commit 2f074ac

Browse files
authored
Merge pull request #1405 from frostming/patch-1
feat: fallback to openai compatible provider if url host doesn't match
2 parents fa20444 + 96e3721 commit 2f074ac

File tree

3 files changed

+117
-46
lines changed

3 files changed

+117
-46
lines changed

apps/dokploy/components/dashboard/settings/handle-ai.tsx

Lines changed: 21 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -45,21 +45,12 @@ const Schema = z.object({
4545

4646
type Schema = z.infer<typeof Schema>;
4747

48-
interface Model {
49-
id: string;
50-
object: string;
51-
created: number;
52-
owned_by: string;
53-
}
54-
5548
interface Props {
5649
aiId?: string;
5750
}
5851

5952
export const HandleAi = ({ aiId }: Props) => {
60-
const [models, setModels] = useState<Model[]>([]);
6153
const utils = api.useUtils();
62-
const [isLoadingModels, setIsLoadingModels] = useState(false);
6354
const [error, setError] = useState<string | null>(null);
6455
const [open, setOpen] = useState(false);
6556
const { data, refetch } = api.ai.one.useQuery(
@@ -73,6 +64,7 @@ export const HandleAi = ({ aiId }: Props) => {
7364
const { mutateAsync, isLoading } = aiId
7465
? api.ai.update.useMutation()
7566
: api.ai.create.useMutation();
67+
7668
const form = useForm<Schema>({
7769
resolver: zodResolver(Schema),
7870
defaultValues: {
@@ -94,50 +86,33 @@ export const HandleAi = ({ aiId }: Props) => {
9486
});
9587
}, [aiId, form, data]);
9688

97-
const fetchModels = async (apiUrl: string, apiKey: string) => {
98-
setIsLoadingModels(true);
99-
setError(null);
100-
try {
101-
const response = await fetch(`${apiUrl}/models`, {
102-
headers: {
103-
Authorization: `Bearer ${apiKey}`,
104-
},
105-
});
106-
if (!response.ok) {
107-
throw new Error("Failed to fetch models");
108-
}
109-
const res = await response.json();
110-
setModels(res.data);
89+
const apiUrl = form.watch("apiUrl");
90+
const apiKey = form.watch("apiKey");
11191

112-
// Set default model to gpt-4 if present
113-
const defaultModel = res.data.find(
114-
(model: Model) => model.id === "gpt-4",
115-
);
116-
if (defaultModel) {
117-
form.setValue("model", defaultModel.id);
118-
return defaultModel.id;
119-
}
120-
} catch (error) {
121-
setError("Failed to fetch models. Please check your API URL and Key.");
122-
setModels([]);
123-
} finally {
124-
setIsLoadingModels(false);
125-
}
126-
};
92+
const { data: models, isLoading: isLoadingServerModels } =
93+
api.ai.getModels.useQuery(
94+
{
95+
apiUrl: apiUrl ?? "",
96+
apiKey: apiKey ?? "",
97+
},
98+
{
99+
enabled: !!apiUrl && !!apiKey,
100+
onError: (error) => {
101+
setError(`Failed to fetch models: ${error.message}`);
102+
},
103+
},
104+
);
127105

128106
useEffect(() => {
129107
const apiUrl = form.watch("apiUrl");
130108
const apiKey = form.watch("apiKey");
131109
if (apiUrl && apiKey) {
132110
form.setValue("model", "");
133-
fetchModels(apiUrl, apiKey);
134111
}
135112
}, [form.watch("apiUrl"), form.watch("apiKey")]);
136113

137114
const onSubmit = async (data: Schema) => {
138115
try {
139-
console.log("Form data:", data);
140-
console.log("Current model value:", form.getValues("model"));
141116
await mutateAsync({
142117
...data,
143118
aiId: aiId || "",
@@ -148,8 +123,9 @@ export const HandleAi = ({ aiId }: Props) => {
148123
refetch();
149124
setOpen(false);
150125
} catch (error) {
151-
console.error("Submit error:", error);
152-
toast.error("Failed to save AI settings");
126+
toast.error("Failed to save AI settings", {
127+
description: error instanceof Error ? error.message : "Unknown error",
128+
});
153129
}
154130
};
155131

@@ -232,13 +208,13 @@ export const HandleAi = ({ aiId }: Props) => {
232208
)}
233209
/>
234210

235-
{isLoadingModels && (
211+
{isLoadingServerModels && (
236212
<span className="text-sm text-muted-foreground">
237213
Loading models...
238214
</span>
239215
)}
240216

241-
{!isLoadingModels && models.length > 0 && (
217+
{!isLoadingServerModels && models && models.length > 0 && (
242218
<FormField
243219
control={form.control}
244220
name="model"

apps/dokploy/server/api/routers/ai.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ import {
2525
addNewService,
2626
checkServiceAccess,
2727
} from "@dokploy/server/services/user";
28+
import {
29+
getProviderHeaders,
30+
type Model,
31+
} from "@dokploy/server/utils/ai/select-ai-provider";
2832
import { TRPCError } from "@trpc/server";
2933
import { z } from "zod";
3034

@@ -41,6 +45,58 @@ export const aiRouter = createTRPCRouter({
4145
}
4246
return aiSetting;
4347
}),
48+
49+
getModels: protectedProcedure
50+
.input(z.object({ apiUrl: z.string().min(1), apiKey: z.string().min(1) }))
51+
.query(async ({ input }) => {
52+
try {
53+
const headers = getProviderHeaders(input.apiUrl, input.apiKey);
54+
const response = await fetch(`${input.apiUrl}/models`, { headers });
55+
56+
if (!response.ok) {
57+
const errorText = await response.text();
58+
throw new Error(`Failed to fetch models: ${errorText}`);
59+
}
60+
61+
const res = await response.json();
62+
63+
if (Array.isArray(res)) {
64+
return res.map((model) => ({
65+
id: model.id || model.name,
66+
object: "model",
67+
created: Date.now(),
68+
owned_by: "provider",
69+
}));
70+
}
71+
72+
if (res.models) {
73+
return res.models.map((model: any) => ({
74+
id: model.id || model.name,
75+
object: "model",
76+
created: Date.now(),
77+
owned_by: "provider",
78+
})) as Model[];
79+
}
80+
81+
if (res.data) {
82+
return res.data as Model[];
83+
}
84+
85+
const possibleModels =
86+
(Object.values(res).find(Array.isArray) as any[]) || [];
87+
return possibleModels.map((model) => ({
88+
id: model.id || model.name,
89+
object: "model",
90+
created: Date.now(),
91+
owned_by: "provider",
92+
})) as Model[];
93+
} catch (error) {
94+
throw new TRPCError({
95+
code: "BAD_REQUEST",
96+
message: error instanceof Error ? error?.message : `Error: ${error}`,
97+
});
98+
}
99+
}),
44100
create: adminProcedure.input(apiCreateAi).mutation(async ({ ctx, input }) => {
45101
return await saveAiSettings(ctx.session.activeOrganizationId, input);
46102
}),

packages/server/src/utils/ai/select-ai-provider.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function getProviderName(apiUrl: string) {
1717
if (apiUrl.includes("localhost:11434") || apiUrl.includes("ollama"))
1818
return "ollama";
1919
if (apiUrl.includes("api.deepinfra.com")) return "deepinfra";
20-
throw new Error(`Unsupported AI provider for URL: ${apiUrl}`);
20+
return "custom";
2121
}
2222

2323
export function selectAIProvider(config: { apiUrl: string; apiKey: string }) {
@@ -67,7 +67,46 @@ export function selectAIProvider(config: { apiUrl: string; apiKey: string }) {
6767
baseURL: config.apiUrl,
6868
apiKey: config.apiKey,
6969
});
70+
case "custom":
71+
return createOpenAICompatible({
72+
name: "custom",
73+
baseURL: config.apiUrl,
74+
headers: {
75+
Authorization: `Bearer ${config.apiKey}`,
76+
},
77+
});
7078
default:
7179
throw new Error(`Unsupported AI provider: ${providerName}`);
7280
}
7381
}
82+
83+
export const getProviderHeaders = (
84+
apiUrl: string,
85+
apiKey: string,
86+
): Record<string, string> => {
87+
// Anthropic
88+
if (apiUrl.includes("anthropic")) {
89+
return {
90+
"x-api-key": apiKey,
91+
"anthropic-version": "2023-06-01",
92+
};
93+
}
94+
95+
// Mistral
96+
if (apiUrl.includes("mistral")) {
97+
return {
98+
Authorization: apiKey,
99+
};
100+
}
101+
102+
// Default (OpenAI style)
103+
return {
104+
Authorization: `Bearer ${apiKey}`,
105+
};
106+
};
107+
export interface Model {
108+
id: string;
109+
object: string;
110+
created: number;
111+
owned_by: string;
112+
}

0 commit comments

Comments
 (0)