Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion betamessageutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -452,9 +452,19 @@ func (r BetaToolSearchToolResultBlock) ToParam() BetaToolSearchToolResultBlockPa
p.Type = r.Type
p.ToolUseID = r.ToolUseID
if r.Content.JSON.ErrorCode.Valid() {
p.Content.OfRequestToolSearchToolResultError = &BetaToolSearchToolResultErrorParam{
errParam := &BetaToolSearchToolResultErrorParam{
ErrorCode: BetaToolSearchToolResultErrorParamErrorCode(r.Content.ErrorCode),
}
// error_code is required by the API but is tagged `omitzero`, so an
// empty string (e.g. an enum value the SDK does not recognize yet)
// would otherwise be dropped during marshaling. Force the field to be
// emitted using the raw value received from the server.
if string(errParam.ErrorCode) == "" {
errParam.SetExtraFields(map[string]any{
"error_code": string(r.Content.ErrorCode),
})
}
p.Content.OfRequestToolSearchToolResultError = errParam
} else {
p.Content.OfRequestToolSearchToolSearchResultBlock = &BetaToolSearchToolSearchResultBlockParam{}
for _, block := range r.Content.ToolReferences {
Expand Down
12 changes: 11 additions & 1 deletion messageutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -390,9 +390,19 @@ func (r ToolSearchToolResultBlock) ToParam() ToolSearchToolResultBlockParam {
p.Type = r.Type
p.ToolUseID = r.ToolUseID
if r.Content.JSON.ErrorCode.Valid() {
p.Content.OfRequestToolSearchToolResultError = &ToolSearchToolResultErrorParam{
errParam := &ToolSearchToolResultErrorParam{
ErrorCode: ToolSearchToolResultErrorCode(r.Content.ErrorCode),
}
// error_code is required by the API but is tagged `omitzero`, so an
// empty string (e.g. an enum value the SDK does not recognize yet)
// would otherwise be dropped during marshaling. Force the field to be
// emitted using the raw value received from the server.
if string(errParam.ErrorCode) == "" {
errParam.SetExtraFields(map[string]any{
"error_code": string(r.Content.ErrorCode),
})
}
p.Content.OfRequestToolSearchToolResultError = errParam
} else {
p.Content.OfRequestToolSearchToolSearchResultBlock = &ToolSearchToolSearchResultBlockParam{}
for _, block := range r.Content.ToolReferences {
Expand Down
77 changes: 77 additions & 0 deletions messageutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,81 @@ func TestContentBlockUnionToParam(t *testing.T) {
t.Errorf("Expected 1 search result in param, got %d", len(result.OfWebSearchToolResult.Content.OfWebSearchToolResultBlockItem))
}
})

// Regression test for https://github.com/anthropics/anthropic-sdk-go/issues/317.
// The API requires `error_code` on a tool_search_tool_result_error block, but
// the param field is tagged `omitzero`, so a zero-value enum (e.g. an enum value
// the SDK does not yet recognize) would be elided and the API would 400 on the
// next request.
t.Run("ToolSearchToolResultBlock error preserves known error_code", func(t *testing.T) {
result := unmarshalContentBlockParam(t, `{"type":"tool_search_tool_result","tool_use_id":"tsu_1","content":{"type":"tool_search_tool_result_error","error_code":"unavailable"}}`)
if result.OfToolSearchToolResult == nil {
t.Fatal("Expected OfToolSearchToolResult to be non-nil")
}
if result.OfToolSearchToolResult.Content.OfRequestToolSearchToolResultError == nil {
t.Fatal("Expected OfRequestToolSearchToolResultError to be non-nil")
}

data, err := json.Marshal(result)
if err != nil {
t.Fatalf("Failed to marshal: %v", err)
}
var decoded struct {
Content struct {
ErrorCode *string `json:"error_code"`
Type string `json:"type"`
} `json:"content"`
}
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("Failed to unmarshal round-trip: %v", err)
}
if decoded.Content.Type != "tool_search_tool_result_error" {
t.Errorf("Expected content.type 'tool_search_tool_result_error', got %q", decoded.Content.Type)
}
if decoded.Content.ErrorCode == nil {
t.Fatalf("Expected content.error_code to be present, got missing field; raw: %s", string(data))
}
if *decoded.Content.ErrorCode != "unavailable" {
t.Errorf("Expected content.error_code 'unavailable', got %q", *decoded.Content.ErrorCode)
}
})

t.Run("ToolSearchToolResultBlock error preserves empty error_code", func(t *testing.T) {
// Regression: when the response's error_code resolves to the zero value
// of ToolSearchToolResultErrorCode (an empty string), the param field
// is tagged `omitzero` and the field gets silently dropped. The API
// then rejects the next request with
// "tool_search_tool_result.content.RequestToolSearchToolResultError.error_code: Field required".
// ToParam() must still emit the field with the original value.
result := unmarshalContentBlockParam(t, `{"type":"tool_search_tool_result","tool_use_id":"tsu_1","content":{"type":"tool_search_tool_result_error","error_code":""}}`)
if result.OfToolSearchToolResult == nil {
t.Fatal("Expected OfToolSearchToolResult to be non-nil")
}
if result.OfToolSearchToolResult.Content.OfRequestToolSearchToolResultError == nil {
t.Fatal("Expected OfRequestToolSearchToolResultError to be non-nil")
}

data, err := json.Marshal(result)
if err != nil {
t.Fatalf("Failed to marshal: %v", err)
}
var decoded struct {
Content struct {
ErrorCode *string `json:"error_code"`
Type string `json:"type"`
} `json:"content"`
}
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("Failed to unmarshal round-trip: %v", err)
}
if decoded.Content.Type != "tool_search_tool_result_error" {
t.Errorf("Expected content.type 'tool_search_tool_result_error', got %q", decoded.Content.Type)
}
if decoded.Content.ErrorCode == nil {
t.Fatalf("Expected content.error_code to be present even when empty, got missing field; raw: %s", string(data))
}
if *decoded.Content.ErrorCode != "" {
t.Errorf("Expected content.error_code '', got %q", *decoded.Content.ErrorCode)
}
})
}