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.
- package/README.md +62 -27
- package/docs/AGENT_PROFILE_CONTRACT.md +45 -18
- package/docs/CLI_SPEC.md +2 -2
- package/docs/SECRETS.md +27 -22
- package/esm/_dnt.polyfills.d.ts +99 -0
- package/esm/_dnt.polyfills.js +130 -1
- package/esm/mod.d.ts +19 -3
- package/esm/mod.js +17 -3
- package/esm/src/cli/commands/profile.d.ts +2 -0
- package/esm/src/cli/commands/profile.js +53 -0
- package/esm/src/cli/commands/run.d.ts +10 -0
- package/esm/src/cli/commands/run.js +55 -0
- package/esm/src/cli/commands/test.d.ts +15 -0
- package/esm/src/cli/commands/test.js +140 -0
- package/esm/src/cli/index.d.ts +4 -0
- package/esm/src/cli/index.js +148 -0
- package/esm/src/cli/matrix/fixtures.d.ts +13 -0
- package/esm/src/cli/matrix/fixtures.js +77 -0
- package/esm/src/cli/matrix/synthesizer.d.ts +29 -0
- package/esm/src/cli/matrix/synthesizer.js +187 -0
- package/esm/src/guardrails/error.d.ts +1 -3
- package/esm/src/guardrails/error.js +4 -6
- package/esm/src/guardrails/mod.d.ts +1 -1
- package/esm/src/guardrails/mod.js +1 -1
- package/esm/src/guardrails/sanitize.js +1 -1
- package/esm/src/host/mint-trace.d.ts +27 -0
- package/esm/src/host/mint-trace.js +33 -0
- package/esm/src/host/mod.d.ts +13 -0
- package/esm/src/host/mod.js +12 -0
- package/esm/src/host/reply.d.ts +14 -0
- package/esm/src/host/reply.js +27 -0
- package/esm/src/kernel/engine/delta.js +31 -4
- package/esm/src/kernel/engine/repair.d.ts +1 -3
- package/esm/src/kernel/engine/repair.js +1 -1
- package/esm/src/kernel/engine/runner/gates.d.ts +4 -0
- package/esm/src/kernel/engine/runner/gates.js +206 -0
- package/esm/src/kernel/engine/runner/mod.d.ts +14 -0
- package/esm/src/kernel/engine/runner/mod.js +103 -0
- package/esm/src/kernel/engine/runner/state.d.ts +16 -0
- package/esm/src/kernel/engine/runner/state.js +8 -0
- package/esm/src/kernel/engine/runner/steps.d.ts +15 -0
- package/esm/src/kernel/engine/runner/steps.js +119 -0
- package/esm/src/kernel/engine/runner/stream.d.ts +11 -0
- package/esm/src/kernel/engine/runner/stream.js +65 -0
- package/esm/src/kernel/engine/runner/tokens.d.ts +3 -0
- package/esm/src/kernel/engine/runner/tokens.js +38 -0
- package/esm/src/kernel/engine/runner/tools.d.ts +13 -0
- package/esm/src/kernel/engine/runner/tools.js +197 -0
- package/esm/src/kernel/engine/runner.d.ts +1 -9
- package/esm/src/kernel/engine/runner.js +1 -726
- package/esm/src/kernel/mod.d.ts +3 -1
- package/esm/src/kernel/mod.js +2 -1
- package/esm/src/{providers → kernel/registry}/attachments.d.ts +2 -3
- package/esm/src/{providers → kernel/registry}/attachments.js +6 -6
- package/esm/src/kernel/registry/catalog.d.ts +28 -21
- package/esm/src/kernel/registry/catalog.js +55 -133
- package/esm/src/kernel/registry/ingress.d.ts +13 -0
- package/esm/src/kernel/registry/ingress.js +163 -0
- package/esm/src/kernel/registry/profiles.d.ts +2 -4
- package/esm/src/kernel/registry/profiles.js +14 -5
- package/esm/src/kernel/registry/provider-request.d.ts +4 -0
- package/esm/src/kernel/registry/provider-request.js +25 -0
- package/esm/src/kernel/registry/resolve.d.ts +0 -1
- package/esm/src/kernel/registry/resolve.js +35 -53
- package/esm/src/kernel/registry/tools.js +2 -2
- package/esm/src/kernel/registry/vault.d.ts +11 -0
- package/esm/src/kernel/registry/vault.js +18 -0
- package/esm/src/kernel/types.d.ts +102 -78
- package/esm/src/observability/trace-attach.js +6 -20
- package/esm/src/presets/google.d.ts +50 -0
- package/esm/src/presets/google.js +96 -0
- package/esm/src/presets/mod.d.ts +11 -0
- package/esm/src/presets/mod.js +10 -0
- package/esm/src/providers/create-provider.d.ts +29 -0
- package/esm/src/providers/create-provider.js +38 -0
- package/esm/src/providers/gemini-tape.d.ts +1 -2
- package/esm/src/providers/gemini-tape.js +1 -1
- package/esm/src/providers/interactions.js +41 -16
- package/esm/src/{guardrails → providers}/keys.d.ts +10 -3
- package/esm/src/{guardrails → providers}/keys.js +10 -13
- package/esm/src/providers/mod.d.ts +4 -7
- package/esm/src/providers/mod.js +2 -5
- package/esm/src/providers/openrouter-mod.d.ts +13 -0
- package/esm/src/providers/openrouter-mod.js +12 -0
- package/esm/src/providers/openrouter-payload.d.ts +11 -3
- package/esm/src/providers/openrouter-payload.js +28 -26
- package/esm/src/providers/openrouter.d.ts +5 -7
- package/esm/src/providers/openrouter.js +488 -231
- package/esm/src/providers/pcm.d.ts +7 -0
- package/esm/src/providers/pcm.js +35 -0
- package/esm/src/providers/provider.d.ts +3 -1
- package/esm/src/providers/provider.js +45 -2
- package/esm/src/providers/speech.d.ts +23 -0
- package/esm/src/providers/speech.js +125 -0
- package/package.json +20 -3
- package/esm/src/providers/media.d.ts +0 -5
- package/esm/src/providers/media.js +0 -125
- package/esm/src/providers/tts.d.ts +0 -24
- package/esm/src/providers/tts.js +0 -144
|
@@ -1,95 +1,208 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpenRouter
|
|
2
|
+
* OpenRouter provider adapter powered by Vercel AI SDK Core.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
10
|
-
import {
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
39
|
-
if (
|
|
40
|
-
return
|
|
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
|
-
|
|
43
|
-
|
|
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
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
index = callObj.index;
|
|
98
|
+
function historyMessage(msg) {
|
|
99
|
+
if (msg.role === 'tool') {
|
|
100
|
+
return toolResultContent(msg);
|
|
51
101
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
return
|
|
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
|
|
109
|
-
for (const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
133
|
-
|
|
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
|
|
147
|
-
if (
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
297
|
+
return undefined;
|
|
164
298
|
}
|
|
165
|
-
function
|
|
166
|
-
|
|
167
|
-
|
|
299
|
+
function rawChoiceMessageEvidence(raw, acc) {
|
|
300
|
+
const choices = raw.choices;
|
|
301
|
+
if (!Array.isArray(choices)) {
|
|
302
|
+
return undefined;
|
|
168
303
|
}
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
316
|
+
function rawEvents(raw, acc) {
|
|
317
|
+
const record = rawRecord(raw);
|
|
318
|
+
if (!record) {
|
|
319
|
+
return [];
|
|
320
|
+
}
|
|
174
321
|
const events = [];
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
202
|
-
|
|
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
|
|
359
|
+
return events;
|
|
205
360
|
}
|
|
206
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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
|
|
225
|
-
if (
|
|
416
|
+
function eventFromPart(part, acc) {
|
|
417
|
+
if (appendToolInput(part, acc)) {
|
|
226
418
|
return [];
|
|
227
419
|
}
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (evidence) {
|
|
232
|
-
events.push(evidence);
|
|
420
|
+
const mapped = primaryEventFromPart(part, acc);
|
|
421
|
+
if (mapped) {
|
|
422
|
+
return [mapped];
|
|
233
423
|
}
|
|
234
|
-
|
|
424
|
+
const evidence = providerMetadataEvent(part, acc);
|
|
425
|
+
return evidence ? [evidence] : [];
|
|
235
426
|
}
|
|
236
|
-
function
|
|
237
|
-
|
|
238
|
-
|
|
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
|
|
243
|
-
if (
|
|
449
|
+
function finishEvent(part, acc) {
|
|
450
|
+
if (acc.emittedTokens) {
|
|
244
451
|
return undefined;
|
|
245
452
|
}
|
|
246
|
-
const
|
|
247
|
-
if (
|
|
248
|
-
|
|
453
|
+
const event = tokenEvent(part);
|
|
454
|
+
if (event) {
|
|
455
|
+
acc.emittedTokens = true;
|
|
249
456
|
}
|
|
250
|
-
|
|
251
|
-
return evidence;
|
|
457
|
+
return event;
|
|
252
458
|
}
|
|
253
|
-
function
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
464
|
+
if (acc.evidenceSeen) {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
acc.evidenceSeen = true;
|
|
468
|
+
return true;
|
|
469
|
+
}));
|
|
264
470
|
}
|
|
265
|
-
function
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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*
|
|
274
|
-
const
|
|
275
|
-
|
|
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
|
-
|
|
279
|
-
|
|
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 =
|
|
534
|
+
const apiKey = trimApiKey(config.apiKey);
|
|
284
535
|
if (!apiKey) {
|
|
285
|
-
yield
|
|
536
|
+
yield missingOpenRouterKey();
|
|
286
537
|
return;
|
|
287
538
|
}
|
|
539
|
+
const acc = createAccumulator();
|
|
540
|
+
const context = createStreamContext(req, config, apiKey);
|
|
288
541
|
try {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
return explicitKey.trim();
|
|
551
|
+
function* finalEvents(req, acc) {
|
|
552
|
+
if (acc.errored) {
|
|
553
|
+
return;
|
|
323
554
|
}
|
|
324
|
-
|
|
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
|
|
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),
|