code:
# Patches
Contains monkey patches for certain packages, applied using `patch-package`.

## `http-proxy+1.18.1.patch`
Modifies the `http-proxy` package to work around an incompatibility with
body-parser and SOCKS5 proxies due to some esoteric stream handling behavior
when `socks-proxy-agent` is used instead of a generic http.Agent.

Modification involves adjusting the `buffer` property on ProxyServer's `options`
object to be a function that returns a stream instead of a stream itself. This
allows us to give it a function which produces a new Readable from the already-
parsed request body.

With the old implementation we would need to create an entirely new ProxyServer
instance for each request, which is not ideal under heavy load.

`http-proxy` hasn't been updated in six years so it's unlikely that this patch
will be broken by future updates, but it's stil pinned to 1.18.1 for now.

### See also
https://github.com/chimurai/http-proxy-middleware/issues/40
https://github.com/chimurai/http-proxy-middleware/issues/299
https://github.com/http-party/node-http-proxy/pull/1027


code:
diff --git a/node_modules/http-proxy/lib/http-proxy/passes/web-incoming.js b/node_modules/http-proxy/lib/http-proxy/passes/web-incoming.js
index 7ae7355..c825c27 100644
--- a/node_modules/http-proxy/lib/http-proxy/passes/web-incoming.js
+++ b/node_modules/http-proxy/lib/http-proxy/passes/web-incoming.js
@@ -167,7 +167,7 @@ module.exports = {
       }
     }

-    (options.buffer || req).pipe(proxyReq);
+    (options.buffer(req) || req).pipe(proxyReq);

     proxyReq.on('response', function(proxyRes) {
       if(server) { server.emit('proxyRes', proxyRes, req, res); }

license: unknown


code:
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */

/* Document
   ========================================================================== */

/**
 * 1. Correct the line height in all browsers.
 * 2. Prevent adjustments of font size after orientation changes in iOS.
 */

html {
    line-height: 1.15; /* 1 */
    -webkit-text-size-adjust: 100%; /* 2 */
}

/* Sections
   ========================================================================== */

/**
 * Remove the margin in all browsers.
 */

body {
    margin: 0;
}

/**
 * Render the `main` element consistently in IE.
 */

main {
    display: block;
}

/**
 * Correct the font size and margin on `h1` elements within `section` and
 * `article` contexts in Chrome, Firefox, and Safari.
 */

h1 {
    font-size: 2em;
    margin: 0.67em 0;
}

/* Grouping content
   ========================================================================== */

/**
 * 1. Add the correct box sizing in Firefox.
 * 2. Show the overflow in Edge and IE.
 */

hr {
    box-sizing: content-box; /* 1 */
    height: 0; /* 1 */
    overflow: visible; /* 2 */
}

/**
 * 1. Correct the inheritance and scaling of font size in all browsers.
 * 2. Correct the odd `em` font sizing in all browsers.
 */

pre {
    font-family: monospace, monospace; /* 1 */
    font-size: 1em; /* 2 */
}

/* Text-level semantics
   ========================================================================== */

/**
 * Remove the gray background on active links in IE 10.
 */

a {
    background-color: transparent;
}

/**
 * 1. Remove the bottom border in Chrome 57-
 * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
 */

abbr[title] {
    border-bottom: none; /* 1 */
    text-decoration: underline; /* 2 */
    text-decoration: underline dotted; /* 2 */
}

/**
 * Add the correct font weight in Chrome, Edge, and Safari.
 */

b,
strong {
    font-weight: bolder;
}

/**
 * 1. Correct the inheritance and scaling of font size in all browsers.
 * 2. Correct the odd `em` font sizing in all browsers.
 */

code,
kbd,
samp {
    font-family: monospace, monospace; /* 1 */
    font-size: 1em; /* 2 */
}

/**
 * Add the correct font size in all browsers.
 */

small {
    font-size: 80%;
}

/**
 * Prevent `sub` and `sup` elements from affecting the line height in
 * all browsers.
 */

sub,
sup {
    font-size: 75%;
    line-height: 0;
    position: relative;
    vertical-align: baseline;
}

sub {
    bottom: -0.25em;
}

sup {
    top: -0.5em;
}

/* Embedded content
   ========================================================================== */

/**
 * Remove the border on images inside links in IE 10.
 */

img {
    border-style: none;
}

/* Forms
   ========================================================================== */

/**
 * 1. Change the font styles in all browsers.
 * 2. Remove the margin in Firefox and Safari.
 */

button,
input,
optgroup,
select,
textarea {
    font-family: inherit; /* 1 */
    font-size: 100%; /* 1 */
    line-height: 1.15; /* 1 */
    margin: 0; /* 2 */
}

/**
 * Show the overflow in IE.
 * 1. Show the overflow in Edge.
 */

button,
input { /* 1 */
    overflow: visible;
}

/**
 * Remove the inheritance of text transform in Edge, Firefox, and IE.
 * 1. Remove the inheritance of text transform in Firefox.
 */

button,
select { /* 1 */
    text-transform: none;
}

/**
 * Correct the inability to style clickable types in iOS and Safari.
 */

button,
[type="button"],
[type="reset"],
[type="submit"] {
    -webkit-appearance: button;
}

/**
 * Remove the inner border and padding in Firefox.
 */

button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
    border-style: none;
    padding: 0;
}

/**
 * Restore the focus styles unset by the previous rule.
 */

button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
    outline: 1px dotted ButtonText;
}

/**
 * Correct the padding in Firefox.
 */

fieldset {
    padding: 0.35em 0.75em 0.625em;
}

/**
 * 1. Correct the text wrapping in Edge and IE.
 * 2. Correct the color inheritance from `fieldset` elements in IE.
 * 3. Remove the padding so developers are not caught out when they zero out
 *    `fieldset` elements in all browsers.
 */

legend {
    box-sizing: border-box; /* 1 */
    color: inherit; /* 2 */
    display: table; /* 1 */
    max-width: 100%; /* 1 */
    padding: 0; /* 3 */
    white-space: normal; /* 1 */
}

/**
 * Add the correct vertical alignment in Chrome, Firefox, and Opera.
 */

progress {
    vertical-align: baseline;
}

/**
 * Remove the default vertical scrollbar in IE 10+.
 */

textarea {
    overflow: auto;
}

/**
 * 1. Add the correct box sizing in IE 10.
 * 2. Remove the padding in IE 10.
 */

[type="checkbox"],
[type="radio"] {
    box-sizing: border-box; /* 1 */
    padding: 0; /* 2 */
}

/**
 * Correct the cursor style of increment and decrement buttons in Chrome.
 */

[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
    height: auto;
}

/**
 * 1. Correct the odd appearance in Chrome and Safari.
 * 2. Correct the outline style in Safari.
 */

[type="search"] {
    -webkit-appearance: textfield; /* 1 */
    outline-offset: -2px; /* 2 */
}

/**
 * Remove the inner padding in Chrome and Safari on macOS.
 */

[type="search"]::-webkit-search-decoration {
    -webkit-appearance: none;
}

/**
 * 1. Correct the inability to style clickable types in iOS and Safari.
 * 2. Change font properties to `inherit` in Safari.
 */

::-webkit-file-upload-button {
    -webkit-appearance: button; /* 1 */
    font: inherit; /* 2 */
}

/* Interactive
   ========================================================================== */

/*
 * Add the correct display in Edge, IE 10+, and Firefox.
 */

details {
    display: block;
}

/*
 * Add the correct display in all browsers.
 */

summary {
    display: list-item;
}

/* Misc
   ========================================================================== */

/**
 * Add the correct display in IE 10+.
 */

template {
    display: none;
}

/**
 * Add the correct display in IE 10.
 */

[hidden] {
    display: none;
}


code:
/* modified https://github.com/oxalorg/sakura */
html {
  font-size: 62.5%;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, "Noto Sans", sans-serif;
}
body {
  font-size: 1.8rem;
  line-height: 1.618;
  max-width: 38em;
  margin: auto;
  color: #c9c9c9;
  background-color: #222222;
  padding: 13px;
}
@media (max-width: 684px) {
  body {
    font-size: 1.53rem;
  }
}
@media (max-width: 382px) {
  body {
    font-size: 1.35rem;
  }
}
h1,
h2,
h3,
h4,
h5,
h6 {
  line-height: 1.1;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, "Noto Sans", sans-serif;
  font-weight: 700;
  margin-top: 3rem;
  margin-bottom: 1.5rem;
  overflow-wrap: break-word;
  word-wrap: break-word;
  -ms-word-break: break-all;
  word-break: break-word;
}
h1 {
  font-size: 2.35em;
}
h2 {
  font-size: 2em;
}
h3 {
  font-size: 1.75em;
}
h4 {
  font-size: 1.5em;
}
h5 {
  font-size: 1.25em;
}
h6 {
  font-size: 1em;
}
p {
  margin-top: 0px;
  margin-bottom: 2.5rem;
}
small,
sub,
sup {
  font-size: 75%;
}
hr {
  border-color: #ffffff;
}
a {
  text-decoration: none;
  color: #ffffff;
}
a:visited {
  color: #e6e6e6;
}
a:hover {
  color: #c9c9c9;
  text-decoration: underline;
}
ul {
  padding-left: 1.4em;
  margin-top: 0px;
  margin-bottom: 2.5rem;
}
li {
  margin-bottom: 0.4em;
}
blockquote {
  margin-left: 0px;
  margin-right: 0px;
  padding-left: 1em;
  padding-top: 0.8em;
  padding-bottom: 0.8em;
  padding-right: 0.8em;
  border-left: 5px solid #ffffff;
  margin-bottom: 2.5rem;
  background-color: #4a4a4a;
}
blockquote p {
  margin-bottom: 0;
}
img,
video {
  height: auto;
  max-width: 100%;
  margin-top: 0px;
  margin-bottom: 2.5rem;
}
pre {
  background-color: #4a4a4a;
  display: block;
  padding: 1em;
  overflow-x: auto;
  margin-top: 0px;
  margin-bottom: 2.5rem;
  font-size: 0.9em;
}
code,
kbd,
samp {
  font-size: 0.9em;
  padding: 0 0.5em;
  background-color: #4a4a4a;
  white-space: pre-wrap;
}
pre > code {
  padding: 0;
  background-color: transparent;
  white-space: pre;
  font-size: 1em;
}
table {
  text-align: justify;
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 2rem;
}
td,
th {
  padding: 0.5em;
  border-bottom: 1px solid #4a4a4a;
}
input,
textarea {
  border: 1px solid #c9c9c9;
}
input:focus,
textarea:focus {
  border: 1px solid #ffffff;
}
textarea {
  width: 100%;
}
.button,
button,
input[type="submit"],
input[type="reset"],
input[type="button"],
input[type="file"]::file-selector-button {
  display: inline-block;
  padding: 5px 10px;
  text-align: center;
  text-decoration: none;
  white-space: nowrap;
  background-color: #ffffff;
  color: #222222;
  border-radius: 1px;
  border: 1px solid #ffffff;
  cursor: pointer;
  box-sizing: border-box;
}
.button[disabled],
button[disabled],
input[type="submit"][disabled],
input[type="reset"][disabled],
input[type="button"][disabled],
input[type="file"][disabled] {
  cursor: default;
  opacity: 0.5;
}
.button:hover,
button:hover,
input[type="submit"]:hover,
input[type="reset"]:hover,
input[type="button"]:hover,
input[type="file"]::file-selector-button:hover {
  background-color: #c9c9c9;
  color: #222222;
  outline: 0;
}
.button:focus-visible,
button:focus-visible,
input[type="submit"]:focus-visible,
input[type="reset"]:focus-visible,
input[type="button"]:focus-visible,
input[type="file"]::file-selector-button:focus-visible {
  outline-style: solid;
  outline-width: 2px;
}
textarea,
select,
input {
  color: #c9c9c9;
  padding: 6px 10px;
  margin-bottom: 10px;
  background-color: #4a4a4a;
  border: 1px solid #4a4a4a;
  border-radius: 4px;
  box-shadow: none;
  box-sizing: border-box;
}
textarea:focus,
select:focus,
input:focus {
  border: 1px solid #ffffff;
  outline: 0;
}
input[type="checkbox"]:focus {
  outline: 1px dotted #ffffff;
}
label,
legend,
fieldset {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 600;
}


