adds user-specific overrides for daily quota refresh

This commit is contained in:
nai-degen
2024-07-27 14:25:53 -05:00
parent f242777596
commit 2aa19e5b09
8 changed files with 92 additions and 37 deletions
+2
View File
@@ -37,6 +37,8 @@ export const UserSchema = z
tokenCounts: tokenCountsSchema,
/** Maximum number of tokens the user can consume, by model family. */
tokenLimits: tokenCountsSchema,
/** User-specific token refresh amount, by model family. */
tokenRefresh: tokenCountsSchema,
/** Time at which the user was created. */
createdAt: z.number(),
/** Time at which the user last connected. */
+30 -12
View File
@@ -70,6 +70,7 @@ export function createUser(createOptions?: {
type?: User["type"];
expiresAt?: number;
tokenLimits?: User["tokenLimits"];
tokenRefresh?: User["tokenRefresh"];
}) {
const token = uuid();
const newUser: User = {
@@ -79,6 +80,7 @@ export function createUser(createOptions?: {
promptCount: 0,
tokenCounts: { ...INITIAL_TOKENS },
tokenLimits: createOptions?.tokenLimits ?? { ...config.tokenQuota },
tokenRefresh: createOptions?.tokenRefresh ?? { ...INITIAL_TOKENS },
createdAt: Date.now(),
meta: {},
};
@@ -123,6 +125,7 @@ export function upsertUser(user: UserUpdate) {
promptCount: 0,
tokenCounts: { ...INITIAL_TOKENS },
tokenLimits: { ...config.tokenQuota },
tokenRefresh: { ...INITIAL_TOKENS },
createdAt: Date.now(),
meta: {},
};
@@ -139,7 +142,6 @@ export function upsertUser(user: UserUpdate) {
}
}
// TODO: Write firebase migration to backfill new fields
if (updates.tokenCounts) {
for (const family of MODEL_FAMILIES) {
updates.tokenCounts[family] ??= 0;
@@ -150,6 +152,11 @@ export function upsertUser(user: UserUpdate) {
updates.tokenLimits[family] ??= 0;
}
}
if (updates.tokenRefresh) {
for (const family of MODEL_FAMILIES) {
updates.tokenRefresh[family] ??= 0;
}
}
users.set(user.token, Object.assign(existing, updates));
usersToFlush.add(user.token);
@@ -245,19 +252,30 @@ export function hasAvailableQuota({
return tokensConsumed < tokenLimit;
}
/**
* For the given user, sets token limits for each model family to the sum of the
* current count and the refresh amount, up to the default limit. If a quota is
* not specified for a model family, it is not touched.
*/
export function refreshQuota(token: string) {
const user = users.get(token);
if (!user) return;
const { tokenCounts, tokenLimits } = user;
const quotas = Object.entries(config.tokenQuota) as [ModelFamily, number][];
quotas
// If a quota is not configured, don't touch any existing limits a user may
// already have been assigned manually.
.filter(([, quota]) => quota > 0)
.forEach(
([model, quota]) =>
(tokenLimits[model] = (tokenCounts[model] ?? 0) + quota)
);
const { tokenQuota } = config;
const { tokenCounts, tokenLimits, tokenRefresh } = user;
// Get default quotas for each model family.
const defaultQuotas = Object.entries(tokenQuota) as [ModelFamily, number][];
// If any user-specific refresh quotas are present, override default quotas.
const userQuotas = defaultQuotas.map(([f, q]) => [
f,
(tokenRefresh[f] ?? 0) || q,
] as const /* narrow to tuple */);
userQuotas
// Ignore families with no global or user-specific refresh quota.
.filter(([, q]) => q > 0)
// Increase family token limit by the family's refresh amount.
.forEach(([f, q]) => (tokenLimits[f] = (tokenCounts[f] ?? 0) + q));
usersToFlush.add(token);
}
@@ -307,7 +325,7 @@ function cleanupExpiredTokens() {
user.meta.refreshable = config.captchaMode !== "none";
disabled++;
}
const purgeTimeout = config.powTokenPurgeHours * 60 * 60 * 1000;
const purgeTimeout = config.powTokenPurgeHours * 60 * 60 * 1000;
if (user.disabledAt && user.disabledAt + purgeTimeout < now) {
users.delete(user.token);
usersToFlush.add(user.token);
+20 -9
View File
@@ -33,7 +33,7 @@
.pagination li a {
display: block;
padding: 0.5em 1em;
border-bottom: none;
border-bottom: none;
text-decoration: none;
}
.pagination li.active a {
@@ -71,20 +71,24 @@
td.actions:hover {
background-color: #e0e6f6;
}
tr > td,
tr > th {
border-right: 1px solid #dedede;
}
@media (max-width: 800px) {
body {
padding: 0.5em;
}
table.full-width {
width: 100%;
position: static;
left: auto;
right: auto;
margin-left: 0;
margin-right: 0;
}
table.full-width {
width: 100%;
position: static;
left: auto;
right: auto;
margin-left: 0;
margin-right: 0;
}
}
@media (prefers-color-scheme: dark) {
@@ -95,6 +99,13 @@
th.active {
background-color: #446;
}
td.actions:hover {
background-color: #446;
}
tr > td,
tr > th {
border-right: 1px solid #444;
}
}
</style>
</head>
@@ -1,4 +1,6 @@
<p>Next refresh: <time><%- nextQuotaRefresh %></time></p>
<p>
Next refresh: <time><%- nextQuotaRefresh %></time>
</p>
<table class="striped">
<thead>
<tr>
@@ -9,7 +11,7 @@
<% } %>
<th scope="col">Limit</th>
<th scope="col">Remaining</th>
<th scope="col">Refresh Amount</th>
<th scope="col" colspan="<%= showRefreshEdit ? 2 : 1 %>">Refresh Amount</th>
</tr>
</thead>
<tbody>
@@ -19,7 +21,7 @@
<td><%- prettyTokens(user.tokenCounts[key]) %></td>
<% if (showTokenCosts) { %>
<td>$<%- tokenCost(key, user.tokenCounts[key]).toFixed(2) %></td>
<% } %>
<% } %>
<% if (!user.tokenLimits[key]) { %>
<td colspan="2" style="text-align: center">unlimited</td>
<% } else { %>
@@ -29,7 +31,20 @@
<% if (user.type === "temporary") { %>
<td>N/A</td>
<% } else { %>
<td><%- prettyTokens(quota[key]) %></td>
<td><%- prettyTokens(user.tokenRefresh[key] || quota[key]) %></td>
<% } %>
<% if (showRefreshEdit) { %>
<td class="actions">
<a
title="Edit"
id="edit-refresh"
href="#"
data-field="tokenRefresh_<%= key %>"
data-token="<%= user.token %>"
data-modelFamily="<%= key %>"
>✏️</a
>
</td>
<% } %>
</tr>
<% }) %>