@librechat/agents 3.1.73 → 3.1.75-dev.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.
Files changed (159) hide show
  1. package/README.md +66 -0
  2. package/dist/cjs/agents/AgentContext.cjs +146 -57
  3. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  4. package/dist/cjs/graphs/Graph.cjs +13 -3
  5. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  6. package/dist/cjs/llm/anthropic/index.cjs +145 -52
  7. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  8. package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
  9. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +25 -15
  10. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  11. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +84 -70
  12. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
  13. package/dist/cjs/llm/bedrock/index.cjs +1 -1
  14. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  15. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +213 -3
  16. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  17. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs +2 -1
  18. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs.map +1 -1
  19. package/dist/cjs/llm/google/utils/common.cjs +5 -4
  20. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  21. package/dist/cjs/llm/openai/index.cjs +468 -647
  22. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  23. package/dist/cjs/llm/openai/utils/index.cjs +1 -448
  24. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  25. package/dist/cjs/llm/openrouter/index.cjs +57 -175
  26. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  27. package/dist/cjs/llm/vertexai/index.cjs +5 -3
  28. package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
  29. package/dist/cjs/main.cjs +1 -0
  30. package/dist/cjs/main.cjs.map +1 -1
  31. package/dist/cjs/messages/cache.cjs +39 -4
  32. package/dist/cjs/messages/cache.cjs.map +1 -1
  33. package/dist/cjs/messages/core.cjs +7 -6
  34. package/dist/cjs/messages/core.cjs.map +1 -1
  35. package/dist/cjs/messages/format.cjs +7 -6
  36. package/dist/cjs/messages/format.cjs.map +1 -1
  37. package/dist/cjs/messages/langchain.cjs +26 -0
  38. package/dist/cjs/messages/langchain.cjs.map +1 -0
  39. package/dist/cjs/messages/prune.cjs +7 -6
  40. package/dist/cjs/messages/prune.cjs.map +1 -1
  41. package/dist/cjs/tools/BashExecutor.cjs +21 -11
  42. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  43. package/dist/cjs/tools/CodeExecutor.cjs +37 -10
  44. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  45. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +16 -11
  46. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  47. package/dist/cjs/tools/ToolNode.cjs +5 -1
  48. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  49. package/dist/esm/agents/AgentContext.mjs +147 -58
  50. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  51. package/dist/esm/graphs/Graph.mjs +13 -3
  52. package/dist/esm/graphs/Graph.mjs.map +1 -1
  53. package/dist/esm/llm/anthropic/index.mjs +146 -54
  54. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  55. package/dist/esm/llm/anthropic/types.mjs.map +1 -1
  56. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +25 -15
  57. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  58. package/dist/esm/llm/anthropic/utils/message_outputs.mjs +84 -71
  59. package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
  60. package/dist/esm/llm/bedrock/index.mjs +1 -1
  61. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  62. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +214 -4
  63. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  64. package/dist/esm/llm/bedrock/utils/message_outputs.mjs +2 -1
  65. package/dist/esm/llm/bedrock/utils/message_outputs.mjs.map +1 -1
  66. package/dist/esm/llm/google/utils/common.mjs +5 -4
  67. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  68. package/dist/esm/llm/openai/index.mjs +469 -648
  69. package/dist/esm/llm/openai/index.mjs.map +1 -1
  70. package/dist/esm/llm/openai/utils/index.mjs +4 -449
  71. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  72. package/dist/esm/llm/openrouter/index.mjs +57 -175
  73. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  74. package/dist/esm/llm/vertexai/index.mjs +5 -3
  75. package/dist/esm/llm/vertexai/index.mjs.map +1 -1
  76. package/dist/esm/main.mjs +1 -1
  77. package/dist/esm/messages/cache.mjs +39 -4
  78. package/dist/esm/messages/cache.mjs.map +1 -1
  79. package/dist/esm/messages/core.mjs +7 -6
  80. package/dist/esm/messages/core.mjs.map +1 -1
  81. package/dist/esm/messages/format.mjs +7 -6
  82. package/dist/esm/messages/format.mjs.map +1 -1
  83. package/dist/esm/messages/langchain.mjs +23 -0
  84. package/dist/esm/messages/langchain.mjs.map +1 -0
  85. package/dist/esm/messages/prune.mjs +7 -6
  86. package/dist/esm/messages/prune.mjs.map +1 -1
  87. package/dist/esm/tools/BashExecutor.mjs +22 -12
  88. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  89. package/dist/esm/tools/CodeExecutor.mjs +37 -11
  90. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  91. package/dist/esm/tools/ProgrammaticToolCalling.mjs +17 -12
  92. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  93. package/dist/esm/tools/ToolNode.mjs +5 -1
  94. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  95. package/dist/types/agents/AgentContext.d.ts +29 -4
  96. package/dist/types/agents/__tests__/promptCacheLiveHelpers.d.ts +46 -0
  97. package/dist/types/llm/anthropic/index.d.ts +22 -9
  98. package/dist/types/llm/anthropic/types.d.ts +5 -1
  99. package/dist/types/llm/anthropic/utils/message_outputs.d.ts +13 -6
  100. package/dist/types/llm/anthropic/utils/output_parsers.d.ts +1 -1
  101. package/dist/types/llm/openai/index.d.ts +21 -24
  102. package/dist/types/llm/openrouter/index.d.ts +11 -9
  103. package/dist/types/llm/vertexai/index.d.ts +1 -0
  104. package/dist/types/messages/cache.d.ts +4 -1
  105. package/dist/types/messages/langchain.d.ts +27 -0
  106. package/dist/types/tools/CodeExecutor.d.ts +6 -0
  107. package/dist/types/types/graph.d.ts +26 -38
  108. package/dist/types/types/llm.d.ts +3 -3
  109. package/dist/types/types/run.d.ts +2 -0
  110. package/dist/types/types/stream.d.ts +1 -1
  111. package/dist/types/types/tools.d.ts +9 -0
  112. package/package.json +17 -16
  113. package/src/agents/AgentContext.ts +189 -71
  114. package/src/agents/__tests__/AgentContext.anthropic.live.test.ts +116 -0
  115. package/src/agents/__tests__/AgentContext.bedrock.live.test.ts +149 -0
  116. package/src/agents/__tests__/AgentContext.test.ts +333 -2
  117. package/src/agents/__tests__/promptCacheLiveHelpers.ts +165 -0
  118. package/src/graphs/Graph.ts +24 -4
  119. package/src/graphs/__tests__/composition.smoke.test.ts +188 -0
  120. package/src/llm/anthropic/index.ts +252 -84
  121. package/src/llm/anthropic/llm.spec.ts +751 -102
  122. package/src/llm/anthropic/types.ts +9 -1
  123. package/src/llm/anthropic/utils/message_inputs.ts +43 -20
  124. package/src/llm/anthropic/utils/message_outputs.ts +119 -101
  125. package/src/llm/anthropic/utils/server-tool-inputs.test.ts +77 -0
  126. package/src/llm/bedrock/index.ts +2 -2
  127. package/src/llm/bedrock/llm.spec.ts +341 -0
  128. package/src/llm/bedrock/utils/message_inputs.ts +303 -4
  129. package/src/llm/bedrock/utils/message_outputs.ts +2 -1
  130. package/src/llm/custom-chat-models.smoke.test.ts +662 -0
  131. package/src/llm/google/llm.spec.ts +339 -57
  132. package/src/llm/google/utils/common.ts +53 -48
  133. package/src/llm/openai/contentBlocks.test.ts +346 -0
  134. package/src/llm/openai/index.ts +736 -837
  135. package/src/llm/openai/utils/index.ts +84 -64
  136. package/src/llm/openrouter/index.ts +124 -247
  137. package/src/llm/openrouter/reasoning.test.ts +8 -1
  138. package/src/llm/vertexai/index.ts +11 -5
  139. package/src/llm/vertexai/llm.spec.ts +28 -1
  140. package/src/messages/cache.test.ts +106 -4
  141. package/src/messages/cache.ts +57 -5
  142. package/src/messages/core.ts +16 -9
  143. package/src/messages/format.ts +9 -6
  144. package/src/messages/langchain.ts +39 -0
  145. package/src/messages/prune.ts +12 -8
  146. package/src/scripts/caching.ts +2 -3
  147. package/src/specs/anthropic.simple.test.ts +61 -0
  148. package/src/specs/summarization.test.ts +58 -61
  149. package/src/tools/BashExecutor.ts +37 -13
  150. package/src/tools/CodeExecutor.ts +55 -11
  151. package/src/tools/ProgrammaticToolCalling.ts +29 -14
  152. package/src/tools/ToolNode.ts +5 -1
  153. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +60 -0
  154. package/src/types/graph.ts +35 -88
  155. package/src/types/llm.ts +3 -3
  156. package/src/types/run.ts +2 -0
  157. package/src/types/stream.ts +1 -1
  158. package/src/types/tools.ts +9 -0
  159. package/src/utils/llmConfig.ts +1 -6
