Skip to content

AnthropicTextGenerationModel fails on tool function call if it has empty function arguments for input #171

@saarnilauri

Description

@saarnilauri

AnthropicTextGenerationModel fails when sending function calls with empty or null arguments to Anthropic API, resulting in "Bad Request (400) - messages.X.content.Y.tool_use.input: Input should be a valid dictionary" error.

  • Provider: Anthropic Claude (Sonnet 4.5)
  • Affected file: php-ai-client/src/ProviderImplementations/Anthropic/AnthropicTextGenerationModel.php

When an AI function/tool is called without parameters (see the example ability: list-block-patterns ability that accepts optional parameters only), the FunctionCall object may contain:

  • args = null
  • args = [] (empty array)

When preparing the request for Anthropic API in getMessagePartData() method (line ~346), the code directly passes these values:

if ($type->isFunctionCall()) {
    $functionCall = $part->getFunctionCall();
    return [
        'type' => 'tool_use',
        'id' => $functionCall->getId(),
        'name' => $functionCall->getName(),
        'input' => $functionCall->getArgs(), // ❌ Can be null or []
    ];
}

Anthropic API Requirements:

  • The tool_use.input field MUST be a valid dictionary/object
  • Empty arrays [] are rejected
  • null values are rejected
  • Only {} (empty object) or populated objects are accepted

Proposed solution

Modify getMessagePartData() method in AnthropicTextGenerationModel.php to ensure input is always a valid object:

if ($type->isFunctionCall()) {
    $functionCall = $part->getFunctionCall();
    if (!$functionCall) {
        throw new RuntimeException(
            'The function_call typed message part must contain a function call.'
        );
    }
    
    $args = $functionCall->getArgs();
    
    // ✅ Ensure args is always a valid object for Anthropic API
    if ($args === null || (is_array($args) && empty($args))) {
        $args = new \stdClass(); // Serializes to {} in JSON
    }
    
    return [
        'type' => 'tool_use',
        'id' => $functionCall->getId(),
        'name' => $functionCall->getName(),
        'input' => $args,
    ];
}

The example ability to see what is the real case input:

<?php
/**
 * List Block Patterns Ability implementation.
 *
 * @package ValuDev\Abilities
 */

declare(strict_types=1);

namespace ValuDev\Abilities;

use ValuDev\Abstracts\Abstract_Valu_Ability;
use WP_Block_Patterns_Registry;

/**
 * List available block patterns ability.
 */
class List_Block_Patterns_Ability extends Abstract_Valu_Ability {

    /**
     * {@inheritDoc}
     */
    protected function input_schema(): array {
        return array(
            'type'                 => array( 'object', 'null' ),
            'properties'           => array(
                'category' => array(
                    'type'        => 'string',
                    'description' => __( 'Filter patterns by category slug (optional)', 'valu-dev-mcp' ),
                ),
            ),
            'additionalProperties' => false,
        );
    }

    /**
     * {@inheritDoc}
     */
    protected function output_schema(): array {
        return array(
            'type'       => 'object',
            'properties' => array(
                'success'  => array(
                    'type'        => 'boolean',
                    'description' => __( 'Whether the patterns were retrieved successfully', 'valu-dev-mcp' ),
                ),
                'patterns' => array(
                    'type'        => 'array',
                    'description' => __( 'List of available block patterns', 'valu-dev-mcp' ),
                    'items'       => array(
                        'type'       => 'object',
                        'properties' => array(
                            'id'          => array(
                                'type'        => array( 'integer', 'null' ),
                                'description' => __( 'Pattern post ID (null for registered patterns)', 'valu-dev-mcp' ),
                            ),
                            'slug'        => array(
                                'type'        => 'string',
                                'description' => __( 'Pattern slug for use with get-block-pattern-raw and get-block-pattern-parsed', 'valu-dev-mcp' ),
                            ),
                            'title'       => array(
                                'type'        => 'string',
                                'description' => __( 'Pattern title', 'valu-dev-mcp' ),
                            ),
                            'description' => array(
                                'type'        => 'string',
                                'description' => __( 'Pattern description', 'valu-dev-mcp' ),
                            ),
                        ),
                    ),
                ),
                'count'    => array(
                    'type'        => 'integer',
                    'description' => __( 'Total number of patterns returned', 'valu-dev-mcp' ),
                ),
                'message'  => array(
                    'type'        => 'string',
                    'description' => __( 'Status message', 'valu-dev-mcp' ),
                ),
            ),
            'additionalProperties' => false,
        );
    }

   // and the code continues …
       
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions