theorum 0.1.2 → 0.1.3

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 (99) hide show
  1. package/README.md +62 -27
  2. package/docs/AGENT_PROFILE_CONTRACT.md +45 -18
  3. package/docs/CLI_SPEC.md +2 -2
  4. package/docs/SECRETS.md +27 -22
  5. package/esm/_dnt.polyfills.d.ts +99 -0
  6. package/esm/_dnt.polyfills.js +130 -1
  7. package/esm/mod.d.ts +19 -3
  8. package/esm/mod.js +17 -3
  9. package/esm/src/cli/commands/profile.d.ts +2 -0
  10. package/esm/src/cli/commands/profile.js +53 -0
  11. package/esm/src/cli/commands/run.d.ts +10 -0
  12. package/esm/src/cli/commands/run.js +55 -0
  13. package/esm/src/cli/commands/test.d.ts +15 -0
  14. package/esm/src/cli/commands/test.js +140 -0
  15. package/esm/src/cli/index.d.ts +4 -0
  16. package/esm/src/cli/index.js +148 -0
  17. package/esm/src/cli/matrix/fixtures.d.ts +13 -0
  18. package/esm/src/cli/matrix/fixtures.js +77 -0
  19. package/esm/src/cli/matrix/synthesizer.d.ts +29 -0
  20. package/esm/src/cli/matrix/synthesizer.js +187 -0
  21. package/esm/src/guardrails/error.d.ts +1 -3
  22. package/esm/src/guardrails/error.js +4 -6
  23. package/esm/src/guardrails/mod.d.ts +1 -1
  24. package/esm/src/guardrails/mod.js +1 -1
  25. package/esm/src/guardrails/sanitize.js +1 -1
  26. package/esm/src/host/mint-trace.d.ts +27 -0
  27. package/esm/src/host/mint-trace.js +33 -0
  28. package/esm/src/host/mod.d.ts +13 -0
  29. package/esm/src/host/mod.js +12 -0
  30. package/esm/src/host/reply.d.ts +14 -0
  31. package/esm/src/host/reply.js +27 -0
  32. package/esm/src/kernel/engine/delta.js +31 -4
  33. package/esm/src/kernel/engine/repair.d.ts +1 -3
  34. package/esm/src/kernel/engine/repair.js +1 -1
  35. package/esm/src/kernel/engine/runner/gates.d.ts +4 -0
  36. package/esm/src/kernel/engine/runner/gates.js +206 -0
  37. package/esm/src/kernel/engine/runner/mod.d.ts +14 -0
  38. package/esm/src/kernel/engine/runner/mod.js +103 -0
  39. package/esm/src/kernel/engine/runner/state.d.ts +16 -0
  40. package/esm/src/kernel/engine/runner/state.js +8 -0
  41. package/esm/src/kernel/engine/runner/steps.d.ts +15 -0
  42. package/esm/src/kernel/engine/runner/steps.js +119 -0
  43. package/esm/src/kernel/engine/runner/stream.d.ts +11 -0
  44. package/esm/src/kernel/engine/runner/stream.js +65 -0
  45. package/esm/src/kernel/engine/runner/tokens.d.ts +3 -0
  46. package/esm/src/kernel/engine/runner/tokens.js +38 -0
  47. package/esm/src/kernel/engine/runner/tools.d.ts +13 -0
  48. package/esm/src/kernel/engine/runner/tools.js +197 -0
  49. package/esm/src/kernel/engine/runner.d.ts +1 -9
  50. package/esm/src/kernel/engine/runner.js +1 -726
  51. package/esm/src/kernel/mod.d.ts +3 -1
  52. package/esm/src/kernel/mod.js +2 -1
  53. package/esm/src/{providers → kernel/registry}/attachments.d.ts +2 -3
  54. package/esm/src/{providers → kernel/registry}/attachments.js +6 -6
  55. package/esm/src/kernel/registry/catalog.d.ts +28 -21
  56. package/esm/src/kernel/registry/catalog.js +55 -133
  57. package/esm/src/kernel/registry/ingress.d.ts +13 -0
  58. package/esm/src/kernel/registry/ingress.js +163 -0
  59. package/esm/src/kernel/registry/profiles.d.ts +2 -4
  60. package/esm/src/kernel/registry/profiles.js +14 -5
  61. package/esm/src/kernel/registry/provider-request.d.ts +4 -0
  62. package/esm/src/kernel/registry/provider-request.js +25 -0
  63. package/esm/src/kernel/registry/resolve.d.ts +0 -1
  64. package/esm/src/kernel/registry/resolve.js +35 -53
  65. package/esm/src/kernel/registry/tools.js +2 -2
  66. package/esm/src/kernel/registry/vault.d.ts +11 -0
  67. package/esm/src/kernel/registry/vault.js +18 -0
  68. package/esm/src/kernel/types.d.ts +102 -78
  69. package/esm/src/observability/trace-attach.js +6 -20
  70. package/esm/src/presets/google.d.ts +50 -0
  71. package/esm/src/presets/google.js +96 -0
  72. package/esm/src/presets/mod.d.ts +11 -0
  73. package/esm/src/presets/mod.js +10 -0
  74. package/esm/src/providers/create-provider.d.ts +29 -0
  75. package/esm/src/providers/create-provider.js +38 -0
  76. package/esm/src/providers/gemini-tape.d.ts +1 -2
  77. package/esm/src/providers/gemini-tape.js +1 -1
  78. package/esm/src/providers/interactions.js +41 -16
  79. package/esm/src/{guardrails → providers}/keys.d.ts +10 -3
  80. package/esm/src/{guardrails → providers}/keys.js +10 -13
  81. package/esm/src/providers/mod.d.ts +4 -7
  82. package/esm/src/providers/mod.js +2 -5
  83. package/esm/src/providers/openrouter-mod.d.ts +13 -0
  84. package/esm/src/providers/openrouter-mod.js +12 -0
  85. package/esm/src/providers/openrouter-payload.d.ts +11 -3
  86. package/esm/src/providers/openrouter-payload.js +28 -26
  87. package/esm/src/providers/openrouter.d.ts +5 -7
  88. package/esm/src/providers/openrouter.js +488 -231
  89. package/esm/src/providers/pcm.d.ts +7 -0
  90. package/esm/src/providers/pcm.js +35 -0
  91. package/esm/src/providers/provider.d.ts +3 -1
  92. package/esm/src/providers/provider.js +45 -2
  93. package/esm/src/providers/speech.d.ts +23 -0
  94. package/esm/src/providers/speech.js +125 -0
  95. package/package.json +20 -3
  96. package/esm/src/providers/media.d.ts +0 -5
  97. package/esm/src/providers/media.js +0 -125
  98. package/esm/src/providers/tts.d.ts +0 -24
  99. package/esm/src/providers/tts.js +0 -144
