@openrouter/agent 0.1.0
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 +367 -0
- package/esm/api-shape-helpers/claude-message.d.ts +218 -0
- package/esm/api-shape-helpers/claude-message.d.ts.map +1 -0
- package/esm/api-shape-helpers/claude-message.js +6 -0
- package/esm/api-shape-helpers/claude-message.js.map +1 -0
- package/esm/index.d.ts +22 -0
- package/esm/index.d.ts.map +1 -0
- package/esm/index.js +27 -0
- package/esm/index.js.map +1 -0
- package/esm/inner-loop/call-model.d.ts +67 -0
- package/esm/inner-loop/call-model.d.ts.map +1 -0
- package/esm/inner-loop/call-model.js +116 -0
- package/esm/inner-loop/call-model.js.map +1 -0
- package/esm/lib/anthropic-compat.d.ts +51 -0
- package/esm/lib/anthropic-compat.d.ts.map +1 -0
- package/esm/lib/anthropic-compat.js +216 -0
- package/esm/lib/anthropic-compat.js.map +1 -0
- package/esm/lib/anthropic-compat.test.d.ts +2 -0
- package/esm/lib/anthropic-compat.test.d.ts.map +1 -0
- package/esm/lib/anthropic-compat.test.js +668 -0
- package/esm/lib/anthropic-compat.test.js.map +1 -0
- package/esm/lib/async-params.d.ts +107 -0
- package/esm/lib/async-params.d.ts.map +1 -0
- package/esm/lib/async-params.js +94 -0
- package/esm/lib/async-params.js.map +1 -0
- package/esm/lib/chat-compat.d.ts +46 -0
- package/esm/lib/chat-compat.d.ts.map +1 -0
- package/esm/lib/chat-compat.js +111 -0
- package/esm/lib/chat-compat.js.map +1 -0
- package/esm/lib/chat-compat.test.d.ts +2 -0
- package/esm/lib/chat-compat.test.d.ts.map +1 -0
- package/esm/lib/chat-compat.test.js +405 -0
- package/esm/lib/chat-compat.test.js.map +1 -0
- package/esm/lib/claude-constants.d.ts +22 -0
- package/esm/lib/claude-constants.d.ts.map +1 -0
- package/esm/lib/claude-constants.js +20 -0
- package/esm/lib/claude-constants.js.map +1 -0
- package/esm/lib/claude-type-guards.d.ts +10 -0
- package/esm/lib/claude-type-guards.d.ts.map +1 -0
- package/esm/lib/claude-type-guards.js +68 -0
- package/esm/lib/claude-type-guards.js.map +1 -0
- package/esm/lib/conversation-state.d.ts +61 -0
- package/esm/lib/conversation-state.d.ts.map +1 -0
- package/esm/lib/conversation-state.js +230 -0
- package/esm/lib/conversation-state.js.map +1 -0
- package/esm/lib/model-result.d.ts +370 -0
- package/esm/lib/model-result.d.ts.map +1 -0
- package/esm/lib/model-result.js +1483 -0
- package/esm/lib/model-result.js.map +1 -0
- package/esm/lib/next-turn-params.d.ts +30 -0
- package/esm/lib/next-turn-params.d.ts.map +1 -0
- package/esm/lib/next-turn-params.js +129 -0
- package/esm/lib/next-turn-params.js.map +1 -0
- package/esm/lib/reusable-stream.d.ts +39 -0
- package/esm/lib/reusable-stream.d.ts.map +1 -0
- package/esm/lib/reusable-stream.js +192 -0
- package/esm/lib/reusable-stream.js.map +1 -0
- package/esm/lib/stop-conditions.d.ts +80 -0
- package/esm/lib/stop-conditions.d.ts.map +1 -0
- package/esm/lib/stop-conditions.js +104 -0
- package/esm/lib/stop-conditions.js.map +1 -0
- package/esm/lib/stream-transformers.d.ts +109 -0
- package/esm/lib/stream-transformers.d.ts.map +1 -0
- package/esm/lib/stream-transformers.js +856 -0
- package/esm/lib/stream-transformers.js.map +1 -0
- package/esm/lib/stream-type-guards.d.ts +29 -0
- package/esm/lib/stream-type-guards.d.ts.map +1 -0
- package/esm/lib/stream-type-guards.js +85 -0
- package/esm/lib/stream-type-guards.js.map +1 -0
- package/esm/lib/tool-context.d.ts +68 -0
- package/esm/lib/tool-context.d.ts.map +1 -0
- package/esm/lib/tool-context.js +188 -0
- package/esm/lib/tool-context.js.map +1 -0
- package/esm/lib/tool-event-broadcaster.d.ts +44 -0
- package/esm/lib/tool-event-broadcaster.d.ts.map +1 -0
- package/esm/lib/tool-event-broadcaster.js +162 -0
- package/esm/lib/tool-event-broadcaster.js.map +1 -0
- package/esm/lib/tool-executor.d.ts +73 -0
- package/esm/lib/tool-executor.d.ts.map +1 -0
- package/esm/lib/tool-executor.js +267 -0
- package/esm/lib/tool-executor.js.map +1 -0
- package/esm/lib/tool-orchestrator.d.ts +50 -0
- package/esm/lib/tool-orchestrator.d.ts.map +1 -0
- package/esm/lib/tool-orchestrator.js +180 -0
- package/esm/lib/tool-orchestrator.js.map +1 -0
- package/esm/lib/tool-types.d.ts +572 -0
- package/esm/lib/tool-types.d.ts.map +1 -0
- package/esm/lib/tool-types.js +80 -0
- package/esm/lib/tool-types.js.map +1 -0
- package/esm/lib/tool.d.ts +108 -0
- package/esm/lib/tool.d.ts.map +1 -0
- package/esm/lib/tool.js +84 -0
- package/esm/lib/tool.js.map +1 -0
- package/esm/lib/turn-context.d.ts +50 -0
- package/esm/lib/turn-context.d.ts.map +1 -0
- package/esm/lib/turn-context.js +61 -0
- package/esm/lib/turn-context.js.map +1 -0
- package/package.json +125 -0
|
@@ -0,0 +1,856 @@
|
|
|
1
|
+
import { isFileCitationAnnotation, isFilePathAnnotation, isFileSearchCallOutputItem, isFunctionCallArgumentsDeltaEvent, isFunctionCallArgumentsDoneEvent, isFunctionCallItem, isImageGenerationCallOutputItem, isOutputItemAddedEvent, isOutputItemDoneEvent, isOutputMessage, isOutputTextDeltaEvent, isOutputTextPart, isReasoningDeltaEvent, isReasoningOutputItem, isRefusalPart, isResponseCompletedEvent, isResponseFailedEvent, isResponseIncompleteEvent, isURLCitationAnnotation, isWebSearchCallOutputItem, } from './stream-type-guards.js';
|
|
2
|
+
/**
|
|
3
|
+
* Extract text deltas from responses stream events
|
|
4
|
+
*/
|
|
5
|
+
export async function* extractTextDeltas(stream) {
|
|
6
|
+
const consumer = stream.createConsumer();
|
|
7
|
+
for await (const event of consumer) {
|
|
8
|
+
if (isOutputTextDeltaEvent(event)) {
|
|
9
|
+
if (event.delta) {
|
|
10
|
+
yield event.delta;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Extract reasoning deltas from responses stream events
|
|
17
|
+
*/
|
|
18
|
+
export async function* extractReasoningDeltas(stream) {
|
|
19
|
+
const consumer = stream.createConsumer();
|
|
20
|
+
for await (const event of consumer) {
|
|
21
|
+
if (isReasoningDeltaEvent(event)) {
|
|
22
|
+
if (event.delta) {
|
|
23
|
+
yield event.delta;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Extract tool call argument deltas from responses stream events
|
|
30
|
+
*/
|
|
31
|
+
export async function* extractToolDeltas(stream) {
|
|
32
|
+
const consumer = stream.createConsumer();
|
|
33
|
+
for await (const event of consumer) {
|
|
34
|
+
if (isFunctionCallArgumentsDeltaEvent(event)) {
|
|
35
|
+
if (event.delta) {
|
|
36
|
+
yield event.delta;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Core message stream builder - shared logic for both formats
|
|
43
|
+
* Accumulates text deltas and yields updates
|
|
44
|
+
*/
|
|
45
|
+
async function* buildMessageStreamCore(stream) {
|
|
46
|
+
const consumer = stream.createConsumer();
|
|
47
|
+
// Track the accumulated text and message info
|
|
48
|
+
let currentText = '';
|
|
49
|
+
let currentId = '';
|
|
50
|
+
let hasStarted = false;
|
|
51
|
+
for await (const event of consumer) {
|
|
52
|
+
if (!('type' in event)) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
switch (event.type) {
|
|
56
|
+
case 'response.output_item.added': {
|
|
57
|
+
if (isOutputItemAddedEvent(event)) {
|
|
58
|
+
if (event.item && isOutputMessage(event.item)) {
|
|
59
|
+
hasStarted = true;
|
|
60
|
+
currentText = '';
|
|
61
|
+
currentId = event.item.id;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
case 'response.output_text.delta': {
|
|
67
|
+
if (isOutputTextDeltaEvent(event)) {
|
|
68
|
+
if (hasStarted && event.delta) {
|
|
69
|
+
currentText += event.delta;
|
|
70
|
+
yield {
|
|
71
|
+
type: 'delta',
|
|
72
|
+
text: currentText,
|
|
73
|
+
messageId: currentId,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case 'response.output_item.done': {
|
|
80
|
+
if (isOutputItemDoneEvent(event)) {
|
|
81
|
+
if (event.item && isOutputMessage(event.item)) {
|
|
82
|
+
yield {
|
|
83
|
+
type: 'complete',
|
|
84
|
+
completeMessage: event.item,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case 'response.completed':
|
|
91
|
+
case 'response.failed':
|
|
92
|
+
case 'response.incomplete':
|
|
93
|
+
// Stream is complete, stop consuming
|
|
94
|
+
return;
|
|
95
|
+
default:
|
|
96
|
+
// Ignore other event types - this is intentionally not exhaustive
|
|
97
|
+
// as we only care about specific events for message building
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Build incremental message updates from responses stream events
|
|
104
|
+
* Returns OutputMessage (assistant/responses format)
|
|
105
|
+
*/
|
|
106
|
+
export async function* buildResponsesMessageStream(stream) {
|
|
107
|
+
for await (const update of buildMessageStreamCore(stream)) {
|
|
108
|
+
if (update.type === 'delta' && update.text !== undefined && update.messageId !== undefined) {
|
|
109
|
+
// Yield incremental update in OutputMessage format
|
|
110
|
+
yield {
|
|
111
|
+
id: update.messageId,
|
|
112
|
+
type: 'message',
|
|
113
|
+
role: 'assistant',
|
|
114
|
+
status: 'in_progress',
|
|
115
|
+
content: [
|
|
116
|
+
{
|
|
117
|
+
type: 'output_text',
|
|
118
|
+
text: update.text,
|
|
119
|
+
annotations: [],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
else if (update.type === 'complete' && update.completeMessage) {
|
|
125
|
+
// Yield final complete message
|
|
126
|
+
yield update.completeMessage;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Handle output_item.added event - Initialize tracking for new items
|
|
132
|
+
*/
|
|
133
|
+
function handleOutputItemAdded(event, itemsInProgress) {
|
|
134
|
+
if (!isOutputItemAddedEvent(event) || !event.item) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
const item = event.item;
|
|
138
|
+
if (isOutputMessage(item)) {
|
|
139
|
+
itemsInProgress.set(item.id, {
|
|
140
|
+
type: 'message',
|
|
141
|
+
id: item.id,
|
|
142
|
+
textContent: '',
|
|
143
|
+
});
|
|
144
|
+
return {
|
|
145
|
+
id: item.id,
|
|
146
|
+
type: 'message',
|
|
147
|
+
role: 'assistant',
|
|
148
|
+
status: 'in_progress',
|
|
149
|
+
content: [],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (isFunctionCallItem(item)) {
|
|
153
|
+
// Use item.id if available (matches itemId in delta events), fall back to callId
|
|
154
|
+
const itemKey = item.id ?? item.callId;
|
|
155
|
+
itemsInProgress.set(itemKey, {
|
|
156
|
+
type: 'function_call',
|
|
157
|
+
id: itemKey,
|
|
158
|
+
name: item.name,
|
|
159
|
+
callId: item.callId,
|
|
160
|
+
argumentsAccumulated: '',
|
|
161
|
+
});
|
|
162
|
+
return {
|
|
163
|
+
type: 'function_call',
|
|
164
|
+
id: item.id,
|
|
165
|
+
callId: item.callId,
|
|
166
|
+
name: item.name,
|
|
167
|
+
arguments: '',
|
|
168
|
+
status: 'in_progress',
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (isReasoningOutputItem(item)) {
|
|
172
|
+
itemsInProgress.set(item.id, {
|
|
173
|
+
type: 'reasoning',
|
|
174
|
+
id: item.id,
|
|
175
|
+
reasoningContent: '',
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
type: 'reasoning',
|
|
179
|
+
id: item.id,
|
|
180
|
+
status: 'in_progress',
|
|
181
|
+
summary: [],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
if (isWebSearchCallOutputItem(item)) {
|
|
185
|
+
return item;
|
|
186
|
+
}
|
|
187
|
+
if (isFileSearchCallOutputItem(item)) {
|
|
188
|
+
return item;
|
|
189
|
+
}
|
|
190
|
+
if (isImageGenerationCallOutputItem(item)) {
|
|
191
|
+
return item;
|
|
192
|
+
}
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Handle text delta event for messages
|
|
197
|
+
*/
|
|
198
|
+
function handleTextDelta(event, itemsInProgress) {
|
|
199
|
+
if (!isOutputTextDeltaEvent(event) || !event.delta) {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
const item = itemsInProgress.get(event.itemId);
|
|
203
|
+
if (item?.type === 'message') {
|
|
204
|
+
item.textContent += event.delta;
|
|
205
|
+
return {
|
|
206
|
+
id: item.id,
|
|
207
|
+
type: 'message',
|
|
208
|
+
role: 'assistant',
|
|
209
|
+
status: 'in_progress',
|
|
210
|
+
content: [
|
|
211
|
+
{
|
|
212
|
+
type: 'output_text',
|
|
213
|
+
text: item.textContent,
|
|
214
|
+
annotations: [],
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Handle function call argument delta event
|
|
223
|
+
*/
|
|
224
|
+
function handleFunctionCallDelta(event, itemsInProgress) {
|
|
225
|
+
if (!isFunctionCallArgumentsDeltaEvent(event) || !event.delta) {
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
const item = itemsInProgress.get(event.itemId);
|
|
229
|
+
if (item?.type === 'function_call') {
|
|
230
|
+
item.argumentsAccumulated += event.delta;
|
|
231
|
+
return {
|
|
232
|
+
type: 'function_call',
|
|
233
|
+
// Include id if it differs from callId (means API provided an id)
|
|
234
|
+
id: item.id !== item.callId ? item.id : undefined,
|
|
235
|
+
callId: item.callId,
|
|
236
|
+
name: item.name,
|
|
237
|
+
arguments: item.argumentsAccumulated,
|
|
238
|
+
status: 'in_progress',
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Handle reasoning text delta event
|
|
245
|
+
*/
|
|
246
|
+
function handleReasoningDelta(event, itemsInProgress) {
|
|
247
|
+
if (!isReasoningDeltaEvent(event) || !event.delta) {
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
const item = itemsInProgress.get(event.itemId);
|
|
251
|
+
if (item?.type === 'reasoning') {
|
|
252
|
+
item.reasoningContent += event.delta;
|
|
253
|
+
return {
|
|
254
|
+
type: 'reasoning',
|
|
255
|
+
id: item.id,
|
|
256
|
+
status: 'in_progress',
|
|
257
|
+
summary: [
|
|
258
|
+
{
|
|
259
|
+
type: 'summary_text',
|
|
260
|
+
text: item.reasoningContent,
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Handle output_item.done event - Yield final complete item
|
|
269
|
+
*/
|
|
270
|
+
function handleOutputItemDone(event, itemsInProgress) {
|
|
271
|
+
if (!isOutputItemDoneEvent(event) || !event.item) {
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
const item = event.item;
|
|
275
|
+
if (isOutputMessage(item)) {
|
|
276
|
+
itemsInProgress.delete(item.id);
|
|
277
|
+
return item;
|
|
278
|
+
}
|
|
279
|
+
if (isFunctionCallItem(item)) {
|
|
280
|
+
// Use item.id if available (matches itemId in delta events), fall back to callId
|
|
281
|
+
itemsInProgress.delete(item.id ?? item.callId);
|
|
282
|
+
return item;
|
|
283
|
+
}
|
|
284
|
+
if (isReasoningOutputItem(item)) {
|
|
285
|
+
itemsInProgress.delete(item.id);
|
|
286
|
+
return item;
|
|
287
|
+
}
|
|
288
|
+
if (isWebSearchCallOutputItem(item)) {
|
|
289
|
+
return item;
|
|
290
|
+
}
|
|
291
|
+
if (isFileSearchCallOutputItem(item)) {
|
|
292
|
+
return item;
|
|
293
|
+
}
|
|
294
|
+
if (isImageGenerationCallOutputItem(item)) {
|
|
295
|
+
return item;
|
|
296
|
+
}
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
export const itemsStreamHandlers = {
|
|
300
|
+
'response.output_item.added': handleOutputItemAdded,
|
|
301
|
+
'response.output_text.delta': handleTextDelta,
|
|
302
|
+
'response.function_call_arguments.delta': handleFunctionCallDelta,
|
|
303
|
+
'response.reasoning_text.delta': handleReasoningDelta,
|
|
304
|
+
'response.output_item.done': handleOutputItemDone,
|
|
305
|
+
};
|
|
306
|
+
export const streamTerminationEvents = new Set([
|
|
307
|
+
'response.completed',
|
|
308
|
+
'response.failed',
|
|
309
|
+
'response.incomplete',
|
|
310
|
+
]);
|
|
311
|
+
//#endregion
|
|
312
|
+
/**
|
|
313
|
+
* Build incremental output item updates from responses stream events.
|
|
314
|
+
* Yields all item types cumulatively - same item may be emitted multiple times
|
|
315
|
+
* with the same ID but progressively updated content as streaming progresses.
|
|
316
|
+
*/
|
|
317
|
+
export async function* buildItemsStream(stream) {
|
|
318
|
+
const consumer = stream.createConsumer();
|
|
319
|
+
const itemsInProgress = new Map();
|
|
320
|
+
for await (const event of consumer) {
|
|
321
|
+
if (!('type' in event)) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (streamTerminationEvents.has(event.type)) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const handler = itemsStreamHandlers[event.type];
|
|
328
|
+
if (handler) {
|
|
329
|
+
const result = handler(event, itemsInProgress);
|
|
330
|
+
if (result) {
|
|
331
|
+
yield result;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Build incremental message updates from responses stream events
|
|
338
|
+
* Returns ChatAssistantMessage (chat format) instead of OutputMessage
|
|
339
|
+
*/
|
|
340
|
+
export async function* buildMessageStream(stream) {
|
|
341
|
+
for await (const update of buildMessageStreamCore(stream)) {
|
|
342
|
+
if (update.type === 'delta' && update.text !== undefined) {
|
|
343
|
+
// Yield incremental update in chat format
|
|
344
|
+
yield {
|
|
345
|
+
role: 'assistant',
|
|
346
|
+
content: update.text,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
else if (update.type === 'complete' && update.completeMessage) {
|
|
350
|
+
// Yield final complete message converted to chat format
|
|
351
|
+
yield convertToAssistantMessage(update.completeMessage);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Consume stream until completion and return the complete response
|
|
357
|
+
*/
|
|
358
|
+
export async function consumeStreamForCompletion(stream) {
|
|
359
|
+
const consumer = stream.createConsumer();
|
|
360
|
+
for await (const event of consumer) {
|
|
361
|
+
if (!('type' in event)) {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (isResponseCompletedEvent(event)) {
|
|
365
|
+
return event.response;
|
|
366
|
+
}
|
|
367
|
+
if (isResponseFailedEvent(event)) {
|
|
368
|
+
// The failed event contains the full response with error information
|
|
369
|
+
throw new Error(`Response failed: ${JSON.stringify(event.response.error)}`);
|
|
370
|
+
}
|
|
371
|
+
if (isResponseIncompleteEvent(event)) {
|
|
372
|
+
// Return the incomplete response
|
|
373
|
+
return event.response;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
throw new Error('Stream ended without completion event');
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Convert OutputMessage to ChatAssistantMessage (chat format)
|
|
380
|
+
*/
|
|
381
|
+
function convertToAssistantMessage(outputMessage) {
|
|
382
|
+
// Extract text content
|
|
383
|
+
const textContent = outputMessage.content
|
|
384
|
+
.filter((part) => 'type' in part && part.type === 'output_text')
|
|
385
|
+
.map((part) => part.text)
|
|
386
|
+
.join('');
|
|
387
|
+
return {
|
|
388
|
+
role: 'assistant',
|
|
389
|
+
content: textContent || null,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Extract the first message from a completed response (chat format)
|
|
394
|
+
*/
|
|
395
|
+
export function extractMessageFromResponse(response) {
|
|
396
|
+
const messageItem = response.output.find((item) => 'type' in item && item.type === 'message');
|
|
397
|
+
if (!messageItem) {
|
|
398
|
+
throw new Error('No message found in response output');
|
|
399
|
+
}
|
|
400
|
+
return convertToAssistantMessage(messageItem);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Extract the first message from a completed response (responses format)
|
|
404
|
+
*/
|
|
405
|
+
export function extractResponsesMessageFromResponse(response) {
|
|
406
|
+
const messageItem = response.output.find((item) => 'type' in item && item.type === 'message');
|
|
407
|
+
if (!messageItem) {
|
|
408
|
+
throw new Error('No message found in response output');
|
|
409
|
+
}
|
|
410
|
+
return messageItem;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Extract text from a response, either from outputText or by concatenating message content
|
|
414
|
+
*/
|
|
415
|
+
export function extractTextFromResponse(response) {
|
|
416
|
+
// Use pre-concatenated outputText if available
|
|
417
|
+
if (response.outputText) {
|
|
418
|
+
return response.outputText;
|
|
419
|
+
}
|
|
420
|
+
// Check if there's a message in the output
|
|
421
|
+
const hasMessage = response.output.some((item) => 'type' in item && item.type === 'message');
|
|
422
|
+
if (!hasMessage) {
|
|
423
|
+
// No message in response (e.g., only function calls)
|
|
424
|
+
return '';
|
|
425
|
+
}
|
|
426
|
+
// Otherwise, extract from the first message (convert to ChatAssistantMessage which has string content)
|
|
427
|
+
const message = extractMessageFromResponse(response);
|
|
428
|
+
// ChatAssistantMessage.content is string | Array | null | undefined
|
|
429
|
+
if (typeof message.content === 'string') {
|
|
430
|
+
return message.content;
|
|
431
|
+
}
|
|
432
|
+
return '';
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Extract all tool calls from a completed response
|
|
436
|
+
* Returns parsed tool calls with arguments as objects (not JSON strings)
|
|
437
|
+
*/
|
|
438
|
+
export function extractToolCallsFromResponse(response) {
|
|
439
|
+
const toolCalls = [];
|
|
440
|
+
for (const item of response.output) {
|
|
441
|
+
if (isFunctionCallItem(item)) {
|
|
442
|
+
try {
|
|
443
|
+
const trimmedArgs = item.arguments.trim();
|
|
444
|
+
const parsedArguments = trimmedArgs ? JSON.parse(trimmedArgs) : {};
|
|
445
|
+
toolCalls.push({
|
|
446
|
+
id: item.callId,
|
|
447
|
+
name: item.name,
|
|
448
|
+
arguments: parsedArguments,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
console.warn(`Failed to parse tool call arguments for ${item.name}:`, error instanceof Error ? error.message : String(error), `\nArguments: ${item.arguments.substring(0, 100)}${item.arguments.length > 100 ? '...' : ''}`);
|
|
453
|
+
// Include the tool call with unparsed arguments
|
|
454
|
+
toolCalls.push({
|
|
455
|
+
id: item.callId,
|
|
456
|
+
name: item.name,
|
|
457
|
+
arguments: item.arguments, // Keep as string if parsing fails
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return toolCalls;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Build incremental tool call updates from responses stream events
|
|
466
|
+
* Yields structured tool call objects as they're built from deltas
|
|
467
|
+
*/
|
|
468
|
+
export async function* buildToolCallStream(stream) {
|
|
469
|
+
const consumer = stream.createConsumer();
|
|
470
|
+
// Track tool calls being built
|
|
471
|
+
const toolCallsInProgress = new Map();
|
|
472
|
+
for await (const event of consumer) {
|
|
473
|
+
if (!('type' in event)) {
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
switch (event.type) {
|
|
477
|
+
case 'response.output_item.added': {
|
|
478
|
+
if (isOutputItemAddedEvent(event) && event.item && isFunctionCallItem(event.item)) {
|
|
479
|
+
// Use item.id if available (matches itemId in delta events), fall back to callId
|
|
480
|
+
const itemKey = event.item.id ?? event.item.callId;
|
|
481
|
+
toolCallsInProgress.set(itemKey, {
|
|
482
|
+
id: event.item.callId,
|
|
483
|
+
name: event.item.name,
|
|
484
|
+
argumentsAccumulated: '',
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
case 'response.function_call_arguments.delta': {
|
|
490
|
+
if (isFunctionCallArgumentsDeltaEvent(event)) {
|
|
491
|
+
const toolCall = toolCallsInProgress.get(event.itemId);
|
|
492
|
+
if (toolCall && event.delta) {
|
|
493
|
+
toolCall.argumentsAccumulated += event.delta;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
case 'response.function_call_arguments.done': {
|
|
499
|
+
if (isFunctionCallArgumentsDoneEvent(event)) {
|
|
500
|
+
const toolCall = toolCallsInProgress.get(event.itemId);
|
|
501
|
+
if (toolCall) {
|
|
502
|
+
// Parse complete arguments (empty string → empty object for no-param tools)
|
|
503
|
+
try {
|
|
504
|
+
const trimmedArgs = event.arguments.trim();
|
|
505
|
+
const parsedArguments = trimmedArgs ? JSON.parse(trimmedArgs) : {};
|
|
506
|
+
yield {
|
|
507
|
+
id: toolCall.id,
|
|
508
|
+
name: event.name,
|
|
509
|
+
arguments: parsedArguments,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
catch (error) {
|
|
513
|
+
console.warn(`Failed to parse tool call arguments for ${event.name}:`, error instanceof Error ? error.message : String(error), `\nArguments: ${event.arguments.substring(0, 100)}${event.arguments.length > 100 ? '...' : ''}`);
|
|
514
|
+
// Yield with unparsed arguments if parsing fails
|
|
515
|
+
yield {
|
|
516
|
+
id: toolCall.id,
|
|
517
|
+
name: event.name,
|
|
518
|
+
arguments: event.arguments,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
// Clean up
|
|
522
|
+
toolCallsInProgress.delete(event.itemId);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
case 'response.output_item.done': {
|
|
528
|
+
if (isOutputItemDoneEvent(event) && event.item && isFunctionCallItem(event.item)) {
|
|
529
|
+
// Use item.id if available (matches itemId in delta events), fall back to callId
|
|
530
|
+
const itemKey = event.item.id ?? event.item.callId;
|
|
531
|
+
// Yield final tool call if we haven't already
|
|
532
|
+
if (toolCallsInProgress.has(itemKey)) {
|
|
533
|
+
try {
|
|
534
|
+
const trimmedArgs = event.item.arguments.trim();
|
|
535
|
+
const parsedArguments = trimmedArgs ? JSON.parse(trimmedArgs) : {};
|
|
536
|
+
yield {
|
|
537
|
+
id: event.item.callId,
|
|
538
|
+
name: event.item.name,
|
|
539
|
+
arguments: parsedArguments,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
catch (_error) {
|
|
543
|
+
yield {
|
|
544
|
+
id: event.item.callId,
|
|
545
|
+
name: event.item.name,
|
|
546
|
+
arguments: event.item.arguments,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
toolCallsInProgress.delete(itemKey);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Check if a response contains any tool calls
|
|
559
|
+
*/
|
|
560
|
+
export function responseHasToolCalls(response) {
|
|
561
|
+
return response.output.some((item) => 'type' in item && item.type === 'function_call');
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Convert OpenRouter annotations to Claude citations
|
|
565
|
+
*/
|
|
566
|
+
function mapAnnotationsToCitations(annotations) {
|
|
567
|
+
if (!annotations || annotations.length === 0) {
|
|
568
|
+
return undefined;
|
|
569
|
+
}
|
|
570
|
+
const citations = [];
|
|
571
|
+
for (const annotation of annotations) {
|
|
572
|
+
if (!('type' in annotation)) {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
switch (annotation.type) {
|
|
576
|
+
case 'file_citation': {
|
|
577
|
+
if (isFileCitationAnnotation(annotation)) {
|
|
578
|
+
citations.push({
|
|
579
|
+
type: 'char_location',
|
|
580
|
+
cited_text: '',
|
|
581
|
+
document_index: annotation.index,
|
|
582
|
+
document_title: annotation.filename,
|
|
583
|
+
file_id: annotation.fileId,
|
|
584
|
+
start_char_index: 0,
|
|
585
|
+
end_char_index: 0,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
case 'url_citation': {
|
|
591
|
+
if (isURLCitationAnnotation(annotation)) {
|
|
592
|
+
citations.push({
|
|
593
|
+
type: 'web_search_result_location',
|
|
594
|
+
cited_text: '',
|
|
595
|
+
title: annotation.title,
|
|
596
|
+
url: annotation.url,
|
|
597
|
+
encrypted_index: '',
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
case 'file_path': {
|
|
603
|
+
if (isFilePathAnnotation(annotation)) {
|
|
604
|
+
citations.push({
|
|
605
|
+
type: 'char_location',
|
|
606
|
+
cited_text: '',
|
|
607
|
+
document_index: annotation.index,
|
|
608
|
+
document_title: '',
|
|
609
|
+
file_id: annotation.fileId,
|
|
610
|
+
start_char_index: 0,
|
|
611
|
+
end_char_index: 0,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
default:
|
|
617
|
+
// Unknown annotation types are skipped for forward compatibility.
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return citations.length > 0 ? citations : undefined;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Map OpenResponses status to Claude stop reason
|
|
625
|
+
*/
|
|
626
|
+
function mapStopReason(response) {
|
|
627
|
+
// Check if any tool calls exist in the response
|
|
628
|
+
const hasToolCalls = response.output.some((item) => 'type' in item && item.type === 'function_call');
|
|
629
|
+
if (hasToolCalls) {
|
|
630
|
+
return 'tool_use';
|
|
631
|
+
}
|
|
632
|
+
// Check the response status
|
|
633
|
+
if (response.status === 'completed') {
|
|
634
|
+
return 'end_turn';
|
|
635
|
+
}
|
|
636
|
+
if (response.status === 'incomplete') {
|
|
637
|
+
// Check incomplete reason if available
|
|
638
|
+
const incompleteReason = response.incompleteDetails?.reason;
|
|
639
|
+
if (incompleteReason === 'max_output_tokens') {
|
|
640
|
+
return 'max_tokens';
|
|
641
|
+
}
|
|
642
|
+
return 'end_turn';
|
|
643
|
+
}
|
|
644
|
+
return 'end_turn';
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Convert OpenResponsesResult to ClaudeMessage format
|
|
648
|
+
* Compatible with the Anthropic SDK BetaMessage type
|
|
649
|
+
*/
|
|
650
|
+
export function convertToClaudeMessage(response) {
|
|
651
|
+
const content = [];
|
|
652
|
+
const unsupportedContent = [];
|
|
653
|
+
for (const item of response.output) {
|
|
654
|
+
if (!('type' in item)) {
|
|
655
|
+
// Handle items without type field
|
|
656
|
+
// Convert unknown item to a record format for storage
|
|
657
|
+
const itemData = typeof item === 'object' && item !== null
|
|
658
|
+
? item
|
|
659
|
+
: {
|
|
660
|
+
value: item,
|
|
661
|
+
};
|
|
662
|
+
unsupportedContent.push({
|
|
663
|
+
original_type: 'unknown',
|
|
664
|
+
data: itemData,
|
|
665
|
+
reason: 'Output item missing type field',
|
|
666
|
+
});
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
switch (item.type) {
|
|
670
|
+
case 'message': {
|
|
671
|
+
if (isOutputMessage(item)) {
|
|
672
|
+
for (const part of item.content) {
|
|
673
|
+
if (!('type' in part)) {
|
|
674
|
+
// Convert unknown part to a record format for storage
|
|
675
|
+
const partData = typeof part === 'object' && part !== null
|
|
676
|
+
? part
|
|
677
|
+
: {
|
|
678
|
+
value: part,
|
|
679
|
+
};
|
|
680
|
+
unsupportedContent.push({
|
|
681
|
+
original_type: 'unknown_message_part',
|
|
682
|
+
data: partData,
|
|
683
|
+
reason: 'Message content part missing type field',
|
|
684
|
+
});
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
if (isOutputTextPart(part)) {
|
|
688
|
+
const citations = mapAnnotationsToCitations(part.annotations);
|
|
689
|
+
content.push({
|
|
690
|
+
type: 'text',
|
|
691
|
+
text: part.text,
|
|
692
|
+
...(citations && {
|
|
693
|
+
citations,
|
|
694
|
+
}),
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
else if (isRefusalPart(part)) {
|
|
698
|
+
unsupportedContent.push({
|
|
699
|
+
original_type: 'refusal',
|
|
700
|
+
data: {
|
|
701
|
+
refusal: part.refusal,
|
|
702
|
+
},
|
|
703
|
+
reason: 'Claude does not have a native refusal content type',
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
else {
|
|
707
|
+
// Unknown content types are skipped for forward compatibility.
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
case 'function_call': {
|
|
714
|
+
if (isFunctionCallItem(item)) {
|
|
715
|
+
let parsedInput;
|
|
716
|
+
try {
|
|
717
|
+
const trimmedArgs = item.arguments.trim();
|
|
718
|
+
parsedInput = trimmedArgs ? JSON.parse(trimmedArgs) : {};
|
|
719
|
+
}
|
|
720
|
+
catch (error) {
|
|
721
|
+
console.warn(`Failed to parse tool call arguments for ${item.name}:`, error instanceof Error ? error.message : String(error), `\nArguments: ${item.arguments.substring(0, 100)}${item.arguments.length > 100 ? '...' : ''}`);
|
|
722
|
+
// Preserve raw arguments if JSON parsing fails
|
|
723
|
+
parsedInput = {
|
|
724
|
+
_raw_arguments: item.arguments,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
content.push({
|
|
728
|
+
type: 'tool_use',
|
|
729
|
+
id: item.callId,
|
|
730
|
+
name: item.name,
|
|
731
|
+
input: parsedInput,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
case 'reasoning': {
|
|
737
|
+
if (isReasoningOutputItem(item)) {
|
|
738
|
+
if (item.summary && item.summary.length > 0) {
|
|
739
|
+
for (const summaryItem of item.summary) {
|
|
740
|
+
if (summaryItem.type === 'summary_text' && summaryItem.text) {
|
|
741
|
+
content.push({
|
|
742
|
+
type: 'thinking',
|
|
743
|
+
thinking: summaryItem.text,
|
|
744
|
+
signature: '',
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (item.encryptedContent) {
|
|
750
|
+
unsupportedContent.push({
|
|
751
|
+
original_type: 'reasoning_encrypted',
|
|
752
|
+
data: {
|
|
753
|
+
id: item.id,
|
|
754
|
+
encrypted_content: item.encryptedContent,
|
|
755
|
+
},
|
|
756
|
+
reason: 'Encrypted reasoning content preserved for round-trip',
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
case 'web_search_call': {
|
|
763
|
+
if (isWebSearchCallOutputItem(item)) {
|
|
764
|
+
content.push({
|
|
765
|
+
type: 'server_tool_use',
|
|
766
|
+
id: item.id,
|
|
767
|
+
name: 'web_search',
|
|
768
|
+
input: {
|
|
769
|
+
status: item.status,
|
|
770
|
+
},
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
case 'file_search_call': {
|
|
776
|
+
if (isFileSearchCallOutputItem(item)) {
|
|
777
|
+
content.push({
|
|
778
|
+
type: 'tool_use',
|
|
779
|
+
id: item.id,
|
|
780
|
+
name: 'file_search',
|
|
781
|
+
input: {
|
|
782
|
+
queries: item.queries,
|
|
783
|
+
status: item.status,
|
|
784
|
+
},
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
case 'image_generation_call': {
|
|
790
|
+
if (isImageGenerationCallOutputItem(item)) {
|
|
791
|
+
unsupportedContent.push({
|
|
792
|
+
original_type: 'image_generation_call',
|
|
793
|
+
data: {
|
|
794
|
+
id: item.id,
|
|
795
|
+
result: item.result,
|
|
796
|
+
status: item.status,
|
|
797
|
+
},
|
|
798
|
+
reason: 'Claude does not support image outputs in assistant messages',
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
default:
|
|
804
|
+
// Unknown output types (e.g. new server tools) are skipped during Claude format
|
|
805
|
+
// conversion — they round-trip natively via the Responses API input union.
|
|
806
|
+
break;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return {
|
|
810
|
+
id: response.id,
|
|
811
|
+
type: 'message',
|
|
812
|
+
role: 'assistant',
|
|
813
|
+
model: response.model ?? 'unknown',
|
|
814
|
+
content,
|
|
815
|
+
stop_reason: mapStopReason(response),
|
|
816
|
+
stop_sequence: null,
|
|
817
|
+
usage: {
|
|
818
|
+
input_tokens: response.usage?.inputTokens ?? 0,
|
|
819
|
+
output_tokens: response.usage?.outputTokens ?? 0,
|
|
820
|
+
cache_creation_input_tokens: response.usage?.inputTokensDetails?.cachedTokens ?? 0,
|
|
821
|
+
cache_read_input_tokens: 0,
|
|
822
|
+
},
|
|
823
|
+
...(unsupportedContent.length > 0 && {
|
|
824
|
+
unsupported_content: unsupportedContent,
|
|
825
|
+
}),
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Extract unsupported content by original type
|
|
830
|
+
*/
|
|
831
|
+
export function extractUnsupportedContent(message, originalType) {
|
|
832
|
+
if (!message.unsupported_content) {
|
|
833
|
+
return [];
|
|
834
|
+
}
|
|
835
|
+
return message.unsupported_content.filter((item) => item.original_type === originalType);
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Check if message has any unsupported content
|
|
839
|
+
*/
|
|
840
|
+
export function hasUnsupportedContent(message) {
|
|
841
|
+
return !!(message.unsupported_content && message.unsupported_content.length > 0);
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Get summary of unsupported content types
|
|
845
|
+
*/
|
|
846
|
+
export function getUnsupportedContentSummary(message) {
|
|
847
|
+
if (!message.unsupported_content) {
|
|
848
|
+
return {};
|
|
849
|
+
}
|
|
850
|
+
const summary = {};
|
|
851
|
+
for (const item of message.unsupported_content) {
|
|
852
|
+
summary[item.original_type] = (summary[item.original_type] || 0) + 1;
|
|
853
|
+
}
|
|
854
|
+
return summary;
|
|
855
|
+
}
|
|
856
|
+
//# sourceMappingURL=stream-transformers.js.map
|