theorum 0.1.6 → 0.1.9

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 (41) hide show
  1. package/docs/AGENT_PROFILE_CONTRACT.md +1 -0
  2. package/esm/mod.d.ts +1 -1
  3. package/esm/mod.js +1 -1
  4. package/esm/src/guardrails/error.d.ts +7 -1
  5. package/esm/src/guardrails/error.js +31 -1
  6. package/esm/src/guardrails/mod.d.ts +1 -1
  7. package/esm/src/guardrails/mod.js +1 -1
  8. package/esm/src/kernel/engine/runner/gates.js +2 -1
  9. package/esm/src/kernel/engine/runner/mod.js +2 -0
  10. package/esm/src/kernel/engine/runner/steps.js +5 -2
  11. package/esm/src/kernel/engine/runner/stream.d.ts +1 -0
  12. package/esm/src/kernel/engine/runner/stream.js +5 -2
  13. package/esm/src/kernel/types.d.ts +7 -0
  14. package/esm/src/observability/trace-record.js +4 -3
  15. package/esm/src/providers/create-provider.d.ts +7 -0
  16. package/esm/src/providers/create-provider.js +2 -0
  17. package/esm/src/providers/gemini-tape.d.ts +14 -0
  18. package/esm/src/providers/gemini-tape.js +9 -0
  19. package/esm/src/providers/google-tap.d.ts +10 -0
  20. package/esm/src/providers/google-tap.js +2 -0
  21. package/esm/src/providers/interactions.d.ts +37 -1
  22. package/esm/src/providers/interactions.js +16 -0
  23. package/esm/src/providers/keys.d.ts +19 -0
  24. package/esm/src/providers/keys.js +24 -3
  25. package/esm/src/providers/openrouter-payload.d.ts +9 -2
  26. package/esm/src/providers/openrouter-payload.js +23 -7
  27. package/esm/src/providers/openrouter.d.ts +118 -1
  28. package/esm/src/providers/openrouter.js +106 -141
  29. package/esm/src/providers/pcm.d.ts +7 -0
  30. package/esm/src/providers/pcm.js +2 -0
  31. package/esm/src/providers/provider.d.ts +29 -1
  32. package/esm/src/providers/provider.js +18 -2
  33. package/esm/src/providers/speech.d.ts +18 -1
  34. package/esm/src/providers/speech.js +11 -0
  35. package/esm/src/providers/sse.d.ts +8 -0
  36. package/esm/src/providers/sse.js +2 -0
  37. package/esm/src/streaming/mod.d.ts +7 -0
  38. package/esm/src/streaming/mod.js +7 -0
  39. package/esm/src/streaming/readStreamingJsonStringField.d.ts +6 -0
  40. package/esm/src/streaming/readStreamingJsonStringField.js +55 -0
  41. package/package.json +6 -3
@@ -7,9 +7,126 @@
7
7
  *
8
8
  * @module
9
9
  */
10
- import type { ModelProvider } from '../kernel/types.js';
10
+ import { createOpenRouter, type OpenRouterChatSettings } from '@openrouter/ai-sdk-provider';
11
+ import { type LanguageModelUsage, type ModelMessage, streamText, type TextStreamPart, type ToolSet } from 'ai';
12
+ import type { DynamicToolDeclaration, InteractionPart, ModelProvider, ProviderCompleteRequest, TurnEvent, TurnHistoryMessage, TurnTokens } from '../kernel/types.js';
11
13
  import { type OpenRouterConfig, resolveOpenRouterModel, toOpenRouterPayload } from './openrouter-payload.js';
