@openrouter/sdk 0.3.7 → 0.3.11

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 (42) hide show
  1. package/.zed/settings.json +10 -0
  2. package/_speakeasy/.github/action-inputs-config.json +53 -0
  3. package/_speakeasy/.github/action-security-config.json +88 -0
  4. package/esm/funcs/call-model.d.ts +94 -9
  5. package/esm/funcs/call-model.js +102 -120
  6. package/esm/index.d.ts +20 -8
  7. package/esm/index.js +20 -7
  8. package/esm/lib/anthropic-compat.d.ts +6 -2
  9. package/esm/lib/anthropic-compat.js +117 -98
  10. package/esm/lib/async-params.d.ts +53 -0
  11. package/esm/lib/async-params.js +76 -0
  12. package/esm/lib/chat-compat.js +4 -0
  13. package/esm/lib/claude-constants.d.ts +22 -0
  14. package/esm/lib/claude-constants.js +20 -0
  15. package/esm/lib/claude-type-guards.d.ts +10 -0
  16. package/esm/lib/claude-type-guards.js +70 -0
  17. package/esm/lib/config.d.ts +4 -2
  18. package/esm/lib/config.js +2 -2
  19. package/esm/lib/model-result.d.ts +18 -25
  20. package/esm/lib/model-result.js +137 -176
  21. package/esm/lib/next-turn-params.d.ts +30 -0
  22. package/esm/lib/next-turn-params.js +129 -0
  23. package/esm/lib/reusable-stream.js +10 -10
  24. package/esm/lib/stop-conditions.d.ts +80 -0
  25. package/esm/lib/stop-conditions.js +104 -0
  26. package/esm/lib/stream-transformers.d.ts +3 -3
  27. package/esm/lib/stream-transformers.js +311 -260
  28. package/esm/lib/stream-type-guards.d.ts +29 -0
  29. package/esm/lib/stream-type-guards.js +109 -0
  30. package/esm/lib/tool-executor.d.ts +9 -7
  31. package/esm/lib/tool-executor.js +7 -1
  32. package/esm/lib/tool-orchestrator.d.ts +7 -7
  33. package/esm/lib/tool-orchestrator.js +38 -10
  34. package/esm/lib/tool-types.d.ts +163 -29
  35. package/esm/lib/tool-types.js +6 -0
  36. package/esm/lib/tool.d.ts +99 -0
  37. package/esm/lib/tool.js +71 -0
  38. package/esm/lib/turn-context.d.ts +50 -0
  39. package/esm/lib/turn-context.js +59 -0
  40. package/esm/sdk/sdk.d.ts +3 -9
  41. package/jsr.json +1 -1
  42. package/package.json +6 -3
@@ -1,13 +1,13 @@
1
+ import { isOutputTextDeltaEvent, isReasoningDeltaEvent, isFunctionCallArgumentsDeltaEvent, isOutputItemAddedEvent, isOutputItemDoneEvent, isResponseCompletedEvent, isResponseFailedEvent, isResponseIncompleteEvent, isFunctionCallArgumentsDoneEvent, isOutputMessage, isFunctionCallOutputItem, isReasoningOutputItem, isWebSearchCallOutputItem, isFileSearchCallOutputItem, isImageGenerationCallOutputItem, isOutputTextPart, isRefusalPart, isFileCitationAnnotation, isURLCitationAnnotation, isFilePathAnnotation, } from './stream-type-guards.js';
1
2
  /**
2
3
  * Extract text deltas from responses stream events
3
4
  */
