Turn per-user token quota off by default; keep code for later
This commit is contained in:
12
.env.example
12
.env.example
@@ -25,9 +25,11 @@ APP_PASSWORD=
|
||||
RATE_LIMIT_MAX=20
|
||||
RATE_LIMIT_WINDOW_MS=300000
|
||||
|
||||
# Caps combined input+output tokens per named user, per UTC calendar day.
|
||||
# Default: 50000 tokens/day (roughly 100-200 AI drafts with this tool's
|
||||
# prompt size). Usage is written to TOKEN_USAGE_FILE and persists across
|
||||
# restarts if that file's folder is a mounted volume.
|
||||
TOKEN_LIMIT_PER_USER=50000
|
||||
# On the back burner: off by default. Caps combined input+output tokens per
|
||||
# named user, per UTC calendar day. Leave at 0 (or unset) for unlimited,
|
||||
# which also skips writing TOKEN_USAGE_FILE. Set a positive number (for
|
||||
# example 50000, roughly 100-200 AI drafts with this tool's prompt size) to
|
||||
# turn it back on. Usage then persists across restarts if TOKEN_USAGE_FILE's
|
||||
# folder is a mounted volume.
|
||||
TOKEN_LIMIT_PER_USER=0
|
||||
TOKEN_USAGE_FILE=./data/token-usage.json
|
||||
|
||||
@@ -29,12 +29,12 @@ If either command fails, install Docker before you continue.
|
||||
APP_USERS=tester:test-password-123,tester2:test-password-456
|
||||
RATE_LIMIT_MAX=5
|
||||
RATE_LIMIT_WINDOW_MS=60000
|
||||
TOKEN_LIMIT_PER_USER=2000
|
||||
TOKEN_USAGE_FILE=./data/token-usage.json
|
||||
```
|
||||
4. Save the file.
|
||||
|
||||
Use a personal or trial API key here, not the company-billed key. Keep the company key for the real deployment. `APP_USERS` sets up two logins so you can confirm each person gets a separate quota. `RATE_LIMIT_MAX=5` with a 60 second window makes the rate limit easy to trigger on purpose. `TOKEN_LIMIT_PER_USER=2000` is deliberately low, so you can hit the daily quota in a couple of clicks instead of a couple hundred.
|
||||
Use a personal or trial API key here, not the company-billed key. Keep the company key for the real deployment. `APP_USERS` sets up two logins so you can confirm each person gets their own. `RATE_LIMIT_MAX=5` with a 60 second window makes the rate limit easy to trigger on purpose.
|
||||
|
||||
`TOKEN_LIMIT_PER_USER` is left out of this file on purpose: the per-user daily token quota ships off by default (see Step 9, optional). Leave it out unless you specifically want to test that feature.
|
||||
|
||||
`.env` is not tracked by Git. Docker Compose reads it automatically because `docker-compose.yml` lists it under `env_file`.
|
||||
|
||||
@@ -92,18 +92,22 @@ Use a personal or trial API key here, not the company-billed key. Keep the compa
|
||||
2. Confirm the next click returns a rate-limit message instead of a normal draft or a silent failure.
|
||||
3. Wait for the time window to pass, then confirm the button works again.
|
||||
|
||||
## Step 9: Test the per-user token quota
|
||||
## Step 9 (optional): Test the per-user token quota
|
||||
|
||||
1. Log in as `tester` and click the AI draft button once or twice, until the response reports a quota error instead of a draft. With `TOKEN_LIMIT_PER_USER=2000`, this takes one or two clicks.
|
||||
2. Confirm the error names `tester`, the tokens used, the limit, and a countdown to the reset.
|
||||
3. Open a new private or incognito browser window and log in as `tester2` instead.
|
||||
4. Confirm `tester2` can still click the AI draft button. Each named user has a separate quota.
|
||||
5. Run this command to view the usage file directly:
|
||||
Skip this step for a normal test run. The quota ships off by default; this is only for confirming the feature still works if you turn it back on.
|
||||
|
||||
1. Add `TOKEN_LIMIT_PER_USER=2000` to `.env` and restart: `docker compose up -d --build`.
|
||||
2. Log in as `tester` and click the AI draft button once or twice, until the response reports a quota error instead of a draft.
|
||||
3. Confirm the error names `tester`, the tokens used, the limit, and a countdown to the reset.
|
||||
4. Open a new private or incognito browser window and log in as `tester2` instead.
|
||||
5. Confirm `tester2` can still click the AI draft button. Each named user has a separate quota.
|
||||
6. Run this command to view the usage file directly:
|
||||
```
|
||||
cat data/token-usage.json
|
||||
```
|
||||
6. Confirm it lists a separate entry for each user who made a call, with today's date and a token count.
|
||||
7. Run `docker compose restart`, then confirm `tester` is still blocked. The quota survives a restart because `data` is a mounted volume.
|
||||
7. Confirm it lists a separate entry for each user who made a call, with today's date and a token count.
|
||||
8. Run `docker compose restart`, then confirm `tester` is still blocked. The quota survives a restart because `data` is a mounted volume.
|
||||
9. Remove `TOKEN_LIMIT_PER_USER` from `.env` and restart again to turn it back off.
|
||||
|
||||
## Step 10: Stop the container
|
||||
|
||||
|
||||
13
README.md
13
README.md
@@ -128,15 +128,14 @@ The `.gitignore` file excludes `node_modules` and common system files from commi
|
||||
|
||||
## Deploy for the whole company
|
||||
|
||||
Use this when the tool needs to be reachable by anyone on the internal network or VPN, not just on one person's machine. This uses a company-owned Anthropic API key shared by everyone who reaches the tool, so it adds a per-person login, a per-person daily token quota, and a request cap that a solo local setup does not need.
|
||||
Use this when the tool needs to be reachable by anyone on the internal network or VPN, not just on one person's machine. This uses a company-owned Anthropic API key shared by everyone who reaches the tool, so it adds a per-person login and a request cap that a solo local setup does not need.
|
||||
|
||||
1. Get an Anthropic API key billed to a company account, not a personal one. IT or finance should provision this, since it is billed like any other company vendor cost.
|
||||
2. Decide who needs their own login. Each name gets its own password and its own daily token quota, tracked separately.
|
||||
2. Decide who needs their own login. Each name gets its own password.
|
||||
3. On the host or container platform, set these values as environment variables, or in a `.env` file next to `docker-compose.yml`:
|
||||
```
|
||||
ANTHROPIC_API_KEY=<company key>
|
||||
APP_USERS=alice:pass1,bob:pass2,carol:pass3
|
||||
TOKEN_LIMIT_PER_USER=50000
|
||||
```
|
||||
4. Build and run the container:
|
||||
```
|
||||
@@ -152,13 +151,13 @@ Use this when the tool needs to be reachable by anyone on the internal network o
|
||||
- It is a shared-credential list, not a real identity system. Anyone who has a name's password can use that name's quota. If real single-sign-on attribution matters later, that needs a larger integration than this tool currently supports.
|
||||
- Old single-shared-login setups still work: set `APP_USERNAME` and `APP_PASSWORD` instead of `APP_USERS` if you want everyone to share one login and one quota, unchanged from before this feature existed.
|
||||
|
||||
### Per-user token quota
|
||||
### Per-user token quota (built, currently off)
|
||||
|
||||
`TOKEN_LIMIT_PER_USER` caps combined input and output tokens per named user, per UTC calendar day. Default: 50000 tokens/day, which is roughly 100 to 200 AI drafts with this tool's prompt size. A user who hits the cap gets a clear error naming their usage and the time until reset, instead of a silent failure or an unexplained cost.
|
||||
This tool can cap combined input and output tokens per named user, per UTC calendar day, but it ships disabled. A rough cost analysis against this tool's own baseline usage (a workshop session drafts around 6 to 10 breadcrumbs, using roughly 15 to 25 percent of a 50000-token daily allowance) showed the spend at stake is small enough, and the number of people and sessions low enough, that the quota was not worth the added complexity for now. The code stays in place in case usage grows.
|
||||
|
||||
Usage is written to `TOKEN_USAGE_FILE` (default `./data/token-usage.json`) after every AI call. In Docker, `docker-compose.yml` mounts `./data` as a volume so this file survives a restart or redeploy. If you remove that volume mount, usage resets to zero every time the container restarts, which defeats the point of a daily cap.
|
||||
To turn it on: set `TOKEN_LIMIT_PER_USER` to a positive number of tokens (for example 50000) in `.env`. Leave it unset or `0` for unlimited, which is the default, and which also skips writing a usage file at all.
|
||||
|
||||
To reset one person's quota early, stop the container, edit their entry out of the usage file (or set its `date` to any past date), and restart. To raise or lower the cap for everyone, change `TOKEN_LIMIT_PER_USER` and restart; the change applies from that point on, not retroactively.
|
||||
Once enabled, usage is written to `TOKEN_USAGE_FILE` (default `./data/token-usage.json`) after every AI call. In Docker, `docker-compose.yml` already mounts `./data` as a volume, so usage survives a restart or redeploy once you turn this on. A user who hits the cap gets a clear error naming their usage and the time until reset, instead of a silent failure or an unexplained cost. To reset one person's quota early, stop the container, edit their entry out of the usage file (or set its `date` to any past date), and restart. To raise or lower the cap for everyone, change `TOKEN_LIMIT_PER_USER` and restart; the change applies from that point on, not retroactively.
|
||||
|
||||
### Rate limit
|
||||
|
||||
|
||||
18
server.js
18
server.js
@@ -120,7 +120,12 @@ function isRateLimited(ip) {
|
||||
return hits.length > RATE_LIMIT_MAX;
|
||||
}
|
||||
|
||||
/* ===== Per-user daily token quota =====
|
||||
/* ===== Per-user daily token quota (off by default) =====
|
||||
* On the back burner: real usage does not look large enough to justify
|
||||
* running this. Left in place, disabled, in case that changes. Set
|
||||
* TOKEN_LIMIT_PER_USER to a positive number to turn it back on; leave it
|
||||
* unset or 0 for unlimited, which skips the check and the usage file.
|
||||
*
|
||||
* TOKEN_LIMIT_PER_USER caps combined input+output tokens per identified user,
|
||||
* per UTC calendar day. Usage is written to TOKEN_USAGE_FILE after every AI
|
||||
* call, so it survives a container restart. Mount that file's folder as a
|
||||
@@ -132,7 +137,8 @@ function isRateLimited(ip) {
|
||||
* tally past the cap by that one call's tokens; the next call is blocked.
|
||||
* That is an accepted tradeoff, since real token cost of a call is only
|
||||
* known once its response returns. */
|
||||
const TOKEN_LIMIT_PER_USER = parseInt(process.env.TOKEN_LIMIT_PER_USER || '50000', 10);
|
||||
const TOKEN_LIMIT_PER_USER = parseInt(process.env.TOKEN_LIMIT_PER_USER || '0', 10);
|
||||
const TOKEN_LIMIT_ENABLED = TOKEN_LIMIT_PER_USER > 0;
|
||||
const TOKEN_USAGE_FILE = process.env.TOKEN_USAGE_FILE || path.join(ROOT, 'data', 'token-usage.json');
|
||||
|
||||
function todayUTC() {
|
||||
@@ -193,7 +199,7 @@ function mockClaudeResponse(res, user) {
|
||||
content: [{ type: 'text', text: JSON.stringify(draft) }],
|
||||
usage
|
||||
};
|
||||
addTokens(user, usage.input_tokens + usage.output_tokens);
|
||||
if (TOKEN_LIMIT_ENABLED) addTokens(user, usage.input_tokens + usage.output_tokens);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(JSON.stringify(body));
|
||||
}
|
||||
@@ -225,6 +231,7 @@ function proxyToClaude(req, res, user) {
|
||||
const text = await upstream.text();
|
||||
res.writeHead(upstream.status, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(text);
|
||||
if (TOKEN_LIMIT_ENABLED) {
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
if (parsed.usage) {
|
||||
@@ -233,6 +240,7 @@ function proxyToClaude(req, res, user) {
|
||||
} catch (err) {
|
||||
// Response was not JSON, or had no usage field. Nothing to record.
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
sendJson(res, 502, { error: 'Could not reach the Anthropic API: ' + err.message });
|
||||
}
|
||||
@@ -283,6 +291,7 @@ const server = http.createServer((req, res) => {
|
||||
sendJson(res, 429, { error: `Rate limit reached (${RATE_LIMIT_MAX} AI drafts per ${Math.round(RATE_LIMIT_WINDOW_MS / 60000)} min). Wait a bit and try again.` });
|
||||
return;
|
||||
}
|
||||
if (TOKEN_LIMIT_ENABLED) {
|
||||
const usedToday = usedTokensToday(loadUsage(), user);
|
||||
if (usedToday >= TOKEN_LIMIT_PER_USER) {
|
||||
sendJson(res, 429, {
|
||||
@@ -290,6 +299,7 @@ const server = http.createServer((req, res) => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
proxyToClaude(req, res, user);
|
||||
return;
|
||||
}
|
||||
@@ -309,5 +319,5 @@ server.listen(PORT, () => {
|
||||
console.log(process.env.ANTHROPIC_API_KEY ? 'ANTHROPIC_API_KEY loaded: AI draft button is live.' : 'No ANTHROPIC_API_KEY found: AI draft button will return an error until you add one to .env.');
|
||||
}
|
||||
console.log(AUTH_REQUIRED ? `Login required: ${Object.keys(USER_MAP).length} named user(s) configured.` : 'No login required: no APP_USERS or APP_USERNAME/APP_PASSWORD are set.');
|
||||
console.log(`Per-user daily token quota: ${TOKEN_LIMIT_PER_USER} tokens. Usage file: ${TOKEN_USAGE_FILE}`);
|
||||
console.log(TOKEN_LIMIT_ENABLED ? `Per-user daily token quota: ${TOKEN_LIMIT_PER_USER} tokens. Usage file: ${TOKEN_USAGE_FILE}` : 'No per-user token quota: TOKEN_LIMIT_PER_USER is unset or 0 (unlimited, feature on the back burner).');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user