14
+ interface StreamAccumulator {
15
+ text: string;
16
+ evidenceSeen: boolean;
17
+ emittedTokens: boolean;
18
+ errored: boolean;
19
+ }
20
+ interface OpenRouterStreamContext {
21
+ openrouter: ReturnType<typeof createOpenRouter>;
22
+ modelName: string;
23
+ }
24
+ type JsonValue = string | number | boolean | null | JsonValue[] | {
25
+ [key: string]: JsonValue;
26
+ };
27
+ type ProviderOptions = Record<string, {
28
+ [key: string]: JsonValue;
29
+ }>;
30
+ declare function trimApiKey(explicitKey?: string): string | undefined;
31
+ declare function createAccumulator(): StreamAccumulator;
32
+ declare function mediaPart(part: InteractionPart): Record<string, unknown>;
33
+ declare function contentFromParts(parts: InteractionPart[]): string | Array<Record<string, unknown>>;
34
+ declare function parseToolInput(raw: string): unknown;
35
+ declare function stringDefault(value: string | undefined, fallback: string): string;
36
+ declare function fallbackToolCallId(name?: string): string;
37
+ declare function toolResultContent(msg: TurnHistoryMessage): ModelMessage;
38
+ declare function assistantToolCallContent(msg: TurnHistoryMessage): ModelMessage | null;
39
+ declare function historyMessage(msg: TurnHistoryMessage): ModelMessage | null;
40
+ declare function contentFromOptionalParts(parts: InteractionPart[] | undefined, text: string | undefined): string | Array<Record<string, unknown>>;
41
+ declare function contentHistoryMessage(msg: TurnHistoryMessage): ModelMessage;
42
+ declare function sourceEvent(part: Extract<TextStreamPart<ToolSet>, {
43
+ type: 'source';
44
+ }>): TurnEvent;
45
+ declare function buildMessages(req: ProviderCompleteRequest): ModelMessage[];
46
+ declare function schemaForTool(decl: DynamicToolDeclaration): Record<string, unknown>;
47
+ declare function buildTools(dynamicTools?: DynamicToolDeclaration[]): ToolSet | undefined;
48
+ declare function openRouterSettings(req: ProviderCompleteRequest): OpenRouterChatSettings | undefined;
49
+ declare function tokensFromUsage(usage: LanguageModelUsage): TurnTokens | undefined;
50
+ declare function rawRecord(value: unknown): Record<string, unknown> | undefined;
51
+ declare function stringArray(value: unknown): string[] | undefined;
52
+ declare function metadataRecord(raw: Record<string, unknown>, key: string): Record<string, unknown> | undefined;
53
+ declare function citationCandidates(raw: Record<string, unknown>): unknown[];
54
+ declare function nestedCitations(raw: Record<string, unknown>): string[] | undefined;
55
+ declare function metadataAnnotations(raw: Record<string, unknown>): unknown[] | undefined;
56
+ declare function evidenceFromMetadata(metadata: unknown, acc: StreamAccumulator): TurnEvent | undefined;
57
+ declare function toolArguments(input: unknown): Record<string, unknown> | undefined;
58
+ declare function toolResultData(output: unknown): Record<string, unknown> | undefined;
59
+ declare function rawThoughtEvent(raw: Record<string, unknown>): TurnEvent | undefined;
60
+ declare function rawChoiceMessageEvidence(raw: Record<string, unknown>, acc: StreamAccumulator): TurnEvent | undefined;
61
+ declare function rawEvents(raw: unknown, acc: StreamAccumulator): TurnEvent[];
62
+ declare function toolCallEvent(part: Extract<TextStreamPart<ToolSet>, {
63
+ type: 'tool-call';
64
+ }>): TurnEvent;
65
+ declare function toolResultEvent(part: Extract<TextStreamPart<ToolSet>, {
66
+ type: 'tool-result';
67
+ }>): TurnEvent;
68
+ declare function tokenEvent(part: Extract<TextStreamPart<ToolSet>, {
69
+ type: 'finish';
70
+ }>): TurnEvent | undefined;
71
+ declare function providerMetadataEvent(part: TextStreamPart<ToolSet>, acc: StreamAccumulator): TurnEvent | undefined;
72
+ declare function eventFromPart(part: TextStreamPart<ToolSet>, acc: StreamAccumulator): TurnEvent[];
73
+ declare function primaryEventFromPart(part: TextStreamPart<ToolSet>, acc: StreamAccumulator): TurnEvent | undefined;
74
+ declare function finishEvent(part: Extract<TextStreamPart<ToolSet>, {
75
+ type: 'finish';
76
+ }>, acc: StreamAccumulator): TurnEvent | undefined;
77
+ declare function streamTextOptions(req: ProviderCompleteRequest, context: OpenRouterStreamContext): Parameters<typeof streamText>[0];
78
+ declare function missingOpenRouterKey(): TurnEvent;
79
+ declare function finalEvents(req: ProviderCompleteRequest, acc: StreamAccumulator): Generator<TurnEvent>;
80
+ declare function responseFormatFor(req: ProviderCompleteRequest): Record<string, JsonValue> | undefined;
81
+ declare function providerOptionsFor(req: ProviderCompleteRequest): ProviderOptions | undefined;
82
+ declare function openRouterHeaders(config: OpenRouterConfig): Record<string, string> | undefined;
12
83
  /** Create a `ModelProvider` backed by OpenRouter through AI SDK Core. */
