@@ -670,6 +670,17 @@ func awaitAnthropicCall(t *testing.T, calls <-chan anthropicCall) anthropicCall
670670 }
671671}
672672
673+ func requireWebSearchResultMetadata (t * testing.T , metadata fantasy.ProviderMetadata ) * WebSearchResultMetadata {
674+ t .Helper ()
675+
676+ providerOption , ok := fantasy .ProviderOptions (metadata )[Name ]
677+ require .True (t , ok , "provider metadata should contain anthropic key" )
678+ result , ok := providerOption .(* WebSearchResultMetadata )
679+ require .True (t , ok , "provider metadata should be *WebSearchResultMetadata" )
680+ require .NotNil (t , result )
681+ return result
682+ }
683+
673684func assertNoAnthropicCall (t * testing.T , calls <- chan anthropicCall ) {
674685 t .Helper ()
675686
@@ -786,6 +797,103 @@ func mockAnthropicWebSearchResponse() map[string]any {
786797 }
787798}
788799
800+ func mockAnthropicWebSearchErrorResponse () map [string ]any {
801+ return map [string ]any {
802+ "id" : "msg_01WebSearchError" ,
803+ "type" : "message" ,
804+ "role" : "assistant" ,
805+ "model" : "claude-sonnet-4-20250514" ,
806+ "content" : []any {
807+ map [string ]any {
808+ "type" : "server_tool_use" ,
809+ "id" : "srvtoolu_err" ,
810+ "name" : "web_search" ,
811+ "input" : map [string ]any {"query" : "latest AI news" },
812+ "caller" : map [string ]any {"type" : "direct" },
813+ },
814+ map [string ]any {
815+ "type" : "web_search_tool_result" ,
816+ "tool_use_id" : "srvtoolu_err" ,
817+ "caller" : map [string ]any {"type" : "direct" },
818+ "content" : map [string ]any {
819+ "type" : "web_search_tool_result_error" ,
820+ "error_code" : "max_uses_exceeded" ,
821+ },
822+ },
823+ map [string ]any {
824+ "type" : "text" ,
825+ "text" : "I was unable to search." ,
826+ },
827+ },
828+ "stop_reason" : "end_turn" ,
829+ "stop_sequence" : nil ,
830+ "usage" : map [string ]any {
831+ "input_tokens" : 100 ,
832+ "output_tokens" : 20 ,
833+ "cache_creation_input_tokens" : 0 ,
834+ "cache_read_input_tokens" : 0 ,
835+ "server_tool_use" : map [string ]any {
836+ "web_search_requests" : 1 ,
837+ },
838+ },
839+ }
840+ }
841+
842+ func TestToPrompt_WebSearchProviderExecutedErrorRoundTrip (t * testing.T ) {
843+ t .Parallel ()
844+
845+ prompt := fantasy.Prompt {
846+ {
847+ Role : fantasy .MessageRoleUser ,
848+ Content : []fantasy.MessagePart {
849+ fantasy.TextPart {Text : "Search for the latest AI news" },
850+ },
851+ },
852+ {
853+ Role : fantasy .MessageRoleAssistant ,
854+ Content : []fantasy.MessagePart {
855+ fantasy.ToolCallPart {
856+ ToolCallID : "srvtoolu_err" ,
857+ ToolName : "web_search" ,
858+ Input : `{"query":"latest AI news"}` ,
859+ ProviderExecuted : true ,
860+ },
861+ fantasy.ToolResultPart {
862+ ToolCallID : "srvtoolu_err" ,
863+ ProviderExecuted : true ,
864+ ProviderOptions : fantasy.ProviderOptions {
865+ Name : & WebSearchResultMetadata {ErrorCode : "max_uses_exceeded" },
866+ },
867+ },
868+ fantasy.TextPart {Text : "I was unable to search." },
869+ },
870+ },
871+ }
872+
873+ _ , messages , warnings := toPrompt (prompt , true )
874+ require .Empty (t , warnings )
875+ require .Len (t , messages , 2 )
876+
877+ assistantMsg := messages [1 ]
878+ require .Len (t , assistantMsg .Content , 3 )
879+ require .NotNil (t , assistantMsg .Content [0 ].OfServerToolUse )
880+ require .Equal (t , "srvtoolu_err" , assistantMsg .Content [0 ].OfServerToolUse .ID )
881+
882+ webResult := assistantMsg .Content [1 ]
883+ require .NotNil (t , webResult .OfWebSearchToolResult )
884+ require .Equal (t , "srvtoolu_err" , webResult .OfWebSearchToolResult .ToolUseID )
885+ require .Nil (t , webResult .OfWebSearchToolResult .Content .OfWebSearchToolResultBlockItem )
886+ require .NotNil (t , webResult .OfWebSearchToolResult .Content .OfRequestWebSearchToolResultError )
887+ require .Equal (
888+ t ,
889+ anthropic .WebSearchToolResultErrorCodeMaxUsesExceeded ,
890+ webResult .OfWebSearchToolResult .Content .OfRequestWebSearchToolResultError .ErrorCode ,
891+ )
892+
893+ require .NotNil (t , assistantMsg .Content [2 ].OfText )
894+ require .Equal (t , "I was unable to search." , assistantMsg .Content [2 ].OfText .Text )
895+ }
896+
789897func TestToPrompt_WebSearchProviderExecutedToolResults (t * testing.T ) {
790898 t .Parallel ()
791899
@@ -965,6 +1073,66 @@ func TestGenerate_WebSearchResponse(t *testing.T) {
9651073 )
9661074}
9671075
1076+ func TestGenerate_WebSearchErrorPreservesErrorCode (t * testing.T ) {
1077+ t .Parallel ()
1078+
1079+ server , calls := newAnthropicJSONServer (mockAnthropicWebSearchErrorResponse ())
1080+ defer server .Close ()
1081+
1082+ provider , err := New (
1083+ WithAPIKey ("test-api-key" ),
1084+ WithBaseURL (server .URL ),
1085+ )
1086+ require .NoError (t , err )
1087+
1088+ model , err := provider .LanguageModel (context .Background (), "claude-sonnet-4-20250514" )
1089+ require .NoError (t , err )
1090+
1091+ resp , err := model .Generate (context .Background (), fantasy.Call {
1092+ Prompt : testPrompt (),
1093+ Tools : []fantasy.Tool {
1094+ WebSearchTool (nil ),
1095+ },
1096+ })
1097+ require .NoError (t , err )
1098+
1099+ call := awaitAnthropicCall (t , calls )
1100+ require .Equal (t , "POST" , call .method )
1101+ require .Equal (t , "/v1/messages" , call .path )
1102+
1103+ var (
1104+ toolCalls []fantasy.ToolCallContent
1105+ sources []fantasy.SourceContent
1106+ toolResults []fantasy.ToolResultContent
1107+ texts []fantasy.TextContent
1108+ )
1109+ for _ , c := range resp .Content {
1110+ switch v := c .(type ) {
1111+ case fantasy.ToolCallContent :
1112+ toolCalls = append (toolCalls , v )
1113+ case fantasy.SourceContent :
1114+ sources = append (sources , v )
1115+ case fantasy.ToolResultContent :
1116+ toolResults = append (toolResults , v )
1117+ case fantasy.TextContent :
1118+ texts = append (texts , v )
1119+ }
1120+ }
1121+
1122+ require .Len (t , toolCalls , 1 )
1123+ require .True (t , toolCalls [0 ].ProviderExecuted )
1124+ require .Equal (t , "srvtoolu_err" , toolCalls [0 ].ToolCallID )
1125+ require .Empty (t , sources )
1126+ require .Len (t , toolResults , 1 )
1127+ require .True (t , toolResults [0 ].ProviderExecuted )
1128+ require .Equal (t , "srvtoolu_err" , toolResults [0 ].ToolCallID )
1129+ webMeta := requireWebSearchResultMetadata (t , toolResults [0 ].ProviderMetadata )
1130+ require .Equal (t , "max_uses_exceeded" , webMeta .ErrorCode )
1131+ require .Empty (t , webMeta .Results )
1132+ require .Len (t , texts , 1 )
1133+ require .Equal (t , "I was unable to search." , texts [0 ].Text )
1134+ }
1135+
9681136func TestGenerate_WebSearchToolInRequest (t * testing.T ) {
9691137 t .Parallel ()
9701138
@@ -1436,6 +1604,75 @@ func TestStream_WebSearchResponse(t *testing.T) {
14361604 require .Equal (t , "Here are the results." , textDeltas [0 ].Delta )
14371605}
14381606
1607+ func TestStream_WebSearchErrorPreservesErrorCode (t * testing.T ) {
1608+ t .Parallel ()
1609+
1610+ chunks := []string {
1611+ "event: message_start\n " ,
1612+ `data: {"type":"message_start","message":{"id":"msg_01WebSearchError","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[],"stop_reason":null,"usage":{"input_tokens":100,"output_tokens":0}}}` + "\n \n " ,
1613+ "event: content_block_start\n " ,
1614+ `data: {"type":"content_block_start","index":0,"content_block":{"type":"server_tool_use","id":"srvtoolu_err","name":"web_search","input":{}}}` + "\n \n " ,
1615+ "event: content_block_stop\n " ,
1616+ `data: {"type":"content_block_stop","index":0}` + "\n \n " ,
1617+ "event: content_block_start\n " ,
1618+ `data: {"type":"content_block_start","index":1,"content_block":{"type":"web_search_tool_result","tool_use_id":"srvtoolu_err","content":{"type":"web_search_tool_result_error","error_code":"max_uses_exceeded"}}}` + "\n \n " ,
1619+ "event: content_block_stop\n " ,
1620+ `data: {"type":"content_block_stop","index":1}` + "\n \n " ,
1621+ "event: message_stop\n " ,
1622+ `data: {"type":"message_stop"}` + "\n \n " ,
1623+ }
1624+
1625+ server , calls := newAnthropicStreamingServer (chunks )
1626+ defer server .Close ()
1627+
1628+ provider , err := New (
1629+ WithAPIKey ("test-api-key" ),
1630+ WithBaseURL (server .URL ),
1631+ )
1632+ require .NoError (t , err )
1633+
1634+ model , err := provider .LanguageModel (context .Background (), "claude-sonnet-4-20250514" )
1635+ require .NoError (t , err )
1636+
1637+ stream , err := model .Stream (context .Background (), fantasy.Call {
1638+ Prompt : testPrompt (),
1639+ Tools : []fantasy.Tool {
1640+ WebSearchTool (nil ),
1641+ },
1642+ })
1643+ require .NoError (t , err )
1644+
1645+ var parts []fantasy.StreamPart
1646+ stream (func (part fantasy.StreamPart ) bool {
1647+ parts = append (parts , part )
1648+ return true
1649+ })
1650+
1651+ _ = awaitAnthropicCall (t , calls )
1652+
1653+ var (
1654+ toolResults []fantasy.StreamPart
1655+ sourceParts []fantasy.StreamPart
1656+ )
1657+ for _ , p := range parts {
1658+ switch p .Type {
1659+ case fantasy .StreamPartTypeToolResult :
1660+ toolResults = append (toolResults , p )
1661+ case fantasy .StreamPartTypeSource :
1662+ sourceParts = append (sourceParts , p )
1663+ }
1664+ }
1665+
1666+ require .Len (t , toolResults , 1 )
1667+ require .Empty (t , sourceParts )
1668+ require .True (t , toolResults [0 ].ProviderExecuted )
1669+ require .Equal (t , "srvtoolu_err" , toolResults [0 ].ID )
1670+ require .Equal (t , "web_search" , toolResults [0 ].ToolCallName )
1671+ webMeta := requireWebSearchResultMetadata (t , toolResults [0 ].ProviderMetadata )
1672+ require .Equal (t , "max_uses_exceeded" , webMeta .ErrorCode )
1673+ require .Empty (t , webMeta .Results )
1674+ }
1675+
14391676func TestGenerate_ToolChoiceNone (t * testing.T ) {
14401677 t .Parallel ()
14411678
0 commit comments