github.com/anthropics/anthropic-sdk-go@v1.70.1/betamessage_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  
    13  	"github.com/anthropics/anthropic-sdk-go"
    14  	"github.com/anthropics/anthropic-sdk-go/internal/testutil"
    15  	"github.com/anthropics/anthropic-sdk-go/option"
    16  	"github.com/anthropics/anthropic-sdk-go/shared/constant"
    17  )
    18  
    19  func TestBetaMessageNewWithOptionalParams(t *testing.T) {
    20  	baseURL := "http://localhost:4010"
    21  	if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
    22  		baseURL = envURL
    23  	}
    24  	if !testutil.CheckTestServer(t, baseURL) {
    25  		return
    26  	}
    27  	client := anthropic.NewClient(
    28  		option.WithBaseURL(baseURL),
    29  		option.WithAPIKey("my-anthropic-api-key"),
    30  	)
    31  	_, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{
    32  		MaxTokens: 1024,
    33  		Messages: []anthropic.BetaMessageParam{{
    34  			Content: []anthropic.BetaContentBlockParamUnion{{
    35  				OfText: &anthropic.BetaTextBlockParam{
    36  					Text: "x",
    37  					CacheControl: anthropic.BetaCacheControlEphemeralParam{
    38  						TTL: anthropic.BetaCacheControlEphemeralTTLTTL5m,
    39  					},
    40  					Citations: []anthropic.BetaTextCitationParamUnion{{
    41  						OfCharLocation: &anthropic.BetaCitationCharLocationParam{
    42  							CitedText:      "The grass is green. The sky is blue.",
    43  							DocumentIndex:  0,
    44  							DocumentTitle:  anthropic.String("x"),
    45  							EndCharIndex:   0,
    46  							StartCharIndex: 0,
    47  						},
    48  					}},
    49  				},
    50  			}},
    51  			Role:    anthropic.BetaMessageParamRoleUser,
    52  			ClearAt: anthropic.BetaMessageParamClearAtNextUserMessage,
    53  			OutputConfig: anthropic.BetaSystemMessageOutputConfigParam{
    54  				Effort: anthropic.BetaSystemMessageOutputConfigEffortLow,
    55  			},
    56  		}},
    57  		Model: anthropic.ModelClaudeOpus5,
    58  		CacheControl: anthropic.BetaCacheControlEphemeralParam{
    59  			TTL: anthropic.BetaCacheControlEphemeralTTLTTL5m,
    60  		},
    61  		Container: anthropic.BetaMessageNewParamsContainerUnion{
    62  			OfContainers: &anthropic.BetaContainerParams{
    63  				ID: anthropic.String("id"),
    64  				Skills: []anthropic.BetaSkillParams{{
    65  					SkillID: "pdf",
    66  					Type:    anthropic.BetaSkillParamsTypeAnthropic,
    67  					Version: anthropic.String("latest"),
    68  				}},
    69  			},
    70  		},
    71  		ContextManagement: anthropic.BetaContextManagementConfigParam{
    72  			Edits: []anthropic.BetaContextManagementConfigEditUnionParam{{
    73  				OfClearToolUses20250919: &anthropic.BetaClearToolUses20250919EditParam{
    74  					ClearAtLeast: anthropic.BetaInputTokensClearAtLeastParam{
    75  						Value: 0,
    76  					},
    77  					ClearToolInputs: anthropic.BetaClearToolUses20250919EditClearToolInputsUnionParam{
    78  						OfBool: anthropic.Bool(true),
    79  					},
    80  					ExcludeTools: []string{"string"},
    81  					Keep: anthropic.BetaToolUsesKeepParam{
    82  						Value: 0,
    83  					},
    84  					Trigger: anthropic.BetaClearToolUses20250919EditTriggerUnionParam{
    85  						OfInputTokens: &anthropic.BetaInputTokensTriggerParam{
    86  							Value: 1,
    87  						},
    88  					},
    89  				},
    90  			}},
    91  		},
    92  		Diagnostics: anthropic.BetaDiagnosticsParam{
    93  			PreviousMessageID: anthropic.String("previous_message_id"),
    94  		},
    95  		FallbackCreditToken: anthropic.BetaMessageNewParamsFallbackCreditTokenUnion{
    96  			OfString: anthropic.String("x"),
    97  		},
    98  		Fallbacks: anthropic.BetaFallbacksParamUnion{
    99  			OfDefault: constant.ValueOf[constant.Default](),
   100  		},
   101  		InferenceGeo: anthropic.String("inference_geo"),
   102  		MCPServers: []anthropic.BetaRequestMCPServerURLDefinitionParam{{
   103  			Name:               "name",
   104  			URL:                "url",
   105  			AuthorizationToken: anthropic.String("authorization_token"),
   106  			ToolConfiguration: anthropic.BetaRequestMCPServerToolConfigurationParam{
   107  				AllowedTools: []string{"string"},
   108  				Enabled:      anthropic.Bool(true),
   109  			},
   110  		}},
   111  		Metadata: anthropic.BetaMetadataParam{
   112  			UserID: anthropic.String("13803d75-b4b5-4c3e-b2a2-6f21399b021b"),
   113  		},
   114  		OutputConfig: anthropic.BetaOutputConfigParam{
   115  			Effort: anthropic.BetaOutputConfigEffortLow,
   116  			Format: anthropic.BetaJSONOutputFormatParam{
   117  				Schema: map[string]any{
   118  					"foo": "bar",
   119  				},
   120  			},
   121  			TaskBudget: anthropic.BetaTokenTaskBudgetParam{
   122  				Total:     1024,
   123  				Remaining: anthropic.Int(0),
   124  			},
   125  		},
   126  		OutputFormat: anthropic.BetaJSONOutputFormatParam{
   127  			Schema: map[string]any{
   128  				"foo": "bar",
   129  			},
   130  		},
   131  		ServiceTier:   anthropic.BetaMessageNewParamsServiceTierAuto,
   132  		Speed:         anthropic.BetaMessageNewParamsSpeedStandard,
   133  		StopSequences: []string{"string"},
   134  		System: []anthropic.BetaTextBlockParam{{
   135  			Text: "Today's date is 2024-06-01.",
   136  			CacheControl: anthropic.BetaCacheControlEphemeralParam{
   137  				TTL: anthropic.BetaCacheControlEphemeralTTLTTL5m,
   138  			},
   139  			Citations: []anthropic.BetaTextCitationParamUnion{{
   140  				OfCharLocation: &anthropic.BetaCitationCharLocationParam{
   141  					CitedText:      "The grass is green. The sky is blue.",
   142  					DocumentIndex:  0,
   143  					DocumentTitle:  anthropic.String("x"),
   144  					EndCharIndex:   0,
   145  					StartCharIndex: 0,
   146  				},
   147  			}},
   148  		}},
   149  		Temperature: anthropic.Float(1),
   150  		Thinking: anthropic.BetaThinkingConfigParamUnion{
   151  			OfAdaptive: &anthropic.BetaThinkingConfigAdaptiveParam{
   152  				BlockBinding: anthropic.BetaThinkingBlockBindingParam{
   153  					PrefixMismatchBehavior: anthropic.BetaThinkingPrefixMismatchBehaviorError,
   154  				},
   155  				Display: anthropic.BetaThinkingConfigAdaptiveDisplaySummarized,
   156  			},
   157  		},
   158  		ToolChoice: anthropic.BetaToolChoiceUnionParam{
   159  			OfAuto: &anthropic.BetaToolChoiceAutoParam{
   160  				DisableParallelToolUse: anthropic.Bool(true),
   161  			},
   162  		},
   163  		Tools: []anthropic.BetaToolUnionParam{{
   164  			OfTool: &anthropic.BetaToolParam{
   165  				InputSchema: anthropic.BetaToolInputSchemaParam{
   166  					Properties: map[string]any{
   167  						"location": "bar",
   168  						"unit":     "bar",
   169  					},
   170  					Required: []string{"location"},
   171  				},
   172  				Name:           "name",
   173  				AllowedCallers: []string{"direct"},
   174  				CacheControl: anthropic.BetaCacheControlEphemeralParam{
   175  					TTL: anthropic.BetaCacheControlEphemeralTTLTTL5m,
   176  				},
   177  				DeferLoading:        anthropic.Bool(true),
   178  				Description:         anthropic.String("Get the current weather in a given location"),
   179  				EagerInputStreaming: anthropic.Bool(true),
   180  				InputExamples: []map[string]any{{
   181  					"foo": "bar",
   182  				}},
   183  				Strict: anthropic.Bool(true),
   184  				Type:   anthropic.BetaToolTypeCustom,
   185  			},
   186  		}},
   187  		TopK:          anthropic.Int(5),
   188  		TopP:          anthropic.Float(0.7),
   189  		Betas:         []anthropic.AnthropicBeta{anthropic.AnthropicBetaMessageBatches2024_09_24},
   190  		UserProfileID: anthropic.String("anthropic-user-profile-id"),
   191  		WorkspaceID:   anthropic.String("wrkspc_011CZkZaBF1tNoB5wlCeusgy"),
   192  	})
   193  	if err != nil {
   194  		var apierr *anthropic.Error
   195  		if errors.As(err, &apierr) {
   196  			t.Log(string(apierr.DumpRequest(true)))
   197  		}
   198  		t.Fatalf("err should be nil: %s", err.Error())
   199  	}
   200  }
   201  
   202  func TestBetaMessageCountTokensWithOptionalParams(t *testing.T) {
   203  	baseURL := "http://localhost:4010"
   204  	if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
   205  		baseURL = envURL
   206  	}
   207  	if !testutil.CheckTestServer(t, baseURL) {
   208  		return
   209  	}
   210  	client := anthropic.NewClient(
   211  		option.WithBaseURL(baseURL),
   212  		option.WithAPIKey("my-anthropic-api-key"),
   213  	)
   214  	_, err := client.Beta.Messages.CountTokens(context.TODO(), anthropic.BetaMessageCountTokensParams{
   215  		Messages: []anthropic.BetaMessageParam{{
   216  			Content: []anthropic.BetaContentBlockParamUnion{{
   217  				OfText: &anthropic.BetaTextBlockParam{
   218  					Text: "x",
   219  					CacheControl: anthropic.BetaCacheControlEphemeralParam{
   220  						TTL: anthropic.BetaCacheControlEphemeralTTLTTL5m,
   221  					},
   222  					Citations: []anthropic.BetaTextCitationParamUnion{{
   223  						OfCharLocation: &anthropic.BetaCitationCharLocationParam{
   224  							CitedText:      "The grass is green. The sky is blue.",
   225  							DocumentIndex:  0,
   226  							DocumentTitle:  anthropic.String("x"),
   227  							EndCharIndex:   0,
   228  							StartCharIndex: 0,
   229  						},
   230  					}},
   231  				},
   232  			}},
   233  			Role:    anthropic.BetaMessageParamRoleUser,
   234  			ClearAt: anthropic.BetaMessageParamClearAtNextUserMessage,
   235  			OutputConfig: anthropic.BetaSystemMessageOutputConfigParam{
   236  				Effort: anthropic.BetaSystemMessageOutputConfigEffortLow,
   237  			},
   238  		}},
   239  		Model: anthropic.ModelClaudeOpus5,
   240  		CacheControl: anthropic.BetaCacheControlEphemeralParam{
   241  			TTL: anthropic.BetaCacheControlEphemeralTTLTTL5m,
   242  		},
   243  		ContextManagement: anthropic.BetaContextManagementConfigParam{
   244  			Edits: []anthropic.BetaContextManagementConfigEditUnionParam{{
   245  				OfClearToolUses20250919: &anthropic.BetaClearToolUses20250919EditParam{
   246  					ClearAtLeast: anthropic.BetaInputTokensClearAtLeastParam{
   247  						Value: 0,
   248  					},
   249  					ClearToolInputs: anthropic.BetaClearToolUses20250919EditClearToolInputsUnionParam{
   250  						OfBool: anthropic.Bool(true),
   251  					},
   252  					ExcludeTools: []string{"string"},
   253  					Keep: anthropic.BetaToolUsesKeepParam{
   254  						Value: 0,
   255  					},
   256  					Trigger: anthropic.BetaClearToolUses20250919EditTriggerUnionParam{
   257  						OfInputTokens: &anthropic.BetaInputTokensTriggerParam{
   258  							Value: 1,
   259  						},
   260  					},
   261  				},
   262  			}},
   263  		},
   264  		MCPServers: []anthropic.BetaRequestMCPServerURLDefinitionParam{{
   265  			Name:               "name",
   266  			URL:                "url",
   267  			AuthorizationToken: anthropic.String("authorization_token"),
   268  			ToolConfiguration: anthropic.BetaRequestMCPServerToolConfigurationParam{
   269  				AllowedTools: []string{"string"},
   270  				Enabled:      anthropic.Bool(true),
   271  			},
   272  		}},
   273  		OutputConfig: anthropic.BetaOutputConfigParam{
   274  			Effort: anthropic.BetaOutputConfigEffortLow,
   275  			Format: anthropic.BetaJSONOutputFormatParam{
   276  				Schema: map[string]any{
   277  					"foo": "bar",
   278  				},
   279  			},
   280  			TaskBudget: anthropic.BetaTokenTaskBudgetParam{
   281  				Total:     1024,
   282  				Remaining: anthropic.Int(0),
   283  			},
   284  		},
   285  		OutputFormat: anthropic.BetaJSONOutputFormatParam{
   286  			Schema: map[string]any{
   287  				"foo": "bar",
   288  			},
   289  		},
   290  		Speed: anthropic.BetaMessageCountTokensParamsSpeedStandard,
   291  		System: anthropic.BetaMessageCountTokensParamsSystemUnion{
   292  			OfBetaTextBlockArray: []anthropic.BetaTextBlockParam{{
   293  				Text: "Today's date is 2024-06-01.",
   294  				CacheControl: anthropic.BetaCacheControlEphemeralParam{
   295  					TTL: anthropic.BetaCacheControlEphemeralTTLTTL5m,
   296  				},
   297  				Citations: []anthropic.BetaTextCitationParamUnion{{
   298  					OfCharLocation: &anthropic.BetaCitationCharLocationParam{
   299  						CitedText:      "The grass is green. The sky is blue.",
   300  						DocumentIndex:  0,
   301  						DocumentTitle:  anthropic.String("x"),
   302  						EndCharIndex:   0,
   303  						StartCharIndex: 0,
   304  					},
   305  				}},
   306  			}},
   307  		},
   308  		Thinking: anthropic.BetaThinkingConfigParamUnion{
   309  			OfAdaptive: &anthropic.BetaThinkingConfigAdaptiveParam{
   310  				BlockBinding: anthropic.BetaThinkingBlockBindingParam{
   311  					PrefixMismatchBehavior: anthropic.BetaThinkingPrefixMismatchBehaviorError,
   312  				},
   313  				Display: anthropic.BetaThinkingConfigAdaptiveDisplaySummarized,
   314  			},
   315  		},
   316  		ToolChoice: anthropic.BetaToolChoiceUnionParam{
   317  			OfAuto: &anthropic.BetaToolChoiceAutoParam{
   318  				DisableParallelToolUse: anthropic.Bool(true),
   319  			},
   320  		},
   321  		Tools: []anthropic.BetaMessageCountTokensParamsToolUnion{{
   322  			OfTool: &anthropic.BetaToolParam{
   323  				InputSchema: anthropic.BetaToolInputSchemaParam{
   324  					Properties: map[string]any{
   325  						"location": "bar",
   326  						"unit":     "bar",
   327  					},
   328  					Required: []string{"location"},
   329  				},
   330  				Name:           "name",
   331  				AllowedCallers: []string{"direct"},
   332  				CacheControl: anthropic.BetaCacheControlEphemeralParam{
   333  					TTL: anthropic.BetaCacheControlEphemeralTTLTTL5m,
   334  				},
   335  				DeferLoading:        anthropic.Bool(true),
   336  				Description:         anthropic.String("Get the current weather in a given location"),
   337  				EagerInputStreaming: anthropic.Bool(true),
   338  				InputExamples: []map[string]any{{
   339  					"foo": "bar",
   340  				}},
   341  				Strict: anthropic.Bool(true),
   342  				Type:   anthropic.BetaToolTypeCustom,
   343  			},
   344  		}},
   345  		Betas:         []anthropic.AnthropicBeta{anthropic.AnthropicBetaMessageBatches2024_09_24},
   346  		UserProfileID: anthropic.String("anthropic-user-profile-id"),
   347  		WorkspaceID:   anthropic.String("wrkspc_011CZkZaBF1tNoB5wlCeusgy"),
   348  	})
   349  	if err != nil {
   350  		var apierr *anthropic.Error
   351  		if errors.As(err, &apierr) {
   352  			t.Log(string(apierr.DumpRequest(true)))
   353  		}
   354  		t.Fatalf("err should be nil: %s", err.Error())
   355  	}
   356  }
   357  
   358  func TestBetaAccumulate(t *testing.T) {
   359  	for name, testCase := range map[string]struct {
   360  		expected anthropic.BetaMessage
   361  		events   []string
   362  	}{
   363  		"empty message": {
   364  			expected: anthropic.BetaMessage{Usage: anthropic.BetaUsage{}},
   365  			events: []string{
   366  				`{"type": "message_start", "message": {}}`,
   367  				`{"type: "message_stop"}`,
   368  			},
   369  		},
   370  		"text content block": {
   371  			events: []string{
   372  				`{"type": "message_start", "message": {}}`,
   373  				`{"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": "This "}}`,
   374  				`{"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "is a "}}`,
   375  				`{"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta": "text": "text block!"}}`,
   376  				`{"type": "content_block_stop", "index": 0}`,
   377  				`{"type": "message_stop"}`,
   378  			},
   379  			expected: anthropic.BetaMessage{Content: []anthropic.BetaContentBlockUnion{
   380  				{Type: "text", Text: "This is a text block!"},
   381  			}},
   382  		},
   383  		"text content block with citations": {
   384  			events: []string{
   385  				`{"type": "message_start", "message": {}}`,
   386  				`{"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": "1 + 1"}}`,
   387  				`{"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": " = 2"}}`,
   388  				`{"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 }}}`,
   389  				`{"type": "content_block_stop", "index": 0}`,
   390  				`{"type": "message_stop"}`,
   391  			},
   392  			expected: anthropic.BetaMessage{Content: []anthropic.BetaContentBlockUnion{
   393  				{Type: "text", Text: "1 + 1 = 2", Citations: []anthropic.BetaTextCitationUnion{{
   394  					Type:           "char_location",
   395  					CitedText:      "1 + 1 = 2",
   396  					DocumentIndex:  0,
   397  					DocumentTitle:  "Math Facts",
   398  					StartCharIndex: 300,
   399  					EndCharIndex:   310,
   400  				}}},
   401  			}},
   402  		},
   403  		"tool use block": {
   404  			events: []string{
   405  				`{"type": "message_start", "message": {}}`,
   406  				`{"type": "content_block_start", "index": 0, "content_block": {"type": "tool_use", "id": "toolu_id", "name": "tool_name", "input": {}}}`,
   407  				`{"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "{\"argument\":"}}`,
   408  				`{"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": " \"value\"}"}}`,
   409  				`{"type": "content_block_stop", "index": 0}`,
   410  				`{"type": "message_stop"}`,
   411  			},
   412  			expected: anthropic.BetaMessage{Content: []anthropic.BetaContentBlockUnion{
   413  				{Type: "tool_use", ID: "toolu_id", Name: "tool_name", Input: []byte(`{"argument": "value"}`)},
   414  			}},
   415  		},
   416  		"tool use block with no params": {
   417  			events: []string{
   418  				`{"type": "message_start", "message": {}}`,
   419  				`{"type": "content_block_start": "index": 0, "content_block": {"type": "tool_use", "id": "toolu_id", "name": "tool_name", input: {}}}`,
   420  				`{"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": ""}}`,
   421  				`{"type": "content_block_stop", "index": 0}`,
   422  				`{"type": "message_stop"}`,
   423  			},
   424  			expected: anthropic.BetaMessage{Content: []anthropic.BetaContentBlockUnion{
   425  				{Type: "tool_use", ID: "toolu_id", Name: "tool_name"},
   426  			}},
   427  		},
   428  		"server tool use block": {
   429  			events: []string{
   430  				`{"type": "message_start", "message": {}}`,
   431  				`{"type": "content_block_start": "index": 0, "content_block": {"type": "server_tool_use", "id": "srvtoolu_id", "name": "web_search", input: {}}}`,
   432  				`{"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": ""}}`,
   433  				`{"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "{\"query\": \"weat"}}`,
   434  				`{"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "her\"}"}}`,
   435  				`{"type": "content_block_stop", "index": 0}`,
   436  				`{"type": "message_stop"}`,
   437  			},
   438  			expected: anthropic.BetaMessage{Content: []anthropic.BetaContentBlockUnion{
   439  				{Type: "server_tool_use", ID: "srvtoolu_id", Name: "web_search", Input: []byte(`{"query": "weather"}`)},
   440  			}},
   441  		},
   442  		"thinking block": {
   443  			events: []string{
   444  				`{"type": "message_start", "message": {}}`,
   445  				`{"type": "content_block_start", "index": 0, "content_block": {"type": "thinking", "thinking": "Let me think..."}}`,
   446  				`{"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "
   447  First, let's try this..."}}`,
   448  				`{"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "
   449  Therefore, the answer is..."}}`,
   450  				`{"type": "content_block_delta", "index": 0, "delta": {"type": "signature_delta", "signature": "ThinkingSignature"}}`,
   451  				`{"type": "content_block_stop", "index": 0}`,
   452  				`{"type": "message_stop"}`,
   453  			},
   454  			expected: anthropic.BetaMessage{Content: []anthropic.BetaContentBlockUnion{
   455  				{Type: "thinking", Thinking: "Let me think...\nFirst, let's try this...\nTherefore, the answer is...", Signature: "ThinkingSignature"},
   456  			}},
   457  		},
   458  		"redacted thinking block": {
   459  			events: []string{
   460  				`{"type": "message_start", "message": {}}`,
   461  				`{"type": "content_block_start", "index": 0, "content_block": {"type": "redacted_thinking", "data": "Redacted"}}`,
   462  				`{"type": "content_block_stop", "index": 0}`,
   463  				`{"type": "message_stop"}`,
   464  			},
   465  			expected: anthropic.BetaMessage{Content: []anthropic.BetaContentBlockUnion{
   466  				{Type: "redacted_thinking", Data: "Redacted"},
   467  			}},
   468  		},
   469  		"compaction block": {
   470  			events: []string{
   471  				`{"type": "message_start", "message": {}}`,
   472  				`{"type": "content_block_start", "index": 0, "content_block": {"type": "compaction", "content": ""}}`,
   473  				`{"type": "content_block_delta", "index": 0, "delta": {"type": "compaction_delta", "content": "Summary of the conversation so far."}}`,
   474  				`{"type": "content_block_stop", "index": 0}`,
   475  				`{"type": "message_stop"}`,
   476  			},
   477  			expected: anthropic.BetaMessage{Content: []anthropic.BetaContentBlockUnion{
   478  				{Type: "compaction", Content: anthropic.BetaContentBlockUnionContent{OfString: "Summary of the conversation so far."}},
   479  			}},
   480  		},
   481  		"refusal with stop_details and usage": {
   482  			events: []string{
   483  				`{"type": "message_start", "message": {}}`,
   484  				`{"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": "I cannot help"}}`,
   485  				`{"type": "content_block_stop", "index": 0}`,
   486  				`{"type": "message_delta", "delta": {"stop_reason": "refusal", "stop_details": {"type": "refusal", "category": "cyber", "explanation": "Declined by a streaming policy classifier."}}, "usage": {"input_tokens": 15, "output_tokens": 8, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "server_tool_use": {"web_search_requests": 2}}}`,
   487  				`{"type": "message_stop"}`,
   488  			},
   489  			expected: anthropic.BetaMessage{
   490  				Content: []anthropic.BetaContentBlockUnion{
   491  					{Type: "text", Text: "I cannot help"},
   492  				},
   493  				StopReason: "refusal",
   494  				StopDetails: anthropic.BetaRefusalStopDetails{
   495  					Type:        "refusal",
   496  					Category:    "cyber",
   497  					Explanation: "Declined by a streaming policy classifier.",
   498  				},
   499  				Usage: anthropic.BetaUsage{
   500  					InputTokens:   15,
   501  					OutputTokens:  8,
   502  					ServerToolUse: anthropic.BetaServerToolUsage{WebSearchRequests: 2},
   503  				},
   504  			},
   505  		},
   506  		"message_delta usage with fallback_credit": {
   507  			events: []string{
   508  				`{"type": "message_start", "message": {}}`,
   509  				`{"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": "Hi"}}`,
   510  				`{"type": "content_block_stop", "index": 0}`,
   511  				`{"type": "message_delta", "delta": {"stop_reason": "end_turn"}, "usage": {"input_tokens": 15, "output_tokens": 8, "fallback_credit": {"status": {"type": "not_applied", "reason": "expired", "remove_to_redeem": ["fallback_credit_token"]}}}}`,
   512  				`{"type": "message_stop"}`,
   513  			},
   514  			expected: anthropic.BetaMessage{
   515  				Content: []anthropic.BetaContentBlockUnion{
   516  					{Type: "text", Text: "Hi"},
   517  				},
   518  				StopReason: "end_turn",
   519  				Usage: anthropic.BetaUsage{
   520  					InputTokens:  15,
   521  					OutputTokens: 8,
   522  					FallbackCredit: anthropic.BetaFallbackCreditUsage{
   523  						Status: anthropic.BetaFallbackCreditUsageStatusUnion{
   524  							Type:           "not_applied",
   525  							Reason:         anthropic.BetaFallbackCreditNotAppliedReasonExpired,
   526  							RemoveToRedeem: []string{"fallback_credit_token"},
   527  						},
   528  					},
   529  				},
   530  			},
   531  		},
   532  		"multiple content blocks": {
   533  			events: []string{
   534  				`{"type": "message_start", "message": {}}`,
   535  				`{"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": "Let me look up "}}`,
   536  				`{"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "the weather for "}}`,
   537  				`{"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta": "text": "you."}}`,
   538  				`{"type": "content_block_stop", "index": 0}`,
   539  				`{"type": "content_block_start", "index": 1, "content_block": {"type": "thinking", "thinking": ""}}`,
   540  				`{"type": "content_block_delta", "index": 1, "delta": {"type": "thinking_delta", "thinking": "I can look this "}}`,
   541  				`{"type": "content_block_delta", "index": 1, "delta": {"type": "thinking_delta", "thinking": "up using a tool."}}`,
   542  				`{"type": "content_block_stop", "index": 1}`,
   543  				`{"type": "content_block_start", "index": 2, "content_block": {"type": "tool_use", "id": "toolu_id", "name": "get_weather", "input": {}}}`,
   544  				`{"type": "content_block_delta", "index": 2, "delta": {"type": "input_json_delta", "partial_json": "{\"city\": "}}`,
   545  				`{"type": "content_block_delta", "index": 2, "delta": {"type": "input_json_delta", "partial_json": "\"Los Angeles\"}"}}`,
   546  				`{"type": "content_block_stop", "index": 2}`,
   547  				`{"type": "content_block_start", "index": 3, "content_block": {"type": "text", "text": ""}}`,
   548  				`{"type": "content_block_delta", "index": 3, "delta": {"type": "text_delta", "text": "The weather in Los Angeles"}}`,
   549  				`{"type": "content_block_delta", "index": 3, "delta": {"type": "text_delta", "text": " is 85 degrees Fahrenheit!"}}`,
   550  				`{"type": "content_block_stop", "index": 3"}`,
   551  				`{"type": "message_stop"}`,
   552  			},
   553  			expected: anthropic.BetaMessage{Content: []anthropic.BetaContentBlockUnion{
   554  				{Type: "text", Text: "Let me look up the weather for you."},
   555  				{Type: "thinking", Thinking: "I can look this up using a tool."},
   556  				{Type: "tool_use", ID: "toolu_id", Name: "get_weather", Input: []byte(`{"city": "Los Angeles"}`)},
   557  				{Type: "text", Text: "The weather in Los Angeles is 85 degrees Fahrenheit!"},
   558  			}},
   559  		},
   560  		"fallback block relabels accumulated model": {
   561  			events: []string{
   562  				`{"type": "message_start", "message": {"model": "model-a"}}`,
   563  				`{"type": "content_block_start", "index": 0, "content_block": {"type": "fallback", "from": {"model": "model-a"}, "to": {"model": "model-b"}}}`,
   564  				`{"type": "content_block_stop", "index": 0}`,
   565  				`{"type": "message_delta", "delta": {"stop_reason": "end_turn"}, "usage": {"output_tokens": 5}}`,
   566  				`{"type": "message_stop"}`,
   567  			},
   568  			expected: anthropic.BetaMessage{
   569  				Model: "model-b",
   570  				Content: []anthropic.BetaContentBlockUnion{
   571  					{
   572  						Type: "fallback",
   573  						From: anthropic.BetaFallbackInfo{Model: "model-a"},
   574  						To:   anthropic.BetaFallbackInfo{Model: "model-b"},
   575  					},
   576  				},
   577  				StopReason: "end_turn",
   578  				Usage:      anthropic.BetaUsage{OutputTokens: 5},
   579  			},
   580  		},
   581  		"interleaved content blocks": {
   582  			events: []string{
   583  				`{"type": "message_start", "message": {}}`,
   584  				`{"type": "content_block_start", "index": 0, "content_block": {"type": "thinking", "thinking": ""}}`,
   585  				`{"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "Let me think."}}`,
   586  				`{"type": "content_block_delta", "index": 0, "delta": {"type": "signature_delta", "signature": "sig123"}}`,
   587  				`{"type": "content_block_stop", "index": 0}`,
   588  				`{"type": "content_block_start", "index": 1, "content_block": {"type": "text", "text": ""}}`,
   589  				`{"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": "Hello"}}`,
   590  				`{"type": "content_block_start", "index": 2, "content_block": {"type": "tool_use", "id": "toolu_id", "name": "get_weather", "input": {}}}`,
   591  				`{"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": " world"}}`,
   592  				`{"type": "content_block_delta", "index": 2, "delta": {"type": "input_json_delta", "partial_json": "{\"city\": "}}`,
   593  				`{"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": "!"}}`,
   594  				`{"type": "content_block_delta", "index": 2, "delta": {"type": "input_json_delta", "partial_json": "\"Los Angeles\"}"}}`,
   595  				`{"type": "content_block_stop", "index": 1}`,
   596  				`{"type": "content_block_stop", "index": 2}`,
   597  				`{"type": "message_stop"}`,
   598  			},
   599  			expected: anthropic.BetaMessage{Content: []anthropic.BetaContentBlockUnion{
   600  				{Type: "thinking", Thinking: "Let me think.", Signature: "sig123"},
   601  				{Type: "text", Text: "Hello world!"},
   602  				{Type: "tool_use", ID: "toolu_id", Name: "get_weather", Input: []byte(`{"city": "Los Angeles"}`)},
   603  			}},
   604  		},
   605  	} {
   606  		t.Run(name, func(t *testing.T) {
   607  			message := anthropic.BetaMessage{}
   608  			for _, eventStr := range testCase.events {
   609  				event := anthropic.BetaRawMessageStreamEventUnion{}
   610  				err := (&event).UnmarshalJSON([]byte(eventStr))
   611  				if err != nil {
   612  					t.Fatal(err)
   613  				}
   614  				(&message).Accumulate(event)
   615  			}
   616  			marshaledMessage, err := json.Marshal(message)
   617  			if err != nil {
   618  				t.Fatal(err)
   619  			}
   620  			marshaledExpectedMessage, err := json.Marshal(testCase.expected)
   621  			if err != nil {
   622  				t.Fatal(err)
   623  			}
   624  			if string(marshaledMessage) != string(marshaledExpectedMessage) {
   625  				t.Fatalf("Mismatched message: expected %s but got %s", marshaledExpectedMessage, marshaledMessage)
   626  			}
   627  		})
   628  	}
   629  }
   630  
   631  func TestBetaAccumulateContentBlockIndexErrors(t *testing.T) {
   632  	for name, testCase := range map[string]struct {
   633  		events  []string
   634  		wantErr string
   635  	}{
   636  		"start with an index gap": {
   637  			events: []string{
   638  				`{"type": "message_start", "message": {}}`,
   639  				`{"type": "content_block_start", "index": 1, "content_block": {"type": "text", "text": ""}}`,
   640  			},
   641  			wantErr: "expected index 0",
   642  		},
   643  		"delta for a block that never started": {
   644  			events: []string{
   645  				`{"type": "message_start", "message": {}}`,
   646  				`{"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}`,
   647  				`{"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": "hi"}}`,
   648  			},
   649  			wantErr: "only 1 content blocks",
   650  		},
   651  		"delta with a negative index": {
   652  			events: []string{
   653  				`{"type": "message_start", "message": {}}`,
   654  				`{"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}`,
   655  				`{"type": "content_block_delta", "index": -1, "delta": {"type": "text_delta", "text": "hi"}}`,
   656  			},
   657  			wantErr: "index -1",
   658  		},
   659  		"stop for a block that never started": {
   660  			events: []string{
   661  				`{"type": "message_start", "message": {}}`,
   662  				`{"type": "content_block_stop", "index": 0}`,
   663  			},
   664  			wantErr: "only 0 content blocks",
   665  		},
   666  	} {
   667  		t.Run(name, func(t *testing.T) {
   668  			message := anthropic.BetaMessage{}
   669  			for i, eventStr := range testCase.events {
   670  				event := anthropic.BetaRawMessageStreamEventUnion{}
   671  				if err := (&event).UnmarshalJSON([]byte(eventStr)); err != nil {
   672  					t.Fatal(err)
   673  				}
   674  				err := (&message).Accumulate(event)
   675  				if i < len(testCase.events)-1 {
   676  					if err != nil {
   677  						t.Fatal(err)
   678  					}
   679  					continue
   680  				}
   681  				if err == nil {
   682  					t.Fatalf("expected error containing %q, got nil", testCase.wantErr)
   683  				}
   684  				if !strings.Contains(err.Error(), testCase.wantErr) {
   685  					t.Fatalf("expected error containing %q, got %q", testCase.wantErr, err.Error())
   686  				}
   687  			}
   688  		})
   689  	}
   690  }