diff --git a/src/Providers/Gemini/Handlers/Stream.php b/src/Providers/Gemini/Handlers/Stream.php index e7acfad33..45105243b 100644 --- a/src/Providers/Gemini/Handlers/Stream.php +++ b/src/Providers/Gemini/Handlers/Stream.php @@ -14,7 +14,7 @@ use Prism\Prism\Exceptions\PrismStreamDecodeException; use Prism\Prism\Providers\Gemini\Maps\FinishReasonMap; use Prism\Prism\Providers\Gemini\Maps\MessageMap; -use Prism\Prism\Providers\Gemini\Maps\ToolChoiceMap; +use Prism\Prism\Providers\Gemini\Maps\ToolConfigMap; use Prism\Prism\Providers\Gemini\Maps\ToolMap; use Prism\Prism\Streaming\EventID; use Prism\Prism\Streaming\Events\StepFinishEvent; @@ -447,13 +447,8 @@ protected function sendRequest(Request $request): Response { $providerOptions = $request->providerOptions(); - if ($request->tools() !== [] && $request->providerTools() !== []) { - throw new PrismException('Use of provider tools with custom tools is not currently supported by Gemini.'); - } - - if ($request->tools() !== [] && ($providerOptions['searchGrounding'] ?? false)) { - throw new PrismException('Use of search grounding with custom tools is not currently supported by Prism.'); - } + $hasSearchGrounding = (bool) ($providerOptions['searchGrounding'] ?? false); + $hasBothToolTypes = $request->tools() !== [] && ($request->providerTools() !== [] || $hasSearchGrounding); $tools = []; @@ -464,14 +459,16 @@ protected function sendRequest(Request $request): Response ], $request->providerTools() ); - } elseif ($providerOptions['searchGrounding'] ?? false) { + } elseif ($hasSearchGrounding) { $tools = [ [ 'google_search' => (object) [], ], ]; - } elseif ($request->tools() !== []) { - $tools = ['function_declarations' => ToolMap::map($request->tools())]; + } + + if ($request->tools() !== []) { + $tools['function_declarations'] = ToolMap::map($request->tools()); } $thinkingConfig = $providerOptions['thinkingConfig'] ?? null; @@ -490,6 +487,8 @@ protected function sendRequest(Request $request): Response ]; } + $toolConfig = ToolConfigMap::map($request->toolChoice(), $hasBothToolTypes); + /** @var Response $response */ $response = $this->client ->withOptions(['stream' => true]) @@ -505,7 +504,7 @@ protected function sendRequest(Request $request): Response 'thinkingConfig' => $thinkingConfig, ]) ?: null, 'tools' => $tools !== [] ? $tools : null, - 'tool_config' => $request->toolChoice() ? ToolChoiceMap::map($request->toolChoice()) : null, + 'tool_config' => $toolConfig, 'safetySettings' => $providerOptions['safetySettings'] ?? null, ]) ); diff --git a/src/Providers/Gemini/Handlers/Structured.php b/src/Providers/Gemini/Handlers/Structured.php index 6d4870f59..cc279c387 100644 --- a/src/Providers/Gemini/Handlers/Structured.php +++ b/src/Providers/Gemini/Handlers/Structured.php @@ -18,7 +18,7 @@ use Prism\Prism\Providers\Gemini\Maps\MessageMap; use Prism\Prism\Providers\Gemini\Maps\SchemaMap; use Prism\Prism\Providers\Gemini\Maps\ToolCallMap; -use Prism\Prism\Providers\Gemini\Maps\ToolChoiceMap; +use Prism\Prism\Providers\Gemini\Maps\ToolConfigMap; use Prism\Prism\Providers\Gemini\Maps\ToolMap; use Prism\Prism\Structured\Request; use Prism\Prism\Structured\Response as StructuredResponse; @@ -79,9 +79,7 @@ public function sendRequest(Request $request): array { $providerOptions = $request->providerOptions(); - if ($request->tools() !== [] && $request->providerTools() !== []) { - throw new PrismException('Use of provider tools with custom tools is not currently supported by Gemini.'); - } + $hasBothToolTypes = $request->tools() !== [] && $request->providerTools() !== []; $tools = []; @@ -97,10 +95,8 @@ public function sendRequest(Request $request): array } if ($request->tools() !== []) { - $tools = [ - [ - 'function_declarations' => ToolMap::map($request->tools()), - ], + $tools[] = [ + 'function_declarations' => ToolMap::map($request->tools()), ]; } @@ -120,6 +116,8 @@ public function sendRequest(Request $request): array ]); } + $toolConfig = ToolConfigMap::map($request->toolChoice(), $hasBothToolTypes); + /** @var Response $response */ $response = $this->client->post( "{$request->model()}:generateContent", @@ -135,7 +133,7 @@ public function sendRequest(Request $request): array 'thinkingConfig' => $thinkingConfig, ]), 'tools' => $tools !== [] ? $tools : null, - 'tool_config' => $request->toolChoice() ? ToolChoiceMap::map($request->toolChoice()) : null, + 'tool_config' => $toolConfig, 'safetySettings' => $providerOptions['safetySettings'] ?? null, ]) ); diff --git a/src/Providers/Gemini/Handlers/Text.php b/src/Providers/Gemini/Handlers/Text.php index fce051b67..c892091f6 100644 --- a/src/Providers/Gemini/Handlers/Text.php +++ b/src/Providers/Gemini/Handlers/Text.php @@ -15,7 +15,7 @@ use Prism\Prism\Providers\Gemini\Maps\FinishReasonMap; use Prism\Prism\Providers\Gemini\Maps\MessageMap; use Prism\Prism\Providers\Gemini\Maps\ToolCallMap; -use Prism\Prism\Providers\Gemini\Maps\ToolChoiceMap; +use Prism\Prism\Providers\Gemini\Maps\ToolConfigMap; use Prism\Prism\Providers\Gemini\Maps\ToolMap; use Prism\Prism\Text\Request; use Prism\Prism\Text\Response as TextResponse; @@ -90,9 +90,7 @@ protected function sendRequest(Request $request): ClientResponse 'thinkingConfig' => $thinkingConfig, ]); - if ($request->tools() !== [] && $request->providerTools() != []) { - throw new PrismException('Use of provider tools with custom tools is not currently supported by Gemini.'); - } + $hasBothToolTypes = $request->tools() !== [] && $request->providerTools() !== []; $tools = []; @@ -109,6 +107,8 @@ protected function sendRequest(Request $request): ClientResponse $tools['function_declarations'] = ToolMap::map($request->tools()); } + $toolConfig = ToolConfigMap::map($request->toolChoice(), $hasBothToolTypes); + /** @var ClientResponse $response */ $response = $this->client->post( "{$request->model()}:generateContent", @@ -117,7 +117,7 @@ protected function sendRequest(Request $request): ClientResponse 'cachedContent' => $providerOptions['cachedContentName'] ?? null, 'generationConfig' => $generationConfig !== [] ? $generationConfig : null, 'tools' => $tools !== [] ? $tools : null, - 'tool_config' => $request->toolChoice() ? ToolChoiceMap::map($request->toolChoice()) : null, + 'tool_config' => $toolConfig, 'safetySettings' => $providerOptions['safetySettings'] ?? null, ]) ); diff --git a/src/Providers/Gemini/Maps/ToolConfigMap.php b/src/Providers/Gemini/Maps/ToolConfigMap.php new file mode 100644 index 000000000..5519b10cc --- /dev/null +++ b/src/Providers/Gemini/Maps/ToolConfigMap.php @@ -0,0 +1,27 @@ +|null + */ + public static function map(string|ToolChoice|null $toolChoice, bool $includeServerSideToolInvocations = false): ?array + { + $config = ToolChoiceMap::map($toolChoice); + + /** @var array|null $config */ + $config = is_array($config) ? $config : null; + + if ($includeServerSideToolInvocations) { + return array_merge($config ?? [], ['includeServerSideToolInvocations' => true]); + } + + return $config; + } +} diff --git a/tests/Providers/Gemini/GeminiTextTest.php b/tests/Providers/Gemini/GeminiTextTest.php index 354d86cb8..d7f155c9d 100644 --- a/tests/Providers/Gemini/GeminiTextTest.php +++ b/tests/Providers/Gemini/GeminiTextTest.php @@ -9,7 +9,6 @@ use Prism\Prism\Enums\Citations\CitationSourceType; use Prism\Prism\Enums\FinishReason; use Prism\Prism\Enums\Provider; -use Prism\Prism\Exceptions\PrismException; use Prism\Prism\Facades\Prism; use Prism\Prism\Schema\ArraySchema; use Prism\Prism\Schema\BooleanSchema; @@ -405,7 +404,7 @@ function (Request $request): bool { }); }); - it('throws an exception if provider tools are enabled with other tools', function (): void { + it('sends includeServerSideToolInvocations when provider tools and custom tools are both present', function (): void { FixtureResponse::fakeResponseSequence('*', 'gemini/generate-text-with-search-grounding'); $tools = [ @@ -417,13 +416,19 @@ function (Request $request): bool { ]; Prism::text() - ->using(Provider::Gemini, 'gemini-2.0-flash') - ->withMaxSteps(3) + ->using(Provider::Gemini, 'gemini-3.1-pro-preview') + ->withMaxSteps(1) ->withTools($tools) ->withProviderTools([new ProviderTool('google_search')]) ->withPrompt('What sport fixtures are on today, and will I need a coat based on today\'s weather forecast?') ->asText(); - })->throws(PrismException::class, 'Use of provider tools with custom tools is not currently supported by Gemini.'); + + Http::assertSent(function (Request $request): bool { + $data = $request->data(); + + return ($data['tool_config']['includeServerSideToolInvocations'] ?? false) === true; + }); + }); it('adds file_search provider tool with options to the request', function (): void { FixtureResponse::fakeResponseSequence('*', 'gemini/generate-text-with-file-search');