diff --git a/betamessageutil.go b/betamessageutil.go index d3b53f49..ec36a03b 100644 --- a/betamessageutil.go +++ b/betamessageutil.go @@ -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 { diff --git a/messageutil.go b/messageutil.go index e735e479..e16ac8e9 100644 --- a/messageutil.go +++ b/messageutil.go @@ -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 { diff --git a/messageutil_test.go b/messageutil_test.go index cf1aedcb..ce729931 100644 --- a/messageutil_test.go +++ b/messageutil_test.go @@ -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) + } + }) }