theorum 0.1.7 → 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.
@@ -22,8 +22,15 @@ export interface CreateProviderOptions {
22
22
  voice?: string;
23
23
  };
24
24
  }
25
+ declare function isSpeechRole(profile: Profile): boolean;
25
26
  /**
26
27
  * Create a `ModelProvider` for a profile.
27
28
  * One call: protocol/provider (and speech role) pick the transport.
28
29
  */
29
30
  export declare function createProvider(profile: Profile, options?: CreateProviderOptions): ModelProvider;
31
+ /** @internal Exported for direct unit testing only. */
32
+ export declare const _internals: {
33
+ isSpeechRole: typeof isSpeechRole;
34
+ createProvider: typeof createProvider;
35
+ };
36
+ export {};
@@ -36,3 +36,5 @@ export function createProvider(profile, options = {}) {
36
36
  }
37
37
  throw new TheorumError(`createProvider: unsupported protocol/provider pair '${protocol}'/'${provider}'`);
38
38
  }
39
+ /** @internal Exported for direct unit testing only. */
40
+ export const _internals = { isSpeechRole, createProvider };
@@ -1,2 +1,16 @@
1
+ declare function isImageBlob(rec: Record<string, unknown>): boolean;
2
+ declare function scrubEntry(rec: Record<string, unknown>, key: string, nested: unknown): Promise<[string, unknown]>;
3
+ declare function scrubRecord(rec: Record<string, unknown>): Promise<Record<string, unknown>>;
4
+ declare function scrubGemini(value: unknown): Promise<unknown>;
5
+ declare function redactCanaryInTree(value: unknown, canary: string): unknown;
1
6
  declare function tapeGemini(value: unknown, canary: string): Promise<unknown>;
2
7
  export { tapeGemini };
8
+ /** @internal Exported for direct unit testing only. */
9
+ export declare const _internals: {
10
+ isImageBlob: typeof isImageBlob;
11
+ scrubEntry: typeof scrubEntry;
12
+ scrubRecord: typeof scrubRecord;
13
+ scrubGemini: typeof scrubGemini;
14
+ redactCanaryInTree: typeof redactCanaryInTree;
15
+ tapeGemini: typeof tapeGemini;
16
+ };
@@ -44,3 +44,12 @@ async function tapeGemini(value, canary) {
44
44
  return redactCanaryInTree(await scrubGemini(value), canary);
45
45
  }
46
46
  export { tapeGemini };
47
+ /** @internal Exported for direct unit testing only. */
48
+ export const _internals = {
49
+ isImageBlob,
50
+ scrubEntry,
51
+ scrubRecord,
52
+ scrubGemini,
53
+ redactCanaryInTree,
54
+ tapeGemini,
55
+ };
@@ -1,3 +1,13 @@
1
1
  import type { ProviderCompleteRequest } from '../kernel/types.js';
2
+ declare function tapeHeaderValue(key: string, value: string): string;
3
+ declare function tapeHeaders(headers?: HeadersInit): Record<string, string>;
4
+ declare function throwRow(err: unknown): Record<string, unknown>;
2
5
  declare function tapFetch(tap: ProviderCompleteRequest['tapGemini'], send?: typeof fetch): typeof fetch;
3
6
  export { tapFetch };
7
+ /** @internal Exported for direct unit testing only. */
8
+ export declare const _internals: {
9
+ tapeHeaderValue: typeof tapeHeaderValue;
10
+ tapeHeaders: typeof tapeHeaders;
11
+ throwRow: typeof throwRow;
12
+ tapFetch: typeof tapFetch;
13
+ };
@@ -46,3 +46,5 @@ function tapFetch(tap, send = fetch) {
46
46
  };
47
47
  }
48
48
  export { tapFetch };
49
+ /** @internal Exported for direct unit testing only. */
50
+ export const _internals = { tapeHeaderValue, tapeHeaders, throwRow, tapFetch };
@@ -1,5 +1,41 @@
1
- import type { ProviderCompleteRequest } from '../kernel/types.js';
1
+ import type { InteractionPart, ProviderCompleteRequest } from '../kernel/types.js';
2
2
  declare function camelToSnake(key: string): string;
