fix(server): keep cost throttles on for positive fractional limits

A limit such as 0.5 floored to 0 and disabled the throttle, although only an
explicit 0 is meant to. Positive values below 1 now count as 1.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Sameh KhamisandClaude Opus 5.5 committed 2026-10-02 20:17:21 -07:00
1 parent 9b4ccbff62
commit 913d45bc07
3 files changed
+19 -2

No files matched your search

+2 -2
View File
@@ -40,7 +40,7 @@ GOOGLE_MAPS_SERVER_API_KEY=
# with a positive integer; 60 is a sensible tightening whenever HOST is not # with a positive integer; 60 is a sensible tightening whenever HOST is not
# localhost. Set it to exactly 0 to run unthrottled. An unreadable value (a # localhost. Set it to exactly 0 to run unthrottled. An unreadable value (a
# typo, a stray unit) falls back to the default rather than to unlimited, so a # typo, a stray unit) falls back to the default rather than to unlimited, so a
# slip cannot quietly disarm the guard. # slip cannot quietly disarm the guard. A positive value below 1 counts as 1.
# NOTE: this is a per-IP, process-local guard (in-memory, resets on restart; # NOTE: this is a per-IP, process-local guard (in-memory, resets on restart;
# over-limit requests get a sanitized 429) — it is NOT a billing cap. For hard # over-limit requests get a sanitized 429) — it is NOT a billing cap. For hard
# spend protection set provider-side budget alerts: Google Cloud Console -> # spend protection set provider-side budget alerts: Google Cloud Console ->
@@ -75,7 +75,7 @@ OPENAI_HUD_SUMMARY_MODEL=gpt-5-nano
# and a voice session mints one token. Raise or lower it with a positive # and a voice session mints one token. Raise or lower it with a positive
# integer; set it to exactly 0 to run unthrottled. An unreadable value (a typo, # integer; set it to exactly 0 to run unthrottled. An unreadable value (a typo,
# a stray unit) falls back to the default rather than to unlimited, so a slip # a stray unit) falls back to the default rather than to unlimited, so a slip
# cannot quietly disarm the guard. # cannot quietly disarm the guard. A positive value below 1 counts as 1.
# NOTE: this is a per-IP, process-local guard (in-memory, resets on restart; # NOTE: this is a per-IP, process-local guard (in-memory, resets on restart;
# over-limit requests get a sanitized 429) — it is NOT a billing cap. For hard # over-limit requests get a sanitized 429) — it is NOT a billing cap. For hard
# spend protection set a provider-side budget: OpenAI platform -> Settings -> # spend protection set a provider-side budget: OpenAI platform -> Settings ->
+3
View File
@@ -23,6 +23,9 @@ export function resolvePerMinuteCap(envValue, defaultPerMin) {
if (raw === undefined || raw === null || raw === '') return defaultPerMin; if (raw === undefined || raw === null || raw === '') return defaultPerMin;
const parsed = Number(raw); const parsed = Number(raw);
if (!Number.isFinite(parsed) || parsed < 0) return defaultPerMin; if (!Number.isFinite(parsed) || parsed < 0) return defaultPerMin;
// Only an explicit 0 disables the guard: a positive fraction such as 0.5
// would floor to 0, so it takes the strictest real cap instead.
if (parsed > 0 && parsed < 1) return 1;
return Math.floor(parsed); // includes the explicit 0 = unlimited opt-out return Math.floor(parsed); // includes the explicit 0 = unlimited opt-out
} }
+14
View File
@@ -95,6 +95,20 @@ test('a configured value wins, and a fractional one floors', () => {
assert.equal(resolvePerMinuteCap('30.7', 30), 30); assert.equal(resolvePerMinuteCap('30.7', 30), 30);
}); });
test('a positive fraction keeps the guard rather than flooring to unlimited', () => {
for (const fraction of ['0.5', '0.99', 0.01, '1e-9']) {
assert.equal(resolvePerMinuteCap(fraction, 30), 1, String(fraction));
assert.equal(
typeof makeCostRateLimiter(fraction, 30),
'function',
`${fraction} must still throttle`,
);
}
// Only an explicit zero turns the limiter off.
assert.equal(makeCostRateLimiter('0', 30), null);
assert.equal(makeCostRateLimiter('0.0', 30), null);
});
test('an unreadable value keeps the guard instead of removing it', () => { test('an unreadable value keeps the guard instead of removing it', () => {
// `3O` (letter O) and `60/min` are the shapes a hand-edited .env produces. // `3O` (letter O) and `60/min` are the shapes a hand-edited .env produces.
// Reading either as "unlimited" would disarm the spend guard on a typo — // Reading either as "unlimited" would disarm the spend guard on a typo —