code:
/* modified https://github.com/oxalorg/sakura */
:root {
  --accent-color: #4a4a4a;
  --accent-color-hover: #5a5a5a;
  --link-color: #58739c;
  --link-visted-color: #6f5e6f;
}
html {
  font-size: 62.5%;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, "Noto Sans", sans-serif;
}
body {
  font-size: 1.8rem;
  line-height: 1.618;
  max-width: 38em;
  margin: auto;
  color: #4a4a4a;
  background-color: #f9f9f9;
  padding: 13px;
}
@media (max-width: 684px) {
  body {
    font-size: 1.53rem;
  }
}
@media (max-width: 382px) {
  body {
    font-size: 1.35rem;
  }
}
h1,
h2,
h3,
h4,
h5,
h6 {
  line-height: 1.1;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, "Noto Sans", sans-serif;
  font-weight: 700;
  margin-top: 3rem;
  margin-bottom: 1.5rem;
  overflow-wrap: break-word;
  word-wrap: break-word;
  -ms-word-break: break-all;
  word-break: break-word;
}
h1 {
  font-size: 2.35em;
}
h2 {
  font-size: 2em;
}
h3 {
  font-size: 1.75em;
}
h4 {
  font-size: 1.5em;
}
h5 {
  font-size: 1.25em;
}
h6 {
  font-size: 1em;
}
p {
  margin-top: 0;
  margin-bottom: 2.5rem;
}
small,
sub,
sup {
  font-size: 75%;
}
hr {
  border-color: var(--accent-color);
}
a {
  text-decoration: none;
  color: var(--link-color);
}
a:visited {
  color: var(--link-visted-color);
}
a:hover {
  color: var(--accent-color-hover);
  text-decoration: underline;
}
ul {
  padding-left: 1.4em;
  margin-top: 0;
  margin-bottom: 2.5rem;
}
li {
  margin-bottom: 0.4em;
}
blockquote {
  margin-left: 0;
  margin-right: 0;
  padding-left: 1em;
  padding-top: 0.8em;
  padding-bottom: 0.8em;
  padding-right: 0.8em;
  border-left: 5px solid var(--accent-color);
  margin-bottom: 2.5rem;
  background-color: #f1f1f1;
}
blockquote p {
  margin-bottom: 0;
}
img,
video {
  height: auto;
  max-width: 100%;
  margin-top: 0;
  margin-bottom: 2.5rem;
}
pre {
  background-color: #f1f1f1;
  display: block;
  padding: 1em;
  overflow-x: auto;
  margin-top: 0;
  margin-bottom: 2.5rem;
  font-size: 0.9em;
}
code,
kbd,
samp {
  font-size: 0.9em;
  padding: 0 0.5em;
  background-color: #f1f1f1;
  white-space: pre-wrap;
}
pre > code {
  padding: 0;
  background-color: transparent;
  white-space: pre;
  font-size: 1em;
}
table {
  text-align: justify;
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 2rem;
}
td,
th {
  padding: 0.5em;
  border-bottom: 1px solid #f1f1f1;
}
input,
textarea {
  border: 1px solid #4a4a4a;
}
input:focus,
textarea:focus {
  border: 1px solid var(--accent-color);
}
textarea {
  width: 100%;
}
.button,
button,
input[type="submit"],
input[type="reset"],
input[type="button"],
input[type="file"]::file-selector-button {
  display: inline-block;
  padding: 5px 10px;
  text-align: center;
  text-decoration: none;
  white-space: nowrap;
  background-color: var(--accent-color);
  color: #f9f9f9;
  border-radius: 2px;
  border: 1px solid var(--accent-color);
  cursor: pointer;
  box-sizing: border-box;
}
.button[disabled],
button[disabled],
input[type="submit"][disabled],
input[type="reset"][disabled],
input[type="button"][disabled],
input[type="file"][disabled] {
  cursor: default;
  opacity: 0.5;
}
.button:hover,
button:hover,
input[type="submit"]:hover,
input[type="reset"]:hover,
input[type="button"]:hover,
input[type="file"]::file-selector-button:hover {
  background-color: var(--accent-color-hover);
  color: #f9f9f9;
  outline: 0;
}
.button:focus-visible,
button:focus-visible,
input[type="submit"]:focus-visible,
input[type="reset"]:focus-visible,
input[type="button"]:focus-visible,
input[type="file"]::file-selector-button:focus-visible {
  outline-style: solid;
  outline-width: 2px;
}
textarea,
select,
input {
  color: #4a4a4a;
  padding: 6px 10px;
  margin-bottom: 10px;
  background-color: #f1f1f1;
  border: 1px solid #f1f1f1;
  border-radius: 4px;
  box-shadow: none;
  box-sizing: border-box;
}
textarea:focus,
select:focus,
input:focus {
  border: 1px solid var(--accent-color);
  outline: 0;
}
input[type="checkbox"]:focus {
  outline: 1px dotted var(--accent-color);
}
label,
legend,
fieldset {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 600;
}


code:
importScripts(
  "https://cdn.jsdelivr.net/npm/hash-wasm@4.11.0/dist/argon2.umd.min.js"
);

let active = false;
let nonce = 0;
let signature = "";
let lastNotify = 0;
let hashesSinceLastNotify = 0;
let params = {
  salt: null,
  hashLength: 0,
  iterations: 0,
  memorySize: 0,
  parallelism: 0,
  targetValue: BigInt(0),
  safariFix: false,
};

self.onmessage = async (event) => {
  const { data } = event;
  switch (data.type) {
    case "stop":
      active = false;
      self.postMessage({ type: "paused", hashes: hashesSinceLastNotify });
      return;
    case "start":
      active = true;
      signature = data.signature;
      nonce = data.nonce;

      const c = data.challenge;
      const salt = new Uint8Array(c.s.length / 2);
      for (let i = 0; i < c.s.length; i += 2) {
        salt[i / 2] = parseInt(c.s.slice(i, i + 2), 16);
      }

      params = {
        salt: salt,
        hashLength: c.hl,
        iterations: c.t,
        memorySize: c.m,
        parallelism: c.p,
        targetValue: BigInt(c.d.slice(0, -1)),
        safariFix: data.isMobileWebkit,
      };

      console.log("Started", params);
      self.postMessage({ type: "started" });
      setTimeout(solve, 0);
      break;
  }
};

const doHash = async (password) => {
  const { salt, hashLength, iterations, memorySize, parallelism } = params;
  return await self.hashwasm.argon2id({
    password,
    salt,
    hashLength,
    iterations,
    memorySize,
    parallelism,
  });
};

const checkHash = (hash) => {
  const { targetValue } = params;
  const hashValue = BigInt(`0x${hash}`);
  return hashValue <= targetValue;
};

const solve = async () => {
  if (!active) {
    console.log("Stopped solver", nonce);
    return;
  }

  // Safari WASM doesn't like multiple calls in one worker
  const batchSize = 1;
  const batch = [];
  for (let i = 0; i < batchSize; i++) {
    batch.push(nonce++);
  }

  try {
    const results = await Promise.all(
      batch.map(async (nonce) => {
        const hash = await doHash(String(nonce));
        return { hash, nonce };
      })
    );
    hashesSinceLastNotify += batchSize;

    const solution = results.find(({ hash }) => checkHash(hash));
    if (solution) {
      console.log("Solution found", solution, params.salt);
      self.postMessage({ type: "solved", nonce: solution.nonce });
      active = false;
    } else {
      if (Date.now() - lastNotify >= 500) {
        console.log("Last nonce", nonce, "Hashes", hashesSinceLastNotify);
        self.postMessage({ type: "progress", hashes: hashesSinceLastNotify });
        lastNotify = Date.now();
        hashesSinceLastNotify = 0;
      }
      setTimeout(solve, 10);
    }
  } catch (error) {
    console.error("Error", error);
    const stack = error.stack;
    const debug = {
      stack,
      lastNonce: nonce,
      targetValue: params.targetValue,
    };
    self.postMessage({ type: "error", error: error.message, debug });
    active = false;
  }
};


code:
services:
  - type: web
    name: oai-proxy
    env: docker
    repo: https://gitlab.com/khanon/oai-proxy.git
    region: oregon
    plan: free
    branch: main
    healthCheckPath: /health
    dockerfilePath: ./docker/render/Dockerfile


code:
import Database from "better-sqlite3";
import { DATABASE_VERSION, migrateDatabase } from "../src/shared/database";
import { logger } from "../src/logger";
import { config } from "../src/config";

