Skip to content

Commit efa3276

Browse files
authored
feat(anthropic): add provider tool support for sturctured mode (prism-php#969)
1 parent 557382d commit efa3276

5 files changed

Lines changed: 145 additions & 7 deletions

File tree

src/Providers/Anthropic/Handlers/Structured.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,16 @@
2222
use Prism\Prism\Providers\Anthropic\Handlers\StructuredStrategies\ToolStructuredStrategy;
2323
use Prism\Prism\Providers\Anthropic\Maps\FinishReasonMap;
2424
use Prism\Prism\Providers\Anthropic\Maps\MessageMap;
25+
use Prism\Prism\Providers\Anthropic\Maps\ToolChoiceMap;
26+
use Prism\Prism\Providers\Anthropic\Maps\ToolMap;
2527
use Prism\Prism\Structured\Request as StructuredRequest;
2628
use Prism\Prism\Structured\Response;
2729
use Prism\Prism\Structured\ResponseBuilder;
2830
use Prism\Prism\Structured\Step;
2931
use Prism\Prism\ValueObjects\Messages\AssistantMessage;
3032
use Prism\Prism\ValueObjects\Messages\ToolResultMessage;
3133
use Prism\Prism\ValueObjects\Meta;
34+
use Prism\Prism\ValueObjects\ProviderTool;
3235
use Prism\Prism\ValueObjects\ToolCall;
3336
use Prism\Prism\ValueObjects\ToolResult;
3437
use Prism\Prism\ValueObjects\Usage;
@@ -100,6 +103,8 @@ public static function buildHttpRequestPayload(PrismRequest $request): array
100103
'max_tokens' => $request->maxTokens() ?? 64000,
101104
'temperature' => $request->temperature(),
102105
'top_p' => $request->topP(),
106+
'tools' => static::buildTools($request) ?: null,
107+
'tool_choice' => ToolChoiceMap::map($request->toolChoice()),
103108
'mcp_servers' => $request->providerOptions('mcp_servers'),
104109
'cache_control' => $request->providerOptions('cache_control'),
105110
]);
@@ -114,6 +119,29 @@ protected static function createStrategy(StructuredRequest $request): AnthropicS
114119
: new NativeOutputFormatStructuredStrategy(request: $request);
115120
}
116121

122+
/**
123+
* @return array<int|string, mixed>
124+
*/
125+
protected static function buildTools(StructuredRequest $request): array
126+
{
127+
$tools = ToolMap::map($request->tools());
128+
129+
if ($request->providerTools() === []) {
130+
return $tools;
131+
}
132+
133+
$providerTools = array_map(
134+
fn (ProviderTool $tool): array => [
135+
'type' => $tool->type,
136+
'name' => $tool->name,
137+
...$tool->options,
138+
],
139+
$request->providerTools()
140+
);
141+
142+
return array_merge($providerTools, $tools);
143+
}
144+
117145
/**
118146
* @param ToolCall[] $toolCalls
119147
*/

src/Providers/Anthropic/Handlers/StructuredStrategies/ToolStructuredStrategy.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use Prism\Prism\Enums\ToolChoice;
99
use Prism\Prism\Exceptions\PrismException;
1010
use Prism\Prism\Providers\Anthropic\Maps\ToolChoiceMap;
11-
use Prism\Prism\Providers\Anthropic\Maps\ToolMap;
1211
use Prism\Prism\Structured\Response as PrismResponse;
1312
use Prism\Prism\ValueObjects\Messages\UserMessage;
1413

@@ -48,16 +47,18 @@ public function mutatePayload(array $payload): array
4847
],
4948
];
5049

51-
$customTools = ToolMap::map($this->request->tools());
50+
$existingTools = $payload['tools'] ?? [];
5251

5352
$payload = [
5453
...$payload,
55-
'tools' => [...$customTools, $structuredOutputTool],
54+
'tools' => [...$existingTools, $structuredOutputTool],
5655
];
5756