13
84
  declare function createOpenRouterProvider(config?: OpenRouterConfig): ModelProvider;
14
85
  export type { OpenRouterConfig };
15
86
  export { createOpenRouterProvider, resolveOpenRouterModel, toOpenRouterPayload };
87
+ /** @internal Exported for direct unit testing only. */
88
+ export declare const _internals: {
89
+ trimApiKey: typeof trimApiKey;
90
+ createAccumulator: typeof createAccumulator;
91
+ mediaPart: typeof mediaPart;
92
+ contentFromParts: typeof contentFromParts;
93
+ parseToolInput: typeof parseToolInput;
94
+ stringDefault: typeof stringDefault;
95
+ fallbackToolCallId: typeof fallbackToolCallId;
96
+ toolResultContent: typeof toolResultContent;
97
+ assistantToolCallContent: typeof assistantToolCallContent;
98
+ historyMessage: typeof historyMessage;
99
+ contentFromOptionalParts: typeof contentFromOptionalParts;
100
+ contentHistoryMessage: typeof contentHistoryMessage;
101
+ buildMessages: typeof buildMessages;
102
+ schemaForTool: typeof schemaForTool;
103
+ buildTools: typeof buildTools;
104
+ openRouterSettings: typeof openRouterSettings;
105
+ tokensFromUsage: typeof tokensFromUsage;
106
+ rawRecord: typeof rawRecord;
107
+ stringArray: typeof stringArray;
108
+ metadataRecord: typeof metadataRecord;
109
+ citationCandidates: typeof citationCandidates;
110
+ nestedCitations: typeof nestedCitations;
111
+ metadataAnnotations: typeof metadataAnnotations;
112
+ evidenceFromMetadata: typeof evidenceFromMetadata;
113
+ toolArguments: typeof toolArguments;
114
+ toolResultData: typeof toolResultData;
115
+ rawThoughtEvent: typeof rawThoughtEvent;
116
+ rawChoiceMessageEvidence: typeof rawChoiceMessageEvidence;
117
+ rawEvents: typeof rawEvents;
118
+ toolCallEvent: typeof toolCallEvent;
119
+ toolResultEvent: typeof toolResultEvent;
120
+ tokenEvent: typeof tokenEvent;
121
+ providerMetadataEvent: typeof providerMetadataEvent;
122
+ eventFromPart: typeof eventFromPart;
123
+ primaryEventFromPart: typeof primaryEventFromPart;
124
+ finishEvent: typeof finishEvent;
125
+ sourceEvent: typeof sourceEvent;
126
+ finalEvents: typeof finalEvents;
127
+ responseFormatFor: typeof responseFormatFor;
128
+ providerOptionsFor: typeof providerOptionsFor;
129
+ openRouterHeaders: typeof openRouterHeaders;
130
+ missingOpenRouterKey: typeof missingOpenRouterKey;
131
+ streamTextOptions: typeof streamTextOptions;
132
+ };
@@ -9,11 +9,10 @@
9
9
  */
