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>
93 lines
3.1 KiB
TypeScript
93 lines
3.1 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|
|
}
|
|
}
|
|
}
|