3
+ declare function toGoogleValue(value: unknown): unknown;
4
+ declare function wirePart(part: InteractionPart): Record<string, string>;
5
+ declare function userInputStep(parts: InteractionPart[]): {
6
+ type: string;
7
+ content: Record<string, string>[];
8
+ };
9
+ declare function historyStep(msg: import('../kernel/types.js').TurnHistoryMessage): {
10
+ type: string;
11
+ content: Record<string, string>[];
12
+ };
13
+ declare function systemHoldsUserInput(system: string, parts: InteractionPart[]): boolean;
14
+ declare function jsonResponseFormat(schema: Record<string, unknown>): unknown[];
15
+ declare function attachResponseFormat(req: ProviderCompleteRequest, camel: Record<string, unknown>): void;
16
+ declare function attachSpeechConfig(req: ProviderCompleteRequest, generationConfig: Record<string, unknown>): void;
17
+ declare function inputStepsFromRequest(req: ProviderCompleteRequest): {
18
+ type: string;
19
+ content: Record<string, string>[];
20
+ }[];
21
+ declare function applyOptionalRequestFields(req: ProviderCompleteRequest, camel: Record<string, unknown>): void;
22
+ declare function baseInteractionsBody(req: ProviderCompleteRequest): Record<string, unknown>;
3
23
  /** Interactions REST body for one complete() call (Google snake_case keys). */
4
24
  declare function toInteractionsBody(req: ProviderCompleteRequest): Record<string, unknown>;
5
25
  export { camelToSnake, toInteractionsBody };
26
+ /** @internal Exported for direct unit testing only. */
27
+ export declare const _internals: {
28
+ camelToSnake: typeof camelToSnake;
29
+ toGoogleValue: typeof toGoogleValue;
30
+ wirePart: typeof wirePart;
31
+ userInputStep: typeof userInputStep;
32
+ historyStep: typeof historyStep;
33
+ systemHoldsUserInput: typeof systemHoldsUserInput;
34
+ jsonResponseFormat: typeof jsonResponseFormat;
35
+ attachResponseFormat: typeof attachResponseFormat;
36
+ attachSpeechConfig: typeof attachSpeechConfig;
37
+ inputStepsFromRequest: typeof inputStepsFromRequest;
38
+ applyOptionalRequestFields: typeof applyOptionalRequestFields;
39
+ baseInteractionsBody: typeof baseInteractionsBody;
40
+ toInteractionsBody: typeof toInteractionsBody;
41
+ };
@@ -151,3 +151,19 @@ function toInteractionsBody(req) {
151
151
  return toGoogleValue(camel);
152
152
  }
153
153
  export { camelToSnake, toInteractionsBody };
154
+ /** @internal Exported for direct unit testing only. */
155
+ export const _internals = {
156
+ camelToSnake,
157
+ toGoogleValue,
158
+ wirePart,
159
+ userInputStep,
160
+ historyStep,
161
+ systemHoldsUserInput,
162
+ jsonResponseFormat,
163
+ attachResponseFormat,
164
+ attachSpeechConfig,
165
+ inputStepsFromRequest,
166
+ applyOptionalRequestFields,
167
+ baseInteractionsBody,
168
+ toInteractionsBody,
169
+ };
@@ -13,7 +13,26 @@ interface GeminiTransport {
13
13
  wait?: (ms: number) => Promise<void>;
14
14
  fetch?: typeof fetch;
15
15
  }
16
+ declare function waitDefault(ms: number): Promise<void>;
17
+ declare function isQuota(err: unknown): boolean;
18
+ declare function isTransientHttp(status: number): boolean;
19
+ declare function isTransientThrown(err: unknown): boolean;
20
+ declare function requireKey(vault: GeminiVault, bucket: GeminiBucket): string;
21
+ declare function backoffMs(attempt: number): number;
22
+ declare function canOverflow(bucket: GeminiBucket, vault: GeminiVault, primary: string): string | undefined;
16
23
  declare function withGeminiKey<T>(bucket: GeminiBucket, run: (apiKey: string) => Promise<T>, transport: GeminiTransport): Promise<T>;
24
+ declare function withApiKey(init: RequestInit, apiKey: string): RequestInit;
17
25
  declare function fetchGemini(url: string, init: RequestInit, bucket: GeminiBucket, transport: GeminiTransport): Promise<Response>;
18
26
  export type { GeminiTransport, GeminiVault };
19
27
  export { fetchGemini, withGeminiKey };
28
+ /** @internal Exported for direct unit testing only. */
29
+ export declare const _internals: {
30
+ waitDefault: typeof waitDefault;
31
+ isQuota: typeof isQuota;
32
+ isTransientHttp: typeof isTransientHttp;
33
+ isTransientThrown: typeof isTransientThrown;
34
+ requireKey: typeof requireKey;
35
+ backoffMs: typeof backoffMs;
36
+ canOverflow: typeof canOverflow;
37
+ withApiKey: typeof withApiKey;
38
+ };
@@ -137,3 +137,14 @@ async function fetchGemini(url, init, bucket, transport) {
137
137
  return last;
138
138
  }
139
139
  export { fetchGemini, withGeminiKey };
