github.com/anthropics/anthropic-sdk-go@v1.70.1/message_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 "encoding/json" 8 "errors" 9 "os" 10 "strings" 11 "testing" 12 "time" 13 14 "github.com/anthropics/anthropic-sdk-go" 15 "github.com/anthropics/anthropic-sdk-go/internal/testutil" 16 "github.com/anthropics/anthropic-sdk-go/option" 17 "github.com/anthropics/anthropic-sdk-go/shared/constant" 18 ) 19 20 func TestMessageNewWithOptionalParams(t *testing.T) { 21 baseURL := "http://localhost:4010" 22 if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 23 baseURL = envURL 24 } 25 if !testutil.CheckTestServer(t, baseURL) { 26 return 27 } 28 client := anthropic.NewClient( 29 option.WithBaseURL(baseURL), 30 option.WithAPIKey("my-anthropic-api-key"), 31 ) 32 _, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ 33 MaxTokens: 1024, 34 Messages: []anthropic.MessageParam{{ 35 Content: []anthropic.ContentBlockParamUnion{{ 36 OfText: &anthropic.TextBlockParam{ 37 Text: "x", 38 CacheControl: anthropic.CacheControlEphemeralParam{ 39 TTL: anthropic.CacheControlEphemeralTTLTTL5m, 40 }, 41 Citations: []anthropic.TextCitationParamUnion{{ 42 OfCharLocation: &anthropic.CitationCharLocationParam{ 43 CitedText: "The grass is green. The sky is blue.", 44 DocumentIndex: 0, 45 DocumentTitle: anthropic.String("x"), 46 EndCharIndex: 0, 47 StartCharIndex: 0, 48 }, 49 }}, 50 }, 51 }}, 52 Role: anthropic.MessageParamRoleUser, 53 }}, 54 Model: anthropic.ModelClaudeOpus5, 55 CacheControl: anthropic.CacheControlEphemeralParam{ 56 TTL: anthropic.CacheControlEphemeralTTLTTL5m, 57 }, 58 Container: anthropic.MessageCreateParamsContainerUnion{ 59 OfContainers: &anthropic.ContainerParams{ 60 ID: anthropic.String("id"), 61 Skills: []anthropic.SkillParams{{ 62 SkillID: "pdf", 63 Type: anthropic.SkillParamsTypeAnthropic, 64 Version: anthropic.String("latest"), 65 }}, 66 }, 67 }, 68 InferenceGeo: anthropic.String("inference_geo"), 69 Metadata: anthropic.MetadataParam{ 70 UserID: anthropic.String("13803d75-b4b5-4c3e-b2a2-6f21399b021b"), 71 }, 72 OutputConfig: anthropic.OutputConfigParam{ 73 Effort: anthropic.OutputConfigEffortLow, 74 Format: anthropic.JSONOutputFormatParam{ 75 Schema: map[string]any{ 76 "foo": "bar", 77 }, 78 }, 79 }, 80 ServiceTier: anthropic.MessageNewParamsServiceTierAuto, 81 StopSequences: []string{"string"}, 82 System: []anthropic.TextBlockParam{{ 83 Text: "Today's date is 2024-06-01.", 84 CacheControl: anthropic.CacheControlEphemeralParam{ 85 TTL: anthropic.CacheControlEphemeralTTLTTL5m, 86 }, 87 Citations: []anthropic.TextCitationParamUnion{{ 88 OfCharLocation: &anthropic.CitationCharLocationParam{ 89 CitedText: "The grass is green. The sky is blue.", 90 DocumentIndex: 0, 91 DocumentTitle: anthropic.String("x"), 92 EndCharIndex: 0, 93 StartCharIndex: 0, 94 }, 95 }}, 96 }}, 97 Temperature: anthropic.Float(1), 98 Thinking: anthropic.ThinkingConfigParamUnion{ 99 OfAdaptive: &anthropic.ThinkingConfigAdaptiveParam{ 100 Display: anthropic.ThinkingConfigAdaptiveDisplaySummarized, 101 }, 102 }, 103 ToolChoice: anthropic.ToolChoiceUnionParam{ 104 OfAuto: &anthropic.ToolChoiceAutoParam{ 105 DisableParallelToolUse: anthropic.Bool(true), 106 }, 107 }, 108 Tools: []anthropic.ToolUnionParam{{ 109 OfTool: &anthropic.ToolParam{ 110 InputSchema: anthropic.ToolInputSchemaParam{ 111 Properties: map[string]any{ 112 "location": "bar", 113 "unit": "bar", 114 }, 115 Required: []string{"location"}, 116 }, 117 Name: "name", 118 AllowedCallers: []string{"direct"}, 119 CacheControl: anthropic.CacheControlEphemeralParam{ 120 TTL: anthropic.CacheControlEphemeralTTLTTL5m, 121 }, 122 DeferLoading: anthropic.Bool(true), 123 Description: anthropic.String("Get the current weather in a given location"), 124 EagerInputStreaming: anthropic.Bool(true), 125 InputExamples: []map[string]any{{ 126 "foo": "bar", 127 }}, 128 Strict: anthropic.Bool(true), 129 Type: anthropic.ToolTypeCustom, 130 }, 131 }}, 132 TopK: anthropic.Int(5), 133 TopP: anthropic.Float(0.7), 134 UserProfileID: anthropic.String("anthropic-user-profile-id"), 135 WorkspaceID: anthropic.String("wrkspc_011CZkZaBF1tNoB5wlCeusgy"), 136 }) 137 if err != nil { 138 var apierr *anthropic.Error 139 if errors.As(err, &apierr) { 140 t.Log(string(apierr.DumpRequest(true))) 141 } 142 t.Fatalf("err should be nil: %s", err.Error()) 143 } 144 } 145 146 func TestMessageCountTokensWithOptionalParams(t *testing.T) { 147 baseURL := "http://localhost:4010" 148 if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 149 baseURL = envURL 150 } 151 if !testutil.CheckTestServer(t, baseURL) { 152 return 153 } 154 client := anthropic.NewClient( 155 option.WithBaseURL(baseURL), 156 option.WithAPIKey("my-anthropic-api-key"), 157 ) 158 _, err := client.Messages.CountTokens(context.TODO(), anthropic.MessageCountTokensParams{ 159 Messages: []anthropic.MessageParam{{ 160 Content: []anthropic.ContentBlockParamUnion{{ 161 OfText: &anthropic.TextBlockParam{ 162 Text: "x", 163 CacheControl: anthropic.CacheControlEphemeralParam{ 164 TTL: anthropic.CacheControlEphemeralTTLTTL5m, 165 }, 166 Citations: []anthropic.TextCitationParamUnion{{ 167 OfCharLocation: &anthropic.CitationCharLocationParam{ 168 CitedText: "The grass is green. The sky is blue.", 169 DocumentIndex: 0, 170 DocumentTitle: anthropic.String("x"), 171 EndCharIndex: 0, 172 StartCharIndex: 0, 173 }, 174 }}, 175 }, 176 }}, 177 Role: anthropic.MessageParamRoleUser, 178 }}, 179 Model: anthropic.ModelClaudeOpus5, 180 CacheControl: anthropic.CacheControlEphemeralParam{ 181 TTL: anthropic.CacheControlEphemeralTTLTTL5m, 182 }, 183 OutputConfig: anthropic.OutputConfigParam{ 184 Effort: anthropic.OutputConfigEffortLow, 185 Format: anthropic.JSONOutputFormatParam{ 186 Schema: map[string]any{ 187 "foo": "bar", 188 }, 189 }, 190 }, 191 System: anthropic.MessageCountTokensParamsSystemUnion{ 192 OfTextBlockArray: []anthropic.TextBlockParam{{ 193 Text: "Today's date is 2024-06-01.", 194 CacheControl: anthropic.CacheControlEphemeralParam{ 195 TTL: anthropic.CacheControlEphemeralTTLTTL5m, 196 }, 197 Citations: []anthropic.TextCitationParamUnion{{ 198 OfCharLocation: &anthropic.CitationCharLocationParam{ 199 CitedText: "The grass is green. The sky is blue.", 200 DocumentIndex: 0, 201 DocumentTitle: anthropic.String("x"), 202 EndCharIndex: 0, 203 StartCharIndex: 0, 204 }, 205 }}, 206 }}, 207 }, 208 Thinking: anthropic.ThinkingConfigParamUnion{ 209 OfAdaptive: &anthropic.ThinkingConfigAdaptiveParam{ 210 Display: anthropic.ThinkingConfigAdaptiveDisplaySummarized, 211 }, 212 }, 213 ToolChoice: anthropic.ToolChoiceUnionParam{ 214 OfAuto: &anthropic.ToolChoiceAutoParam{ 215 DisableParallelToolUse: anthropic.Bool(true), 216 }, 217 }, 218 Tools: []anthropic.MessageCountTokensToolUnionParam{{ 219 OfTool: &anthropic.ToolParam{ 220 InputSchema: anthropic.ToolInputSchemaParam{ 221 Properties: map[string]any{ 222 "location": "bar", 223 "unit": "bar", 224 }, 225 Required: []string{"location"}, 226 }, 227 Name: "name", 228 AllowedCallers: []string{"direct"}, 229 CacheControl: anthropic.CacheControlEphemeralParam{ 230 TTL: anthropic.CacheControlEphemeralTTLTTL5m, 231 }, 232 DeferLoading: anthropic.Bool(true), 233 Description: anthropic.String("Get the current weather in a given location"), 234 EagerInputStreaming: anthropic.Bool(true), 235 InputExamples: []map[string]any{{ 236 "foo": "bar", 237 }}, 238 Strict: anthropic.Bool(true), 239 Type: anthropic.ToolTypeCustom, 240 }, 241 }}, 242 UserProfileID: anthropic.String("anthropic-user-profile-id"), 243 WorkspaceID: anthropic.String("wrkspc_011CZkZaBF1tNoB5wlCeusgy"), 244 }) 245 if err != nil { 246 var apierr *anthropic.Error 247 if errors.As(err, &apierr) { 248 t.Log(string(apierr.DumpRequest(true))) 249 } 250 t.Fatalf("err should be nil: %s", err.Error()) 251 } 252 } 253 254 func TestAccumulate(t *testing.T) { 255 for name, testCase := range map[string]struct { 256 expected anthropic.Message 257 events []string 258 }{ 259 "empty message": { 260 expected: anthropic.Message{Usage: anthropic.Usage{}}, 261 events: []string{ 262 `{"type": "message_start", "message": {}}`, 263 `{"type: "message_stop"}`, 264 }, 265 }, 266 "text content block": { 267 events: []string{ 268 `{"type": "message_start", "message": {}}`, 269 `{"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": "This "}}`, 270 `{"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "is a "}}`, 271 `{"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta": "text": "text block!"}}`, 272 `{"type": "content_block_stop", "index": 0}`, 273 `{"type": "message_stop"}`, 274 }, 275 expected: anthropic.Message{Content: []anthropic.ContentBlockUnion{ 276 {Type: "text", Text: "This is a text block!"}, 277 }}, 278 }, 279 "text content block with citations": { 280 events: []string{ 281 `{"type": "message_start", "message": {}}`, 282 `{"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": "1 + 1"}}`, 283 `{"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": " = 2"}}`, 284 `{"type": "content_block_delta", "index": 0, "delta": {"type": "citations_delta", "citation": {"type": "char_location", "cited_text": "1 + 1 = 2", "document_index": 0, "document_title": "Math Facts", "start_char_index": 300, "end_char_index": 310 }}}`, 285 `{"type": "content_block_stop", "index": 0}`, 286 `{"type": "message_stop"}`, 287 }, 288 expected: anthropic.Message{Content: []anthropic.ContentBlockUnion{ 289 {Type: "text", Text: "1 + 1 = 2", Citations: []anthropic.TextCitationUnion{{ 290 Type: "char_location", 291 CitedText: "1 + 1 = 2", 292 DocumentIndex: 0, 293 DocumentTitle: "Math Facts", 294 StartCharIndex: 300, 295 EndCharIndex: 310, 296 }}}, 297 }}, 298 }, 299 "tool use block": { 300 events: []string{ 301 `{"type": "message_start", "message": {}}`, 302 `{"type": "content_block_start", "index": 0, "content_block": {"type": "tool_use", "id": "toolu_id", "name": "tool_name", "input": {}}}`, 303 `{"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "{\"argument\":"}}`, 304 `{"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": " \"value\"}"}}`, 305 `{"type": "content_block_stop", "index": 0}`, 306 `{"type": "message_stop"}`, 307 }, 308 expected: anthropic.Message{Content: []anthropic.ContentBlockUnion{ 309 {Type: "tool_use", ID: "toolu_id", Name: "tool_name", Input: []byte(`{"argument": "value"}`)}, 310 }}, 311 }, 312 "tool use block with no params": { 313 events: []string{ 314 `{"type": "message_start", "message": {}}`, 315 `{"type": "content_block_start": "index": 0, "content_block": {"type": "tool_use", "id": "toolu_id", "name": "tool_name", input: {}}}`, 316 `{"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": ""}}`, 317 `{"type": "content_block_stop", "index": 0}`, 318 `{"type": "message_stop"}`, 319 }, 320 expected: anthropic.Message{Content: []anthropic.ContentBlockUnion{ 321 {Type: "tool_use", ID: "toolu_id", Name: "tool_name"}, 322 }}, 323 }, 324 "server tool use block": { 325 events: []string{ 326 `{"type": "message_start", "message": {}}`, 327 `{"type": "content_block_start": "index": 0, "content_block": {"type": "server_tool_use", "id": "srvtoolu_id", "name": "web_search", input: {}}}`, 328 `{"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": ""}}`, 329 `{"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "{\"query\": \"weat"}}`, 330 `{"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "her\"}"}}`, 331 `{"type": "content_block_stop", "index": 0}`, 332 `{"type": "message_stop"}`, 333 }, 334 expected: anthropic.Message{Content: []anthropic.ContentBlockUnion{ 335 {Type: "server_tool_use", ID: "srvtoolu_id", Name: "web_search", Input: []byte(`{"query": "weather"}`)}, 336 }}, 337 }, 338 "thinking block": { 339 events: []string{ 340 `{"type": "message_start", "message": {}}`, 341 `{"type": "content_block_start", "index": 0, "content_block": {"type": "thinking", "thinking": "Let me think..."}}`, 342 `{"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": " 343 First, let's try this..."}}`, 344 `{"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": " 345 Therefore, the answer is..."}}`, 346 `{"type": "content_block_delta", "index": 0, "delta": {"type": "signature_delta", "signature": "ThinkingSignature"}}`, 347 `{"type": "content_block_stop", "index": 0}`, 348 `{"type": "message_stop"}`, 349 }, 350 expected: anthropic.Message{Content: []anthropic.ContentBlockUnion{ 351 {Type: "thinking", Thinking: "Let me think...\nFirst, let's try this...\nTherefore, the answer is...", Signature: "ThinkingSignature"}, 352 }}, 353 }, 354 "redacted thinking block": { 355 events: []string{ 356 `{"type": "message_start", "message": {}}`, 357 `{"type": "content_block_start", "index": 0, "content_block": {"type": "redacted_thinking", "data": "Redacted"}}`, 358 `{"type": "content_block_stop", "index": 0}`, 359 `{"type": "message_stop"}`, 360 }, 361 expected: anthropic.Message{Content: []anthropic.ContentBlockUnion{ 362 {Type: "redacted_thinking", Data: "Redacted"}, 363 }}, 364 }, 365 "multiple content blocks": { 366 events: []string{ 367 `{"type": "message_start", "message": {}}`, 368 `{"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": "Let me look up "}}`, 369 `{"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "the weather for "}}`, 370 `{"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta": "text": "you."}}`, 371 `{"type": "content_block_stop", "index": 0}`, 372 `{"type": "content_block_start", "index": 1, "content_block": {"type": "thinking", "thinking": ""}}`, 373 `{"type": "content_block_delta", "index": 1, "delta": {"type": "thinking_delta", "thinking": "I can look this "}}`, 374 `{"type": "content_block_delta", "index": 1, "delta": {"type": "thinking_delta", "thinking": "up using a tool."}}`, 375 `{"type": "content_block_stop", "index": 1}`, 376 `{"type": "content_block_start", "index": 2, "content_block": {"type": "tool_use", "id": "toolu_id", "name": "get_weather", "input": {}}}`, 377 `{"type": "content_block_delta", "index": 2, "delta": {"type": "input_json_delta", "partial_json": "{\"city\": "}}`, 378 `{"type": "content_block_delta", "index": 2, "delta": {"type": "input_json_delta", "partial_json": "\"Los Angeles\"}"}}`, 379 `{"type": "content_block_stop", "index": 2}`, 380 `{"type": "content_block_start", "index": 3, "content_block": {"type": "text", "text": ""}}`, 381 `{"type": "content_block_delta", "index": 3, "delta": {"type": "text_delta", "text": "The weather in Los Angeles"}}`, 382 `{"type": "content_block_delta", "index": 3, "delta": {"type": "text_delta", "text": " is 85 degrees Fahrenheit!"}}`, 383 `{"type": "content_block_stop", "index": 3"}`, 384 `{"type": "message_stop"}`, 385 }, 386 expected: anthropic.Message{Content: []anthropic.ContentBlockUnion{ 387 {Type: "text", Text: "Let me look up the weather for you."}, 388 {Type: "thinking", Thinking: "I can look this up using a tool."}, 389 {Type: "tool_use", ID: "toolu_id", Name: "get_weather", Input: []byte(`{"city": "Los Angeles"}`)}, 390 {Type: "text", Text: "The weather in Los Angeles is 85 degrees Fahrenheit!"}, 391 }}, 392 }, 393 "interleaved content blocks": { 394 events: []string{ 395 `{"type": "message_start", "message": {}}`, 396 `{"type": "content_block_start", "index": 0, "content_block": {"type": "thinking", "thinking": ""}}`, 397 `{"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "Let me think."}}`, 398 `{"type": "content_block_delta", "index": 0, "delta": {"type": "signature_delta", "signature": "sig123"}}`, 399 `{"type": "content_block_stop", "index": 0}`, 400 `{"type": "content_block_start", "index": 1, "content_block": {"type": "text", "text": ""}}`, 401 `{"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": "Hello"}}`, 402 `{"type": "content_block_start", "index": 2, "content_block": {"type": "tool_use", "id": "toolu_id", "name": "get_weather", "input": {}}}`, 403 `{"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": " world"}}`, 404 `{"type": "content_block_delta", "index": 2, "delta": {"type": "input_json_delta", "partial_json": "{\"city\": "}}`, 405 `{"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": "!"}}`, 406 `{"type": "content_block_delta", "index": 2, "delta": {"type": "input_json_delta", "partial_json": "\"Los Angeles\"}"}}`, 407 `{"type": "content_block_stop", "index": 1}`, 408 `{"type": "content_block_stop", "index": 2}`, 409 `{"type": "message_stop"}`, 410 }, 411 expected: anthropic.Message{Content: []anthropic.ContentBlockUnion{ 412 {Type: "thinking", Thinking: "Let me think.", Signature: "sig123"}, 413 {Type: "text", Text: "Hello world!"}, 414 {Type: "tool_use", ID: "toolu_id", Name: "get_weather", Input: []byte(`{"city": "Los Angeles"}`)}, 415 }}, 416 }, 417 } { 418 t.Run(name, func(t *testing.T) { 419 message := anthropic.Message{} 420 for _, eventStr := range testCase.events { 421 event := anthropic.MessageStreamEventUnion{} 422 err := (&event).UnmarshalJSON([]byte(eventStr)) 423 if err != nil { 424 t.Fatal(err) 425 } 426 (&message).Accumulate(event) 427 } 428 marshaledMessage, err := json.Marshal(message) 429 if err != nil { 430 t.Fatal(err) 431 } 432 marshaledExpectedMessage, err := json.Marshal(testCase.expected) 433 if err != nil { 434 t.Fatal(err) 435 } 436 if string(marshaledMessage) != string(marshaledExpectedMessage) { 437 t.Fatalf("Mismatched message: expected %s but got %s", marshaledExpectedMessage, marshaledMessage) 438 } 439 }) 440 } 441 } 442 443 func TestAccumulateContentBlockIndexErrors(t *testing.T) { 444 for name, testCase := range map[string]struct { 445 events []string 446 wantErr string 447 }{ 448 "start with an index gap": { 449 events: []string{ 450 `{"type": "message_start", "message": {}}`, 451 `{"type": "content_block_start", "index": 1, "content_block": {"type": "text", "text": ""}}`, 452 }, 453 wantErr: "expected index 0", 454 }, 455 "delta for a block that never started": { 456 events: []string{ 457 `{"type": "message_start", "message": {}}`, 458 `{"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}`, 459 `{"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": "hi"}}`, 460 }, 461 wantErr: "only 1 content blocks", 462 }, 463 "delta with a negative index": { 464 events: []string{ 465 `{"type": "message_start", "message": {}}`, 466 `{"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}`, 467 `{"type": "content_block_delta", "index": -1, "delta": {"type": "text_delta", "text": "hi"}}`, 468 }, 469 wantErr: "index -1", 470 }, 471 "stop for a block that never started": { 472 events: []string{ 473 `{"type": "message_start", "message": {}}`, 474 `{"type": "content_block_stop", "index": 0}`, 475 }, 476 wantErr: "only 0 content blocks", 477 }, 478 } { 479 t.Run(name, func(t *testing.T) { 480 message := anthropic.Message{} 481 for i, eventStr := range testCase.events { 482 event := anthropic.MessageStreamEventUnion{} 483 if err := (&event).UnmarshalJSON([]byte(eventStr)); err != nil { 484 t.Fatal(err) 485 } 486 err := (&message).Accumulate(event) 487 if i < len(testCase.events)-1 { 488 if err != nil { 489 t.Fatal(err) 490 } 491 continue 492 } 493 if err == nil { 494 t.Fatalf("expected error containing %q, got nil", testCase.wantErr) 495 } 496 if !strings.Contains(err.Error(), testCase.wantErr) { 497 t.Fatalf("expected error containing %q, got %q", testCase.wantErr, err.Error()) 498 } 499 } 500 }) 501 } 502 } 503 504 func TestMessageNewWithNonStreamingTimeoutLimits(t *testing.T) { 505 baseURL := "http://localhost:4010" 506 if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { 507 baseURL = envURL 508 } 509 if !testutil.CheckTestServer(t, baseURL) { 510 return 511 } 512 client := anthropic.NewClient( 513 option.WithBaseURL(baseURL), 514 option.WithAPIKey("my-anthropic-api-key"), 515 ) 516 517 originalModelTokenLimits := constant.ModelNonStreamingTokens 518 defer func() { constant.ModelNonStreamingTokens = originalModelTokenLimits }() 519 constant.ModelNonStreamingTokens = map[string]int{"test-model": 8192} 520 521 model := anthropic.Model("test-model") 522 testModelLimit := constant.ModelNonStreamingTokens[string(model)] 523 524 // This test verifies that we can still create a message with tokens below the limit 525 safeParams := anthropic.MessageNewParams{ 526 MaxTokens: int64(testModelLimit - 1000), // Well below the limit 527 Messages: []anthropic.MessageParam{{ 528 Content: []anthropic.ContentBlockParamUnion{{ 529 OfText: &anthropic.TextBlockParam{Text: "What is a quaternion?"}, 530 }}, 531 Role: anthropic.MessageParamRoleUser, 532 }}, 533 Model: model, 534 } 535 536 _, err := client.Messages.New(context.TODO(), safeParams) 537 if err != nil { 538 var apierr *anthropic.Error 539 if errors.As(err, &apierr) { 540 t.Log(string(apierr.DumpRequest(true))) 541 } 542 t.Fatalf("Expected no error for tokens below limit, got: %v", err) 543 } 544 545 // This test verifies that we get an error when exceeding the limit 546 unsafeParams := anthropic.MessageNewParams{ 547 MaxTokens: int64(testModelLimit + 1000), // Exceed the limit 548 Messages: []anthropic.MessageParam{{ 549 Content: []anthropic.ContentBlockParamUnion{{ 550 OfText: &anthropic.TextBlockParam{Text: "What is a quaternion?"}, 551 }}, 552 Role: anthropic.MessageParamRoleUser, 553 }}, 554 Model: model, 555 } 556 557 _, err = client.Messages.New(context.TODO(), unsafeParams) 558 if err == nil { 559 t.Fatal("Expected error for tokens above limit, got nil") 560 } 561 } 562 563 func TestCalculateNonStreamingTimeout(t *testing.T) { 564 // Store original model token limits to restore after test 565 originalModelTokenLimits := make(map[string]int) 566 for k, v := range constant.ModelNonStreamingTokens { 567 originalModelTokenLimits[k] = v 568 } 569 defer func() { 570 // Restore original model token limits 571 constant.ModelNonStreamingTokens = originalModelTokenLimits 572 }() 573 574 // Set up a test model for consistent testing 575 constant.ModelNonStreamingTokens = map[string]int{ 576 "test-model": 8192, 577 } 578 defaultTimeout := 10 * time.Minute 579 580 tests := []struct { 581 name string 582 maxTokens int 583 model string 584 expectTimeout time.Duration 585 expectError bool 586 opts []option.RequestOption 587 }{ 588 { 589 name: "small token count returns default timeout", 590 maxTokens: 1000, 591 model: "any-model", 592 expectTimeout: defaultTimeout, 593 expectError: false, 594 }, 595 { 596 name: "large token count above default time limit throws error", 597 maxTokens: 100000, 598 model: "any-model", 599 expectTimeout: 0, 600 expectError: true, 601 }, 602 { 603 name: "token count above model specific limit throws error", 604 maxTokens: 9000, 605 model: "test-model", 606 expectTimeout: 0, 607 expectError: true, 608 }, 609 { 610 name: "token count below model specific limit is ok", 611 maxTokens: 8000, 612 model: "test-model", 613 expectTimeout: defaultTimeout, 614 expectError: false, 615 }, 616 { 617 name: "user supplies custom timeout so we'll use that instead", 618 maxTokens: 1000, 619 model: "test-model", 620 expectTimeout: 30 * time.Second, // Custom timeout 621 expectError: false, 622 opts: []option.RequestOption{ 623 option.WithRequestTimeout(30 * time.Second), 624 }, 625 }, 626 } 627 628 for _, tc := range tests { 629 t.Run(tc.name, func(t *testing.T) { 630 timeout, err := anthropic.CalculateNonStreamingTimeout(tc.maxTokens, anthropic.Model(tc.model), tc.opts) 631 632 if tc.expectError && err == nil { 633 t.Error("Expected error but got nil") 634 } 635 636 if !tc.expectError && err != nil { 637 t.Errorf("Did not expect error but got: %v", err) 638 } 639 640 if timeout != tc.expectTimeout { 641 t.Errorf("Expected timeout %v but got %v", tc.expectTimeout, timeout) 642 } 643 }) 644 } 645 } 646 647 // Test specific model limits 648 func TestModelLimits(t *testing.T) { 649 // Verify the model limits are defined for opus-4 models 650 if _, exists := constant.ModelNonStreamingTokens["claude-opus-4-20250514"]; !exists { 651 t.Error("Expected model limit for claude-opus-4-20250514 but not found") 652 } 653 654 if _, exists := constant.ModelNonStreamingTokens["anthropic.claude-opus-4-20250514-v1:0"]; !exists { 655 t.Error("Expected model limit for anthropic.claude-opus-4-20250514-v1:0 but not found") 656 } 657 658 if _, exists := constant.ModelNonStreamingTokens["claude-opus-4@20250514"]; !exists { 659 t.Error("Expected model limit for claude-opus-4@20250514 but not found") 660 } 661 } 662 663 func TestToolResultBlockParamStringContent(t *testing.T) { 664 toolResultJSON := `{"type":"tool_result","content":"error message","tool_use_id":"123"}` 665 var toolResult anthropic.ToolResultBlockParam 666 err := json.Unmarshal([]byte(toolResultJSON), &toolResult) 667 if err != nil { 668 t.Fatal(err) 669 } 670 if len(toolResult.Content) != 1 || toolResult.Content[0].OfText.Text != "error message" { 671 t.Error("String content not converted to TextBlock") 672 } 673 } 674 675 func TestMessageParamStringContent(t *testing.T) { 676 messageJSON := `{"role":"user","content":"hello world"}` 677 var message anthropic.MessageParam 678 err := json.Unmarshal([]byte(messageJSON), &message) 679 if err != nil { 680 t.Fatal(err) 681 } 682 if len(message.Content) != 1 || message.Content[0].OfText.Text != "hello world" { 683 t.Error("String content not converted to TextBlock") 684 } 685 } 686 687 func TestMessageParamArrayContent(t *testing.T) { 688 messageJSON := `{"role":"user","content":[{"type":"text","text":"first block"},{"type":"text","text":"second block"}]}` 689 var message anthropic.MessageParam 690 err := json.Unmarshal([]byte(messageJSON), &message) 691 if err != nil { 692 t.Fatal(err) 693 } 694 if len(message.Content) != 2 { 695 t.Errorf("Expected 2 content blocks, got %d", len(message.Content)) 696 } 697 if message.Content[0].OfText.Text != "first block" { 698 t.Errorf("Expected first block text 'first block', got '%s'", message.Content[0].OfText.Text) 699 } 700 if message.Content[1].OfText.Text != "second block" { 701 t.Errorf("Expected second block text 'second block', got '%s'", message.Content[1].OfText.Text) 702 } 703 }