From fe638a14d4c7a19b6a9610fe8b2393bb52c90c86 Mon Sep 17 00:00:00 2001 From: Owen Qwen Date: Tue, 21 Jul 2026 15:07:08 -0500 Subject: [PATCH] Adding unlimited key --- .env.example | 4 +++ README.md | 6 +++++ src/proxy.js | 62 +++++++++++++++++++++++++++++++++++----------- test/proxy.test.js | 40 +++++++++++++++++++++++++++++- 4 files changed, 97 insertions(+), 15 deletions(-) diff --git a/.env.example b/.env.example index 20282d7..8cd14cd 100644 --- a/.env.example +++ b/.env.example @@ -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 or x-api-key. +# UNLIMITED_API_KEYS=["personal-unlimited-key"] + # Optional server settings PORT=4005 MAX_CONTEXT_TOKENS=262144 diff --git a/README.md b/README.md index 1ad374b..cfb9fb0 100644 --- a/README.md +++ b/README.md @@ -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 ` 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 diff --git a/src/proxy.js b/src/proxy.js index e631ab3..6f34b1a 100644 --- a/src/proxy.js +++ b/src/proxy.js @@ -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 */ } } diff --git a/test/proxy.test.js b/test/proxy.test.js index c7e168c..5217f3e 100644 --- a/test/proxy.test.js +++ b/test/proxy.test.js @@ -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 });