fix(anthropic): ToolChoiceNone should send tool_choice:{type:"none"} to API (#178)

Andrey Nering created

The Anthropic provider's toTools function was returning nil for
anthropicToolChoice when ToolChoiceNone was set, which meant
tool_choice was never included in the API request. The API defaults
to "auto", so the model could still make tool calls.

Now properly constructs ToolChoiceUnionParam with OfNone set using
anthropic.NewToolChoiceNoneParam().

Also adds test coverage for ToolChoiceNone.

* Closes #177

💘 Generated with Crush

Assisted-by: Kimi K2.5 via Crush <crush@charm.land>

Change summary

providers/anthropic/anthropic.go      |  5 +++
providers/anthropic/anthropic_test.go | 31 +++++++++++++++++++++++++++++
2 files changed, 35 insertions(+), 1 deletion(-)

Detailed changes

providers/anthropic/anthropic.go 🔗

@@ -638,7 +638,10 @@ func (a languageModel) toTools(tools []fantasy.Tool, toolChoice *fantasy.ToolCho
 			},
 		}
 	case fantasy.ToolChoiceNone:
-		return anthropicTools, anthropicToolChoice, warnings
+		none := anthropic.NewToolChoiceNoneParam()
+		anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
+			OfNone: &none,
+		}
 	default:
 		anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
 			OfTool: &anthropic.ToolChoiceToolParam{

providers/anthropic/anthropic_test.go 🔗

@@ -1333,3 +1333,34 @@ func TestStream_WebSearchResponse(t *testing.T) {
 	require.NotEmpty(t, textDeltas, "should have text deltas")
 	require.Equal(t, "Here are the results.", textDeltas[0].Delta)
 }
+
+func TestGenerate_ToolChoiceNone(t *testing.T) {
+	t.Parallel()
+
+	server, calls := newAnthropicJSONServer(mockAnthropicGenerateResponse())
+	defer server.Close()
+
+	provider, err := New(
+		WithAPIKey("test-api-key"),
+		WithBaseURL(server.URL),
+	)
+	require.NoError(t, err)
+
+	model, err := provider.LanguageModel(context.Background(), "claude-sonnet-4-20250514")
+	require.NoError(t, err)
+
+	toolChoiceNone := fantasy.ToolChoiceNone
+	_, err = model.Generate(context.Background(), fantasy.Call{
+		Prompt: testPrompt(),
+		Tools: []fantasy.Tool{
+			WebSearchTool(nil),
+		},
+		ToolChoice: &toolChoiceNone,
+	})
+	require.NoError(t, err)
+
+	call := awaitAnthropicCall(t, calls)
+	toolChoice, ok := call.body["tool_choice"].(map[string]any)
+	require.True(t, ok, "request body should have tool_choice")
+	require.Equal(t, "none", toolChoice["type"], "tool_choice should be 'none'")
+}