feat: Add LLM (Large Language Model) settings and management features

- Introduced new settings page for managing global LLM configurations.
- Added routes and permissions for LLM management in the admin dashboard.
- Implemented LLM model with necessary fields and access controls.
- Created API endpoints for fetching global LLMs.
- Developed UI components for displaying and editing LLM details.
- Integrated LLM settings into the existing admin dashboard structure.
- Added support for multiple LLM providers including OpenAI, Anthropic, and Ollama.
This commit is contained in:
Nawaz Dhandala
2025-12-11 20:45:23 +00:00
parent abc0446c3a
commit 94b107beb3
20 changed files with 1374 additions and 0 deletions

View File

@@ -9,6 +9,7 @@ import SettingsWhatsApp from "./Pages/Settings/WhatsApp/Index";
// Settings Pages.
import SettingsEmail from "./Pages/Settings/Email/Index";
import SettingsProbes from "./Pages/Settings/Probes/Index";
import SettingsLlms from "./Pages/Settings/Llms/Index";
import Users from "./Pages/Users/Index";
import PageMap from "./Utils/PageMap";
import RouteMap from "./Utils/RouteMap";
@@ -122,6 +123,11 @@ const App: () => JSX.Element = () => {
element={<SettingsProbes />}
/>
<PageRoute
path={RouteMap[PageMap.SETTINGS_LLMS]?.toString() || ""}
element={<SettingsLlms />}
/>
<PageRoute
path={RouteMap[PageMap.SETTINGS_AUTHENTICATION]?.toString() || ""}
element={<SettingsAuthentication />}

View File

@@ -0,0 +1,238 @@
import AdminModelAPI from "../../../Utils/ModelAPI";
import PageMap from "../../../Utils/PageMap";
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import DashboardSideMenu from "../SideMenu";
import Route from "Common/Types/API/Route";
import IsNull from "Common/Types/BaseDatabase/IsNull";
import Banner from "Common/UI/Components/Banner/Banner";
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
import ModelTable from "Common/UI/Components/ModelTable/ModelTable";
import Page from "Common/UI/Components/Page/Page";
import FieldType from "Common/UI/Components/Types/FieldType";
import Llm from "Common/Models/DatabaseModels/Llm";
import LlmType from "Common/Types/LLM/LlmType";
import React, { FunctionComponent, ReactElement } from "react";
import Pill from "Common/UI/Components/Pill/Pill";
import { Green, Red } from "Common/Types/BrandColors";
import DropdownUtil from "Common/UI/Utils/Dropdown";
const Settings: FunctionComponent = (): ReactElement => {
return (
<Page
title={"Admin Settings"}
breadcrumbLinks={[
{
title: "Admin Dashboard",
to: RouteUtil.populateRouteParams(RouteMap[PageMap.HOME] as Route),
},
{
title: "Settings",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SETTINGS] as Route,
),
},
{
title: "Global LLMs",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SETTINGS_LLMS] as Route,
),
},
]}
sideMenu={<DashboardSideMenu />}
>
{/* LLM Settings View */}
<Banner
openInNewTab={true}
title="Need help with setting up LLMs?"
description="LLMs (Large Language Models) enable AI features. You can configure global LLMs that are available to all projects."
link={Route.fromString("/docs/ai/llm")}
hideOnMobile={true}
/>
<ModelTable<Llm>
userPreferencesKey={"admin-llms-table"}
modelType={Llm}
id="llms-table"
name="Settings > Global LLMs"
isDeleteable={true}
isEditable={true}
isCreateable={true}
cardProps={{
title: "Global LLMs",
description:
"Global LLMs are available to all projects for AI features. Configure OpenAI, Anthropic, Ollama, or other LLM providers.",
}}
query={{
projectId: new IsNull(),
isGlobalLlm: true,
}}
modelAPI={AdminModelAPI}
noItemsMessage={"No LLMs configured. Add an LLM to enable AI features."}
showRefreshButton={true}
onBeforeCreate={(item: Llm) => {
item.isGlobalLlm = true;
return Promise.resolve(item);
}}
formSteps={[
{
title: "Basic Info",
id: "basic-info",
},
{
title: "Provider Settings",
id: "provider-settings",
},
]}
formFields={[
{
field: {
name: true,
},
title: "Name",
stepId: "basic-info",
fieldType: FormFieldSchemaType.Text,
required: true,
placeholder: "My OpenAI GPT-4",
validation: {
minLength: 2,
},
},
{
field: {
description: true,
},
title: "Description",
stepId: "basic-info",
fieldType: FormFieldSchemaType.LongText,
required: false,
placeholder: "GPT-4 for general AI features.",
},
{
field: {
llmType: true,
},
title: "LLM Provider",
stepId: "provider-settings",
fieldType: FormFieldSchemaType.Dropdown,
required: true,
placeholder: "Select LLM Provider",
dropdownOptions: DropdownUtil.getDropdownOptionsFromEnum(LlmType),
},
{
field: {
apiKey: true,
},
title: "API Key",
stepId: "provider-settings",
fieldType: FormFieldSchemaType.Text,
required: false,
placeholder: "sk-...",
description:
"Required for OpenAI and Anthropic. Not required for Ollama if self-hosted.",
},
{
field: {
modelName: true,
},
title: "Model Name",
stepId: "provider-settings",
fieldType: FormFieldSchemaType.Text,
required: false,
placeholder: "gpt-4, claude-3-opus, llama2",
description:
"The specific model to use (e.g., gpt-4, claude-3-opus, llama2).",
},
{
field: {
baseUrl: true,
},
title: "Base URL",
stepId: "provider-settings",
fieldType: FormFieldSchemaType.URL,
required: false,
placeholder: "http://localhost:11434",
description:
"Required for Ollama. Optional for others to use custom endpoints.",
},
{
field: {
isEnabled: true,
},
title: "Enabled",
stepId: "provider-settings",
fieldType: FormFieldSchemaType.Toggle,
required: false,
description: "Enable or disable this LLM configuration.",
},
]}
selectMoreFields={{
apiKey: true,
}}
filters={[
{
field: {
name: true,
},
title: "Name",
type: FieldType.Text,
},
{
field: {
description: true,
},
title: "Description",
type: FieldType.LongText,
},
{
field: {
llmType: true,
},
title: "Provider",
type: FieldType.Text,
},
]}
columns={[
{
field: {
name: true,
},
title: "Name",
type: FieldType.Text,
},
{
field: {
llmType: true,
},
title: "Provider",
type: FieldType.Text,
},
{
field: {
modelName: true,
},
title: "Model",
type: FieldType.Text,
noValueMessage: "-",
},
{
field: {
isEnabled: true,
},
title: "Status",
type: FieldType.Boolean,
getElement: (item: Llm): ReactElement => {
if (item.isEnabled) {
return <Pill text="Enabled" color={Green} />;
}
return <Pill text="Disabled" color={Red} />;
},
},
]}
/>
</Page>
);
};
export default Settings;