4
5
  export async function* extractTextDeltas(stream) {
5
6
  const consumer = stream.createConsumer();
6
7
  for await (const event of consumer) {
7
- if ('type' in event && event.type === 'response.output_text.delta') {
8
- const deltaEvent = event;
9
- if (deltaEvent.delta) {
10
- yield deltaEvent.delta;
8
+ if (isOutputTextDeltaEvent(event)) {
9
+ if (event.delta) {
10
+ yield event.delta;
11
11
  }
12
12
  }
13
13
  }
@@ -18,10 +18,9 @@ export async function* extractTextDeltas(stream) {
18
18
  export async function* extractReasoningDeltas(stream) {
19
19
  const consumer = stream.createConsumer();
20
20
  for await (const event of consumer) {
21
- if ('type' in event && event.type === 'response.reasoning_text.delta') {
22
- const deltaEvent = event;
23
- if (deltaEvent.delta) {
24
- yield deltaEvent.delta;
21
+ if (isReasoningDeltaEvent(event)) {
22
+ if (event.delta) {
23
+ yield event.delta;
25
24
  }
26
25
  }
27
26
  }
@@ -32,19 +31,18 @@ export async function* extractReasoningDeltas(stream) {
32
31
  export async function* extractToolDeltas(stream) {
33
32
  const consumer = stream.createConsumer();
34
33
  for await (const event of consumer) {
35
- if ('type' in event && event.type === 'response.function_call_arguments.delta') {
36
- const deltaEvent = event;
37
- if (deltaEvent.delta) {
38
- yield deltaEvent.delta;
34
+ if (isFunctionCallArgumentsDeltaEvent(event)) {
35
+ if (event.delta) {
36
+ yield event.delta;
39
37
  }
40
38
  }
41
39
  }
42
40
  }
43
41
  /**
44
- * Build incremental message updates from responses stream events
45
- * Returns ResponsesOutputMessage (assistant/responses format)
42
+ * Core message stream builder - shared logic for both formats
43
+ * Accumulates text deltas and yields updates
46
44
  */
47
- export async function* buildResponsesMessageStream(stream) {
45
+ async function* buildMessageStreamCore(stream) {
48
46
  const consumer = stream.createConsumer();
49
47
  // Track the accumulated text and message info
50
48
  let currentText = '';
@@ -56,47 +54,76 @@ export async function* buildResponsesMessageStream(stream) {
56
54
  }
57
55
  switch (event.type) {
58
56
  case 'response.output_item.added': {
59
- const itemEvent = event;
60
- if (itemEvent.item && 'type' in itemEvent.item && itemEvent.item.type === 'message') {
61
- hasStarted = true;
62
- currentText = '';
63
- const msgItem = itemEvent.item;
64
- currentId = msgItem.id;
57
+ if (isOutputItemAddedEvent(event)) {
58
+ if (event.item && isOutputMessage(event.item)) {
59
+ hasStarted = true;
60
+ currentText = '';
61
+ currentId = event.item.id;
62
+ }
65
63
  }
66
64
  break;
67
65
  }
68
66
  case 'response.output_text.delta': {
69
- const deltaEvent = event;
70
- if (hasStarted && deltaEvent.delta) {
71
- currentText += deltaEvent.delta;
72
- // Yield updated message in ResponsesOutputMessage format
73
- yield {
74
- id: currentId,
75
- type: 'message',
76
- role: 'assistant',
77
- status: 'in_progress',
78
- content: [
79
- {
80
- type: 'output_text',
81
- text: currentText,
82
- annotations: [],
83
- },
84
- ],
85
- };
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
+ }
86
76
  }
87
77
  break;
88
78
  }
89
79
  case 'response.output_item.done': {
90
- const itemDoneEvent = event;
91
- if (itemDoneEvent.item &&
92
- 'type' in itemDoneEvent.item &&
93
- itemDoneEvent.item.type === 'message') {
94
- // Yield final complete message in ResponsesOutputMessage format
95
- const outputMessage = itemDoneEvent.item;
96
- yield outputMessage;
80
+ if (isOutputItemDoneEvent(event)) {
81
+ if (event.item && isOutputMessage(event.item)) {
82
+ yield {
83
+ type: 'complete',
84
+ completeMessage: event.item,
85
+ };
86
+ }
97
87
  }
98
88
  break;
99
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 ResponsesOutputMessage (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 ResponsesOutputMessage 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;
100
127
  }
101
128
  }
102
129
  }
@@ -105,46 +132,17 @@ export async function* buildResponsesMessageStream(stream) {
105
132
  * Returns AssistantMessage (chat format) instead of ResponsesOutputMessage
106
133
  */
107
134
  export async function* buildMessageStream(stream) {
108
- const consumer = stream.createConsumer();
109
- // Track the accumulated text
110
- let currentText = '';
111
- let hasStarted = false;
112
- for await (const event of consumer) {
113
- if (!('type' in event)) {
114
- continue;
135
+ for await (const update of buildMessageStreamCore(stream)) {
136
+ if (update.type === 'delta' && update.text !== undefined) {
137
+ // Yield incremental update in chat format
138
+ yield {
139
+ role: 'assistant',
140
+ content: update.text,
141
+ };
115
142
  }
116
- switch (event.type) {
117
- case 'response.output_item.added': {
118
- const itemEvent = event;
119
- if (itemEvent.item && 'type' in itemEvent.item && itemEvent.item.type === 'message') {
120
- hasStarted = true;
121
- currentText = '';
122
- }
123
- break;
124
- }
125
- case 'response.output_text.delta': {
126
- const deltaEvent = event;
127
- if (hasStarted && deltaEvent.delta) {
128
- currentText += deltaEvent.delta;
129
- // Yield updated message
130
- yield {
131
- role: 'assistant',
132
- content: currentText,
133
- };
134
- }
135
- break;
136
- }
137
- case 'response.output_item.done': {
138
- const itemDoneEvent = event;
139
- if (itemDoneEvent.item &&
140
- 'type' in itemDoneEvent.item &&
141
- itemDoneEvent.item.type === 'message') {
142
- // Yield final complete message
143
- const outputMessage = itemDoneEvent.item;
144
- yield convertToAssistantMessage(outputMessage);
145
- }
146
- break;
147
- }
143
+ else if (update.type === 'complete' && update.completeMessage) {
144
+ // Yield final complete message converted to chat format
145
+ yield convertToAssistantMessage(update.completeMessage);
148
146
  }
149
147
  }
150
148
  }
@@ -157,19 +155,16 @@ export async function consumeStreamForCompletion(stream) {
157
155
  if (!('type' in event)) {
158
156
  continue;
159
157
  }
160
- if (event.type === 'response.completed') {
161
- const completedEvent = event;
162
- return completedEvent.response;
158
+ if (isResponseCompletedEvent(event)) {
159
+ return event.response;
163
160
  }
164
- if (event.type === 'response.failed') {
165
- const failedEvent = event;
161
+ if (isResponseFailedEvent(event)) {
166
162
  // The failed event contains the full response with error information
167
- throw new Error(`Response failed: ${JSON.stringify(failedEvent.response.error)}`);
163
+ throw new Error(`Response failed: ${JSON.stringify(event.response.error)}`);
168
164
  }
169
- if (event.type === 'response.incomplete') {
170
- const incompleteEvent = event;
165
+ if (isResponseIncompleteEvent(event)) {
171
166
  // Return the incomplete response
172
- return incompleteEvent.response;
167
+ return event.response;
173
168
  }
174
169
  }
175
170
  throw new Error('Stream ended without completion event');
@@ -216,6 +211,12 @@ export function extractTextFromResponse(response) {
216
211
  if (response.outputText) {
217
212
  return response.outputText;
218
213
  }
214
+ // Check if there's a message in the output
215
+ const hasMessage = response.output.some((item) => 'type' in item && item.type === 'message');
216
+ if (!hasMessage) {
217
+ // No message in response (e.g., only function calls)
218
+ return '';
219
+ }
219
220
  // Otherwise, extract from the first message (convert to AssistantMessage which has string content)
220
221
  const message = extractMessageFromResponse(response);
221
222
  // AssistantMessage.content is string | Array | null | undefined
@@ -231,22 +232,22 @@ export function extractTextFromResponse(response) {
231
232
  export function extractToolCallsFromResponse(response) {
232
233
  const toolCalls = [];
233
234
  for (const item of response.output) {
234
- if ('type' in item && item.type === 'function_call') {
235
- const functionCallItem = item;
235
+ if (isFunctionCallOutputItem(item)) {
236
236
  try {
237
- const parsedArguments = JSON.parse(functionCallItem.arguments);
237
+ const parsedArguments = JSON.parse(item.arguments);
238
238
  toolCalls.push({
239
- id: functionCallItem.callId,
240
- name: functionCallItem.name,
239
+ id: item.callId,
240
+ name: item.name,
241
241
  arguments: parsedArguments,
242
242
  });
243
243
  }
244
- catch (_error) {
244
+ catch (error) {
245
+ 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 ? '...' : ''}`);
245
246
  // Include the tool call with unparsed arguments
246
247
  toolCalls.push({
247
- id: functionCallItem.callId,
248
- name: functionCallItem.name,
249
- arguments: functionCallItem.arguments, // Keep as string if parsing fails
248
+ id: item.callId,
249
+ name: item.name,
250
+ arguments: item.arguments, // Keep as string if parsing fails
250
251
  });
251
252
  }
252
253
  }
@@ -267,75 +268,72 @@ export async function* buildToolCallStream(stream) {
267
268
  }
268
269
  switch (event.type) {
269
270
  case 'response.output_item.added': {
270
- const itemEvent = event;
271
- if (itemEvent.item && 'type' in itemEvent.item && itemEvent.item.type === 'function_call') {
272
- const functionCallItem = itemEvent.item;
273
- toolCallsInProgress.set(functionCallItem.callId, {
274
- id: functionCallItem.callId,
275
- name: functionCallItem.name,
271
+ if (isOutputItemAddedEvent(event) && event.item && isFunctionCallOutputItem(event.item)) {
272
+ toolCallsInProgress.set(event.item.callId, {
273
+ id: event.item.callId,
274
+ name: event.item.name,
276
275
  argumentsAccumulated: '',
277
276
  });
278
277
  }
279
278
  break;
280
279
  }
281
280
  case 'response.function_call_arguments.delta': {
282
- const deltaEvent = event;
283
- const toolCall = toolCallsInProgress.get(deltaEvent.itemId);
284
- if (toolCall && deltaEvent.delta) {
285
- toolCall.argumentsAccumulated += deltaEvent.delta;
281
+ if (isFunctionCallArgumentsDeltaEvent(event)) {
282
+ const toolCall = toolCallsInProgress.get(event.itemId);
283
+ if (toolCall && event.delta) {
284
+ toolCall.argumentsAccumulated += event.delta;
285
+ }
286
286
  }
287
287
  break;
288
288
  }
289
289
  case 'response.function_call_arguments.done': {
290
- const doneEvent = event;
291
- const toolCall = toolCallsInProgress.get(doneEvent.itemId);
292
- if (toolCall) {
293
- // Parse complete arguments
294
- try {
295
- const parsedArguments = JSON.parse(doneEvent.arguments);
296
- yield {
297
- id: toolCall.id,
298
- name: doneEvent.name,
299
- arguments: parsedArguments,
300
- };
301
- }
302
- catch (_error) {
303
- // Yield with unparsed arguments if parsing fails
304
- yield {
305
- id: toolCall.id,
306
- name: doneEvent.name,
307
- arguments: doneEvent.arguments,
308
- };
290
+ if (isFunctionCallArgumentsDoneEvent(event)) {
291
+ const toolCall = toolCallsInProgress.get(event.itemId);
292
+ if (toolCall) {
293
+ // Parse complete arguments
294
+ try {
295
+ const parsedArguments = JSON.parse(event.arguments);
296
+ yield {
297
+ id: toolCall.id,
298
+ name: event.name,
299
+ arguments: parsedArguments,
300
+ };
301
+ }
302
+ catch (error) {
303
+ 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 ? '...' : ''}`);
304
+ // Yield with unparsed arguments if parsing fails
305
+ yield {
306
+ id: toolCall.id,
307
+ name: event.name,
308
+ arguments: event.arguments,
309
+ };
310
+ }
311
+ // Clean up
312
+ toolCallsInProgress.delete(event.itemId);
309
313
  }
310
- // Clean up
311
- toolCallsInProgress.delete(doneEvent.itemId);
312
314
  }
313
315
  break;
314
316
  }
315
317
  case 'response.output_item.done': {
316
- const itemDoneEvent = event;
317
- if (itemDoneEvent.item &&
318
- 'type' in itemDoneEvent.item &&
319
- itemDoneEvent.item.type === 'function_call') {
320
- const functionCallItem = itemDoneEvent.item;
318
+ if (isOutputItemDoneEvent(event) && event.item && isFunctionCallOutputItem(event.item)) {
321
319
  // Yield final tool call if we haven't already
322
- if (toolCallsInProgress.has(functionCallItem.callId)) {
320
+ if (toolCallsInProgress.has(event.item.callId)) {
323
321
  try {
324
- const parsedArguments = JSON.parse(functionCallItem.arguments);
322
+ const parsedArguments = JSON.parse(event.item.arguments);
325
323
  yield {
326
- id: functionCallItem.callId,
327
- name: functionCallItem.name,
324
+ id: event.item.callId,
325
+ name: event.item.name,
328
326
  arguments: parsedArguments,
329
327
  };
330
328
  }
331
329
  catch (_error) {
332
330
  yield {
333
- id: functionCallItem.callId,
334
- name: functionCallItem.name,
335
- arguments: functionCallItem.arguments,
331
+ id: event.item.callId,
332
+ name: event.item.name,
333
+ arguments: event.item.arguments,
336
334
  };
337
335
  }
338
- toolCallsInProgress.delete(functionCallItem.callId);
336
+ toolCallsInProgress.delete(event.item.callId);
339
337
  }
340
338
  }
341
339
  break;
@@ -363,45 +361,52 @@ function mapAnnotationsToCitations(annotations) {
363
361
  }
364
362
  switch (annotation.type) {
365
363
  case 'file_citation': {
366
- const fileCite = annotation;
367
- citations.push({
368
- type: 'char_location',
369
- cited_text: '',
370
- document_index: fileCite.index,
371
- document_title: fileCite.filename,
372
- file_id: fileCite.fileId,
373
- start_char_index: 0,
374
- end_char_index: 0,
375
- });
364
+ if (isFileCitationAnnotation(annotation)) {
365
+ citations.push({
366
+ type: 'char_location',
367
+ cited_text: '',
368
+ document_index: annotation.index,
369
+ document_title: annotation.filename,
370
+ file_id: annotation.fileId,
371
+ start_char_index: 0,
372
+ end_char_index: 0,
373
+ });
374
+ }
376
375
  break;
377
376
  }
378
377
  case 'url_citation': {
379
- const urlCite = annotation;
380
- citations.push({
381
- type: 'web_search_result_location',
382
- cited_text: '',
383
- title: urlCite.title,
384
- url: urlCite.url,
385
- encrypted_index: '',
386
- });
378
+ if (isURLCitationAnnotation(annotation)) {
379
+ citations.push({
380
+ type: 'web_search_result_location',
381
+ cited_text: '',
382
+ title: annotation.title,
383
+ url: annotation.url,
384
+ encrypted_index: '',
385
+ });
386
+ }
387
387
  break;
388
388
  }
389
389
  case 'file_path': {
390
- const pathCite = annotation;
391
- citations.push({
392
- type: 'char_location',
393
- cited_text: '',
394
- document_index: pathCite.index,
395
- document_title: '',
396
- file_id: pathCite.fileId,
397
- start_char_index: 0,
398
- end_char_index: 0,
399
- });
390
+ if (isFilePathAnnotation(annotation)) {
391
+ citations.push({
392
+ type: 'char_location',
393
+ cited_text: '',
394
+ document_index: annotation.index,
395
+ document_title: '',
396
+ file_id: annotation.fileId,
397
+ start_char_index: 0,
398
+ end_char_index: 0,
399
+ });
400
+ }
400
401
  break;
401
402
  }
402
403
  default: {
403
- const _exhaustiveCheck = annotation;
404
- throw new Error(`Unhandled annotation type: ${_exhaustiveCheck.type}`);
404
+ // Exhaustiveness check - TypeScript will error if we don't handle all annotation types
405
+ const exhaustiveCheck = annotation;
406
+ // Cast to unknown for runtime debugging if type system bypassed
407
+ // This should never execute - throw with JSON of the unhandled value
408
+ throw new Error(`Unhandled annotation type. This indicates a new annotation type was added. ` +
409
+ `Annotation: ${JSON.stringify(exhaustiveCheck)}`);
405
410
  }
406
411
  }
407
412
  }
@@ -439,116 +444,160 @@ export function convertToClaudeMessage(response) {
439
444
  const unsupportedContent = [];
440
445
  for (const item of response.output) {
441
446
  if (!('type' in item)) {
447
+ // Handle items without type field
448
+ // Convert unknown item to a record format for storage
449
+ const itemData = typeof item === 'object' && item !== null
450
+ ? item
451
+ : { value: item };
452
+ unsupportedContent.push({
453
+ original_type: 'unknown',
454
+ data: itemData,
455
+ reason: 'Output item missing type field',
456
+ });
442
457
  continue;
443
458
  }
444
459
  switch (item.type) {
445
460
  case 'message': {
446
- const msgItem = item;
447
- for (const part of msgItem.content) {
448
- if (!('type' in part)) {
449
- continue;
450
- }
451
- if (part.type === 'output_text') {
452
- const textPart = part;
453
- const citations = mapAnnotationsToCitations(textPart.annotations);
454
- content.push({
455
- type: 'text',
456
- text: textPart.text,
457
- ...(citations && { citations }),
458
- });
459
- }
460
- else if (part.type === 'refusal') {
461
- const refusalPart = part;
462
- unsupportedContent.push({
463
- original_type: 'refusal',
464
- data: { refusal: refusalPart.refusal },
465
- reason: 'Claude does not have a native refusal content type',
466
- });
461
+ if (isOutputMessage(item)) {
462
+ for (const part of item.content) {
463
+ if (!('type' in part)) {
464
+ // Convert unknown part to a record format for storage
465
+ const partData = typeof part === 'object' && part !== null
466
+ ? part
467
+ : { value: part };
468
+ unsupportedContent.push({
469
+ original_type: 'unknown_message_part',
470
+ data: partData,
471
+ reason: 'Message content part missing type field',
472
+ });
473
+ continue;
474
+ }
475
+ if (isOutputTextPart(part)) {
476
+ const citations = mapAnnotationsToCitations(part.annotations);
477
+ content.push({
478
+ type: 'text',
479
+ text: part.text,
480
+ ...(citations && {
481
+ citations,
482
+ }),
483
+ });
484
+ }
485
+ else if (isRefusalPart(part)) {
486
+ unsupportedContent.push({
487
+ original_type: 'refusal',
488
+ data: {
489
+ refusal: part.refusal,
490
+ },
491
+ reason: 'Claude does not have a native refusal content type',
492
+ });
493
+ }
494
+ else {
495
+ // Exhaustiveness check - TypeScript will error if we don't handle all part types
496
+ const exhaustiveCheck = part;
497
+ // This should never execute - new content type was added
498
+ throw new Error(`Unhandled message content type. This indicates a new content type was added. ` +
499
+ `Part: ${JSON.stringify(exhaustiveCheck)}`);
500
+ }
467
501
  }
468
502
  }
469
503
  break;
470
504
  }
471
505
  case 'function_call': {
472
- const fnCall = item;
473
- let parsedInput = {};
474
- try {
475
- parsedInput = JSON.parse(fnCall.arguments);
476
- }
477
- catch {
478
- parsedInput = {};
506
+ if (isFunctionCallOutputItem(item)) {
507
+ let parsedInput;
508
+ try {
509
+ parsedInput = JSON.parse(item.arguments);
510
+ }
511
+ catch (error) {
512
+ 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 ? '...' : ''}`);
513
+ // Preserve raw arguments if JSON parsing fails
514
+ parsedInput = {
515
+ _raw_arguments: item.arguments,
516
+ };
517
+ }
518
+ content.push({
519
+ type: 'tool_use',
520
+ id: item.callId,
521
+ name: item.name,
522
+ input: parsedInput,
523
+ });
479
524
  }
480
- content.push({
481
- type: 'tool_use',
482
- id: fnCall.callId,
483
- name: fnCall.name,
484
- input: parsedInput,
485
- });
486
525
  break;
487
526
  }
488
527
  case 'reasoning': {
489
- const reasoningItem = item;
490
- if (reasoningItem.summary && reasoningItem.summary.length > 0) {
491
- for (const summaryItem of reasoningItem.summary) {
492
- if (summaryItem.type === 'summary_text' && summaryItem.text) {
493
- content.push({
494
- type: 'thinking',
495
- thinking: summaryItem.text,
496
- signature: '',
497
- });
528
+ if (isReasoningOutputItem(item)) {
529
+ if (item.summary && item.summary.length > 0) {
530
+ for (const summaryItem of item.summary) {
531
+ if (summaryItem.type === 'summary_text' && summaryItem.text) {
532
+ content.push({
533
+ type: 'thinking',
534
+ thinking: summaryItem.text,
535
+ signature: '',
536
+ });
537
+ }
498
538
  }
499
539
  }
500
- }
501
- if (reasoningItem.encryptedContent) {
502
- unsupportedContent.push({
503
- original_type: 'reasoning_encrypted',
504
- data: {
505
- id: reasoningItem.id,
506
- encrypted_content: reasoningItem.encryptedContent,
507
- },
508
- reason: 'Encrypted reasoning content preserved for round-trip',
509
- });
540
+ if (item.encryptedContent) {
541
+ unsupportedContent.push({
542
+ original_type: 'reasoning_encrypted',
543
+ data: {
544
+ id: item.id,
545
+ encrypted_content: item.encryptedContent,
546
+ },
547
+ reason: 'Encrypted reasoning content preserved for round-trip',
548
+ });
549
+ }
510
550
  }
511
551
  break;
512
552
  }
513
553
  case 'web_search_call': {
514
- const webSearchItem = item;
515
- content.push({
516
- type: 'server_tool_use',
517
- id: webSearchItem.id,
518
- name: 'web_search',
519
- input: { status: webSearchItem.status },
520
- });
554
+ if (isWebSearchCallOutputItem(item)) {
555
+ content.push({
556
+ type: 'server_tool_use',
557
+ id: item.id,
558
+ name: 'web_search',
559
+ input: {
560
+ status: item.status,
561
+ },
562
+ });
563
+ }
521
564
  break;
522
565
  }
523
566
  case 'file_search_call': {
524
- const fileSearchItem = item;
525
- content.push({
526
- type: 'tool_use',
527
- id: fileSearchItem.id,
528
- name: 'file_search',
529
- input: {
530
- queries: fileSearchItem.queries,
531
- status: fileSearchItem.status,
532
- },
533
- });
567
+ if (isFileSearchCallOutputItem(item)) {
568
+ content.push({
569
+ type: 'tool_use',
570
+ id: item.id,
571
+ name: 'file_search',
572
+ input: {
573
+ queries: item.queries,
574
+ status: item.status,
575
+ },
576
+ });
577
+ }
534
578
  break;
535
579
  }
536
580
  case 'image_generation_call': {
537
- const imageGenItem = item;
538
- unsupportedContent.push({
539
- original_type: 'image_generation_call',
540
- data: {
541
- id: imageGenItem.id,
542
- result: imageGenItem.result,
543
- status: imageGenItem.status,
544
- },
545
- reason: 'Claude does not support image outputs in assistant messages',
546
- });
581
+ if (isImageGenerationCallOutputItem(item)) {
582
+ unsupportedContent.push({
583
+ original_type: 'image_generation_call',
584
+ data: {
585
+ id: item.id,
586
+ result: item.result,
587
+ status: item.status,
588
+ },
589
+ reason: 'Claude does not support image outputs in assistant messages',
590
+ });
591
+ }
547
592
  break;
548
593
  }
549
594
  default: {
550
- const _exhaustiveCheck = item;
551
- throw new Error(`Unhandled output item type: ${_exhaustiveCheck.type}`);
595
+ // Exhaustiveness check - if a new output type is added, TypeScript will error here
596
+ const exhaustiveCheck = item;
597
+ // This line should never execute - it means a new type was added to the union
598
+ // Throw an error instead of silently continuing to ensure we catch new types
599
+ throw new Error(`Unhandled output item type. This indicates a new output type was added to the API. ` +
600
+ `Item: ${JSON.stringify(exhaustiveCheck)}`);
552
601
  }
553
602
  }
554
603
  }
@@ -566,7 +615,9 @@ export function convertToClaudeMessage(response) {
566
615
  cache_creation_input_tokens: response.usage?.inputTokensDetails?.cachedTokens ?? 0,
567
616
  cache_read_input_tokens: 0,
568
617
  },
569
- ...(unsupportedContent.length > 0 && { unsupported_content: unsupportedContent }),
618
+ ...(unsupportedContent.length > 0 && {
619
+ unsupported_content: unsupportedContent,
620
+ }),
570
621
  };
571
622
  }
572
623
  /**
@@ -576,7 +627,7 @@ export function extractUnsupportedContent(message, originalType) {
576
627
  if (!message.unsupported_content) {
577
628
  return [];
578
629
  }
579
- return message.unsupported_content.filter(item => item.original_type === originalType);
630
+ return message.unsupported_content.filter((item) => item.original_type === originalType);
580
631
  }
581
632
  /**
582
633
  * Check if message has any unsupported content