theorum 0.1.7 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/esm/src/cli/commands/bench.d.ts +20 -0
  2. package/esm/src/cli/commands/bench.js +454 -0
  3. package/esm/src/cli/commands/fuzz-guardrails.d.ts +9 -0
  4. package/esm/src/cli/commands/fuzz-guardrails.js +440 -0
  5. package/esm/src/cli/index.js +20 -1
  6. package/esm/src/guardrails/injection.d.ts +0 -1
  7. package/esm/src/guardrails/injection.js +105 -12
  8. package/esm/src/guardrails/normalize.d.ts +11 -0
  9. package/esm/src/guardrails/normalize.js +110 -0
  10. package/esm/src/guardrails/sanitize.d.ts +10 -2
  11. package/esm/src/guardrails/sanitize.js +27 -1
  12. package/esm/src/guardrails/sensitive.js +5 -3
  13. package/esm/src/kernel/engine/boundary.js +10 -10
  14. package/esm/src/kernel/engine/runner/mod.js +4 -1
  15. package/esm/src/kernel/engine/runner/tools.js +11 -5
  16. package/esm/src/kernel/types.d.ts +1 -1
  17. package/esm/src/observability/trace-record.d.ts +1 -0
  18. package/esm/src/observability/trace-record.js +4 -4
  19. package/esm/src/providers/create-provider.js +2 -0
  20. package/esm/src/providers/expose-for-tests.d.ts +1 -0
  21. package/esm/src/providers/expose-for-tests.js +21 -0
  22. package/esm/src/providers/gemini-tape.js +9 -0
  23. package/esm/src/providers/google-tap.js +2 -0
  24. package/esm/src/providers/interactions.js +16 -0
  25. package/esm/src/providers/keys.js +11 -0
  26. package/esm/src/providers/openrouter-payload.d.ts +9 -2
  27. package/esm/src/providers/openrouter-payload.js +23 -7
  28. package/esm/src/providers/openrouter.js +101 -140
  29. package/esm/src/providers/pcm.js +2 -0
  30. package/esm/src/providers/provider.js +13 -0
  31. package/esm/src/providers/speech.js +10 -0
  32. package/esm/src/providers/sse.js +2 -0
  33. package/esm/src/streaming/mod.d.ts +7 -0
  34. package/esm/src/streaming/mod.js +7 -0
  35. package/esm/src/streaming/readStreamingJsonStringField.d.ts +6 -0
  36. package/esm/src/streaming/readStreamingJsonStringField.js +55 -0
  37. package/package.json +6 -3
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Synthetic performance benchmark for the THEORUM kernel pipeline.
3
+ *
4
+ * Measures overhead added by profile resolution, sanitization, canary
5
+ * binding, stream processing, and event dispatch vs. a bare provider call.
6
+ *
7
+ * Metrics:
8
+ * TTFE — time to first event (profile resolve + provider setup)
9
+ * TTFT — time to first text delta
10
+ * T/s — text tokens per second throughput
11
+ * Overhead — total wall-clock delta vs. raw provider consumption
12
+ *
13
+ * @module
14
+ */
15
+ export interface BenchOptions {
16
+ chunks?: number;
17
+ iterations?: number;
18
+ warmup?: number;
19
+ }
20
+ export declare function benchCommand(options?: BenchOptions): Promise<void>;
@@ -0,0 +1,454 @@
1
+ /**
2
+ * Synthetic performance benchmark for the THEORUM kernel pipeline.
3
+ *
4
+ * Measures overhead added by profile resolution, sanitization, canary
5
+ * binding, stream processing, and event dispatch vs. a bare provider call.
6
+ *
7
+ * Metrics:
8
+ * TTFE — time to first event (profile resolve + provider setup)
9
+ * TTFT — time to first text delta
10
+ * T/s — text tokens per second throughput
11
+ * Overhead — total wall-clock delta vs. raw provider consumption
12
+ *
13
+ * @module
14
+ */
15
+ import { sanitizeTurnRequest } from '../../guardrails/sanitize.js';
16
+ import { bindCanary, eventHasCanary, mintCanary } from '../../kernel/engine/boundary.js';
17
+ import { runTurn } from '../../kernel/engine/runner.js';
18
+ import { clearProfiles, registerProfile, } from '../../kernel/registry/profiles.js';
19
+ import { pickSystemRole, resolveTurn } from '../../kernel/registry/resolve.js';
20
+ import { buildRecord } from '../../observability/trace-record.js';
21
+ const DEFAULT_CHUNKS = 200;
22
+ const DEFAULT_ITERATIONS = 50;
23
+ const DEFAULT_WARMUP = 5;
24
+ const MS_PER_SEC = 1000;
25
+ const BENCH_PROFILE_ID = '__bench__';
26
+ function registerBenchProfile() {
27
+ registerProfile({
28
+ id: BENCH_PROFILE_ID,
29
+ identity: {
30
+ handle: 'bench',
31
+ system: 'You are a benchmark stub.',
32
+ },
33
+ model: {
34
+ protocol: 'openAi',
35
+ provider: 'openrouter',
36
+ allow: ['bench-model'],
37
+ config: {
38
+ 'bench-model': {
39
+ apiId: 'bench-model',
40
+ thinking: { on: 'none', off: 'none' },
41
+ thinkingLevels: ['none'],
42
+ summaries: { on: 'none', off: 'none' },
43
+ maxOutputTokens: 4096,
44
+ temperature: 0,
45
+ keyBuiltins: [],
46
+ },
47
+ },
48
+ thinking: 'none',
49
+ },
50
+ guardrails: {
51
+ canary: true,
52
+ sanitizeInput: true,
53
+ redactSensitive: true,
54
+ },
55
+ });
56
+ }
57
+ function generateChunks(count) {
58
+ const events = [];
59
+ for (let i = 0; i < count; i++) {
60
+ events.push({ type: 'text', text: `token_${i} ` });
61
+ }
62
+ events.push({
63
+ type: 'tokens',
64
+ tokens: { input: 10, output: count, total: 10 + count },
65
+ });
66
+ events.push({ type: 'done' });
67
+ return events;
68
+ }
69
+ function createMockProvider(chunks) {
70
+ return {
71
+ async *complete(_req) {
72
+ for (const chunk of chunks) {
73
+ yield chunk;
74
+ }
75
+ },
76
+ };
77
+ }
78
+ function buildBenchRequest() {
79
+ return {
80
+ profile: BENCH_PROFILE_ID,
81
+ input: { text: 'Benchmark prompt.' },
82
+ };
83
+ }
84
+ async function measureRawProvider(provider) {
85
+ const req = buildBenchRequest();
86
+ const start = performance.now();
87
+ let firstEvent = 0;
88
+ let firstText = 0;
89
+ let textEvents = 0;
90
+ let gotFirst = false;
91
+ let gotFirstText = false;
92
+ // Simulate what a bare consumer does — no kernel overhead
93
+ const fakeReq = {
94
+ model: 'bench-model',
95
+ apiId: 'bench-model',
96
+ thinking: 'none',
97
+ summaries: 'none',
98
+ maxOutputTokens: 4096,
99
+ temperature: 0,
100
+ builtins: [],
101
+ system: '',
102
+ input: [{ type: 'text', text: req.input?.text ?? '' }],
103
+ structured: null,
104
+ image: null,
105
+ };
106
+ for await (const event of provider.complete(fakeReq)) {
107
+ if (!gotFirst) {
108
+ firstEvent = performance.now() - start;
109
+ gotFirst = true;
110
+ }
111
+ if (!gotFirstText && event.type === 'text') {
112
+ firstText = performance.now() - start;
113
+ gotFirstText = true;
114
+ }
115
+ if (event.type === 'text') {
116
+ textEvents++;
117
+ }
118
+ }
119
+ return {
120
+ ttfe: firstEvent,
121
+ ttft: firstText,
122
+ totalMs: performance.now() - start,
123
+ textEvents,
124
+ };
125
+ }
126
+ async function measureKernelPipeline(provider) {
127
+ const req = buildBenchRequest();
128
+ const start = performance.now();
129
+ let firstEvent = 0;
130
+ let firstText = 0;
131
+ let textEvents = 0;
132
+ let gotFirst = false;
133
+ let gotFirstText = false;
134
+ for await (const event of runTurn(req, provider)) {
135
+ if (!gotFirst) {
136
+ firstEvent = performance.now() - start;
137
+ gotFirst = true;
138
+ }
139
+ if (!gotFirstText && event.type === 'text') {
140
+ firstText = performance.now() - start;
141
+ gotFirstText = true;
142
+ }
143
+ if (event.type === 'text') {
144
+ textEvents++;
145
+ }
146
+ }
147
+ return {
148
+ ttfe: firstEvent,
149
+ ttft: firstText,
150
+ totalMs: performance.now() - start,
151
+ textEvents,
152
+ };
153
+ }
154
+ function percentile(sorted, p) {
155
+ const idx = Math.ceil((p / 100) * sorted.length) - 1;
156
+ return sorted[Math.max(0, idx)];
157
+ }
158
+ function aggregate(results) {
159
+ const ttfe = results.map((r) => r.ttfe).sort((a, b) => a - b);
160
+ const ttft = results.map((r) => r.ttft).sort((a, b) => a - b);
161
+ const total = results.map((r) => r.totalMs).sort((a, b) => a - b);
162
+ const tps = results
163
+ .map((r) => (r.textEvents / r.totalMs) * MS_PER_SEC)
164
+ .sort((a, b) => a - b);
165
+ const mean = (arr) => arr.reduce((a, b) => a + b, 0) / arr.length;
166
+ return {
167
+ ttfeMs: {
168
+ median: percentile(ttfe, 50),
169
+ p95: percentile(ttfe, 95),
170
+ mean: mean(ttfe),
171
+ },
172
+ ttftMs: {
173
+ median: percentile(ttft, 50),
174
+ p95: percentile(ttft, 95),
175
+ mean: mean(ttft),
176
+ },
177
+ tokensPerSec: {
178
+ median: percentile(tps, 50),
179
+ p95: percentile(tps, 5), // lower is worse for throughput
180
+ mean: mean(tps),
181
+ },
182
+ totalMs: {
183
+ median: percentile(total, 50),
184
+ p95: percentile(total, 95),
185
+ mean: mean(total),
186
+ },
187
+ };
188
+ }
189
+ function fmtMs(ms) {
190
+ if (ms < 1) {
191
+ return `${(ms * MS_PER_SEC).toFixed(0)}µs`;
192
+ }
193
+ return `${ms.toFixed(2)}ms`;
194
+ }
195
+ function fmtRate(tps) {
196
+ if (tps >= MS_PER_SEC * MS_PER_SEC) {
197
+ return `${(tps / (MS_PER_SEC * MS_PER_SEC)).toFixed(1)}M/s`;
198
+ }
199
+ if (tps >= MS_PER_SEC) {
200
+ return `${(tps / MS_PER_SEC).toFixed(1)}K/s`;
201
+ }
202
+ return `${tps.toFixed(0)}/s`;
203
+ }
204
+ function printMetricRow(label, raw, fmt) {
205
+ console.log(` ${label.padEnd(14)} median ${fmt(raw.median).padStart(10)} p95 ${fmt(raw.p95).padStart(10)} mean ${fmt(raw.mean).padStart(10)}`);
206
+ }
207
+ function printResults(label, metrics) {
208
+ console.log(`\n${label}`);
209
+ console.log('─'.repeat(72));
210
+ printMetricRow('TTFE', metrics.ttfeMs, fmtMs);
211
+ printMetricRow('TTFT', metrics.ttftMs, fmtMs);
212
+ printMetricRow('Throughput', metrics.tokensPerSec, fmtRate);
213
+ printMetricRow('Total', metrics.totalMs, fmtMs);
214
+ }
215
+ function printOverhead(raw, kernel) {
216
+ console.log('\nOverhead (kernel − raw)');
217
+ console.log('─'.repeat(72));
218
+ const ttfeDelta = kernel.ttfeMs.median - raw.ttfeMs.median;
219
+ const ttftDelta = kernel.ttftMs.median - raw.ttftMs.median;
220
+ const totalDelta = kernel.totalMs.median - raw.totalMs.median;
221
+ const tpsDelta = kernel.tokensPerSec.median - raw.tokensPerSec.median;
222
+ console.log(` TTFE ${ttfeDelta >= 0 ? '+' : ''}${fmtMs(ttfeDelta)}`);
223
+ console.log(` TTFT ${ttftDelta >= 0 ? '+' : ''}${fmtMs(ttftDelta)}`);
224
+ console.log(` Throughput ${tpsDelta >= 0 ? '+' : ''}${fmtRate(tpsDelta)}`);
225
+ console.log(` Total ${totalDelta >= 0 ? '+' : ''}${fmtMs(totalDelta)}`);
226
+ if (raw.totalMs.median > 0) {
227
+ const pct = ((totalDelta / raw.totalMs.median) * 100).toFixed(1);
228
+ console.log(` Relative ${totalDelta >= 0 ? '+' : ''}${pct}%`);
229
+ }
230
+ }
231
+ function measureSetupPhases() {
232
+ const req = buildBenchRequest();
233
+ const t0 = performance.now();
234
+ const safe = sanitizeTurnRequest(req);
235
+ const t1 = performance.now();
236
+ const { profile, generation } = resolveTurn(safe);
237
+ const t2 = performance.now();
238
+ pickSystemRole(profile, safe.input?.role);
239
+ const sys = profile.identity.system ?? '';
240
+ const t3 = performance.now();
241
+ bindCanary(sys, generation.canary);
242
+ const t4 = performance.now();
243
+ return {
244
+ sanitizeMs: t1 - t0,
245
+ resolveMs: t2 - t1,
246
+ systemMs: t3 - t2,
247
+ canaryMs: t4 - t3,
248
+ totalSetupMs: t4 - t0,
249
+ };
250
+ }
251
+ function printPhaseBreakdown(iterations) {
252
+ const results = [];
253
+ for (let i = 0; i < iterations; i++) {
254
+ results.push(measureSetupPhases());
255
+ }
256
+ const median = (arr) => {
257
+ const s = arr.slice().sort((a, b) => a - b);
258
+ return percentile(s, 50);
259
+ };
260
+ const sanitize = median(results.map((r) => r.sanitizeMs));
261
+ const resolve = median(results.map((r) => r.resolveMs));
262
+ const system = median(results.map((r) => r.systemMs));
263
+ const canary = median(results.map((r) => r.canaryMs));
264
+ const total = median(results.map((r) => r.totalSetupMs));
265
+ console.log('\nSetup Phase Breakdown (median)');
266
+ console.log('─'.repeat(72));
267
+ console.log(` sanitizeTurnRequest ${fmtMs(sanitize).padStart(10)}`);
268
+ console.log(` resolveTurn ${fmtMs(resolve).padStart(10)}`);
269
+ console.log(` pickSystemRole ${fmtMs(system).padStart(10)}`);
270
+ console.log(` bindCanary ${fmtMs(canary).padStart(10)}`);
271
+ console.log(` ─────────────────────────────`);
272
+ console.log(` Total setup ${fmtMs(total).padStart(10)}`);
273
+ }
274
+ function microCanaryCheck(chunks, iterations) {
275
+ const canary = mintCanary();
276
+ const total = chunks.length * iterations;
277
+ const start = performance.now();
278
+ for (let i = 0; i < iterations; i++) {
279
+ for (const chunk of chunks) {
280
+ eventHasCanary(chunk, canary);
281
+ }
282
+ }
283
+ const elapsed = performance.now() - start;
284
+ return {
285
+ label: 'eventHasCanary',
286
+ totalMs: elapsed,
287
+ perEventNs: (elapsed / total) * 1e6,
288
+ };
289
+ }
290
+ function microArrayPush(chunks, iterations) {
291
+ const total = chunks.length * iterations;
292
+ const start = performance.now();
293
+ for (let i = 0; i < iterations; i++) {
294
+ const seen = [];
295
+ const allEmitted = [];
296
+ const attempt = [];
297
+ for (const chunk of chunks) {
298
+ seen.push(chunk);
299
+ allEmitted.push(chunk);
300
+ attempt.push(chunk);
301
+ }
302
+ }
303
+ const elapsed = performance.now() - start;
304
+ return {
305
+ label: 'Array.push ×3',
306
+ totalMs: elapsed,
307
+ perEventNs: (elapsed / total) * 1e6,
308
+ };
309
+ }
310
+ async function microAsyncGenOverhead(chunks, iterations) {
311
+ async function* layer1(events) {
312
+ for (const e of events)
313
+ yield e;
314
+ }
315
+ async function* layer2(events) {
316
+ for await (const e of layer1(events))
317
+ yield e;
318
+ }
319
+ async function* layer3(events) {
320
+ for await (const e of layer2(events))
321
+ yield e;
322
+ }
323
+ async function* layer4(events) {
324
+ for await (const e of layer3(events))
325
+ yield e;
326
+ }
327
+ const total = chunks.length * iterations;
328
+ const start = performance.now();
329
+ for (let i = 0; i < iterations; i++) {
330
+ for await (const _e of layer4(chunks)) {
331
+ // consume
332
+ }
333
+ }
334
+ const elapsed = performance.now() - start;
335
+ return {
336
+ label: 'AsyncGen ×4 layers',
337
+ totalMs: elapsed,
338
+ perEventNs: (elapsed / total) * 1e6,
339
+ };
340
+ }
341
+ function microAbortCheck(chunks, iterations) {
342
+ const controller = new AbortController();
343
+ const { signal } = controller;
344
+ const total = chunks.length * iterations;
345
+ const start = performance.now();
346
+ for (let i = 0; i < iterations; i++) {
347
+ for (const _chunk of chunks) {
348
+ if (signal.aborted)
349
+ throw signal.reason;
350
+ }
351
+ }
352
+ const elapsed = performance.now() - start;
353
+ return {
354
+ label: 'signal.aborted check',
355
+ totalMs: elapsed,
356
+ perEventNs: (elapsed / total) * 1e6,
357
+ };
358
+ }
359
+ async function microTraceRecord(chunks, iterations) {
360
+ const req = buildBenchRequest();
361
+ const start = performance.now();
362
+ for (let i = 0; i < iterations; i++) {
363
+ await buildRecord({
364
+ req,
365
+ events: chunks,
366
+ started: Date.now(),
367
+ model: 'bench-model',
368
+ });
369
+ }
370
+ const elapsed = performance.now() - start;
371
+ return {
372
+ label: 'buildRecord (trace)',
373
+ totalMs: elapsed,
374
+ perEventNs: (elapsed / iterations) * 1e6,
375
+ };
376
+ }
377
+ function microSanitizeScaling() {
378
+ const sizes = [50, 200, 1000, 5000];
379
+ console.log('\nSanitize Scaling (input text length)');
380
+ console.log('─'.repeat(72));
381
+ for (const size of sizes) {
382
+ const text = 'A'.repeat(size);
383
+ const req = {
384
+ profile: BENCH_PROFILE_ID,
385
+ input: { text },
386
+ };
387
+ const runs = 500;
388
+ const start = performance.now();
389
+ for (let i = 0; i < runs; i++) {
390
+ sanitizeTurnRequest(req);
391
+ }
392
+ const elapsed = performance.now() - start;
393
+ const perCall = elapsed / runs;
394
+ console.log(` ${String(size).padStart(5)} chars ${fmtMs(perCall).padStart(10)}/call`);
395
+ }
396
+ }
397
+ function fmtNs(ns) {
398
+ if (ns < 1000) {
399
+ return `${ns.toFixed(0)}ns`;
400
+ }
401
+ return `${(ns / 1000).toFixed(1)}µs`;
402
+ }
403
+ async function printMicroBenchmarks(chunks, iterations) {
404
+ const results = [];
405
+ results.push(microCanaryCheck(chunks, iterations));
406
+ results.push(microArrayPush(chunks, iterations));
407
+ results.push(await microAsyncGenOverhead(chunks, iterations));
408
+ results.push(microAbortCheck(chunks, iterations));
409
+ results.push(await microTraceRecord(chunks, Math.min(iterations, 20)));
410
+ console.log('\nPer-Event Micro-Benchmarks');
411
+ console.log('─'.repeat(72));
412
+ for (const r of results) {
413
+ const perEvent = r.label === 'buildRecord (trace)'
414
+ ? `${fmtMs(r.totalMs / Math.min(iterations, 20)).padStart(10)}/turn`
415
+ : `${fmtNs(r.perEventNs).padStart(8)}/event`;
416
+ console.log(` ${r.label.padEnd(24)} ${perEvent} (${fmtMs(r.totalMs)} total)`);
417
+ }
418
+ microSanitizeScaling();
419
+ }
420
+ export async function benchCommand(options = {}) {
421
+ const chunkCount = options.chunks ?? DEFAULT_CHUNKS;
422
+ const iterations = options.iterations ?? DEFAULT_ITERATIONS;
423
+ const warmup = options.warmup ?? DEFAULT_WARMUP;
424
+ console.log(`\n⏱ Theorum Kernel Benchmark`);
425
+ console.log(` ${chunkCount} chunks × ${iterations} iterations (${warmup} warmup)\n`);
426
+ const chunks = generateChunks(chunkCount);
427
+ const provider = createMockProvider(chunks);
428
+ registerBenchProfile();
429
+ // Warmup
430
+ for (let i = 0; i < warmup; i++) {
431
+ await measureRawProvider(provider);
432
+ await measureKernelPipeline(provider);
433
+ }
434
+ // Collect raw baseline
435
+ const rawResults = [];
436
+ for (let i = 0; i < iterations; i++) {
437
+ rawResults.push(await measureRawProvider(provider));
438
+ }
439
+ // Collect kernel pipeline
440
+ const kernelResults = [];
441
+ for (let i = 0; i < iterations; i++) {
442
+ kernelResults.push(await measureKernelPipeline(provider));
443
+ }
444
+ const rawMetrics = aggregate(rawResults);
445
+ const kernelMetrics = aggregate(kernelResults);
446
+ printResults('Raw Provider (baseline)', rawMetrics);
447
+ printResults('Kernel Pipeline (runTurn)', kernelMetrics);
448
+ printOverhead(rawMetrics, kernelMetrics);
449
+ printPhaseBreakdown(iterations);
450
+ await printMicroBenchmarks(chunks, iterations);
451
+ // Cleanup
452
+ clearProfiles();
453
+ console.log('\n');
454
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Adversarial guardrail fuzzer.
3
+ *
4
+ * Throws injection payloads at every sanitization ingress and reports
5
+ * what gets through unchanged — meaning the guardrails missed it.
6
+ *
7
+ * @module
8
+ */
9
+ export declare function fuzzGuardrailsCommand(): void;