diff --git a/docs/providers/anthropic.md b/docs/providers/anthropic.md index 8592d760e..2ccc3a472 100644 --- a/docs/providers/anthropic.md +++ b/docs/providers/anthropic.md @@ -235,6 +235,29 @@ foreach ($stream as $event) { } ``` +### Fine-grained tool streaming + +Anthropic’s [fine-grained tool streaming](https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/fine-grained-tool-streaming) lets tool-use input arrive incrementally (with lower latency for large arguments) instead of waiting for a complete JSON payload. Enable it on each tool that should use this behavior with the `eager_input_streaming` provider option: + +```php +use Prism\Prism\Facades\Prism; +use Prism\Prism\Tool; + +$weatherTool = Tool::as('get_weather') + ->for('Get current weather for a location') + ->withStringParameter('location', 'The city and state') + ->withProviderOptions(['eager_input_streaming' => true]) + ->using(fn (string $location): string => "Weather in {$location}: 72°F, sunny"); + +$stream = Prism::text() + ->using('anthropic', 'claude-sonnet-4-5-20250929') + ->withTools([$weatherTool]) + ->withPrompt('What is the weather in San Francisco?') + ->asStream(); +``` + +Use this together with a streaming entrypoint (`asStream()`, `asEventStreamResponse()`, etc.). While the model is still emitting a tool call, streamed input may be partial JSON; only treat tool arguments as final once the stream indicates the tool input is complete. + For complete streaming documentation including Vercel Data Protocol and WebSocket broadcasting, see [Streaming Output](/core-concepts/streaming-output). ## Documents diff --git a/src/Providers/Anthropic/Maps/ToolMap.php b/src/Providers/Anthropic/Maps/ToolMap.php index 9588f6316..b6a817436 100644 --- a/src/Providers/Anthropic/Maps/ToolMap.php +++ b/src/Providers/Anthropic/Maps/ToolMap.php @@ -30,6 +30,7 @@ public static function map(array $tools): array ], 'cache_control' => self::normalizeCacheControl($tool), 'strict' => (bool) $tool->providerOptions('strict'), + 'eager_input_streaming' => (bool) $tool->providerOptions('eager_input_streaming'), ]); }, $tools); } diff --git a/tests/Providers/Anthropic/ToolMapTest.php b/tests/Providers/Anthropic/ToolMapTest.php index c40cda65f..3e0815699 100644 --- a/tests/Providers/Anthropic/ToolMapTest.php +++ b/tests/Providers/Anthropic/ToolMapTest.php @@ -58,3 +58,45 @@ 'ephemeral', AnthropicCacheType::Ephemeral->value, ]); + +it('sets eager_input_streaming when eager_input_streaming provider option is true on tool', function (): void { + $tool = (new Tool) + ->as('search') + ->for('Searching the web') + ->withStringParameter('query', 'the detailed search query') + ->using(fn (): string => '[Search results]') + ->withProviderOptions(['eager_input_streaming' => true]); + + expect(ToolMap::map([$tool]))->toBe([[ + 'name' => 'search', + 'description' => 'Searching the web', + 'input_schema' => [ + 'type' => 'object', + 'properties' => [ + 'query' => [ + 'description' => 'the detailed search query', + 'type' => 'string', + ], + ], + 'required' => ['query'], + ], + 'eager_input_streaming' => true, + ]]); +}); + +it('omits eager_input_streaming when eager_input_streaming provider option is omitted or false', function (?bool $eagerInputStreaming): void { + $tool = (new Tool) + ->as('search') + ->for('Searching the web') + ->withStringParameter('query', 'the detailed search query') + ->using(fn (): string => '[Search results]'); + + if ($eagerInputStreaming !== null) { + $tool = $tool->withProviderOptions(['eager_input_streaming' => $eagerInputStreaming]); + } + + expect(ToolMap::map([$tool])[0])->not->toHaveKey('eager_input_streaming'); +})->with([ + 'omitted' => [null], + 'false' => [false], +]);