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 { withSession } from "../shared/with-session";
import { injectCsrfToken, checkCsrfToken } from "../shared/inject-csrf";
import { buildInfoPageHtml } from "../info-page";
import { loginRouter } from "./login";
import { usersApiRouter as apiRouter } from "./api/users";
import { usersWebRouter as webRouter } from "./web/manage";
@ -23,6 +24,11 @@ adminRouter.use(checkCsrfToken);
adminRouter.use(injectLocals);
adminRouter.use("/", loginRouter);
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(
(

View File

@ -1,5 +1,11 @@
<%- include("partials/shared_header", { title: "OAI Reverse Proxy Admin" }) %>
<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) { %>
<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 />
@ -19,6 +25,7 @@
<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/download-stats">Download Rentry Stats</a>
<li><a href="/admin/service-info">Service Info</a></li>
</ul>
<h3>Maintenance</h3>
<form id="maintenanceForm" action="/admin/manage/maintenance" method="post">

View File

@ -171,6 +171,13 @@ type Config = {
* the admin UI to used over HTTP.
*/
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.
@ -249,6 +256,7 @@ export const config: Config = {
allowNicknameChanges: getEnvWithDefault("ALLOW_NICKNAME_CHANGES", true),
showRecentImages: getEnvWithDefault("SHOW_RECENT_IMAGES", true),
useInsecureCookies: getEnvWithDefault("USE_INSECURE_COOKIES", isDev),
staticServiceInfo: getEnvWithDefault("STATIC_SERVICE_INFO", false),
} as const;
function generateCookieSecret() {
@ -314,7 +322,8 @@ export async function assertConfigIsValid() {
// them to users.
for (const key of getKeys(config)) {
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]);
if (maybeSensitive && !secured.has(key))
@ -345,7 +354,6 @@ export const OMITTED_KEYS: (keyof Config)[] = [
"awsCredentials",
"proxyKey",
"adminKey",
"checkKeys",
"rejectPhrases",
"showTokenCosts",
"googleSheetsKey",
@ -359,6 +367,7 @@ export const OMITTED_KEYS: (keyof Config)[] = [
"allowNicknameChanges",
"showRecentImages",
"useInsecureCookies",
"staticServiceInfo",
];
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 {
AnthropicKey,
GooglePalmKey,
OpenAIKey,
AwsBedrockKey,
GooglePalmKey,
keyPool,
OpenAIKey,
} from "./shared/key-management";
import { ModelFamily, OpenAIModelFamily } from "./shared/models";
import { getUniqueIps } from "./proxy/rate-limit";
@ -72,7 +72,10 @@ export const handleInfoPage = (req: Request, res: Response) => {
? getExternalUrlForHuggingfaceSpaceId(process.env.SPACE_ID)
: req.protocol + "://" + req.get("host");
res.send(cacheInfoPageHtml(baseUrl));
infoPageHtml = buildInfoPageHtml(baseUrl);
infoPageLastUpdated = Date.now();
res.send(infoPageHtml);
};
function getCostString(cost: number) {
@ -80,8 +83,9 @@ function getCostString(cost: number) {
return ` ($${cost.toFixed(2)})`;
}
function cacheInfoPageHtml(baseUrl: string) {
export function buildInfoPageHtml(baseUrl: string, asAdmin = false) {
const keys = keyPool.list();
const hideFullInfo = config.staticServiceInfo && !asAdmin;
modelStats.clear();
serviceStats.clear();
@ -97,31 +101,45 @@ function cacheInfoPageHtml(baseUrl: string) {
const allowDalle = config.allowedModelFamilies.includes("dall-e");
const info = {
uptime: Math.floor(process.uptime()),
endpoints: {
...(openaiKeys ? { openai: baseUrl + "/proxy/openai" } : {}),
...(openaiKeys
? { ["openai2"]: baseUrl + "/proxy/openai/turbo-instruct" }
: {}),
...(openaiKeys && allowDalle
? { ["openai-image"]: baseUrl + "/proxy/openai-image" }
: {}),
...(anthropicKeys ? { anthropic: baseUrl + "/proxy/anthropic" } : {}),
...(palmKeys ? { "google-palm": baseUrl + "/proxy/google-palm" } : {}),
...(awsKeys ? { aws: baseUrl + "/proxy/aws/claude" } : {}),
},
const endpoints = {
...(openaiKeys ? { openai: baseUrl + "/proxy/openai" } : {}),
...(openaiKeys
? { ["openai2"]: baseUrl + "/proxy/openai/turbo-instruct" }
: {}),
...(openaiKeys && allowDalle
? { ["openai-image"]: baseUrl + "/proxy/openai-image" }
: {}),
...(anthropicKeys ? { anthropic: baseUrl + "/proxy/anthropic" } : {}),
...(palmKeys ? { "google-palm": baseUrl + "/proxy/google-palm" } : {}),
...(awsKeys ? { aws: baseUrl + "/proxy/aws/claude" } : {}),
};
const stats = {
proompts,
tookens: `${prettyTokens(tokens)}${getCostString(tokenCost)}`,
...(config.textModelRateLimit ? { proomptersNow: getUniqueIps() } : {}),
};
const keyInfo = {
openaiKeys,
anthropicKeys,
palmKeys,
awsKeys,
};
const providerInfo = {
...(openaiKeys ? getOpenAIInfo() : {}),
...(anthropicKeys ? getAnthropicInfo() : {}),
...(palmKeys ? { "palm-bison": getPalmInfo() } : {}),
...(awsKeys ? { "aws-claude": getAwsInfo() } : {}),
}
const info = {
uptime: Math.floor(process.uptime()),
endpoints,
...(hideFullInfo ? {} : stats),
...keyInfo,
...(hideFullInfo ? {} : providerInfo),
config: listConfig(),
build: process.env.BUILD_INFO || "dev",
};
@ -129,7 +147,7 @@ function cacheInfoPageHtml(baseUrl: string) {
const title = getServerTitle();
const headerHtml = buildInfoPageHeader(new showdown.Converter(), title);
const pageBody = `<!DOCTYPE html>
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
@ -144,11 +162,6 @@ function cacheInfoPageHtml(baseUrl: string) {
${getSelfServiceLinks()}
</body>
</html>`;
infoPageHtml = pageBody;
infoPageLastUpdated = Date.now();
return pageBody;
}
function getUniqueOpenAIOrgs(keys: KeyPoolKey[]) {
@ -402,8 +415,8 @@ function getAwsInfo() {
}
const customGreeting = fs.existsSync("greeting.md")
? fs.readFileSync("greeting.md", "utf8")
: null;
? `\n## Server Greeting\n${fs.readFileSync("greeting.md", "utf8")}`
: "";
/**
* 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 (config.staticServiceInfo) {
return converter.makeHtml(infoBody + customGreeting);
}
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) {
// 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(" / ");
if (customGreeting) {
infoBody += `\n## Server Greeting\n${customGreeting}`;
}
infoBody += customGreeting;
infoBody += buildRecentImageSection();
@ -544,11 +559,11 @@ function buildRecentImageSection() {
function escapeHtml(unsafe: string) {
return unsafe
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function getExternalUrlForHuggingfaceSpaceId(spaceId: string) {

View File

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