const log = logger.child({ module: "scripts/migrate" });

async function runMigration() {
  let targetVersion = Number(process.argv[2]) || undefined;

  if (!targetVersion) {
    log.info("Enter target version or leave empty to use the latest version.");
    process.stdin.resume();
    process.stdin.setEncoding("utf8");
    const input = await new Promise<string>((resolve) => {
      process.stdin.on("data", (text) => {
        resolve((String(text) || "").trim());
      });
    });
    process.stdin.pause();
    targetVersion = Number(input);
    if (!targetVersion) {
      targetVersion = DATABASE_VERSION;
    }
  }

  const db = new Database(config.sqliteDataPath, {
    verbose: (msg, ...args) => log.debug({ args }, String(msg)),
  });

  const currentVersion = db.pragma("user_version", { simple: true });
  log.info({ currentVersion, targetVersion }, "Running migrations.");
  migrateDatabase(targetVersion, db);
}

runMigration().catch((error) => {
  log.error(error, "Migration failed.");
  process.exit(1);
});


code:
# OAI Reverse Proxy

###
# @name OpenAI -- Chat Completions
POST https://api.openai.com/v1/chat/completions
Authorization: Bearer {{oai-key-1}}
Content-Type: application/json

{
  "model": "gpt-3.5-turbo",
  "max_tokens": 30,
  "stream": false,
  "messages": [
    {
      "role": "user",
      "content": "This is a test prompt."
    }
  ]
}

###
# @name OpenAI -- Text Completions
POST https://api.openai.com/v1/completions
Authorization: Bearer {{oai-key-1}}
Content-Type: application/json

{
  "model": "gpt-3.5-turbo-instruct",
  "max_tokens": 30,
  "stream": false,
  "prompt": "This is a test prompt where"
}

###
# @name OpenAI -- Create Embedding
POST https://api.openai.com/v1/embeddings
Authorization: Bearer {{oai-key-1}}
Content-Type: application/json

{
  "model": "text-embedding-ada-002",
  "input": "This is a test embedding input."
}

###
# @name OpenAI -- Get Organizations
GET https://api.openai.com/v1/organizations
Authorization: Bearer {{oai-key-1}}

###
# @name OpenAI -- Get Models
GET https://api.openai.com/v1/models
Authorization: Bearer {{oai-key-1}}

###
# @name Azure OpenAI -- Chat Completions
POST https://{{azu-resource-name}}.openai.azure.com/openai/deployments/{{azu-deployment-id}}/chat/completions?api-version=2023-09-01-preview
api-key: {{azu-key-1}}
Content-Type: application/json

{
  "max_tokens": 1,
  "stream": false,
  "messages": [
    {
      "role": "user",
      "content": "This is a test prompt."
    }
  ]
}

###
# @name Proxy / OpenAI -- Get Models
GET {{proxy-host}}/proxy/openai/v1/models
Authorization: Bearer {{proxy-key}}

###
# @name Proxy / OpenAI -- Native Chat Completions
POST {{proxy-host}}/proxy/openai/chat/completions
Authorization: Bearer {{proxy-key}}
Content-Type: application/json

{
  "model": "gpt-4-1106-preview",
  "max_tokens": 20,
  "stream": true,
  "temperature": 1,
  "seed": 123,
  "messages": [
    {
      "role": "user",
      "content": "phrase one"
    }
  ]
}

###
# @name Proxy / OpenAI -- Native Text Completions
POST {{proxy-host}}/proxy/openai/v1/turbo-instruct/chat/completions
Authorization: Bearer {{proxy-key}}
Content-Type: application/json

{
  "model": "gpt-3.5-turbo-instruct",
  "max_tokens": 20,
  "temperature": 0,
  "prompt": "Genshin Impact is a game about",
  "stream": false
}

###
# @name Proxy / OpenAI -- Chat-to-Text API Translation
# Accepts a chat completion request and reformats it to work with the text completion API. `model` is ignored.
POST {{proxy-host}}/proxy/openai/turbo-instruct/chat/completions
Authorization: Bearer {{proxy-key}}
Content-Type: application/json

{
  "model": "gpt-4",
  "max_tokens": 20,
  "stream": true,
  "messages": [
    {
      "role": "user",
      "content": "What is the name of the fourth president of the united states?"
    },
    {
      "role": "assistant",
      "content": "That would be George Washington."
    },
    {
      "role": "user",
      "content": "I don't think that's right..."
    }
  ]
}

###
# @name Proxy / OpenAI -- Create Embedding
POST {{proxy-host}}/proxy/openai/embeddings
Authorization: Bearer {{proxy-key}}
Content-Type: application/json

{
  "model": "text-embedding-ada-002",
  "input": "This is a test embedding input."
}

###
# @name Proxy / Anthropic -- Native Completion (old API)
POST {{proxy-host}}/proxy/anthropic/v1/complete
Authorization: Bearer {{proxy-key}}
anthropic-version: 2023-01-01
Content-Type: application/json

{
  "model": "claude-v1.3",
  "max_tokens_to_sample": 20,
  "temperature": 0.2,
  "stream": true,
  "prompt": "What is genshin impact\n\n:Assistant:"
}

###
# @name Proxy / Anthropic -- Native Completion (2023-06-01 API)
POST {{proxy-host}}/proxy/anthropic/v1/complete
Authorization: Bearer {{proxy-key}}
anthropic-version: 2023-06-01
Content-Type: application/json

{
  "model": "claude-v1.3",
  "max_tokens_to_sample": 20,
  "temperature": 0.2,
  "stream": true,
  "prompt": "What is genshin impact\n\n:Assistant:"
}

###
# @name Proxy / Anthropic -- OpenAI-to-Anthropic API Translation
POST {{proxy-host}}/proxy/anthropic/v1/chat/completions
Authorization: Bearer {{proxy-key}}
#anthropic-version: 2023-06-01
Content-Type: application/json

{
  "model": "gpt-3.5-turbo",
  "max_tokens": 20,
  "stream": false,
  "temperature": 0,
  "messages": [
    {
      "role": "user",
      "content": "What is genshin impact"
    }
  ]
}

###
# @name Proxy / AWS Claude -- Native Completion
POST {{proxy-host}}/proxy/aws/claude/v1/complete
Authorization: Bearer {{proxy-key}}
anthropic-version: 2023-01-01
Content-Type: application/json

{
  "model": "claude-v2",
  "max_tokens_to_sample": 10,
  "temperature": 0,
  "stream": true,
  "prompt": "What is genshin impact\n\n:Assistant:"
}

###
# @name Proxy / AWS Claude -- OpenAI-to-Anthropic API Translation
POST {{proxy-host}}/proxy/aws/claude/chat/completions
Authorization: Bearer {{proxy-key}}
Content-Type: application/json

{
  "model": "gpt-3.5-turbo",
  "max_tokens": 50,
  "stream": true,
  "messages": [
    {
      "role": "user",
      "content": "What is genshin impact?"
    }
  ]
}

###
# @name Proxy / GCP Claude -- Native Completion
POST {{proxy-host}}/proxy/gcp/claude/v1/complete
Authorization: Bearer {{proxy-key}}
anthropic-version: 2023-01-01
Content-Type: application/json

{
  "model": "claude-v2",
  "max_tokens_to_sample": 10,
  "temperature": 0,
  "stream": true,
  "prompt": "What is genshin impact\n\n:Assistant:"
}

###
# @name Proxy / GCP Claude -- OpenAI-to-Anthropic API Translation
POST {{proxy-host}}/proxy/gcp/claude/chat/completions
Authorization: Bearer {{proxy-key}}
Content-Type: application/json

{
  "model": "gpt-3.5-turbo",
  "max_tokens": 50,
  "stream": true,
  "messages": [
    {
      "role": "user",
      "content": "What is genshin impact?"
    }
  ]
}

###
# @name Proxy / Azure OpenAI -- Native Chat Completions
POST {{proxy-host}}/proxy/azure/openai/chat/completions
Authorization: Bearer {{proxy-key}}
Content-Type: application/json

{
  "model": "gpt-4",
  "max_tokens": 20,
  "stream": true,
  "temperature": 1,
  "seed": 2,
  "messages": [
    {
      "role": "user",
      "content": "Hi what is the name of the fourth president of the united states?"
    },
    {
      "role": "assistant",
      "content": "That would be George Washington."
    },
    {
      "role": "user",
      "content": "That's not right."
    }
  ]
}

###
# @name Proxy / Google AI -- OpenAI-to-Google AI API Translation
POST {{proxy-host}}/proxy/google-ai/v1/chat/completions
Authorization: Bearer {{proxy-key}}
Content-Type: application/json

{
  "model": "gpt-4",
  "max_tokens": 42,
  "messages": [
    {
      "role": "user",
      "content": "Hi what is the name of the fourth president of the united states?"
    }
  ]
}


code:
import Database from "better-sqlite3";
import { v4 as uuidv4 } from "uuid";
import { config } from "../src/config";

function generateRandomIP() {
  return (
    Math.floor(Math.random() * 255) +
    "." +
    Math.floor(Math.random() * 255) +
    "." +
    Math.floor(Math.random() * 255) +
    "." +
    Math.floor(Math.random() * 255)
  );
}

function generateRandomDate() {
  const end = new Date();
  const start = new Date(end);
  start.setDate(end.getDate() - 90);
  const randomDate = new Date(
    start.getTime() + Math.random() * (end.getTime() - start.getTime())
  );
  return randomDate.toISOString();
}

function generateMockSHA256() {
  const characters = 'abcdef0123456789';
  let hash = '';

  for (let i = 0; i < 64; i++) {
    const randomIndex = Math.floor(Math.random() * characters.length);
    hash += characters[randomIndex];
  }

  return hash;
}

function getRandomModelFamily() {
  const modelFamilies = [
    "turbo",
    "gpt4",
    "gpt4-32k",
    "gpt4-turbo",
    "claude",
    "claude-opus",
    "gemini-pro",
    "mistral-tiny",
    "mistral-small",
    "mistral-medium",
    "mistral-large",
    "aws-claude",
    "aws-claude-opus",
    "gcp-claude",
    "gcp-claude-opus",
    "azure-turbo",
    "azure-gpt4",
    "azure-gpt4-32k",
    "azure-gpt4-turbo",
    "dall-e",
    "azure-dall-e",
  ];
  return modelFamilies[Math.floor(Math.random() * modelFamilies.length)];
}

