anything-llm/server/endpoints/api/admin/index.js
Timothy Carambat dd7c4675d3
LLM performance metric tracking ()
* WIP performance metric tracking

* fix: patch UI trying to .toFixed() null metric
Anthropic tracking migraiton
cleanup logs

* Apipie implmentation, not tested

* Cleanup Anthropic notes, Add support for AzureOpenAI tracking

* bedrock token metric tracking

* Cohere support

* feat: improve default stream handler to track for provider who are actually OpenAI compliant in usage reporting
add deepseek support

* feat: Add FireworksAI tracking reporting
fix: improve handler when usage:null is reported (why?)

* Add token reporting for GenericOpenAI

* token reporting for koboldcpp + lmstudio

* lint

* support Groq token tracking

* HF token tracking

* token tracking for togetherai

* LiteLLM token tracking

* linting + Mitral token tracking support

* XAI token metric reporting

* native provider runner

* LocalAI token tracking

* Novita token tracking

* OpenRouter token tracking

* Apipie stream metrics

* textwebgenui token tracking

* perplexity token reporting

* ollama token reporting

* lint

* put back comment

* Rip out LC ollama wrapper and use official library

* patch images with new ollama lib

* improve ollama offline message

* fix image handling in ollama llm provider

* lint

* NVIDIA NIM token tracking

* update openai compatbility responses

* UI/UX show/hide metrics on click for user preference

* update bedrock client

---------

Co-authored-by: shatfield4 <seanhatfield5@gmail.com>
2024-12-16 14:31:17 -08:00

775 lines
21 KiB
JavaScript

