mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-05-25 19:20:26 -04:00
feat: In-app AI Provider Configuration (#7650)
This commit is contained in:
27
frontend/app/lib/api/admin/admin-ai-providers.ts
Normal file
27
frontend/app/lib/api/admin/admin-ai-providers.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { BaseAPI } from "../base/base-clients";
|
||||
import type { AIProviderCreate, AIProviderOut, AIProviderUpdate } from "~/lib/api/types/group";
|
||||
|
||||
const prefix = "/api/admin";
|
||||
|
||||
const routes = {
|
||||
providers: (groupId: string) => `${prefix}/groups/${groupId}/ai-providers/providers`,
|
||||
providersId: (groupId: string, providerId: string) => `${prefix}/groups/${groupId}/ai-providers/providers/${providerId}`,
|
||||
};
|
||||
|
||||
export class AdminAIProvidersApi extends BaseAPI {
|
||||
async createProvider(groupId: string, payload: AIProviderCreate) {
|
||||
return await this.requests.post<AIProviderOut>(routes.providers(groupId), payload);
|
||||
}
|
||||
|
||||
async getProvider(groupId: string, providerId: string) {
|
||||
return await this.requests.get<AIProviderOut>(routes.providersId(groupId, providerId));
|
||||
}
|
||||
|
||||
async updateProvider(groupId: string, providerId: string, payload: AIProviderUpdate) {
|
||||
return await this.requests.put<AIProviderOut>(routes.providersId(groupId, providerId), payload);
|
||||
}
|
||||
|
||||
async deleteProvider(groupId: string, providerId: string) {
|
||||
return await this.requests.delete<AIProviderOut>(routes.providersId(groupId, providerId));
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,11 @@ import type { DebugResponse } from "~/lib/api/types/admin";
|
||||
const prefix = "/api";
|
||||
|
||||
const routes = {
|
||||
openai: `${prefix}/admin/debug/openai`,
|
||||
openai: providerId => `${prefix}/admin/debug/openai/${providerId}`,
|
||||
};
|
||||
|
||||
export class AdminDebugAPI extends BaseAPI {
|
||||
async debugOpenAI(fileObject: Blob | File | undefined = undefined, fileName = "") {
|
||||
async debugOpenAI(providerId: string, fileObject: Blob | File | undefined = undefined, fileName = "") {
|
||||
let formData: FormData | null = null;
|
||||
if (fileObject) {
|
||||
formData = new FormData();
|
||||
@@ -16,6 +16,6 @@ export class AdminDebugAPI extends BaseAPI {
|
||||
formData.append("extension", fileName.split(".").pop() ?? "");
|
||||
}
|
||||
|
||||
return await this.requests.post<DebugResponse>(routes.openai, formData);
|
||||
return await this.requests.post<DebugResponse>(routes.openai(providerId), formData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { AdminBackupsApi } from "./admin/admin-backups";
|
||||
import { AdminMaintenanceApi } from "./admin/admin-maintenance";
|
||||
import { AdminAnalyticsApi } from "./admin/admin-analytics";
|
||||
import { AdminDebugAPI } from "./admin/admin-debug";
|
||||
import { AdminAIProvidersApi } from "./admin/admin-ai-providers";
|
||||
import type { ApiRequestInstance } from "~/lib/api/types/non-generated";
|
||||
|
||||
export class AdminAPI {
|
||||
@@ -17,6 +18,7 @@ export class AdminAPI {
|
||||
public maintenance: AdminMaintenanceApi;
|
||||
public analytics: AdminAnalyticsApi;
|
||||
public debug: AdminDebugAPI;
|
||||
public aiProviders: AdminAIProvidersApi;
|
||||
|
||||
constructor(requests: ApiRequestInstance) {
|
||||
this.about = new AdminAboutAPI(requests);
|
||||
@@ -27,6 +29,7 @@ export class AdminAPI {
|
||||
this.maintenance = new AdminMaintenanceApi(requests);
|
||||
this.analytics = new AdminAnalyticsApi(requests);
|
||||
this.debug = new AdminDebugAPI(requests);
|
||||
this.aiProviders = new AdminAIProvidersApi(requests);
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import { MultiPurposeLabelsApi } from "./user/group-multiple-purpose-labels";
|
||||
import { GroupEventNotifierApi } from "./user/group-event-notifier";
|
||||
import { MealPlanRulesApi } from "./user/group-mealplan-rules";
|
||||
import { GroupDataSeederApi } from "./user/group-seeder";
|
||||
import { AIProvidersAPI } from "./user/group-ai-providers";
|
||||
import type { ApiRequestInstance } from "~/lib/api/types/non-generated";
|
||||
|
||||
export class UserApiClient {
|
||||
@@ -53,6 +54,7 @@ export class UserApiClient {
|
||||
public groupEventNotifier: GroupEventNotifierApi;
|
||||
public upload: UploadFile;
|
||||
public seeders: GroupDataSeederApi;
|
||||
public aiProviders: AIProvidersAPI;
|
||||
|
||||
constructor(requests: ApiRequestInstance) {
|
||||
// Recipes
|
||||
@@ -80,6 +82,7 @@ export class UserApiClient {
|
||||
this.shopping = new ShoppingApi(requests);
|
||||
this.multiPurposeLabels = new MultiPurposeLabelsApi(requests);
|
||||
this.seeders = new GroupDataSeederApi(requests);
|
||||
this.aiProviders = new AIProvidersAPI(requests);
|
||||
|
||||
// Admin
|
||||
this.backups = new BackupAPI(requests);
|
||||
|
||||
@@ -16,9 +16,6 @@ export interface AdminAboutInfo {
|
||||
enableOidc: boolean;
|
||||
oidcRedirect: boolean;
|
||||
oidcProviderName: string;
|
||||
enableOpenai: boolean;
|
||||
enableOpenaiImageServices: boolean;
|
||||
enableOpenaiTranscriptionServices: boolean;
|
||||
tokenTime: number;
|
||||
versionLatest: string;
|
||||
apiPort: number;
|
||||
@@ -50,9 +47,6 @@ export interface AppInfo {
|
||||
enableOidc: boolean;
|
||||
oidcRedirect: boolean;
|
||||
oidcProviderName: string;
|
||||
enableOpenai: boolean;
|
||||
enableOpenaiImageServices: boolean;
|
||||
enableOpenaiTranscriptionServices: boolean;
|
||||
tokenTime: number;
|
||||
}
|
||||
export interface AppStartupInfo {
|
||||
@@ -95,7 +89,6 @@ export interface CheckAppConfig {
|
||||
emailReady: boolean;
|
||||
ldapReady: boolean;
|
||||
oidcReady: boolean;
|
||||
enableOpenai: boolean;
|
||||
baseUrlSet: boolean;
|
||||
isUpToDate: boolean;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,77 @@ export type SupportedMigrations =
|
||||
| "recipekeeper"
|
||||
| "cookn";
|
||||
|
||||
export interface AIProviderCreate {
|
||||
name: string;
|
||||
baseUrl?: string | null;
|
||||
model: string;
|
||||
timeout?: number;
|
||||
requestHeaders?: {
|
||||
[k: string]: string;
|
||||
};
|
||||
requestParams?: {
|
||||
[k: string]: string;
|
||||
};
|
||||
}
|
||||
export interface AIProviderOut {
|
||||
name: string;
|
||||
baseUrl?: string | null;
|
||||
model: string;
|
||||
timeout?: number;
|
||||
requestHeaders?: {
|
||||
[k: string]: string;
|
||||
};
|
||||
requestParams?: {
|
||||
[k: string]: string;
|
||||
};
|
||||
id: string;
|
||||
}
|
||||
export interface AIProviderSave {
|
||||
name: string;
|
||||
baseUrl?: string | null;
|
||||
model: string;
|
||||
timeout?: number;
|
||||
requestHeaders?: {
|
||||
[k: string]: string;
|
||||
};
|
||||
requestParams?: {
|
||||
[k: string]: string;
|
||||
};
|
||||
settingsId: string;
|
||||
}
|
||||
export interface AIProviderSettingsCreate {
|
||||
groupId: string;
|
||||
}
|
||||
export interface AIProviderSettingsOut {
|
||||
defaultProviderId: string | null;
|
||||
audioProviderId: string | null;
|
||||
imageProviderId: string | null;
|
||||
providers: AIProviderSummary[];
|
||||
aiEnabled: boolean;
|
||||
audioProviderEnabled: boolean;
|
||||
imageProviderEnabled: boolean;
|
||||
}
|
||||
export interface AIProviderSummary {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
export interface AIProviderSettingsUpdate {
|
||||
defaultProviderId: string | null;
|
||||
audioProviderId: string | null;
|
||||
imageProviderId: string | null;
|
||||
}
|
||||
export interface AIProviderUpdate {
|
||||
name: string;
|
||||
baseUrl?: string | null;
|
||||
model: string;
|
||||
timeout?: number;
|
||||
requestHeaders?: {
|
||||
[k: string]: string;
|
||||
};
|
||||
requestParams?: {
|
||||
[k: string]: string;
|
||||
};
|
||||
}
|
||||
export interface CreateGroupPreferences {
|
||||
privateGroup?: boolean;
|
||||
showAnnouncements?: boolean;
|
||||
@@ -29,6 +100,7 @@ export interface GroupAdminUpdate {
|
||||
id: string;
|
||||
name: string;
|
||||
preferences?: UpdateGroupPreferences | null;
|
||||
aiProviderSettings?: AIProviderSettingsUpdate | null;
|
||||
}
|
||||
export interface UpdateGroupPreferences {
|
||||
privateGroup?: boolean;
|
||||
|
||||
@@ -59,6 +59,7 @@ export interface GroupInDB {
|
||||
households?: GroupHouseholdSummary[] | null;
|
||||
users?: UserSummary[] | null;
|
||||
preferences?: ReadGroupPreferences | null;
|
||||
aiProviderSettings?: AIProviderSettingsOut | null;
|
||||
}
|
||||
export interface CategoryBase {
|
||||
name: string;
|
||||
@@ -89,11 +90,25 @@ export interface ReadGroupPreferences {
|
||||
groupId: string;
|
||||
id: string;
|
||||
}
|
||||
export interface AIProviderSettingsOut {
|
||||
defaultProviderId: string | null;
|
||||
audioProviderId: string | null;
|
||||
imageProviderId: string | null;
|
||||
providers: AIProviderSummary[];
|
||||
aiEnabled: boolean;
|
||||
audioProviderEnabled: boolean;
|
||||
imageProviderEnabled: boolean;
|
||||
}
|
||||
export interface AIProviderSummary {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
export interface GroupSummary {
|
||||
name: string;
|
||||
id: string;
|
||||
slug: string;
|
||||
preferences?: ReadGroupPreferences | null;
|
||||
aiProviderSettings?: AIProviderSettingsOut | null;
|
||||
}
|
||||
export interface LongLiveTokenCreateResponse {
|
||||
name: string;
|
||||
|
||||
27
frontend/app/lib/api/user/group-ai-providers.ts
Normal file
27
frontend/app/lib/api/user/group-ai-providers.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { BaseAPI } from "../base/base-clients";
|
||||
import type { AIProviderCreate, AIProviderOut, AIProviderUpdate } from "~/lib/api/types/group";
|
||||
|
||||
const prefix = "/api/groups/ai-providers";
|
||||
|
||||
const routes = {
|
||||
providers: `${prefix}/providers`,
|
||||
providersId: (id: string) => `${prefix}/providers/${id}`,
|
||||
};
|
||||
|
||||
export class AIProvidersAPI extends BaseAPI {
|
||||
async getOne(id: string) {
|
||||
return await this.requests.get<AIProviderOut>(routes.providersId(id));
|
||||
}
|
||||
|
||||
async createOne(payload: AIProviderCreate) {
|
||||
return await this.requests.post<AIProviderOut>(routes.providers, payload);
|
||||
}
|
||||
|
||||
async updateOne(id: string, payload: AIProviderUpdate) {
|
||||
return await this.requests.put<AIProviderOut, AIProviderUpdate>(routes.providersId(id), payload);
|
||||
}
|
||||
|
||||
async deleteOne(id: string) {
|
||||
return await this.requests.delete<AIProviderOut>(routes.providersId(id));
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import type { PaginationData } from "../types/non-generated";
|
||||
import type { QueryValue } from "../base/route";
|
||||
import type { GroupBase, GroupInDB, GroupSummary, UserSummary } from "~/lib/api/types/user";
|
||||
import type {
|
||||
AIProviderSettingsUpdate,
|
||||
AIProviderSettingsOut,
|
||||
GroupAdminUpdate,
|
||||
GroupStorage,
|
||||
ReadGroupPreferences,
|
||||
@@ -15,6 +17,7 @@ const routes = {
|
||||
groups: `${prefix}/admin/groups`,
|
||||
groupsSelf: `${prefix}/groups/self`,
|
||||
preferences: `${prefix}/groups/preferences`,
|
||||
aiProviderSettings: `${prefix}/groups/ai-providers/settings`,
|
||||
storage: `${prefix}/groups/storage`,
|
||||
members: `${prefix}/groups/members`,
|
||||
groupsId: (id: string | number) => `${prefix}/admin/groups/${id}`,
|
||||
@@ -29,15 +32,15 @@ export class GroupAPI extends BaseCRUDAPI<GroupBase, GroupInDB, GroupAdminUpdate
|
||||
return await this.requests.get<GroupSummary>(routes.groupsSelf);
|
||||
}
|
||||
|
||||
async getPreferences() {
|
||||
return await this.requests.get<ReadGroupPreferences>(routes.preferences);
|
||||
}
|
||||
|
||||
async setPreferences(payload: UpdateGroupPreferences) {
|
||||
// TODO: This should probably be a patch request, which isn't offered by the API currently
|
||||
return await this.requests.put<ReadGroupPreferences, UpdateGroupPreferences>(routes.preferences, payload);
|
||||
}
|
||||
|
||||
async setAIProviderSettings(payload: AIProviderSettingsUpdate) {
|
||||
return await this.requests.put<AIProviderSettingsOut, AIProviderSettingsUpdate>(routes.aiProviderSettings, payload);
|
||||
}
|
||||
|
||||
async fetchMembers(page = 1, perPage = -1, params = {} as Record<string, QueryValue>) {
|
||||
return await this.requests.get<PaginationData<UserSummary>>(routes.members, { page, perPage, ...params });
|
||||
}
|
||||
|
||||
@@ -43,10 +43,6 @@ export class HouseholdAPI extends BaseCRUDAPIReadOnly<HouseholdSummary> {
|
||||
return await this.requests.get<HouseholdRecipeSummary>(routes.householdsSelfRecipesSlug(recipeSlug));
|
||||
}
|
||||
|
||||
async getPreferences() {
|
||||
return await this.requests.get<ReadHouseholdPreferences>(routes.preferences);
|
||||
}
|
||||
|
||||
async setPreferences(payload: UpdateHouseholdPreferences) {
|
||||
// TODO: This should probably be a patch request, which isn't offered by the API currently
|
||||
return await this.requests.put<ReadHouseholdPreferences, UpdateHouseholdPreferences>(routes.preferences, payload);
|
||||
|
||||
Reference in New Issue
Block a user