(async () => {
  const db = new Database(config.sqliteDataPath);
  const numRows = 100;
  const insertStatement = db.prepare(`
  INSERT INTO events (type, ip, date, model, family, hashes, userToken, inputTokens, outputTokens)
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);

  const users = Array.from({ length: 10 }, () => uuidv4());
  function getRandomUser() {
    return users[Math.floor(Math.random() * users.length)];
  }

  const transaction = db.transaction(() => {
    for (let i = 0; i < numRows; i++) {
      insertStatement.run(
        "chat_completion",
        generateRandomIP(),
        generateRandomDate(),
        getRandomModelFamily() + "-" + Math.floor(Math.random() * 100),
        getRandomModelFamily(),
        Array.from(
          { length: Math.floor(Math.random() * 10) },
          generateMockSHA256
        ).join(","),
        getRandomUser(),
        Math.floor(Math.random() * 500),
        Math.floor(Math.random() * 6000)
      );
    }
  });

  transaction();

  console.log(`Inserted ${numRows} rows into the events table.`);
  db.close();
})();


code:
// uses the aws sdk to sign a request, then uses axios to send it to the bedrock REST API manually
import axios from "axios";
import { Sha256 } from "@aws-crypto/sha256-js";
import { SignatureV4 } from "@smithy/signature-v4";
import { HttpRequest } from "@smithy/protocol-http";

const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID!;
const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY!;

// Copied from amazon bedrock docs

// List models
// ListFoundationModels
// Service: Amazon Bedrock
// List of Bedrock foundation models that you can use. For more information, see Foundation models in the
// Bedrock User Guide.
//   Request Syntax
// GET /foundation-models?
//   byCustomizationType=byCustomizationType&byInferenceType=byInferenceType&byOutputModality=byOutputModality&byProvider=byProvider
//   HTTP/1.1
// URI Request Parameters
// The request uses the following URI parameters.
// byCustomizationType (p. 38)
// List by customization type.
//   Valid Values: FINE_TUNING
// byInferenceType (p. 38)
// List by inference type.
//   Valid Values: ON_DEMAND | PROVISIONED
// byOutputModality (p. 38)
// List by output modality type.
//   Valid Values: TEXT | IMAGE | EMBEDDING
// byProvider (p. 38)
// A Bedrock model provider.
//   Pattern: ^[a-z0-9-]{1,63}$
// Request Body
// The request does not have a request body

// Run inference on a text model
// Send an invoke request to run inference on a Titan Text G1 - Express model. We set the accept
// parameter to accept any content type in the response.
//   POST https://bedrock.us-east-1.amazonaws.com/model/amazon.titan-text-express-v1/invoke
//   -H accept: */*
// -H content-type: application/json
// Payload
// {"inputText": "Hello world"}
// Example response
// Response for the above request.
// -H content-type: application/json
// Payload
// <the model response>

const AMZ_REGION = "us-east-1";
const AMZ_HOST = "invoke-bedrock.us-east-1.amazonaws.com";

async function listModels() {
  const httpRequest = new HttpRequest({
    method: "GET",
    protocol: "https:",
    hostname: AMZ_HOST,
    path: "/foundation-models",
    headers: { ["Host"]: AMZ_HOST },
  });

  const signedRequest = await signRequest(httpRequest);
  const response = await axios.get(
    `https://${signedRequest.hostname}${signedRequest.path}`,
    { headers: signedRequest.headers }
  );
  console.log(response.data);
}

async function invokeModel() {
  const model = "anthropic.claude-v1";
  const httpRequest = new HttpRequest({
    method: "POST",
    protocol: "https:",
    hostname: AMZ_HOST,
    path: `/model/${model}/invoke`,
    headers: {
      ["Host"]: AMZ_HOST,
      ["accept"]: "*/*",
      ["content-type"]: "application/json",
    },
    body: JSON.stringify({
      temperature: 0.5,
      prompt: "\n\nHuman:Hello world\n\nAssistant:",
      max_tokens_to_sample: 10,
    }),
  });
  console.log("httpRequest", httpRequest);

  const signedRequest = await signRequest(httpRequest);
  const response = await axios.post(
    `https://${signedRequest.hostname}${signedRequest.path}`,
    signedRequest.body,
    { headers: signedRequest.headers }
  );
  console.log(response.status);
  console.log(response.headers);
  console.log(response.data);
  console.log("full url", response.request.res.responseUrl);
}

async function signRequest(request: HttpRequest) {
  const signer = new SignatureV4({
    sha256: Sha256,
    credentials: {
      accessKeyId: AWS_ACCESS_KEY_ID,
      secretAccessKey: AWS_SECRET_ACCESS_KEY,
    },
    region: AMZ_REGION,
    service: "bedrock",
  });
  return await signer.sign(request, { signingDate: new Date() });
}

// listModels();
// invokeModel();


code:
const axios = require("axios");

const concurrentRequests = 75;
const headers = {
  Authorization: "Bearer test",
  "Content-Type": "application/json",
};

const payload = {
  model: "gpt-4",
  max_tokens: 1,
  stream: false,
  messages: [{ role: "user", content: "Hi" }],
};

const makeRequest = async (i) => {
  try {
    const response = await axios.post(
      "http://localhost:7860/proxy/google-ai/v1/chat/completions",
      payload,
      { headers }
    );
    console.log(
      `Req ${i} finished with status code ${response.status} and response:`,
      response.data
    );
  } catch (error) {
    const msg = error.response
    console.error(`Error in req ${i}:`, error.message, msg || "");
  }
};

const executeRequestsConcurrently = () => {
  const promises = [];
  for (let i = 1; i <= concurrentRequests; i++) {
    console.log(`Starting request ${i}`);
    promises.push(makeRequest(i));
  }

  Promise.all(promises).then(() => {
    console.log("All requests finished");
  });
};

executeRequestsConcurrently();


code:
const axios = require("axios");

function randomInteger(max) {
  return Math.floor(Math.random() * max + 1);
}

async function testQueue() {
  const requests = Array(10).fill(undefined).map(async function() {
    const maxTokens = randomInteger(2000);

    const headers = {
      "Authorization": "Bearer test",
      "Content-Type": "application/json",
      "X-Forwarded-For": `${randomInteger(255)}.${randomInteger(255)}.${randomInteger(255)}.${randomInteger(255)}`,
    };

    const payload = {
      model: "gpt-4o-mini-2024-07-18",
      max_tokens: 20 + maxTokens,
      stream: false,
      messages: [{role: "user", content: "You are being benchmarked regarding your reliability at outputting exact, machine-comprehensible data. Output the sentence \"The quick brown fox jumps over the lazy dog.\" Do not precede it with quotemarks or any form of preamble, and do not output anything after the sentence."}],
      temperature: 0,
    };

    try {
      const response = await axios.post(
        "http://localhost:7860/proxy/openai/v1/chat/completions",
        payload,
        { headers }
      );

            if (response.status !== 200) {
          console.error(`Request {$maxTokens} finished with status code ${response.status} and response`, response.data);
          return;
        }

      const content = response.data.choices[0].message.content;

      console.log(
        `Request ${maxTokens} `,
        content === "The quick brown fox jumps over the lazy dog." ? "OK" : `mangled: ${content}`
      );
    } catch (error) {
      const msg = error.response;
      console.error(`Error in req ${maxTokens}:`, error.message, msg || "");
    }
  });

  await Promise.all(requests);
  console.log("All requests finished");
}

testQueue();


code:
import { Router } from "express";
import { z } from "zod";
import { encodeCursor, decodeCursor } from "../../shared/utils";
import { eventsRepo } from "../../shared/database/repos/event";

const router = Router();

/**
 * Returns events for the given user token.
 * GET /admin/events/:token
 * @query first - The number of events to return.
 * @query after - The cursor to start returning events from (exclusive).
 */
router.get("/:token", (req, res) => {
  const schema = z.object({
    token: z.string(),
    first: z.coerce.number().int().positive().max(200).default(25),
    after: z
      .string()
      .optional()
      .transform((v) => {
        try {
          return decodeCursor(v);
        } catch {
          return null;
        }
      })
      .nullable(),
    sort: z.string().optional(),
  });
  const args = schema.safeParse({ ...req.params, ...req.query });
  if (!args.success) {
    return res.status(400).json({ error: args.error });
  }

  const data = eventsRepo
    .getUserEvents(args.data.token, {
      limit: args.data.first,
      cursor: args.data.after,
    })
    .map((e) => ({ node: e, cursor: encodeCursor(e.date) }));

  res.json({
    data,
    endCursor: data[data.length - 1]?.cursor,
  });
});

export { router as eventsApiRouter };


code:
import { Router } from "express";
import { z } from "zod";
import * as userStore from "../../shared/users/user-store";
import { parseSort, sortBy } from "../../shared/utils";
import { UserPartialSchema, UserSchema } from "../../shared/users/schema";

const router = Router();

/**
 * Returns a list of all users, sorted by prompt count and then last used time.
 * GET /admin/users
 */
router.get("/", (req, res) => {
  const sort = parseSort(req.query.sort) || ["promptCount", "lastUsedAt"];
  const users = userStore.getUsers().sort(sortBy(sort, false));
  res.json({ users, count: users.length });
});

/**
 * Returns the user with the given token.
 * GET /admin/users/:token
 */
router.get("/:token", (req, res) => {
  const user = userStore.getUser(req.params.token);
  if (!user) {
    return res.status(404).json({ error: "Not found" });
  }
  res.json(user);
});

/**
 * Creates a new user.
 * Optionally accepts a JSON body containing `type`, and for temporary-type
 * users, `tokenLimits` and `expiresAt` fields.
 * Returns the created user's token.
 * POST /admin/users
 */
router.post("/", (req, res) => {
  const body = req.body;

  const base = z.object({
    type: UserSchema.shape.type.exclude(["temporary"]).default("normal"),
  });
  const tempUser = base
    .extend({
      type: z.literal("temporary"),
      expiresAt: UserSchema.shape.expiresAt,
      tokenLimits: UserSchema.shape.tokenLimits,
    })
    .required();

  const schema = z.union([base, tempUser]);
  const result = schema.safeParse(body);
  if (!result.success) {
    return res.status(400).json({ error: result.error });
  }

  const token = userStore.createUser({ ...result.data });
  res.json({ token });
});

/**
 * Updates the user with the given token, creating them if they don't exist.
 * Accepts a JSON body containing at least one field on the User type.
 * Returns the upserted user.
 * PUT /admin/users/:token
 */
router.put("/:token", (req, res) => {
  const result = UserPartialSchema.safeParse({
    ...req.body,
    token: req.params.token,
  });
  if (!result.success) {
    return res.status(400).json({ error: result.error });
  }
  userStore.upsertUser(result.data);
  res.json(userStore.getUser(req.params.token));
});

/**
 * Bulk-upserts users given a list of User updates.
 * Accepts a JSON body with the field `users` containing an array of updates.
 * Returns an object containing the upserted users and the number of upserts.
 * PUT /admin/users
 */
router.put("/", (req, res) => {
  const result = z.array(UserPartialSchema).safeParse(req.body.users);
  if (!result.success) {
    return res.status(400).json({ error: result.error });
  }
  const upserts = result.data.map((user) => userStore.upsertUser(user));
  res.json({ upserted_users: upserts, count: upserts.length });
});

/**
 * Disables the user with the given token. Optionally accepts a `disabledReason`
 * query parameter.
 * Returns the disabled user.
 * DELETE /admin/users/:token
 */
router.delete("/:token", (req, res) => {
  const user = userStore.getUser(req.params.token);
  const disabledReason = z
    .string()
    .optional()
    .safeParse(req.query.disabledReason);
  if (!disabledReason.success) {
    return res.status(400).json({ error: disabledReason.error });
  }
  if (!user) {
    return res.status(404).json({ error: "Not found" });
  }
  userStore.disableUser(req.params.token, disabledReason.data);
  res.json(userStore.getUser(req.params.token));
});

export { router as usersApiRouter };


code:
import { Request, Response, RequestHandler } from "express";
import { config } from "../config";

const ADMIN_KEY = config.adminKey;
const failedAttempts = new Map<string, number>();

type AuthorizeParams = { via: "cookie" | "header" };

export const authorize: ({ via }: AuthorizeParams) => RequestHandler =
  ({ via }) =>
  (req, res, next) => {
    const bearerToken = req.headers.authorization?.slice("Bearer ".length);
    const cookieToken = req.session.adminToken;
    const token = via === "cookie" ? cookieToken : bearerToken;
    const attempts = failedAttempts.get(req.ip) ?? 0;

    if (!ADMIN_KEY) {
      req.log.warn(
        { ip: req.ip },
        `Blocked admin request because no admin key is configured`
      );
      return res.status(401).json({ error: "Unauthorized" });
    }

    if (attempts > 5) {
      req.log.warn(
        { ip: req.ip, token: bearerToken },
        `Blocked admin request due to too many failed attempts`
      );
      return res.status(401).json({ error: "Too many attempts" });
    }

    if (token && token === ADMIN_KEY) {
      return next();
    }

    req.log.warn(
      { ip: req.ip, attempts, invalidToken: String(token) },
      `Attempted admin request with invalid token`
    );
    return handleFailedLogin(req, res);
  };

function handleFailedLogin(req: Request, res: Response) {
  const attempts = failedAttempts.get(req.ip) ?? 0;
  const newAttempts = attempts + 1;
  failedAttempts.set(req.ip, newAttempts);
  if (req.accepts("json", "html") === "json") {
    return res.status(401).json({ error: "Unauthorized" });
  }
  delete req.session.adminToken;
  req.session.flash = { type: "error", message: `Invalid admin key.` };
  return res.redirect("/admin/login");
}


code:
import { Router } from "express";

const loginRouter = Router();

loginRouter.get("/login", (_req, res) => {
  res.render("admin_login");
});

loginRouter.post("/login", (req, res) => {
  req.session.adminToken = req.body.token;
  res.redirect("/admin");
});

loginRouter.get("/logout", (req, res) => {
  delete req.session.adminToken;
  res.redirect("/admin/login");
});

loginRouter.get("/", (req, res) => {
  if (req.session.adminToken) {
    return res.redirect("/admin/manage");
  }
  res.redirect("/admin/login");
});

export { loginRouter };


code:
import express, { Router } from "express";
import { createWhitelistMiddleware } from "../shared/cidr";
import { HttpError } from "../shared/errors";
import { injectCsrfToken, checkCsrfToken } from "../shared/inject-csrf";
import { injectLocals } from "../shared/inject-locals";
import { withSession } from "../shared/with-session";
import { config } from "../config";
import { renderPage } from "../info-page";
import { buildInfo } from "../service-info";
import { authorize } from "./auth";
import { loginRouter } from "./login";
import { eventsApiRouter } from "./api/events";
import { usersApiRouter } from "./api/users";
import { usersWebRouter as webRouter } from "./web/manage";
import { logger } from "../logger";

const adminRouter = Router();

const whitelist = createWhitelistMiddleware(
  "ADMIN_WHITELIST",
  config.adminWhitelist
);

if (!whitelist.ranges.length && config.adminKey?.length) {
  logger.error("ADMIN_WHITELIST is empty. No admin requests will be allowed. Set 0.0.0.0/0 to allow all.");
}

adminRouter.use(whitelist);
adminRouter.use(
  express.json({ limit: "20mb" }),
  express.urlencoded({ extended: true, limit: "20mb" })
);
adminRouter.use(withSession);
adminRouter.use(injectCsrfToken);

adminRouter.use("/users", authorize({ via: "header" }), usersApiRouter);
adminRouter.use("/events", authorize({ via: "header" }), eventsApiRouter);

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(
    renderPage(buildInfo(req.protocol + "://" + req.get("host"), true))
  );
});