10
10
  import { createOpenRouter } from '@openrouter/ai-sdk-provider';
11
11
  import { jsonSchema, streamText, tool, } from 'ai';
12
- import { toErrorEvent } from '../guardrails/error.js';
12
+ import { isAbortError, toErrorEvent } from '../guardrails/error.js';
13
13
  import { tryStructured } from '../kernel/engine/delta.js';
14
- import { getTool } from '../kernel/registry/catalog.js';
15
- import { resolveOpenRouterModel, toOpenRouterPayload, } from './openrouter-payload.js';
16
- import { takeSsePayloads } from './sse.js';
14
+ import { getStructured } from '../kernel/registry/schemas.js';
15
+ import { resolveOpenRouterModel, resolveOpenRouterPlugins, toOpenRouterPayload, } from './openrouter-payload.js';
17
16
  function trimApiKey(explicitKey) {
18
17
  if (explicitKey?.trim()) {
19
18
  return explicitKey.trim();
@@ -26,7 +25,6 @@ function createAccumulator() {
26
25
  evidenceSeen: false,
27
26
  emittedTokens: false,
28
27
  errored: false,
29
- toolInputs: new Map(),
30
28
  };
31
29
  }
32
30
  function mediaPart(part) {
@@ -61,9 +59,6 @@ function parseToolInput(raw) {
61
59
  function stringDefault(value, fallback) {
62
60
  return value === undefined ? fallback : value;
63
61
  }
64
- function optionalSingle(value) {
65
- return value === undefined ? undefined : [value];
66
- }
67
62
  function fallbackToolCallId(name) {
68
63
  return `call_${stringDefault(name, 'tool')}`;
69
64
  }
@@ -111,34 +106,21 @@ function contentHistoryMessage(msg) {
111
106
  const content = contentFromOptionalParts(msg.parts, msg.content);
112
107
  return { role: msg.role, content };
113
108
  }
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
109
  function sourceEvent(part) {
132
- const raw = part;
133
- const url = sourceUrl(part);
134
- const title = sourceTitle(part);
110
+ if (part.sourceType !== 'url') {
111
+ return {
112
+ type: 'evidence',
113
+ evidence: { provider: 'openrouter', raw: part },
114
+ };
115
+ }
116
+ const title = part.title ?? part.url;
135
117
  return {
136
118
  type: 'evidence',
137
119
  evidence: {
138
120
  provider: 'openrouter',
139
- raw,
140
- citations: optionalSingle(url),
141
- sources: sourceList(title, url),
121
+ raw: part,
122
+ citations: [part.url],
123
+ sources: [{ title, uri: part.url, type: 'web' }],
142
124
  },
143
125
  };
144
126
  }
@@ -171,19 +153,17 @@ function buildTools(dynamicTools) {
171
153
  }
172
154
  return tools;
173
155
  }
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
156
  function openRouterSettings(req) {
182
- const plugins = openRouterPlugins(req);
183
- if (!plugins) {
157
+ const { plugins, webSearch } = resolveOpenRouterPlugins(req.builtins);
158
+ if (plugins.length === 0 && !webSearch) {
184
159
  return undefined;
185
160
  }
186
- return { plugins };
161
+ const settings = {};
162
+ if (plugins.length > 0)
163
+ settings.plugins = plugins;
164
+ if (webSearch)
165
+ settings.web_search_options = {};
166
+ return settings;
187
167
  }
188
168
  function tokensFromUsage(usage) {
189
169
  const input = usage.inputTokens ?? 0;
@@ -269,17 +249,6 @@ function toolArguments(input) {
269
249
  }
270
250
  return { value: input };
271
251
  }
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
252
  function toolResultData(output) {
284
253
  return rawRecord(output);
285
254
  }
@@ -333,45 +302,12 @@ function rawEvents(raw, acc) {
333
302
  }
334
303
  return events;
335
304
  }
336
- async function collectRawEvents(res) {
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) {
305
+ function toolCallEvent(part) {
370
306
  return {
371
307
  type: 'tool',
372
308
  tool: {
373
309
  name: part.toolName,
374
- arguments: toolCallArguments(part, acc),
310
+ arguments: toolArguments(part.input),
375
311
  id: part.toolCallId,
376
312
  },
377
313
  };
@@ -401,22 +337,7 @@ function providerMetadataEvent(part, acc) {
401
337
  }
402
338
  return evidenceFromMetadata(part.providerMetadata, acc);
403
339
  }
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
340
  function eventFromPart(part, acc) {
417
- if (appendToolInput(part, acc)) {
418
- return [];
419
- }
420
341
  const mapped = primaryEventFromPart(part, acc);
421
342
  if (mapped) {
422
343
  return [mapped];
@@ -432,11 +353,15 @@ function primaryEventFromPart(part, acc) {
432
353
  case 'reasoning-delta':
433
354
  return { type: 'thought', text: part.text };
434
355
  case 'tool-call':
435
- return toolCallEvent(part, acc);
356
+ return toolCallEvent(part);
436
357
  case 'tool-result':
437
358
  return toolResultEvent(part);
438
- case 'source':
359
+ case 'source': {
360
+ if (acc.evidenceSeen)
361
+ return undefined;
362
+ acc.evidenceSeen = true;
439
363
  return sourceEvent(part);
364
+ }
440
365
  case 'finish':
441
366
  return finishEvent(part, acc);
442
367
  case 'error':
@@ -456,27 +381,12 @@ function finishEvent(part, acc) {
456
381
  }
457
382
  return event;
458
383
  }
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
384
  function createStreamContext(req, config, apiKey) {
472
- let rawCapture;
473
385
  const openrouter = createOpenRouter({
474
386
  apiKey,
475
387
  baseURL: config.baseUrl,
476
388
  headers: openRouterHeaders(config),
477
- fetch: captureFetch(config, (capture) => {
478
- rawCapture = capture;
479
- }),
389
+ fetch: config.fetch,
480
390
  compatibility: 'strict',
481
391
  });
482
392
  return {
@@ -485,26 +395,26 @@ function createStreamContext(req, config, apiKey) {
485
395
  apiId: req.apiId,
486
396
  openRouterId: req.openRouterId,
487
397
  }),
488
- rawCapture: () => rawCapture,
489
398
  };
490
399
  }
491
400
  function streamTextOptions(req, context) {
492
401
  return {
493
402
  model: context.openrouter.chat(context.modelName, openRouterSettings(req)),
494
- system: req.system,
403
+ instructions: req.system,
495
404
  messages: buildMessages(req),
496
405
  allowSystemInMessages: true,
497
406
  temperature: req.temperature,
498
407
  maxOutputTokens: req.maxOutputTokens,
499
408
  tools: buildTools(req.dynamicTools),
500
409
  providerOptions: providerOptionsFor(req),
501
- includeRawChunks: true,
410
+ include: { rawChunks: true },
411
+ abortSignal: req.signal,
502
412
  onError: () => undefined,
503
413
  };
504
414
  }
505
415
  async function* yieldAiSdkStream(req, acc, context) {
506
416
  const result = streamText(streamTextOptions(req, context));
507
- for await (const part of result.fullStream) {
417
+ for await (const part of result.stream) {
508
418
  if (part.type === 'raw') {
509
419
  req.tapGemini?.(rawRecord(part.rawValue) ?? { rawValue: part.rawValue });
510
420
  for (const event of rawEvents(part.rawValue, acc)) {
@@ -517,16 +427,6 @@ async function* yieldAiSdkStream(req, acc, context) {
517
427
  }
518
428
  }
519
429
  }
520
- async function* yieldCapturedRawEvents(context, acc) {
521
- for (const event of await emitRawEvents(context.rawCapture(), acc)) {
522
- yield event;
523
- }
524
- }
525
- async function* yieldCapturedRawEventsUnchecked(context) {
526
- for (const event of await (context.rawCapture() ?? Promise.resolve([]))) {
527
- yield event;
528
- }
529
- }
530
430
  function missingOpenRouterKey() {
531
431
  return toErrorEvent('missing OpenRouter API key');
532
432
  }
@@ -540,11 +440,12 @@ async function* streamOpenRouter(req, config) {
540
440
  const context = createStreamContext(req, config, apiKey);
541
441
  try {
542
442
  yield* yieldAiSdkStream(req, acc, context);
543
- yield* yieldCapturedRawEvents(context, acc);
544
443
  yield* finalEvents(req, acc);
545
444
  }
546
445
  catch (err) {
547
- yield* yieldCapturedRawEventsUnchecked(context);
446
+ if (isAbortError(err)) {
447
+ throw err;
448
+ }
548
449
  yield toErrorEvent(err);
549
450
  }
550
451
  }
@@ -560,16 +461,34 @@ function* finalEvents(req, acc) {
560
461
  }
561
462
  yield { type: 'done' };
562
463
  }
563
- function providerOptionsFor(req) {
564
- if (req.thinking === 'none') {
464
+ function responseFormatFor(req) {
465
+ if (!req.structured)
466
+ return undefined;
467
+ const spec = getStructured(req.structured);
468
+ if (!spec.jsonSchema)
565
469
  return undefined;
566
- }
567
470
  return {
568
- openrouter: {
569
- reasoning: { effort: req.thinking },
471
+ type: 'json_schema',
472
+ json_schema: {
473
+ name: String(req.structured),
474
+ strict: true,
475
+ schema: spec.jsonSchema,
570
476
  },
571
477
  };
572
478
  }
479
+ function providerOptionsFor(req) {
480
+ const openrouter = {};
481
+ if (req.thinking !== 'none') {
482
+ openrouter.reasoning = { effort: req.thinking };
483
+ }
484
+ const responseFormat = responseFormatFor(req);
485
+ if (responseFormat) {
486
+ openrouter.response_format = responseFormat;
487
+ }
488
+ if (Object.keys(openrouter).length === 0)
489
+ return undefined;
490
+ return { openrouter };
491
+ }
573
492
  function openRouterHeaders(config) {
574
493
  const headers = {};
575
494
  if (config.siteUrl) {
@@ -587,3 +506,49 @@ function createOpenRouterProvider(config = {}) {
587
506
  };
588
507
  }
589
508
  export { createOpenRouterProvider, resolveOpenRouterModel, toOpenRouterPayload };
509
+ /** @internal Exported for direct unit testing only. */
510
+ export const _internals = {
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
+ };
@@ -3,5 +3,12 @@
3
3
  *
4
4
  * @module
5
5
  */
6
+ declare function writeAscii(view: DataView, offset: number, str: string): void;
6
7
  /** Wrap raw PCM bytes in a RIFF/WAVE container. */
7
8
  export declare function wrapPcmAsWav(pcm: Uint8Array, sampleRate?: number): Uint8Array;
9
+ /** @internal Exported for direct unit testing only. */
10
+ export declare const _internals: {
11
+ writeAscii: typeof writeAscii;
12
+ wrapPcmAsWav: typeof wrapPcmAsWav;
13
+ };
14
+ export {};
@@ -33,3 +33,5 @@ export function wrapPcmAsWav(pcm, sampleRate = SAMPLE_RATE) {
33
33
  new Uint8Array(buf, 44).set(pcm);
34
34
  return new Uint8Array(buf);
35
35
  }
36
+ /** @internal Exported for direct unit testing only. */
37
+ export const _internals = { writeAscii, wrapPcmAsWav };
@@ -8,8 +8,36 @@
8
8
  *
9
9
  * @module
10
10
  */
11
- import type { ModelProvider } from '../kernel/types.js';
11
+ import type { ModelProvider, ProviderCompleteRequest, TurnEvent } from '../kernel/types.js';
12
12
  import { type GeminiTransport } from './keys.js';
13
+ declare function base64ToBytes(data: string): Uint8Array;
14
+ declare function bytesToBase64(bytes: Uint8Array): string;
15
+ declare function isRawPcmMime(mime: string): boolean;
16
+ /** Google TTS returns raw PCM; wrap as WAV for hosts (matches OpenRouter pcm path). */
17
+ declare function normalizeSpeechMedia(event: TurnEvent, speech: boolean): TurnEvent;
18
+ declare function eventType(event: Record<string, unknown>): string;
19
+ declare function isDeltaEvent(kind: string): boolean;
20
+ declare function isCompleteEvent(kind: string): boolean;
21
+ declare function foldDeltaPayload(event: Record<string, unknown>, acc: {
22
+ text: string;
23
+ }): TurnEvent[];
24
+ declare function foldPayload(event: Record<string, unknown>, acc: {
25
+ text: string;
26
+ }): TurnEvent[];
27
+ declare function withTap(req: ProviderCompleteRequest, transport: GeminiTransport): GeminiTransport;
13
28
  /** Create a `ModelProvider` backed by Google Interactions streaming. */
14
29
  declare function createInteractionsProvider(transport: GeminiTransport): ModelProvider;
15
30
  export { createInteractionsProvider };
31
+ /** @internal Exported for direct unit testing only. */
32
+ export declare const _internals: {
33
+ base64ToBytes: typeof base64ToBytes;
34
+ bytesToBase64: typeof bytesToBase64;
35
+ isRawPcmMime: typeof isRawPcmMime;
36
+ normalizeSpeechMedia: typeof normalizeSpeechMedia;
37
+ eventType: typeof eventType;
38
+ isDeltaEvent: typeof isDeltaEvent;
39
+ isCompleteEvent: typeof isCompleteEvent;
40
+ foldDeltaPayload: typeof foldDeltaPayload;
41
+ foldPayload: typeof foldPayload;
42
+ withTap: typeof withTap;
43
+ };
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * @module
10
10
  */
11
- import { TheorumError, toErrorEvent } from '../guardrails/error.js';
11
+ import { isAbortError, TheorumError, toErrorEvent } from '../guardrails/error.js';
12
12
  import { eventsFromComplete, eventsFromDelta, extractTokenEvent, groundingFromEvent, tryStructured, } from '../kernel/engine/delta.js';
13
13
  import { tapFetch } from './google-tap.js';
14
14
  import { toInteractionsBody } from './interactions.js';
@@ -121,7 +121,7 @@ async function* streamComplete(req, transport) {
121
121
  yield toErrorEvent('missing Gemini vault bucket for Interactions');
122
122
  return;
123
123
  }
124
- const res = await fetchGemini(INTERACTIONS_URL, { method: 'POST', body: JSON.stringify(toInteractionsBody(req)) }, req.geminiBucket, withTap(req, transport));
124
+ const res = await fetchGemini(INTERACTIONS_URL, { method: 'POST', body: JSON.stringify(toInteractionsBody(req)), signal: req.signal }, req.geminiBucket, withTap(req, transport));
125
125
  if (res.status !== HTTP_OK) {
126
126
  const errorBody = await res.text().catch(() => '');
127
127
  yield toErrorEvent(`Gemini HTTP ${String(res.status)}: ${errorBody}`);
@@ -148,6 +148,9 @@ async function* streamGuarded(req, transport) {
148
148
  yield* streamComplete(req, transport);
149
149
  }
150
150
  catch (err) {
151
+ if (isAbortError(err)) {
152
+ throw err;
153
+ }
151
154
  yield toErrorEvent(err);
152
155
  }
153
156
  }
@@ -158,3 +161,16 @@ function createInteractionsProvider(transport) {
158
161
  };
159
162
  }
160
163
  export { createInteractionsProvider };
164
+ /** @internal Exported for direct unit testing only. */
165
+ export const _internals = {
166
+ base64ToBytes,
167
+ bytesToBase64,
168
+ isRawPcmMime,
169
+ normalizeSpeechMedia,
170
+ eventType,
171
+ isDeltaEvent,
172
+ isCompleteEvent,
173
+ foldDeltaPayload,
174
+ foldPayload,
175
+ withTap,
176
+ };
@@ -6,7 +6,8 @@
6
6
  *
7
7
  * @module
8
8
  */
9
- import type { ModelProvider, ProviderCompleteRequest, TurnEvent } from '../kernel/types.js';
9
+ import type { InteractionPart, ModelProvider, ProfileSpeechSpec, ProviderCompleteRequest, SpeechAudioFormat, TurnEvent } from '../kernel/types.js';
10
+ declare function bytesToBase64(bytes: Uint8Array): string;
10
11
  /** Credentials for the openAi speech path (same shape as OpenRouter chat config + voice). */
11
12
  export interface SpeechProviderConfig {
12
13
  apiKey?: string;
@@ -17,7 +18,23 @@ export interface SpeechProviderConfig {
17
18
  siteName?: string;
18
19
  fetch?: typeof fetch;
19
20
  }
21
+ declare function extractInputText(input: InteractionPart[]): string;
22
+ declare function resolveSpeechWireModel(req: ProviderCompleteRequest): string;
23
+ declare function buildHeaders(apiKey: string, config: SpeechProviderConfig): Record<string, string>;
24
+ declare function buildPayload(req: ProviderCompleteRequest, text: string, speech: ProfileSpeechSpec | undefined, configVoice?: string): Record<string, unknown>;
25
+ declare function requestSpeech(apiKey: string, text: string, req: ProviderCompleteRequest, config: SpeechProviderConfig): Promise<Response>;
26
+ declare function yieldSpeechSuccess(rawBytes: Uint8Array, text: string, format: SpeechAudioFormat): Generator<TurnEvent>;
20
27
  declare function streamSpeech(req: ProviderCompleteRequest, config?: SpeechProviderConfig): AsyncGenerator<TurnEvent>;
21
28
  /** Internal ModelProvider for openAi speech roles. */
22
29
  declare function createSpeechProvider(config?: SpeechProviderConfig): ModelProvider;
23
30
  export { createSpeechProvider, streamSpeech };
31
+ /** @internal Exported for direct unit testing only. */
32
+ export declare const _internals: {
33
+ bytesToBase64: typeof bytesToBase64;
34
+ extractInputText: typeof extractInputText;
35
+ resolveSpeechWireModel: typeof resolveSpeechWireModel;
36
+ buildHeaders: typeof buildHeaders;
37
+ buildPayload: typeof buildPayload;
38
+ requestSpeech: typeof requestSpeech;
39
+ yieldSpeechSuccess: typeof yieldSpeechSuccess;
40
+ };
@@ -68,6 +68,7 @@ async function requestSpeech(apiKey, text, req, config) {
68
68
  method: 'POST',
69
69
  headers: buildHeaders(apiKey, config),
70
70
  body: JSON.stringify(buildPayload(req, text, req.speech, config.voice)),
71
+ signal: req.signal,
71
72
  });
72
73
  }
73
74
  function* yieldSpeechSuccess(rawBytes, text, format) {
@@ -123,3 +124,13 @@ function createSpeechProvider(config = {}) {
123
124
  };
124
125
  }
125
126
  export { createSpeechProvider, streamSpeech };
127
+ /** @internal Exported for direct unit testing only. */
128
+ export const _internals = {
129
+ bytesToBase64,
130
+ extractInputText,
131
+ resolveSpeechWireModel,
132
+ buildHeaders,
133
+ buildPayload,
134
+ requestSpeech,
135
+ yieldSpeechSuccess,
136
+ };