@@ -195,4 +195,86 @@ var _ = Describe("ReasoningExtractor", func() {
195195 Expect (ext .CleanedContent ()).To (Equal ("visible content" ))
196196 })
197197 })
198+
199+ Context ("ProcessChatDeltaReasoning with Gemma 4 tags" , func () {
200+ It ("should strip <|channel>thought and <channel|> tags from streaming deltas" , func () {
201+ ext := NewReasoningExtractor ("<|channel>thought" , Config {})
202+
203+ // Simulate C++ autoparser sending tag tokens as reasoning
204+ d1 := ext .ProcessChatDeltaReasoning ("<|channel>" )
205+ Expect (d1 ).To (BeEmpty (), "start tag prefix should be buffered, not emitted" )
206+
207+ d2 := ext .ProcessChatDeltaReasoning ("thought" )
208+ Expect (d2 ).To (BeEmpty (), "start tag suffix should be buffered, not emitted" )
209+
210+ d3 := ext .ProcessChatDeltaReasoning ("\n " )
211+ Expect (d3 ).To (BeEmpty (), "newline after start tag should not emit yet" )
212+
213+ d4 := ext .ProcessChatDeltaReasoning ("The" )
214+ Expect (d4 ).To (Equal ("The" ))
215+
216+ d5 := ext .ProcessChatDeltaReasoning (" user" )
217+ Expect (d5 ).To (Equal (" user" ))
218+
219+ d6 := ext .ProcessChatDeltaReasoning (" asks" )
220+ Expect (d6 ).To (Equal (" asks" ))
221+
222+ d7 := ext .ProcessChatDeltaReasoning ("\n " )
223+ Expect (d7 ).To (Equal ("\n " ))
224+
225+ d8 := ext .ProcessChatDeltaReasoning ("<channel|>" )
226+ Expect (d8 ).To (BeEmpty (), "close tag should be consumed, not emitted" )
227+ })
228+
229+ It ("should handle empty deltas" , func () {
230+ ext := NewReasoningExtractor ("<|channel>thought" , Config {})
231+ d := ext .ProcessChatDeltaReasoning ("" )
232+ Expect (d ).To (BeEmpty ())
233+ })
234+
235+ It ("should pass through reasoning without tags unchanged" , func () {
236+ ext := NewReasoningExtractor ("<think>" , Config {})
237+
238+ // When C++ autoparser already strips tags (e.g. <think> models),
239+ // reasoning arrives clean — just pass it through.
240+ d1 := ext .ProcessChatDeltaReasoning ("I need to" )
241+ Expect (d1 ).To (Equal ("I need to" ))
242+
243+ d2 := ext .ProcessChatDeltaReasoning (" think carefully" )
244+ Expect (d2 ).To (Equal (" think carefully" ))
245+ })
246+
247+ It ("should strip <think> tags if C++ autoparser includes them" , func () {
248+ ext := NewReasoningExtractor ("<think>" , Config {})
249+
250+ d1 := ext .ProcessChatDeltaReasoning ("<think>" )
251+ Expect (d1 ).To (BeEmpty ())
252+
253+ d2 := ext .ProcessChatDeltaReasoning ("reasoning" )
254+ Expect (d2 ).To (Equal ("reasoning" ))
255+
256+ d3 := ext .ProcessChatDeltaReasoning ("</think>" )
257+ Expect (d3 ).To (BeEmpty ())
258+ })
259+
260+ It ("should respect suppressReasoning" , func () {
261+ ext := NewReasoningExtractor ("<|channel>thought" , Config {})
262+ ext .ResetAndSuppressReasoning ()
263+
264+ d := ext .ProcessChatDeltaReasoning ("some reasoning" )
265+ Expect (d ).To (BeEmpty ())
266+ })
267+
268+ It ("should reset ChatDelta state on Reset" , func () {
269+ ext := NewReasoningExtractor ("<|channel>thought" , Config {})
270+
271+ ext .ProcessChatDeltaReasoning ("<|channel>thought" )
272+ ext .ProcessChatDeltaReasoning ("\n first reasoning" )
273+ ext .Reset ()
274+
275+ // After reset, should start fresh
276+ d := ext .ProcessChatDeltaReasoning ("clean reasoning" )
277+ Expect (d ).To (Equal ("clean reasoning" ))
278+ })
279+ })
198280})
0 commit comments