adminRouter.use(
  (
    err: Error,
    req: express.Request,
    res: express.Response,
    _next: express.NextFunction
  ) => {
    const data: any = { message: err.message, stack: err.stack };
    if (err instanceof HttpError) {
      data.status = err.status;
      res.status(err.status);
      if (req.accepts(["html", "json"]) === "json") {
        return res.json({ error: data });
      }
      return res.render("admin_error", data);
    } else if (err.name === "ForbiddenError") {
      data.status = 403;
      if (err.message === "invalid csrf token") {
        data.message =
          "Invalid CSRF token; try refreshing the previous page before submitting again.";
      }
      return res.status(403).render("admin_error", { ...data, flash: null });
    }
    res.status(500).json({ error: data });
  }
);

export { adminRouter };


code:
import { Router } from "express";
import ipaddr from "ipaddr.js";
import multer from "multer";
import { z } from "zod";
import { config } from "../../config";
import { HttpError } from "../../shared/errors";
import * as userStore from "../../shared/users/user-store";
import { parseSort, sortBy, paginate } from "../../shared/utils";
import { keyPool } from "../../shared/key-management";
import { LLMService, MODEL_FAMILIES } from "../../shared/models";
import { getTokenCostUsd, prettyTokens } from "../../shared/stats";
import {
  User,
  UserPartialSchema,
  UserSchema,
  UserTokenCounts,
} from "../../shared/users/schema";
import { getLastNImages } from "../../shared/file-storage/image-history";
import { blacklists, parseCidrs, whitelists } from "../../shared/cidr";
import { invalidatePowChallenges } from "../../user/web/pow-captcha";

const router = Router();

const upload = multer({
  storage: multer.memoryStorage(),
  fileFilter: (_req, file, cb) => {
    if (file.mimetype !== "application/json") {
      cb(new Error("Invalid file type"));
    } else {
      cb(null, true);
    }
  },
});

router.get("/create-user", (req, res) => {
  const recentUsers = userStore
    .getUsers()
    .sort(sortBy(["createdAt"], false))
    .slice(0, 5);
  res.render("admin_create-user", {
    recentUsers,
    newToken: !!req.query.created,
  });
});

router.get("/anti-abuse", (_req, res) => {
  const wl = [...whitelists.entries()];
  const bl = [...blacklists.entries()];

  res.render("admin_anti-abuse", {
    captchaMode: config.captchaMode,
    difficulty: config.powDifficultyLevel,
    whitelists: wl.map((w) => ({
      name: w[0],
      mode: "whitelist",
      ranges: w[1].ranges,
    })),
    blacklists: bl.map((b) => ({
      name: b[0],
      mode: "blacklist",
      ranges: b[1].ranges,
    })),
  });
});

router.post("/cidr", (req, res) => {
  const body = req.body;
  const valid = z
    .object({
      action: z.enum(["add", "remove"]),
      mode: z.enum(["whitelist", "blacklist"]),
      name: z.string().min(1),
      mask: z.string().min(1),
    })
    .safeParse(body);

  if (!valid.success) {
    throw new HttpError(
      400,
      valid.error.issues.flatMap((issue) => issue.message).join(", ")
    );
  }

  const { mode, name, mask } = valid.data;
  const list = (mode === "whitelist" ? whitelists : blacklists).get(name);
  if (!list) {
    throw new HttpError(404, "List not found");
  }
  if (valid.data.action === "remove") {
    const newRanges = new Set(list.ranges);
    newRanges.delete(mask);
    list.updateRanges([...newRanges]);
    req.session.flash = {
      type: "success",
      message: `${mode} ${name} updated`,
    };
    return res.redirect("/admin/manage/anti-abuse");
  } else if (valid.data.action === "add") {
    const result = parseCidrs(mask);
    if (result.length === 0) {
      throw new HttpError(400, "Invalid CIDR mask");
    }

    const newRanges = new Set([...list.ranges, mask]);
    list.updateRanges([...newRanges]);
    req.session.flash = {
      type: "success",
      message: `${mode} ${name} updated`,
    };
    return res.redirect("/admin/manage/anti-abuse");
  }
});

router.post("/create-user", (req, res) => {
  const body = req.body;

  const base = z.object({ type: UserSchema.shape.type.default("normal") });
  const tempUser = base
    .extend({
      temporaryUserDuration: z.coerce
        .number()
        .int()
        .min(1)
        .max(10080 * 4),
    })
    .merge(
      MODEL_FAMILIES.reduce((schema, model) => {
        return schema.extend({
          [`temporaryUserQuota_${model}`]: z.coerce.number().int().min(0),
        });
      }, z.object({}))
    )
    .transform((data: any) => {
      const expiresAt = Date.now() + data.temporaryUserDuration * 60 * 1000;
      const tokenLimits = MODEL_FAMILIES.reduce((limits, model) => {
        limits[model] = data[`temporaryUserQuota_${model}`];
        return limits;
      }, {} as UserTokenCounts);
      return { ...data, expiresAt, tokenLimits };
    });

  const createSchema = body.type === "temporary" ? tempUser : base;
  const result = createSchema.safeParse(body);
  if (!result.success) {
    throw new HttpError(
      400,
      result.error.issues.flatMap((issue) => issue.message).join(", ")
    );
  }

  userStore.createUser({ ...result.data });
  return res.redirect(`/admin/manage/create-user?created=true`);
});

