Skip to content

Commit 2936d2e

Browse files
authored
feat!: provider tools (#405)
BREAKING CHANGE: Gemini search grounding must now be enabled using withProviderTools
1 parent 2d85e7c commit 2936d2e

19 files changed

+692
-167
lines changed

docs/providers/anthropic.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,40 @@ Prism supports [Anthropic's citations feature](https://docs.anthropic.com/en/doc
181181

182182
Please note however that due to Anthropic not supporting "native" structured output, and Prism's workaround for this, the output can be unreliable. You should therefore ensure you implement proper error handling for the scenario where Anthropic does not return a valid decodable schema.
183183

184+
## Code execution
185+
186+
To enable code execution, you will first need to enable the beta feature.
187+
188+
Either in prism/config.php:
189+
190+
```php
191+
'anthropic' => [
192+
...
193+
'anthropic_beta' => 'code-execution-2025-05-22',
194+
],
195+
196+
```
197+
198+
Or in your env file (assuming config/prism.php reflects the default prism setup):
199+
200+
```
201+
ANTHROPIC_BETA="code-execution-2025-05-22"
202+
```
203+
204+
You may then use code execution as follows:
205+
206+
```php
207+
use Prism\Prism\Prism;
208+
use Prism\Prism\ValueObjects\ProviderTool;
209+
210+
Prism::text()
211+
->using('anthropic', 'claude-3-5-haiku-latest')
212+
->withPrompt('Solve the equation 3x + 10 = 14.')
213+
->withProviderTools([new ProviderTool(type: 'code_execution_20250522', name: 'code_execution')])
214+
->asText();
215+
216+
```
217+
184218
### Enabling citations
185219

186220
Anthropic require citations to be enabled on all documents in a request. To enable them, using the `withProviderOptions()` method when building your request:

docs/providers/gemini.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
## Search grounding
1212

13-
You may enable Google search grounding on text requests using providerOptions:
13+
You may enable Google search grounding on text requests using withProviderTools:
1414

1515
```php
1616
use Prism\Prism\Prism;
@@ -20,7 +20,7 @@ $response = Prism::text()
2020
->using(Provider::Gemini, 'gemini-2.0-flash')
2121
->withPrompt('What is the stock price of Google right now?')
2222
// Enable search grounding
23-
->withProviderOptions(['searchGrounding' => true])
23+
->withProviderTools(['google_search'])
2424
->generate();
2525
```
2626

docs/providers/openai.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,18 @@ $response = Prism::structured()
6969
### Caching
7070

7171
Automatic caching does not currently work with JsonMode. Please ensure you use StructuredMode if you wish to utilise automatic caching.
72+
73+
### Code interpreter
74+
75+
You can use the OpenAI code interpreter as follows:
76+
77+
```php
78+
use Prism\Prism\Prism;
79+
use Prism\Prism\ValueObjects\ProviderTool;
80+
81+
Prism::text()
82+
->using('openai', 'gpt-4.1')
83+
->withPrompt('Solve the equation 3x + 10 = 14.')
84+
->withProviderTools([new ProviderTool(type: 'code_interpreter', options: ['container' => ['type' => 'auto']])])
85+
->asText();
86+
```

src/Concerns/HasProviderTools.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Prism\Prism\Concerns;
6+
7+
use Prism\Prism\ValueObjects\ProviderTool;
8+
9+
trait HasProviderTools
10+
{
11+
/** @var array<int,ProviderTool> */
12+
protected array $providerTools = [];
13+
14+
/**
15+
* @param array<int,ProviderTool> $providerTools
16+
*/
17+
public function withProviderTools(array $providerTools): self
18+
{
19+
$this->providerTools = $providerTools;
20+
21+
return $this;
22+
}
23+
}

src/Providers/Anthropic/Handlers/Text.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Prism\Prism\ValueObjects\Messages\AssistantMessage;
2222
use Prism\Prism\ValueObjects\Messages\ToolResultMessage;
2323
use Prism\Prism\ValueObjects\Meta;
24+
use Prism\Prism\ValueObjects\ProviderTool;
2425
use Prism\Prism\ValueObjects\ToolCall;
2526
use Prism\Prism\ValueObjects\ToolResult;
2627
use Prism\Prism\ValueObjects\Usage;
@@ -97,7 +98,7 @@ public static function buildHttpRequestPayload(PrismRequest $request): array
9798
'max_tokens' => $request->maxTokens(),
9899
'temperature' => $request->temperature(),
99100
'top_p' => $request->topP(),
100-
'tools' => ToolMap::map($request->tools()),
101+
'tools' => static::buildTools($request),
101102
'tool_choice' => ToolChoiceMap::map($request->toolChoice()),
102103
]);
103104
}
@@ -178,6 +179,28 @@ protected function prepareTempResponse(): void
178179
);
179180
}
180181