@@ -1,95 +1,208 @@
1
1
  /**
2
- * OpenRouter-compatible streaming provider adapter.
2
+ * OpenRouter provider adapter powered by Vercel AI SDK Core.
3
3
  *
4
- * This module maps THEORUM requests to OpenAI-style chat completions, including
5
- * reasoning deltas, tool calls, citations, structured output, and token usage.
4
+ * THEORUM keeps the public `ModelProvider` and `TurnEvent` contract; AI SDK
5
+ * owns the OpenRouter call, stream parsing, provider compatibility, and tool
6
+ * call normalization.
6
7
  *
7
8
  * @module
8
9
  */
9
- import "../../_dnt.polyfills.js";
10
- import { publicError, TheorumError } from '../guardrails/error.js';
10
+ import { createOpenRouter } from '@openrouter/ai-sdk-provider';
11
+ import { jsonSchema, streamText, tool, } from 'ai';
12
+ import { publicError } from '../guardrails/error.js';
11
13
  import { tryStructured } from '../kernel/engine/delta.js';
14
+ import { getTool } from '../kernel/registry/catalog.js';
12
15
  import { resolveOpenRouterModel, toOpenRouterPayload, } from './openrouter-payload.js';
13
16
  import { takeSsePayloads } from './sse.js';
14
- const HTTP_OK = 200;
15
- const DEFAULT_OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
16
- async function* readSseLines(res) {
17
- if (!res.body) {
18
- throw new TheorumError('empty OpenRouter stream');
17
+ function trimApiKey(explicitKey) {
18
+ if (explicitKey?.trim()) {
19
+ return explicitKey.trim();
19
20
  }
20
- const reader = res.body.getReader();
21
- const decoder = new TextDecoder();
22
- let buffer = '';
23
- let pendingEvent = '';
24
- while (true) {
25
- const { done, value } = await reader.read();
26
- if (done) {
27
- break;
28
- }
29
- buffer += decoder.decode(value, { stream: true });
30
- const taken = takeSsePayloads(buffer, pendingEvent);
31
- buffer = taken.rest;
32
- pendingEvent = taken.pendingEvent;
33
- for (const payload of taken.payloads) {
34
- yield payload;
35
- }
21
+ return undefined;
22
+ }
23
+ function createAccumulator() {
24
+ return {
25
+ text: '',
26
+ evidenceSeen: false,
27
+ emittedTokens: false,
28
+ errored: false,
29
+ toolInputs: new Map(),
30
+ };
31
+ }
32
+ function mediaPart(part) {
33
+ if (part.type === 'text') {
34
+ return { type: 'text', text: part.text };
35
+ }
36
+ if (part.type === 'image') {
37
+ return {
38
+ type: 'image',
39
+ image: `data:${part.mimeType};base64,${part.data}`,
40
+ };
36
41
  }
42
+ return { type: 'file', mediaType: part.mimeType, data: part.data };
37
43
  }
38
- function extractReasoning(delta) {
39
- if (typeof delta.reasoning === 'string') {
40
- return delta.reasoning;
44
+ function contentFromParts(parts) {
45
+ if (parts.every((part) => part.type === 'text')) {
46
+ return parts
47
+ .map((part) => (part.type === 'text' ? part.text : ''))
48
+ .filter(Boolean)
49
+ .join('\n');
50
+ }
51
+ return parts.map(mediaPart);
52
+ }
53
+ function parseToolInput(raw) {
54
+ try {
55
+ return JSON.parse(raw);
56
+ }
57
+ catch {
58
+ return { _raw: raw };
41
59
  }
42
- if (typeof delta.thinking === 'string') {
43
- return delta.thinking;
60
+ }
61
+ function stringDefault(value, fallback) {
62
+ return value === undefined ? fallback : value;
63
+ }
64
+ function optionalSingle(value) {
65
+ return value === undefined ? undefined : [value];
66
+ }
67
+ function fallbackToolCallId(name) {
68
+ return `call_${stringDefault(name, 'tool')}`;
69
+ }
70
+ function toolResultContent(msg) {
71
+ const toolName = stringDefault(msg.name, 'tool');
72
+ return {
73
+ role: 'tool',
74
+ content: [
75
+ {
76
+ type: 'tool-result',
77
+ toolCallId: stringDefault(msg.tool_call_id, fallbackToolCallId(msg.name)),
78
+ toolName,
79
+ output: { type: 'text', value: stringDefault(msg.content, '') },
80
+ },
81
+ ],
82
+ };
83
+ }
84
+ function assistantToolCallContent(msg) {
85
+ if (!msg.tool_calls || msg.tool_calls.length === 0) {
86
+ return null;
44
87
  }
45
- return undefined;
88
+ return {
89
+ role: 'assistant',
90
+ content: msg.tool_calls.map((call) => ({
91
+ type: 'tool-call',
92
+ toolCallId: call.id,
93
+ toolName: call.function.name,
94
+ input: parseToolInput(call.function.arguments),
95
+ })),
96
+ };
46
97
  }
47
- function applyToolCallObject(callObj, toolMap) {
48
- let index = 0;
49
- if (typeof callObj.index === 'number') {
50
- index = callObj.index;
98
+ function historyMessage(msg) {
99
+ if (msg.role === 'tool') {
100
+ return toolResultContent(msg);
51
101
  }
52
- const current = toolMap.get(index) ?? { index, arguments: '' };
53
- if (typeof callObj.id === 'string') {
54
- current.id = callObj.id;
102
+ return assistantToolCallContent(msg) || contentHistoryMessage(msg);
103
+ }
104
+ function contentFromOptionalParts(parts, text) {
105
+ if (!parts || parts.length === 0) {
106
+ return stringDefault(text, '');
55
107
  }
56
- const fn = callObj.function;
57
- if (fn) {
58
- if (typeof fn.name === 'string') {
59
- current.name = (current.name ?? '') + fn.name;
60
- }
61
- if (typeof fn.arguments === 'string') {
62
- current.arguments += fn.arguments;
63
- }
108
+ return contentFromParts(parts);
109
+ }
110
+ function contentHistoryMessage(msg) {
111
+ const content = contentFromOptionalParts(msg.parts, msg.content);
112
+ return { role: msg.role, content };
113
+ }
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;
64
128
  }
65
- toolMap.set(index, current);
129
+ return [{ title, uri: url, type: 'web' }];
130
+ }
131
+ function sourceEvent(part) {
132
+ const raw = part;
133
+ const url = sourceUrl(part);
134
+ const title = sourceTitle(part);
135
+ return {
136
+ type: 'evidence',
137
+ evidence: {
138
+ provider: 'openrouter',
139
+ raw,
140
+ citations: optionalSingle(url),
141
+ sources: sourceList(title, url),
142
+ },
143
+ };
66
144
  }
67
- function processToolCalls(rawCalls, toolMap) {
68
- for (const rawCall of rawCalls) {
69
- if (rawCall && typeof rawCall === 'object') {
70
- applyToolCallObject(rawCall, toolMap);
145
+ function buildMessages(req) {
146
+ const messages = [];
147
+ for (const msg of req.history ?? []) {
148
+ const wired = historyMessage(msg);
149
+ if (wired) {
150
+ messages.push(wired);
71
151
  }
72
152
  }
153
+ if (req.input.length > 0) {
154
+ messages.push({ role: 'user', content: contentFromParts(req.input) });
155
+ }
156
+ return messages;
73
157
  }
74
- function processDelta(delta, acc) {
75
- const events = [];
76
- const reasoning = extractReasoning(delta);
77
- if (reasoning) {
78
- events.push({ type: 'thought', text: reasoning });
158
+ function schemaForTool(decl) {
159
+ return decl.parameters ?? { type: 'object', properties: {}, additionalProperties: true };
160
+ }
161
+ function buildTools(dynamicTools) {
162
+ if (!dynamicTools || dynamicTools.length === 0) {
163
+ return undefined;
79
164
  }
80
- if (typeof delta.content === 'string') {
81
- acc.text += delta.content;
82
- events.push({ type: 'text', text: delta.content });
165
+ const tools = {};
166
+ for (const decl of dynamicTools) {
167
+ tools[decl.name] = tool({
168
+ description: decl.description ?? '',
169
+ inputSchema: jsonSchema(schemaForTool(decl)),
170
+ });
83
171
  }
84
- if (Array.isArray(delta.tool_calls)) {
85
- processToolCalls(delta.tool_calls, acc.toolCalls);
172
+ return tools;
173
+ }
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
+ function openRouterSettings(req) {
182
+ const plugins = openRouterPlugins(req);
183
+ if (!plugins) {
184
+ return undefined;
86
185
  }
87
- const evidence = evidenceFromRecord(delta);
88
- if (evidence && !acc.evidenceSeen) {
89
- acc.evidenceSeen = true;
90
- events.push(evidence);
186
+ return { plugins };
187
+ }
188
+ function tokensFromUsage(usage) {
189
+ const input = usage.inputTokens ?? 0;
190
+ const output = usage.outputTokens ?? 0;
191
+ const total = usage.totalTokens ?? input + output;
192
+ if (input === 0 && output === 0 && total === 0) {
193
+ return undefined;
91
194
  }
92
- return events;
195
+ return {
196
+ input,
197
+ output,
198
+ total,
199
+ };
200
+ }
201
+ function rawRecord(value) {
202
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
203
+ return value;
204
+ }
205
+ return undefined;
93
206
  }
94
207
  function stringArray(value) {
95
208
  if (!Array.isArray(value)) {
@@ -98,232 +211,376 @@ function stringArray(value) {
98
211
  const out = value.filter((item) => typeof item === 'string');
99
212
  return out.length > 0 ? out : undefined;
100
213
  }
101
- function recordField(record, key) {
102
- const value = record[key];
103
- if (value && typeof value === 'object' && !Array.isArray(value)) {
104
- return value;
105
- }
106
- return undefined;
214
+ function metadataRecord(raw, key) {
215
+ return rawRecord(raw[key]);
216
+ }
217
+ function citationCandidates(raw) {
218
+ const openrouter = metadataRecord(raw, 'openrouter') ?? {};
219
+ return [
220
+ raw.citations,
221
+ metadataRecord(raw, 'providerMetadata')?.citations,
222
+ metadataRecord(raw, 'provider_metadata')?.citations,
223
+ openrouter.citations,
224
+ metadataRecord(openrouter, 'providerMetadata')?.citations,
225
+ metadataRecord(openrouter, 'provider_metadata')?.citations,
226
+ ];
107
227
  }
108
- function firstEvidenceRaw(record) {
109
- for (const key of [
110
- 'annotations',
111
- 'citations',
112
- 'search_results',
113
- 'searchResults',
114
- 'provider_metadata',
115
- 'providerMetadata',
116
- ]) {
117
- const value = record[key];
118
- if (value !== undefined) {
119
- return value;
228
+ function nestedCitations(raw) {
229
+ for (const candidate of citationCandidates(raw)) {
230
+ const citations = stringArray(candidate);
231
+ if (citations) {
232
+ return citations;
120
233
  }
121
234
  }
122
235
  return undefined;
123
236
  }
124
- function evidenceCitations(record) {
125
- return (stringArray(record.citations) ??
126
- stringArray(recordField(record, 'provider_metadata')?.citations) ??
127
- stringArray(recordField(record, 'providerMetadata')?.citations));
128
- }
129
- function evidenceAnnotations(record) {
130
- return Array.isArray(record.annotations) ? record.annotations : undefined;
237
+ function metadataAnnotations(raw) {
238
+ const openrouter = metadataRecord(raw, 'openrouter');
239
+ if (Array.isArray(raw.annotations)) {
240
+ return raw.annotations;
241
+ }
242
+ if (Array.isArray(openrouter?.annotations)) {
243
+ return openrouter.annotations;
244
+ }
245
+ return undefined;
131
246
  }
132
- function evidenceFromRecord(record) {
133
- if (firstEvidenceRaw(record) === undefined) {
247
+ function evidenceFromMetadata(metadata, acc) {
248
+ const raw = rawRecord(metadata);
249
+ if (!raw || acc.evidenceSeen) {
250
+ return undefined;
251
+ }
252
+ const citations = nestedCitations(raw);
253
+ const annotations = metadataAnnotations(raw);
254
+ if (!citations && !annotations) {
134
255
  return undefined;
135
256
  }
257
+ acc.evidenceSeen = true;
136
258
  return {
137
259
  type: 'evidence',
138
- evidence: {
139
- provider: 'openrouter',
140
- raw: record,
141
- citations: evidenceCitations(record),
142
- annotations: evidenceAnnotations(record),
143
- },
260
+ evidence: { provider: 'openrouter', raw, citations, annotations },
144
261
  };
145
262
  }
146
- function parseUsage(usageRaw) {
147
- if (!usageRaw || typeof usageRaw !== 'object') {
263
+ function toolArguments(input) {
264
+ if (input && typeof input === 'object' && !Array.isArray(input)) {
265
+ return input;
266
+ }
267
+ if (input === undefined) {
148
268
  return undefined;
149
269
  }
150
- const u = usageRaw;
151
- let input = 0;
152
- if (typeof u.prompt_tokens === 'number') {
153
- input = u.prompt_tokens;
270
+ return { value: input };
271
+ }
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));
154
280
  }
155
- let output = 0;
156
- if (typeof u.completion_tokens === 'number') {
157
- output = u.completion_tokens;
281
+ return fromInput;
282
+ }
283
+ function toolResultData(output) {
284
+ return rawRecord(output);
285
+ }
286
+ function rawThoughtEvent(raw) {
287
+ const choices = raw.choices;
288
+ if (!Array.isArray(choices)) {
289
+ return undefined;
158
290
  }
159
- let total = input + output;
160
- if (typeof u.total_tokens === 'number') {
161
- total = u.total_tokens;
291
+ for (const choice of choices) {
292
+ const delta = rawRecord(rawRecord(choice)?.delta);
293
+ if (typeof delta?.thinking === 'string') {
294
+ return { type: 'thought', text: delta.thinking };
295
+ }
162
296
  }
163
- return { input, output, total };
297
+ return undefined;
164
298
  }
165
- function parseToolArgs(rawArgs) {
166
- try {
167
- return JSON.parse(rawArgs);
299
+ function rawChoiceMessageEvidence(raw, acc) {
300
+ const choices = raw.choices;
301
+ if (!Array.isArray(choices)) {
302
+ return undefined;
168
303
  }
169
- catch {
170
- return { _raw: rawArgs };
304
+ for (const choice of choices) {
305
+ const message = rawRecord(rawRecord(choice)?.message);
306
+ if (!message) {
307
+ continue;
308
+ }
309
+ const evidence = evidenceFromMetadata(message, acc);
310
+ if (evidence) {
311
+ return evidence;
312
+ }
171
313
  }
314
+ return undefined;
172
315
  }
173
- function emitRemainingTools(acc) {
316
+ function rawEvents(raw, acc) {
317
+ const record = rawRecord(raw);
318
+ if (!record) {
319
+ return [];
320
+ }
174
321
  const events = [];
175
- for (const tool of acc.toolCalls.values()) {
176
- if (tool.name) {
177
- let args;
178
- if (tool.arguments) {
179
- args = parseToolArgs(tool.arguments);
180
- }
181
- events.push({
182
- type: 'tool',
183
- tool: {
184
- name: tool.name,
185
- arguments: args,
186
- ...(tool.id ? { id: tool.id } : {}),
187
- },
188
- });
189
- }
322
+ const thought = rawThoughtEvent(record);
323
+ if (thought) {
324
+ events.push(thought);
325
+ }
326
+ const evidence = evidenceFromMetadata(record, acc);
327
+ if (evidence) {
328
+ events.push(evidence);
329
+ }
330
+ const messageEvidence = rawChoiceMessageEvidence(record, acc);
331
+ if (messageEvidence) {
332
+ events.push(messageEvidence);
190
333
  }
191
334
  return events;
192
335
  }
193
- function buildHeaders(apiKey, config) {
194
- const headers = {
195
- 'Content-Type': 'application/json',
196
- Authorization: `Bearer ${apiKey}`,
197
- };
198
- if (config.siteUrl) {
199
- headers['HTTP-Referer'] = config.siteUrl;
336
+ async function collectRawEvents(res) {
337
+ if (!res.body) {
338
+ return [];
200
339
  }
201
- if (config.siteName) {
202
- headers['X-Title'] = config.siteName;
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
+ }
203
358
  }
204
- return headers;
359
+ return events;
205
360
  }
206
- async function postOpenRouter(req, config, apiKey) {
207
- const baseUrl = config.baseUrl ?? DEFAULT_OPENROUTER_BASE_URL;
208
- const url = `${baseUrl.replace(/\/+$/, '')}/chat/completions`;
361
+ function captureFetch(config, setCapture) {
209
362
  const fetchFn = config.fetch ?? fetch;
210
- const headers = buildHeaders(apiKey, config);
211
- const body = JSON.stringify(toOpenRouterPayload(req, config));
212
- return await fetchFn(url, { method: 'POST', headers, body });
363
+ return async (input, init) => {
364
+ const res = await fetchFn(input, init);
365
+ setCapture(collectRawEvents(res.clone()).catch(() => []));
366
+ return res;
367
+ };
213
368
  }
214
- function processChoiceDeltas(choices, acc) {
215
- if (!Array.isArray(choices)) {
216
- return [];
369
+ function toolCallEvent(part, acc) {
370
+ return {
371
+ type: 'tool',
372
+ tool: {
373
+ name: part.toolName,
374
+ arguments: toolCallArguments(part, acc),
375
+ id: part.toolCallId,
376
+ },
377
+ };
378
+ }
379
+ function toolResultEvent(part) {
380
+ return {
381
+ type: 'tool',
382
+ tool: {
383
+ name: part.toolName,
384
+ arguments: toolArguments(part.input),
385
+ result: {
386
+ status: 'ok',
387
+ data: toolResultData(part.output),
388
+ finding: typeof part.output === 'string' ? part.output : undefined,
389
+ },
390
+ id: part.toolCallId,
391
+ },
392
+ };
393
+ }
394
+ function tokenEvent(part) {
395
+ const tokens = tokensFromUsage(part.totalUsage);
396
+ return tokens ? { type: 'tokens', tokens } : undefined;
397
+ }
398
+ function providerMetadataEvent(part, acc) {
399
+ if (!('providerMetadata' in part)) {
400
+ return undefined;
217
401
  }
218
- const events = [];
219
- for (const choice of choices) {
220
- events.push(...processChoiceDelta(choice, acc));
402
+ return evidenceFromMetadata(part.providerMetadata, acc);
403
+ }
404
+ function appendToolInput(part, acc) {
405
+ if (part.type === 'tool-input-start') {
406
+ acc.toolInputs.set(part.id, '');
407
+ return true;
221
408
  }
222
- return events;
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;
223
415
  }
224
- function processChoiceDelta(choice, acc) {
225
- if (!choice || typeof choice !== 'object') {
416
+ function eventFromPart(part, acc) {
417
+ if (appendToolInput(part, acc)) {
226
418
  return [];
227
419
  }
228
- const c = choice;
229
- const events = processChoiceDeltaPayload(c.delta, acc);
230
- const evidence = evidenceFromChoiceMessage(c.message, acc);
231
- if (evidence) {
232
- events.push(evidence);
420
+ const mapped = primaryEventFromPart(part, acc);
421
+ if (mapped) {
422
+ return [mapped];
233
423
  }
234
- return events;
424
+ const evidence = providerMetadataEvent(part, acc);
425
+ return evidence ? [evidence] : [];
235
426
  }
236
- function processChoiceDeltaPayload(delta, acc) {
237
- if (!delta || typeof delta !== 'object') {
238
- return [];
427
+ function primaryEventFromPart(part, acc) {
428
+ switch (part.type) {
429
+ case 'text-delta':
430
+ acc.text += part.text;
431
+ return { type: 'text', text: part.text };
432
+ case 'reasoning-delta':
433
+ return { type: 'thought', text: part.text };
434
+ case 'tool-call':
435
+ return toolCallEvent(part, acc);
436
+ case 'tool-result':
437
+ return toolResultEvent(part);
438
+ case 'source':
439
+ return sourceEvent(part);
440
+ case 'finish':
441
+ return finishEvent(part, acc);
442
+ case 'error':
443
+ acc.errored = true;
444
+ return { type: 'error', error: publicError(part.error) };
445
+ default:
446
+ return undefined;
239
447
  }
240
- return processDelta(delta, acc);
241
448
  }
242
- function evidenceFromChoiceMessage(message, acc) {
243
- if (!message || typeof message !== 'object') {
449
+ function finishEvent(part, acc) {
450
+ if (acc.emittedTokens) {
244
451
  return undefined;
245
452
  }
246
- const evidence = evidenceFromRecord(message);
247
- if (!evidence || acc.evidenceSeen) {
248
- return undefined;
453
+ const event = tokenEvent(part);
454
+ if (event) {
455
+ acc.emittedTokens = true;
249
456
  }
250
- acc.evidenceSeen = true;
251
- return evidence;
457
+ return event;
252
458
  }
253
- function* yieldRemainingStreamEvents(req, acc) {
254
- for (const ev of emitRemainingTools(acc)) {
255
- yield ev;
256
- }
257
- if (req.structured && acc.text) {
258
- const structured = tryStructured(acc.text);
259
- if (structured) {
260
- yield structured;
459
+ function emitRawEvents(rawCapture, acc) {
460
+ return (rawCapture ?? Promise.resolve([])).then((events) => events.filter((event) => {
461
+ if (event.type !== 'evidence') {
462
+ return true;
261
463
  }
262
- }
263
- yield { type: 'done' };
464
+ if (acc.evidenceSeen) {
465
+ return false;
466
+ }
467
+ acc.evidenceSeen = true;
468
+ return true;
469
+ }));
264
470
  }
265
- function evidenceFromPayload(payload, acc) {
266
- const evidence = evidenceFromRecord(payload);
267
- if (!evidence || acc.evidenceSeen) {
268
- return undefined;
471
+ function createStreamContext(req, config, apiKey) {
472
+ let rawCapture;
473
+ const openrouter = createOpenRouter({
474
+ apiKey,
475
+ baseURL: config.baseUrl,
476
+ headers: openRouterHeaders(config),
477
+ fetch: captureFetch(config, (capture) => {
478
+ rawCapture = capture;
479
+ }),
480
+ compatibility: 'strict',
481
+ });
482
+ return {
483
+ openrouter,
484
+ modelName: resolveOpenRouterModel(req.model, config.modelMap, {
485
+ apiId: req.apiId,
486
+ openRouterId: req.openRouterId,
487
+ }),
488
+ rawCapture: () => rawCapture,
489
+ };
490
+ }
491
+ function streamTextOptions(req, context) {
492
+ return {
493
+ model: context.openrouter.chat(context.modelName, openRouterSettings(req)),
494
+ system: req.system,
495
+ messages: buildMessages(req),
496
+ allowSystemInMessages: true,
497
+ temperature: req.temperature,
498
+ maxOutputTokens: req.maxOutputTokens,
499
+ tools: buildTools(req.dynamicTools),
500
+ providerOptions: providerOptionsFor(req),
501
+ includeRawChunks: true,
502
+ onError: () => undefined,
503
+ };
504
+ }
505
+ async function* yieldAiSdkStream(req, acc, context) {
506
+ const result = streamText(streamTextOptions(req, context));
507
+ for await (const part of result.fullStream) {
508
+ if (part.type === 'raw') {
509
+ req.tapGemini?.(rawRecord(part.rawValue) ?? { rawValue: part.rawValue });
510
+ for (const event of rawEvents(part.rawValue, acc)) {
511
+ yield event;
512
+ }
513
+ continue;
514
+ }
515
+ for (const event of eventFromPart(part, acc)) {
516
+ yield event;
517
+ }
269
518
  }
270
- acc.evidenceSeen = true;
271
- return evidence;
272
519
  }
273
- function* eventsFromPayload(payload, acc) {
274
- const evidence = evidenceFromPayload(payload, acc);
275
- if (evidence) {
276
- yield evidence;
520
+ async function* yieldCapturedRawEvents(context, acc) {
521
+ for (const event of await emitRawEvents(context.rawCapture(), acc)) {
522
+ yield event;
277
523
  }
278
- for (const ev of processChoiceDeltas(payload.choices, acc)) {
279
- yield ev;
524
+ }
525
+ async function* yieldCapturedRawEventsUnchecked(context) {
526
+ for (const event of await (context.rawCapture() ?? Promise.resolve([]))) {
527
+ yield event;
280
528
  }
281
529
  }
530
+ function missingOpenRouterKey() {
531
+ return { type: 'error', error: publicError('missing OpenRouter API key') };
532
+ }
282
533
  async function* streamOpenRouter(req, config) {
283
- const apiKey = resolveOpenRouterApiKey(config.apiKey);
534
+ const apiKey = trimApiKey(config.apiKey);
284
535
  if (!apiKey) {
285
- yield { type: 'error', error: publicError('missing OpenRouter API key') };
536
+ yield missingOpenRouterKey();
286
537
  return;
287
538
  }
539
+ const acc = createAccumulator();
540
+ const context = createStreamContext(req, config, apiKey);
288
541
  try {
289
- const res = await postOpenRouter(req, config, apiKey);
290
- if (res.status !== HTTP_OK) {
291
- yield { type: 'error', error: publicError(`OpenRouter HTTP ${String(res.status)}`) };
292
- return;
293
- }
294
- const acc = { text: '', toolCalls: new Map(), evidenceSeen: false };
295
- let emittedTokens = false;
296
- let sawPayload = false;
297
- for await (const payload of readSseLines(res)) {
298
- sawPayload = true;
299
- req.tapGemini?.(payload);
300
- for (const ev of eventsFromPayload(payload, acc)) {
301
- yield ev;
302
- }
303
- const usage = parseUsage(payload.usage);
304
- if (usage && !emittedTokens) {
305
- emittedTokens = true;
306
- yield { type: 'tokens', tokens: usage };
307
- }
308
- }
309
- if (!sawPayload) {
310
- yield { type: 'error', error: publicError('empty OpenRouter stream') };
311
- return;
312
- }
313
- yield* yieldRemainingStreamEvents(req, acc);
542
+ yield* yieldAiSdkStream(req, acc, context);
543
+ yield* yieldCapturedRawEvents(context, acc);
544
+ yield* finalEvents(req, acc);
314
545
  }
315
546
  catch (err) {
547
+ yield* yieldCapturedRawEventsUnchecked(context);
316
548
  yield { type: 'error', error: publicError(err) };
317
549
  }
318
550
  }
319
- /** Resolve an OpenRouter API key supplied explicitly by the host application. */
320
- export function resolveOpenRouterApiKey(explicitKey) {
321
- if (explicitKey?.trim()) {
322
- return explicitKey.trim();
551
+ function* finalEvents(req, acc) {
552
+ if (acc.errored) {
553
+ return;
323
554
  }
324
- return undefined;
555
+ if (req.structured && acc.text) {
556
+ const structured = tryStructured(acc.text);
557
+ if (structured) {
558
+ yield structured;
559
+ }
560
+ }
561
+ yield { type: 'done' };
562
+ }
563
+ function providerOptionsFor(req) {
564
+ if (req.thinking === 'none') {
565
+ return undefined;
566
+ }
567
+ return {
568
+ openrouter: {
569
+ reasoning: { effort: req.thinking },
570
+ },
571
+ };
572
+ }
573
+ function openRouterHeaders(config) {
574
+ const headers = {};
575
+ if (config.siteUrl) {
576
+ headers['HTTP-Referer'] = config.siteUrl;
577
+ }
578
+ if (config.siteName) {
579
+ headers['X-Title'] = config.siteName;
580
+ }
581
+ return Object.keys(headers).length > 0 ? headers : undefined;
325
582
  }
326
- /** Create a `ModelProvider` backed by OpenRouter-compatible chat completions. */
583
+ /** Create a `ModelProvider` backed by OpenRouter through AI SDK Core. */
327
584
  function createOpenRouterProvider(config = {}) {
328
585
  return {
329
586
  complete: (req) => streamOpenRouter(req, config),