router.get("/view-user/:token", (req, res) => {
  const user = userStore.getUser(req.params.token);
  if (!user) throw new HttpError(404, "User not found");
  res.render("admin_view-user", { user });
});

router.get("/list-users", (req, res) => {
  const sort = parseSort(req.query.sort) || ["sumTokens", "createdAt"];
  const requestedPageSize =
    Number(req.query.perPage) || Number(req.cookies.perPage) || 20;
  const perPage = Math.max(1, Math.min(1000, requestedPageSize));
  const users = userStore
    .getUsers()
    .map((user) => {
      const sums = getSumsForUser(user);
      return { ...user, ...sums };
    })
    .sort(sortBy(sort, false));

  const page = Number(req.query.page) || 1;
  const { items, ...pagination } = paginate(users, page, perPage);

  return res.render("admin_list-users", {
    sort: sort.join(","),
    users: items,
    ...pagination,
  });
});

router.get("/import-users", (_req, res) => {
  res.render("admin_import-users");
});

router.post("/import-users", upload.single("users"), (req, res) => {
  if (!req.file) throw new HttpError(400, "No file uploaded");

  const data = JSON.parse(req.file.buffer.toString());
  const result = z.array(UserPartialSchema).safeParse(data.users);
  if (!result.success) throw new HttpError(400, result.error.toString());

  const upserts = result.data.map((user) => userStore.upsertUser(user));
  req.session.flash = {
    type: "success",
    message: `${upserts.length} users imported`,
  };
  res.redirect("/admin/manage/import-users");
});

router.get("/export-users", (_req, res) => {
  res.render("admin_export-users");
});

router.get("/export-users.json", (_req, res) => {
  const users = userStore.getUsers();
  res.setHeader("Content-Disposition", "attachment; filename=users.json");
  res.setHeader("Content-Type", "application/json");
  res.send(JSON.stringify({ users }, null, 2));
});

router.get("/", (_req, res) => {
  res.render("admin_index");
});

router.post("/edit-user/:token", (req, res) => {
  const result = UserPartialSchema.safeParse({
    ...req.body,
    token: req.params.token,
  });
  if (!result.success) {
    throw new HttpError(
      400,
      result.error.issues.flatMap((issue) => issue.message).join(", ")
    );
  }

  userStore.upsertUser(result.data);
  return res.status(200).json({ success: true });
});

router.post("/reactivate-user/:token", (req, res) => {
  const user = userStore.getUser(req.params.token);
  if (!user) throw new HttpError(404, "User not found");

  userStore.upsertUser({
    token: user.token,
    disabledAt: null,
    disabledReason: null,
  });
  return res.sendStatus(204);
});

router.post("/disable-user/:token", (req, res) => {
  const user = userStore.getUser(req.params.token);
  if (!user) throw new HttpError(404, "User not found");

  userStore.disableUser(req.params.token, req.body.reason);
  return res.sendStatus(204);
});

router.post("/refresh-user-quota", (req, res) => {
  const user = userStore.getUser(req.body.token);
  if (!user) throw new HttpError(404, "User not found");

  userStore.refreshQuota(user.token);
  req.session.flash = {
    type: "success",
    message: "User's quota was refreshed",
  };
  return res.redirect(`/admin/manage/view-user/${user.token}`);
});

router.post("/maintenance", (req, res) => {
  const action = req.body.action;
  let flash = { type: "", message: "" };
  switch (action) {
    case "recheck": {
      const checkable: LLMService[] = [
        "openai",
        "anthropic",
        "aws",
        "gcp",
        "azure",
        "google-ai"
      ];
      checkable.forEach((s) => keyPool.recheck(s));
      const keyCount = keyPool
        .list()
        .filter((k) => checkable.includes(k.service)).length;

      flash.type = "success";
      flash.message = `Scheduled recheck of ${keyCount} keys.`;
      break;
    }
    case "resetQuotas": {
      const users = userStore.getUsers();
      users.forEach((user) => userStore.refreshQuota(user.token));
      const { claude, gpt4, turbo } = config.tokenQuota;
      flash.type = "success";
      flash.message = `All users' token quotas reset to ${turbo} (Turbo), ${gpt4} (GPT-4), ${claude} (Claude).`;
      break;
    }
    case "resetCounts": {
      const users = userStore.getUsers();
      users.forEach((user) => userStore.resetUsage(user.token));
      flash.type = "success";
      flash.message = `All users' token usage records reset.`;
      break;
    }
    case "downloadImageMetadata": {
      const data = JSON.stringify(
        {
          exportedAt: new Date().toISOString(),
          generations: getLastNImages(),
        },
        null,
        2
      );
      res.setHeader(
        "Content-Disposition",
        `attachment; filename=image-metadata-${new Date().toISOString()}.json`
      );
      res.setHeader("Content-Type", "application/json");
      return res.send(data);
    }
    case "expireTempTokens": {
      const users = userStore.getUsers();
      const temps = users.filter((u) => u.type === "temporary");
      temps.forEach((user) => {
        user.expiresAt = Date.now();
        user.disabledReason = "Admin forced expiration.";
        userStore.upsertUser(user);
      });
      invalidatePowChallenges();
      flash.type = "success";
      flash.message = `${temps.length} temporary users marked for expiration.`;
      break;
    }
    case "cleanTempTokens": {
      const users = userStore.getUsers();
      const disabledTempUsers = users.filter(
        (u) => u.type === "temporary" && u.expiresAt && u.expiresAt < Date.now()
      );
      disabledTempUsers.forEach((user) => {
        user.disabledAt = 1; //will be cleaned up by the next cron job
        userStore.upsertUser(user);
      });
      flash.type = "success";
      flash.message = `${disabledTempUsers.length} disabled temporary users marked for cleanup.`;
      break;
    }
    case "setDifficulty": {
      const selected = req.body["pow-difficulty"];
      const valid = ["low", "medium", "high", "extreme"];
      const isNumber = Number.isInteger(Number(selected));
      if (!selected || !valid.includes(selected) && !isNumber) {
        throw new HttpError(400, "Invalid difficulty " + selected);
      }
      config.powDifficultyLevel = isNumber ? Number(selected) : selected;
      invalidatePowChallenges();
      break;
    }
    case "generateTempIpReport": {
      const tempUsers = userStore
        .getUsers()
        .filter((u) => u.type === "temporary");
      const ipv4RangeMap = new Map<string, Set<string>>();
      const ipv6RangeMap = new Map<string, Set<string>>();

      tempUsers.forEach((u) => {
        u.ip.forEach((ip) => {
          try {
            const parsed = ipaddr.parse(ip);
            if (parsed.kind() === "ipv4") {
              const subnet =
                parsed.toNormalizedString().split(".").slice(0, 3).join(".") +
                ".0/24";
              const userSet = ipv4RangeMap.get(subnet) || new Set();
              userSet.add(u.token);
              ipv4RangeMap.set(subnet, userSet);
            } else if (parsed.kind() === "ipv6") {
              const subnet =
                parsed.toNormalizedString().split(":").slice(0, 4).join(":") +
                "::/48";
              const userSet = ipv6RangeMap.get(subnet) || new Set();
              userSet.add(u.token);
              ipv6RangeMap.set(subnet, userSet);
            }
          } catch (e) {
            req.log.warn(
              { ip, error: e.message },
              "Invalid IP address; skipping"
            );
          }
        });
      });

      const ipv4Ranges = Array.from(ipv4RangeMap.entries())
        .map(([subnet, userSet]) => ({
          subnet,
          distinctTokens: userSet.size,
        }))
        .sort((a, b) => b.distinctTokens - a.distinctTokens);

      const ipv6Ranges = Array.from(ipv6RangeMap.entries())
        .map(([subnet, userSet]) => ({
          subnet,
          distinctTokens: userSet.size,
        }))
        .sort((a, b) => {
          if (a.distinctTokens === b.distinctTokens) {
            return a.subnet.localeCompare(b.subnet);
          }
          return b.distinctTokens - a.distinctTokens;
        });

      const data = JSON.stringify(
        {
          exportedAt: new Date().toISOString(),
          ipv4Ranges,
          ipv6Ranges,
        },
        null,
        2
      );

      res.setHeader(
        "Content-Disposition",
        `attachment; filename=temp-ip-report-${new Date().toISOString()}.json`
      );
      res.setHeader("Content-Type", "application/json");
      return res.send(data);
    }
    default: {
      throw new HttpError(400, "Invalid action");
    }
  }

  req.session.flash = flash;
  const referer = req.get("referer");

  return res.redirect(referer || "/admin/manage");
});

router.get("/download-stats", (_req, res) => {
  return res.render("admin_download-stats");
});