View File

@@ -72,6 +72,17 @@ const DashboardSideMenu: () => JSX.Element = (): ReactElement => {
icon={IconProp.Signal}
/>
</SideMenuSection>
<SideMenuSection title="AI">
<SideMenuItem
link={{
title: "Global LLMs",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SETTINGS_LLMS] as Route,
),
}}
icon={IconProp.Robot}
/>
</SideMenuSection>
<SideMenuSection title="API and Integrations">
<SideMenuItem
link={{

View File

@@ -18,6 +18,7 @@ enum PageMap {
SETTINGS_CALL_AND_SMS = "SETTINGS_CALL_AND_SMS",
SETTINGS_WHATSAPP = "SETTINGS_WHATSAPP",
SETTINGS_PROBES = "SETTINGS_PROBES",
SETTINGS_LLMS = "SETTINGS_LLMS",
SETTINGS_AUTHENTICATION = "SETTINGS_AUTHENTICATION",
SETTINGS_API_KEY = "SETTINGS_API_KEY",
}

View File

@@ -30,6 +30,7 @@ const RouteMap: Dictionary<Route> = {
[PageMap.SETTINGS_CALL_AND_SMS]: new Route(`/admin/settings/call-and-sms`),
[PageMap.SETTINGS_WHATSAPP]: new Route(`/admin/settings/whatsapp`),
[PageMap.SETTINGS_PROBES]: new Route(`/admin/settings/probes`),
[PageMap.SETTINGS_LLMS]: new Route(`/admin/settings/llms`),
[PageMap.SETTINGS_AUTHENTICATION]: new Route(
`/admin/settings/authentication`,
),

View File

@@ -12,6 +12,7 @@ import MonitorGroupAPI from "Common/Server/API/MonitorGroupAPI";
import NotificationAPI from "Common/Server/API/NotificationAPI";
import TelemetryAPI from "Common/Server/API/TelemetryAPI";
import ProbeAPI from "Common/Server/API/ProbeAPI";
import LlmAPI from "Common/Server/API/LlmAPI";
import ProjectAPI from "Common/Server/API/ProjectAPI";
import ProjectSsoAPI from "Common/Server/API/ProjectSSO";
import WhatsAppLogAPI from "./WhatsAppLogAPI";
@@ -1702,6 +1703,7 @@ const BaseAPIFeatureSet: FeatureSet = {
);
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new UserPushAPI().getRouter());
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new ProbeAPI().getRouter());
app.use(`/${APP_NAME.toLocaleLowerCase()}`, new LlmAPI().getRouter());
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,

View File

@@ -74,6 +74,7 @@ import OnCallDutyPolicyTimeLog from "./OnCallDutyPolicyTimeLog";
import Probe from "./Probe";
import ProbeOwnerTeam from "./ProbeOwnerTeam";
import ProbeOwnerUser from "./ProbeOwnerUser";
import Llm from "./Llm";
import Project from "./Project";
import ProjectCallSMSConfig from "./ProjectCallSMSConfig";
// Project SMTP Config.
@@ -381,6 +382,8 @@ const AllModelTypes: Array<{
ProbeOwnerTeam,
ProbeOwnerUser,
Llm,
UserSession,
UserTotpAuth,

View File

@@ -0,0 +1,464 @@
import Project from "./Project";
import User from "./User";
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
import TableBillingAccessControl from "../../Types/Database/AccessControl/TableBillingAccessControl";
import ColumnLength from "../../Types/Database/ColumnLength";
import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import IsPermissionsIf from "../../Types/Database/IsPermissionsIf";
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
import TenantColumn from "../../Types/Database/TenantColumn";
import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import LlmType from "../../Types/LLM/LlmType";
@EnableDocumentation()
@TableBillingAccessControl({
create: PlanType.Growth,
read: PlanType.Free,
update: PlanType.Growth,
delete: PlanType.Free,
})
@IsPermissionsIf(Permission.Public, "projectId", null)
@TenantColumn("projectId")
@CrudApiEndpoint(new Route("/llm"))
@SlugifyColumn("name", "slug")
@Entity({
name: "LLM",
})
@TableMetadata({
tableName: "LLM",
singularName: "LLM",
pluralName: "LLMs",
icon: IconProp.Bolt,
tableDescription:
"Manage LLM (Large Language Model) configurations. Connect to OpenAI, Anthropic, Ollama, or other LLM providers to enable AI features.",
})
@TableAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectLlm,
],
read: [
Permission.Public,
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectLlm,
],
delete: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.DeleteProjectLlm,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditProjectLlm,
],
})
export default class LLM extends BaseModel {
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectLlm,
],
read: [Permission.Public],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditProjectLlm,
],
})
@TableColumn({
required: true,
type: TableColumnType.Name,
canReadOnRelationQuery: true,
title: "Name",
description: "A friendly name for this LLM configuration.",
})
@Column({
nullable: false,
type: ColumnType.Name,
length: ColumnLength.Name,
})
public name?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectLlm,
],
read: [Permission.Public],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditProjectLlm,
],
})
@TableColumn({
required: false,
type: TableColumnType.LongText,
title: "Description",
description: "Description of this LLM configuration.",
})
@Column({
nullable: true,
type: ColumnType.LongText,
})
public description?: string = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.Public],
update: [],
})
@TableColumn({
required: true,
unique: true,
type: TableColumnType.Slug,
computed: true,
title: "Slug",
description: "Friendly globally unique name for your object",
})
@Column({
nullable: false,
type: ColumnType.Slug,
length: ColumnLength.Slug,
})
public slug?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectLlm,
],
read: [Permission.Public],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditProjectLlm,
],
})
@TableColumn({
required: true,
type: TableColumnType.ShortText,
title: "LLM Type",
description: "The type of LLM provider (OpenAI, Anthropic, Ollama, etc.)",
})
@Column({
nullable: false,
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
})
public llmType?: LlmType = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectLlm,
],
read: [Permission.ProjectOwner, Permission.ProjectAdmin],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditProjectLlm,
],
})
@TableColumn({
required: false,
type: TableColumnType.LongText,
title: "API Key",
description:
"The API key for the LLM provider. Required for OpenAI and Anthropic.",
encrypted: true,
})
@Column({
nullable: true,
type: ColumnType.LongText,
})
public apiKey?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectLlm,
],
read: [Permission.Public],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditProjectLlm,
],
})
@TableColumn({
required: false,
type: TableColumnType.ShortText,
title: "Model Name",
description:
"The name of the model to use (e.g., gpt-4, claude-3-opus, llama2).",
})
@Column({
nullable: true,
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
})
public modelName?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectLlm,
],
read: [Permission.Public],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditProjectLlm,
],
})
@TableColumn({
required: false,
type: TableColumnType.ShortURL,
title: "Base URL",
description:
"The base URL for the LLM API. Required for Ollama, optional for others.",
})
@Column({
nullable: true,
type: ColumnType.ShortURL,
length: ColumnLength.ShortURL,
})
public baseUrl?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectLlm,
],
read: [Permission.Public],
update: [],
})
@TableColumn({
type: TableColumnType.Entity,
required: false,
modelType: Project,
title: "Project",
description:
"The project this LLM belongs to. If null, it is a global LLM.",
})
@ManyToOne(
() => {
return Project;
},
{
cascade: false,
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "projectId" })
public project?: Project = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectLlm,
],
read: [Permission.Public],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
required: false,
canReadOnRelationQuery: true,
title: "Project ID",
description:
"ID of the project this LLM belongs to. If null, it is a global LLM.",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public projectId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({ type: TableColumnType.Entity, modelType: User })
@ManyToOne(
() => {
return User;
},
{
cascade: false,
eager: false,
nullable: true,
onDelete: "SET NULL",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "deletedByUserId" })
public deletedByUser?: User = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Deleted by User ID",
description:
"User ID who deleted this object (if this object was deleted by a User)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public deletedByUserId?: ObjectID = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectLlm,
],
read: [Permission.ProjectOwner],
update: [],
})
@TableColumn({ type: TableColumnType.Entity, modelType: User })
@ManyToOne(
() => {
return User;
},
{
eager: false,
nullable: true,
onDelete: "SET NULL",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "createdByUserId" })
public createdByUser?: User = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectLlm,
],
read: [Permission.ProjectOwner],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Created by User ID",
description:
"User ID who created this object (if this object was created by a User)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public createdByUserId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
isDefaultValueColumn: true,
required: true,
type: TableColumnType.Boolean,
canReadOnRelationQuery: true,
title: "Is Global LLM",
description:
"Is this a global LLM that is available to all projects? Only admins can create global LLMs.",
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,
nullable: false,
unique: false,
default: false,
})
public isGlobalLlm?: boolean = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectLlm,
],
read: [Permission.Public],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditProjectLlm,
],
})
@TableColumn({
isDefaultValueColumn: true,
required: true,
type: TableColumnType.Boolean,
title: "Is Enabled",
description: "Is this LLM configuration enabled and available for use?",
defaultValue: true,
})
@Column({
type: ColumnType.Boolean,
nullable: false,
unique: false,
default: true,
})
public isEnabled?: boolean = undefined;
}

View File

@@ -0,0 +1,58 @@
import UserMiddleware from "../Middleware/UserAuthorization";
import LlmService, {
Service as LlmServiceType,
} from "../Services/LlmService";
import {
ExpressRequest,
ExpressResponse,
NextFunction,
} from "../Utils/Express";
import Response from "../Utils/Response";
import BaseAPI from "./BaseAPI";
import LIMIT_MAX from "../../Types/Database/LimitMax";
import PositiveNumber from "../../Types/PositiveNumber";
import Llm from "../../Models/DatabaseModels/Llm";
export default class LlmAPI extends BaseAPI<Llm, LlmServiceType> {
public constructor() {
super(Llm, LlmService);
this.router.post(
`${new this.entityType().getCrudApiPath()?.toString()}/global-llms`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const llms: Array<Llm> = await LlmService.findBy({
query: {
isGlobalLlm: true,
isEnabled: true,
},
select: {
name: true,
description: true,
llmType: true,
modelName: true,
baseUrl: true,
isEnabled: true,
},
props: {
isRoot: true,
},
skip: 0,
limit: LIMIT_MAX,
});
return Response.sendEntityArrayResponse(
req,
res,
llms,
new PositiveNumber(llms.length),
Llm,
);
} catch (err) {
next(err);
}
},
);
}
}

View File

@@ -33,6 +33,7 @@ import IncidentStateService from "./IncidentStateService";
import IncidentStateTimelineService from "./IncidentStateTimelineService";
//Labels.
import LabelService from "./LabelService";
import LlmService from "./LlmService";
import LogService from "./LogService";
import MailService from "./MailService";
import MetricService from "./MetricService";
@@ -217,6 +218,7 @@ const services: Array<BaseService> = [
IncidentFeedService,
LabelService,
LlmService,
MailService,
MonitorCustomFieldService,

View File

@@ -0,0 +1,10 @@
import DatabaseService from "./DatabaseService";
import Model from "../../Models/DatabaseModels/Llm";
export class Service extends DatabaseService<Model> {
public constructor() {
super(Model);
}
}
export default new Service();

View File

@@ -0,0 +1,4 @@
import LlmType from "./LlmType";
export default LlmType;
export { LlmType };

View File

@@ -0,0 +1,7 @@
enum LlmType {
OpenAI = "OpenAI",
Anthropic = "Anthropic",
Ollama = "Ollama",
}
export default LlmType;

View File

@@ -96,6 +96,11 @@ enum Permission {
EditProjectProbe = "EditProjectProbe",
ReadProjectProbe = "ReadProjectProbe",
CreateProjectLlm = "CreateProjectLlm",
DeleteProjectLlm = "DeleteProjectLlm",
EditProjectLlm = "EditProjectLlm",
ReadProjectLlm = "ReadProjectLlm",
CreateTelemetryService = "CreateTelemetryService",
DeleteTelemetryService = "DeleteTelemetryService",
EditTelemetryService = "EditTelemetryService",
@@ -2744,6 +2749,35 @@ export class PermissionHelper {
isAccessControlPermission: true,
},
{
permission: Permission.CreateProjectLlm,
title: "Create LLM",
description: "This permission can create LLM configurations for this project.",
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.DeleteProjectLlm,
title: "Delete LLM",
description: "This permission can delete LLM configurations of this project.",
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.EditProjectLlm,
title: "Edit LLM",
description: "This permission can edit LLM configurations of this project.",
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.ReadProjectLlm,
title: "Read LLM",
description: "This permission can read LLM configurations of this project.",
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.CreateTelemetryService,
title: "Create Telemetry Service",

View File

@@ -0,0 +1,197 @@
import PageMap from "../../Utils/PageMap";
import RouteMap from "../../Utils/RouteMap";
import PageComponentProps from "../PageComponentProps";
import Route from "Common/Types/API/Route";
import ObjectID from "Common/Types/ObjectID";
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
import ModelDelete from "Common/UI/Components/ModelDelete/ModelDelete";
import CardModelDetail from "Common/UI/Components/ModelDetail/CardModelDetail";
import FieldType from "Common/UI/Components/Types/FieldType";
import Navigation from "Common/UI/Utils/Navigation";
import Llm from "Common/Models/DatabaseModels/Llm";
import LlmType from "Common/Types/LLM/LlmType";
import React, { Fragment, FunctionComponent, ReactElement, useState } from "react";
import Pill from "Common/UI/Components/Pill/Pill";
import { Green, Red } from "Common/Types/BrandColors";
import DropdownUtil from "Common/UI/Utils/Dropdown";
const LlmView: FunctionComponent<PageComponentProps> = (
_props: PageComponentProps,
): ReactElement => {
const [modelId] = useState<ObjectID>(Navigation.getLastParamAsObjectID());
return (
<Fragment>
{/* LLM View */}
<CardModelDetail<Llm>
name="LLM Details"
cardProps={{
title: "LLM Details",
description: "Here are more details for this LLM configuration.",
}}
isEditable={true}
formSteps={[
{
title: "Basic Info",
id: "basic-info",
},
{
title: "Provider Settings",
id: "provider-settings",
},
]}
formFields={[
{
field: {
name: true,
},
stepId: "basic-info",
title: "Name",
fieldType: FormFieldSchemaType.Text,
required: true,
placeholder: "My OpenAI GPT-4",
validation: {
minLength: 2,
},
},
{
field: {
description: true,
},
title: "Description",
stepId: "basic-info",
fieldType: FormFieldSchemaType.LongText,
required: false,
placeholder: "GPT-4 for general AI features.",
},
{
field: {
llmType: true,
},
title: "LLM Provider",
stepId: "provider-settings",
fieldType: FormFieldSchemaType.Dropdown,
required: true,
placeholder: "Select LLM Provider",
dropdownOptions: DropdownUtil.getDropdownOptionsFromEnum(LlmType),
},
{
field: {
apiKey: true,
},
title: "API Key",
stepId: "provider-settings",
fieldType: FormFieldSchemaType.Text,
required: false,
placeholder: "sk-...",
description:
"Required for OpenAI and Anthropic. Not required for Ollama if self-hosted.",
},
{
field: {
modelName: true,
},
title: "Model Name",
stepId: "provider-settings",
fieldType: FormFieldSchemaType.Text,
required: false,
placeholder: "gpt-4, claude-3-opus, llama2",
description:
"The specific model to use (e.g., gpt-4, claude-3-opus, llama2).",
},
{
field: {
baseUrl: true,
},
title: "Base URL",
stepId: "provider-settings",
fieldType: FormFieldSchemaType.URL,
required: false,
placeholder: "http://localhost:11434",
description:
"Required for Ollama. Optional for others to use custom endpoints.",
},
{
field: {
isEnabled: true,
},
title: "Enabled",
stepId: "provider-settings",
fieldType: FormFieldSchemaType.Toggle,
required: false,
description: "Enable or disable this LLM configuration.",
},
]}
modelDetailProps={{
modelType: Llm,
id: "model-detail-llm",
fields: [
{
field: {
_id: true,
},
title: "LLM ID",
},
{
field: {
name: true,
},
title: "Name",
},
{
field: {
description: true,
},
title: "Description",
placeholder: "No description provided.",
},
{
field: {
llmType: true,
},
title: "Provider",
},
{
field: {
modelName: true,
},
title: "Model Name",
placeholder: "Not specified",
},
{
field: {
baseUrl: true,
},
title: "Base URL",
placeholder: "Not specified (using default)",
},
{
field: {
isEnabled: true,
},
title: "Status",
fieldType: FieldType.Boolean,
getElement: (item: Llm): ReactElement => {
if (item.isEnabled) {
return <Pill text="Enabled" color={Green} />;
}
return <Pill text="Disabled" color={Red} />;
},
},
],
modelId: modelId,
}}
/>
<ModelDelete
modelType={Llm}
modelId={modelId}
onDeleteSuccess={() => {
Navigation.navigate(RouteMap[PageMap.SETTINGS_LLMS] as Route);
}}
/>
</Fragment>
);
};
export default LlmView;

View File

@@ -0,0 +1,275 @@
import ProjectUtil from "Common/UI/Utils/Project";
import PageComponentProps from "../PageComponentProps";
import Route from "Common/Types/API/Route";
import URL from "Common/Types/API/URL";
import Banner from "Common/UI/Components/Banner/Banner";
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
import ModelTable from "Common/UI/Components/ModelTable/ModelTable";
import FieldType from "Common/UI/Components/Types/FieldType";
import { APP_API_URL } from "Common/UI/Config";
import Navigation from "Common/UI/Utils/Navigation";
import Llm from "Common/Models/DatabaseModels/Llm";
import LlmType from "Common/Types/LLM/LlmType";
import React, { Fragment, FunctionComponent, ReactElement } from "react";
import Pill from "Common/UI/Components/Pill/Pill";
import { Green, Red } from "Common/Types/BrandColors";
import DropdownUtil from "Common/UI/Utils/Dropdown";
const LlmPage: FunctionComponent<PageComponentProps> = (): ReactElement => {
return (
<Fragment>
<>
<ModelTable<Llm>
modelType={Llm}
id="global-llms-table"
name="Settings > Global LLMs"
userPreferencesKey={"settings-global-llms-table"}
isDeleteable={false}
isEditable={false}
isCreateable={false}
cardProps={{
title: "Global LLMs",
description:
"Global LLMs are configured by your administrator and are available to all projects for AI features.",
}}
fetchRequestOptions={{
overrideRequestUrl: URL.fromString(APP_API_URL.toString()).addRoute(
"/llm/global-llms",
),
}}
noItemsMessage={"No global LLMs configured."}
showRefreshButton={true}
filters={[
{
field: {
name: true,
},
title: "Name",
type: FieldType.Text,
},
{
field: {
llmType: true,
},
title: "Provider",
type: FieldType.Text,
},
]}
columns={[
{
field: {
name: true,
},
title: "Name",
type: FieldType.Text,
},
{
field: {
llmType: true,
},
title: "Provider",
type: FieldType.Text,
},
{
field: {
modelName: true,
},
title: "Model",
type: FieldType.Text,
noValueMessage: "-",
},
]}
/>
<Banner
openInNewTab={true}
title="Need help with setting up Custom LLMs?"
description="Here is a guide which will help you get set up with your own LLM configurations."
link={Route.fromString("/docs/ai/llm")}
hideOnMobile={true}
/>
<ModelTable<Llm>
modelType={Llm}
query={{
projectId: ProjectUtil.getCurrentProjectId()!,
}}
id="project-llms-table"
userPreferencesKey={"settings-project-llms-table"}
name="Settings > LLMs"
isDeleteable={true}
isEditable={true}
isViewable={true}
isCreateable={true}
cardProps={{
title: "Project LLMs",
description:
"Configure LLMs (Large Language Models) for AI features. Connect to OpenAI, Anthropic, Ollama, or other providers.",
}}
selectMoreFields={{
apiKey: true,
}}
noItemsMessage={
"No LLMs configured. Add an LLM to enable AI features for your project."
}
viewPageRoute={Navigation.getCurrentRoute()}
formSteps={[
{
title: "Basic Info",
id: "basic-info",
},
{
title: "Provider Settings",
id: "provider-settings",
},
]}
formFields={[
{
field: {
name: true,
},
stepId: "basic-info",
title: "Name",
fieldType: FormFieldSchemaType.Text,
required: true,
placeholder: "My OpenAI GPT-4",
validation: {
minLength: 2,
},
},
{
field: {
description: true,
},
title: "Description",
stepId: "basic-info",
fieldType: FormFieldSchemaType.LongText,
required: false,
placeholder: "GPT-4 for general AI features.",
},
{
field: {
llmType: true,
},
title: "LLM Provider",
stepId: "provider-settings",
fieldType: FormFieldSchemaType.Dropdown,
required: true,
placeholder: "Select LLM Provider",
dropdownOptions: DropdownUtil.getDropdownOptionsFromEnum(LlmType),
},
{
field: {
apiKey: true,
},
title: "API Key",
stepId: "provider-settings",
fieldType: FormFieldSchemaType.Text,
required: false,
placeholder: "sk-...",
description:
"Required for OpenAI and Anthropic. Not required for Ollama if self-hosted.",
},
{
field: {
modelName: true,
},
title: "Model Name",
stepId: "provider-settings",
fieldType: FormFieldSchemaType.Text,
required: false,
placeholder: "gpt-4, claude-3-opus, llama2",
description:
"The specific model to use (e.g., gpt-4, claude-3-opus, llama2).",
},
{
field: {
baseUrl: true,
},
title: "Base URL",
stepId: "provider-settings",
fieldType: FormFieldSchemaType.URL,
required: false,
placeholder: "http://localhost:11434",
description:
"Required for Ollama. Optional for others to use custom endpoints.",
},
{
field: {
isEnabled: true,
},
title: "Enabled",
stepId: "provider-settings",
fieldType: FormFieldSchemaType.Toggle,
required: false,
description: "Enable or disable this LLM configuration.",
},
]}
showRefreshButton={true}
filters={[
{
field: {
name: true,
},
title: "Name",
type: FieldType.Text,
},
{
field: {
llmType: true,
},
title: "Provider",
type: FieldType.Text,
},
{
field: {
isEnabled: true,
},
title: "Enabled",
type: FieldType.Boolean,
},
]}
columns={[
{
field: {
name: true,
},
title: "Name",
type: FieldType.Text,
},
{
field: {
llmType: true,
},
title: "Provider",
type: FieldType.Text,
},
{
field: {
modelName: true,
},
title: "Model",
type: FieldType.Text,
noValueMessage: "-",
},
{
field: {
isEnabled: true,
},
title: "Status",
type: FieldType.Boolean,
getElement: (item: Llm): ReactElement => {
if (item.isEnabled) {
return <Pill text="Enabled" color={Green} />;
}
return <Pill text="Disabled" color={Red} />;
},
},
]}
/>
</>
</Fragment>
);
};
export default LlmPage;

View File

@@ -360,6 +360,15 @@ const DashboardSideMenu: () => JSX.Element = (): ReactElement => {
},
icon: IconProp.Signal,
},
{
link: {
title: "LLMs",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SETTINGS_LLMS] as Route,
),
},
icon: IconProp.Robot,
},
{
link: {
title: "Domains",

View File

@@ -79,6 +79,16 @@ const SettingProbes: LazyExoticComponent<FunctionComponent<ComponentProps>> =
return import("../Pages/Settings/Probes");
});
const SettingLlms: LazyExoticComponent<FunctionComponent<ComponentProps>> =
lazy(() => {
return import("../Pages/Settings/Llms");
});
const SettingsLlmView: LazyExoticComponent<FunctionComponent<ComponentProps>> =
lazy(() => {
return import("../Pages/Settings/LlmView");
});
const SettingFeatureFlags: LazyExoticComponent<
FunctionComponent<ComponentProps>
> = lazy(() => {
@@ -1028,6 +1038,18 @@ const SettingsRoutes: FunctionComponent<ComponentProps> = (
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(PageMap.SETTINGS_LLMS)}
element={
<Suspense fallback={Loader}>
<SettingLlms
{...props}
pageRoute={RouteMap[PageMap.SETTINGS_LLMS] as Route}
/>
</Suspense>
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(PageMap.SETTINGS_TEAMS)}
element={
@@ -1087,6 +1109,18 @@ const SettingsRoutes: FunctionComponent<ComponentProps> = (
</Suspense>
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(PageMap.SETTINGS_LLM_VIEW, 2)}
element={
<Suspense fallback={Loader}>
<SettingsLlmView
{...props}
pageRoute={RouteMap[PageMap.SETTINGS_LLM_VIEW] as Route}
/>
</Suspense>
}
/>
</PageRoute>
</Routes>
);

View File

@@ -363,6 +363,10 @@ enum PageMap {
// Probes.
SETTINGS_PROBES = "SETTINGS_PROBES",
// LLMs.
SETTINGS_LLMS = "SETTINGS_LLMS",
SETTINGS_LLM_VIEW = "SETTINGS_LLM_VIEW",
// SSO.
SETTINGS_SSO = "SETTINGS_SSO",

View File

@@ -280,6 +280,8 @@ export const SettingsRoutePath: Dictionary<string> = {
[PageMap.SETTINGS_PROBE_VIEW]: `probes/${RouteParams.ModelID}`,
[PageMap.SETTINGS_LABELS]: "labels",
[PageMap.SETTINGS_PROBES]: "probes",
[PageMap.SETTINGS_LLMS]: "llm",
[PageMap.SETTINGS_LLM_VIEW]: `llm/${RouteParams.ModelID}`,
};
export const OnCallDutyRoutePath: Dictionary<string> = {
@@ -1937,6 +1939,18 @@ const RouteMap: Dictionary<Route> = {
}`,
),
// LLMs.
[PageMap.SETTINGS_LLMS]: new Route(
`/dashboard/${RouteParams.ProjectID}/settings/${
SettingsRoutePath[PageMap.SETTINGS_LLMS]
}`,
),
[PageMap.SETTINGS_LLM_VIEW]: new Route(
`/dashboard/${RouteParams.ProjectID}/settings/${
SettingsRoutePath[PageMap.SETTINGS_LLM_VIEW]
}`,
),
// workflows.
[PageMap.WORKFLOWS_ROOT]: new Route(
`/dashboard/${RouteParams.ProjectID}/workflows/*`,