26
26
import org .springframework .ai .anthropic .api .AnthropicApi .ContentBlockDeltaEvent ;
27
27
import org .springframework .ai .anthropic .api .AnthropicApi .ContentBlockDeltaEvent .ContentBlockDeltaJson ;
28
28
import org .springframework .ai .anthropic .api .AnthropicApi .ContentBlockDeltaEvent .ContentBlockDeltaText ;
29
+ import org .springframework .ai .anthropic .api .AnthropicApi .ContentBlockDeltaEvent .ContentBlockDeltaThinking ;
30
+ import org .springframework .ai .anthropic .api .AnthropicApi .ContentBlockDeltaEvent .ContentBlockDeltaSignature ;
29
31
import org .springframework .ai .anthropic .api .AnthropicApi .ContentBlockStartEvent ;
30
32
import org .springframework .ai .anthropic .api .AnthropicApi .ContentBlockStartEvent .ContentBlockText ;
31
33
import org .springframework .ai .anthropic .api .AnthropicApi .ContentBlockStartEvent .ContentBlockToolUse ;
34
+ import org .springframework .ai .anthropic .api .AnthropicApi .ContentBlockStartEvent .ContentBlockThinking ;
32
35
import org .springframework .ai .anthropic .api .AnthropicApi .EventType ;
33
36
import org .springframework .ai .anthropic .api .AnthropicApi .MessageDeltaEvent ;
34
37
import org .springframework .ai .anthropic .api .AnthropicApi .MessageStartEvent ;
35
38
import org .springframework .ai .anthropic .api .AnthropicApi .Role ;
36
39
import org .springframework .ai .anthropic .api .AnthropicApi .StreamEvent ;
37
40
import org .springframework .ai .anthropic .api .AnthropicApi .ToolUseAggregationEvent ;
38
41
import org .springframework .ai .anthropic .api .AnthropicApi .Usage ;
39
- import org .springframework .util .Assert ;
40
42
import org .springframework .util .CollectionUtils ;
41
43
import org .springframework .util .StringUtils ;
42
44
43
45
/**
44
- * Helper class to support streaming function calling.
46
+ * Helper class to support streaming function calling and thinking events .
45
47
* <p>
46
48
* It can merge the streamed {@link StreamEvent} chunks in case of function calling
47
- * message.
49
+ * message. It passes through other events like text, thinking, and signature deltas.
48
50
*
49
51
* @author Mariusz Bernacki
50
52
* @author Christian Tzolov
51
53
* @author Jihoon Kim
54
+ * @author Alexandros Pappas
52
55
* @since 1.0.0
53
56
*/
54
57
public class StreamHelper {
@@ -61,13 +64,16 @@ public boolean isToolUseStart(StreamEvent event) {
61
64
}
62
65
63
66
public boolean isToolUseFinish (StreamEvent event ) {
64
-
65
- if (event == null || event .type () == null || event .type () != EventType .CONTENT_BLOCK_STOP ) {
66
- return false ;
67
- }
68
- return true ;
67
+ // Tool use streaming sequence ends with a CONTENT_BLOCK_STOP event.
68
+ // The logic relies on the state machine (isInsideTool flag) managed in
69
+ // chatCompletionStream to know if this stop event corresponds to a tool use.
70
+ return event != null && event .type () != null && event .type () == EventType .CONTENT_BLOCK_STOP ;
69
71
}
70
72
73
+ /**
74
+ * Merge the tool‑use related streaming events into one aggregate event so that the
75
+ * upper layers see a single ContentBlock with the full JSON input.
76
+ */
71
77
public StreamEvent mergeToolUseEvents (StreamEvent previousEvent , StreamEvent event ) {
72
78
73
79
ToolUseAggregationEvent eventAggregator = (ToolUseAggregationEvent ) previousEvent ;
@@ -76,8 +82,7 @@ public StreamEvent mergeToolUseEvents(StreamEvent previousEvent, StreamEvent eve
76
82
ContentBlockStartEvent contentBlockStart = (ContentBlockStartEvent ) event ;
77
83
78
84
if (ContentBlock .Type .TOOL_USE .getValue ().equals (contentBlockStart .contentBlock ().type ())) {
79
- ContentBlockStartEvent .ContentBlockToolUse cbToolUse = (ContentBlockToolUse ) contentBlockStart
80
- .contentBlock ();
85
+ ContentBlockToolUse cbToolUse = (ContentBlockToolUse ) contentBlockStart .contentBlock ();
81
86
82
87
return eventAggregator .withIndex (contentBlockStart .index ())
83
88
.withId (cbToolUse .id ())
@@ -102,6 +107,14 @@ else if (event.type() == EventType.CONTENT_BLOCK_STOP) {
102
107
return event ;
103
108
}
104
109
110
+ /**
111
+ * Converts a raw {@link StreamEvent} potentially containing tool use aggregates or
112
+ * other block types (text, thinking) into a {@link ChatCompletionResponse} chunk.
113
+ * @param event The incoming StreamEvent.
114
+ * @param contentBlockReference Holds the state of the response being built across
115
+ * multiple events.
116
+ * @return A ChatCompletionResponse representing the processed chunk.
117
+ */
105
118
public ChatCompletionResponse eventToChatCompletionResponse (StreamEvent event ,
106
119
AtomicReference <ChatCompletionResponseBuilder > contentBlockReference ) {
107
120
@@ -135,28 +148,41 @@ else if (event.type().equals(EventType.TOOL_USE_AGGREGATE)) {
135
148
else if (event .type ().equals (EventType .CONTENT_BLOCK_START )) {
136
149
ContentBlockStartEvent contentBlockStartEvent = (ContentBlockStartEvent ) event ;
137
150
138
- Assert .isTrue (contentBlockStartEvent .contentBlock ().type ().equals ("text" ),
139
- "The json content block should have been aggregated. Unsupported content block type: "
140
- + contentBlockStartEvent .contentBlock ().type ());
141
-
142
- ContentBlockText contentBlockText = (ContentBlockText ) contentBlockStartEvent .contentBlock ();
143
- ContentBlock contentBlock = new ContentBlock (Type .TEXT , null , contentBlockText .text (),
144
- contentBlockStartEvent .index ());
145
- contentBlockReference .get ().withType (event .type ().name ()).withContent (List .of (contentBlock ));
151
+ if (contentBlockStartEvent .contentBlock () instanceof ContentBlockText textBlock ) {
152
+ ContentBlock cb = new ContentBlock (Type .TEXT , null , textBlock .text (), contentBlockStartEvent .index ());
153
+ contentBlockReference .get ().withType (event .type ().name ()).withContent (List .of (cb ));
154
+ }
155
+ else if (contentBlockStartEvent .contentBlock () instanceof ContentBlockThinking thinkingBlock ) {
156
+ ContentBlock cb = new ContentBlock (Type .THINKING , null , null , contentBlockStartEvent .index (), null ,
157
+ null , null , null , null , null , thinkingBlock .thinking (), null );
158
+ contentBlockReference .get ().withType (event .type ().name ()).withContent (List .of (cb ));
159
+ }
160
+ else {
161
+ throw new IllegalArgumentException (
162
+ "Unsupported content block type: " + contentBlockStartEvent .contentBlock ().type ());
163
+ }
146
164
}
147
165
else if (event .type ().equals (EventType .CONTENT_BLOCK_DELTA )) {
148
-
149
166
ContentBlockDeltaEvent contentBlockDeltaEvent = (ContentBlockDeltaEvent ) event ;
150
167
151
- Assert .isTrue (contentBlockDeltaEvent .delta ().type ().equals ("text_delta" ),
152
- "The json content block delta should have been aggregated. Unsupported content block type: "
153
- + contentBlockDeltaEvent .delta ().type ());
154
-
155
- ContentBlockDeltaText deltaTxt = (ContentBlockDeltaText ) contentBlockDeltaEvent .delta ();
156
-
157
- var contentBlock = new ContentBlock (Type .TEXT_DELTA , null , deltaTxt .text (), contentBlockDeltaEvent .index ());
158
-
159
- contentBlockReference .get ().withType (event .type ().name ()).withContent (List .of (contentBlock ));
168
+ if (contentBlockDeltaEvent .delta () instanceof ContentBlockDeltaText txt ) {
169
+ ContentBlock cb = new ContentBlock (Type .TEXT_DELTA , null , txt .text (), contentBlockDeltaEvent .index ());
170
+ contentBlockReference .get ().withType (event .type ().name ()).withContent (List .of (cb ));
171
+ }
172
+ else if (contentBlockDeltaEvent .delta () instanceof ContentBlockDeltaThinking thinking ) {
173
+ ContentBlock cb = new ContentBlock (Type .THINKING_DELTA , null , null , contentBlockDeltaEvent .index (),
174
+ null , null , null , null , null , null , thinking .thinking (), null );
175
+ contentBlockReference .get ().withType (event .type ().name ()).withContent (List .of (cb ));
176
+ }
177
+ else if (contentBlockDeltaEvent .delta () instanceof ContentBlockDeltaSignature sig ) {
178
+ ContentBlock cb = new ContentBlock (Type .SIGNATURE_DELTA , null , null , contentBlockDeltaEvent .index (),
179
+ null , null , null , null , null , sig .signature (), null , null );
180
+ contentBlockReference .get ().withType (event .type ().name ()).withContent (List .of (cb ));
181
+ }
182
+ else {
183
+ throw new IllegalArgumentException (
184
+ "Unsupported content block delta type: " + contentBlockDeltaEvent .delta ().type ());
185
+ }
160
186
}
161
187
else if (event .type ().equals (EventType .MESSAGE_DELTA )) {
162
188
@@ -173,21 +199,26 @@ else if (event.type().equals(EventType.MESSAGE_DELTA)) {
173
199
}
174
200
175
201
if (messageDeltaEvent .usage () != null ) {
176
- var totalUsage = new Usage (contentBlockReference .get ().usage .inputTokens (),
202
+ Usage totalUsage = new Usage (contentBlockReference .get ().usage .inputTokens (),
177
203
messageDeltaEvent .usage ().outputTokens ());
178
204
contentBlockReference .get ().withUsage (totalUsage );
179
205
}
180
206
}
181
207
else if (event .type ().equals (EventType .MESSAGE_STOP )) {
182
- // pass through
208
+ // pass through as‑is
183
209
}
184
210
else {
211
+ // Any other event types that should propagate upwards without content
185
212
contentBlockReference .get ().withType (event .type ().name ()).withContent (List .of ());
186
213
}
187
214
188
215
return contentBlockReference .get ().build ();
189
216
}
190
217
218
+ /**
219
+ * Builder for {@link ChatCompletionResponse}. Used internally by {@link StreamHelper}
220
+ * to aggregate stream events.
221
+ */
191
222
public static class ChatCompletionResponseBuilder {
192
223
193
224
private String type ;
0 commit comments