router.post("/generate-stats", (req, res) => {
  const body = req.body;

  const valid = z
    .object({
      anon: z.coerce.boolean().optional().default(false),
      sort: z.string().optional().default("prompts"),
      maxUsers: z.coerce
        .number()
        .int()
        .min(5)
        .max(1000)
        .optional()
        .default(1000),
      tableType: z.enum(["code", "markdown"]).optional().default("markdown"),
      format: z
        .string()
        .optional()
        .default("# Stats\n{{header}}\n{{stats}}\n{{time}}"),
    })
    .strict()
    .safeParse(body);

  if (!valid.success) {
    throw new HttpError(
      400,
      valid.error.issues.flatMap((issue) => issue.message).join(", ")
    );
  }

  const { anon, sort, format, maxUsers, tableType } = valid.data;
  const users = userStore.getUsers();

  let totalTokens = 0;
  let totalCost = 0;
  let totalPrompts = 0;
  let totalIps = 0;

  const lines = users
    .map((u) => {
      const sums = getSumsForUser(u);
      totalTokens += sums.sumTokens;
      totalCost += sums.sumCost;
      totalPrompts += u.promptCount;
      totalIps += u.ip.length;

      const getName = (u: User) => {
        const id = `...${u.token.slice(-5)}`;
        const banned = !!u.disabledAt;
        let nick = anon || !u.nickname ? "Anonymous" : u.nickname;

        if (tableType === "markdown") {
          nick = banned ? `~~${nick}~~` : nick;
          return `${nick.slice(0, 18)} | ${id}`;
        } else {
          // Strikethrough doesn't work within code blocks
          const dead = !!u.disabledAt ? "[dead] " : "";
          nick = `${dead}${nick}`;
          return `${nick.slice(0, 18).padEnd(18)} ${id}`.padEnd(27);
        }
      };

      const user = getName(u);
      const prompts = `${u.promptCount} proompts`.padEnd(14);
      const ips = `${u.ip.length} IPs`.padEnd(8);
      const tokens = `${sums.prettyUsage} tokens`.padEnd(30);
      const sortField = sort === "prompts" ? u.promptCount : sums.sumTokens;
      return { user, prompts, ips, tokens, sortField };
    })
    .sort((a, b) => b.sortField - a.sortField)
    .map(({ user, prompts, ips, tokens }, i) => {
      const pos = tableType === "markdown" ? (i + 1 + ".").padEnd(4) : "";
      return `${pos}${user} | ${prompts} | ${ips} | ${tokens}`;
    })
    .slice(0, maxUsers);

  const strTotalPrompts = `${totalPrompts} proompts`;
  const strTotalIps = `${totalIps} IPs`;
  const strTotalTokens = `${prettyTokens(totalTokens)} tokens`;
  const strTotalCost = `US$${totalCost.toFixed(2)} cost`;
  const header = `!!!Note ${users.length} users | ${strTotalPrompts} | ${strTotalIps} | ${strTotalTokens} | ${strTotalCost}`;
  const time = `\n-> *(as of ${new Date().toISOString()})* <-`;

  let table = [];
  table.push(lines.join("\n"));

  if (valid.data.tableType === "markdown") {
    table = ["User||Prompts|IPs|Usage", "---|---|---|---|---", ...table];
  } else {
    table = ["```text", ...table, "```"];
  }

  const result = format
    .replace("{{header}}", header)
    .replace("{{stats}}", table.join("\n"))
    .replace("{{time}}", time);

  res.setHeader(
    "Content-Disposition",
    `attachment; filename=proxy-stats-${new Date().toISOString()}.md`
  );
  res.setHeader("Content-Type", "text/markdown");
  res.send(result);
});

function getSumsForUser(user: User) {
  const sums = MODEL_FAMILIES.reduce(
    (s, model) => {
      const tokens = user.tokenCounts[model] ?? 0;
      s.sumTokens += tokens;
      s.sumCost += getTokenCostUsd(model, tokens);
      return s;
    },
    { sumTokens: 0, sumCost: 0, prettyUsage: "" }
  );
  sums.prettyUsage = `${prettyTokens(sums.sumTokens)} ($${sums.sumCost.toFixed(
    2
  )})`;
  return sums;
}

export { router as usersWebRouter };


code:
<%- include("partials/shared_header", { title: "Proof of Work Verification Settings - OAI Reverse Proxy Admin" }) %>

Abuse Mitigation Settings

Proof-of-Work Verification

    The Proof-of-Work difficulty level is used to determine how much work a client must perform to earn a temporary user
    token. Higher difficulty levels require more work, which can help mitigate abuse by making it more expensive for
    attackers to generate tokens. However, higher difficulty levels can also make it more difficult for legitimate users
    to generate tokens. Refer to documentation for guidance.

  <%if (captchaMode === "none") { %>

    PoW verification is not enabled. Set CAPTCHA_MODE=proof_of_work to enable.

  <% } else { %>

Difficulty Level

    Difficulty Level:
    <select name="difficulty" id="difficulty" onchange="difficultyChanged(event)">
      <option value="low">Low</option>
      <option value="medium">Medium</option>
      <option value="high">High</option>
      <option value="extreme">Extreme</option>
      <option value="custom">Custom</option>
    </select>

      Hashes required (average):
      <input type="number" id="customDifficulty" value="0" min="1" max="1000000000" />

    <button onclick='doAction("setDifficulty")'>Update Difficulty</button>

Current Difficulty: <%= difficulty %>

  <% } %>
  <form id="maintenanceForm" action="/admin/manage/maintenance" method="post">
    <input id="_csrf" type="hidden" name="_csrf" value="<%= csrfToken %>" />
    <input id="hiddenAction" type="hidden" name="action" value="" />
    <input id="hiddenDifficulty" type="hidden" name="pow-difficulty" value="" />
  </form>

Manage Temporary User Tokens

<button onclick='doAction("expireTempTokens")'>🕒 Expire All Temp Tokens</button>

<button onclick='doAction("cleanTempTokens")'>🧹 Delete Expired Temp Tokens</button>

<button onclick='doAction("generateTempIpReport")'>📊 Generate Temp Token IP Report</button>

IP Whitelists and Blacklists

    You can specify IP ranges to whitelist or blacklist from accessing the proxy. Entries can be specified as single
    addresses or
    CIDR notation. IPv6 is
    supported but not recommended for use with the current version of the proxy.

    Note: Changes here are not persisted across server restarts. If you want to make changes permanent,
    you can copy the values to your deployment configuration.

  <% for (let i = 0; i < whitelists.length; i++) { %>
  <%- include("partials/admin-cidr-widget", { list: whitelists[i] }) %>
  <% } %>
  <% for (let i = 0; i < blacklists.length; i++) { %>
  <%- include("partials/admin-cidr-widget", { list: blacklists[i] }) %>
  <% } %>
  <form action="/admin/manage/cidr" method="post" id="cidrForm">
    <input id="_csrf" type="hidden" name="_csrf" value="<%= csrfToken %>" />
    <input type="hidden" name="action" value="add" />
    <input type="hidden" name="name" value="" />
    <input type="hidden" name="mode" value="" />
    <input type="hidden" name="mask" value="" />
  </form>

    Copy environment variables

      If you have made changes with the UI, you can copy the values below to your deployment configuration to persist
      them across server restarts.

    <% for (let i = 0; i < whitelists.length; i++) { %><%= whitelists[i].name %>=<%= whitelists[i].ranges.join(",") %><% } %>
    <% for (let i = 0; i < blacklists.length; i++) { %><%= blacklists[i].name %>=<%= blacklists[i].ranges.join(",") %><% } %>

<%- include("partials/admin-footer") %>


code:
<%- include("partials/shared_header", { title: "Create User - OAI Reverse Proxy Admin" }) %>

Create User Token

User token types:

Normal - Standard users.

Special - Exempt from token quotas and MAX_IPS_PER_USER enforcement.

Temporary - Disabled after a specified duration. Quotas never refresh.

<form action="/admin/manage/create-user" method="post">
  <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
  Type
  <select name="type">
    <option value="normal">Normal</option>
    <option value="special">Special</option>
    <option value="temporary">Temporary</option>
  </select>
  <input type="submit" value="Create" />
  <fieldset id="temporaryUserOptions" style="display: none">
    <legend>Temporary User Options</legend>

        Temporary users will be disabled after the specified duration, and their records will be permanently deleted after some time.
        These options apply only to new temporary users; existing ones use whatever options were in effect when they were created.

      Access duration (in minutes)
      <input type="number" name="temporaryUserDuration" id="temporaryUserDuration" value="60" class="full-width" />

      6 hours:360
      12 hours:720
      1 day:1440
      1 week:10080

Token Quotas

Temporary users' quotas are never refreshed.

      <% Object.entries(quota).forEach(function([model, tokens]) { %>
      <label class="quota-label" for="temporaryUserQuota_<%= model %>"><%= model %>
      <input
        type="number"
        name="temporaryUserQuota_<%= model %>"
        id="temporaryUserQuota_<%= model %>"
        value="0"
        data-fieldtype="tokenquota"
        data-default="<%= tokens %>" />
      <% }) %>

  </fieldset>
</form>
<% if (newToken) { %>

Just created <%= recentUsers[0].token %>.

<% } %>

Recent Tokens

  <% recentUsers.forEach(function(user) { %>

<a href="/admin/manage/view-user/<%= user.token %>"><%= user.token %>

  <% }) %>

<%- include("partials/admin-footer") %>


code:
<%- include("partials/shared_header", { title: "Download Stats - OAI Reverse Proxy Admin" }) %>

Download Stats

Download usage statistics to a Markdown document. You can paste this into a service like Rentry.org to share it.

Options

  <form
    id="statsForm"
    action="/admin/manage/generate-stats"
    method="post"
    style="display: flex; flex-direction: column">
    <input id="_csrf" type="hidden" name="_csrf" value="<%= csrfToken %>" />

      <input id="anon" type="checkbox" name="anon" value="true" /> Anonymize

      Sort
      <select id="sort" name="sort">
        <option value="tokens" selected>By Token Count</option>
        <option value="prompts">By Prompt Count</option>
      </select>

      Max Users
      <input id="maxUsers" type="number" name="maxUsers" value="1000" />

      Table Type
      <select id="tableType" name="tableType">
        <option value="markdown" selected>Markdown Table</option>
        <option value="code">Code Block</option>
      </select>

      Custom Format

{{header}}

{{stats}}

{{time}}

      <textarea id="format" name="format" rows="10" cols="50" placeholder="{{stats}}">
# Stats
{{header}}
{{stats}}
{{time}}
    </textarea>

      <button type="submit">Download</button>
      <button id="copyButton" type="button">Copy to Clipboard</button>

  </form>

<%- include("partials/admin-footer") %>


code:
<%- include("partials/shared_header", { title: "Error" }) %>

⚠ Error <%= status %>: <%= message %>

<%= stack %>

      Go Back | Go Home


code:
<%- include("partials/shared_header", { title: "Export Users - OAI Reverse Proxy Admin" }) %>

Export Users

  Export users to JSON. The JSON will be an array of objects under the key
  users. You can use this JSON to import users later.

<button onclick="exportUsers()">Export</button>
<%- include("partials/admin-footer") %>


code:
<%- include("partials/shared_header", { title: "Import Users - OAI Reverse Proxy Admin" }) %>

Import Users

  Import users from JSON. The JSON should be an array of objects under the key
  users. Each object should have the following fields:

token (required): a unique identifier for the user

nickname (optional): a nickname for the user, max 80 chars

