@@ -195,4 +195,91 @@ 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+ // Trailing newline gets TrimSpaced by ExtractReasoning,
223+ // so it appears delayed with the next non-whitespace token
224+ d7 := ext .ProcessChatDeltaReasoning ("\n " )
225+ Expect (d7 ).To (BeEmpty (), "trailing newline is buffered by TrimSpace" )
226+
227+ d8 := ext .ProcessChatDeltaReasoning ("2+2=4" )
228+ Expect (d8 ).To (Equal ("\n 2+2=4" ), "delayed newline emitted with next content" )
229+
230+ d9 := ext .ProcessChatDeltaReasoning ("<channel|>" )
231+ Expect (d9 ).To (BeEmpty (), "close tag should be consumed, not emitted" )
232+ })
233+
234+ It ("should handle empty deltas" , func () {
235+ ext := NewReasoningExtractor ("<|channel>thought" , Config {})
236+ d := ext .ProcessChatDeltaReasoning ("" )
237+ Expect (d ).To (BeEmpty ())
238+ })
239+
240+ It ("should pass through reasoning without tags unchanged" , func () {
241+ ext := NewReasoningExtractor ("<think>" , Config {})
242+
243+ // When C++ autoparser already strips tags (e.g. <think> models),
244+ // reasoning arrives clean — just pass it through.
245+ d1 := ext .ProcessChatDeltaReasoning ("I need to" )
246+ Expect (d1 ).To (Equal ("I need to" ))
247+
248+ d2 := ext .ProcessChatDeltaReasoning (" think carefully" )
249+ Expect (d2 ).To (Equal (" think carefully" ))
250+ })
251+
252+ It ("should strip <think> tags if C++ autoparser includes them" , func () {
253+ ext := NewReasoningExtractor ("<think>" , Config {})
254+
255+ d1 := ext .ProcessChatDeltaReasoning ("<think>" )
256+ Expect (d1 ).To (BeEmpty ())
257+
258+ d2 := ext .ProcessChatDeltaReasoning ("reasoning" )
259+ Expect (d2 ).To (Equal ("reasoning" ))
260+
261+ d3 := ext .ProcessChatDeltaReasoning ("</think>" )
262+ Expect (d3 ).To (BeEmpty ())
263+ })
264+
265+ It ("should respect suppressReasoning" , func () {
266+ ext := NewReasoningExtractor ("<|channel>thought" , Config {})
267+ ext .ResetAndSuppressReasoning ()
268+
269+ d := ext .ProcessChatDeltaReasoning ("some reasoning" )
270+ Expect (d ).To (BeEmpty ())
271+ })
272+
273+ It ("should reset ChatDelta state on Reset" , func () {
274+ ext := NewReasoningExtractor ("<|channel>thought" , Config {})
275+
276+ ext .ProcessChatDeltaReasoning ("<|channel>thought" )
277+ ext .ProcessChatDeltaReasoning ("\n first reasoning" )
278+ ext .Reset ()
279+
280+ // After reset, should start fresh
281+ d := ext .ProcessChatDeltaReasoning ("clean reasoning" )
282+ Expect (d ).To (Equal ("clean reasoning" ))
283+ })
284+ })
198285})
0 commit comments