mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat: Add AI Logs functionality with LLM logging and management
- Introduced LlmLog model to track AI API calls, including details like provider, tokens used, cost, and status. - Implemented AILogService to handle AI log creation and management, including billing checks and log updates. - Created LlmLogsTable component for displaying AI logs in the dashboard with filtering and modal views for request/response details. - Added new routes and pages for viewing AI logs in the context of incidents, alerts, and settings. - Updated PageMap and RouteMap to include new AI log views. - Enhanced error handling and logging for AI API interactions.
This commit is contained in:
@@ -83,6 +83,10 @@ const Settings: FunctionComponent = (): ReactElement => {
|
||||
title: "Provider Settings",
|
||||
id: "provider-settings",
|
||||
},
|
||||
{
|
||||
title: "Cost Settings",
|
||||
id: "cost-settings",
|
||||
},
|
||||
]}
|
||||
formFields={[
|
||||
{
|
||||
@@ -156,6 +160,18 @@ const Settings: FunctionComponent = (): ReactElement => {
|
||||
description:
|
||||
"Required for Ollama. Optional for others to use custom endpoints.",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
costPerMillionTokensInUSDCents: true,
|
||||
},
|
||||
title: "Cost Per Million Tokens (USD Cents)",
|
||||
stepId: "cost-settings",
|
||||
fieldType: FormFieldSchemaType.Number,
|
||||
required: false,
|
||||
placeholder: "0",
|
||||
description:
|
||||
"Cost per million tokens in USD cents. For example, if the cost is $0.01 per 1M tokens, enter 1.",
|
||||
},
|
||||
]}
|
||||
selectMoreFields={{
|
||||
apiKey: true,
|
||||
@@ -206,6 +222,14 @@ const Settings: FunctionComponent = (): ReactElement => {
|
||||
type: FieldType.Text,
|
||||
noValueMessage: "-",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
costPerMillionTokensInUSDCents: true,
|
||||
},
|
||||
title: "Cost (cents/1M)",
|
||||
type: FieldType.Number,
|
||||
noValueMessage: "0",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Page>
|
||||
|
||||
@@ -73,6 +73,7 @@ import Probe from "./Probe";
|
||||
import ProbeOwnerTeam from "./ProbeOwnerTeam";
|
||||
import ProbeOwnerUser from "./ProbeOwnerUser";
|
||||
import LlmProvider from "./LlmProvider";
|
||||
import LlmLog from "./LlmLog";
|
||||
import Project from "./Project";
|
||||
import ProjectCallSMSConfig from "./ProjectCallSMSConfig";
|
||||
// Project SMTP Config.
|
||||
@@ -379,6 +380,7 @@ const AllModelTypes: Array<{
|
||||
ProbeOwnerUser,
|
||||
|
||||
LlmProvider,
|
||||
LlmLog,
|
||||
|
||||
UserSession,
|
||||
UserTotpAuth,
|
||||
|
||||
870
Common/Models/DatabaseModels/LlmLog.ts
Normal file
870
Common/Models/DatabaseModels/LlmLog.ts
Normal file
@@ -0,0 +1,870 @@
|
||||
import Project from "./Project";
|
||||
import Incident from "./Incident";
|
||||
import Alert from "./Alert";
|
||||
import ScheduledMaintenance from "./ScheduledMaintenance";
|
||||
import User from "./User";
|
||||
import LlmProvider from "./LlmProvider";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
|
||||
import ColumnLength from "../../Types/Database/ColumnLength";
|
||||
import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
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 LlmLogStatus from "../../Types/LlmLogStatus";
|
||||
import LlmType from "../../Types/LLM/LlmType";
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
||||
|
||||
@EnableDocumentation()
|
||||
@TenantColumn("projectId")
|
||||
@TableAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
delete: [],
|
||||
update: [],
|
||||
})
|
||||
@CrudApiEndpoint(new Route("/llm-log"))
|
||||
@Entity({
|
||||
name: "LlmLog",
|
||||
})
|
||||
@TableMetadata({
|
||||
tableName: "LlmLog",
|
||||
singularName: "LLM Log",
|
||||
pluralName: "LLM Logs",
|
||||
icon: IconProp.Bolt,
|
||||
tableDescription:
|
||||
"Logs of all the LLM API calls for AI features in this project.",
|
||||
})
|
||||
export default class LlmLog extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "projectId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: Project,
|
||||
title: "Project",
|
||||
description: "Relation to Project Resource in which this object belongs",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return Project;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "projectId" })
|
||||
public project?: Project = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: true,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Project ID",
|
||||
description: "ID of your OneUptime Project in which this object belongs",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public projectId?: ObjectID = undefined;
|
||||
|
||||
// LLM Provider Relation
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "llmProviderId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: LlmProvider,
|
||||
title: "LLM Provider",
|
||||
description: "LLM Provider used for this API call",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return LlmProvider;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "SET NULL",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "llmProviderId" })
|
||||
public llmProvider?: LlmProvider = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "LLM Provider ID",
|
||||
description: "ID of LLM Provider used for this API call",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public llmProviderId?: ObjectID = undefined;
|
||||
|
||||
// Denormalized provider info for historical reference
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.ShortText,
|
||||
title: "LLM Provider Name",
|
||||
description: "Name of the LLM Provider at time of call",
|
||||
canReadOnRelationQuery: false,
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
})
|
||||
public llmProviderName?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.ShortText,
|
||||
title: "LLM Type",
|
||||
description: "Type of LLM (OpenAI, Anthropic, Ollama)",
|
||||
canReadOnRelationQuery: false,
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
})
|
||||
public llmType?: LlmType = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Model Name",
|
||||
description: "Name of the model used (e.g., gpt-4, claude-3-opus)",
|
||||
canReadOnRelationQuery: false,
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
})
|
||||
public modelName?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: true,
|
||||
type: TableColumnType.Boolean,
|
||||
title: "Is Global Provider",
|
||||
description: "Was a global LLM provider used for this call?",
|
||||
canReadOnRelationQuery: false,
|
||||
isDefaultValueColumn: true,
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: ColumnType.Boolean,
|
||||
default: false,
|
||||
})
|
||||
public isGlobalProvider?: boolean = undefined;
|
||||
|
||||
// Token usage
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: true,
|
||||
type: TableColumnType.Number,
|
||||
title: "Input Tokens",
|
||||
description: "Number of input (prompt) tokens used",
|
||||
canReadOnRelationQuery: false,
|
||||
isDefaultValueColumn: true,
|
||||
defaultValue: 0,
|
||||
})
|
||||
@Column({
|
||||
nullable: false,
|
||||
default: 0,
|
||||
type: ColumnType.Number,
|
||||
})
|
||||
public inputTokens?: number = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: true,
|
||||
type: TableColumnType.Number,
|
||||
title: "Output Tokens",
|
||||
description: "Number of output (completion) tokens used",
|
||||
canReadOnRelationQuery: false,
|
||||
isDefaultValueColumn: true,
|
||||
defaultValue: 0,
|
||||
})
|
||||
@Column({
|
||||
nullable: false,
|
||||
default: 0,
|
||||
type: ColumnType.Number,
|
||||
})
|
||||
public outputTokens?: number = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: true,
|
||||
type: TableColumnType.Number,
|
||||
title: "Total Tokens",
|
||||
description: "Total tokens used (input + output)",
|
||||
canReadOnRelationQuery: false,
|
||||
isDefaultValueColumn: true,
|
||||
defaultValue: 0,
|
||||
})
|
||||
@Column({
|
||||
nullable: false,
|
||||
default: 0,
|
||||
type: ColumnType.Number,
|
||||
})
|
||||
public totalTokens?: number = undefined;
|
||||
|
||||
// Cost tracking
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: true,
|
||||
type: TableColumnType.Number,
|
||||
title: "Cost (USD Cents)",
|
||||
description: "Total cost in USD cents",
|
||||
canReadOnRelationQuery: false,
|
||||
isDefaultValueColumn: true,
|
||||
defaultValue: 0,
|
||||
})
|
||||
@Column({
|
||||
nullable: false,
|
||||
default: 0,
|
||||
type: ColumnType.Number,
|
||||
})
|
||||
public costInUSDCents?: number = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: true,
|
||||
type: TableColumnType.Boolean,
|
||||
title: "Was Billed",
|
||||
description: "Was the project charged for this API call?",
|
||||
canReadOnRelationQuery: false,
|
||||
isDefaultValueColumn: true,
|
||||
defaultValue: false,
|
||||
})
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: ColumnType.Boolean,
|
||||
default: false,
|
||||
})
|
||||
public wasBilled?: boolean = undefined;
|
||||
|
||||
// Status
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: true,
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Status",
|
||||
description: "Status of the LLM API call",
|
||||
canReadOnRelationQuery: false,
|
||||
})
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
})
|
||||
public status?: LlmLogStatus = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.LongText,
|
||||
title: "Status Message",
|
||||
description: "Status Message (error details if failed)",
|
||||
canReadOnRelationQuery: false,
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.LongText,
|
||||
length: ColumnLength.LongText,
|
||||
})
|
||||
public statusMessage?: string = undefined;
|
||||
|
||||
// Feature identification
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.ShortText,
|
||||
title: "Feature",
|
||||
description:
|
||||
"The feature that triggered this API call (e.g., IncidentPostmortem)",
|
||||
canReadOnRelationQuery: false,
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
})
|
||||
public feature?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.VeryLongText,
|
||||
title: "Request Prompt",
|
||||
description: "The prompt sent to the LLM (truncated)",
|
||||
canReadOnRelationQuery: false,
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.VeryLongText,
|
||||
})
|
||||
public requestPrompt?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.VeryLongText,
|
||||
title: "Response Preview",
|
||||
description: "Preview of the LLM response (truncated)",
|
||||
canReadOnRelationQuery: false,
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.VeryLongText,
|
||||
})
|
||||
public responsePreview?: string = undefined;
|
||||
|
||||
// Related resources
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "incidentId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: Incident,
|
||||
title: "Incident",
|
||||
description: "Incident associated with this LLM call (if any)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return Incident;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "incidentId" })
|
||||
public incident?: Incident = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Incident ID",
|
||||
description: "ID of Incident associated with this LLM call (if any)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public incidentId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "alertId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: Alert,
|
||||
title: "Alert",
|
||||
description: "Alert associated with this LLM call (if any)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return Alert;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "alertId" })
|
||||
public alert?: Alert = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Alert ID",
|
||||
description: "ID of Alert associated with this LLM call (if any)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public alertId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "scheduledMaintenanceId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: ScheduledMaintenance,
|
||||
title: "Scheduled Maintenance",
|
||||
description: "Scheduled Maintenance associated with this LLM call (if any)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return ScheduledMaintenance;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "scheduledMaintenanceId" })
|
||||
public scheduledMaintenance?: ScheduledMaintenance = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Scheduled Maintenance ID",
|
||||
description:
|
||||
"ID of Scheduled Maintenance associated with this LLM call (if any)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public scheduledMaintenanceId?: ObjectID = undefined;
|
||||
|
||||
// User who triggered the request
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "userId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: User,
|
||||
title: "User",
|
||||
description: "User who triggered this LLM call (if any)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "userId" })
|
||||
public user?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: false,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "User ID",
|
||||
description: "ID of User who triggered this LLM call (if any)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public userId?: ObjectID = undefined;
|
||||
|
||||
// Timestamps and duration
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.Date,
|
||||
title: "Request Started At",
|
||||
description: "When the LLM request started",
|
||||
canReadOnRelationQuery: false,
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.Date,
|
||||
})
|
||||
public requestStartedAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.Date,
|
||||
title: "Request Completed At",
|
||||
description: "When the LLM request completed",
|
||||
canReadOnRelationQuery: false,
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.Date,
|
||||
})
|
||||
public requestCompletedAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadLlmLog,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.Number,
|
||||
title: "Duration (ms)",
|
||||
description: "Request duration in milliseconds",
|
||||
canReadOnRelationQuery: false,
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.Number,
|
||||
})
|
||||
public durationMs?: number = undefined;
|
||||
|
||||
// Deleted by user
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "deletedByUserId",
|
||||
type: TableColumnType.Entity,
|
||||
title: "Deleted by User",
|
||||
modelType: User,
|
||||
description:
|
||||
"Relation to User who deleted this object (if this object was deleted by a 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;
|
||||
}
|
||||
@@ -462,4 +462,25 @@ export default class LlmProvider extends BaseModel {
|
||||
default: false,
|
||||
})
|
||||
public isDefault?: boolean = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [Permission.Public],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
isDefaultValueColumn: true,
|
||||
required: true,
|
||||
type: TableColumnType.Number,
|
||||
title: "Cost Per Million Tokens (USD Cents)",
|
||||
description:
|
||||
"Cost per million tokens in USD cents. Used for billing when using global LLM providers.",
|
||||
defaultValue: 0,
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Number,
|
||||
nullable: false,
|
||||
default: 0,
|
||||
})
|
||||
public costPerMillionTokensInUSDCents?: number = undefined;
|
||||
}
|
||||
|
||||
@@ -16,13 +16,11 @@ import {
|
||||
} from "../Utils/Express";
|
||||
import CommonAPI from "./CommonAPI";
|
||||
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
||||
import LLMService, { LLMProviderConfig } from "../Utils/LLM/LLMService";
|
||||
import AILogService from "../Services/AILogService";
|
||||
import IncidentAIContextBuilder, {
|
||||
AIGenerationContext,
|
||||
IncidentContextData,
|
||||
} from "../Utils/AI/IncidentAIContextBuilder";
|
||||
import LlmProvider from "../../Models/DatabaseModels/LlmProvider";
|
||||
import LlmProviderService from "../Services/LlmProviderService";
|
||||
import JSONFunctions from "../../Types/JSONFunctions";
|
||||
import Permission from "../../Types/Permission";
|
||||
|
||||
@@ -195,22 +193,6 @@ export default class IncidentAPI extends BaseAPI<
|
||||
throw new NotFoundException("Incident not found");
|
||||
}
|
||||
|
||||
// Get LLM provider for the project
|
||||
const llmProvider: LlmProvider | null =
|
||||
await LlmProviderService.getLLMProviderForProject(incident.projectId);
|
||||
|
||||
if (!llmProvider) {
|
||||
throw new BadDataException(
|
||||
"No LLM provider configured for this project. Please configure an LLM provider in Settings > AI > LLM Providers.",
|
||||
);
|
||||
}
|
||||
|
||||
if (!llmProvider.llmType) {
|
||||
throw new BadDataException(
|
||||
"LLM provider type is not configured properly.",
|
||||
);
|
||||
}
|
||||
|
||||
// Build incident context
|
||||
const contextData: IncidentContextData =
|
||||
await IncidentAIContextBuilder.buildIncidentContext({
|
||||
@@ -226,25 +208,12 @@ export default class IncidentAPI extends BaseAPI<
|
||||
template,
|
||||
);
|
||||
|
||||
// Generate postmortem using LLM
|
||||
const llmConfig: LLMProviderConfig = {
|
||||
llmType: llmProvider.llmType,
|
||||
};
|
||||
|
||||
if (llmProvider.apiKey) {
|
||||
llmConfig.apiKey = llmProvider.apiKey;
|
||||
}
|
||||
|
||||
if (llmProvider.baseUrl) {
|
||||
llmConfig.baseUrl = llmProvider.baseUrl.toString();
|
||||
}
|
||||
|
||||
if (llmProvider.modelName) {
|
||||
llmConfig.modelName = llmProvider.modelName;
|
||||
}
|
||||
|
||||
const response = await LLMService.getCompletion({
|
||||
llmProviderConfig: llmConfig,
|
||||
// Generate postmortem using AILogService (handles billing and logging)
|
||||
const response = await AILogService.executeWithLogging({
|
||||
projectId: incident.projectId,
|
||||
userId: props.userId,
|
||||
feature: "Incident Postmortem",
|
||||
incidentId: incidentId,
|
||||
messages: aiContext.messages,
|
||||
maxTokens: 8192,
|
||||
temperature: 0.7,
|
||||
|
||||
222
Common/Server/Services/AILogService.ts
Normal file
222
Common/Server/Services/AILogService.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import { IsBillingEnabled } from "../EnvironmentConfig";
|
||||
import BaseService from "./BaseService";
|
||||
import LlmProviderService from "./LlmProviderService";
|
||||
import LlmLogService from "./LlmLogService";
|
||||
import ProjectService from "./ProjectService";
|
||||
import AIBillingService from "./AIBillingService";
|
||||
import LLMService, {
|
||||
LLMProviderConfig,
|
||||
LLMCompletionResponse,
|
||||
LLMMessage,
|
||||
} from "../Utils/LLM/LLMService";
|
||||
import LlmProvider from "../../Models/DatabaseModels/LlmProvider";
|
||||
import LlmLog from "../../Models/DatabaseModels/LlmLog";
|
||||
import LlmLogStatus from "../../Types/LlmLogStatus";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
import logger from "../Utils/Logger";
|
||||
|
||||
export interface AILogRequest {
|
||||
projectId: ObjectID;
|
||||
userId?: ObjectID;
|
||||
feature: string; // e.g., "IncidentPostmortem", "IncidentNote"
|
||||
incidentId?: ObjectID;
|
||||
alertId?: ObjectID;
|
||||
scheduledMaintenanceId?: ObjectID;
|
||||
messages: Array<LLMMessage>;
|
||||
maxTokens?: number;
|
||||
temperature?: number;
|
||||
}
|
||||
|
||||
export interface AILogResponse {
|
||||
content: string;
|
||||
llmLog: LlmLog;
|
||||
}
|
||||
|
||||
export class Service extends BaseService {
|
||||
public constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async executeWithLogging(
|
||||
request: AILogRequest,
|
||||
): Promise<AILogResponse> {
|
||||
const startTime: Date = new Date();
|
||||
|
||||
// Get LLM provider for the project
|
||||
const llmProvider: LlmProvider | null =
|
||||
await LlmProviderService.getLLMProviderForProject(request.projectId);
|
||||
|
||||
if (!llmProvider) {
|
||||
throw new BadDataException(
|
||||
"No LLM provider configured for this project. Please configure an LLM provider in Settings > AI > LLM Providers.",
|
||||
);
|
||||
}
|
||||
|
||||
if (!llmProvider.llmType) {
|
||||
throw new BadDataException(
|
||||
"LLM provider type is not configured properly.",
|
||||
);
|
||||
}
|
||||
|
||||
// Create log entry (will be updated after completion)
|
||||
const logEntry: LlmLog = new LlmLog();
|
||||
logEntry.projectId = request.projectId;
|
||||
logEntry.llmProviderId = llmProvider.id;
|
||||
logEntry.llmProviderName = llmProvider.name;
|
||||
logEntry.llmType = llmProvider.llmType;
|
||||
logEntry.modelName = llmProvider.modelName;
|
||||
logEntry.isGlobalProvider = llmProvider.isGlobalLlm || false;
|
||||
logEntry.feature = request.feature;
|
||||
logEntry.requestPrompt = request.messages
|
||||
.map((m: LLMMessage) => {
|
||||
return m.content;
|
||||
})
|
||||
.join("\n")
|
||||
.substring(0, 5000); // Store first 5000 chars
|
||||
logEntry.userId = request.userId;
|
||||
logEntry.incidentId = request.incidentId;
|
||||
logEntry.alertId = request.alertId;
|
||||
logEntry.scheduledMaintenanceId = request.scheduledMaintenanceId;
|
||||
logEntry.requestStartedAt = startTime;
|
||||
|
||||
// Check if billing should apply
|
||||
const shouldBill: boolean =
|
||||
IsBillingEnabled && (llmProvider.isGlobalLlm || false);
|
||||
|
||||
// Check balance if billing enabled and using global provider
|
||||
if (shouldBill) {
|
||||
const project = await ProjectService.findOneById({
|
||||
id: request.projectId,
|
||||
select: { aiCurrentBalanceInUSDCents: true },
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
if (!project || (project.aiCurrentBalanceInUSDCents || 0) <= 0) {
|
||||
logEntry.status = LlmLogStatus.InsufficientBalance;
|
||||
logEntry.statusMessage = "Insufficient AI balance";
|
||||
logEntry.requestCompletedAt = new Date();
|
||||
logEntry.durationMs = new Date().getTime() - startTime.getTime();
|
||||
|
||||
await LlmLogService.create({
|
||||
data: logEntry,
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
throw new BadDataException(
|
||||
"Insufficient AI balance. Please recharge your AI balance in Project Settings > Billing.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Build LLM config
|
||||
const llmConfig: LLMProviderConfig = {
|
||||
llmType: llmProvider.llmType,
|
||||
};
|
||||
|
||||
if (llmProvider.apiKey) {
|
||||
llmConfig.apiKey = llmProvider.apiKey;
|
||||
}
|
||||
|
||||
if (llmProvider.baseUrl) {
|
||||
llmConfig.baseUrl = llmProvider.baseUrl.toString();
|
||||
}
|
||||
|
||||
if (llmProvider.modelName) {
|
||||
llmConfig.modelName = llmProvider.modelName;
|
||||
}
|
||||
|
||||
// Execute LLM call
|
||||
const response: LLMCompletionResponse = await LLMService.getCompletion({
|
||||
llmProviderConfig: llmConfig,
|
||||
messages: request.messages,
|
||||
maxTokens: request.maxTokens || 4096,
|
||||
temperature: request.temperature ?? 0.7,
|
||||
});
|
||||
|
||||
const endTime: Date = new Date();
|
||||
|
||||
// Update log with success info
|
||||
logEntry.status = LlmLogStatus.Success;
|
||||
logEntry.inputTokens = response.usage?.promptTokens || 0;
|
||||
logEntry.outputTokens = response.usage?.completionTokens || 0;
|
||||
logEntry.totalTokens = response.usage?.totalTokens || 0;
|
||||
logEntry.responsePreview = response.content.substring(0, 2000); // Store first 2000 chars
|
||||
logEntry.requestCompletedAt = endTime;
|
||||
logEntry.durationMs = endTime.getTime() - startTime.getTime();
|
||||
|
||||
// Calculate and apply costs if using global provider with billing enabled
|
||||
if (shouldBill && response.usage) {
|
||||
const totalCost: number = Math.ceil(
|
||||
(response.usage.totalTokens / 1_000_000) *
|
||||
(llmProvider.costPerMillionTokensInUSDCents || 0),
|
||||
);
|
||||
|
||||
logEntry.costInUSDCents = totalCost;
|
||||
logEntry.wasBilled = true;
|
||||
|
||||
// Deduct from project balance
|
||||
if (totalCost > 0) {
|
||||
const project = await ProjectService.findOneById({
|
||||
id: request.projectId,
|
||||
select: { aiCurrentBalanceInUSDCents: true },
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
if (project) {
|
||||
const newBalance: number = Math.max(
|
||||
0,
|
||||
(project.aiCurrentBalanceInUSDCents || 0) - totalCost,
|
||||
);
|
||||
|
||||
await ProjectService.updateOneById({
|
||||
id: request.projectId,
|
||||
data: {
|
||||
aiCurrentBalanceInUSDCents: newBalance,
|
||||
},
|
||||
props: { isRoot: true },
|
||||
});
|
||||
}
|
||||
|
||||
// Check if auto-recharge is needed (do this async, don't wait)
|
||||
AIBillingService.rechargeIfBalanceIsLow(request.projectId).catch(
|
||||
(err: Error) => {
|
||||
logger.error("Error during AI balance auto-recharge check:");
|
||||
logger.error(err);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Save log entry
|
||||
const savedLog = await LlmLogService.create({
|
||||
data: logEntry,
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
return {
|
||||
content: response.content,
|
||||
llmLog: savedLog,
|
||||
};
|
||||
} catch (error) {
|
||||
// Log the error
|
||||
logEntry.status = LlmLogStatus.Error;
|
||||
logEntry.statusMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
logEntry.requestCompletedAt = new Date();
|
||||
logEntry.durationMs = new Date().getTime() - startTime.getTime();
|
||||
|
||||
await LlmLogService.create({
|
||||
data: logEntry,
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
10
Common/Server/Services/LlmLogService.ts
Normal file
10
Common/Server/Services/LlmLogService.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import DatabaseService from "./DatabaseService";
|
||||
import Model from "../../Models/DatabaseModels/LlmLog";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
super(Model);
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
@@ -108,10 +108,14 @@ export class Service extends DatabaseService<Model> {
|
||||
isDefault: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
llmType: true,
|
||||
apiKey: true,
|
||||
baseUrl: true,
|
||||
modelName: true,
|
||||
isGlobalLlm: true,
|
||||
costPerMillionTokensInUSDCents: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
@@ -129,10 +133,14 @@ export class Service extends DatabaseService<Model> {
|
||||
isGlobalLlm: true,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
llmType: true,
|
||||
apiKey: true,
|
||||
baseUrl: true,
|
||||
modelName: true,
|
||||
isGlobalLlm: true,
|
||||
costPerMillionTokensInUSDCents: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
|
||||
7
Common/Types/LlmLogStatus.ts
Normal file
7
Common/Types/LlmLogStatus.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
enum LlmLogStatus {
|
||||
Success = "Success",
|
||||
Error = "Error",
|
||||
InsufficientBalance = "Insufficient Balance",
|
||||
}
|
||||
|
||||
export default LlmLogStatus;
|
||||
@@ -147,6 +147,7 @@ enum Permission {
|
||||
ReadCallLog = "ReadCallLog",
|
||||
ReadPushLog = "ReadPushLog",
|
||||
ReadWorkspaceNotificationLog = "ReadWorkspaceNotificationLog",
|
||||
ReadLlmLog = "ReadLlmLog",
|
||||
|
||||
CreateIncidentOwnerTeam = "CreateIncidentOwnerTeam",
|
||||
DeleteIncidentOwnerTeam = "DeleteIncidentOwnerTeam",
|
||||
@@ -3143,6 +3144,14 @@ export class PermissionHelper {
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
|
||||
{
|
||||
permission: Permission.ReadLlmLog,
|
||||
title: "Read LLM Log",
|
||||
description: "This permission can read LLM Logs of this project.",
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
},
|
||||
|
||||
{
|
||||
permission: Permission.CreateMonitorProbe,
|
||||
title: "Create Monitor Probe",
|
||||
|
||||
229
Dashboard/src/Components/AILogs/LlmLogsTable.tsx
Normal file
229
Dashboard/src/Components/AILogs/LlmLogsTable.tsx
Normal file
@@ -0,0 +1,229 @@
|
||||
import React, { FunctionComponent, ReactElement, useState } from "react";
|
||||
import ModelTable from "Common/UI/Components/ModelTable/ModelTable";
|
||||
import LlmLog from "Common/Models/DatabaseModels/LlmLog";
|
||||
import FieldType from "Common/UI/Components/Types/FieldType";
|
||||
import Columns from "Common/UI/Components/ModelTable/Columns";
|
||||
import Pill from "Common/UI/Components/Pill/Pill";
|
||||
import { Green, Red, Yellow } from "Common/Types/BrandColors";
|
||||
import LlmLogStatus from "Common/Types/LlmLogStatus";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import Filter from "Common/UI/Components/ModelFilter/Filter";
|
||||
import ConfirmModal from "Common/UI/Components/Modal/ConfirmModal";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import { ButtonStyleType } from "Common/UI/Components/Button/Button";
|
||||
import Query from "Common/Types/BaseDatabase/Query";
|
||||
import BaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
||||
import UserElement from "../User/User";
|
||||
import User from "Common/Models/DatabaseModels/User";
|
||||
|
||||
export interface LlmLogsTableProps {
|
||||
query?: Query<BaseModel>;
|
||||
singularName?: string;
|
||||
}
|
||||
|
||||
const LlmLogsTable: FunctionComponent<LlmLogsTableProps> = (
|
||||
props: LlmLogsTableProps,
|
||||
): ReactElement => {
|
||||
const [showModal, setShowModal] = useState<boolean>(false);
|
||||
const [modalText, setModalText] = useState<string>("");
|
||||
const [modalTitle, setModalTitle] = useState<string>("");
|
||||
|
||||
const defaultColumns: Columns<LlmLog> = [
|
||||
{
|
||||
field: { llmProviderName: true },
|
||||
title: "Provider",
|
||||
type: FieldType.Text,
|
||||
noValueMessage: "-",
|
||||
},
|
||||
{
|
||||
field: { llmType: true },
|
||||
title: "Type",
|
||||
type: FieldType.Text,
|
||||
noValueMessage: "-",
|
||||
},
|
||||
{
|
||||
field: { modelName: true },
|
||||
title: "Model",
|
||||
type: FieldType.Text,
|
||||
noValueMessage: "-",
|
||||
},
|
||||
{
|
||||
field: { isGlobalProvider: true },
|
||||
title: "Global",
|
||||
type: FieldType.Boolean,
|
||||
},
|
||||
{
|
||||
field: { feature: true },
|
||||
title: "Feature",
|
||||
type: FieldType.Text,
|
||||
noValueMessage: "-",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
user: {
|
||||
name: true,
|
||||
email: true,
|
||||
profilePictureId: true,
|
||||
},
|
||||
},
|
||||
title: "User",
|
||||
type: FieldType.Text,
|
||||
noValueMessage: "-",
|
||||
getElement: (item: LlmLog): ReactElement => {
|
||||
if (!item["user"]) {
|
||||
return <p>-</p>;
|
||||
}
|
||||
|
||||
return <UserElement user={item["user"] as User} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: { inputTokens: true },
|
||||
title: "Input Tokens",
|
||||
type: FieldType.Number,
|
||||
},
|
||||
{
|
||||
field: { outputTokens: true },
|
||||
title: "Output Tokens",
|
||||
type: FieldType.Number,
|
||||
},
|
||||
{
|
||||
field: { costInUSDCents: true },
|
||||
title: "Cost (cents)",
|
||||
type: FieldType.Number,
|
||||
},
|
||||
{
|
||||
field: { createdAt: true },
|
||||
title: "Time",
|
||||
type: FieldType.DateTime,
|
||||
},
|
||||
{
|
||||
field: { status: true },
|
||||
title: "Status",
|
||||
type: FieldType.Text,
|
||||
getElement: (item: LlmLog): ReactElement => {
|
||||
if (item["status"]) {
|
||||
let color = Green;
|
||||
if (item["status"] === LlmLogStatus.Error) {
|
||||
color = Red;
|
||||
}
|
||||
if (item["status"] === LlmLogStatus.InsufficientBalance) {
|
||||
color = Yellow;
|
||||
}
|
||||
return (
|
||||
<Pill
|
||||
isMinimal={false}
|
||||
color={color}
|
||||
text={item["status"] as string}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const defaultFilters: Array<Filter<LlmLog>> = [
|
||||
{ field: { createdAt: true }, title: "Time", type: FieldType.Date },
|
||||
{ field: { status: true }, title: "Status", type: FieldType.Text },
|
||||
{ field: { llmType: true }, title: "Provider Type", type: FieldType.Text },
|
||||
{ field: { feature: true }, title: "Feature", type: FieldType.Text },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModelTable<LlmLog>
|
||||
modelType={LlmLog}
|
||||
id={
|
||||
props.singularName
|
||||
? `${props.singularName.replace(/\s+/g, "-").toLowerCase()}-llm-logs-table`
|
||||
: "llm-logs-table"
|
||||
}
|
||||
name="AI Logs"
|
||||
isDeleteable={false}
|
||||
isEditable={false}
|
||||
isCreateable={false}
|
||||
showViewIdButton={true}
|
||||
isViewable={false}
|
||||
query={{
|
||||
projectId: ProjectUtil.getCurrentProjectId()!,
|
||||
...(props.query || {}),
|
||||
}}
|
||||
selectMoreFields={{
|
||||
requestPrompt: true,
|
||||
responsePreview: true,
|
||||
statusMessage: true,
|
||||
}}
|
||||
cardProps={{
|
||||
title: "AI Logs",
|
||||
description: props.singularName
|
||||
? `AI usage logs for this ${props.singularName}.`
|
||||
: "AI usage logs for this project.",
|
||||
}}
|
||||
noItemsMessage={
|
||||
props.singularName
|
||||
? `No AI logs for this ${props.singularName}.`
|
||||
: "No AI logs."
|
||||
}
|
||||
showRefreshButton={true}
|
||||
columns={defaultColumns}
|
||||
filters={defaultFilters}
|
||||
actionButtons={[
|
||||
{
|
||||
title: "View Request",
|
||||
buttonStyleType: ButtonStyleType.NORMAL,
|
||||
icon: IconProp.List,
|
||||
onClick: async (item: LlmLog, onCompleteAction: VoidFunction) => {
|
||||
setModalText(
|
||||
(item["requestPrompt"] as string) || "No request data",
|
||||
);
|
||||
setModalTitle("Request Prompt");
|
||||
setShowModal(true);
|
||||
onCompleteAction();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "View Response",
|
||||
buttonStyleType: ButtonStyleType.NORMAL,
|
||||
icon: IconProp.File,
|
||||
onClick: async (item: LlmLog, onCompleteAction: VoidFunction) => {
|
||||
setModalText(
|
||||
(item["responsePreview"] as string) || "No response data",
|
||||
);
|
||||
setModalTitle("Response Preview");
|
||||
setShowModal(true);
|
||||
onCompleteAction();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "View Error",
|
||||
buttonStyleType: ButtonStyleType.NORMAL,
|
||||
icon: IconProp.Error,
|
||||
onClick: async (item: LlmLog, onCompleteAction: VoidFunction) => {
|
||||
setModalText(
|
||||
(item["statusMessage"] as string) || "No error message",
|
||||
);
|
||||
setModalTitle("Status Message");
|
||||
setShowModal(true);
|
||||
onCompleteAction();
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{showModal && (
|
||||
<ConfirmModal
|
||||
title={modalTitle}
|
||||
description={modalText}
|
||||
onSubmit={() => {
|
||||
return setShowModal(false);
|
||||
}}
|
||||
submitButtonText="Close"
|
||||
submitButtonType={ButtonStyleType.NORMAL}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LlmLogsTable;
|
||||
13
Dashboard/src/Pages/Alerts/View/AILogs.tsx
Normal file
13
Dashboard/src/Pages/Alerts/View/AILogs.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import LlmLogsTable from "../../../Components/AILogs/LlmLogsTable";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
|
||||
const AlertAILogs: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
return <LlmLogsTable singularName="alert" query={{ alertId: modelId }} />;
|
||||
};
|
||||
|
||||
export default AlertAILogs;
|
||||
@@ -100,7 +100,7 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Notification Logs">
|
||||
<SideMenuSection title="Logs">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Notification Logs",
|
||||
@@ -111,6 +111,16 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
|
||||
}}
|
||||
icon={IconProp.Bell}
|
||||
/>
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "AI Logs",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.ALERT_VIEW_AI_LOGS] as Route,
|
||||
{ modelId: props.modelId },
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Bolt}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Alert Notes">
|
||||
|
||||
13
Dashboard/src/Pages/Incidents/View/AILogs.tsx
Normal file
13
Dashboard/src/Pages/Incidents/View/AILogs.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import LlmLogsTable from "../../../Components/AILogs/LlmLogsTable";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
|
||||
const IncidentAILogs: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
return <LlmLogsTable singularName="incident" query={{ incidentId: modelId }} />;
|
||||
};
|
||||
|
||||
export default IncidentAILogs;
|
||||
@@ -111,7 +111,7 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Notification Logs">
|
||||
<SideMenuSection title="Logs">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Notification Logs",
|
||||
@@ -122,6 +122,16 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
|
||||
}}
|
||||
icon={IconProp.Bell}
|
||||
/>
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "AI Logs",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.INCIDENT_VIEW_AI_LOGS] as Route,
|
||||
{ modelId: props.modelId },
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Bolt}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Incident Notes">
|
||||
|
||||
9
Dashboard/src/Pages/Settings/AILogs.tsx
Normal file
9
Dashboard/src/Pages/Settings/AILogs.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import LlmLogsTable from "../../Components/AILogs/LlmLogsTable";
|
||||
|
||||
const SettingsAILogs: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
return <LlmLogsTable />;
|
||||
};
|
||||
|
||||
export default SettingsAILogs;
|
||||
@@ -373,6 +373,15 @@ const DashboardSideMenu: () => JSX.Element = (): ReactElement => {
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
link: {
|
||||
title: "AI Logs",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.SETTINGS_AI_LOGS] as Route,
|
||||
),
|
||||
},
|
||||
icon: IconProp.Bolt,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -31,6 +31,11 @@ const AlertViewNotificationLogs: LazyExoticComponent<
|
||||
return import("../Pages/Alerts/View/NotificationLogs");
|
||||
});
|
||||
|
||||
const AlertViewAILogs: LazyExoticComponent<FunctionComponent<ComponentProps>> =
|
||||
lazy(() => {
|
||||
return import("../Pages/Alerts/View/AILogs");
|
||||
});
|
||||
|
||||
const AlertsWorkspaceConnectionSlack: LazyExoticComponent<
|
||||
FunctionComponent<ComponentProps>
|
||||
> = lazy(() => {
|
||||
@@ -194,6 +199,18 @@ const AlertsRoutes: FunctionComponent<ComponentProps> = (
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(PageMap.ALERT_VIEW_AI_LOGS)}
|
||||
element={
|
||||
<Suspense fallback={Loader}>
|
||||
<AlertViewAILogs
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.ALERT_VIEW_AI_LOGS] as Route}
|
||||
/>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(PageMap.ALERT_VIEW_DESCRIPTION)}
|
||||
element={
|
||||
|
||||
@@ -44,6 +44,12 @@ const IncidentViewNotificationLogs: LazyExoticComponent<
|
||||
return import("../Pages/Incidents/View/NotificationLogs");
|
||||
});
|
||||
|
||||
const IncidentViewAILogs: LazyExoticComponent<
|
||||
FunctionComponent<ComponentProps>
|
||||
> = lazy(() => {
|
||||
return import("../Pages/Incidents/View/AILogs");
|
||||
});
|
||||
|
||||
const IncidentViewDelete: LazyExoticComponent<
|
||||
FunctionComponent<ComponentProps>
|
||||
> = lazy(() => {
|
||||
@@ -405,6 +411,18 @@ const IncidentsRoutes: FunctionComponent<ComponentProps> = (
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(PageMap.INCIDENT_VIEW_AI_LOGS)}
|
||||
element={
|
||||
<Suspense fallback={Loader}>
|
||||
<IncidentViewAILogs
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.INCIDENT_VIEW_AI_LOGS] as Route}
|
||||
/>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
</PageRoute>
|
||||
</Routes>
|
||||
);
|
||||
|
||||
@@ -249,6 +249,10 @@ const SettingsNotificationLogs: LazyExoticComponent<
|
||||
> = lazy(() => {
|
||||
return import("../Pages/Settings/NotificationLogs");
|
||||
});
|
||||
const SettingsAILogs: LazyExoticComponent<FunctionComponent<ComponentProps>> =
|
||||
lazy(() => {
|
||||
return import("../Pages/Settings/AILogs");
|
||||
});
|
||||
const SettingsNotifications: LazyExoticComponent<
|
||||
FunctionComponent<ComponentProps>
|
||||
> = lazy(() => {
|
||||
@@ -348,6 +352,17 @@ const SettingsRoutes: FunctionComponent<ComponentProps> = (
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(PageMap.SETTINGS_AI_LOGS)}
|
||||
element={
|
||||
<Suspense fallback={Loader}>
|
||||
<SettingsAILogs
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.SETTINGS_AI_LOGS] as Route}
|
||||
/>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(
|
||||
|
||||
@@ -89,6 +89,7 @@ enum PageMap {
|
||||
INCIDENT_VIEW_ON_CALL_POLICY_EXECUTION_LOGS = "INCIDENT_VIEW_ON_CALL_POLICY_EXECUTION_LOGS",
|
||||
|
||||
INCIDENT_VIEW_NOTIFICATION_LOGS = "INCIDENT_VIEW_NOTIFICATION_LOGS",
|
||||
INCIDENT_VIEW_AI_LOGS = "INCIDENT_VIEW_AI_LOGS",
|
||||
|
||||
ALERTS_ROOT = "ALERTS_ROOT",
|
||||
ALERTS = "ALERTS",
|
||||
@@ -107,6 +108,7 @@ enum PageMap {
|
||||
ALERT_VIEW_ON_CALL_POLICY_EXECUTION_LOGS = "ALERT_VIEW_ON_CALL_POLICY_EXECUTION_LOGS",
|
||||
|
||||
ALERT_VIEW_NOTIFICATION_LOGS = "ALERT_VIEW_NOTIFICATION_LOGS",
|
||||
ALERT_VIEW_AI_LOGS = "ALERT_VIEW_AI_LOGS",
|
||||
|
||||
SCHEDULED_MAINTENANCE_EVENTS_ROOT = "SCHEDULED_MAINTENANCE_EVENTS_ROOT",
|
||||
SCHEDULED_MAINTENANCE_EVENTS = "SCHEDULED_MAINTENANCE_EVENTS",
|
||||
@@ -412,6 +414,9 @@ enum PageMap {
|
||||
|
||||
SETTINGS_NOTIFICATION_LOGS = "SETTINGS_NOTIFICATION_LOGS",
|
||||
|
||||
// AI Logs
|
||||
SETTINGS_AI_LOGS = "SETTINGS_AI_LOGS",
|
||||
|
||||
// Push Logs in resource views
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +165,7 @@ export const IncidentsRoutePath: Dictionary<string> = {
|
||||
[PageMap.INCIDENT_VIEW_OWNERS]: `${RouteParams.ModelID}/owners`,
|
||||
[PageMap.INCIDENT_VIEW_ON_CALL_POLICY_EXECUTION_LOGS]: `${RouteParams.ModelID}/on-call-policy-execution-logs`,
|
||||
[PageMap.INCIDENT_VIEW_NOTIFICATION_LOGS]: `${RouteParams.ModelID}/notification-logs`,
|
||||
[PageMap.INCIDENT_VIEW_AI_LOGS]: `${RouteParams.ModelID}/ai-logs`,
|
||||
[PageMap.INCIDENT_VIEW_DELETE]: `${RouteParams.ModelID}/delete`,
|
||||
[PageMap.INCIDENT_VIEW_SETTINGS]: `${RouteParams.ModelID}/settings`,
|
||||
[PageMap.INCIDENT_VIEW_CUSTOM_FIELDS]: `${RouteParams.ModelID}/custom-fields`,
|
||||
@@ -182,6 +183,7 @@ export const AlertsRoutePath: Dictionary<string> = {
|
||||
[PageMap.ALERT_VIEW_OWNERS]: `${RouteParams.ModelID}/owners`,
|
||||
[PageMap.ALERT_VIEW_ON_CALL_POLICY_EXECUTION_LOGS]: `${RouteParams.ModelID}/on-call-policy-execution-logs`,
|
||||
[PageMap.ALERT_VIEW_NOTIFICATION_LOGS]: `${RouteParams.ModelID}/notification-logs`,
|
||||
[PageMap.ALERT_VIEW_AI_LOGS]: `${RouteParams.ModelID}/ai-logs`,
|
||||
[PageMap.ALERT_VIEW_DELETE]: `${RouteParams.ModelID}/delete`,
|
||||
[PageMap.ALERT_VIEW_DESCRIPTION]: `${RouteParams.ModelID}/description`,
|
||||
[PageMap.ALERT_VIEW_ROOT_CAUSE]: `${RouteParams.ModelID}/root-cause`,
|
||||
@@ -213,6 +215,7 @@ export const SettingsRoutePath: Dictionary<string> = {
|
||||
[PageMap.SETTINGS_DANGERZONE]: "danger-zone",
|
||||
[PageMap.SETTINGS_NOTIFICATION_SETTINGS]: "notification-settings",
|
||||
[PageMap.SETTINGS_NOTIFICATION_LOGS]: "notification-logs",
|
||||
[PageMap.SETTINGS_AI_LOGS]: "ai-logs",
|
||||
[PageMap.SETTINGS_APIKEYS]: `api-keys`,
|
||||
[PageMap.SETTINGS_APIKEY_VIEW]: `api-keys/${RouteParams.ModelID}`,
|
||||
[PageMap.SETTINGS_TELEMETRY_INGESTION_KEYS]: `telemetry-ingestion-keys`,
|
||||
@@ -541,6 +544,12 @@ const RouteMap: Dictionary<Route> = {
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.ALERT_VIEW_AI_LOGS]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/alerts/${
|
||||
AlertsRoutePath[PageMap.ALERT_VIEW_AI_LOGS]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.ALERT_VIEW_DELETE]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/alerts/${
|
||||
AlertsRoutePath[PageMap.ALERT_VIEW_DELETE]
|
||||
@@ -682,6 +691,12 @@ const RouteMap: Dictionary<Route> = {
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.INCIDENT_VIEW_AI_LOGS]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/incidents/${
|
||||
IncidentsRoutePath[PageMap.INCIDENT_VIEW_AI_LOGS]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.INCIDENT_VIEW_DELETE]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/incidents/${
|
||||
IncidentsRoutePath[PageMap.INCIDENT_VIEW_DELETE]
|
||||
@@ -1635,6 +1650,12 @@ const RouteMap: Dictionary<Route> = {
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.SETTINGS_AI_LOGS]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/settings/${
|
||||
SettingsRoutePath[PageMap.SETTINGS_AI_LOGS]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.SETTINGS_APIKEYS]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/settings/${
|
||||
SettingsRoutePath[PageMap.SETTINGS_APIKEYS]
|
||||
|
||||
Reference in New Issue
Block a user