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  }