182+
/**
183+
* @return array<int|string,mixed>
184+
*/
185+
protected static function buildTools(TextRequest $request): array
186+
{
187+
$tools = ToolMap::map($request->tools());
188+
189+
if ($request->providerTools() === []) {
190+
return $tools;
191+
}
192+
193+
$providerTools = array_map(
194+
fn (ProviderTool $tool): array => [
195+
'type' => $tool->type,
196+
'name' => $tool->name,
197+
],
198+
$request->providerTools()
199+
);
200+
201+
return array_merge($providerTools, $tools);
202+
}
203+
181204
/**
182205
* @param array<string, mixed> $data
183206
* @return ToolCall[]

src/Providers/Gemini/Handlers/Text.php

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Prism\Prism\ValueObjects\Messages\AssistantMessage;
2626
use Prism\Prism\ValueObjects\Messages\ToolResultMessage;
2727
use Prism\Prism\ValueObjects\Meta;
28+
use Prism\Prism\ValueObjects\ProviderTool;
2829
use Prism\Prism\ValueObjects\ToolResult;
2930
use Prism\Prism\ValueObjects\Usage;
3031
use Throwable;
@@ -89,17 +90,24 @@ protected function sendRequest(Request $request): ClientResponse
8990
'thinkingConfig' => $thinkingConfig !== [] ? $thinkingConfig : null,
9091
]);
9192

92-
if ($request->tools() !== [] && ($providerOptions['searchGrounding'] ?? false)) {
93-
throw new Exception('Use of search grounding with custom tools is not currently supported by Prism.');
93+
if ($request->tools() !== [] && $request->providerTools() != []) {
94+
throw new Exception('Use of provider tools with custom tools is not currently supported by Gemini.');
9495
}
9596

96-
$tools = $providerOptions['searchGrounding'] ?? false
97-
? [
98-
[
99-
'google_search' => (object) [],
100-
],
101-
]
102-
: ($request->tools() !== [] ? ['function_declarations' => ToolMap::map($request->tools())] : []);
97+
$tools = [];
98+
99+
if ($request->providerTools() !== []) {
100+
$tools = [
101+
Arr::mapWithKeys(
102+
$request->providerTools(),
103+
fn (ProviderTool $providerTool): array => [$providerTool->type => (object) []]
104+
),
105+
];
106+
}
107+
108+
if ($request->tools() !== []) {
109+
$tools['function_declarations'] = ToolMap::map($request->tools());
110+
}
103111

104112
return $this->client->post(
105113
"{$request->model()}:generateContent",

src/Providers/OpenAI/Handlers/Text.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use Prism\Prism\ValueObjects\Messages\AssistantMessage;
2525
use Prism\Prism\ValueObjects\Messages\ToolResultMessage;
2626
use Prism\Prism\ValueObjects\Meta;
27+
use Prism\Prism\ValueObjects\ProviderTool;
2728
use Prism\Prism\ValueObjects\ToolResult;
2829
use Prism\Prism\ValueObjects\Usage;
2930
use Throwable;
@@ -118,7 +119,7 @@ protected function sendRequest(Request $request): ClientResponse
118119
'temperature' => $request->temperature(),
119120
'top_p' => $request->topP(),
120121
'metadata' => $request->providerOptions('metadata'),
121-
'tools' => ToolMap::map($request->tools()),
122+
'tools' => $this->buildTools($request),
122123
'tool_choice' => ToolChoiceMap::map($request->toolChoice()),
123124
'previous_response_id' => $request->providerOptions('previous_response_id'),
124125
'truncation' => $request->providerOptions('truncation'),
@@ -156,4 +157,26 @@ protected function addStep(array $data, Request $request, ClientResponse $client
156157
systemPrompts: $request->systemPrompts(),
157158
));
158159
}
160+
161+
/**
162+
* @return array<int|string,mixed>
163+
*/
164+
protected function buildTools(Request $request): array
165+
{
166+
$tools = ToolMap::map($request->tools());
167+
168+
if ($request->providerTools() === []) {
169+
return $tools;
170+
}
171+
172+
$providerTools = array_map(
173+
fn (ProviderTool $tool): array => [
174+
'type' => $tool->type,
175+
...$tool->options,
176+
],
177+
$request->providerTools()
178+
);
179+
180+
return array_merge($providerTools, $tools);
181+
}
159182
}

