@providerprotocol/ai 0.0.11 → 0.0.13

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 (104) hide show
  1. package/dist/anthropic/index.d.ts +51 -15
  2. package/dist/anthropic/index.js +54 -19
  3. package/dist/anthropic/index.js.map +1 -1
  4. package/dist/{chunk-SUNYWHTH.js → chunk-MOU4U3PO.js} +55 -3
  5. package/dist/chunk-MOU4U3PO.js.map +1 -0
  6. package/dist/{chunk-Y6Q7JCNP.js → chunk-MSR5P65T.js} +1 -1
  7. package/dist/chunk-MSR5P65T.js.map +1 -0
  8. package/dist/{chunk-W4BB4BG2.js → chunk-SVYROCLD.js} +31 -11
  9. package/dist/chunk-SVYROCLD.js.map +1 -0
  10. package/dist/chunk-U4JJC2YX.js +234 -0
  11. package/dist/chunk-U4JJC2YX.js.map +1 -0
  12. package/dist/{chunk-X5G4EHL7.js → chunk-Z7RBRCRN.js} +1 -1
  13. package/dist/chunk-Z7RBRCRN.js.map +1 -0
  14. package/dist/google/index.d.ts +376 -7
  15. package/dist/google/index.js +127 -15
  16. package/dist/google/index.js.map +1 -1
  17. package/dist/http/index.d.ts +222 -25
  18. package/dist/http/index.js +3 -3
  19. package/dist/index.d.ts +1482 -198
  20. package/dist/index.js +233 -49
  21. package/dist/index.js.map +1 -1
  22. package/dist/ollama/index.d.ts +92 -20
  23. package/dist/ollama/index.js +17 -7
  24. package/dist/ollama/index.js.map +1 -1
  25. package/dist/openai/index.d.ts +340 -61
  26. package/dist/openai/index.js +57 -15
  27. package/dist/openai/index.js.map +1 -1
  28. package/dist/openrouter/index.d.ts +107 -51
  29. package/dist/openrouter/index.js +36 -8
  30. package/dist/openrouter/index.js.map +1 -1
  31. package/dist/provider-mKkz7Q9U.d.ts +488 -0
  32. package/dist/retry-Dh70lgr0.d.ts +508 -0
  33. package/dist/xai/index.d.ts +97 -22
  34. package/dist/xai/index.js +55 -19
  35. package/dist/xai/index.js.map +1 -1
  36. package/package.json +8 -12
  37. package/dist/chunk-CUCRF5W6.js +0 -136
  38. package/dist/chunk-CUCRF5W6.js.map +0 -1
  39. package/dist/chunk-SUNYWHTH.js.map +0 -1
  40. package/dist/chunk-W4BB4BG2.js.map +0 -1
  41. package/dist/chunk-X5G4EHL7.js.map +0 -1
  42. package/dist/chunk-Y6Q7JCNP.js.map +0 -1
  43. package/dist/provider-CUJWjgNl.d.ts +0 -192
  44. package/dist/retry-I2661_rv.d.ts +0 -118
  45. package/src/anthropic/index.ts +0 -3
  46. package/src/core/image.ts +0 -188
  47. package/src/core/llm.ts +0 -650
  48. package/src/core/provider.ts +0 -92
  49. package/src/google/index.ts +0 -3
  50. package/src/http/errors.ts +0 -112
  51. package/src/http/fetch.ts +0 -210
  52. package/src/http/index.ts +0 -31
  53. package/src/http/keys.ts +0 -136
  54. package/src/http/retry.ts +0 -205
  55. package/src/http/sse.ts +0 -136
  56. package/src/index.ts +0 -32
  57. package/src/ollama/index.ts +0 -3
  58. package/src/openai/index.ts +0 -39
  59. package/src/openrouter/index.ts +0 -11
  60. package/src/providers/anthropic/index.ts +0 -17
  61. package/src/providers/anthropic/llm.ts +0 -196
  62. package/src/providers/anthropic/transform.ts +0 -434
  63. package/src/providers/anthropic/types.ts +0 -213
  64. package/src/providers/google/index.ts +0 -17
  65. package/src/providers/google/llm.ts +0 -203
  66. package/src/providers/google/transform.ts +0 -447
  67. package/src/providers/google/types.ts +0 -214
  68. package/src/providers/ollama/index.ts +0 -43
  69. package/src/providers/ollama/llm.ts +0 -272
  70. package/src/providers/ollama/transform.ts +0 -434
  71. package/src/providers/ollama/types.ts +0 -260
  72. package/src/providers/openai/index.ts +0 -186
  73. package/src/providers/openai/llm.completions.ts +0 -201
  74. package/src/providers/openai/llm.responses.ts +0 -211
  75. package/src/providers/openai/transform.completions.ts +0 -561
  76. package/src/providers/openai/transform.responses.ts +0 -708
  77. package/src/providers/openai/types.ts +0 -1249
  78. package/src/providers/openrouter/index.ts +0 -177
  79. package/src/providers/openrouter/llm.completions.ts +0 -201
  80. package/src/providers/openrouter/llm.responses.ts +0 -211
  81. package/src/providers/openrouter/transform.completions.ts +0 -538
  82. package/src/providers/openrouter/transform.responses.ts +0 -742
  83. package/src/providers/openrouter/types.ts +0 -717
  84. package/src/providers/xai/index.ts +0 -223
  85. package/src/providers/xai/llm.completions.ts +0 -201
  86. package/src/providers/xai/llm.messages.ts +0 -195
  87. package/src/providers/xai/llm.responses.ts +0 -211
  88. package/src/providers/xai/transform.completions.ts +0 -565
  89. package/src/providers/xai/transform.messages.ts +0 -448
  90. package/src/providers/xai/transform.responses.ts +0 -678
  91. package/src/providers/xai/types.ts +0 -938
  92. package/src/types/content.ts +0 -133
  93. package/src/types/errors.ts +0 -85
  94. package/src/types/index.ts +0 -105
  95. package/src/types/llm.ts +0 -211
  96. package/src/types/messages.ts +0 -205
  97. package/src/types/provider.ts +0 -195
  98. package/src/types/schema.ts +0 -58
  99. package/src/types/stream.ts +0 -188
  100. package/src/types/thread.ts +0 -226
  101. package/src/types/tool.ts +0 -88
  102. package/src/types/turn.ts +0 -118
  103. package/src/utils/id.ts +0 -28
  104. package/src/xai/index.ts +0 -41
