mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
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]>
282 lines
10 KiB
JavaScript
282 lines
10 KiB
JavaScript
import assert from 'node:assert/strict';
|
||
import test from 'node:test';
|
||
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { Readable } from 'node:stream';
|
||
import { readPinokioEnvironment } from '../scripts/pinokio-environment.mjs';
|
||
import { openAiRealtimeProxy } from 'gods-eye-view/server/providers/openai';
|
||
import {
|
||
makeCostRateLimiter,
|
||
resolvePerMinuteCap,
|
||
} from '../server/providers/common/rate-limit.js';
|
||
import { OPENAI_DEFAULT_PER_MIN } from '../server/providers/openai/rate-limit.js';
|
||
import { handleHudSummary } from '../server/providers/openai/hud-summary.js';
|
||
import {
|
||
googlePlacesContextProxy,
|
||
GOOGLE_DEFAULT_PER_MIN,
|
||
} from '../server/providers/places/google.js';
|
||
|
||
// Both limiters are cached at module scope on first use, so the env value that
|
||
// matters is the one in place when a route is first exercised — restoring the
|
||
// variable afterwards does not rebuild the limiter. Each route is therefore
|
||
// driven by exactly one test here; a second test wanting a different cap would
|
||
// need its own process, not just a different env value.
|
||
|
||
/** Set an env var for one test and put the old value back afterwards. */
|
||
function env(t, name, value) {
|
||
const old = process.env[name];
|
||
if (value === undefined) delete process.env[name];
|
||
else process.env[name] = value;
|
||
t.after(() => {
|
||
if (old === undefined) delete process.env[name];
|
||
else process.env[name] = old;
|
||
});
|
||
}
|
||
|
||
function install(plugin) {
|
||
const routes = new Map();
|
||
plugin.configureServer({
|
||
middlewares: { use: (route, handler) => routes.set(route, handler) },
|
||
restart: async () => {},
|
||
});
|
||
return routes;
|
||
}
|
||
|
||
function request(
|
||
handler,
|
||
{ url = '/', method = 'POST', remoteAddress = '127.0.0.1' } = {},
|
||
) {
|
||
return new Promise((resolve, reject) => {
|
||
const req = Readable.from([]);
|
||
Object.assign(req, {
|
||
method,
|
||
url,
|
||
headers: { host: 'localhost:4173', 'content-type': 'application/json' },
|
||
socket: { remoteAddress },
|
||
});
|
||
const headers = {};
|
||
const res = {
|
||
statusCode: 200,
|
||
setHeader(name, value) {
|
||
headers[name.toLowerCase()] = value;
|
||
},
|
||
end(body = '') {
|
||
resolve({ status: this.statusCode, headers, body: String(body) });
|
||
},
|
||
};
|
||
Promise.resolve(handler(req, res)).catch(reject);
|
||
});
|
||
}
|
||
|
||
test('an unset throttle resolves to the documented default, not to unlimited', () => {
|
||
// The defect #16 reports: the cap was opt-in, so an unconfigured server let
|
||
// anyone who could reach it mint tokens on the operator's key without bound.
|
||
assert.equal(resolvePerMinuteCap(undefined, 30), 30);
|
||
assert.equal(resolvePerMinuteCap('', 30), 30);
|
||
assert.equal(resolvePerMinuteCap(' ', 30), 30);
|
||
assert.ok(
|
||
typeof makeCostRateLimiter(undefined, 30) === 'function',
|
||
'an unconfigured cost endpoint must still be throttled',
|
||
);
|
||
});
|
||
|
||
test('an explicit 0 is the documented way to run unthrottled', () => {
|
||
assert.equal(resolvePerMinuteCap('0', 30), 0);
|
||
assert.equal(
|
||
makeCostRateLimiter('0', 30),
|
||
null,
|
||
'operators who mean "no throttle" must still be able to say so',
|
||
);
|
||
});
|
||
|
||
test('a configured value wins, and a fractional one floors', () => {
|
||
assert.equal(resolvePerMinuteCap('45', 30), 45);
|
||
assert.equal(resolvePerMinuteCap(45, 30), 45);
|
||
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', () => {
|
||
// `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 —
|
||
// the opposite of what the operator was reaching for when they typed it.
|
||
for (const typo of ['3O', '60/min', 'thirty', '-5', 'NaN']) {
|
||
assert.equal(
|
||
resolvePerMinuteCap(typo, 30),
|
||
30,
|
||
`${typo} must fall back to the default, not to unlimited`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('the default cap clears the app’s own demand with room to spare', () => {
|
||
// src/hud.js HUD_SUMMARY_INTERVAL_MS = 15000 -> 4 summaries/min per open
|
||
// tab, plus one token mint per voice session. The cap has to sit well above
|
||
// that or it would throttle the app rather than an abuser.
|
||
const hudRequestsPerMinutePerTab = 60_000 / 15_000;
|
||
assert.ok(
|
||
OPENAI_DEFAULT_PER_MIN >= hudRequestsPerMinutePerTab * 4,
|
||
`default ${OPENAI_DEFAULT_PER_MIN}/min leaves too little headroom over ${hudRequestsPerMinutePerTab}/min of ordinary use`,
|
||
);
|
||
});
|
||
|
||
test('the realtime token route throttles by default, and says so in a 429', async (t) => {
|
||
env(t, 'GEV_RATELIMIT_OPENAI_PER_MIN', undefined);
|
||
env(t, 'OPENAI_API_KEY', 'fixture-upstream-secret');
|
||
let upstreamCalls = 0;
|
||
t.mock.method(globalThis, 'fetch', async () => {
|
||
upstreamCalls += 1;
|
||
return Response.json({ value: 'fixture-ephemeral' });
|
||
});
|
||
|
||
const handler = install(openAiRealtimeProxy({})).get('/api/realtime/token');
|
||
const statuses = [];
|
||
for (let i = 0; i < OPENAI_DEFAULT_PER_MIN + 1; i += 1) {
|
||
statuses.push((await request(handler)).status);
|
||
}
|
||
|
||
assert.deepEqual(
|
||
statuses.slice(0, OPENAI_DEFAULT_PER_MIN),
|
||
Array(OPENAI_DEFAULT_PER_MIN).fill(200),
|
||
'the default must not throttle ordinary use',
|
||
);
|
||
const overLimit = await request(handler);
|
||
assert.equal(overLimit.status, 429);
|
||
assert.equal(overLimit.headers['retry-after'], '5');
|
||
assert.equal(JSON.parse(overLimit.body).error, 'Rate limit exceeded');
|
||
assert.equal(
|
||
overLimit.body.includes('fixture-upstream-secret'),
|
||
false,
|
||
'a throttled reply must not leak the server-held key',
|
||
);
|
||
assert.ok(
|
||
upstreamCalls <= OPENAI_DEFAULT_PER_MIN,
|
||
'a throttled request must be refused before it reaches OpenAI, or it still costs money',
|
||
);
|
||
});
|
||
|
||
test('the HUD summary route throttles the paid path, never the free one', async (t) => {
|
||
env(t, 'OPENAI_API_KEY', 'fixture-upstream-secret');
|
||
let upstreamCalls = 0;
|
||
t.mock.method(globalThis, 'fetch', async () => {
|
||
upstreamCalls += 1;
|
||
return Response.json({ output_text: 'five word hud summary here' });
|
||
});
|
||
|
||
// Its own client address: the limiter is cached at module scope and shared
|
||
// across the OpenAI routes, so the realtime case above has already spent
|
||
// 127.0.0.1's budget inside this process.
|
||
const from = '10.9.9.9';
|
||
const statuses = [];
|
||
for (let i = 0; i < OPENAI_DEFAULT_PER_MIN; i += 1)
|
||
statuses.push(
|
||
(await request(handleHudSummary, { remoteAddress: from })).status,
|
||
);
|
||
assert.deepEqual(
|
||
statuses,
|
||
Array(OPENAI_DEFAULT_PER_MIN).fill(200),
|
||
'the default must not throttle ordinary HUD use',
|
||
);
|
||
|
||
const overLimit = await request(handleHudSummary, { remoteAddress: from });
|
||
assert.equal(overLimit.status, 429);
|
||
assert.equal(overLimit.headers['retry-after'], '5');
|
||
assert.equal(JSON.parse(overLimit.body).error, 'Rate limit exceeded');
|
||
assert.equal(
|
||
overLimit.body.includes('fixture-upstream-secret'),
|
||
false,
|
||
'a throttled reply must not leak the server-held key',
|
||
);
|
||
assert.ok(
|
||
upstreamCalls <= OPENAI_DEFAULT_PER_MIN,
|
||
'a throttled request must be refused before it reaches OpenAI, or it still costs money',
|
||
);
|
||
|
||
// The keyless HUD answer is deliberately NOT throttled: it is composed
|
||
// locally, spends nothing upstream, and capping it would only take the free
|
||
// HUD away from an unconfigured install. Same exhausted client, no key.
|
||
delete process.env.OPENAI_API_KEY;
|
||
const free = await request(handleHudSummary, { remoteAddress: from });
|
||
assert.equal(free.status, 200, 'the free path answers past a spent budget');
|
||
assert.equal(JSON.parse(free.body).configured, false);
|
||
assert.equal(
|
||
upstreamCalls <= OPENAI_DEFAULT_PER_MIN,
|
||
true,
|
||
'the free path reaches no provider',
|
||
);
|
||
});
|
||
|
||
test('the Google places route throttles by default and keeps its places[] contract', async (t) => {
|
||
env(t, 'GEV_RATELIMIT_GOOGLE_PER_MIN', undefined);
|
||
let upstreamCalls = 0;
|
||
const handler = install(
|
||
googlePlacesContextProxy({
|
||
resolveApiKey: () => 'fixture-google-server-key',
|
||
fetchImpl: async () => {
|
||
upstreamCalls += 1;
|
||
return Response.json({ places: [] });
|
||
},
|
||
}),
|
||
).get('/api/google/nearby-places');
|
||
|
||
const url = '/?lat=37.77493&lon=-122.41942';
|
||
const statuses = [];
|
||
for (let i = 0; i < GOOGLE_DEFAULT_PER_MIN; i += 1) {
|
||
statuses.push((await request(handler, { url, method: 'GET' })).status);
|
||
}
|
||
assert.ok(
|
||
statuses.every((s) => s === 200),
|
||
'the default must not throttle ordinary use',
|
||
);
|
||
|
||
const overLimit = await request(handler, { url, method: 'GET' });
|
||
assert.equal(overLimit.status, 429);
|
||
assert.equal(overLimit.headers['retry-after'], '5');
|
||
// Every error response on this route carries places[]; the client reads it
|
||
// unconditionally, so a throttled reply must not be the one shape that omits it.
|
||
assert.deepEqual(JSON.parse(overLimit.body).places, []);
|
||
assert.equal(
|
||
upstreamCalls,
|
||
GOOGLE_DEFAULT_PER_MIN,
|
||
'a throttled request must be refused before it reaches Google, or it still costs money',
|
||
);
|
||
});
|
||
|
||
test('the code defaults match the caps the Pinokio build ships', () => {
|
||
// These two numbers used to live only in pinokio/_ENVIRONMENT, and the value
|
||
// this repo suggested elsewhere had already drifted away from them. Now that
|
||
// they are the server's defaults, the packaged app and an unconfigured
|
||
// checkout must agree, or a Pinokio user's throttle silently stops matching
|
||
// the one everybody else gets. If you deliberately want them to differ, this
|
||
// assertion is the place to say so.
|
||
const shipped = readPinokioEnvironment(
|
||
path.join(
|
||
path.dirname(fileURLToPath(import.meta.url)),
|
||
'..',
|
||
'pinokio',
|
||
'_ENVIRONMENT',
|
||
),
|
||
);
|
||
assert.equal(
|
||
Number(shipped.GEV_RATELIMIT_OPENAI_PER_MIN),
|
||
OPENAI_DEFAULT_PER_MIN,
|
||
);
|
||
assert.equal(
|
||
Number(shipped.GEV_RATELIMIT_GOOGLE_PER_MIN),
|
||
GOOGLE_DEFAULT_PER_MIN,
|
||
);
|
||
});
|