src/Text/PendingRequest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Prism\Prism\Concerns\HasMessages;
1414
use Prism\Prism\Concerns\HasPrompts;
1515
use Prism\Prism\Concerns\HasProviderOptions;
16+
use Prism\Prism\Concerns\HasProviderTools;
1617
use Prism\Prism\Concerns\HasTools;
1718
use Prism\Prism\Exceptions\PrismException;
1819
use Prism\Prism\ValueObjects\Messages\UserMessage;
@@ -27,6 +28,7 @@ class PendingRequest
2728
use HasMessages;
2829
use HasPrompts;
2930
use HasProviderOptions;
31+
use HasProviderTools;
3032
use HasTools;
3133

3234
/**
@@ -76,6 +78,7 @@ public function toRequest(): Request
7678
clientRetry: $this->clientRetry,
7779
toolChoice: $this->toolChoice,
7880
providerOptions: $this->providerOptions,
81+
providerTools: $this->providerTools,
7982
);
8083
}
8184
}

src/Text/Request.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Prism\Prism\Enums\ToolChoice;
1313
use Prism\Prism\Tool;
1414
use Prism\Prism\ValueObjects\Messages\SystemMessage;
15+
use Prism\Prism\ValueObjects\ProviderTool;
1516

1617
class Request implements PrismRequest
1718
{
@@ -24,6 +25,7 @@ class Request implements PrismRequest
2425
* @param array<string, mixed> $clientOptions
2526
* @param array{0: array<int, int>|int, 1?: Closure|int, 2?: ?callable, 3?: bool} $clientRetry
2627
* @param array<string, mixed> $providerOptions
28+
* @param array<int, ProviderTool> $providerTools
2729
*/
2830
public function __construct(
2931
protected string $model,
@@ -39,6 +41,7 @@ public function __construct(
3941
protected array $clientRetry,
4042
protected string|ToolChoice|null $toolChoice,
4143
array $providerOptions = [],
44+
protected array $providerTools = [],
4245
) {
4346
$this->providerOptions = $providerOptions;
4447
}
@@ -77,6 +80,14 @@ public function topP(): int|float|null
7780
return $this->topP;
7881
}
7982