@@ -40,6 +40,7 @@ import {
40
40
  schemaToGenerativeAIParameters,
41
41
  } from './zod_to_genai_parameters';
42
42
  import { GoogleGenerativeAIToolType } from '../types';
43
+ import { toLangChainContent } from '@/messages/langchain';
43
44
 
44
45
  export const _FUNCTION_CALL_THOUGHT_SIGNATURES_MAP_KEY =
45
46
  '__gemini_function_call_thought_signatures__';
@@ -575,30 +576,32 @@ export function convertResponseContentToChatGenerationChunk(
575
576
  }
576
577
  content = textParts.join('');
577
578
  } else if (candidateContent && Array.isArray(candidateContent.parts)) {
578
- content = candidateContent.parts
579
- .map((p) => {
580
- if ('text' in p && 'thought' in p && p.thought === true) {
581
- reasoningParts.push(p.text ?? '');
582
- return undefined;
583
- } else if ('text' in p) {
584
- return {
585
- type: 'text',
586
- text: p.text,
587
- };
588
- } else if ('executableCode' in p) {
589
- return {
590
- type: 'executableCode',
591
- executableCode: p.executableCode,
592
- };
593
- } else if ('codeExecutionResult' in p) {
594
- return {
595
- type: 'codeExecutionResult',
596
- codeExecutionResult: p.codeExecutionResult,
597
- };
598
- }
599
- return p;
600
- })
601
- .filter((p) => p !== undefined);
579
+ content = toLangChainContent(
580
+ candidateContent.parts
581
+ .map((p) => {
582
+ if ('text' in p && 'thought' in p && p.thought === true) {
583
+ reasoningParts.push(p.text ?? '');
584
+ return undefined;
585
+ } else if ('text' in p) {
586
+ return {
587
+ type: 'text',
588
+ text: p.text,
589
+ };
590
+ } else if ('executableCode' in p) {
591
+ return {
592
+ type: 'executableCode',
593
+ executableCode: p.executableCode,
594
+ };
595
+ } else if ('codeExecutionResult' in p) {
596
+ return {
597
+ type: 'codeExecutionResult',
598
+ codeExecutionResult: p.codeExecutionResult,
599
+ };
600
+ }
601
+ return p;
602
+ })
603
+ .filter((p) => p !== undefined)
604
+ );
602
605
  } else {
603
606
  // no content returned - likely due to abnormal stop reason, e.g. malformed function call
604
607
  content = [];
@@ -733,30 +736,32 @@ export function mapGenerateContentResultToChatResult(
733
736
  Array.isArray(candidateContent?.parts) &&
734
737
  candidateContent.parts.length > 0
735
738
  ) {
736
- content = candidateContent.parts
737
- .map((p) => {
738
- if ('text' in p && 'thought' in p && p.thought === true) {
739
- reasoningParts.push(p.text ?? '');
740
- return undefined;
741
- } else if ('text' in p) {
742
- return {
743
- type: 'text',
744
- text: p.text,
745
- };
746
- } else if ('executableCode' in p) {
747
- return {
748
- type: 'executableCode',
749
- executableCode: p.executableCode,
750
- };
751
- } else if ('codeExecutionResult' in p) {
752
- return {
753
- type: 'codeExecutionResult',
754
- codeExecutionResult: p.codeExecutionResult,
755
- };
756
- }
757
- return p;
758
- })
759
- .filter((p) => p !== undefined);
739
+ content = toLangChainContent(
740
+ candidateContent.parts
741
+ .map((p) => {
742
+ if ('text' in p && 'thought' in p && p.thought === true) {
743
+ reasoningParts.push(p.text ?? '');
744
+ return undefined;
745
+ } else if ('text' in p) {
746
+ return {
747
+ type: 'text',
748
+ text: p.text,
749
+ };
750
+ } else if ('executableCode' in p) {
751
+ return {
752
+ type: 'executableCode',
753
+ executableCode: p.executableCode,
754
+ };
755
+ } else if ('codeExecutionResult' in p) {
756
+ return {
757
+ type: 'codeExecutionResult',
758
+ codeExecutionResult: p.codeExecutionResult,
759
+ };
760
+ }
761
+ return p;
762
+ })
763
+ .filter((p) => p !== undefined)
764
+ );
760
765
  } else {
761
766
  content = [];
762
767
  }
@@ -0,0 +1,346 @@
1
+ import { describe, expect, test } from '@jest/globals';
2
+ import {
3
+ AIMessage,
4
+ AIMessageChunk,
5
+ type ContentBlock,
6
+ } from '@langchain/core/messages';
7
+
8
+ describe('OpenAI content block translator compatibility', () => {
9
+ describe('Chat Completions', () => {
10
+ test('translates string content and tool calls to v1 content blocks', () => {
11
+ const message = new AIMessage({
12
+ content: 'Hello from OpenAI',
13
+ tool_calls: [
14
+ {
15
+ id: 'call_123',
16
+ name: 'get_weather',
17
+ args: { location: 'San Francisco' },
18
+ },
19
+ ],
20
+ response_metadata: { model_provider: 'openai' },
21
+ });
22
+
23
+ const expected: Array<ContentBlock.Standard> = [
24
+ { type: 'text', text: 'Hello from OpenAI' },
25
+ {
26
+ type: 'tool_call',
27
+ id: 'call_123',
28
+ name: 'get_weather',
29
+ args: { location: 'San Francisco' },
30
+ },
31
+ ];
32
+
33
+ expect(message.contentBlocks).toEqual(expected);
34
+ expect(message.content).not.toEqual(expected);
35
+
36
+ const v1Message = new AIMessage({
37
+ content: message.contentBlocks,
38
+ response_metadata: { output_version: 'v1' },
39
+ });
40
+ expect(v1Message.contentBlocks).toEqual(expected);
41
+ expect(v1Message.content).toEqual(expected);
42
+ });
43
+
44
+ test('does not include empty text block when content is empty string with tool calls', () => {
45
+ const message = new AIMessage({
46
+ content: '',
47
+ tool_calls: [
48
+ {
49
+ id: 'call_123',
50
+ name: 'get_value',
51
+ args: { key: 'a' },
52
+ },
53
+ {
54
+ id: 'call_456',
55
+ name: 'get_value',
56
+ args: { key: 'b' },
57
+ },
58
+ ],
59
+ response_metadata: { model_provider: 'openai' },
60
+ });
61
+
62
+ expect(message.contentBlocks).toEqual([
63
+ {
64
+ type: 'tool_call',
65
+ id: 'call_123',
66
+ name: 'get_value',
67
+ args: { key: 'a' },
68
+ },
69
+ {
70
+ type: 'tool_call',
71
+ id: 'call_456',
72
+ name: 'get_value',
73
+ args: { key: 'b' },
74
+ },
75
+ ]);
76
+ });
77
+
78
+ test('translates chat completion chunks with parsed tool call chunks', () => {
79
+ const chunk1 = new AIMessageChunk({
80
+ content: [{ type: 'text', text: 'Looking ', index: 0 }],
81
+ response_metadata: { model_provider: 'openai' },
82
+ });
83
+ const chunk2 = new AIMessageChunk({
84
+ content: [{ type: 'text', text: 'up.', index: 0 }],
85
+ tool_call_chunks: [
86
+ {
87
+ type: 'tool_call_chunk',
88
+ id: 'call_abc',
89
+ name: 'search',
90
+ args: '{"query":"weather"}',
91
+ index: 0,
92
+ },
93
+ ],
94
+ response_metadata: { model_provider: 'openai' },
95
+ });
96
+
97
+ expect(chunk1.concat(chunk2).contentBlocks).toEqual([
98
+ { type: 'text', text: 'Looking up.', index: 0 },
99
+ {
100
+ type: 'tool_call',
101
+ id: 'call_abc',
102
+ name: 'search',
103
+ args: { query: 'weather' },
104
+ },
105
+ ]);
106
+ });
107
+ });
108
+
109
+ describe('Responses', () => {
110
+ test('translates Responses messages to v1 content blocks', () => {
111
+ const code = ['print(', 'hello', ')'].join(String.fromCharCode(39));
112
+ const responseTextBlock = {
113
+ type: 'text',
114
+ text: 'Here is a result.',
115
+ annotations: [
116
+ {
117
+ type: 'url_citation',
118
+ url: 'https://example.com',
119
+ title: 'Example',
120
+ start_index: 0,
121
+ end_index: 4,
122
+ },
123
+ {
124
+ type: 'file_citation',
125
+ file_id: 'file_123',
126
+ filename: 'doc.pdf',
127
+ index: 10,
128
+ },
129
+ ],
130
+ } as ContentBlock.Text;
131
+ const message = new AIMessage({
132
+ content: [responseTextBlock],
133
+ tool_calls: [
134
+ { id: 'call_456', name: 'summarize', args: { length: 'short' } },
135
+ ],
136
+ additional_kwargs: {
137
+ reasoning: { summary: [{ text: 'Thinking...' }, { text: ' Done.' }] },
138
+ tool_outputs: [
139
+ {
140
+ id: 'call_456',
141
+ type: 'code_interpreter_call',
142
+ code,
143
+ status: 'completed',
144
+ outputs: [{ type: 'logs', logs: 'hello' }],
145
+ },
146
+ ],
147
+ },
148
+ response_metadata: { model_provider: 'openai' },
149
+ });
150
+
151
+ const expected: Array<ContentBlock.Standard> = [
152
+ { type: 'reasoning', reasoning: 'Thinking... Done.' },
153
+ {
154
+ type: 'text',
155
+ text: 'Here is a result.',
156
+ annotations: [
157
+ {
158
+ type: 'citation',
159
+ url: 'https://example.com',
160
+ title: 'Example',
161
+ startIndex: 0,
162
+ endIndex: 4,
163
+ },
164
+ {
165
+ type: 'citation',
166
+ title: 'doc.pdf',
167
+ startIndex: 10,
168
+ endIndex: 10,
169
+ fileId: 'file_123',
170
+ },
171
+ ],
172
+ },
173
+ {
174
+ type: 'tool_call',
175
+ id: 'call_456',
176
+ name: 'summarize',
177
+ args: { length: 'short' },
178
+ },
179
+ {
180
+ type: 'server_tool_call',
181
+ name: 'code_interpreter',
182
+ id: 'call_456',
183
+ args: { code },
184
+ },
185
+ {
186
+ type: 'server_tool_call_result',
187
+ toolCallId: 'call_456',
188
+ status: 'success',
189
+ output: {
190
+ type: 'code_interpreter_output',
191
+ returnCode: 0,
192
+ stderr: undefined,
193
+ stdout: 'hello',
194
+ },
195
+ },
196
+ ];
197
+
198
+ expect(message.contentBlocks).toEqual(expected);
199
+
200
+ const v1Message = new AIMessage({
201
+ content: message.contentBlocks,
202
+ response_metadata: { output_version: 'v1' },
203
+ });
204
+ expect(v1Message.contentBlocks).toEqual(expected);
205
+ expect(v1Message.content).toEqual(expected);
206
+ });
207
+
208
+ test('translates image_generation_call to image content block', () => {
209
+ const message = new AIMessage({
210
+ content: [{ type: 'text', text: 'Here is your image:' }],
211
+ additional_kwargs: {
212
+ tool_outputs: [
213
+ {
214
+ type: 'image_generation_call',
215
+ id: 'ig_abc123',
216
+ status: 'completed',
217
+ result: 'base64ImageData',
218
+ revised_prompt: 'A beautiful sunset over the ocean',
219
+ },
220
+ ],
221
+ },
222
+ response_metadata: { model_provider: 'openai' },
223
+ });
224
+
225
+ expect(message.contentBlocks).toEqual([
226
+ { type: 'text', text: 'Here is your image:' },
227
+ {
228
+ type: 'image',
229
+ mimeType: 'image/png',
230
+ data: 'base64ImageData',
231
+ id: 'ig_abc123',
232
+ metadata: {
233
+ status: 'completed',
234
+ },
235
+ },
236
+ {
237
+ type: 'non_standard',
238
+ value: {
239
+ type: 'image_generation_call',
240
+ id: 'ig_abc123',
241
+ status: 'completed',
242
+ result: 'base64ImageData',
243
+ revised_prompt: 'A beautiful sunset over the ocean',
244
+ },
245
+ },
246
+ ]);
247
+ });
248
+
249
+ test('translates web_search_call and file_search_call to server tool blocks', () => {
250
+ const message = new AIMessage({
251
+ content: [{ type: 'text', text: 'Search results:' }],
252
+ additional_kwargs: {
253
+ tool_outputs: [
254
+ {
255
+ type: 'web_search_call',
256
+ id: 'ws_abc456',
257
+ status: 'completed',
258
+ action: {
259
+ type: 'search',
260
+ query: 'melbourne australia news today',
261
+ sources: [{ type: 'url', url: 'https://example.com/news' }],
262
+ },
263
+ },
264
+ {
265
+ type: 'file_search_call',
266
+ id: 'fs_abc123',
267
+ status: 'completed',
268
+ queries: ['quarterly report', 'revenue 2025'],
269
+ results: [
270
+ {
271
+ file_id: 'file_001',
272
+ filename: 'report.pdf',
273
+ score: 0.95,
274
+ text: 'Revenue grew 15% in Q3...',
275
+ },
276
+ ],
277
+ },
278
+ ],
279
+ },
280
+ response_metadata: { model_provider: 'openai' },
281
+ });
282
+
283
+ expect(message.contentBlocks).toEqual([
284
+ { type: 'text', text: 'Search results:' },
285
+ {
286
+ type: 'server_tool_call',
287
+ name: 'web_search',
288
+ id: 'ws_abc456',
289
+ args: { query: 'melbourne australia news today' },
290
+ },
291
+ {
292
+ type: 'server_tool_call_result',
293
+ toolCallId: 'ws_abc456',
294
+ status: 'success',
295
+ output: {
296
+ action: {
297
+ type: 'search',
298
+ query: 'melbourne australia news today',
299
+ sources: [{ type: 'url', url: 'https://example.com/news' }],
300
+ },
301
+ },
302
+ },
303
+ {
304
+ type: 'server_tool_call',
305
+ name: 'file_search',
306
+ id: 'fs_abc123',
307
+ args: { queries: ['quarterly report', 'revenue 2025'] },
308
+ },
309
+ {
310
+ type: 'server_tool_call_result',
311
+ toolCallId: 'fs_abc123',
312
+ status: 'success',
313
+ output: {
314
+ results: [
315
+ {
316
+ file_id: 'file_001',
317
+ filename: 'report.pdf',
318
+ score: 0.95,
319
+ text: 'Revenue grew 15% in Q3...',
320
+ },
321
+ ],
322
+ },
323
+ },
324
+ ]);
325
+ });
326
+
327
+ test('moves phase into extras on text content blocks', () => {
328
+ const textBlock = {
329
+ type: 'text',
330
+ text: 'The weather is sunny.',
331
+ annotations: [],
332
+ phase: 'final_answer',
333
+ } as ContentBlock.Text & { phase: string };
334
+ const message = new AIMessage({
335
+ content: [textBlock],
336
+ response_metadata: { model_provider: 'openai' },
337
+ });
338
+
339
+ const contentTextBlock = message.contentBlocks.find(
340
+ (block): block is ContentBlock.Text => block.type === 'text'
341
+ );
342
+ expect(contentTextBlock).toBeDefined();
343
+ expect(contentTextBlock?.extras).toEqual({ phase: 'final_answer' });
344
+ });
345
+ });
346
+ });