2023-04-08 17:32:49 -06:00
import dotenv from "dotenv" ;
2023-05-13 22:26:08 -06:00
import type firebase from "firebase-admin" ;
2023-06-07 18:58:57 -06:00
import pino from "pino" ;
2023-04-08 17:32:49 -06:00
dotenv . config ( ) ;
2023-06-07 18:58:57 -06:00
// Can't import the usual logger here because it itself needs the config.
const startupLogger = pino ( { level : "debug" } ) . child ( { module : "startup" } ) ;
2023-04-10 04:39:13 -06:00
const isDev = process . env . NODE_ENV !== "production" ;
2023-05-09 17:11:57 -06:00
type PromptLoggingBackend = "google_sheets" ;
export type DequeueMode = "fair" | "random" | "none" ;
2023-04-14 19:21:04 -06:00
2023-04-08 17:32:49 -06:00
type Config = {
/** The port the proxy server will listen on. */
port : number ;
2023-05-29 11:08:08 -06:00
/** Comma-delimited list of OpenAI API keys. */
2023-04-08 17:32:49 -06:00
openaiKey? : string ;
2023-05-29 11:08:08 -06:00
/** Comma-delimited list of Anthropic API keys. */
anthropicKey? : string ;
2023-05-12 18:58:15 -06:00
/ * *
* The proxy key to require for requests . Only applicable if the user
* management mode is set to 'proxy_key' , and required if so .
* * /
2023-04-08 17:32:49 -06:00
proxyKey? : string ;
2023-05-12 18:58:15 -06:00
/ * *
2023-05-13 22:26:08 -06:00
* The admin key used to access the / admin API . Required if the user
2023-05-12 18:58:15 -06:00
* management mode is set to 'user_token' .
* * /
adminKey? : string ;
/ * *
* Which user management mode to use .
*
* ` none ` : No user management . Proxy is open to all requests with basic
* abuse protection .
*
* ` proxy_key ` : A specific proxy key must be provided in the Authorization
* header to use the proxy .
*
* ` user_token ` : Users must be created via the / admin REST API and provide
* their personal access token in the Authorization header to use the proxy .
* Configure this function and add users via the / admin API .
* /
gatekeeper : "none" | "proxy_key" | "user_token" ;
2023-05-13 22:26:08 -06:00
/ * *
* Persistence layer to use for user management .
*
* ` memory ` : Users are stored in memory and are lost on restart ( default )
*
* ` firebase_rtdb ` : Users are stored in a Firebase Realtime Database ; requires
* ` firebaseKey ` and ` firebaseRtdbUrl ` to be set .
* * /
gatekeeperStore : "memory" | "firebase_rtdb" ;
/** URL of the Firebase Realtime Database if using the Firebase RTDB store. */
firebaseRtdbUrl? : string ;
/** Base64-encoded Firebase service account key if using the Firebase RTDB store. */
firebaseKey? : string ;
2023-05-14 13:42:56 -06:00
/ * *
* Maximum number of IPs per user , after which their token is disabled .
* Users with the manually - assigned ` special ` role are exempt from this limit .
* By default , this is 0 , meaning that users are not IP - limited .
* /
2023-05-14 13:30:32 -06:00
maxIpsPerUser : number ;
2023-04-08 17:32:49 -06:00
/** Per-IP limit for requests per minute to OpenAI's completions endpoint. */
2023-04-08 19:40:45 -06:00
modelRateLimit : number ;
2023-07-21 18:11:32 -06:00
/ * *
* For OpenAI , the maximum number of context tokens ( prompt + max output ) a
* user can request before their request is rejected .
* Context limits can help prevent excessive spend .
* Defaults to 0 , which means no limit beyond OpenAI ' s stated maximums .
* /
maxContextTokensOpenAI : number ;
/ * *
* For Anthropic , the maximum number of context tokens a user can request .
* Claude context limits can prevent requests from tying up concurrency slots
* for too long , which can lengthen queue times for other users .
* Defaults to 0 , which means no limit beyond Anthropic ' s stated maximums .
* /
maxContextTokensAnthropic : number ;
2023-06-07 18:58:57 -06:00
/** For OpenAI, the maximum number of sampled tokens a user can request. */
maxOutputTokensOpenAI : number ;
/** For Anthropic, the maximum number of sampled tokens a user can request. */
maxOutputTokensAnthropic : number ;
2023-04-08 20:49:06 -06:00
/** Whether requests containing disallowed characters should be rejected. */
rejectDisallowed? : boolean ;
/** Message to return when rejecting requests. */
rejectMessage? : string ;
2023-04-14 19:21:04 -06:00
/** Pino log level. */
2023-04-08 17:32:49 -06:00
logLevel ? : "debug" | "info" | "warn" | "error" ;
2023-04-14 19:21:04 -06:00
/** Whether prompts and responses should be logged to persistent storage. */
2023-05-09 17:11:57 -06:00
promptLogging? : boolean ;
2023-04-14 19:21:04 -06:00
/** Which prompt logging backend to use. */
2023-05-09 17:11:57 -06:00
promptLoggingBackend? : PromptLoggingBackend ;
2023-04-14 19:21:04 -06:00
/** Base64-encoded Google Sheets API key. */
googleSheetsKey? : string ;
/** Google Sheets spreadsheet ID. */
googleSheetsSpreadsheetId? : string ;
2023-04-10 01:27:45 -06:00
/** Whether to periodically check keys for usage and validity. */
checkKeys? : boolean ;
2023-05-09 17:11:57 -06:00
/ * *
* How to display quota information on the info page .
2023-05-12 18:58:15 -06:00
*
2023-05-13 22:26:08 -06:00
* ` none ` : Hide quota information
2023-05-12 18:58:15 -06:00
*
2023-07-20 22:00:12 -06:00
* ` partial ` : ( deprecated ) Same as ` full ` because usage is no longer tracked
2023-05-12 18:58:15 -06:00
*
2023-07-20 22:00:12 -06:00
* ` full ` : Displays information about keys ' quota limits
2023-05-09 17:11:57 -06:00
* /
2023-07-20 22:00:12 -06:00
quotaDisplayMode : "none" | "full" ;
2023-05-09 17:11:57 -06:00
/ * *
* Which request queueing strategy to use when keys are over their rate limit .
2023-05-12 18:58:15 -06:00
*
2023-05-13 22:26:08 -06:00
* ` fair ` : Requests are serviced in the order they were received ( default )
2023-05-12 18:58:15 -06:00
*
2023-05-13 22:26:08 -06:00
* ` random ` : Requests are serviced randomly
2023-05-12 18:58:15 -06:00
*
2023-05-13 22:26:08 -06:00
* ` none ` : Requests are not queued and users have to retry manually
2023-05-09 17:11:57 -06:00
* /
queueMode : DequeueMode ;
2023-05-22 15:08:20 -06:00
/ * *
* Comma - separated list of origins to block . Requests matching any of these
* origins or referers will be rejected .
* Partial matches are allowed , so ` reddit ` will match ` www.reddit.com ` .
* Include only the hostname , not the protocol or path , e . g :
* ` reddit.com,9gag.com,gaiaonline.com `
* /
blockedOrigins? : string ;
/ * *
* Message to return when rejecting requests from blocked origins .
* /
blockMessage? : string ;
/ * *
* Desination URL to redirect blocked requests to , for non - JSON requests .
* /
blockRedirect? : string ;
2023-07-06 15:09:30 -06:00
/ * *
* Whether the proxy should disallow requests for GPT - 4 models in order to
* prevent excessive spend . Applies only to OpenAI .
* /
turboOnly? : boolean ;
2023-04-08 17:32:49 -06:00
} ;
2023-04-10 04:39:13 -06:00
// To change configs, create a file called .env in the root directory.
// See .env.example for an example.
2023-04-08 17:32:49 -06:00
export const config : Config = {
port : getEnvWithDefault ( "PORT" , 7860 ) ,
openaiKey : getEnvWithDefault ( "OPENAI_KEY" , "" ) ,
2023-05-29 11:08:08 -06:00
anthropicKey : getEnvWithDefault ( "ANTHROPIC_KEY" , "" ) ,
2023-04-08 17:32:49 -06:00
proxyKey : getEnvWithDefault ( "PROXY_KEY" , "" ) ,
2023-05-12 18:58:15 -06:00
adminKey : getEnvWithDefault ( "ADMIN_KEY" , "" ) ,
gatekeeper : getEnvWithDefault ( "GATEKEEPER" , "none" ) ,
2023-05-13 22:26:08 -06:00
gatekeeperStore : getEnvWithDefault ( "GATEKEEPER_STORE" , "memory" ) ,
2023-05-14 13:42:56 -06:00
maxIpsPerUser : getEnvWithDefault ( "MAX_IPS_PER_USER" , 0 ) ,
2023-05-13 22:26:08 -06:00
firebaseRtdbUrl : getEnvWithDefault ( "FIREBASE_RTDB_URL" , undefined ) ,
firebaseKey : getEnvWithDefault ( "FIREBASE_KEY" , undefined ) ,
2023-04-10 04:39:13 -06:00
modelRateLimit : getEnvWithDefault ( "MODEL_RATE_LIMIT" , 4 ) ,
2023-07-21 18:11:32 -06:00
maxContextTokensOpenAI : getEnvWithDefault ( "MAX_CONTEXT_TOKENS_OPENAI" , 0 ) ,
maxContextTokensAnthropic : getEnvWithDefault (
"MAX_CONTEXT_TOKENS_ANTHROPIC" ,
0
) ,
2023-06-07 18:58:57 -06:00
maxOutputTokensOpenAI : getEnvWithDefault ( "MAX_OUTPUT_TOKENS_OPENAI" , 300 ) ,
maxOutputTokensAnthropic : getEnvWithDefault (
"MAX_OUTPUT_TOKENS_ANTHROPIC" ,
600
) ,
2023-04-08 20:49:06 -06:00
rejectDisallowed : getEnvWithDefault ( "REJECT_DISALLOWED" , false ) ,
rejectMessage : getEnvWithDefault (
"REJECT_MESSAGE" ,
"This content violates /aicg/'s acceptable use policy."
) ,
2023-04-08 17:32:49 -06:00
logLevel : getEnvWithDefault ( "LOG_LEVEL" , "info" ) ,
2023-04-10 04:39:13 -06:00
checkKeys : getEnvWithDefault ( "CHECK_KEYS" , ! isDev ) ,
2023-07-20 22:00:12 -06:00
quotaDisplayMode : getEnvWithDefault ( "QUOTA_DISPLAY_MODE" , "full" ) ,
2023-04-14 19:21:04 -06:00
promptLogging : getEnvWithDefault ( "PROMPT_LOGGING" , false ) ,
promptLoggingBackend : getEnvWithDefault ( "PROMPT_LOGGING_BACKEND" , undefined ) ,
googleSheetsKey : getEnvWithDefault ( "GOOGLE_SHEETS_KEY" , undefined ) ,
googleSheetsSpreadsheetId : getEnvWithDefault (
"GOOGLE_SHEETS_SPREADSHEET_ID" ,
undefined
) ,
2023-05-09 17:11:57 -06:00
queueMode : getEnvWithDefault ( "QUEUE_MODE" , "fair" ) ,
2023-06-13 22:05:51 -06:00
blockedOrigins : getEnvWithDefault ( "BLOCKED_ORIGINS" , undefined ) ,
2023-05-22 15:08:20 -06:00
blockMessage : getEnvWithDefault (
"BLOCK_MESSAGE" ,
"You must be over the age of majority in your country to use this service."
) ,
blockRedirect : getEnvWithDefault ( "BLOCK_REDIRECT" , "https://www.9gag.com" ) ,
2023-07-06 15:09:30 -06:00
turboOnly : getEnvWithDefault ( "TURBO_ONLY" , false ) ,
2023-04-08 17:32:49 -06:00
} as const ;
2023-06-07 18:58:57 -06:00
function migrateConfigs() {
let migrated = false ;
const deprecatedMax = process . env . MAX_OUTPUT_TOKENS ;
if ( ! process . env . MAX_OUTPUT_TOKENS_OPENAI && deprecatedMax ) {
migrated = true ;
config . maxOutputTokensOpenAI = parseInt ( deprecatedMax ) ;
}
if ( ! process . env . MAX_OUTPUT_TOKENS_ANTHROPIC && deprecatedMax ) {
migrated = true ;
config . maxOutputTokensAnthropic = parseInt ( deprecatedMax ) ;
}
if ( migrated ) {
startupLogger . warn (
{
MAX_OUTPUT_TOKENS : deprecatedMax ,
MAX_OUTPUT_TOKENS_OPENAI : config.maxOutputTokensOpenAI ,
MAX_OUTPUT_TOKENS_ANTHROPIC : config.maxOutputTokensAnthropic ,
} ,
"`MAX_OUTPUT_TOKENS` has been replaced with separate `MAX_OUTPUT_TOKENS_OPENAI` and `MAX_OUTPUT_TOKENS_ANTHROPIC` configs. You should update your .env file to remove `MAX_OUTPUT_TOKENS` and set the new configs."
) ;
}
}
2023-05-12 18:58:15 -06:00
/** Prevents the server from starting if config state is invalid. */
2023-05-13 22:26:08 -06:00
export async function assertConfigIsValid() {
2023-06-07 18:58:57 -06:00
migrateConfigs ( ) ;
2023-05-12 18:58:15 -06:00
// Ensure gatekeeper mode is valid.
if ( ! [ "none" , "proxy_key" , "user_token" ] . includes ( config . gatekeeper ) ) {
throw new Error (
` Invalid gatekeeper mode: ${ config . gatekeeper } . Must be one of: none, proxy_key, user_token. `
) ;
}
// Don't allow `user_token` mode without `ADMIN_KEY`.
if ( config . gatekeeper === "user_token" && ! config . adminKey ) {
throw new Error (
"`user_token` gatekeeper mode requires an `ADMIN_KEY` to be set."
) ;
}
// Don't allow `proxy_key` mode without `PROXY_KEY`.
if ( config . gatekeeper === "proxy_key" && ! config . proxyKey ) {
throw new Error (
"`proxy_key` gatekeeper mode requires a `PROXY_KEY` to be set."
) ;
}
// Don't allow `PROXY_KEY` to be set for other modes.
if ( config . gatekeeper !== "proxy_key" && config . proxyKey ) {
throw new Error (
"`PROXY_KEY` is set, but gatekeeper mode is not `proxy_key`. Make sure to set `GATEKEEPER=proxy_key`."
) ;
}
2023-05-13 22:26:08 -06:00
// Require appropriate firebase config if using firebase store.
if (
config . gatekeeperStore === "firebase_rtdb" &&
( ! config . firebaseKey || ! config . firebaseRtdbUrl )
) {
throw new Error (
"Firebase RTDB store requires `FIREBASE_KEY` and `FIREBASE_RTDB_URL` to be set."
) ;
}
2023-05-14 14:45:30 -06:00
// Ensure forks which add new secret-like config keys don't unwittingly expose
// them to users.
for ( const key of getKeys ( config ) ) {
const maybeSensitive = [ "key" , "credentials" , "secret" , "password" ] . some (
( sensitive ) = > key . toLowerCase ( ) . includes ( sensitive )
) ;
const secured = new Set ( [ . . . SENSITIVE_KEYS , . . . OMITTED_KEYS ] ) ;
if ( maybeSensitive && ! secured . has ( key ) )
throw new Error (
` Config key " ${ key } " may be sensitive but is exposed. Add it to SENSITIVE_KEYS or OMITTED_KEYS. `
) ;
}
2023-05-13 22:26:08 -06:00
await maybeInitializeFirebase ( ) ;
2023-05-12 18:58:15 -06:00
}
2023-05-13 22:26:08 -06:00
/ * *
2023-05-14 14:45:30 -06:00
* Config keys that are masked on the info page , but not hidden as their
* presence may be relevant to the user due to privacy implications .
2023-05-13 22:26:08 -06:00
* /
2023-05-14 14:45:30 -06:00
export const SENSITIVE_KEYS : ( keyof Config ) [ ] = [ "googleSheetsSpreadsheetId" ] ;
2023-05-12 18:58:15 -06:00
2023-05-14 14:45:30 -06:00
/ * *
* Config keys that are not displayed on the info page at all , generally because
* they are not relevant to the user or can be inferred from other config .
* /
2023-05-12 18:58:15 -06:00
export const OMITTED_KEYS : ( keyof Config ) [ ] = [
"port" ,
"logLevel" ,
"openaiKey" ,
2023-05-29 11:08:08 -06:00
"anthropicKey" ,
2023-05-12 18:58:15 -06:00
"proxyKey" ,
"adminKey" ,
2023-05-14 14:45:30 -06:00
"checkKeys" ,
"quotaDisplayMode" ,
"googleSheetsKey" ,
"firebaseKey" ,
"firebaseRtdbUrl" ,
"gatekeeperStore" ,
"maxIpsPerUser" ,
2023-05-22 15:08:20 -06:00
"blockedOrigins" ,
"blockMessage" ,
"blockRedirect" ,
2023-05-12 18:58:15 -06:00
] ;
2023-04-08 17:32:49 -06:00
const getKeys = Object . keys as < T extends object > ( obj : T ) = > Array < keyof T > ;
2023-05-14 14:45:30 -06:00
2023-04-08 17:32:49 -06:00
export function listConfig ( ) : Record < string , string > {
const result : Record < string , string > = { } ;
for ( const key of getKeys ( config ) ) {
const value = config [ key ] ? . toString ( ) || "" ;
2023-05-12 18:58:15 -06:00
2023-05-14 14:45:30 -06:00
const shouldOmit =
OMITTED_KEYS . includes ( key ) || value === "" || value === "undefined" ;
const shouldMask = SENSITIVE_KEYS . includes ( key ) ;
if ( shouldOmit ) {
2023-04-15 01:44:52 -06:00
continue ;
}
2023-05-12 18:58:15 -06:00
2023-05-14 14:45:30 -06:00
if ( value && shouldMask ) {
2023-04-08 17:32:49 -06:00
result [ key ] = "********" ;
} else {
result [ key ] = value ;
}
}
return result ;
}
function getEnvWithDefault < T > ( name : string , defaultValue : T ) : T {
const value = process . env [ name ] ;
if ( value === undefined ) {
return defaultValue ;
}
try {
2023-05-29 11:08:08 -06:00
if ( name === "OPENAI_KEY" || name === "ANTHROPIC_KEY" ) {
2023-04-08 17:32:49 -06:00
return value as unknown as T ;
}
return JSON . parse ( value ) as T ;
} catch ( err ) {
return value as unknown as T ;
}
}
2023-05-13 22:26:08 -06:00
let firebaseApp : firebase.app.App | undefined ;
async function maybeInitializeFirebase() {
if ( ! config . gatekeeperStore . startsWith ( "firebase" ) ) {
return ;
}
const firebase = await import ( "firebase-admin" ) ;
const firebaseKey = Buffer . from ( config . firebaseKey ! , "base64" ) . toString ( ) ;
const app = firebase . initializeApp ( {
credential : firebase.credential.cert ( JSON . parse ( firebaseKey ) ) ,
databaseURL : config.firebaseRtdbUrl ,
} ) ;
await app . database ( ) . ref ( "connection-test" ) . set ( Date . now ( ) ) ;
firebaseApp = app ;
}
export function getFirebaseApp ( ) : firebase . app . App {
if ( ! firebaseApp ) {
throw new Error ( "Firebase app not initialized." ) ;
}
return firebaseApp ;
}