140
+ /** @internal Exported for direct unit testing only. */
141
+ export const _internals = {
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
- export type { OpenRouterConfig, OpenRouterWireIds };
32
- export { resolveOpenRouterModel, toOpenRouterPayload };
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 plugins = req.builtins
171
- .map((id) => getTool(id)?.openRouterPlugin)
172
- .filter((id) => Boolean(id))
173
- .map((id) => ({ id }));
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
- export { resolveOpenRouterModel, toOpenRouterPayload };
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 };
@@ -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
+ };
@@ -11,9 +11,8 @@ 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 { 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,27 +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 },
502
411
  abortSignal: req.signal,
503
412
  onError: () => undefined,
504
413
  };
505
414
  }
506
415
  async function* yieldAiSdkStream(req, acc, context) {
507
416
  const result = streamText(streamTextOptions(req, context));
508
- for await (const part of result.fullStream) {
417
+ for await (const part of result.stream) {
509
418
  if (part.type === 'raw') {
510
419
  req.tapGemini?.(rawRecord(part.rawValue) ?? { rawValue: part.rawValue });
511
420
  for (const event of rawEvents(part.rawValue, acc)) {
@@ -518,16 +427,6 @@ async function* yieldAiSdkStream(req, acc, context) {
518
427
  }
519
428
  }
520
429
  }
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
430
  function missingOpenRouterKey() {
532
431
  return toErrorEvent('missing OpenRouter API key');
533
432
  }
@@ -541,14 +440,12 @@ async function* streamOpenRouter(req, config) {
541
440
  const context = createStreamContext(req, config, apiKey);
542
441
  try {
543
442
  yield* yieldAiSdkStream(req, acc, context);
544
- yield* yieldCapturedRawEvents(context, acc);
545
443
  yield* finalEvents(req, acc);
546
444
  }
547
445
  catch (err) {
548
446
  if (isAbortError(err)) {
549
447
  throw err;
550
448
  }
551
- yield* yieldCapturedRawEventsUnchecked(context);
552
449
  yield toErrorEvent(err);
553
450
  }
554
451
  }
@@ -564,16 +461,34 @@ function* finalEvents(req, acc) {
564
461
  }
565
462
  yield { type: 'done' };
566
463
  }
567
- function providerOptionsFor(req) {
568
- 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)
569
469
  return undefined;
570
- }
571
470
  return {
572
- openrouter: {
573
- reasoning: { effort: req.thinking },
471
+ type: 'json_schema',
472
+ json_schema: {
473
+ name: String(req.structured),
474
+ strict: true,
475
+ schema: spec.jsonSchema,
574
476
  },
575
477
  };
576
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
+ }
577
492
  function openRouterHeaders(config) {
578
493
  const headers = {};
579
494
  if (config.siteUrl) {
@@ -591,3 +506,49 @@ function createOpenRouterProvider(config = {}) {
591
506
  };
592
507
  }
593
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
+ };
@@ -161,3 +161,16 @@ function createInteractionsProvider(transport) {
161
161
  };
162
162
  }
163
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
+ };
@@ -124,3 +124,13 @@ function createSpeechProvider(config = {}) {
124
124
  };
125
125
  }
126
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
+ };
@@ -1,7 +1,15 @@
1
1
  declare const INTERACTIONS_URL = "https://generativelanguage.googleapis.com/v1beta/interactions?alt=sse";
2
+ declare function asObject(parsed: unknown): Record<string, unknown> | undefined;
3
+ declare function dataRecord(raw: string, sseEvent: string): Record<string, unknown>;
2
4
  declare function takeSsePayloads(buffer: string, pendingEvent?: string): {
3
5
  rest: string;
4
6
  payloads: Record<string, unknown>[];
5
7
  pendingEvent: string;
6
8
  };
7
9
  export { INTERACTIONS_URL, takeSsePayloads };
10
+ /** @internal Exported for direct unit testing only. */
11
+ export declare const _internals: {
12
+ asObject: typeof asObject;
13
+ dataRecord: typeof dataRecord;
14
+ takeSsePayloads: typeof takeSsePayloads;
15
+ };
@@ -51,3 +51,5 @@ function takeSsePayloads(buffer, pendingEvent = '') {
51
51
  return { rest, payloads, pendingEvent: sseEvent };
52
52
  }
53
53
  export { INTERACTIONS_URL, takeSsePayloads };
54
+ /** @internal Exported for direct unit testing only. */
55
+ export const _internals = { asObject, dataRecord, takeSsePayloads };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Structured-output streaming helpers (incomplete JSON text buffers).
3
+ *
4
+ * @module
5
+ */
6
+ import "../../_dnt.polyfills.js";
7
+ export { readStreamingJsonStringField } from './readStreamingJsonStringField.js';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Structured-output streaming helpers (incomplete JSON text buffers).
3
+ *
4
+ * @module
5
+ */
6
+ import "../../_dnt.polyfills.js";
7
+ export { readStreamingJsonStringField } from './readStreamingJsonStringField.js';
@@ -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.7",
3
+ "version": "0.1.9",
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": "1.5.4",
58
- "ai": "5.0.244",
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": {