From d749d13e404a9a88096261b5817084ebab7ddeee Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Wed, 18 Mar 2026 15:02:38 -0300 Subject: [PATCH] fix(anthropic): ToolChoiceNone should send tool_choice:{type:"none"} to API (#178) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- providers/anthropic/anthropic.go | 5 ++++- providers/anthropic/anthropic_test.go | 31 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/providers/anthropic/anthropic.go b/providers/anthropic/anthropic.go index b1bcee24cd40d6751c73dab9317e6c30c97037ed..25ac81dab38230debc3c5ad6fb5d6cb933665a30 100644 --- a/providers/anthropic/anthropic.go +++ b/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{ diff --git a/providers/anthropic/anthropic_test.go b/providers/anthropic/anthropic_test.go index 51adb7c333110ed1c3a490cce32312ef05df8be9..5f137a78be7a0b2f6fcfbad8403474ac5ece376a 100644 --- a/providers/anthropic/anthropic_test.go +++ b/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'") +}