ip (optional): IP addresses the user has connected from

    type (optional): either normal or
    special

    promptCount (optional): the number of times the user has sent a
    prompt

    tokenCounts (optional): the number of tokens the user has
    consumed. This should be an object with keys turbo,
    gpt4, and claude.

    tokenLimits (optional): the number of tokens the user can
    consume. This should be an object with keys turbo,
    gpt4, and claude.

    createdAt (optional): the timestamp when the user was created

    disabledAt (optional): the timestamp when the user was disabled

    disabledReason (optional): the reason the user was disabled

  If a user with the same token already exists, the existing user will be
  updated with the new values.

<form action="/admin/manage/import-users?_csrf=<%= csrfToken %>" method="post" enctype="multipart/form-data">
  <input type="file" name="users" />
  <input type="submit" value="Import" />
</form>
</form>
<%- include("partials/admin-footer") %>


code:
<%- include("partials/shared_header", { title: "OAI Reverse Proxy Admin" }) %>

OAI Reverse Proxy Admin

<% if (!usersEnabled) { %>

  🚨 user_token gatekeeper is not enabled.

None of the user management features will do anything.

<% } %>
<% if (!persistenceEnabled) { %>

  ⚠ Users will be lost when the server restarts because persistence is not configured.

Be sure to export your users and import them again after restarting the server if you want to keep them.

  See the

    user management documentation
  to learn how to set up persistence.

<% } %>

Users

List Users

Create User

Import Users

Export Users

Download Rentry Stats

Abuse Mitigation Settings

Service Info

Maintenance

<form id="maintenanceForm" action="/admin/manage/maintenance" method="post">
  <input id="_csrf" type="hidden" name="_csrf" value="<%= csrfToken %>" />
  <input id="hiddenAction" type="hidden" name="action" value="" />

    <fieldset>
      <legend>Key Recheck</legend>
      <button id="recheck-keys" type="button" onclick="submitForm('recheck')">Force Key Recheck</button>
      Triggers a recheck of all keys without restarting the server.
    </fieldset>
    <% if (quotasEnabled) { %>
    <fieldset>
      <legend>Bulk Quota Management</legend>

        <button id="refresh-quotas" type="button" onclick="submitForm('resetQuotas')">Refresh All Quotas</button>
        Immediately refreshes all users' quotas by the configured amounts.

        <button id="clear-token-counts" type="button" onclick="submitForm('resetCounts')">Clear All Token Counts</button>
        Resets all users' token records to zero.

    </fieldset>
    <% } %>
    <% if (imageGenerationEnabled) { %>
    <fieldset>
      <legend>Image Generation</legend>
      <button id="download-image-metadata" type="button" onclick="submitForm('downloadImageMetadata')">Download Image Metadata</button>
      Downloads a metadata file containing URL, prompt, and truncated user token for all cached images.
    </fieldset>
    <% } %>

</form>

<%- include("partials/admin-footer") %>


code:
<%- include("partials/shared_header", { title: "Users - OAI Reverse Proxy Admin" }) %>

User Token List

    <% if (users.length === 0) { %>

No users found.

    <% } else { %>
    <input type="checkbox" id="toggle-nicknames" onchange="toggleNicknames()" /> Show Nicknames

          User
          <th <% if (sort.includes("ip")) { %>class="active"<% } %> >IPs
          <th <% if (sort.includes("promptCount")) { %>class="active"<% } %> >Prompts
          <th <% if (sort.includes("sumCost")) { %>class="active"<% } %> >Usage
          Type
          <th <% if (sort.includes("createdAt")) { %>class="active"<% } %> >Created (UTC)
          <th <% if (sort.includes("lastUsedAt")) { %>class="active"<% } %> >Last Used (UTC)
          Banned?

        <% users.forEach(function(user){ %>

            <a href="/admin/manage/view-user/<%= user.token %>">
            <%= user.token %>
            <% if (user.nickname) { %>
            <%= user.nickname %>
            <% } else { %>
            <%= "..." + user.token.slice(-5) %>
            <% } %>

          <%= user.ip.length %>
          <%= user.promptCount %>
          <%= user.prettyUsage %>
          <%= user.type %>
          <%= user.createdAt %>
          <%= user.lastUsedAt ?? "never" %>

            <% if (user.disabledAt) { %>
            <a title="Unban" href="#" class="unban" data-token="<%= user.token %>">🔄
            <% } else { %>
            <a title="Ban" href="#" class="ban" data-token="<%= user.token %>">🚫
            <% } %>
          <%= user.disabledAt ? "Yes" : "No" %> <%= user.disabledReason ? `(${user.disabledReason})` : "" %>

        <% }); %>

      <% if (page > 1) { %>

<a href="/admin/manage/list-users?sort=<%= sort %>&page=<%= page - 1 %>">«

      <% } %> <% for (var i = 1; i <= pageCount; i++) { %>
      <li <% if (i === page) { %>class="active"<% } %>><a href="/admin/manage/list-users?sort=<%= sort %>&page=<%= i %>"><%= i %>

      <% } %> <% if (page < pageCount) { %>

<a href="/admin/manage/list-users?sort=<%= sort %>&page=<%= page + 1 %>">»

      <% } %>

Showing <%= page * pageSize - pageSize + 1 %> to <%= users.length + page * pageSize - pageSize %> of <%= totalCount %> users.

    <%- include("partials/shared_pagination") %>
    <% } %>

<%- include("partials/admin-ban-xhr-script") %>

<%- include("partials/admin-footer") %>


code:
<%- include("partials/shared_header", { title: "Login" }) %>

Login

    <form action="/admin/login" method="post">
      <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
      Admin Key
      <input type="password" name="token" />
      <input type="submit" value="Login" />
    </form>


code:
<%- include("partials/shared_header", { title: "View User - OAI Reverse Proxy Admin" }) %>

View User

      Key
      Value

      Token
      <%- user.token %>

      Nickname
      <%- user.nickname ?? "none" %>

        <a title="Edit" id="edit-nickname" href="#" data-field="nickname" data-token="<%= user.token %>">✏

      Type
      <%- user.type %>

        <a title="Edit" id="edit-type" href="#" data-field="type" data-token="<%= user.token %>">✏

      Prompts
      <%- user.promptCount %>

      Created At
      <%- user.createdAt %>

      Last Used At
      <%- user.lastUsedAt || "never" %>

      Disabled At
      <%- user.disabledAt %>

        <% if (user.disabledAt) { %>
        <a title="Unban" href="#" class="unban" data-token="<%= user.token %>">🔄
        <% } else { %>
        <a title="Ban" href="#" class="ban" data-token="<%= user.token %>">🚫
        <% } %>

      Disabled Reason
      <%- user.disabledReason %>
      <% if (user.disabledAt) { %>

        <a title="Edit" id="edit-disabledReason" href="#" data-field="disabledReason" data-token="<%= user.token %>"
          >✏

      <% } %>

      IP Address Limit
      <%- (user.maxIps ?? maxIps) || "Unlimited" %>

        <a title="Edit" id="edit-maxIps" href="#" data-field="maxIps" data-token="<%= user.token %>">✏

      IPs
      <%- include("partials/shared_user_ip_list", { user, shouldRedact: false }) %>

        Admin Note 🔒

      <%- user.adminNote ?? "none" %>

        <a title="Edit" id="edit-adminNote" href="#" data-field="adminNote" data-token="<%= user.token %>">✏

    <% if (user.type === "temporary") { %>

      Expires At
      <%- user.expiresAt %>

    <% } %>
    <% if (user.meta) { %>

      Meta
      <%- JSON.stringify(user.meta) %>

    <% } %>

<form style="display: none" id="current-values">
  <input type="hidden" name="token" value="<%- user.token %>" />
  <% ["nickname", "type", "disabledAt", "disabledReason", "maxIps", "adminNote"].forEach(function (key) { %>
  <input type="hidden" name="<%- key %>" value="<%- user[key] %>" />
  <% }); %>

  <% Object.entries(quota).forEach(([family]) => { %>
  <input type="hidden" name="tokenRefresh_<%- family %>" value="<%- user.tokenRefresh[family] || quota[family] %>" />
  <% }); %>
</form>

Quota Information

<% if (quotasEnabled) { %>
<form action="/admin/manage/refresh-user-quota" method="POST">
  <input type="hidden" name="token" value="<%- user.token %>" />
  <input type="hidden" name="_csrf" value="<%- csrfToken %>" />
  <button type="submit" class="btn btn-primary">Refresh Quotas for User</button>
</form>
<% } %>
<%- include("partials/shared_quota-info", { quota, user, showRefreshEdit: true }) %>

Back to User List

<%- include("partials/admin-ban-xhr-script") %>
<%- include("partials/admin-footer") %>


code:
<script>
  document.querySelectorAll("td.actions a.ban").forEach(function (a) {
    a.addEventListener("click", function (e) {
      e.preventDefault();
      var token = a.getAttribute("data-token");
      if (confirm("Are you sure you want to ban this user?")) {
        let reason = prompt("Reason for ban:");
        fetch("/admin/manage/disable-user/" + token, {
          method: "POST",
          credentials: "same-origin",
          body: JSON.stringify({ reason, _csrf: document.querySelector("meta[name=csrf-token]").getAttribute("content") }),
          headers: { "Content-Type": "application/json" },
        }).then(() => window.location.reload());
      }
    });
  });

  document.querySelectorAll("td.actions a.unban").forEach(function (a) {
    a.addEventListener("click", function (e) {
      e.preventDefault();
      var token = a.getAttribute("data-token");
      if (confirm("Are you sure you want to unban this user?")) {
        fetch("/admin/manage/reactivate-user/" + token, {
          method: "POST",
          credentials: "same-origin",
          body: JSON.stringify({ _csrf: document.querySelector("meta[name=csrf-token]").getAttribute("content") }),
          headers: { "Content-Type": "application/json" },
        }).then(() => window.location.reload());
      }
    });
  });
</script>


code:
  <%= list.name %>
  (<%= list.mode %>)

  <% list.ranges.forEach(function(mask) { %>

    <%= mask %>
    <button class="remove" data-mode="<%= list.mode %>" data-name="<%= list.name %>" data-mask="<%= mask %>" onclick="onRemoveCidr(event)">Remove</button>

  <% }); %>

<button class="add" data-mode="<%= list.mode %>" data-name="<%= list.name %>" onclick="onAddCidr(event)">Add</button>