const { EventLogs } = require("../../../models/eventLogs");
const { Invite } = require("../../../models/invite");
const { SystemSettings } = require("../../../models/systemSettings");
const { User } = require("../../../models/user");
const { Workspace } = require("../../../models/workspace");
const { WorkspaceChats } = require("../../../models/workspaceChats");
const { WorkspaceUser } = require("../../../models/workspaceUsers");
const { canModifyAdmin } = require("../../../utils/helpers/admin");
const { multiUserMode, reqBody } = require("../../../utils/http");
const { validApiKey } = require("../../../utils/middleware/validApiKey");
function apiAdminEndpoints(app) {
if (!app) return;
app.get("/v1/admin/is-multi-user-mode", [validApiKey], (_, response) => {
/*
#swagger.tags = ['Admin']
#swagger.description = 'Check to see if the instance is in multi-user-mode first. Methods are disabled until multi user mode is enabled via the UI.'
#swagger.responses[200] = {
content: {
"application/json": {
schema: {
type: 'object',
example: {
"isMultiUser": true
}
}
}
}
}
#swagger.responses[403] = {
schema: {
"$ref": "#/definitions/InvalidAPIKey"
}
}
*/
const isMultiUser = multiUserMode(response);
response.status(200).json({ isMultiUser });
});
app.get("/v1/admin/users", [validApiKey], async (request, response) => {
/*
#swagger.tags = ['Admin']
#swagger.description = 'Check to see if the instance is in multi-user-mode first. Methods are disabled until multi user mode is enabled via the UI.'
#swagger.responses[200] = {
content: {
"application/json": {
schema: {
type: 'object',
example: {
"users": [
{
username: "sample-sam",
role: 'default',
}
]
}
}
}
}
}
#swagger.responses[403] = {
schema: {
"$ref": "#/definitions/InvalidAPIKey"
}
}
#swagger.responses[401] = {
description: "Instance is not in Multi-User mode. Method denied",
}
*/
try {
if (!multiUserMode(response)) {
response.sendStatus(401).end();
return;
}
const users = await User.where();
response.status(200).json({ users });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
});
app.post("/v1/admin/users/new", [validApiKey], async (request, response) => {
/*
#swagger.tags = ['Admin']
#swagger.description = 'Create a new user with username and password. Methods are disabled until multi user mode is enabled via the UI.'
#swagger.requestBody = {
description: 'Key pair object that will define the new user to add to the system.',
required: true,
content: {
"application/json": {
example: {
username: "sample-sam",
password: 'hunter2',
role: 'default | admin'
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
schema: {
type: 'object',
example: {
user: {
id: 1,
username: 'sample-sam',
role: 'default',
},
error: null,
}
}
}
}
}
#swagger.responses[403] = {
schema: {
"$ref": "#/definitions/InvalidAPIKey"
}
}
#swagger.responses[401] = {
description: "Instance is not in Multi-User mode. Method denied",
}
*/
try {
if (!multiUserMode(response)) {
response.sendStatus(401).end();
return;
}
const newUserParams = reqBody(request);
const { user: newUser, error } = await User.create(newUserParams);
response.status(newUser ? 200 : 400).json({ user: newUser, error });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
});
app.post("/v1/admin/users/:id", [validApiKey], async (request, response) => {
/*
#swagger.tags = ['Admin']
#swagger.parameters['id'] = {
in: 'path',
description: 'id of the user in the database.',
required: true,
type: 'string'
}
#swagger.description = 'Update existing user settings. Methods are disabled until multi user mode is enabled via the UI.'
#swagger.requestBody = {
description: 'Key pair object that will update the found user. All fields are optional and will not update unless specified.',
required: true,
content: {
"application/json": {
example: {
username: "sample-sam",
password: 'hunter2',
role: 'default | admin',
suspended: 0,
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
schema: {
type: 'object',
example: {
success: true,
error: null,
}
}
}
}
}
#swagger.responses[403] = {
schema: {
"$ref": "#/definitions/InvalidAPIKey"
}
}
#swagger.responses[401] = {
description: "Instance is not in Multi-User mode. Method denied",
}
*/
try {
if (!multiUserMode(response)) {
response.sendStatus(401).end();
return;
}
const { id } = request.params;
const updates = reqBody(request);
const user = await User.get({ id: Number(id) });
const validAdminRoleModification = await canModifyAdmin(user, updates);
if (!validAdminRoleModification.valid) {
response
.status(200)
.json({ success: false, error: validAdminRoleModification.error });
return;
}
const { success, error } = await User.update(id, updates);
response.status(200).json({ success, error });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
});
app.delete(
"/v1/admin/users/:id",
[validApiKey],
async (request, response) => {
/*
#swagger.tags = ['Admin']
#swagger.description = 'Delete existing user by id. Methods are disabled until multi user mode is enabled via the UI.'
#swagger.parameters['id'] = {
in: 'path',
description: 'id of the user in the database.',
required: true,
type: 'string'
}
#swagger.responses[200] = {
content: {
"application/json": {
schema: {
type: 'object',
example: {
success: true,
error: null,
}
}
}
}
}
#swagger.responses[403] = {
schema: {
"$ref": "#/definitions/InvalidAPIKey"
}
}
#swagger.responses[401] = {
description: "Instance is not in Multi-User mode. Method denied",
}
*/
try {
if (!multiUserMode(response)) {
response.sendStatus(401).end();
return;
}
const { id } = request.params;
const user = await User.get({ id: Number(id) });
await User.delete({ id: user.id });
await EventLogs.logEvent("api_user_deleted", {
userName: user.username,
});
response.status(200).json({ success: true, error: null });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
app.get("/v1/admin/invites", [validApiKey], async (request, response) => {
/*
#swagger.tags = ['Admin']
#swagger.description = 'List all existing invitations to instance regardless of status. Methods are disabled until multi user mode is enabled via the UI.'
#swagger.responses[200] = {
content: {
"application/json": {
schema: {
type: 'object',
example: {
"invites": [
{
id: 1,
status: "pending",
code: 'abc-123',
claimedBy: null
}
]
}
}
}
}
}
#swagger.responses[403] = {
schema: {
"$ref": "#/definitions/InvalidAPIKey"
}
}
#swagger.responses[401] = {
description: "Instance is not in Multi-User mode. Method denied",
}
*/
try {
if (!multiUserMode(response)) {
response.sendStatus(401).end();
return;
}
const invites = await Invite.whereWithUsers();
response.status(200).json({ invites });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
});
app.post("/v1/admin/invite/new", [validApiKey], async (request, response) => {
/*
#swagger.tags = ['Admin']
#swagger.description = 'Create a new invite code for someone to use to register with instance. Methods are disabled until multi user mode is enabled via the UI.'
#swagger.requestBody = {
description: 'Request body for creation parameters of the invitation',
required: false,
content: {
"application/json": {
example: {
workspaceIds: [1,2,45],
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
schema: {
type: 'object',
example: {
invite: {
id: 1,
status: "pending",
code: 'abc-123',
},
error: null,
}
}
}
}
}
#swagger.responses[403] = {
schema: {
"$ref": "#/definitions/InvalidAPIKey"
}
}
#swagger.responses[401] = {
description: "Instance is not in Multi-User mode. Method denied",
}
*/
try {
if (!multiUserMode(response)) {
response.sendStatus(401).end();
return;
}
const body = reqBody(request);
const { invite, error } = await Invite.create({
workspaceIds: body?.workspaceIds ?? [],
});
response.status(200).json({ invite, error });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
});
app.delete(
"/v1/admin/invite/:id",
[validApiKey],
async (request, response) => {
/*
#swagger.tags = ['Admin']
#swagger.description = 'Deactivates (soft-delete) invite by id. Methods are disabled until multi user mode is enabled via the UI.'
#swagger.parameters['id'] = {
in: 'path',
description: 'id of the invite in the database.',
required: true,
type: 'string'
}
#swagger.responses[200] = {
content: {
"application/json": {
schema: {
type: 'object',
example: {
success: true,
error: null,
}
}
}
}
}
#swagger.responses[403] = {
schema: {
"$ref": "#/definitions/InvalidAPIKey"
}
}
#swagger.responses[401] = {
description: "Instance is not in Multi-User mode. Method denied",
}
*/
try {
if (!multiUserMode(response)) {
response.sendStatus(401).end();
return;
}
const { id } = request.params;
const { success, error } = await Invite.deactivate(id);
response.status(200).json({ success, error });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
app.get(
"/v1/admin/workspaces/:workspaceId/users",
[validApiKey],
async (request, response) => {
/*
#swagger.tags = ['Admin']
#swagger.parameters['workspaceId'] = {
in: 'path',
description: 'id of the workspace.',
required: true,
type: 'string'
}
#swagger.description = 'Retrieve a list of users with permissions to access the specified workspace.'
#swagger.responses[200] = {
content: {
"application/json": {
schema: {
type: 'object',
example: {
users: [
{"userId": 1, "role": "admin"},
{"userId": 2, "role": "member"}
]
}
}
}
}
}
#swagger.responses[403] = {
schema: {
"$ref": "#/definitions/InvalidAPIKey"
}
}
#swagger.responses[401] = {
description: "Instance is not in Multi-User mode. Method denied",
}
*/
try {
if (!multiUserMode(response)) {
response.sendStatus(401).end();
return;
}
const workspaceId = request.params.workspaceId;
const users = await Workspace.workspaceUsers(workspaceId);
response.status(200).json({ users });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
app.post(
"/v1/admin/workspaces/:workspaceId/update-users",
[validApiKey],
async (request, response) => {
/*
#swagger.tags = ['Admin']
#swagger.deprecated = true
#swagger.parameters['workspaceId'] = {
in: 'path',
description: 'id of the workspace in the database.',
required: true,
type: 'string'
}
#swagger.description = 'Overwrite workspace permissions to only be accessible by the given user ids and admins. Methods are disabled until multi user mode is enabled via the UI.'
#swagger.requestBody = {
description: 'Entire array of user ids who can access the workspace. All fields are optional and will not update unless specified.',
required: true,
content: {
"application/json": {
example: {
userIds: [1,2,4,12],
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
schema: {
type: 'object',
example: {
success: true,
error: null,
}
}
}
}
}
#swagger.responses[403] = {
schema: {
"$ref": "#/definitions/InvalidAPIKey"
}
}
#swagger.responses[401] = {
description: "Instance is not in Multi-User mode. Method denied",
}
*/
try {
if (!multiUserMode(response)) {
response.sendStatus(401).end();
return;
}
const { workspaceId } = request.params;
const { userIds } = reqBody(request);
const { success, error } = await Workspace.updateUsers(
workspaceId,
userIds
);
response.status(200).json({ success, error });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
app.post(
"/v1/admin/workspaces/:workspaceSlug/manage-users",
[validApiKey],
async (request, response) => {
/*
#swagger.tags = ['Admin']
#swagger.parameters['workspaceSlug'] = {
in: 'path',
description: 'slug of the workspace in the database',
required: true,
type: 'string'
}
#swagger.description = 'Set workspace permissions to be accessible by the given user ids and admins. Methods are disabled until multi user mode is enabled via the UI.'
#swagger.requestBody = {
description: 'Array of user ids who will be given access to the target workspace. <code>reset</code> will remove all existing users from the workspace and only add the new users - default <code>false</code>.',
required: true,
content: {
"application/json": {
example: {
userIds: [1,2,4,12],
reset: false
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
schema: {
type: 'object',
example: {
success: true,
error: null,
users: [
{"userId": 1, "username": "main-admin", "role": "admin"},
{"userId": 2, "username": "sample-sam", "role": "default"}
]
}
}
}
}
}
#swagger.responses[403] = {
schema: {
"$ref": "#/definitions/InvalidAPIKey"
}
}
#swagger.responses[401] = {
description: "Instance is not in Multi-User mode. Method denied",
}
*/
try {
if (!multiUserMode(response)) {
response.sendStatus(401).end();
return;
}
const { workspaceSlug } = request.params;
const { userIds: _uids, reset = false } = reqBody(request);
const userIds = (
await User.where({ id: { in: _uids.map(Number) } })
).map((user) => user.id);
const workspace = await Workspace.get({ slug: String(workspaceSlug) });
const workspaceUsers = await Workspace.workspaceUsers(workspace.id);
if (!workspace) {
response.status(404).json({
success: false,
error: `Workspace ${workspaceSlug} not found`,
users: workspaceUsers,
});
return;
}
if (userIds.length === 0) {
response.status(404).json({
success: false,
error: `No valid user IDs provided.`,
users: workspaceUsers,
});
return;
}
// Reset all users in the workspace and add the new users as the only users in the workspace
if (reset) {
const { success, error } = await Workspace.updateUsers(
workspace.id,
userIds
);
return response.status(200).json({
success,
error,
users: await Workspace.workspaceUsers(workspace.id),
});
}
// Add new users to the workspace if they are not already in the workspace
const existingUserIds = workspaceUsers.map((user) => user.userId);
const usersToAdd = userIds.filter(
(userId) => !existingUserIds.includes(userId)
);
if (usersToAdd.length > 0)
await WorkspaceUser.createManyUsers(usersToAdd, workspace.id);
response.status(200).json({
success: true,
error: null,
users: await Workspace.workspaceUsers(workspace.id),
});
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
app.post(
"/v1/admin/workspace-chats",
[validApiKey],
async (request, response) => {
/*
#swagger.tags = ['Admin']
#swagger.description = 'All chats in the system ordered by most recent. Methods are disabled until multi user mode is enabled via the UI.'
#swagger.requestBody = {
description: 'Page offset to show of workspace chats. All fields are optional and will not update unless specified.',
required: false,
content: {
"application/json": {
example: {
offset: 2,
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
schema: {
type: 'object',
example: {
success: true,
error: null,
}
}
}
}
}
#swagger.responses[403] = {
schema: {
"$ref": "#/definitions/InvalidAPIKey"
}
}
*/
try {
const pgSize = 20;
const { offset = 0 } = reqBody(request);
const chats = await WorkspaceChats.whereWithData(
{},
pgSize,
offset * pgSize,
{ id: "desc" }
);
const hasPages = (await WorkspaceChats.count()) > (offset + 1) * pgSize;
response.status(200).json({ chats: chats, hasPages });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
app.post(
"/v1/admin/preferences",
[validApiKey],
async (request, response) => {
/*
#swagger.tags = ['Admin']
#swagger.description = 'Update multi-user preferences for instance. Methods are disabled until multi user mode is enabled via the UI.'
#swagger.requestBody = {
description: 'Object with setting key and new value to set. All keys are optional and will not update unless specified.',
required: true,
content: {
"application/json": {
example: {
support_email: "support@example.com",
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
schema: {
type: 'object',
example: {
success: true,
error: null,
}
}
}
}
}
#swagger.responses[403] = {
schema: {
"$ref": "#/definitions/InvalidAPIKey"
}
}
#swagger.responses[401] = {
description: "Instance is not in Multi-User mode. Method denied",
}
*/
try {
if (!multiUserMode(response)) {
response.sendStatus(401).end();
return;
}
const updates = reqBody(request);
await SystemSettings.updateSettings(updates);
response.status(200).json({ success: true, error: null });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
}
module.exports = { apiAdminEndpoints };