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:
Nawaz Dhandala
2025-12-15 20:13:36 +00:00
parent 8fda0325d9
commit 035edaf435
22 changed files with 1551 additions and 40 deletions

View File

@@ -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>

View File

@@ -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,

View 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;
}

View File

@@ -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;
}

View File

@@ -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,

View 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();

View 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();

View File

@@ -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,

View File

@@ -0,0 +1,7 @@
enum LlmLogStatus {
Success = "Success",
Error = "Error",
InsufficientBalance = "Insufficient Balance",
}
export default LlmLogStatus;

View File

@@ -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",

View 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;

View 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;

View File

@@ -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">

View 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;

View File

@@ -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">

View 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;

View File

@@ -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,
},
],
},
{

View File

@@ -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={

View File

@@ -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>
);

View File

@@ -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(

View File

@@ -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
}

View File

@@ -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]