83+
/**
84+
* @return array<int,ProviderTool>
85+
*/
86+
public function providerTools(): array
87+
{
88+
return $this->providerTools;
89+
}
90+
8091
public function temperature(): int|float|null
8192
{
8293
return $this->temperature;

src/ValueObjects/ProviderTool.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Prism\Prism\ValueObjects;
4+
5+
class ProviderTool
6+
{
7+
/**
8+
* @param array<string,mixed> $options
9+
*/
10+
public function __construct(
11+
public readonly string $type,
12+
public readonly ?string $name = null,
13+
public readonly array $options = [],
14+
) {}
15+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"id":"msg_01AJ87pcoq6djGGrcYvYGvUy","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"text","text":"I'll solve this equation step by step and then verify the solution using Python.\n\nSolving 3x + 10 = 14 algebraically:\n1. Subtract 10 from both sides of the equation\n 3x + 10 - 10 = 14 - 10\n 3x = 4\n\n2. Divide both sides by 3\n 3x ÷ 3 = 4 ÷ 3\n x = 4/3\n\nLet me verify this solution using Python:"},{"type":"server_tool_use","id":"srvtoolu_01Co9FxvC6Di5aAfcu8RhUHh","name":"code_execution","input":{"code":"# Solve the equation 3x + 10 = 14\nx = (14 - 10) / 3\nprint(f\"x = {x}\")\n\n# Verify the solution\nleft_side = 3 * x + 10\nright_side = 14\n\nprint(f\"\\nVerification:\")\nprint(f\"Left side (3x + 10): {left_side}\")\nprint(f\"Right side (14): {right_side}\")\nprint(f\"Equation holds: {left_side == right_side}\")"}},{"type":"code_execution_tool_result","tool_use_id":"srvtoolu_01Co9FxvC6Di5aAfcu8RhUHh","content":{"type":"code_execution_result","stdout":"x = 1.3333333333333333\n\nVerification:\nLeft side (3x + 10): 14.0\nRight side (14): 14\nEquation holds: True\n","stderr":"","return_code":0,"content":[]}},{"type":"text","text":"The solution is x = 4/3 (or approximately 1.333). \n\nLet's break down the verification:\n- x = 4/3\n- Left side: 3 * (4/3) + 10 = 4 + 10 = 14\n- Right side: 14\n\nThe equation is satisfied, confirming that x = 4/3 is the correct solution."}],"container":{"id":"container_011CQ4hgKqeoPBYecAubG4B9","expires_at":"2025-06-12T18:13:51.893719+00:00"},"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":1779,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":398,"service_tier":"standard","server_tool_use":{"web_search_requests":0}}}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"id":"msg_01RuwnupYURXYMrhuXTxRqxf","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"text","text":"I'll help you solve this by first checking the current temperature in Detroit and then using that value to solve the equation."},{"type":"tool_use","id":"toolu_013vSsaqxgM8GTUMGDoeCsbS","name":"weather","input":{"city":"Detroit"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":854,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":76,"service_tier":"standard"}}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"id":"msg_014o7Vi8g9F3nW2PFEWMWG9a","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"text","text":"Great! The current temperature in Detroit is 75 degrees. Let's solve the equation 3x + 10 = Y by substituting 75 for x:\n\n3(75) + 10 = Y\n225 + 10 = Y\n235 = Y\n\nSo, when x = 75 (the current temperature in Detroit), Y equals 235 in the equation 3x + 10 = Y."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":950,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":99,"service_tier":"standard"}}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"id": "resp_684b06dab43c819c84f623fec58aa55d029bfe449563c425",
3+
"object": "response",
4+
"created_at": 1749747418,
5+
"status": "completed",
6+
"background": false,
7+
"error": null,
8+
"incomplete_details": null,
9+
"instructions": null,
10+
"max_output_tokens": 2048,
11+
"model": "gpt-4.1-2025-04-14",
12+
"output": [
13+
{
14+
"id": "msg_684b06dcce74819c819ffe9b836d7ff8029bfe449563c425",
15+
"type": "message",
16+
"status": "completed",
17+
"content": [
18+
{
19+
"type": "output_text",
20+
"annotations": [],
21+
"text": "Let's solve the equation step by step:\n\nGiven equation:\n\\[ 3x + 10 = 14 \\]\n\n**Step 1:** Subtract 10 from both sides to isolate the term with \\( x \\):\n\\[\n3x + 10 - 10 = 14 - 10\n\\]\n\\[\n3x = 4\n\\]\n\n**Step 2:** Divide both sides by 3 to solve for \\( x \\):\n\\[\nx = \\frac{4}{3}\n\\]\n\n**Final Answer:**\n\\[\n\\boxed{x = \\frac{4}{3}}\n\\]"
22+
}
23+
],
24+
"role": "assistant"
25+
}
26+
],
27+
"parallel_tool_calls": true,
28+
"previous_response_id": null,
29+
"reasoning": {
30+
"effort": null,
31+
"summary": null
32+
},
33+
"service_tier": "default",
34+
"store": true,
35+
"temperature": 1.0,
36+
"text": {
37+
"format": {
38+
"type": "text"
39+
}
40+
},
41+
"tool_choice": "auto",
42+
"tools": [
43+
{
44+
"type": "code_interpreter",
45+
"container": {
46+
"type": "auto"
47+
}
48+
}
49+
],
50+
"top_p": 1.0,
51+
"truncation": "disabled",
52+
"usage": {
53+
"input_tokens": 117,
54+
"input_tokens_details": {
55+
"cached_tokens": 0
56+
},
57+
"output_tokens": 126,
58+
"output_tokens_details": {
59+
"reasoning_tokens": 0
60+
},
61+
"total_tokens": 243
62+
},
63+
"user": null,
64+
"metadata": {}
65+
}

0 commit comments

Comments
 (0)