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 }