Freeway: self-hosted OpenAI-compatible gateway for free-tier providers

One endpoint in front of every free LLM tier, with automatic failover and no
provider key ever leaving the box.

Providers are data, not code: adding one is a JSON file plus an env var, never
a TypeScript change. Ships 12 providers pre-written; `verify-provider --apply`
derives the live model list from your own key.

- core: registry, bucketed quota meter (rolling + UTC-calendar windows),
  key-pool store, 6-step model router with diagnostic rejection reasons
- gateway: node:http, failover that commits to a stream only after 200 headers,
  SSE passthrough with usage sniffing, virtual keys with admin/inference roles
- context engine: sqlite sessions and a 5-rung refit ladder, so a 128k thread
  keeps working when it fails over to an 8k model
- cache: exact (sha256, safe-by-default) + opt-in semantic (flat Float32Array)
- guard: inbound limits, quota forecasting, limit calibration from upstream
  headers, key health, secret/PII/injection scanning
- dashboard: one self-contained HTML file, live SSE request log, token usage
- sdk: zero-dep typed client, drop-in for the OpenAI SDK

Zero runtime dependencies. 326 tests, 27-check smoke suite against a scripted
mock upstream so routing is testable without spending quota.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Aryan SinghandClaude Opus 5 committed 2026-08-16 22:41:49 -05:00
commit cb985dcf9c
86 files changed
+16350

No files matched your search

+92
View File
@@ -0,0 +1,92 @@
/**
* A pass-through Transform that watches an SSE stream go by and picks the
* `usage` block out of it.
*
* Providers only report token usage in the final chunk of a stream, so this is
* the only place the real numbers can be captured. Bytes are forwarded
* unmodified — the client receives exactly what the provider sent.
*/
import { Transform, type TransformCallback } from 'node:stream';
export interface StreamUsage {
prompt_tokens?: number;
completion_tokens?: number;
total_tokens?: number;
}
/** Guard against a provider that never sends a newline filling memory. */
const MAX_PARTIAL_LINE = 256 * 1024;
export class UsageSniffer extends Transform {
usage: StreamUsage | null = null;
/** Accumulated assistant text, for estimating when no usage is reported. */
text = '';
private partial = '';
override _transform(chunk: Buffer | string, _encoding: BufferEncoding, callback: TransformCallback): void {
// Forward first and unchanged: inspection must never alter what the client
// receives, and must never delay it either.
this.push(chunk);
this.partial += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
if (this.partial.length > MAX_PARTIAL_LINE) {
// Not a real SSE stream, or a pathologically long line. Stop accumulating
// rather than growing without bound; pass-through is unaffected.
this.partial = '';
callback();
return;
}
const lines = this.partial.split('\n');
this.partial = lines.pop() ?? '';
for (const line of lines) this.inspect(line);
callback();
}
override _flush(callback: TransformCallback): void {
if (this.partial) this.inspect(this.partial);
this.partial = '';
callback();
}
private inspect(line: string): void {
const trimmed = line.trim();
if (!trimmed.startsWith('data:')) return;
const payload = trimmed.slice(5).trim();
if (!payload || payload === '[DONE]') return;
// Parsing every chunk would be wasteful; only chunks that could carry the
// fields we want are worth the JSON.parse.
const hasUsage = payload.includes('"usage"');
const hasContent = payload.includes('"content"');
if (!hasUsage && !hasContent) return;
let parsed: unknown;
try {
parsed = JSON.parse(payload);
} catch {
return;
}
if (typeof parsed !== 'object' || parsed === null) return;
const usage = (parsed as { usage?: unknown }).usage;
if (typeof usage === 'object' && usage !== null) {
const u = usage as Record<string, unknown>;
this.usage = {
...(typeof u['prompt_tokens'] === 'number' ? { prompt_tokens: u['prompt_tokens'] } : {}),
...(typeof u['completion_tokens'] === 'number' ? { completion_tokens: u['completion_tokens'] } : {}),
...(typeof u['total_tokens'] === 'number' ? { total_tokens: u['total_tokens'] } : {}),
};
}
const choices = (parsed as { choices?: unknown }).choices;
if (Array.isArray(choices)) {
for (const choice of choices) {
const delta = (choice as { delta?: { content?: unknown } } | null)?.delta;
if (typeof delta?.content === 'string') this.text += delta.content;
}
}
}
}