@@ -4,7 +4,10 @@ import (
5 "encoding/json"
5 "encoding/json"
9 "github.com/anthropics/anthropic-sdk-go"
12 "github.com/anthropics/anthropic-sdk-go"
10 "github.com/anthropics/anthropic-sdk-go/option"
13 "github.com/anthropics/anthropic-sdk-go/option"
@@ -125,87 +128,145 @@ func (a *anthropicProvider) StreamResponse(ctx context.Context, messages []messa
125 temperature = anthropic.Float(1)
128 temperature = anthropic.Float(1)
128− stream := a.client.Messages.NewStreaming(
130− anthropic.MessageNewParams{
131− Model: anthropic.Model(a.model.APIModel),
132− MaxTokens: a.maxTokens,
133− Temperature: temperature,
134− Messages: anthropicMessages,
135− Tools: anthropicTools,
136− Thinking: thinkingParam,
137− System: []anthropic.TextBlockParam{
139− Text: a.systemMessage,
140− CacheControl: anthropic.CacheControlEphemeralParam{
146− option.WithMaxRetries(8),
149 eventChan := make(chan ProviderEvent)
131 eventChan := make(chan ProviderEvent)
151 go func() {
133 go func() {
152 defer close(eventChan)
134 defer close(eventChan)
154− accumulatedMessage := anthropic.Message{}
136+ const maxRetries = 8
156− for stream.Next() {
139+ for {
157− event := stream.Current()
140+ // If this isn't the first attempt, we're retrying
158− err := accumulatedMessage.Accumulate(event)
141+ if attempts > 0 {
159− if err != nil {
142+ if attempts > maxRetries {
143+ eventChan <- ProviderEvent{
145+ Error: errors.New("maximum retry attempts reached for rate limit (429)"),
150+ // Inform user we're retrying with attempt number
151+ eventChan <- ProviderEvent{
152+ Type: EventContentDelta,
153+ Content: fmt.Sprintf("\n\n[Retrying due to rate limit... attempt %d of %d]\n\n", attempts, maxRetries),
156+ // Calculate backoff with exponential backoff and jitter
157+ backoffMs := 2000 * (1 << (attempts - 1)) // 2s, 4s, 8s, 16s, 32s
158+ jitterMs := int(float64(backoffMs) * 0.2)
159+ totalBackoffMs := backoffMs + jitterMs
161+ // Sleep with backoff, respecting context cancellation
160− eventChan <- ProviderEvent{Type: EventError, Error: err}
164+ eventChan <- ProviderEvent{Type: EventError, Error: ctx.Err()}
166+ case <-time.After(time.Duration(totalBackoffMs) * time.Millisecond):
167+ // Continue with retry
164− switch event := event.AsAny().(type) {
171+ attempts++
165− case anthropic.ContentBlockStartEvent:
172+
166− eventChan <- ProviderEvent{Type: EventContentStart}
173+ // Create new streaming request
174+ stream := a.client.Messages.NewStreaming(
176+ anthropic.MessageNewParams{
177+ Model: anthropic.Model(a.model.APIModel),
178+ MaxTokens: a.maxTokens,
179+ Temperature: temperature,
180+ Messages: anthropicMessages,
181+ Tools: anthropicTools,
182+ Thinking: thinkingParam,
183+ System: []anthropic.TextBlockParam{
185+ Text: a.systemMessage,
186+ CacheControl: anthropic.CacheControlEphemeralParam{
194+ // Process stream events
195+ accumulatedMessage := anthropic.Message{}
196+ streamSuccess := false
198+ // Process the stream until completion or error
200+ event := stream.Current()
201+ err := accumulatedMessage.Accumulate(event)
203+ eventChan <- ProviderEvent{Type: EventError, Error: err}
204+ return // Don't retry on accumulation errors
207+ switch event := event.AsAny().(type) {
208+ case anthropic.ContentBlockStartEvent:
209+ eventChan <- ProviderEvent{Type: EventContentStart}
168− case anthropic.ContentBlockDeltaEvent:
211+ case anthropic.ContentBlockDeltaEvent:
169− if event.Delta.Type == "thinking_delta" && event.Delta.Thinking != "" {
212+ if event.Delta.Type == "thinking_delta" && event.Delta.Thinking != "" {
170− eventChan <- ProviderEvent{
213+ eventChan <- ProviderEvent{
171− Type: EventThinkingDelta,
214+ Type: EventThinkingDelta,
172− Thinking: event.Delta.Thinking,
215+ Thinking: event.Delta.Thinking,
217+ } else if event.Delta.Type == "text_delta" && event.Delta.Text != "" {
218+ eventChan <- ProviderEvent{
219+ Type: EventContentDelta,
220+ Content: event.Delta.Text,
174− } else if event.Delta.Type == "text_delta" && event.Delta.Text != "" {
223+
224+ case anthropic.ContentBlockStopEvent:
175− eventChan <- ProviderEvent{
225+ eventChan <- ProviderEvent{Type: EventContentStop}
176− Type: EventContentDelta,
226+
177− Content: event.Delta.Text,
227+ case anthropic.MessageStopEvent:
228+ streamSuccess = true
230+ for _, block := range accumulatedMessage.Content {
231+ if text, ok := block.AsAny().(anthropic.TextBlock); ok {
232+ content += text.Text
181− case anthropic.ContentBlockStopEvent:
236+ toolCalls := a.extractToolCalls(accumulatedMessage.Content)
182− eventChan <- ProviderEvent{Type: EventContentStop}
237+ tokenUsage := a.extractTokenUsage(accumulatedMessage.Usage)
184− case anthropic.MessageStopEvent:
239+ eventChan <- ProviderEvent{
185− content := ""
240+ Type: EventComplete,
186− for _, block := range accumulatedMessage.Content {
241+ Response: &ProviderResponse{
187− if text, ok := block.AsAny().(anthropic.TextBlock); ok {
242+ Content: content,
188− content += text.Text
243+ ToolCalls: toolCalls,
245+ FinishReason: string(accumulatedMessage.StopReason),
192− toolCalls := a.extractToolCalls(accumulatedMessage.Content)
251+ // If the stream completed successfully, we're done
193− tokenUsage := a.extractTokenUsage(accumulatedMessage.Usage)
252+ if streamSuccess {
195− eventChan <- ProviderEvent{
256+ // Check for stream errors
196− Type: EventComplete,
257+ err := stream.Err()
197− Response: &ProviderResponse{
258+ if err != nil {
198− Content: content,
259+ log.Println("error", err)
199− ToolCalls: toolCalls,
260+
200− Usage: tokenUsage,
261+ var apierr *anthropic.Error
201− FinishReason: string(accumulatedMessage.StopReason),
262+ if errors.As(err, &apierr) && apierr.StatusCode == 429 {
207− if stream.Err() != nil {
266+ // For non-rate limit errors, report and exit
208− eventChan <- ProviderEvent{Type: EventError, Error: stream.Err()}
267+ eventChan <- ProviderEvent{Type: EventError, Error: err}
@@ -319,3 +380,4 @@ func (a *anthropicProvider) convertToAnthropicMessages(messages []message.Messag
320 return anthropicMessages
381 return anthropicMessages