Skip to content

anthropic provider: toolChoice 'none' strips tools from request, causing empty response with tool_use history #12378

Description

@roaminro

Description

When toolChoice: { type: 'none' } is set, the @ai-sdk/anthropic provider strips both tools and tool_choice from the API request. This causes the Anthropic API to return an empty content: [] response when the conversation history contains tool_use/tool_result blocks.

Reproduction

import { anthropic } from '@ai-sdk/anthropic';
import { generateText, tool } from 'ai';
import { z } from 'zod';

const result = await generateText({
  model: anthropic('claude-sonnet-4-20250514'),
  tools: {
    search: tool({
      description: 'Search for information',
      parameters: z.object({ query: z.string() }),
      execute: async ({ query }) => `Results for "${query}"`,
    }),
  },
  toolChoice: 'auto',
  maxSteps: 3,
  prompt: 'Search for "climate change" and summarize the results',
  // On the last step, set toolChoice to 'none' to force a text response
  // This is a common pattern for agentic loops that need to guarantee
  // the final step produces text rather than another tool call
});

// The final step will have empty text because the provider strips tools
// from the request, and the Anthropic API returns content: [] when it
// sees tool_use/tool_result blocks in history but no tools parameter

A more direct reproduction:

import { anthropic } from '@ai-sdk/anthropic';

const model = anthropic('claude-sonnet-4-20250514');

// Simulate a multi-step conversation where step 1 used tools
const result = await model.doGenerate({
  inputFormat: 'messages',
  mode: {
    type: 'regular',
    tools: [
      {
        type: 'function',
        name: 'search',
        description: 'Search for info',
        parameters: { type: 'object', properties: { query: { type: 'string' } } },
      },
    ],
    toolChoice: { type: 'none' },
  },
  prompt: [
    { role: 'user', content: [{ type: 'text', text: 'Search for climate change' }] },
    {
      role: 'assistant',
      content: [
        { type: 'tool-call', toolCallId: 'tc1', toolName: 'search', args: { query: 'climate change' } },
      ],
    },
    {
      role: 'tool',
      content: [{ type: 'tool-result', toolCallId: 'tc1', result: 'Some results' }],
    },
    { role: 'user', content: [{ type: 'text', text: 'Now summarize what you found' }] },
  ],
});

console.log(result.text); // '' (empty string)
console.log(result.finishReason); // 'stop'
// The API returned content: [] with ~8 output tokens (just stop tokens)

Root Cause

In the Anthropic provider's prepareTools function:

case "none":
  return { tools: void 0, toolChoice: void 0, toolWarnings, betas };

When toolChoice is 'none', the provider sets both tools and toolChoice to undefined, which removes them from the API request entirely. The Anthropic API then receives:

  • Message history with tool_use and tool_result blocks
  • No tools parameter
  • No tool_choice parameter

This causes the API to return content: [] (empty content array) with stop_reason: "end_turn".

Expected Behavior

When toolChoice is 'none', the provider should send tool_choice: { type: "none" } along with the tools array to the Anthropic API. This tells the model "you have these tools but you must not use them" — producing a normal text response.

The Anthropic API has supported tool_choice: { type: "none" } since the 2025-04-15 API version.

Suggested Fix

case "none":
  return { tools, toolChoice: { type: "none" }, toolWarnings, betas };

Keep the tools array and send tool_choice: { type: "none" } instead of stripping both.

Affected Versions

Tested and confirmed in:

  • @ai-sdk/anthropic@2.0.45
  • @ai-sdk/anthropic@2.0.60
  • @ai-sdk/anthropic@3.0.39 (latest)

Workaround

Convert tool_use/tool_result messages to plain text before setting toolChoice: 'none', so the API request doesn't contain tool blocks when tools are stripped:

// Convert tool call/result messages to text descriptions
const cleanedMessages = messages.map(msg => {
  // ... replace tool-call/tool-result parts with text descriptions
});
// Now safe to remove tools since history has no tool blocks

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions