github.com/anthropics/anthropic-sdk-go@v1.70.1/client_test.go (about)

     1  // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
     2  
     3  package anthropic_test
     4  
     5  import (
     6  	"context"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"reflect"
    12  	"strings"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/anthropics/anthropic-sdk-go"
    17  	"github.com/anthropics/anthropic-sdk-go/internal"
    18  	"github.com/anthropics/anthropic-sdk-go/option"
    19  )
    20  
    21  type closureTransport struct {
    22  	fn func(req *http.Request) (*http.Response, error)
    23  }
    24  
    25  func (t *closureTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    26  	return t.fn(req)
    27  }
    28  
    29  func TestUserAgentHeader(t *testing.T) {
    30  	var userAgent string
    31  	client := anthropic.NewClient(
    32  		option.WithAPIKey("my-anthropic-api-key"),
    33  		option.WithHTTPClient(&http.Client{
    34  			Transport: &closureTransport{
    35  				fn: func(req *http.Request) (*http.Response, error) {
    36  					userAgent = req.Header.Get("User-Agent")
    37  					return &http.Response{
    38  						StatusCode: http.StatusOK,
    39  					}, nil
    40  				},
    41  			},
    42  		}),
    43  	)
    44  	_, _ = client.Messages.New(context.Background(), anthropic.MessageNewParams{
    45  		MaxTokens: 1024,
    46  		Messages: []anthropic.MessageParam{{
    47  			Content: []anthropic.ContentBlockParamUnion{{
    48  				OfText: &anthropic.TextBlockParam{
    49  					Text: "x",
    50  				},
    51  			}},
    52  			Role: anthropic.MessageParamRoleUser,
    53  		}},
    54  		Model: anthropic.ModelClaudeSonnet5,
    55  	})
    56  	if userAgent != fmt.Sprintf("Anthropic/Go %s", internal.PackageVersion) {
    57  		t.Errorf("Expected User-Agent to be correct, but got: %#v", userAgent)
    58  	}
    59  }
    60  
    61  func TestRetryAfter(t *testing.T) {
    62  	retryCountHeaders := make([]string, 0)
    63  	client := anthropic.NewClient(
    64  		option.WithAPIKey("my-anthropic-api-key"),
    65  		option.WithHTTPClient(&http.Client{
    66  			Transport: &closureTransport{
    67  				fn: func(req *http.Request) (*http.Response, error) {
    68  					retryCountHeaders = append(retryCountHeaders, req.Header.Get("X-Stainless-Retry-Count"))
    69  					return &http.Response{
    70  						StatusCode: http.StatusTooManyRequests,
    71  						Header: http.Header{
    72  							http.CanonicalHeaderKey("Retry-After"): []string{"0.1"},
    73  						},
    74  					}, nil
    75  				},
    76  			},
    77  		}),
    78  	)
    79  	_, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
    80  		MaxTokens: 1024,
    81  		Messages: []anthropic.MessageParam{{
    82  			Content: []anthropic.ContentBlockParamUnion{{
    83  				OfText: &anthropic.TextBlockParam{
    84  					Text: "x",
    85  				},
    86  			}},
    87  			Role: anthropic.MessageParamRoleUser,
    88  		}},
    89  		Model: anthropic.ModelClaudeSonnet5,
    90  	})
    91  	if err == nil {
    92  		t.Error("Expected there to be a cancel error")
    93  	}
    94  
    95  	attempts := len(retryCountHeaders)
    96  	if attempts != 3 {
    97  		t.Errorf("Expected %d attempts, got %d", 3, attempts)
    98  	}
    99  
   100  	expectedRetryCountHeaders := []string{"0", "1", "2"}
   101  	if !reflect.DeepEqual(retryCountHeaders, expectedRetryCountHeaders) {
   102  		t.Errorf("Expected %v retry count headers, got %v", expectedRetryCountHeaders, retryCountHeaders)
   103  	}
   104  }
   105  
   106  func TestDeleteRetryCountHeader(t *testing.T) {
   107  	retryCountHeaders := make([]string, 0)
   108  	client := anthropic.NewClient(
   109  		option.WithAPIKey("my-anthropic-api-key"),
   110  		option.WithHTTPClient(&http.Client{
   111  			Transport: &closureTransport{
   112  				fn: func(req *http.Request) (*http.Response, error) {
   113  					retryCountHeaders = append(retryCountHeaders, req.Header.Get("X-Stainless-Retry-Count"))
   114  					return &http.Response{
   115  						StatusCode: http.StatusTooManyRequests,
   116  						Header: http.Header{
   117  							http.CanonicalHeaderKey("Retry-After"): []string{"0.1"},
   118  						},
   119  					}, nil
   120  				},
   121  			},
   122  		}),
   123  		option.WithHeaderDel("X-Stainless-Retry-Count"),
   124  	)
   125  	_, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
   126  		MaxTokens: 1024,
   127  		Messages: []anthropic.MessageParam{{
   128  			Content: []anthropic.ContentBlockParamUnion{{
   129  				OfText: &anthropic.TextBlockParam{
   130  					Text: "x",
   131  				},
   132  			}},
   133  			Role: anthropic.MessageParamRoleUser,
   134  		}},
   135  		Model: anthropic.ModelClaudeSonnet5,
   136  	})
   137  	if err == nil {
   138  		t.Error("Expected there to be a cancel error")
   139  	}
   140  
   141  	expectedRetryCountHeaders := []string{"", "", ""}
   142  	if !reflect.DeepEqual(retryCountHeaders, expectedRetryCountHeaders) {
   143  		t.Errorf("Expected %v retry count headers, got %v", expectedRetryCountHeaders, retryCountHeaders)
   144  	}
   145  }
   146  
   147  func TestOverwriteRetryCountHeader(t *testing.T) {
   148  	retryCountHeaders := make([]string, 0)
   149  	client := anthropic.NewClient(
   150  		option.WithAPIKey("my-anthropic-api-key"),
   151  		option.WithHTTPClient(&http.Client{
   152  			Transport: &closureTransport{
   153  				fn: func(req *http.Request) (*http.Response, error) {
   154  					retryCountHeaders = append(retryCountHeaders, req.Header.Get("X-Stainless-Retry-Count"))
   155  					return &http.Response{
   156  						StatusCode: http.StatusTooManyRequests,
   157  						Header: http.Header{
   158  							http.CanonicalHeaderKey("Retry-After"): []string{"0.1"},
   159  						},
   160  					}, nil
   161  				},
   162  			},
   163  		}),
   164  		option.WithHeader("X-Stainless-Retry-Count", "42"),
   165  	)
   166  	_, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
   167  		MaxTokens: 1024,
   168  		Messages: []anthropic.MessageParam{{
   169  			Content: []anthropic.ContentBlockParamUnion{{
   170  				OfText: &anthropic.TextBlockParam{
   171  					Text: "x",
   172  				},
   173  			}},
   174  			Role: anthropic.MessageParamRoleUser,
   175  		}},
   176  		Model: anthropic.ModelClaudeSonnet5,
   177  	})
   178  	if err == nil {
   179  		t.Error("Expected there to be a cancel error")
   180  	}
   181  
   182  	expectedRetryCountHeaders := []string{"42", "42", "42"}
   183  	if !reflect.DeepEqual(retryCountHeaders, expectedRetryCountHeaders) {
   184  		t.Errorf("Expected %v retry count headers, got %v", expectedRetryCountHeaders, retryCountHeaders)
   185  	}
   186  }
   187  
   188  func TestRetryAfterMs(t *testing.T) {
   189  	attempts := 0
   190  	client := anthropic.NewClient(
   191  		option.WithAPIKey("my-anthropic-api-key"),
   192  		option.WithHTTPClient(&http.Client{
   193  			Transport: &closureTransport{
   194  				fn: func(req *http.Request) (*http.Response, error) {
   195  					attempts++
   196  					return &http.Response{
   197  						StatusCode: http.StatusTooManyRequests,
   198  						Header: http.Header{
   199  							http.CanonicalHeaderKey("Retry-After-Ms"): []string{"100"},
   200  						},
   201  					}, nil
   202  				},
   203  			},
   204  		}),
   205  	)
   206  	_, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
   207  		MaxTokens: 1024,
   208  		Messages: []anthropic.MessageParam{{
   209  			Content: []anthropic.ContentBlockParamUnion{{
   210  				OfText: &anthropic.TextBlockParam{
   211  					Text: "x",
   212  				},
   213  			}},
   214  			Role: anthropic.MessageParamRoleUser,
   215  		}},
   216  		Model: anthropic.ModelClaudeSonnet5,
   217  	})
   218  	if err == nil {
   219  		t.Error("Expected there to be a cancel error")
   220  	}
   221  	if want := 3; attempts != want {
   222  		t.Errorf("Expected %d attempts, got %d", want, attempts)
   223  	}
   224  }
   225  
   226  func TestNonJSONErrorBody(t *testing.T) {
   227  	for _, body := range []string{"length limit exceeded", "too large", ""} {
   228  		client := anthropic.NewClient(
   229  			option.WithAPIKey("my-anthropic-api-key"),
   230  			option.WithHTTPClient(&http.Client{
   231  				Transport: &closureTransport{
   232  					fn: func(req *http.Request) (*http.Response, error) {
   233  						return &http.Response{
   234  							StatusCode: http.StatusRequestEntityTooLarge,
   235  							Header: http.Header{
   236  								http.CanonicalHeaderKey("Content-Type"): []string{"text/plain"},
   237  							},
   238  							Body: io.NopCloser(strings.NewReader(body)),
   239  						}, nil
   240  					},
   241  				},
   242  			}),
   243  		)
   244  		_, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
   245  			MaxTokens: 1024,
   246  			Messages: []anthropic.MessageParam{{
   247  				Content: []anthropic.ContentBlockParamUnion{{
   248  					OfText: &anthropic.TextBlockParam{
   249  						Text: "x",
   250  					},
   251  				}},
   252  				Role: anthropic.MessageParamRoleUser,
   253  			}},
   254  			Model: anthropic.ModelClaudeSonnet5,
   255  		})
   256  		var apiErr *anthropic.Error
   257  		if !errors.As(err, &apiErr) || apiErr.StatusCode != http.StatusRequestEntityTooLarge {
   258  			t.Fatalf("Expected a 413 API error, got %v", err)
   259  		}
   260  		if raw := apiErr.RawJSON(); raw != body {
   261  			t.Errorf("Expected raw body %q, got %q", body, raw)
   262  		}
   263  		if want := strings.TrimSpace("413 Request Entity Too Large " + body); !strings.HasSuffix(err.Error(), want) {
   264  			t.Errorf("Expected error %q to end with %q", err.Error(), want)
   265  		}
   266  	}
   267  }
   268  
   269  func TestContextCancel(t *testing.T) {
   270  	client := anthropic.NewClient(
   271  		option.WithAPIKey("my-anthropic-api-key"),
   272  		option.WithHTTPClient(&http.Client{
   273  			Transport: &closureTransport{
   274  				fn: func(req *http.Request) (*http.Response, error) {
   275  					<-req.Context().Done()
   276  					return nil, req.Context().Err()
   277  				},
   278  			},
   279  		}),
   280  	)
   281  	cancelCtx, cancel := context.WithCancel(context.Background())
   282  	cancel()
   283  	_, err := client.Messages.New(cancelCtx, anthropic.MessageNewParams{
   284  		MaxTokens: 1024,
   285  		Messages: []anthropic.MessageParam{{
   286  			Content: []anthropic.ContentBlockParamUnion{{
   287  				OfText: &anthropic.TextBlockParam{
   288  					Text: "x",
   289  				},
   290  			}},
   291  			Role: anthropic.MessageParamRoleUser,
   292  		}},
   293  		Model: anthropic.ModelClaudeSonnet5,
   294  	})
   295  	if err == nil {
   296  		t.Error("Expected there to be a cancel error")
   297  	}
   298  }
   299  
   300  func TestContextCancelDelay(t *testing.T) {
   301  	client := anthropic.NewClient(
   302  		option.WithAPIKey("my-anthropic-api-key"),
   303  		option.WithHTTPClient(&http.Client{
   304  			Transport: &closureTransport{
   305  				fn: func(req *http.Request) (*http.Response, error) {
   306  					<-req.Context().Done()
   307  					return nil, req.Context().Err()
   308  				},
   309  			},
   310  		}),
   311  	)
   312  	cancelCtx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
   313  	defer cancel()
   314  	_, err := client.Messages.New(cancelCtx, anthropic.MessageNewParams{
   315  		MaxTokens: 1024,
   316  		Messages: []anthropic.MessageParam{{
   317  			Content: []anthropic.ContentBlockParamUnion{{
   318  				OfText: &anthropic.TextBlockParam{
   319  					Text: "x",
   320  				},
   321  			}},
   322  			Role: anthropic.MessageParamRoleUser,
   323  		}},
   324  		Model: anthropic.ModelClaudeSonnet5,
   325  	})
   326  	if err == nil {
   327  		t.Error("expected there to be a cancel error")
   328  	}
   329  }
   330  
   331  func TestRequestTimeoutRetried(t *testing.T) {
   332  	attempts := 0
   333  	client := anthropic.NewClient(
   334  		option.WithAPIKey("my-anthropic-api-key"),
   335  		option.WithHTTPClient(&http.Client{
   336  			Transport: &closureTransport{
   337  				fn: func(req *http.Request) (*http.Response, error) {
   338  					attempts++
   339  					<-req.Context().Done()
   340  					return nil, req.Context().Err()
   341  				},
   342  			},
   343  		}),
   344  	)
   345  	_, err := client.Messages.New(
   346  		context.Background(),
   347  		anthropic.MessageNewParams{
   348  			MaxTokens: 1024,
   349  			Messages: []anthropic.MessageParam{{
   350  				Content: []anthropic.ContentBlockParamUnion{{
   351  					OfText: &anthropic.TextBlockParam{
   352  						Text: "x",
   353  					},
   354  				}},
   355  				Role: anthropic.MessageParamRoleUser,
   356  			}},
   357  			Model: anthropic.ModelClaudeSonnet5,
   358  		},
   359  		option.WithRequestTimeout((10 * time.Millisecond)),
   360  		option.WithMaxRetries(1),
   361  	)
   362  	if !errors.Is(err, context.DeadlineExceeded) {
   363  		t.Errorf("expected context.DeadlineExceeded after exhausting retries, got: %v", err)
   364  	}
   365  	if want := 2; attempts != want {
   366  		t.Errorf("Expected %d attempts, got %d", want, attempts)
   367  	}
   368  }
   369  
   370  func TestRequestTimeoutNotRetriedAfterContextDone(t *testing.T) {
   371  	attempts := 0
   372  	cancelCtx, cancel := context.WithCancel(context.Background())
   373  	defer cancel()
   374  	client := anthropic.NewClient(
   375  		option.WithAPIKey("my-anthropic-api-key"),
   376  		option.WithHTTPClient(&http.Client{
   377  			Transport: &closureTransport{
   378  				fn: func(req *http.Request) (*http.Response, error) {
   379  					attempts++
   380  					cancel()
   381  					<-req.Context().Done()
   382  					return nil, req.Context().Err()
   383  				},
   384  			},
   385  		}),
   386  	)
   387  	_, err := client.Messages.New(
   388  		cancelCtx,
   389  		anthropic.MessageNewParams{
   390  			MaxTokens: 1024,
   391  			Messages: []anthropic.MessageParam{{
   392  				Content: []anthropic.ContentBlockParamUnion{{
   393  					OfText: &anthropic.TextBlockParam{
   394  						Text: "x",
   395  					},
   396  				}},
   397  				Role: anthropic.MessageParamRoleUser,
   398  			}},
   399  			Model: anthropic.ModelClaudeSonnet5,
   400  		},
   401  		option.WithRequestTimeout(time.Second),
   402  	)
   403  	if !errors.Is(err, context.Canceled) {
   404  		t.Errorf("expected context.Canceled, got: %v", err)
   405  	}
   406  	if want := 1; attempts != want {
   407  		t.Errorf("Expected %d attempts, got %d", want, attempts)
   408  	}
   409  }
   410  
   411  func TestContextDeadline(t *testing.T) {
   412  	testTimeout := time.After(3 * time.Second)
   413  	testDone := make(chan struct{})
   414  
   415  	deadline := time.Now().Add(100 * time.Millisecond)
   416  	deadlineCtx, cancel := context.WithDeadline(context.Background(), deadline)
   417  	defer cancel()
   418  
   419  	go func() {
   420  		client := anthropic.NewClient(
   421  			option.WithAPIKey("my-anthropic-api-key"),
   422  			option.WithHTTPClient(&http.Client{
   423  				Transport: &closureTransport{
   424  					fn: func(req *http.Request) (*http.Response, error) {
   425  						<-req.Context().Done()
   426  						return nil, req.Context().Err()
   427  					},
   428  				},
   429  			}),
   430  		)
   431  		_, err := client.Messages.New(deadlineCtx, anthropic.MessageNewParams{
   432  			MaxTokens: 1024,
   433  			Messages: []anthropic.MessageParam{{
   434  				Content: []anthropic.ContentBlockParamUnion{{
   435  					OfText: &anthropic.TextBlockParam{
   436  						Text: "x",
   437  					},
   438  				}},
   439  				Role: anthropic.MessageParamRoleUser,
   440  			}},
   441  			Model: anthropic.ModelClaudeSonnet5,
   442  		})
   443  		if err == nil {
   444  			t.Error("expected there to be a deadline error")
   445  		}
   446  		close(testDone)
   447  	}()
   448  
   449  	select {
   450  	case <-testTimeout:
   451  		t.Fatal("client didn't finish in time")
   452  	case <-testDone:
   453  		if diff := time.Since(deadline); diff < -30*time.Millisecond || 30*time.Millisecond < diff {
   454  			t.Fatalf("client did not return within 30ms of context deadline, got %s", diff)
   455  		}
   456  	}
   457  }
   458  
   459  func TestContextDeadlineStreaming(t *testing.T) {
   460  	testTimeout := time.After(3 * time.Second)
   461  	testDone := make(chan struct{})
   462  
   463  	deadline := time.Now().Add(100 * time.Millisecond)
   464  	deadlineCtx, cancel := context.WithDeadline(context.Background(), deadline)
   465  	defer cancel()
   466  
   467  	go func() {
   468  		client := anthropic.NewClient(
   469  			option.WithAPIKey("my-anthropic-api-key"),
   470  			option.WithHTTPClient(&http.Client{
   471  				Transport: &closureTransport{
   472  					fn: func(req *http.Request) (*http.Response, error) {
   473  						return &http.Response{
   474  							StatusCode: 200,
   475  							Status:     "200 OK",
   476  							Body: io.NopCloser(
   477  								io.Reader(readerFunc(func([]byte) (int, error) {
   478  									<-req.Context().Done()
   479  									return 0, req.Context().Err()
   480  								})),
   481  							),
   482  						}, nil
   483  					},
   484  				},
   485  			}),
   486  		)
   487  		stream := client.Messages.NewStreaming(deadlineCtx, anthropic.MessageNewParams{
   488  			MaxTokens: 1024,
   489  			Messages: []anthropic.MessageParam{{
   490  				Content: []anthropic.ContentBlockParamUnion{{
   491  					OfText: &anthropic.TextBlockParam{
   492  						Text: "x",
   493  					},
   494  				}},
   495  				Role: anthropic.MessageParamRoleUser,
   496  			}},
   497  			Model: anthropic.ModelClaudeOpus5,
   498  		})
   499  		for stream.Next() {
   500  			_ = stream.Current()
   501  		}
   502  		if stream.Err() == nil {
   503  			t.Error("expected there to be a deadline error")
   504  		}
   505  		close(testDone)
   506  	}()
   507  
   508  	select {
   509  	case <-testTimeout:
   510  		t.Fatal("client didn't finish in time")
   511  	case <-testDone:
   512  		if diff := time.Since(deadline); diff < -30*time.Millisecond || 30*time.Millisecond < diff {
   513  			t.Fatalf("client did not return within 30ms of context deadline, got %s", diff)
   514  		}
   515  	}
   516  }
   517  
   518  func TestContextDeadlineStreamingWithRequestTimeout(t *testing.T) {
   519  	testTimeout := time.After(3 * time.Second)
   520  	testDone := make(chan struct{})
   521  	deadline := time.Now().Add(100 * time.Millisecond)
   522  
   523  	go func() {
   524  		client := anthropic.NewClient(
   525  			option.WithAPIKey("my-anthropic-api-key"),
   526  			option.WithHTTPClient(&http.Client{
   527  				Transport: &closureTransport{
   528  					fn: func(req *http.Request) (*http.Response, error) {
   529  						return &http.Response{
   530  							StatusCode: 200,
   531  							Status:     "200 OK",
   532  							Body: io.NopCloser(
   533  								io.Reader(readerFunc(func([]byte) (int, error) {
   534  									<-req.Context().Done()
   535  									return 0, req.Context().Err()
   536  								})),
   537  							),
   538  						}, nil
   539  					},
   540  				},
   541  			}),
   542  		)
   543  		stream := client.Messages.NewStreaming(
   544  			context.Background(),
   545  			anthropic.MessageNewParams{
   546  				MaxTokens: 1024,
   547  				Messages: []anthropic.MessageParam{{
   548  					Content: []anthropic.ContentBlockParamUnion{{
   549  						OfText: &anthropic.TextBlockParam{
   550  							Text: "x",
   551  						},
   552  					}},
   553  					Role: anthropic.MessageParamRoleUser,
   554  				}},
   555  				Model: anthropic.ModelClaudeOpus5,
   556  			},
   557  			option.WithRequestTimeout((100 * time.Millisecond)),
   558  		)
   559  		for stream.Next() {
   560  			_ = stream.Current()
   561  		}
   562  		if stream.Err() == nil {
   563  			t.Error("expected there to be a deadline error")
   564  		}
   565  		close(testDone)
   566  	}()
   567  
   568  	select {
   569  	case <-testTimeout:
   570  		t.Fatal("client didn't finish in time")
   571  	case <-testDone:
   572  		if diff := time.Since(deadline); diff < -30*time.Millisecond || 30*time.Millisecond < diff {
   573  			t.Fatalf("client did not return within 30ms of context deadline, got %s", diff)
   574  		}
   575  	}
   576  }
   577  
   578  type readerFunc func([]byte) (int, error)
   579  
   580  func (f readerFunc) Read(p []byte) (int, error) { return f(p) }
   581  func (f readerFunc) Close() error               { return nil }