5857
$toolChoice = $this->resolveToolChoice();
5958
if ($toolChoice !== null) {
6059
$payload['tool_choice'] = $toolChoice;
60+
} else {
61+
unset($payload['tool_choice']);
6162
}
6263

6364
return $payload;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"id":"msg_01ABCDEFprovider1","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"server_tool_use","id":"srvtoolu_01ABC123","name":"web_search","input":{"query":"current weather San Francisco"}},{"type":"web_search_tool_result","tool_use_id":"srvtoolu_01ABC123","content":[{"type":"web_search_result","title":"San Francisco Weather - Current Conditions","url":"https://weather.example.com/sf","page_age":"2h","snippet":"Current conditions in San Francisco: 65°F, partly cloudy with light winds from the west.","encrypted_content":"encrypteddata123"}]},{"type":"text","text":"{\"summary\":\"Currently 65°F and partly cloudy in San Francisco with light westerly winds.\",\"recommendation\":\"Light layers recommended - a light jacket should suffice for the mild conditions.\"}"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":250,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":85,"service_tier":"standard","server_tool_use":{"web_search_requests":1}}}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"id":"msg_01ABCDEFprovider2","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"server_tool_use","id":"srvtoolu_01DEF456","name":"web_search","input":{"query":"current weather San Francisco"}},{"type":"web_search_tool_result","tool_use_id":"srvtoolu_01DEF456","content":[{"type":"web_search_result","title":"San Francisco Weather","url":"https://weather.example.com/sf","page_age":"1h","snippet":"San Francisco: 65°F, partly cloudy.","encrypted_content":"encrypteddata456"}]},{"type":"tool_use","id":"toolu_01GHI789","name":"output_structured_data","input":{"summary":"Currently 65°F and partly cloudy in San Francisco.","recommendation":"A light jacket is recommended for the mild, partly cloudy conditions."}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":300,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":95,"service_tier":"standard","server_tool_use":{"web_search_requests":1}}}

tests/Providers/Anthropic/StructuredWithToolsTest.php

Lines changed: 111 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Prism\Prism\Schema\ObjectSchema;
1414
use Prism\Prism\Schema\StringSchema;
1515
use Prism\Prism\Tool;
16+
use Prism\Prism\ValueObjects\ProviderTool;
1617
use Tests\Fixtures\FixtureResponse;
1718

1819
beforeEach(function (): void {
@@ -120,7 +121,7 @@
120121
expect($finalStep->structured)->toBeArray();
121122
});
122123