@@ -1,742 +0,0 @@
1
- import type { LLMRequest, LLMResponse } from '../../types/llm.ts';
2
- import type { Message } from '../../types/messages.ts';
3
- import type { StreamEvent } from '../../types/stream.ts';
4
- import type { Tool, ToolCall } from '../../types/tool.ts';
5
- import type { TokenUsage } from '../../types/turn.ts';
6
- import type { ContentBlock, TextBlock, ImageBlock } from '../../types/content.ts';
7
- import {
8
- AssistantMessage,
9
- isUserMessage,
10
- isAssistantMessage,
11
- isToolResultMessage,
12
- } from '../../types/messages.ts';
13
- import type {
14
- OpenRouterResponsesParams,
15
- OpenRouterResponsesRequest,
16
- OpenRouterResponsesInputItem,
17
- OpenRouterResponsesContentPart,
18
- OpenRouterResponsesTool,
19
- OpenRouterResponsesResponse,
20
- OpenRouterResponsesStreamEvent,
21
- OpenRouterResponsesOutputItem,
22
- OpenRouterResponsesMessageOutput,
23
- OpenRouterResponsesFunctionCallOutput,
24
- } from './types.ts';
25
-
26
- /**
27
- * Transform UPP request to OpenRouter Responses API format
28
- *
29
- * Params are spread directly to allow pass-through of any OpenRouter API fields,
30
- * even those not explicitly defined in our type. This enables developers to
31
- * use new API features without waiting for library updates.
32
- */
33
- export function transformRequest(
34
- request: LLMRequest<OpenRouterResponsesParams>,
35
- modelId: string
36
- ): OpenRouterResponsesRequest {
37
- const params = request.params ?? ({} as OpenRouterResponsesParams);
38
-
39
- // Spread params to pass through all fields, then set required fields
40
- const openrouterRequest: OpenRouterResponsesRequest = {
41
- ...params,
42
- model: modelId,
43
- input: transformInputItems(request.messages, request.system),
44
- };
45
-
46
- // Tools come from request, not params
47
- if (request.tools && request.tools.length > 0) {
48
- openrouterRequest.tools = request.tools.map(transformTool);
49
- }
50
-
51
- // Structured output via text.format (overrides params.text if set)
52
- if (request.structure) {
53
- const schema: Record<string, unknown> = {
54
- type: 'object',
55
- properties: request.structure.properties,
56
- required: request.structure.required,
57
- ...(request.structure.additionalProperties !== undefined
58
- ? { additionalProperties: request.structure.additionalProperties }
59
- : { additionalProperties: false }),
60
- };
61
- if (request.structure.description) {
62
- schema.description = request.structure.description;
63
- }
64
-
65
- openrouterRequest.text = {
66
- format: {
67
- type: 'json_schema',
68
- name: 'json_response',
69
- description: request.structure.description,
70
- schema,
71
- strict: true,
72
- },
73
- };
74
- }
75
-
76
- return openrouterRequest;
77
- }
78
-
79
- /**
80
- * Transform messages to Responses API input items
81
- */
82
- function transformInputItems(
83
- messages: Message[],
84
- system?: string
85
- ): OpenRouterResponsesInputItem[] | string {
86
- const result: OpenRouterResponsesInputItem[] = [];
87
-
88
- if (system) {
89
- result.push({
90
- type: 'message',
91
- role: 'system',
92
- content: system,
93
- });
94
- }
95
-
96
- for (const message of messages) {
97
- const items = transformMessage(message);
98
- result.push(...items);
99
- }
100
-
101
- // If there's only one user message with simple text, return as string
102
- if (result.length === 1 && result[0]?.type === 'message') {
103
- const item = result[0] as { role?: string; content?: string | unknown[] };
104
- if (item.role === 'user' && typeof item.content === 'string') {
105
- return item.content;
106
- }
107
- }
108
-
109
- return result;
110
- }
111
-
112
- /**
113
- * Filter to only valid content blocks with a type property
114
- */
115
- function filterValidContent<T extends { type?: string }>(content: T[]): T[] {
116
- return content.filter((c) => c && typeof c.type === 'string');
117
- }
118
-
119
- /**
120
- * Transform a UPP Message to OpenRouter Responses API input items
121
- */
122
- function transformMessage(message: Message): OpenRouterResponsesInputItem[] {
123
- if (isUserMessage(message)) {
124
- const validContent = filterValidContent(message.content);
125
- // Check if we can use simple string content
126
- if (validContent.length === 1 && validContent[0]?.type === 'text') {
127
- return [
128
- {
129
- type: 'message',
130
- role: 'user',
131
- content: (validContent[0] as TextBlock).text,
132
- },
133
- ];
134
- }
135
- return [
136
- {
137
- type: 'message',
138
- role: 'user',
139
- content: validContent.map(transformContentPart),
140
- },
141
- ];
142
- }
143
-
144
- if (isAssistantMessage(message)) {
145
- const validContent = filterValidContent(message.content);
146
- const items: OpenRouterResponsesInputItem[] = [];
147
-
148
- // Add message content - only text parts for assistant messages
149
- const contentParts: OpenRouterResponsesContentPart[] = validContent
150
- .filter((c): c is TextBlock => c.type === 'text')
151
- .map((c): OpenRouterResponsesContentPart => ({
152
- type: 'output_text',
153
- text: c.text,
154
- annotations: [],
155
- }));
156
-
157
- // Get message id from metadata or generate one
158
- const messageId = message.id ?? `msg_${Date.now()}`;
159
-
160
- // Add assistant message if we have text content
161
- if (contentParts.length > 0) {
162
- items.push({
163
- type: 'message',
164
- role: 'assistant',
165
- id: messageId,
166
- status: 'completed',
167
- content: contentParts,
168
- });
169
- }
170
-
171
- // Add function_call items for each tool call (must precede function_call_output)
172
- const openrouterMeta = message.metadata?.openrouter as
173
- | { functionCallItems?: Array<{ id: string; call_id: string; name: string; arguments: string }> }
174
- | undefined;
175
- const functionCallItems = openrouterMeta?.functionCallItems;
176
-
177
- if (functionCallItems && functionCallItems.length > 0) {
178
- for (const fc of functionCallItems) {
179
- items.push({
180
- type: 'function_call',
181
- id: fc.id,
182
- call_id: fc.call_id,
183
- name: fc.name,
184
- arguments: fc.arguments,
185
- });
186
- }
187
- } else if (message.toolCalls && message.toolCalls.length > 0) {
188
- for (const call of message.toolCalls) {
189
- items.push({
190
- type: 'function_call',
191
- id: `fc_${call.toolCallId}`,
192
- call_id: call.toolCallId,
193
- name: call.toolName,
194
- arguments: JSON.stringify(call.arguments),
195
- });
196
- }
197
- }
198
-
199
- return items;
200
- }
201
-
202
- if (isToolResultMessage(message)) {
203
- // Tool results are function_call_output items
204
- return message.results.map((result, index) => ({
205
- type: 'function_call_output' as const,
206
- id: `fco_${result.toolCallId}_${index}`,
207
- call_id: result.toolCallId,
208
- output:
209
- typeof result.result === 'string'
210
- ? result.result
211
- : JSON.stringify(result.result),
212
- }));
213
- }
214
-
215
- return [];
216
- }
217
-
218
- /**
219
- * Transform a content block to Responses API format
220
- */
221
- function transformContentPart(block: ContentBlock): OpenRouterResponsesContentPart {
222
- switch (block.type) {
223
- case 'text':
224
- return { type: 'input_text', text: block.text };
225
-
226
- case 'image': {
227
- const imageBlock = block as ImageBlock;
228
- if (imageBlock.source.type === 'base64') {
229
- return {
230
- type: 'input_image',
231
- image_url: `data:${imageBlock.mimeType};base64,${imageBlock.source.data}`,
232
- detail: 'auto',
233
- };
234
- }
235
-
236
- if (imageBlock.source.type === 'url') {
237
- return {
238
- type: 'input_image',
239
- image_url: imageBlock.source.url,
240
- detail: 'auto',
241
- };
242
- }
243
-
244
- if (imageBlock.source.type === 'bytes') {
245
- // Convert bytes to base64
246
- const base64 = btoa(
247
- Array.from(imageBlock.source.data)
248
- .map((b) => String.fromCharCode(b))
249
- .join('')
250
- );
251
- return {
252
- type: 'input_image',
253
- image_url: `data:${imageBlock.mimeType};base64,${base64}`,
254
- detail: 'auto',
255
- };
256
- }
257
-
258
- throw new Error('Unknown image source type');
259
- }
260
-
261
- default:
262
- throw new Error(`Unsupported content type: ${block.type}`);
263
- }
264
- }
265
-
266
- /**
267
- * Transform a UPP Tool to Responses API format
268
- */
269
- function transformTool(tool: Tool): OpenRouterResponsesTool {
270
- return {
271
- type: 'function',
272
- name: tool.name,
273
- description: tool.description,
274
- parameters: {
275
- type: 'object',
276
- properties: tool.parameters.properties,
277
- required: tool.parameters.required,
278
- ...(tool.parameters.additionalProperties !== undefined
279
- ? { additionalProperties: tool.parameters.additionalProperties }
280
- : {}),
281
- },
282
- };
283
- }
284
-
285
- /**
286
- * Transform OpenRouter Responses API response to UPP LLMResponse
287
- */
288
- export function transformResponse(data: OpenRouterResponsesResponse): LLMResponse {
289
- // Extract text content and tool calls from output items
290
- const textContent: TextBlock[] = [];
291
- const toolCalls: ToolCall[] = [];
292
- const functionCallItems: Array<{
293
- id: string;
294
- call_id: string;
295
- name: string;
296
- arguments: string;
297
- }> = [];
298
- let hadRefusal = false;
299
- let structuredData: unknown;
300
-
301
- for (const item of data.output) {
302
- if (item.type === 'message') {
303
- const messageItem = item as OpenRouterResponsesMessageOutput;
304
- for (const content of messageItem.content) {
305
- if (content.type === 'output_text') {
306
- textContent.push({ type: 'text', text: content.text });
307
- // Try to parse as JSON for structured output (native JSON mode)
308
- // Only set data if text is valid JSON
309
- if (structuredData === undefined) {
310
- try {
311
- structuredData = JSON.parse(content.text);
312
- } catch {
313
- // Not valid JSON - that's fine, might not be structured output
314
- }
315
- }
316
- } else if (content.type === 'refusal') {
317
- textContent.push({ type: 'text', text: content.refusal });
318
- hadRefusal = true;
319
- }
320
- }
321
- } else if (item.type === 'function_call') {
322
- const functionCall = item as OpenRouterResponsesFunctionCallOutput;
323
- let args: Record<string, unknown> = {};
324
- try {
325
- args = JSON.parse(functionCall.arguments);
326
- } catch {
327
- // Invalid JSON - use empty object
328
- }
329
- toolCalls.push({
330
- toolCallId: functionCall.call_id,
331
- toolName: functionCall.name,
332
- arguments: args,
333
- });
334
- functionCallItems.push({
335
- id: functionCall.id,
336
- call_id: functionCall.call_id,
337
- name: functionCall.name,
338
- arguments: functionCall.arguments,
339
- });
340
- }
341
- }
342
-
343
- const message = new AssistantMessage(
344
- textContent,
345
- toolCalls.length > 0 ? toolCalls : undefined,
346
- {
347
- id: data.id,
348
- metadata: {
349
- openrouter: {
350
- model: data.model,
351
- status: data.status,
352
- // Store response_id for multi-turn tool calling
353
- response_id: data.id,
354
- functionCallItems:
355
- functionCallItems.length > 0 ? functionCallItems : undefined,
356
- },
357
- },
358
- }
359
- );
360
-
361
- const usage: TokenUsage = {
362
- inputTokens: data.usage.input_tokens,
363
- outputTokens: data.usage.output_tokens,
364
- totalTokens: data.usage.total_tokens,
365
- };
366
-
367
- // Map status to stop reason
368
- let stopReason = 'end_turn';
369
- if (data.status === 'completed') {
370
- stopReason = toolCalls.length > 0 ? 'tool_use' : 'end_turn';
371
- } else if (data.status === 'incomplete') {
372
- stopReason = data.incomplete_details?.reason === 'max_output_tokens'
373
- ? 'max_tokens'
374
- : 'end_turn';
375
- } else if (data.status === 'failed') {
376
- stopReason = 'error';
377
- }
378
- if (hadRefusal && stopReason !== 'error') {
379
- stopReason = 'content_filter';
380
- }
381
-
382
- return {
383
- message,
384
- usage,
385
- stopReason,
386
- data: structuredData,
387
- };
388
- }
389
-
390
- /**
391
- * State for accumulating streaming response
392
- */
393
- export interface ResponsesStreamState {
394
- id: string;
395
- model: string;
396
- textByIndex: Map<number, string>;
397
- toolCalls: Map<
398
- number,
399
- { itemId?: string; callId?: string; name?: string; arguments: string }
400
- >;
401
- status: string;
402
- inputTokens: number;
403
- outputTokens: number;
404
- hadRefusal: boolean;
405
- }
406
-
407
- /**
408
- * Create initial stream state
409
- */
410
- export function createStreamState(): ResponsesStreamState {
411
- return {
412
- id: '',
413
- model: '',
414
- textByIndex: new Map(),
415
- toolCalls: new Map(),
416
- status: 'in_progress',
417
- inputTokens: 0,
418
- outputTokens: 0,
419
- hadRefusal: false,
420
- };
421
- }
422
-
423
- /**
424
- * Transform OpenRouter Responses API stream event to UPP StreamEvent
425
- * Returns array since one event may produce multiple UPP events
426
- */
427
- export function transformStreamEvent(
428
- event: OpenRouterResponsesStreamEvent,
429
- state: ResponsesStreamState
430
- ): StreamEvent[] {
431
- const events: StreamEvent[] = [];
432
-
433
- switch (event.type) {
434
- case 'response.created':
435
- state.id = event.response.id;
436
- state.model = event.response.model;
437
- events.push({ type: 'message_start', index: 0, delta: {} });
438
- break;
439
-
440
- case 'response.in_progress':
441
- state.status = 'in_progress';
442
- break;
443
-
444
- case 'response.completed':
445
- case 'response.done':
446
- // Handle both event type names (OpenRouter docs show response.done)
447
- state.status = 'completed';
448
- if (event.response?.usage) {
449
- state.inputTokens = event.response.usage.input_tokens;
450
- state.outputTokens = event.response.usage.output_tokens;
451
- }
452
- // CRITICAL: OpenRouter's streaming doesn't send function call arguments incrementally.
453
- // They only appear in the final response.completed event's output array.
454
- // We must extract them here to populate state.toolCalls with actual arguments.
455
- if (event.response?.output) {
456
- for (let i = 0; i < event.response.output.length; i++) {
457
- const item = event.response.output[i];
458
- if (item && item.type === 'function_call') {
459
- const functionCall = item as OpenRouterResponsesFunctionCallOutput;
460
- const existing = state.toolCalls.get(i) ?? { arguments: '' };
461
- existing.itemId = functionCall.id ?? existing.itemId;
462
- existing.callId = functionCall.call_id ?? existing.callId;
463
- existing.name = functionCall.name ?? existing.name;
464
- // This is the key fix: get arguments from the completed response
465
- if (functionCall.arguments) {
466
- existing.arguments = functionCall.arguments;
467
- }
468
- state.toolCalls.set(i, existing);
469
- }
470
- }
471
- }
472
- events.push({ type: 'message_stop', index: 0, delta: {} });
473
- break;
474
-
475
- case 'response.failed':
476
- state.status = 'failed';
477
- events.push({ type: 'message_stop', index: 0, delta: {} });
478
- break;
479
-
480
- case 'response.output_item.added':
481
- if (event.item.type === 'function_call') {
482
- const functionCall = event.item as OpenRouterResponsesFunctionCallOutput;
483
- const existing = state.toolCalls.get(event.output_index) ?? {
484
- arguments: '',
485
- };
486
- existing.itemId = functionCall.id;
487
- existing.callId = functionCall.call_id;
488
- existing.name = functionCall.name;
489
- if (functionCall.arguments) {
490
- existing.arguments = functionCall.arguments;
491
- }
492
- state.toolCalls.set(event.output_index, existing);
493
- }
494
- events.push({
495
- type: 'content_block_start',
496
- index: event.output_index,
497
- delta: {},
498
- });
499
- break;
500
-
501
- case 'response.output_item.done':
502
- if (event.item.type === 'function_call') {
503
- const functionCall = event.item as OpenRouterResponsesFunctionCallOutput;
504
- const existing = state.toolCalls.get(event.output_index) ?? {
505
- arguments: '',
506
- };
507
- existing.itemId = functionCall.id;
508
- existing.callId = functionCall.call_id;
509
- existing.name = functionCall.name;
510
- if (functionCall.arguments) {
511
- existing.arguments = functionCall.arguments;
512
- }
513
- state.toolCalls.set(event.output_index, existing);
514
- } else if (event.item.type === 'message') {
515
- // Extract text from completed message item (may not have incremental deltas)
516
- const messageItem = event.item as OpenRouterResponsesMessageOutput;
517
- for (const content of messageItem.content || []) {
518
- if (content.type === 'output_text') {
519
- const existingText = state.textByIndex.get(event.output_index) ?? '';
520
- if (!existingText && content.text) {
521
- // Only use done text if we didn't get incremental deltas
522
- state.textByIndex.set(event.output_index, content.text);
523
- events.push({
524
- type: 'text_delta',
525
- index: event.output_index,
526
- delta: { text: content.text },
527
- });
528
- }
529
- }
530
- }
531
- }
532
- events.push({
533
- type: 'content_block_stop',
534
- index: event.output_index,
535
- delta: {},
536
- });
537
- break;
538
-
539
- case 'response.content_part.delta':
540
- case 'response.output_text.delta': {
541
- // Handle both event type names (OpenRouter docs vs OpenAI-style)
542
- // Accumulate text
543
- const textDelta = (event as { delta: string }).delta;
544
- const currentText = state.textByIndex.get(event.output_index) ?? '';
545
- state.textByIndex.set(event.output_index, currentText + textDelta);
546
- events.push({
547
- type: 'text_delta',
548
- index: event.output_index,
549
- delta: { text: textDelta },
550
- });
551
- break;
552
- }
553
-
554
- case 'response.output_text.done':
555
- case 'response.content_part.done':
556
- // Handle both event type names
557
- if ('text' in event) {
558
- state.textByIndex.set(event.output_index, (event as { text: string }).text);
559
- }
560
- break;
561
-
562
- case 'response.refusal.delta': {
563
- state.hadRefusal = true;
564
- const currentRefusal = state.textByIndex.get(event.output_index) ?? '';
565
- state.textByIndex.set(event.output_index, currentRefusal + event.delta);
566
- events.push({
567
- type: 'text_delta',
568
- index: event.output_index,
569
- delta: { text: event.delta },
570
- });
571
- break;
572
- }
573
-
574
- case 'response.refusal.done':
575
- state.hadRefusal = true;
576
- state.textByIndex.set(event.output_index, event.refusal);
577
- break;
578
-
579
- case 'response.function_call_arguments.delta': {
580
- // Accumulate function call arguments
581
- let toolCall = state.toolCalls.get(event.output_index);
582
- if (!toolCall) {
583
- toolCall = { arguments: '' };
584
- state.toolCalls.set(event.output_index, toolCall);
585
- }
586
- if (event.item_id && !toolCall.itemId) {
587
- toolCall.itemId = event.item_id;
588
- }
589
- if (event.call_id && !toolCall.callId) {
590
- toolCall.callId = event.call_id;
591
- }
592
- toolCall.arguments += event.delta;
593
- events.push({
594
- type: 'tool_call_delta',
595
- index: event.output_index,
596
- delta: {
597
- toolCallId: toolCall.callId ?? toolCall.itemId ?? '',
598
- toolName: toolCall.name,
599
- argumentsJson: event.delta,
600
- },
601
- });
602
- break;
603
- }
604
-
605
- case 'response.function_call_arguments.done': {
606
- // Finalize function call
607
- let toolCall = state.toolCalls.get(event.output_index);
608
- if (!toolCall) {
609
- toolCall = { arguments: '' };
610
- state.toolCalls.set(event.output_index, toolCall);
611
- }
612
- if (event.item_id) {
613
- toolCall.itemId = event.item_id;
614
- }
615
- if (event.call_id) {
616
- toolCall.callId = event.call_id;
617
- }
618
- toolCall.name = event.name;
619
- toolCall.arguments = event.arguments;
620
- break;
621
- }
622
-
623
- case 'response.reasoning.delta':
624
- // Emit reasoning as a reasoning_delta event
625
- events.push({
626
- type: 'reasoning_delta',
627
- index: 0,
628
- delta: { text: event.delta },
629
- });
630
- break;
631
-
632
- case 'error':
633
- // Error events are handled at the handler level
634
- break;
635
-
636
- default:
637
- // Ignore other events
638
- break;
639
- }
640
-
641
- return events;
642
- }
643
-
644
- /**
645
- * Build LLMResponse from accumulated stream state
646
- */
647
- export function buildResponseFromState(state: ResponsesStreamState): LLMResponse {
648
- const textContent: TextBlock[] = [];
649
- let structuredData: unknown;
650
-
651
- // Combine all text content
652
- for (const [, text] of state.textByIndex) {
653
- if (text) {
654
- textContent.push({ type: 'text', text });
655
- // Try to parse as JSON for structured output (native JSON mode)
656
- if (structuredData === undefined) {
657
- try {
658
- structuredData = JSON.parse(text);
659
- } catch {
660
- // Not valid JSON - that's fine, might not be structured output
661
- }
662
- }
663
- }
664
- }
665
-
666
- const toolCalls: ToolCall[] = [];
667
- const functionCallItems: Array<{
668
- id: string;
669
- call_id: string;
670
- name: string;
671
- arguments: string;
672
- }> = [];
673
- for (const [, toolCall] of state.toolCalls) {
674
- let args: Record<string, unknown> = {};
675
- if (toolCall.arguments) {
676
- try {
677
- args = JSON.parse(toolCall.arguments);
678
- } catch {
679
- // Invalid JSON - use empty object
680
- }
681
- }
682
- const itemId = toolCall.itemId ?? '';
683
- const callId = toolCall.callId ?? toolCall.itemId ?? '';
684
- const name = toolCall.name ?? '';
685
- toolCalls.push({
686
- toolCallId: callId,
687
- toolName: name,
688
- arguments: args,
689
- });
690
-
691
- if (itemId && callId && name) {
692
- functionCallItems.push({
693
- id: itemId,
694
- call_id: callId,
695
- name,
696
- arguments: toolCall.arguments,
697
- });
698
- }
699
- }
700
-
701
- const message = new AssistantMessage(
702
- textContent,
703
- toolCalls.length > 0 ? toolCalls : undefined,
704
- {
705
- id: state.id,
706
- metadata: {
707
- openrouter: {
708
- model: state.model,
709
- status: state.status,
710
- // Store response_id for multi-turn tool calling
711
- response_id: state.id,
712
- functionCallItems:
713
- functionCallItems.length > 0 ? functionCallItems : undefined,
714
- },
715
- },
716
- }
717
- );
718
-
719
- const usage: TokenUsage = {
720
- inputTokens: state.inputTokens,
721
- outputTokens: state.outputTokens,
722
- totalTokens: state.inputTokens + state.outputTokens,
723
- };
724
-
725
- // Map status to stop reason
726
- let stopReason = 'end_turn';
727
- if (state.status === 'completed') {
728
- stopReason = toolCalls.length > 0 ? 'tool_use' : 'end_turn';
729
- } else if (state.status === 'failed') {
730
- stopReason = 'error';
731
- }
732
- if (state.hadRefusal && stopReason !== 'error') {
733
- stopReason = 'content_filter';
734
- }
735
-
736
- return {
737
- message,
738
- usage,
739
- stopReason,
740
- data: structuredData,
741
- };
742
- }