Skip to content

Commit 8e8a540

Browse files
authored
anthropic: add improved streaming thinking/reasoning token support (#1418)
llms/anthropic: add streaming thinking/reasoning token support Implement StreamingReasoningFunc support in the Anthropic client to enable real-time streaming of thinking tokens during extended thinking responses. Changes: - Add StreamingReasoningFunc field to messagePayload, MessageRequest structs - Modify handleThinkingDelta() to call StreamingReasoningFunc when thinking chunks arrive during streaming - Wire up StreamingReasoningFunc from llms.CallOptions through to the Anthropic client payload - Update setMessageDefaults to enable streaming when StreamingReasoningFunc is provided This follows the same pattern as the OpenAI client (chat.go:638-663) and enables thinking tokens to stream in real-time at the BEGINNING of the response, rather than appearing after the response completes. Fixes issue where thinking_delta events were not calling the streaming reasoning callback, causing thinking content to only be available after response completion.
1 parent 334f4c3 commit 8e8a540

3 files changed

Lines changed: 43 additions & 28 deletions

File tree

llms/anthropic/anthropicllm.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -146,17 +146,18 @@ func generateMessagesContent(ctx context.Context, o *LLM, messages []llms.Messag
146146
betaHeaders, thinking := extractThinkingOptions(o, opts)
147147

148148
result, err := o.client.CreateMessage(ctx, &anthropicclient.MessageRequest{
149-
Model: opts.Model,
150-
Messages: chatMessages,
151-
System: systemPrompt,
152-
MaxTokens: opts.MaxTokens,
153-
StopWords: opts.StopWords,
154-
Temperature: opts.Temperature,
155-
TopP: opts.TopP,
156-
Tools: tools,
157-
Thinking: thinking,
158-
BetaHeaders: betaHeaders,
159-
StreamingFunc: opts.StreamingFunc,
149+
Model: opts.Model,
150+
Messages: chatMessages,
151+
System: systemPrompt,
152+
MaxTokens: opts.MaxTokens,
153+
StopWords: opts.StopWords,
154+
Temperature: opts.Temperature,
155+
TopP: opts.TopP,
156+
Tools: tools,
157+
Thinking: thinking,
158+
BetaHeaders: betaHeaders,
159+
StreamingFunc: opts.StreamingFunc,
160+
StreamingReasoningFunc: opts.StreamingReasoningFunc,
160161
})
161162
if err != nil {
162163
if o.CallbacksHandler != nil {

llms/anthropic/internal/anthropicclient/anthropicclient.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -141,24 +141,26 @@ type MessageRequest struct {
141141
Thinking *ThinkingConfig `json:"thinking,omitempty"`
142142

143143
// BetaHeaders are additional beta feature headers to include
144-
BetaHeaders []string `json:"-"`
145-
StreamingFunc func(ctx context.Context, chunk []byte) error `json:"-"`
144+
BetaHeaders []string `json:"-"`
145+
StreamingFunc func(ctx context.Context, chunk []byte) error `json:"-"`
146+
StreamingReasoningFunc func(ctx context.Context, reasoningChunk, chunk []byte) error `json:"-"`
146147
}
147148

148149
// CreateMessage creates message for the messages api.
149150
func (c *Client) CreateMessage(ctx context.Context, r *MessageRequest) (*MessageResponsePayload, error) {
150151
resp, err := c.createMessage(ctx, &messagePayload{
151-
Model: r.Model,
152-
Messages: r.Messages,
153-
System: r.System,
154-
Temperature: r.Temperature,
155-
MaxTokens: r.MaxTokens,
156-
StopWords: r.StopWords,
157-
TopP: r.TopP,
158-
Tools: r.Tools,
159-
Stream: r.Stream,
160-
Thinking: r.Thinking,
161-
StreamingFunc: r.StreamingFunc,
152+
Model: r.Model,
153+
Messages: r.Messages,
154+
System: r.System,
155+
Temperature: r.Temperature,
156+
MaxTokens: r.MaxTokens,
157+
StopWords: r.StopWords,
158+
TopP: r.TopP,
159+
Tools: r.Tools,
160+
Stream: r.Stream,
161+
Thinking: r.Thinking,
162+
StreamingFunc: r.StreamingFunc,
163+
StreamingReasoningFunc: r.StreamingReasoningFunc,
162164
}, r.BetaHeaders)
163165
if err != nil {
164166
return nil, err

llms/anthropic/internal/anthropicclient/messages.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ type messagePayload struct {
4747
// Extended thinking parameters (Claude 3.7+)
4848
Thinking *ThinkingConfig `json:"thinking,omitempty"`
4949

50-
StreamingFunc func(ctx context.Context, chunk []byte) error `json:"-"`
50+
StreamingFunc func(ctx context.Context, chunk []byte) error `json:"-"`
51+
StreamingReasoningFunc func(ctx context.Context, reasoningChunk, chunk []byte) error `json:"-"`
5152
}
5253

5354
// ThinkingConfig represents the thinking configuration for Claude 3.7+
@@ -219,7 +220,7 @@ func (c *Client) setMessageDefaults(payload *messagePayload) {
219220
default:
220221
payload.Model = defaultModel
221222
}
222-
if payload.StreamingFunc != nil {
223+
if payload.StreamingFunc != nil || payload.StreamingReasoningFunc != nil {
223224
payload.Stream = true
224225
}
225226
}
@@ -440,7 +441,7 @@ func handleContentBlockDeltaEvent(ctx context.Context, event map[string]interfac
440441
case "input_json_delta":
441442
return handleJSONDelta(delta, response, index)
442443
case "thinking_delta":
443-
return handleThinkingDelta(delta, response, index)
444+
return handleThinkingDelta(ctx, delta, response, payload, index)
444445
}
445446

446447
return response, nil
@@ -485,7 +486,7 @@ func handleJSONDelta(delta map[string]interface{}, response MessageResponsePaylo
485486
}
486487

487488
// handleThinkingDelta processes thinking delta events for content blocks.
488-
func handleThinkingDelta(delta map[string]interface{}, response MessageResponsePayload, index int) (MessageResponsePayload, error) {
489+
func handleThinkingDelta(ctx context.Context, delta map[string]interface{}, response MessageResponsePayload, payload *messagePayload, index int) (MessageResponsePayload, error) {
489490
thinking, ok := delta["thinking"].(string)
490491
if !ok {
491492
return response, ErrInvalidDeltaTextField
@@ -495,6 +496,17 @@ func handleThinkingDelta(delta map[string]interface{}, response MessageResponseP
495496
return response, fmt.Errorf("failed to cast to ThinkingContent at index %d", index)
496497
}
497498
thinkingContent.Thinking += thinking
499+
500+
// Call StreamingReasoningFunc if provided (similar to OpenAI pattern)
501+
if payload.StreamingReasoningFunc != nil {
502+
reasoningChunk := []byte(thinking)
503+
// For thinking deltas, the content chunk is empty since this is pure reasoning
504+
err := payload.StreamingReasoningFunc(ctx, reasoningChunk, []byte{})
505+
if err != nil {
506+
return response, fmt.Errorf("streaming reasoning func returned an error: %w", err)
507+
}
508+
}
509+
498510
return response, nil
499511
}
500512

0 commit comments

Comments
 (0)