123-
it('can use JSON mode strategy with custom tools', function (): void {
124+
it('can use native output format strategy with custom tools', function (): void {
124125
FixtureResponse::fakeResponseSequence('*', 'anthropic/structured-with-tools-json-mode');
125126

126127
$schema = new ObjectSchema(
@@ -143,16 +144,13 @@
143144
->withSchema($schema)
144145
->withTools([$searchTool])
145146
->withMaxSteps(3)
146-
->withProviderOptions(['use_tool_calling' => false]) // Use JSON mode instead
147147
->withPrompt('Search for "Prism PHP" and give me a summary')
148148
->asStructured();
149149

150-
// Verify structured output
151150
expect($response->structured)->toBeArray()
152151
->and($response->structured)->toHaveKey('result')
153152
->and($response->structured['result'])->toBeString();
154153

155-
// JSON mode should still support tools
156154
expect($response->toolCalls)->toBeArray();
157155
expect($response->toolResults)->toBeArray();
158156
});
@@ -377,4 +375,113 @@
377375

378376
expect($payload['tool_choice'])->toBe(['type' => 'tool', 'name' => 'output_structured_data']);
379377
});
378+
379+
it('can generate structured output with provider tools using native output format', function (): void {
380+
FixtureResponse::fakeResponseSequence('*', 'anthropic/structured-with-provider-tool-native');
381+
382+
$schema = new ObjectSchema(
383+
'weather_analysis',
384+
'Analysis of weather conditions',
385+
[
386+
new StringSchema('summary', 'A summary of the weather', true),
387+
new StringSchema('recommendation', 'A recommendation based on weather', true),
388+
],
389+
['summary', 'recommendation']
390+
);
391+
392+
$response = Prism::structured()
393+
->using(Provider::Anthropic, 'claude-sonnet-4-6')
394+
->withSchema($schema)
395+
->withProviderTools([new ProviderTool(type: 'web_search_20250305', name: 'web_search')])
396+
->withPrompt('What is the weather in San Francisco and should I wear a coat?')
397+
->asStructured();
398+
399+
expect($response->structured)->toBeArray()
400+
->and($response->structured)->toHaveKeys(['summary', 'recommendation'])
401+
->and($response->structured['summary'])->toBeString()
402+
->and($response->structured['recommendation'])->toBeString();
403+
});
404+
405+
it('can generate structured output with provider tools using tool calling mode', function (): void {
406+
FixtureResponse::fakeResponseSequence('*', 'anthropic/structured-with-provider-tool-toolcalling');
407+
408+
$schema = new ObjectSchema(
409+
'weather_analysis',
410+
'Analysis of weather conditions',
411+
[
412+
new StringSchema('summary', 'A summary of the weather', true),
413+
new StringSchema('recommendation', 'A recommendation based on weather', true),
414+
],
415+
['summary', 'recommendation']
416+
);
417+
418+
$response = Prism::structured()
419+
->using(Provider::Anthropic, 'claude-sonnet-4-6')
420+
->withSchema($schema)
421+
->withProviderTools([new ProviderTool(type: 'web_search_20250305', name: 'web_search')])
422+
->withProviderOptions(['use_tool_calling' => true])
423+
->withPrompt('What is the weather in San Francisco and should I wear a coat?')
424+
->asStructured();
425+
426+
expect($response->structured)->toBeArray()
427+
->and($response->structured)->toHaveKeys(['summary', 'recommendation'])
428+
->and($response->structured['summary'])->toBeString()
429+
->and($response->structured['recommendation'])->toBeString();
430+
});
431+
432+
it('includes provider tools in native output format payload', function (): void {
433+
Prism::fake();
434+
435+
$schema = new ObjectSchema(
436+
'output',
437+
'the output object',
438+
[new StringSchema('result', 'The result', true)],
439+
['result']
440+
);
441+
442+
$request = Prism::structured()
443+
->using(Provider::Anthropic, 'claude-sonnet-4-6')
444+
->withSchema($schema)
445+
->withProviderTools([new ProviderTool(type: 'web_search_20250305', name: 'web_search')]);
446+
447+
$payload = Structured::buildHttpRequestPayload(
448+
$request->toRequest()
449+
);
450+
451+
expect($payload)->toHaveKey('tools')
452+
->and($payload)->toHaveKey('output_config')
453+
->and($payload['tools'])->toHaveCount(1)
454+
->and($payload['tools'][0]['type'])->toBe('web_search_20250305')
455+
->and($payload['tools'][0]['name'])->toBe('web_search');
456+
});
457+
458+
it('includes provider tools alongside structured tool in tool calling mode', function (): void {
459+
Prism::fake();
460+
461+
$schema = new ObjectSchema(
462+
'output',
463+
'the output object',
464+
[new StringSchema('result', 'The result', true)],
465+
['result']
466+
);
467+
468+
$request = Prism::structured()
469+
->using(Provider::Anthropic, 'claude-sonnet-4-6')
470+
->withSchema($schema)
471+
->withProviderTools([new ProviderTool(type: 'web_search_20250305', name: 'web_search')])
472+
->withProviderOptions(['use_tool_calling' => true]);
473+
474+
$payload = Structured::buildHttpRequestPayload(
475+
$request->toRequest()
476+
);
477+
478+
expect($payload)->toHaveKey('tools');
479+
480+
$toolTypes = array_column($payload['tools'], 'type');
481+
$toolNames = array_column($payload['tools'], 'name');
482+
483+
expect($toolNames)->toContain('web_search')
484+
->and($toolNames)->toContain('output_structured_data')
485+
->and($toolTypes)->toContain('web_search_20250305');
486+
});
380487
});

0 commit comments

Comments
 (0)