Adding unlimited key

This commit is contained in:
2026-07-21 15:07:08 -05:00
parent b9b183df40
commit fe638a14d4
4 changed files with 97 additions and 15 deletions
+4
View File
@@ -4,6 +4,10 @@ DATABASE_URL=postgres://user:password@localhost:5432/opencode_proxy
# Optional: use this instead of keys.txt. Must be a JSON array of strings.
# OPENCODE_API_KEYS=["go-first-key","go-second-key"]
# Optional: client keys that bypass proxy rate limits. Must be a JSON array of strings.
# Send one in Authorization: Bearer <key> or x-api-key.
# UNLIMITED_API_KEYS=["personal-unlimited-key"]
# Optional server settings
PORT=4005
MAX_CONTEXT_TOKENS=262144
+6
View File
@@ -37,6 +37,12 @@ The global context limit defaults to 256k tokens and can be changed with `MAX_CO
Inference requests are limited per client IP to 2 requests per second, 100 requests per five hours, and $10 of reported upstream cost per five hours. Railway's `X-Real-IP` header is used to identify clients.
To exempt trusted clients from those proxy limits, set `UNLIMITED_API_KEYS` to a JSON array and have the client send a configured key using `Authorization: Bearer <key>` or `x-api-key`. These keys only bypass this proxy's rate and spend limits; they do not bypass upstream OpenCode Go limits.
```sh
UNLIMITED_API_KEYS='["personal-unlimited-key"]'
```
Set `DATABASE_URL` in `.env` (or the process environment) to a Postgres connection string. The proxy creates its `requests` table and index automatically on startup. Only requests to the OpenAI Chat Completions and Responses endpoints and the Anthropic Messages endpoints are stored. Their complete responses—including streaming responses and errors—are stored with headers, status, model, client IP, and timestamp. On startup, stored requests for all other endpoints are deleted. Successful chat completion requests also store a normalized training conversation containing the full chat history and generated assistant output, including reasoning, tool calls, and tool results.
```sh
+48 -14
View File
@@ -123,7 +123,32 @@ function withContextLimit(payload, maxContextTokens) {
};
}
export function createProxyApp({ keys, fetchImpl = globalThis.fetch, upstreamBaseUrl = DEFAULT_UPSTREAM_BASE_URL, maxContextTokens = maxContextFromEnv(), rateLimiter = createRateLimiter(), pagesDirectory = defaultPagesDirectory, requestLogger } = {}) {
export function unlimitedKeysFromEnv(value = process.env.UNLIMITED_API_KEYS) {
if (value === undefined || value.trim() === '') return new Set();
let keys;
try {
keys = JSON.parse(value);
} catch (error) {
throw new Error(`UNLIMITED_API_KEYS must be valid JSON: ${error.message}`);
}
if (!Array.isArray(keys) || keys.some((key) => typeof key !== 'string' || key.trim() === '')) {
throw new Error('UNLIMITED_API_KEYS must be a JSON array of non-empty strings');
}
return new Set(keys);
}
function requestApiKey(request) {
const authorization = request.get('authorization');
const bearer = authorization?.match(/^Bearer\s+(.+)$/i)?.[1]?.trim();
return bearer || request.get('x-api-key');
}
function hasUnlimitedKey(request, unlimitedKeys) {
return unlimitedKeys.has(requestApiKey(request));
}
export function createProxyApp({ keys, fetchImpl = globalThis.fetch, upstreamBaseUrl = DEFAULT_UPSTREAM_BASE_URL, maxContextTokens = maxContextFromEnv(), rateLimiter = createRateLimiter(), unlimitedKeys = unlimitedKeysFromEnv(), pagesDirectory = defaultPagesDirectory, requestLogger } = {}) {
const rotator = createKeyRotator(keys);
const app = express();
if (requestLogger) app.post(CHAT_COMPLETION_ENDPOINTS, collectRequestData(requestLogger));
@@ -190,8 +215,11 @@ export function createProxyApp({ keys, fetchImpl = globalThis.fetch, upstreamBas
try {
validateContext(request.body, maxContextTokens);
const ip = clientIp(request);
const limit = rateLimiter.check(ip);
if (!limit.allowed) return rateLimitError(response, false, limit);
const unlimited = hasUnlimitedKey(request, unlimitedKeys);
if (!unlimited) {
const limit = rateLimiter.check(ip);
if (!limit.allowed) return rateLimitError(response, false, limit);
}
const upstream = await fetchWithKeyRetries(`${upstreamBaseUrl}/chat/completions`, {
method: 'POST',
headers: {
@@ -219,21 +247,21 @@ export function createProxyApp({ keys, fetchImpl = globalThis.fetch, upstreamBas
if (!recordedCost && data && data !== '[DONE]') {
try {
const cost = numericCost(JSON.parse(data));
if (cost !== null) { rateLimiter.recordCost(ip, cost); recordedCost = true; }
if (cost !== null) { if (!unlimited) rateLimiter.recordCost(ip, cost); recordedCost = true; }
} catch { /* ignore malformed stream events */ }
}
}
}
if (!recordedCost) {
const data = streamBuffer.split('\n').find((line) => line.startsWith('data:'))?.slice(5).trim();
try { const cost = numericCost(JSON.parse(data)); if (cost !== null) rateLimiter.recordCost(ip, cost); } catch { /* ignore */ }
try { const cost = numericCost(JSON.parse(data)); if (cost !== null && !unlimited) rateLimiter.recordCost(ip, cost); } catch { /* ignore */ }
}
response.end();
return;
}
const body = await upstream.text();
try { rateLimiter.recordCost(ip, numericCost(JSON.parse(body))); } catch { /* non-JSON upstream response */ }
try { if (!unlimited) rateLimiter.recordCost(ip, numericCost(JSON.parse(body))); } catch { /* non-JSON upstream response */ }
response.status(upstream.status).send(body);
} catch (error) {
if (!response.headersSent) {
@@ -255,8 +283,11 @@ export function createProxyApp({ keys, fetchImpl = globalThis.fetch, upstreamBas
try {
const ip = clientIp(request);
const limit = rateLimiter.check(ip);
if (!limit.allowed) return rateLimitError(response, false, limit);
const unlimited = hasUnlimitedKey(request, unlimitedKeys);
if (!unlimited) {
const limit = rateLimiter.check(ip);
if (!limit.allowed) return rateLimitError(response, false, limit);
}
const upstream = await fetchWithKeyRetries(`${upstreamBaseUrl}/chat/completions`, {
method: 'POST',
headers: { 'content-type': 'application/json', accept: translated.stream ? 'text/event-stream' : 'application/json' },
@@ -268,7 +299,7 @@ export function createProxyApp({ keys, fetchImpl = globalThis.fetch, upstreamBas
if (!translated.stream) {
const body = await upstream.text();
const payload = JSON.parse(body);
rateLimiter.recordCost(ip, numericCost(payload));
if (!unlimited) rateLimiter.recordCost(ip, numericCost(payload));
return response.type('application/json').send(JSON.stringify(translateResponsesResponse(payload, translated.model, responseId)));
}
if (!upstream.body) return response.status(502).json({ error: { type: 'upstream_error', message: 'Upstream returned no streaming body' } });
@@ -287,7 +318,7 @@ export function createProxyApp({ keys, fetchImpl = globalThis.fetch, upstreamBas
try {
const payload = JSON.parse(data);
const cost = numericCost(payload);
if (!recordedCost && cost !== null) { rateLimiter.recordCost(ip, cost); recordedCost = true; }
if (!recordedCost && cost !== null) { if (!unlimited) rateLimiter.recordCost(ip, cost); recordedCost = true; }
response.write(translateResponsesChunk(payload, state));
} catch { /* ignore malformed upstream events */ }
}
@@ -320,8 +351,11 @@ export function createProxyApp({ keys, fetchImpl = globalThis.fetch, upstreamBas
try {
validateContext(translated, maxContextTokens);
const ip = clientIp(request);
const limit = rateLimiter.check(ip);
if (!limit.allowed) return rateLimitError(response, true, limit);
const unlimited = hasUnlimitedKey(request, unlimitedKeys);
if (!unlimited) {
const limit = rateLimiter.check(ip);
if (!limit.allowed) return rateLimitError(response, true, limit);
}
const upstream = await fetchWithKeyRetries(`${upstreamBaseUrl}/chat/completions`, {
method: 'POST',
headers: { 'content-type': 'application/json', accept: translated.stream ? 'text/event-stream' : 'application/json' },
@@ -330,7 +364,7 @@ export function createProxyApp({ keys, fetchImpl = globalThis.fetch, upstreamBas
if (!upstream.ok) return forwardError(upstream, response);
if (!translated.stream) {
const payload = JSON.parse(await upstream.text());
rateLimiter.recordCost(ip, numericCost(payload));
if (!unlimited) rateLimiter.recordCost(ip, numericCost(payload));
return response.type('application/json').send(JSON.stringify(translateAnthropicResponse(payload, translated.model)));
}
if (!upstream.body) return response.status(502).json({ type: 'error', error: { type: 'upstream_error', message: 'Upstream returned no streaming body' } });
@@ -349,7 +383,7 @@ export function createProxyApp({ keys, fetchImpl = globalThis.fetch, upstreamBas
try {
const payload = JSON.parse(data);
const cost = numericCost(payload);
if (!recordedCost && cost !== null) { rateLimiter.recordCost(ip, cost); recordedCost = true; }
if (!recordedCost && cost !== null) { if (!unlimited) rateLimiter.recordCost(ip, cost); recordedCost = true; }
response.write(translateOpenAIChunk(payload, state));
} catch { /* ignore malformed upstream event */ }
}
+39 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from 'vitest';
import request from 'supertest';
import { createProxyApp } from '../src/proxy.js';
import { createProxyApp, unlimitedKeysFromEnv } from '../src/proxy.js';
const response = (body, options = {}) => new Response(body, {
status: options.status ?? 200,
@@ -69,6 +69,44 @@ describe('proxy', () => {
await request(app).post('/oai/v1/chat/completions').set('X-Real-IP', '198.51.100.1').send({ model: 'm', max_tokens: 1 }).expect(429);
});
it('bypasses rate checks and spend accounting for configured unlimited keys', async () => {
const fetchImpl = vi.fn().mockResolvedValue(response(JSON.stringify({ id: 'c', cost: 1 })));
const rateLimiter = { check: vi.fn(() => ({ allowed: false, retryAfter: 1, reason: 'rate' })), recordCost: vi.fn() };
const app = createProxyApp({ keys: ['key'], fetchImpl, rateLimiter, unlimitedKeys: new Set(['unlimited-key']) });
await request(app)
.post('/oai/v1/chat/completions')
.set('Authorization', 'Bearer unlimited-key')
.send({ model: 'm' })
.expect(200);
expect(rateLimiter.check).not.toHaveBeenCalled();
expect(rateLimiter.recordCost).not.toHaveBeenCalled();
});
it('recognizes x-api-key unlimited keys for Anthropic requests', async () => {
const fetchImpl = vi.fn().mockResolvedValue(response(JSON.stringify({
id: 'c', model: 'm', choices: [{ message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }], cost: 1,
})));
const rateLimiter = { check: vi.fn(() => ({ allowed: false, retryAfter: 1, reason: 'rate' })), recordCost: vi.fn() };
const app = createProxyApp({ keys: ['key'], fetchImpl, rateLimiter, unlimitedKeys: new Set(['unlimited-key']) });
await request(app)
.post('/ant/v1/messages')
.set('x-api-key', 'unlimited-key')
.send({ model: 'm', max_tokens: 10, messages: [{ role: 'user', content: 'Hi' }] })
.expect(200);
expect(rateLimiter.check).not.toHaveBeenCalled();
expect(rateLimiter.recordCost).not.toHaveBeenCalled();
});
it('validates the unlimited API key environment variable', () => {
expect(unlimitedKeysFromEnv('["key-a", "key-b"]')).toEqual(new Set(['key-a', 'key-b']));
expect(() => unlimitedKeysFromEnv('key-a')).toThrow('UNLIMITED_API_KEYS must be valid JSON');
expect(() => unlimitedKeysFromEnv('[""]')).toThrow('UNLIMITED_API_KEYS must be a JSON array of non-empty strings');
});
it('adds the global context limit to model listings', async () => {
const fetchImpl = vi.fn().mockResolvedValue(response('{"data":[{"id":"m"}]}'));
const app = createProxyApp({ keys: ['key'], fetchImpl, maxContextTokens: 123 });