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.
- package/esm/src/cli/commands/bench.d.ts +20 -0
- package/esm/src/cli/commands/bench.js +454 -0
- package/esm/src/cli/commands/fuzz-guardrails.d.ts +9 -0
- package/esm/src/cli/commands/fuzz-guardrails.js +440 -0
- package/esm/src/cli/index.js +20 -1
- package/esm/src/guardrails/injection.d.ts +0 -1
- package/esm/src/guardrails/injection.js +105 -12
- package/esm/src/guardrails/normalize.d.ts +11 -0
- package/esm/src/guardrails/normalize.js +110 -0
- package/esm/src/guardrails/sanitize.d.ts +10 -2
- package/esm/src/guardrails/sanitize.js +27 -1
- package/esm/src/guardrails/sensitive.js +5 -3
- package/esm/src/kernel/engine/boundary.js +10 -10
- package/esm/src/kernel/engine/runner/mod.js +4 -1
- package/esm/src/kernel/engine/runner/tools.js +11 -5
- package/esm/src/kernel/types.d.ts +1 -1
- package/esm/src/observability/trace-record.d.ts +1 -0
- package/esm/src/observability/trace-record.js +4 -4
- package/esm/src/providers/create-provider.js +2 -0
- package/esm/src/providers/expose-for-tests.d.ts +1 -0
- package/esm/src/providers/expose-for-tests.js +21 -0
- package/esm/src/providers/gemini-tape.js +9 -0
- package/esm/src/providers/google-tap.js +2 -0
- package/esm/src/providers/interactions.js +16 -0
- package/esm/src/providers/keys.js +11 -0
- package/esm/src/providers/openrouter-payload.d.ts +9 -2
- package/esm/src/providers/openrouter-payload.js +23 -7
- package/esm/src/providers/openrouter.js +101 -140
- package/esm/src/providers/pcm.js +2 -0
- package/esm/src/providers/provider.js +13 -0
- package/esm/src/providers/speech.js +10 -0
- package/esm/src/providers/sse.js +2 -0
- package/esm/src/streaming/mod.d.ts +7 -0
- package/esm/src/streaming/mod.js +7 -0
- package/esm/src/streaming/readStreamingJsonStringField.d.ts +6 -0
- package/esm/src/streaming/readStreamingJsonStringField.js +55 -0
- package/package.json +6 -3
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
import { isAbortError, TheorumError, UPSTREAM_FAILED } from '../guardrails/error.js';
|
|
10
|
+
import { exposeForTests } from './expose-for-tests.js';
|
|
10
11
|
const ATTEMPTS = 3;
|
|
11
12
|
const LAST_ATTEMPT = ATTEMPTS - 1;
|
|
12
13
|
const BACKOFF_FIRST_MS = 1000;
|
|
@@ -137,3 +138,13 @@ async function fetchGemini(url, init, bucket, transport) {
|
|
|
137
138
|
return last;
|
|
138
139
|
}
|
|
139
140
|
export { fetchGemini, withGeminiKey };
|
|
141
|
+
exposeForTests('keys', {
|
|
142
|
+
waitDefault,
|
|
143
|
+
isQuota,
|
|
144
|
+
isTransientHttp,
|
|
145
|
+
isTransientThrown,
|
|
146
|
+
requireKey,
|
|
147
|
+
backoffMs,
|
|
148
|
+
canOverflow,
|
|
149
|
+
withApiKey,
|
|
150
|
+
});
|
|
@@ -28,5 +28,12 @@ interface OpenRouterWireIds {
|
|
|
28
28
|
declare function resolveOpenRouterModel(modelId: ModelId | string, customMap?: Record<string, string>, wire?: OpenRouterWireIds): string;
|
|
29
29
|
/** Convert a provider-neutral request into an OpenRouter chat completion payload. */
|
|
30
30
|
declare function toOpenRouterPayload(req: ProviderCompleteRequest, config: OpenRouterConfig): Record<string, unknown>;
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
interface ResolvedPlugins {
|
|
32
|
+
plugins: Array<{
|
|
33
|
+
id: string;
|
|
34
|
+
}>;
|
|
35
|
+
webSearch: boolean;
|
|
36
|
+
}
|
|
37
|
+
declare function resolveOpenRouterPlugins(builtins: readonly string[]): ResolvedPlugins;
|
|
38
|
+
export type { OpenRouterConfig, OpenRouterWireIds, ResolvedPlugins };
|
|
39
|
+
export { resolveOpenRouterModel, resolveOpenRouterPlugins, toOpenRouterPayload };
|
|
@@ -167,13 +167,29 @@ function toOpenRouterPayload(req, config) {
|
|
|
167
167
|
if (tools.length > 0) {
|
|
168
168
|
payload.tools = tools;
|
|
169
169
|
}
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
.
|
|
173
|
-
|
|
174
|
-
if (plugins.length > 0) {
|
|
175
|
-
payload.plugins = plugins;
|
|
170
|
+
const resolved = resolveOpenRouterPlugins(req.builtins);
|
|
171
|
+
if (resolved.webSearch) {
|
|
172
|
+
payload.web_search_options = {};
|
|
173
|
+
}
|
|
174
|
+
if (resolved.plugins.length > 0) {
|
|
175
|
+
payload.plugins = resolved.plugins;
|
|
176
176
|
}
|
|
177
177
|
return payload;
|
|
178
178
|
}
|
|
179
|
-
|
|
179
|
+
function resolveOpenRouterPlugins(builtins) {
|
|
180
|
+
let webSearch = false;
|
|
181
|
+
const plugins = [];
|
|
182
|
+
for (const id of builtins) {
|
|
183
|
+
const pluginId = getTool(id)?.openRouterPlugin;
|
|
184
|
+
if (!pluginId)
|
|
185
|
+
continue;
|
|
186
|
+
if (pluginId === 'web') {
|
|
187
|
+
webSearch = true;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
plugins.push({ id: pluginId });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return { plugins, webSearch };
|
|
194
|
+
}
|
|
195
|
+
export { resolveOpenRouterModel, resolveOpenRouterPlugins, toOpenRouterPayload };
|
|
@@ -11,9 +11,9 @@ import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
|
|
11
11
|
import { jsonSchema, streamText, tool, } from 'ai';
|
|
12
12
|
import { isAbortError, toErrorEvent } from '../guardrails/error.js';
|
|
13
13
|
import { tryStructured } from '../kernel/engine/delta.js';
|
|
14
|
-
import {
|
|
15
|
-
import { resolveOpenRouterModel, toOpenRouterPayload, } from './openrouter-payload.js';
|
|
16
|
-
import {
|
|
14
|
+
import { getStructured } from '../kernel/registry/schemas.js';
|
|
15
|
+
import { resolveOpenRouterModel, resolveOpenRouterPlugins, toOpenRouterPayload, } from './openrouter-payload.js';
|
|
16
|
+
import { exposeForTests } from './expose-for-tests.js';
|
|
17
17
|
function trimApiKey(explicitKey) {
|
|
18
18
|
if (explicitKey?.trim()) {
|
|
19
19
|
return explicitKey.trim();
|
|
@@ -26,7 +26,6 @@ function createAccumulator() {
|
|
|
26
26
|
evidenceSeen: false,
|
|
27
27
|
emittedTokens: false,
|
|
28
28
|
errored: false,
|
|
29
|
-
toolInputs: new Map(),
|
|
30
29
|
};
|
|
31
30
|
}
|
|
32
31
|
function mediaPart(part) {
|
|
@@ -61,9 +60,6 @@ function parseToolInput(raw) {
|
|
|
61
60
|
function stringDefault(value, fallback) {
|
|
62
61
|
return value === undefined ? fallback : value;
|
|
63
62
|
}
|
|
64
|
-
function optionalSingle(value) {
|
|
65
|
-
return value === undefined ? undefined : [value];
|
|
66
|
-
}
|
|
67
63
|
function fallbackToolCallId(name) {
|
|
68
64
|
return `call_${stringDefault(name, 'tool')}`;
|
|
69
65
|
}
|
|
@@ -111,34 +107,21 @@ function contentHistoryMessage(msg) {
|
|
|
111
107
|
const content = contentFromOptionalParts(msg.parts, msg.content);
|
|
112
108
|
return { role: msg.role, content };
|
|
113
109
|
}
|
|
114
|
-
function sourceValue(part, key) {
|
|
115
|
-
const source = part;
|
|
116
|
-
const value = source[key];
|
|
117
|
-
return typeof value === 'string' ? value : undefined;
|
|
118
|
-
}
|
|
119
|
-
function sourceUrl(part) {
|
|
120
|
-
return sourceValue(part, 'url');
|
|
121
|
-
}
|
|
122
|
-
function sourceTitle(part) {
|
|
123
|
-
return sourceValue(part, 'title') ?? sourceUrl(part);
|
|
124
|
-
}
|
|
125
|
-
function sourceList(title, url) {
|
|
126
|
-
if (title === undefined || url === undefined) {
|
|
127
|
-
return undefined;
|
|
128
|
-
}
|
|
129
|
-
return [{ title, uri: url, type: 'web' }];
|
|
130
|
-
}
|
|
131
110
|
function sourceEvent(part) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
111
|
+
if (part.sourceType !== 'url') {
|
|
112
|
+
return {
|
|
113
|
+
type: 'evidence',
|
|
114
|
+
evidence: { provider: 'openrouter', raw: part },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const title = part.title ?? part.url;
|
|
135
118
|
return {
|
|
136
119
|
type: 'evidence',
|
|
137
120
|
evidence: {
|
|
138
121
|
provider: 'openrouter',
|
|
139
|
-
raw,
|
|
140
|
-
citations:
|
|
141
|
-
sources:
|
|
122
|
+
raw: part,
|
|
123
|
+
citations: [part.url],
|
|
124
|
+
sources: [{ title, uri: part.url, type: 'web' }],
|
|
142
125
|
},
|
|
143
126
|
};
|
|
144
127
|
}
|
|
@@ -171,19 +154,17 @@ function buildTools(dynamicTools) {
|
|
|
171
154
|
}
|
|
172
155
|
return tools;
|
|
173
156
|
}
|
|
174
|
-
function openRouterPlugins(req) {
|
|
175
|
-
const plugins = req.builtins
|
|
176
|
-
.map((id) => getTool(id)?.openRouterPlugin)
|
|
177
|
-
.filter((id) => Boolean(id))
|
|
178
|
-
.map((id) => ({ id }));
|
|
179
|
-
return plugins.length > 0 ? plugins : undefined;
|
|
180
|
-
}
|
|
181
157
|
function openRouterSettings(req) {
|
|
182
|
-
const plugins =
|
|
183
|
-
if (!
|
|
158
|
+
const { plugins, webSearch } = resolveOpenRouterPlugins(req.builtins);
|
|
159
|
+
if (plugins.length === 0 && !webSearch) {
|
|
184
160
|
return undefined;
|
|
185
161
|
}
|
|
186
|
-
|
|
162
|
+
const settings = {};
|
|
163
|
+
if (plugins.length > 0)
|
|
164
|
+
settings.plugins = plugins;
|
|
165
|
+
if (webSearch)
|
|
166
|
+
settings.web_search_options = {};
|
|
167
|
+
return settings;
|
|
187
168
|
}
|
|
188
169
|
function tokensFromUsage(usage) {
|
|
189
170
|
const input = usage.inputTokens ?? 0;
|
|
@@ -269,17 +250,6 @@ function toolArguments(input) {
|
|
|
269
250
|
}
|
|
270
251
|
return { value: input };
|
|
271
252
|
}
|
|
272
|
-
function isEmptyRecord(input) {
|
|
273
|
-
return Boolean(input) && Object.keys(input ?? {}).length === 0;
|
|
274
|
-
}
|
|
275
|
-
function toolCallArguments(part, acc) {
|
|
276
|
-
const fromInput = toolArguments(part.input);
|
|
277
|
-
const rawInput = acc.toolInputs.get(part.toolCallId);
|
|
278
|
-
if ((!fromInput || isEmptyRecord(fromInput)) && rawInput) {
|
|
279
|
-
return toolArguments(parseToolInput(rawInput));
|
|
280
|
-
}
|
|
281
|
-
return fromInput;
|
|
282
|
-
}
|
|
283
253
|
function toolResultData(output) {
|
|
284
254
|
return rawRecord(output);
|
|
285
255
|
}
|
|
@@ -333,45 +303,12 @@ function rawEvents(raw, acc) {
|
|
|
333
303
|
}
|
|
334
304
|
return events;
|
|
335
305
|
}
|
|
336
|
-
|
|
337
|
-
if (!res.body) {
|
|
338
|
-
return [];
|
|
339
|
-
}
|
|
340
|
-
const reader = res.body.getReader();
|
|
341
|
-
const decoder = new TextDecoder();
|
|
342
|
-
const events = [];
|
|
343
|
-
const acc = createAccumulator();
|
|
344
|
-
let buffer = '';
|
|
345
|
-
let pendingEvent = '';
|
|
346
|
-
while (true) {
|
|
347
|
-
const { done, value } = await reader.read();
|
|
348
|
-
if (done) {
|
|
349
|
-
break;
|
|
350
|
-
}
|
|
351
|
-
buffer += decoder.decode(value, { stream: true });
|
|
352
|
-
const taken = takeSsePayloads(buffer, pendingEvent);
|
|
353
|
-
buffer = taken.rest;
|
|
354
|
-
pendingEvent = taken.pendingEvent;
|
|
355
|
-
for (const payload of taken.payloads) {
|
|
356
|
-
events.push(...rawEvents(payload, acc));
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
return events;
|
|
360
|
-
}
|
|
361
|
-
function captureFetch(config, setCapture) {
|
|
362
|
-
const fetchFn = config.fetch ?? fetch;
|
|
363
|
-
return async (input, init) => {
|
|
364
|
-
const res = await fetchFn(input, init);
|
|
365
|
-
setCapture(collectRawEvents(res.clone()).catch(() => []));
|
|
366
|
-
return res;
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
function toolCallEvent(part, acc) {
|
|
306
|
+
function toolCallEvent(part) {
|
|
370
307
|
return {
|
|
371
308
|
type: 'tool',
|
|
372
309
|
tool: {
|
|
373
310
|
name: part.toolName,
|
|
374
|
-
arguments:
|
|
311
|
+
arguments: toolArguments(part.input),
|
|
375
312
|
id: part.toolCallId,
|
|
376
313
|
},
|
|
377
314
|
};
|
|
@@ -401,22 +338,7 @@ function providerMetadataEvent(part, acc) {
|
|
|
401
338
|
}
|
|
402
339
|
return evidenceFromMetadata(part.providerMetadata, acc);
|
|
403
340
|
}
|
|
404
|
-
function appendToolInput(part, acc) {
|
|
405
|
-
if (part.type === 'tool-input-start') {
|
|
406
|
-
acc.toolInputs.set(part.id, '');
|
|
407
|
-
return true;
|
|
408
|
-
}
|
|
409
|
-
if (part.type === 'tool-input-delta') {
|
|
410
|
-
const current = acc.toolInputs.get(part.id) ?? '';
|
|
411
|
-
acc.toolInputs.set(part.id, current + part.delta);
|
|
412
|
-
return true;
|
|
413
|
-
}
|
|
414
|
-
return false;
|
|
415
|
-
}
|
|
416
341
|
function eventFromPart(part, acc) {
|
|
417
|
-
if (appendToolInput(part, acc)) {
|
|
418
|
-
return [];
|
|
419
|
-
}
|
|
420
342
|
const mapped = primaryEventFromPart(part, acc);
|
|
421
343
|
if (mapped) {
|
|
422
344
|
return [mapped];
|
|
@@ -432,11 +354,15 @@ function primaryEventFromPart(part, acc) {
|
|
|
432
354
|
case 'reasoning-delta':
|
|
433
355
|
return { type: 'thought', text: part.text };
|
|
434
356
|
case 'tool-call':
|
|
435
|
-
return toolCallEvent(part
|
|
357
|
+
return toolCallEvent(part);
|
|
436
358
|
case 'tool-result':
|
|
437
359
|
return toolResultEvent(part);
|
|
438
|
-
case 'source':
|
|
360
|
+
case 'source': {
|
|
361
|
+
if (acc.evidenceSeen)
|
|
362
|
+
return undefined;
|
|
363
|
+
acc.evidenceSeen = true;
|
|
439
364
|
return sourceEvent(part);
|
|
365
|
+
}
|
|
440
366
|
case 'finish':
|
|
441
367
|
return finishEvent(part, acc);
|
|
442
368
|
case 'error':
|
|
@@ -456,27 +382,12 @@ function finishEvent(part, acc) {
|
|
|
456
382
|
}
|
|
457
383
|
return event;
|
|
458
384
|
}
|
|
459
|
-
function emitRawEvents(rawCapture, acc) {
|
|
460
|
-
return (rawCapture ?? Promise.resolve([])).then((events) => events.filter((event) => {
|
|
461
|
-
if (event.type !== 'evidence') {
|
|
462
|
-
return true;
|
|
463
|
-
}
|
|
464
|
-
if (acc.evidenceSeen) {
|
|
465
|
-
return false;
|
|
466
|
-
}
|
|
467
|
-
acc.evidenceSeen = true;
|
|
468
|
-
return true;
|
|
469
|
-
}));
|
|
470
|
-
}
|
|
471
385
|
function createStreamContext(req, config, apiKey) {
|
|
472
|
-
let rawCapture;
|
|
473
386
|
const openrouter = createOpenRouter({
|
|
474
387
|
apiKey,
|
|
475
388
|
baseURL: config.baseUrl,
|
|
476
389
|
headers: openRouterHeaders(config),
|
|
477
|
-
fetch:
|
|
478
|
-
rawCapture = capture;
|
|
479
|
-
}),
|
|
390
|
+
fetch: config.fetch,
|
|
480
391
|
compatibility: 'strict',
|
|
481
392
|
});
|
|
482
393
|
return {
|
|
@@ -485,27 +396,26 @@ function createStreamContext(req, config, apiKey) {
|
|
|
485
396
|
apiId: req.apiId,
|
|
486
397
|
openRouterId: req.openRouterId,
|
|
487
398
|
}),
|
|
488
|
-
rawCapture: () => rawCapture,
|
|
489
399
|
};
|
|
490
400
|
}
|
|
491
401
|
function streamTextOptions(req, context) {
|
|
492
402
|
return {
|
|
493
403
|
model: context.openrouter.chat(context.modelName, openRouterSettings(req)),
|
|
494
|
-
|
|
404
|
+
instructions: req.system,
|
|
495
405
|
messages: buildMessages(req),
|
|
496
406
|
allowSystemInMessages: true,
|
|
497
407
|
temperature: req.temperature,
|
|
498
408
|
maxOutputTokens: req.maxOutputTokens,
|
|
499
409
|
tools: buildTools(req.dynamicTools),
|
|
500
410
|
providerOptions: providerOptionsFor(req),
|
|
501
|
-
|
|
411
|
+
include: { rawChunks: true },
|
|
502
412
|
abortSignal: req.signal,
|
|
503
413
|
onError: () => undefined,
|
|
504
414
|
};
|
|
505
415
|
}
|
|
506
416
|
async function* yieldAiSdkStream(req, acc, context) {
|
|
507
417
|
const result = streamText(streamTextOptions(req, context));
|
|
508
|
-
for await (const part of result.
|
|
418
|
+
for await (const part of result.stream) {
|
|
509
419
|
if (part.type === 'raw') {
|
|
510
420
|
req.tapGemini?.(rawRecord(part.rawValue) ?? { rawValue: part.rawValue });
|
|
511
421
|
for (const event of rawEvents(part.rawValue, acc)) {
|
|
@@ -518,16 +428,6 @@ async function* yieldAiSdkStream(req, acc, context) {
|
|
|
518
428
|
}
|
|
519
429
|
}
|
|
520
430
|
}
|
|
521
|
-
async function* yieldCapturedRawEvents(context, acc) {
|
|
522
|
-
for (const event of await emitRawEvents(context.rawCapture(), acc)) {
|
|
523
|
-
yield event;
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
async function* yieldCapturedRawEventsUnchecked(context) {
|
|
527
|
-
for (const event of await (context.rawCapture() ?? Promise.resolve([]))) {
|
|
528
|
-
yield event;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
431
|
function missingOpenRouterKey() {
|
|
532
432
|
return toErrorEvent('missing OpenRouter API key');
|
|
533
433
|
}
|
|
@@ -541,14 +441,12 @@ async function* streamOpenRouter(req, config) {
|
|
|
541
441
|
const context = createStreamContext(req, config, apiKey);
|
|
542
442
|
try {
|
|
543
443
|
yield* yieldAiSdkStream(req, acc, context);
|
|
544
|
-
yield* yieldCapturedRawEvents(context, acc);
|
|
545
444
|
yield* finalEvents(req, acc);
|
|
546
445
|
}
|
|
547
446
|
catch (err) {
|
|
548
447
|
if (isAbortError(err)) {
|
|
549
448
|
throw err;
|
|
550
449
|
}
|
|
551
|
-
yield* yieldCapturedRawEventsUnchecked(context);
|
|
552
450
|
yield toErrorEvent(err);
|
|
553
451
|
}
|
|
554
452
|
}
|
|
@@ -564,16 +462,34 @@ function* finalEvents(req, acc) {
|
|
|
564
462
|
}
|
|
565
463
|
yield { type: 'done' };
|
|
566
464
|
}
|
|
567
|
-
function
|
|
568
|
-
if (req.
|
|
465
|
+
function responseFormatFor(req) {
|
|
466
|
+
if (!req.structured)
|
|
467
|
+
return undefined;
|
|
468
|
+
const spec = getStructured(req.structured);
|
|
469
|
+
if (!spec.jsonSchema)
|
|
569
470
|
return undefined;
|
|
570
|
-
}
|
|
571
471
|
return {
|
|
572
|
-
|
|
573
|
-
|
|
472
|
+
type: 'json_schema',
|
|
473
|
+
json_schema: {
|
|
474
|
+
name: String(req.structured),
|
|
475
|
+
strict: true,
|
|
476
|
+
schema: spec.jsonSchema,
|
|
574
477
|
},
|
|
575
478
|
};
|
|
576
479
|
}
|
|
480
|
+
function providerOptionsFor(req) {
|
|
481
|
+
const openrouter = {};
|
|
482
|
+
if (req.thinking !== 'none') {
|
|
483
|
+
openrouter.reasoning = { effort: req.thinking };
|
|
484
|
+
}
|
|
485
|
+
const responseFormat = responseFormatFor(req);
|
|
486
|
+
if (responseFormat) {
|
|
487
|
+
openrouter.response_format = responseFormat;
|
|
488
|
+
}
|
|
489
|
+
if (Object.keys(openrouter).length === 0)
|
|
490
|
+
return undefined;
|
|
491
|
+
return { openrouter };
|
|
492
|
+
}
|
|
577
493
|
function openRouterHeaders(config) {
|
|
578
494
|
const headers = {};
|
|
579
495
|
if (config.siteUrl) {
|
|
@@ -591,3 +507,48 @@ function createOpenRouterProvider(config = {}) {
|
|
|
591
507
|
};
|
|
592
508
|
}
|
|
593
509
|
export { createOpenRouterProvider, resolveOpenRouterModel, toOpenRouterPayload };
|
|
510
|
+
exposeForTests('openrouter', {
|
|
511
|
+
trimApiKey,
|
|
512
|
+
createAccumulator,
|
|
513
|
+
mediaPart,
|
|
514
|
+
contentFromParts,
|
|
515
|
+
parseToolInput,
|
|
516
|
+
stringDefault,
|
|
517
|
+
fallbackToolCallId,
|
|
518
|
+
toolResultContent,
|
|
519
|
+
assistantToolCallContent,
|
|
520
|
+
historyMessage,
|
|
521
|
+
contentFromOptionalParts,
|
|
522
|
+
contentHistoryMessage,
|
|
523
|
+
buildMessages,
|
|
524
|
+
schemaForTool,
|
|
525
|
+
buildTools,
|
|
526
|
+
openRouterSettings,
|
|
527
|
+
tokensFromUsage,
|
|
528
|
+
rawRecord,
|
|
529
|
+
stringArray,
|
|
530
|
+
metadataRecord,
|
|
531
|
+
citationCandidates,
|
|
532
|
+
nestedCitations,
|
|
533
|
+
metadataAnnotations,
|
|
534
|
+
evidenceFromMetadata,
|
|
535
|
+
toolArguments,
|
|
536
|
+
toolResultData,
|
|
537
|
+
rawThoughtEvent,
|
|
538
|
+
rawChoiceMessageEvidence,
|
|
539
|
+
rawEvents,
|
|
540
|
+
toolCallEvent,
|
|
541
|
+
toolResultEvent,
|
|
542
|
+
tokenEvent,
|
|
543
|
+
providerMetadataEvent,
|
|
544
|
+
eventFromPart,
|
|
545
|
+
primaryEventFromPart,
|
|
546
|
+
finishEvent,
|
|
547
|
+
sourceEvent,
|
|
548
|
+
finalEvents,
|
|
549
|
+
responseFormatFor,
|
|
550
|
+
providerOptionsFor,
|
|
551
|
+
openRouterHeaders,
|
|
552
|
+
missingOpenRouterKey,
|
|
553
|
+
streamTextOptions,
|
|
554
|
+
});
|
package/esm/src/providers/pcm.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
|
+
import { exposeForTests } from './expose-for-tests.js';
|
|
6
7
|
const SAMPLE_RATE = 24000;
|
|
7
8
|
function writeAscii(view, offset, str) {
|
|
8
9
|
for (let i = 0; i < str.length; i++)
|
|
@@ -33,3 +34,4 @@ export function wrapPcmAsWav(pcm, sampleRate = SAMPLE_RATE) {
|
|
|
33
34
|
new Uint8Array(buf, 44).set(pcm);
|
|
34
35
|
return new Uint8Array(buf);
|
|
35
36
|
}
|
|
37
|
+
exposeForTests('pcm', { writeAscii, wrapPcmAsWav });
|
|
@@ -15,6 +15,7 @@ import { toInteractionsBody } from './interactions.js';
|
|
|
15
15
|
import { fetchGemini } from './keys.js';
|
|
16
16
|
import { wrapPcmAsWav } from './pcm.js';
|
|
17
17
|
import { INTERACTIONS_URL, takeSsePayloads } from './sse.js';
|
|
18
|
+
import { exposeForTests } from './expose-for-tests.js';
|
|
18
19
|
const HTTP_OK = 200;
|
|
19
20
|
function base64ToBytes(data) {
|
|
20
21
|
const bin = atob(data);
|
|
@@ -161,3 +162,15 @@ function createInteractionsProvider(transport) {
|
|
|
161
162
|
};
|
|
162
163
|
}
|
|
163
164
|
export { createInteractionsProvider };
|
|
165
|
+
exposeForTests('provider', {
|
|
166
|
+
base64ToBytes,
|
|
167
|
+
bytesToBase64,
|
|
168
|
+
isRawPcmMime,
|
|
169
|
+
normalizeSpeechMedia,
|
|
170
|
+
eventType,
|
|
171
|
+
isDeltaEvent,
|
|
172
|
+
isCompleteEvent,
|
|
173
|
+
foldDeltaPayload,
|
|
174
|
+
foldPayload,
|
|
175
|
+
withTap,
|
|
176
|
+
});
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { toErrorEvent } from '../guardrails/error.js';
|
|
10
10
|
import { wrapPcmAsWav } from './pcm.js';
|
|
11
|
+
import { exposeForTests } from './expose-for-tests.js';
|
|
11
12
|
const HTTP_OK = 200;
|
|
12
13
|
function bytesToBase64(bytes) {
|
|
13
14
|
let bin = '';
|
|
@@ -124,3 +125,12 @@ function createSpeechProvider(config = {}) {
|
|
|
124
125
|
};
|
|
125
126
|
}
|
|
126
127
|
export { createSpeechProvider, streamSpeech };
|
|
128
|
+
exposeForTests('speech', {
|
|
129
|
+
bytesToBase64,
|
|
130
|
+
extractInputText,
|
|
131
|
+
resolveSpeechWireModel,
|
|
132
|
+
buildHeaders,
|
|
133
|
+
buildPayload,
|
|
134
|
+
requestSpeech,
|
|
135
|
+
yieldSpeechSuccess,
|
|
136
|
+
});
|
package/esm/src/providers/sse.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { exposeForTests } from './expose-for-tests.js';
|
|
1
2
|
const INTERACTIONS_URL = 'https://generativelanguage.googleapis.com/v1beta/interactions?alt=sse';
|
|
2
3
|
const DATA_PREFIX = 'data: ';
|
|
3
4
|
const EVENT_PREFIX = 'event: ';
|
|
@@ -51,3 +52,4 @@ function takeSsePayloads(buffer, pendingEvent = '') {
|
|
|
51
52
|
return { rest, payloads, pendingEvent: sseEvent };
|
|
52
53
|
}
|
|
53
54
|
export { INTERACTIONS_URL, takeSsePayloads };
|
|
55
|
+
exposeForTests('sse', { asObject, dataRecord, takeSsePayloads });
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read one string field from incomplete JSON while structured output streams as text deltas.
|
|
3
|
+
*
|
|
4
|
+
* The buffer may lack a closing quote; any decoded prefix is returned for live preview.
|
|
5
|
+
*/
|
|
6
|
+
export declare function readStreamingJsonStringField(jsonText: string, key: string): string | null;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const JSON_ESCAPES = {
|
|
2
|
+
n: '\n',
|
|
3
|
+
t: '\t',
|
|
4
|
+
r: '\r',
|
|
5
|
+
'"': '"',
|
|
6
|
+
'\\': '\\',
|
|
7
|
+
'/': '/',
|
|
8
|
+
};
|
|
9
|
+
function decodeEscapedChar(ch, jsonText, index) {
|
|
10
|
+
if (ch === 'u' && index + 4 < jsonText.length) {
|
|
11
|
+
const hex = jsonText.slice(index + 1, index + 5);
|
|
12
|
+
if (/^[0-9a-fA-F]{4}$/.test(hex)) {
|
|
13
|
+
return { text: String.fromCharCode(Number.parseInt(hex, 16)), next: index + 4 };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const mapped = JSON_ESCAPES[ch];
|
|
17
|
+
return { text: mapped ?? ch, next: index };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Read one string field from incomplete JSON while structured output streams as text deltas.
|
|
21
|
+
*
|
|
22
|
+
* The buffer may lack a closing quote; any decoded prefix is returned for live preview.
|
|
23
|
+
*/
|
|
24
|
+
export function readStreamingJsonStringField(jsonText, key) {
|
|
25
|
+
const keyPattern = new RegExp(`"${key}"\\s*:\\s*"`);
|
|
26
|
+
const match = keyPattern.exec(jsonText);
|
|
27
|
+
if (!match || match.index === undefined) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
let i = match.index + match[0].length;
|
|
31
|
+
let result = '';
|
|
32
|
+
let escaped = false;
|
|
33
|
+
while (i < jsonText.length) {
|
|
34
|
+
const ch = jsonText[i];
|
|
35
|
+
if (escaped) {
|
|
36
|
+
const decoded = decodeEscapedChar(ch, jsonText, i);
|
|
37
|
+
result += decoded.text;
|
|
38
|
+
i = decoded.next;
|
|
39
|
+
escaped = false;
|
|
40
|
+
i += 1;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (ch === '\\') {
|
|
44
|
+
escaped = true;
|
|
45
|
+
}
|
|
46
|
+
else if (ch === '"') {
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
result += ch;
|
|
51
|
+
}
|
|
52
|
+
i += 1;
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "theorum",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "A flat TypeScript agent kernel for typed profiles, deterministic turn execution, dynamic tools, provider adapters, guardrails, and host-injected traces.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -45,6 +45,9 @@
|
|
|
45
45
|
},
|
|
46
46
|
"./presets/google": {
|
|
47
47
|
"import": "./esm/src/presets/google.js"
|
|
48
|
+
},
|
|
49
|
+
"./streaming": {
|
|
50
|
+
"import": "./esm/src/streaming/mod.js"
|
|
48
51
|
}
|
|
49
52
|
},
|
|
50
53
|
"scripts": {},
|
|
@@ -54,8 +57,8 @@
|
|
|
54
57
|
"type": "module",
|
|
55
58
|
"sideEffects": false,
|
|
56
59
|
"dependencies": {
|
|
57
|
-
"@openrouter/ai-sdk-provider": "
|
|
58
|
-
"ai": "
|
|
60
|
+
"@openrouter/ai-sdk-provider": "^3.0.0",
|
|
61
|
+
"ai": "^7.0.0",
|
|
59
62
|
"@deno/shim-deno": "~0.18.0"
|
|
60
63
|
},
|
|
61
64
|
"devDependencies": {
|