adds STATIC_SERVICE_INFO config

This commit is contained in:
nai-degen 2023-11-15 17:12:07 -06:00
parent 3aca9e90f0
commit 3de79873e9
5 changed files with 75 additions and 37 deletions

View File

@ -4,6 +4,7 @@ import { HttpError } from "../shared/errors";
import { injectLocals } from "../shared/inject-locals"; import { injectLocals } from "../shared/inject-locals";
import { withSession } from "../shared/with-session"; import { withSession } from "../shared/with-session";
import { injectCsrfToken, checkCsrfToken } from "../shared/inject-csrf"; import { injectCsrfToken, checkCsrfToken } from "../shared/inject-csrf";
import { buildInfoPageHtml } from "../info-page";
import { loginRouter } from "./login"; import { loginRouter } from "./login";
import { usersApiRouter as apiRouter } from "./api/users"; import { usersApiRouter as apiRouter } from "./api/users";
import { usersWebRouter as webRouter } from "./web/manage"; import { usersWebRouter as webRouter } from "./web/manage";
@ -23,6 +24,11 @@ adminRouter.use(checkCsrfToken);
adminRouter.use(injectLocals); adminRouter.use(injectLocals);
adminRouter.use("/", loginRouter); adminRouter.use("/", loginRouter);
adminRouter.use("/manage", authorize({ via: "cookie" }), webRouter); adminRouter.use("/manage", authorize({ via: "cookie" }), webRouter);
adminRouter.use("/service-info", authorize({ via: "cookie" }), (req, res) => {
return res.send(
buildInfoPageHtml(req.protocol + "://" + req.get("host"), true)
);
});
adminRouter.use( adminRouter.use(
( (

View File

@ -1,5 +1,11 @@
<%- include("partials/shared_header", { title: "OAI Reverse Proxy Admin" }) %> <%- include("partials/shared_header", { title: "OAI Reverse Proxy Admin" }) %>
<h1>OAI Reverse Proxy Admin</h1> <h1>OAI Reverse Proxy Admin</h1>
<% if (!usersEnabled) { %>
<p style="color: red; background-color: #eedddd; padding: 1em">
<strong>🚨 <code>user_token</code> gatekeeper is not enabled.</strong><br />
<br />None of the user management features will do anything.
</p>
<% } %>
<% if (!persistenceEnabled) { %> <% if (!persistenceEnabled) { %>
<p style="color: red; background-color: #eedddd; padding: 1em"> <p style="color: red; background-color: #eedddd; padding: 1em">
<strong>⚠️ Users will be lost when the server restarts because persistence is not configured.</strong><br /> <strong>⚠️ Users will be lost when the server restarts because persistence is not configured.</strong><br />
@ -19,6 +25,7 @@
<li><a href="/admin/manage/import-users">Import Users</a></li> <li><a href="/admin/manage/import-users">Import Users</a></li>
<li><a href="/admin/manage/export-users">Export Users</a></li> <li><a href="/admin/manage/export-users">Export Users</a></li>
<li><a href="/admin/manage/download-stats">Download Rentry Stats</a> <li><a href="/admin/manage/download-stats">Download Rentry Stats</a>
<li><a href="/admin/service-info">Service Info</a></li>
</ul> </ul>
<h3>Maintenance</h3> <h3>Maintenance</h3>
<form id="maintenanceForm" action="/admin/manage/maintenance" method="post"> <form id="maintenanceForm" action="/admin/manage/maintenance" method="post">

View File

@ -171,6 +171,13 @@ type Config = {
* the admin UI to used over HTTP. * the admin UI to used over HTTP.
*/ */
useInsecureCookies: boolean; useInsecureCookies: boolean;
/**
* Whether to use a more minimal public Service Info page with static content.
* This disables all stats, including traffic, keys, and queue info.
* The full info page will appear if you have signed in as an admin using the
* configured ADMIN_KEY and go to /admin/service-info.
**/
staticServiceInfo?: boolean;
}; };
// To change configs, create a file called .env in the root directory. // To change configs, create a file called .env in the root directory.
@ -249,6 +256,7 @@ export const config: Config = {
allowNicknameChanges: getEnvWithDefault("ALLOW_NICKNAME_CHANGES", true), allowNicknameChanges: getEnvWithDefault("ALLOW_NICKNAME_CHANGES", true),
showRecentImages: getEnvWithDefault("SHOW_RECENT_IMAGES", true), showRecentImages: getEnvWithDefault("SHOW_RECENT_IMAGES", true),
useInsecureCookies: getEnvWithDefault("USE_INSECURE_COOKIES", isDev), useInsecureCookies: getEnvWithDefault("USE_INSECURE_COOKIES", isDev),
staticServiceInfo: getEnvWithDefault("STATIC_SERVICE_INFO", false),
} as const; } as const;
function generateCookieSecret() { function generateCookieSecret() {
@ -314,7 +322,8 @@ export async function assertConfigIsValid() {
// them to users. // them to users.
for (const key of getKeys(config)) { for (const key of getKeys(config)) {
const maybeSensitive = ["key", "credentials", "secret", "password"].some( const maybeSensitive = ["key", "credentials", "secret", "password"].some(
(sensitive) => key.toLowerCase().includes(sensitive) (sensitive) =>
key.toLowerCase().includes(sensitive) && !["checkKeys"].includes(key)
); );
const secured = new Set([...SENSITIVE_KEYS, ...OMITTED_KEYS]); const secured = new Set([...SENSITIVE_KEYS, ...OMITTED_KEYS]);
if (maybeSensitive && !secured.has(key)) if (maybeSensitive && !secured.has(key))
@ -345,7 +354,6 @@ export const OMITTED_KEYS: (keyof Config)[] = [
"awsCredentials", "awsCredentials",
"proxyKey", "proxyKey",
"adminKey", "adminKey",
"checkKeys",
"rejectPhrases", "rejectPhrases",
"showTokenCosts", "showTokenCosts",
"googleSheetsKey", "googleSheetsKey",
@ -359,6 +367,7 @@ export const OMITTED_KEYS: (keyof Config)[] = [
"allowNicknameChanges", "allowNicknameChanges",
"showRecentImages", "showRecentImages",
"useInsecureCookies", "useInsecureCookies",
"staticServiceInfo",
]; ];
const getKeys = Object.keys as <T extends object>(obj: T) => Array<keyof T>; const getKeys = Object.keys as <T extends object>(obj: T) => Array<keyof T>;

View File

@ -4,10 +4,10 @@ import showdown from "showdown";
import { config, listConfig } from "./config"; import { config, listConfig } from "./config";
import { import {
AnthropicKey, AnthropicKey,
GooglePalmKey,
OpenAIKey,
AwsBedrockKey, AwsBedrockKey,
GooglePalmKey,
keyPool, keyPool,
OpenAIKey,
} from "./shared/key-management"; } from "./shared/key-management";
import { ModelFamily, OpenAIModelFamily } from "./shared/models"; import { ModelFamily, OpenAIModelFamily } from "./shared/models";
import { getUniqueIps } from "./proxy/rate-limit"; import { getUniqueIps } from "./proxy/rate-limit";
@ -72,7 +72,10 @@ export const handleInfoPage = (req: Request, res: Response) => {
? getExternalUrlForHuggingfaceSpaceId(process.env.SPACE_ID) ? getExternalUrlForHuggingfaceSpaceId(process.env.SPACE_ID)
: req.protocol + "://" + req.get("host"); : req.protocol + "://" + req.get("host");
res.send(cacheInfoPageHtml(baseUrl)); infoPageHtml = buildInfoPageHtml(baseUrl);
infoPageLastUpdated = Date.now();
res.send(infoPageHtml);
}; };
function getCostString(cost: number) { function getCostString(cost: number) {
@ -80,8 +83,9 @@ function getCostString(cost: number) {
return ` ($${cost.toFixed(2)})`; return ` ($${cost.toFixed(2)})`;
} }
function cacheInfoPageHtml(baseUrl: string) { export function buildInfoPageHtml(baseUrl: string, asAdmin = false) {
const keys = keyPool.list(); const keys = keyPool.list();
const hideFullInfo = config.staticServiceInfo && !asAdmin;
modelStats.clear(); modelStats.clear();
serviceStats.clear(); serviceStats.clear();
@ -97,31 +101,45 @@ function cacheInfoPageHtml(baseUrl: string) {
const allowDalle = config.allowedModelFamilies.includes("dall-e"); const allowDalle = config.allowedModelFamilies.includes("dall-e");
const info = { const endpoints = {
uptime: Math.floor(process.uptime()), ...(openaiKeys ? { openai: baseUrl + "/proxy/openai" } : {}),
endpoints: { ...(openaiKeys
...(openaiKeys ? { openai: baseUrl + "/proxy/openai" } : {}), ? { ["openai2"]: baseUrl + "/proxy/openai/turbo-instruct" }
...(openaiKeys : {}),
? { ["openai2"]: baseUrl + "/proxy/openai/turbo-instruct" } ...(openaiKeys && allowDalle
: {}), ? { ["openai-image"]: baseUrl + "/proxy/openai-image" }
...(openaiKeys && allowDalle : {}),
? { ["openai-image"]: baseUrl + "/proxy/openai-image" } ...(anthropicKeys ? { anthropic: baseUrl + "/proxy/anthropic" } : {}),
: {}), ...(palmKeys ? { "google-palm": baseUrl + "/proxy/google-palm" } : {}),
...(anthropicKeys ? { anthropic: baseUrl + "/proxy/anthropic" } : {}), ...(awsKeys ? { aws: baseUrl + "/proxy/aws/claude" } : {}),
...(palmKeys ? { "google-palm": baseUrl + "/proxy/google-palm" } : {}), };
...(awsKeys ? { aws: baseUrl + "/proxy/aws/claude" } : {}),
}, const stats = {
proompts, proompts,
tookens: `${prettyTokens(tokens)}${getCostString(tokenCost)}`, tookens: `${prettyTokens(tokens)}${getCostString(tokenCost)}`,
...(config.textModelRateLimit ? { proomptersNow: getUniqueIps() } : {}), ...(config.textModelRateLimit ? { proomptersNow: getUniqueIps() } : {}),
};
const keyInfo = {
openaiKeys, openaiKeys,
anthropicKeys, anthropicKeys,
palmKeys, palmKeys,
awsKeys, awsKeys,
};
const providerInfo = {
...(openaiKeys ? getOpenAIInfo() : {}), ...(openaiKeys ? getOpenAIInfo() : {}),
...(anthropicKeys ? getAnthropicInfo() : {}), ...(anthropicKeys ? getAnthropicInfo() : {}),
...(palmKeys ? { "palm-bison": getPalmInfo() } : {}), ...(palmKeys ? { "palm-bison": getPalmInfo() } : {}),
...(awsKeys ? { "aws-claude": getAwsInfo() } : {}), ...(awsKeys ? { "aws-claude": getAwsInfo() } : {}),
}
const info = {
uptime: Math.floor(process.uptime()),
endpoints,
...(hideFullInfo ? {} : stats),
...keyInfo,
...(hideFullInfo ? {} : providerInfo),
config: listConfig(), config: listConfig(),
build: process.env.BUILD_INFO || "dev", build: process.env.BUILD_INFO || "dev",
}; };
@ -129,7 +147,7 @@ function cacheInfoPageHtml(baseUrl: string) {
const title = getServerTitle(); const title = getServerTitle();
const headerHtml = buildInfoPageHeader(new showdown.Converter(), title); const headerHtml = buildInfoPageHeader(new showdown.Converter(), title);
const pageBody = `<!DOCTYPE html> return `<!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
@ -144,11 +162,6 @@ function cacheInfoPageHtml(baseUrl: string) {
${getSelfServiceLinks()} ${getSelfServiceLinks()}
</body> </body>
</html>`; </html>`;
infoPageHtml = pageBody;
infoPageLastUpdated = Date.now();
return pageBody;
} }
function getUniqueOpenAIOrgs(keys: KeyPoolKey[]) { function getUniqueOpenAIOrgs(keys: KeyPoolKey[]) {
@ -402,8 +415,8 @@ function getAwsInfo() {
} }
const customGreeting = fs.existsSync("greeting.md") const customGreeting = fs.existsSync("greeting.md")
? fs.readFileSync("greeting.md", "utf8") ? `\n## Server Greeting\n${fs.readFileSync("greeting.md", "utf8")}`
: null; : "";
/** /**
* If the server operator provides a `greeting.md` file, it will be included in * If the server operator provides a `greeting.md` file, it will be included in
@ -422,8 +435,12 @@ Logs are anonymous and do not contain IP addresses or timestamps. [You can see t
**If you are uncomfortable with this, don't send prompts to this proxy!**`; **If you are uncomfortable with this, don't send prompts to this proxy!**`;
} }
if (config.staticServiceInfo) {
return converter.makeHtml(infoBody + customGreeting);
}
const waits: string[] = []; const waits: string[] = [];
infoBody += `\n## Estimated Wait Times\nIf the AI is busy, your prompt will processed when a slot frees up.`; infoBody += `\n## Estimated Wait Times`;
if (config.openaiKey) { if (config.openaiKey) {
// TODO: un-fuck this // TODO: un-fuck this
@ -466,9 +483,7 @@ Logs are anonymous and do not contain IP addresses or timestamps. [You can see t
infoBody += "\n\n" + waits.join(" / "); infoBody += "\n\n" + waits.join(" / ");
if (customGreeting) { infoBody += customGreeting;
infoBody += `\n## Server Greeting\n${customGreeting}`;
}
infoBody += buildRecentImageSection(); infoBody += buildRecentImageSection();
@ -544,11 +559,11 @@ function buildRecentImageSection() {
function escapeHtml(unsafe: string) { function escapeHtml(unsafe: string) {
return unsafe return unsafe
.replace(/&/g, '&amp;') .replace(/&/g, "&amp;")
.replace(/</g, '&lt;') .replace(/</g, "&lt;")
.replace(/>/g, '&gt;') .replace(/>/g, "&gt;")
.replace(/"/g, '&quot;') .replace(/"/g, "&quot;")
.replace(/'/g, '&#39;'); .replace(/'/g, "&#39;");
} }
function getExternalUrlForHuggingfaceSpaceId(spaceId: string) { function getExternalUrlForHuggingfaceSpaceId(spaceId: string) {

View File

@ -12,6 +12,7 @@ export const injectLocals: RequestHandler = (req, res, next) => {
res.locals.quota = quota; res.locals.quota = quota;
res.locals.nextQuotaRefresh = userStore.getNextQuotaRefresh(); res.locals.nextQuotaRefresh = userStore.getNextQuotaRefresh();
res.locals.persistenceEnabled = config.gatekeeperStore !== "memory"; res.locals.persistenceEnabled = config.gatekeeperStore !== "memory";
res.locals.usersEnabled = config.gatekeeper === "user_token";
res.locals.showTokenCosts = config.showTokenCosts; res.locals.showTokenCosts = config.showTokenCosts;
res.locals.maxIps = config.maxIpsPerUser; res.locals.maxIps = config.maxIpsPerUser;