From 6be3143f72deceb87a9ab071ddf103f12cbe7d31 Mon Sep 17 00:00:00 2001 From: MrCrayon Date: Sun, 6 Jul 2025 07:48:20 +0200 Subject: [PATCH 1/2] refactor(providers): refactor text handlers Refactor `Text` handlers to extend `TextHandler` abstract class - Use class properties instead of moving around parameters in methods. - Pass `$request` in constructor not as `handle` parameter. - Always create `tempResponse` so we have something consistent to pass to events - Always build request parameters with a static method `buildHttpRequestPayload`, it can be useful to build the request and use it in batched requests. --- src/Images/PendingRequest.php | 2 +- src/Providers/Anthropic/Anthropic.php | 11 +- src/Providers/Anthropic/Handlers/Text.php | 70 +------- .../DeepSeek/Concerns/ValidatesResponses.php | 7 +- src/Providers/DeepSeek/DeepSeek.php | 10 +- src/Providers/DeepSeek/Handlers/Text.php | 129 +++++---------- .../Gemini/Concerns/ValidatesResponse.php | 5 +- src/Providers/Gemini/Gemini.php | 8 +- src/Providers/Gemini/Handlers/Text.php | 143 ++++++----------- .../Groq/Concerns/ValidateResponse.php | 4 +- src/Providers/Groq/Groq.php | 7 +- src/Providers/Groq/Handlers/Structured.php | 8 +- src/Providers/Groq/Handlers/Text.php | 132 ++++++--------- .../Mistral/Concerns/ValidatesResponse.php | 6 +- src/Providers/Mistral/Handlers/Embeddings.php | 6 +- src/Providers/Mistral/Handlers/Structured.php | 6 +- src/Providers/Mistral/Handlers/Text.php | 136 ++++++---------- src/Providers/Mistral/Mistral.php | 11 +- .../Ollama/Concerns/MapsFinishReason.php | 4 + .../Ollama/Concerns/ValidatesResponse.php | 10 +- src/Providers/Ollama/Handlers/Structured.php | 6 +- src/Providers/Ollama/Handlers/Text.php | 140 ++++++---------- src/Providers/Ollama/Ollama.php | 10 +- src/Providers/OpenAI/Concerns/BuildsTools.php | 2 +- .../OpenAI/Concerns/ValidatesResponse.php | 6 +- src/Providers/OpenAI/Handlers/Embeddings.php | 6 +- src/Providers/OpenAI/Handlers/Images.php | 6 +- src/Providers/OpenAI/Handlers/Stream.php | 2 +- src/Providers/OpenAI/Handlers/Structured.php | 6 +- src/Providers/OpenAI/Handlers/Text.php | 142 ++++++----------- src/Providers/OpenAI/OpenAI.php | 10 +- .../Concerns/ValidatesResponses.php | 10 +- src/Providers/OpenRouter/Handlers/Text.php | 129 +++++---------- src/Providers/OpenRouter/OpenRouter.php | 10 +- src/Providers/TextHandler.php | 98 ++++++++++++ .../XAI/Concerns/ValidatesResponses.php | 5 +- src/Providers/XAI/Handlers/Text.php | 150 +++++++----------- src/Providers/XAI/XAI.php | 7 +- 38 files changed, 580 insertions(+), 880 deletions(-) create mode 100644 src/Providers/TextHandler.php diff --git a/src/Images/PendingRequest.php b/src/Images/PendingRequest.php index 8581dca5c..1dba9e505 100644 --- a/src/Images/PendingRequest.php +++ b/src/Images/PendingRequest.php @@ -32,7 +32,7 @@ public function generate(): Response $request = $this->toRequest(); try { - return $this->provider->images($this->toRequest()); + return $this->provider->images($request); } catch (RequestException $e) { $this->provider->handleRequestException($request->model(), $e); } diff --git a/src/Providers/Anthropic/Anthropic.php b/src/Providers/Anthropic/Anthropic.php index 4b66938bf..dab817567 100644 --- a/src/Providers/Anthropic/Anthropic.php +++ b/src/Providers/Anthropic/Anthropic.php @@ -36,15 +36,10 @@ public function __construct( #[\Override] public function text(TextRequest $request): TextResponse { - $handler = new Text( - $this->client( - $request->clientOptions(), - $request->clientRetry() - ), + return (new Text( + $this->client($request->clientOptions(), $request->clientRetry()), $request - ); - - return $handler->handle(); + ))->handle(); } #[\Override] diff --git a/src/Providers/Anthropic/Handlers/Text.php b/src/Providers/Anthropic/Handlers/Text.php index cd61a2981..c0e166770 100644 --- a/src/Providers/Anthropic/Handlers/Text.php +++ b/src/Providers/Anthropic/Handlers/Text.php @@ -4,13 +4,10 @@ namespace Prism\Prism\Providers\Anthropic\Handlers; -use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Prism\Prism\Concerns\CallsTools; use Prism\Prism\Contracts\PrismRequest; -use Prism\Prism\Enums\FinishReason; -use Prism\Prism\Exceptions\PrismException; use Prism\Prism\Providers\Anthropic\Concerns\ExtractsCitations; use Prism\Prism\Providers\Anthropic\Concerns\ExtractsText; use Prism\Prism\Providers\Anthropic\Concerns\ExtractsThinking; @@ -20,57 +17,21 @@ use Prism\Prism\Providers\Anthropic\Maps\MessageMap; use Prism\Prism\Providers\Anthropic\Maps\ToolChoiceMap; use Prism\Prism\Providers\Anthropic\Maps\ToolMap; +use Prism\Prism\Providers\TextHandler; use Prism\Prism\Text\Request as TextRequest; use Prism\Prism\Text\Response; -use Prism\Prism\Text\ResponseBuilder; -use Prism\Prism\Text\Step; -use Prism\Prism\ValueObjects\Messages\AssistantMessage; use Prism\Prism\ValueObjects\Messages\ToolResultMessage; use Prism\Prism\ValueObjects\Meta; use Prism\Prism\ValueObjects\ProviderTool; use Prism\Prism\ValueObjects\ToolCall; -use Prism\Prism\ValueObjects\ToolResult; use Prism\Prism\ValueObjects\Usage; -class Text +class Text extends TextHandler { use CallsTools, ExtractsCitations, ExtractsText, ExtractsThinking, HandlesHttpRequests, ProcessesRateLimits; - protected Response $tempResponse; - - protected ResponseBuilder $responseBuilder; - - public function __construct(protected PendingRequest $client, protected TextRequest $request) - { - $this->responseBuilder = new ResponseBuilder; - } - - public function handle(): Response - { - $this->sendRequest(); - - $this->prepareTempResponse(); - - $responseMessage = new AssistantMessage( - $this->tempResponse->text, - $this->tempResponse->toolCalls, - $this->tempResponse->additionalContent, - ); - - $this->responseBuilder->addResponseMessage($responseMessage); - - $this->request->addMessage($responseMessage); - - return match ($this->tempResponse->finishReason) { - FinishReason::ToolCalls => $this->handleToolCalls(), - FinishReason::Stop, FinishReason::Length => $this->handleStop(), - default => throw new PrismException('Anthropic: unknown finish reason'), - }; - } - /** * @param TextRequest $request - * @return array */ #[\Override] public static function buildHttpRequestPayload(PrismRequest $request): array @@ -113,38 +74,13 @@ protected function handleToolCalls(): Response $this->addStep($toolResults); - if ($this->responseBuilder->steps->count() < $this->request->maxSteps()) { + if ($this->shouldContinue()) { return $this->handle(); } return $this->responseBuilder->toResponse(); } - protected function handleStop(): Response - { - $this->addStep(); - - return $this->responseBuilder->toResponse(); - } - - /** - * @param ToolResult[] $toolResults - */ - protected function addStep(array $toolResults = []): void - { - $this->responseBuilder->addStep(new Step( - text: $this->tempResponse->text, - finishReason: $this->tempResponse->finishReason, - toolCalls: $this->tempResponse->toolCalls, - toolResults: $toolResults, - usage: $this->tempResponse->usage, - meta: $this->tempResponse->meta, - messages: $this->request->messages(), - systemPrompts: $this->request->systemPrompts(), - additionalContent: $this->tempResponse->additionalContent, - )); - } - protected function prepareTempResponse(): void { $data = $this->httpResponse->json(); diff --git a/src/Providers/DeepSeek/Concerns/ValidatesResponses.php b/src/Providers/DeepSeek/Concerns/ValidatesResponses.php index 2389a4032..4e18498f2 100644 --- a/src/Providers/DeepSeek/Concerns/ValidatesResponses.php +++ b/src/Providers/DeepSeek/Concerns/ValidatesResponses.php @@ -8,12 +8,9 @@ trait ValidatesResponses { - /** - * @param array $data - */ - protected function validateResponse(array $data): void + protected function validateResponse(): void { - if ($data === []) { + if ($this->httpResponse->json() === []) { throw PrismException::providerResponseError('DeepSeek Error: Empty response'); } } diff --git a/src/Providers/DeepSeek/DeepSeek.php b/src/Providers/DeepSeek/DeepSeek.php index 8b8b1279d..8c7b6d581 100644 --- a/src/Providers/DeepSeek/DeepSeek.php +++ b/src/Providers/DeepSeek/DeepSeek.php @@ -26,12 +26,10 @@ public function __construct( #[\Override] public function text(TextRequest $request): TextResponse { - $handler = new Text($this->client( - $request->clientOptions(), - $request->clientRetry() - )); - - return $handler->handle($request); + return (new Text( + $this->client($request->clientOptions(), $request->clientRetry()), + $request + ))->handle(); } #[\Override] diff --git a/src/Providers/DeepSeek/Handlers/Text.php b/src/Providers/DeepSeek/Handlers/Text.php index 179a84c86..810a00ad5 100644 --- a/src/Providers/DeepSeek/Handlers/Text.php +++ b/src/Providers/DeepSeek/Handlers/Text.php @@ -4,132 +4,91 @@ namespace Prism\Prism\Providers\DeepSeek\Handlers; -use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Arr; +use Illuminate\Support\Collection; use Prism\Prism\Concerns\CallsTools; -use Prism\Prism\Enums\FinishReason; -use Prism\Prism\Exceptions\PrismException; +use Prism\Prism\Contracts\PrismRequest; use Prism\Prism\Providers\DeepSeek\Concerns\MapsFinishReason; use Prism\Prism\Providers\DeepSeek\Concerns\ValidatesResponses; use Prism\Prism\Providers\DeepSeek\Maps\MessageMap; use Prism\Prism\Providers\DeepSeek\Maps\ToolCallMap; use Prism\Prism\Providers\DeepSeek\Maps\ToolChoiceMap; use Prism\Prism\Providers\DeepSeek\Maps\ToolMap; -use Prism\Prism\Text\Request; +use Prism\Prism\Providers\TextHandler; +use Prism\Prism\Text\Request as TextRequest; use Prism\Prism\Text\Response as TextResponse; -use Prism\Prism\Text\ResponseBuilder; -use Prism\Prism\Text\Step; -use Prism\Prism\ValueObjects\Messages\AssistantMessage; use Prism\Prism\ValueObjects\Messages\ToolResultMessage; use Prism\Prism\ValueObjects\Meta; -use Prism\Prism\ValueObjects\ToolResult; use Prism\Prism\ValueObjects\Usage; -class Text +class Text extends TextHandler { use CallsTools; use MapsFinishReason; use ValidatesResponses; - protected ResponseBuilder $responseBuilder; - - public function __construct(protected PendingRequest $client) - { - $this->responseBuilder = new ResponseBuilder; - } - - public function handle(Request $request): TextResponse + /** + * @param TextRequest $request + */ + #[\Override] + public static function buildHttpRequestPayload(PrismRequest $request): array { - $data = $this->sendRequest($request); - - $this->validateResponse($data); - - $responseMessage = new AssistantMessage( - data_get($data, 'choices.0.message.content') ?? '', - ToolCallMap::map(data_get($data, 'choices.0.message.tool_calls', [])), - [] - ); - - $this->responseBuilder->addResponseMessage($responseMessage); - - $request = $request->addMessage($responseMessage); + if (! $request->is(TextRequest::class)) { + throw new \InvalidArgumentException('Request must be an instance of '.TextRequest::class); + } - return match ($this->mapFinishReason($data)) { - FinishReason::ToolCalls => $this->handleToolCalls($data, $request), - FinishReason::Stop => $this->handleStop($data, $request), - default => throw new PrismException('DeepSeek: unknown finish reason'), - }; + return array_merge([ + 'model' => $request->model(), + 'messages' => (new MessageMap($request->messages(), $request->systemPrompts()))(), + 'max_completion_tokens' => $request->maxTokens(), + ], Arr::whereNotNull([ + 'temperature' => $request->temperature(), + 'top_p' => $request->topP(), + 'tools' => ToolMap::map($request->tools()) ?: null, + 'tool_choice' => ToolChoiceMap::map($request->toolChoice()), + ])); } - /** - * @param array $data - */ - protected function handleToolCalls(array $data, Request $request): TextResponse + protected function handleToolCalls(): TextResponse { + $data = $this->httpResponse->json(); + $toolResults = $this->callTools( - $request->tools(), + $this->request->tools(), ToolCallMap::map(data_get($data, 'choices.0.message.tool_calls', [])) ); - $request = $request->addMessage(new ToolResultMessage($toolResults)); + $this->request->addMessage(new ToolResultMessage($toolResults)); - $this->addStep($data, $request, $toolResults); + $this->addStep($toolResults); - if ($this->shouldContinue($request)) { - return $this->handle($request); + if ($this->shouldContinue()) { + return $this->handle(); } return $this->responseBuilder->toResponse(); } - /** - * @param array $data - */ - protected function handleStop(array $data, Request $request): TextResponse - { - $this->addStep($data, $request); - - return $this->responseBuilder->toResponse(); - } - - protected function shouldContinue(Request $request): bool - { - return $this->responseBuilder->steps->count() < $request->maxSteps(); - } - - /** - * @return array - */ - protected function sendRequest(Request $request): array + protected function sendRequest(): void { - $response = $this->client->post( + $this->httpResponse = $this->client->post( 'chat/completions', - array_merge([ - 'model' => $request->model(), - 'messages' => (new MessageMap($request->messages(), $request->systemPrompts()))(), - 'max_completion_tokens' => $request->maxTokens(), - ], Arr::whereNotNull([ - 'temperature' => $request->temperature(), - 'top_p' => $request->topP(), - 'tools' => ToolMap::map($request->tools()) ?: null, - 'tool_choice' => ToolChoiceMap::map($request->toolChoice()), - ])) + static::buildHttpRequestPayload($this->request), ); - - return $response->json(); } - /** - * @param array $data - * @param array $toolResults - */ - protected function addStep(array $data, Request $request, array $toolResults = []): void + protected function prepareTempResponse(): void { - $this->responseBuilder->addStep(new Step( + $data = $this->httpResponse->json(); + + $this->tempResponse = new TextResponse( + steps: new Collection, + responseMessages: new Collection, + messages: new Collection, text: data_get($data, 'choices.0.message.content') ?? '', finishReason: $this->mapFinishReason($data), toolCalls: ToolCallMap::map(data_get($data, 'choices.0.message.tool_calls', [])), - toolResults: $toolResults, + toolResults: [], usage: new Usage( data_get($data, 'usage.prompt_tokens'), data_get($data, 'usage.completion_tokens'), @@ -138,9 +97,7 @@ protected function addStep(array $data, Request $request, array $toolResults = [ id: data_get($data, 'id'), model: data_get($data, 'model'), ), - messages: $request->messages(), additionalContent: [], - systemPrompts: $request->systemPrompts(), - )); + ); } } diff --git a/src/Providers/Gemini/Concerns/ValidatesResponse.php b/src/Providers/Gemini/Concerns/ValidatesResponse.php index f2326dd9d..e212f4437 100644 --- a/src/Providers/Gemini/Concerns/ValidatesResponse.php +++ b/src/Providers/Gemini/Concerns/ValidatesResponse.php @@ -4,14 +4,13 @@ namespace Prism\Prism\Providers\Gemini\Concerns; -use Illuminate\Http\Client\Response; use Prism\Prism\Exceptions\PrismException; trait ValidatesResponse { - protected function validateResponse(Response $response): void + protected function validateResponse(): void { - $data = $response->json(); + $data = $this->httpResponse->json(); if (! $data || data_get($data, 'error')) { throw PrismException::providerResponseError(vsprintf( diff --git a/src/Providers/Gemini/Gemini.php b/src/Providers/Gemini/Gemini.php index ff7e68ddb..5df64aa36 100644 --- a/src/Providers/Gemini/Gemini.php +++ b/src/Providers/Gemini/Gemini.php @@ -37,12 +37,10 @@ public function __construct( #[\Override] public function text(TextRequest $request): TextResponse { - $handler = new Text( + return (new Text( $this->client($request->clientOptions(), $request->clientRetry()), - $this->apiKey - ); - - return $handler->handle($request); + $request + ))->handle(); } #[\Override] diff --git a/src/Providers/Gemini/Handlers/Text.php b/src/Providers/Gemini/Handlers/Text.php index 5bc145383..84317dc59 100644 --- a/src/Providers/Gemini/Handlers/Text.php +++ b/src/Providers/Gemini/Handlers/Text.php @@ -4,11 +4,10 @@ namespace Prism\Prism\Providers\Gemini\Handlers; -use Illuminate\Http\Client\PendingRequest; -use Illuminate\Http\Client\Response as ClientResponse; use Illuminate\Support\Arr; +use Illuminate\Support\Collection; use Prism\Prism\Concerns\CallsTools; -use Prism\Prism\Enums\FinishReason; +use Prism\Prism\Contracts\PrismRequest; use Prism\Prism\Exceptions\PrismException; use Prism\Prism\Providers\Gemini\Concerns\ExtractSearchGroundings; use Prism\Prism\Providers\Gemini\Concerns\ValidatesResponse; @@ -17,63 +16,29 @@ use Prism\Prism\Providers\Gemini\Maps\ToolCallMap; use Prism\Prism\Providers\Gemini\Maps\ToolChoiceMap; use Prism\Prism\Providers\Gemini\Maps\ToolMap; -use Prism\Prism\Text\Request; +use Prism\Prism\Providers\TextHandler; +use Prism\Prism\Text\Request as TextRequest; +use Prism\Prism\Text\Response; use Prism\Prism\Text\Response as TextResponse; -use Prism\Prism\Text\ResponseBuilder; -use Prism\Prism\Text\Step; -use Prism\Prism\ValueObjects\Messages\AssistantMessage; use Prism\Prism\ValueObjects\Messages\ToolResultMessage; use Prism\Prism\ValueObjects\Meta; use Prism\Prism\ValueObjects\ProviderTool; -use Prism\Prism\ValueObjects\ToolResult; use Prism\Prism\ValueObjects\Usage; -class Text +class Text extends TextHandler { use CallsTools, ExtractSearchGroundings, ValidatesResponse; - protected ResponseBuilder $responseBuilder; - - public function __construct( - protected PendingRequest $client, - #[\SensitiveParameter] protected string $apiKey, - ) { - $this->responseBuilder = new ResponseBuilder; - } - - public function handle(Request $request): TextResponse + /** + * @param TextRequest $request + */ + #[\Override] + public static function buildHttpRequestPayload(PrismRequest $request): array { - $response = $this->sendRequest($request); - - $this->validateResponse($response); - - $data = $response->json(); - - $isToolCall = ! empty(data_get($data, 'candidates.0.content.parts.0.functionCall')); - - $responseMessage = new AssistantMessage( - data_get($data, 'candidates.0.content.parts.0.text') ?? '', - $isToolCall ? ToolCallMap::map(data_get($data, 'candidates.0.content.parts', [])) : [], - ); - - $this->responseBuilder->addResponseMessage($responseMessage); - - $request->addMessage($responseMessage); - - $finishReason = FinishReasonMap::map( - data_get($data, 'candidates.0.finishReason'), - $isToolCall - ); - - return match ($finishReason) { - FinishReason::ToolCalls => $this->handleToolCalls($data, $request), - FinishReason::Stop, FinishReason::Length => $this->handleStop($data, $request, $finishReason), - default => throw new PrismException('Gemini: unhandled finish reason'), - }; - } + if (! $request->is(TextRequest::class)) { + throw new \InvalidArgumentException('Request must be an instance of '.TextRequest::class); + } - protected function sendRequest(Request $request): ClientResponse - { $providerOptions = $request->providerOptions(); $thinkingConfig = Arr::whereNotNull([ @@ -106,68 +71,62 @@ protected function sendRequest(Request $request): ClientResponse $tools['function_declarations'] = ToolMap::map($request->tools()); } - return $this->client->post( - "{$request->model()}:generateContent", - Arr::whereNotNull([ - ...(new MessageMap($request->messages(), $request->systemPrompts()))(), - 'cachedContent' => $providerOptions['cachedContentName'] ?? null, - 'generationConfig' => $generationConfig !== [] ? $generationConfig : null, - 'tools' => $tools !== [] ? $tools : null, - 'tool_config' => $request->toolChoice() ? ToolChoiceMap::map($request->toolChoice()) : null, - 'safetySettings' => $providerOptions['safetySettings'] ?? null, - ]) - ); + return Arr::whereNotNull([ + ...(new MessageMap($request->messages(), $request->systemPrompts()))(), + 'cachedContent' => $providerOptions['cachedContentName'] ?? null, + 'generationConfig' => $generationConfig !== [] ? $generationConfig : null, + 'tools' => $tools !== [] ? $tools : null, + 'tool_config' => $request->toolChoice() ? ToolChoiceMap::map($request->toolChoice()) : null, + 'safetySettings' => $providerOptions['safetySettings'] ?? null, + ]); } - /** - * @param array $data - */ - protected function handleStop(array $data, Request $request, FinishReason $finishReason): TextResponse + protected function sendRequest(): void { - $this->addStep($data, $request, $finishReason); - - return $this->responseBuilder->toResponse(); + $this->httpResponse = $this->client->post( + "{$this->request->model()}:generateContent", + static::buildHttpRequestPayload($this->request), + ); } - /** - * @param array $data - */ - protected function handleToolCalls(array $data, Request $request): TextResponse + protected function handleToolCalls(): TextResponse { + $data = $this->httpResponse->json(); + $toolResults = $this->callTools( - $request->tools(), + $this->request->tools(), ToolCallMap::map(data_get($data, 'candidates.0.content.parts', [])) ); - $request->addMessage(new ToolResultMessage($toolResults)); + $this->request->addMessage(new ToolResultMessage($toolResults)); - $this->addStep($data, $request, FinishReason::ToolCalls, $toolResults); + $this->addStep($toolResults); - if ($this->shouldContinue($request)) { - return $this->handle($request); + if ($this->shouldContinue()) { + return $this->handle(); } return $this->responseBuilder->toResponse(); } - protected function shouldContinue(Request $request): bool + protected function prepareTempResponse(): void { - return $this->responseBuilder->steps->count() < $request->maxSteps(); - } + $data = $this->httpResponse->json(); + $providerOptions = $this->request->providerOptions(); - /** - * @param array $data - * @param ToolResult[] $toolResults - */ - protected function addStep(array $data, Request $request, FinishReason $finishReason, array $toolResults = []): void - { - $providerOptions = $request->providerOptions(); + $isToolCall = ! empty(data_get($data, 'candidates.0.content.parts.0.functionCall')); - $this->responseBuilder->addStep(new Step( + $this->tempResponse = new Response( + steps: new Collection, + responseMessages: new Collection, + messages: new Collection, text: data_get($data, 'candidates.0.content.parts.0.text') ?? '', - finishReason: $finishReason, - toolCalls: $finishReason === FinishReason::ToolCalls ? ToolCallMap::map(data_get($data, 'candidates.0.content.parts', [])) : [], - toolResults: $toolResults, + finishReason: FinishReasonMap::map( + data_get($data, 'candidates.0.finishReason'), + $isToolCall + ), + toolCalls: $isToolCall ? ToolCallMap::map(data_get($data, 'candidates.0.content.parts', [])) : [], + toolResults: [], usage: new Usage( promptTokens: isset($providerOptions['cachedContentName']) ? (data_get($data, 'usageMetadata.promptTokenCount', 0) - data_get($data, 'usageMetadata.cachedContentTokenCount', 0)) @@ -180,11 +139,9 @@ protected function addStep(array $data, Request $request, FinishReason $finishRe id: data_get($data, 'id', ''), model: data_get($data, 'modelVersion'), ), - messages: $request->messages(), - systemPrompts: $request->systemPrompts(), additionalContent: [ ...$this->extractSearchGroundingContent($data), ], - )); + ); } } diff --git a/src/Providers/Groq/Concerns/ValidateResponse.php b/src/Providers/Groq/Concerns/ValidateResponse.php index 1b44217da..31b9e9a33 100644 --- a/src/Providers/Groq/Concerns/ValidateResponse.php +++ b/src/Providers/Groq/Concerns/ValidateResponse.php @@ -10,9 +10,9 @@ trait ValidateResponse { - protected function validateResponse(Response $response): void + protected function validateResponse(): void { - $data = $response->json(); + $data = $this->httpResponse->json(); if (! $data || data_get($data, 'error')) { throw PrismException::providerResponseError(vsprintf( diff --git a/src/Providers/Groq/Groq.php b/src/Providers/Groq/Groq.php index 07787153b..416ae22f7 100644 --- a/src/Providers/Groq/Groq.php +++ b/src/Providers/Groq/Groq.php @@ -36,9 +36,10 @@ public function __construct( #[\Override] public function text(TextRequest $request): TextResponse { - $handler = new Text($this->client($request->clientOptions(), $request->clientRetry())); - - return $handler->handle($request); + return (new Text( + $this->client($request->clientOptions(), $request->clientRetry()), + $request + ))->handle(); } #[\Override] diff --git a/src/Providers/Groq/Handlers/Structured.php b/src/Providers/Groq/Handlers/Structured.php index a7c901c3c..5f0b2a9fd 100644 --- a/src/Providers/Groq/Handlers/Structured.php +++ b/src/Providers/Groq/Handlers/Structured.php @@ -22,6 +22,8 @@ class Structured { use ProcessRateLimits, ValidateResponse; + protected ClientResponse $httpResponse; + protected ResponseBuilder $responseBuilder; public function __construct(protected PendingRequest $client) @@ -35,7 +37,7 @@ public function handle(Request $request): StructuredResponse $response = $this->sendRequest($request); - $this->validateResponse($response); + $this->validateResponse(); $data = $response->json(); @@ -44,7 +46,7 @@ public function handle(Request $request): StructuredResponse protected function sendRequest(Request $request): ClientResponse { - return $this->client->post( + $this->httpResponse = $this->client->post( 'chat/completions', array_merge([ 'model' => $request->model(), @@ -56,6 +58,8 @@ protected function sendRequest(Request $request): ClientResponse 'response_format' => ['type' => 'json_object'], ])) ); + + return $this->httpResponse; } /** diff --git a/src/Providers/Groq/Handlers/Text.php b/src/Providers/Groq/Handlers/Text.php index 193a31e75..a389250ee 100644 --- a/src/Providers/Groq/Handlers/Text.php +++ b/src/Providers/Groq/Handlers/Text.php @@ -4,129 +4,89 @@ namespace Prism\Prism\Providers\Groq\Handlers; -use Illuminate\Http\Client\PendingRequest; -use Illuminate\Http\Client\Response as ClientResponse; use Illuminate\Support\Arr; +use Illuminate\Support\Collection; use Prism\Prism\Concerns\CallsTools; -use Prism\Prism\Enums\FinishReason; -use Prism\Prism\Exceptions\PrismException; +use Prism\Prism\Contracts\PrismRequest; use Prism\Prism\Providers\Groq\Concerns\ProcessRateLimits; use Prism\Prism\Providers\Groq\Concerns\ValidateResponse; use Prism\Prism\Providers\Groq\Maps\FinishReasonMap; use Prism\Prism\Providers\Groq\Maps\MessageMap; use Prism\Prism\Providers\Groq\Maps\ToolChoiceMap; use Prism\Prism\Providers\Groq\Maps\ToolMap; -use Prism\Prism\Text\Request; +use Prism\Prism\Providers\TextHandler; +use Prism\Prism\Text\Request as TextRequest; use Prism\Prism\Text\Response as TextResponse; -use Prism\Prism\Text\ResponseBuilder; -use Prism\Prism\Text\Step; -use Prism\Prism\ValueObjects\Messages\AssistantMessage; use Prism\Prism\ValueObjects\Messages\ToolResultMessage; use Prism\Prism\ValueObjects\Meta; use Prism\Prism\ValueObjects\ToolCall; -use Prism\Prism\ValueObjects\ToolResult; use Prism\Prism\ValueObjects\Usage; -class Text +class Text extends TextHandler { use CallsTools, ProcessRateLimits, ValidateResponse; - protected ResponseBuilder $responseBuilder; - - public function __construct(protected PendingRequest $client) - { - $this->responseBuilder = new ResponseBuilder; - } - - public function handle(Request $request): TextResponse + /** + * @param TextRequest $request + */ + #[\Override] + public static function buildHttpRequestPayload(PrismRequest $request): array { - $response = $this->sendRequest($request); - - $this->validateResponse($response); - - $data = $response->json(); - - $responseMessage = new AssistantMessage( - data_get($data, 'choices.0.message.content') ?? '', - $this->mapToolCalls(data_get($data, 'choices.0.message.tool_calls', []) ?? []), - ); - - $this->responseBuilder->addResponseMessage($responseMessage); - - $request->addMessage($responseMessage); - - $finishReason = FinishReasonMap::map(data_get($data, 'choices.0.finish_reason', '')); + if (! $request->is(TextRequest::class)) { + throw new \InvalidArgumentException('Request must be an instance of '.TextRequest::class); + } - return match ($finishReason) { - FinishReason::ToolCalls => $this->handleToolCalls($data, $request, $response), - FinishReason::Stop, FinishReason::Length => $this->handleStop($data, $request, $response, $finishReason), - default => throw new PrismException('Groq: unhandled finish reason'), - }; + return Arr::whereNotNull([ + 'model' => $request->model(), + 'messages' => (new MessageMap($request->messages(), $request->systemPrompts()))(), + 'max_tokens' => $request->maxTokens(), + 'temperature' => $request->temperature(), + 'top_p' => $request->topP(), + 'tools' => ToolMap::map($request->tools()), + 'tool_choice' => ToolChoiceMap::map($request->toolChoice()), + ]); } - protected function sendRequest(Request $request): ClientResponse + protected function sendRequest(): void { - return $this->client->post( + $this->httpResponse = $this->client->post( 'chat/completions', - Arr::whereNotNull([ - 'model' => $request->model(), - 'messages' => (new MessageMap($request->messages(), $request->systemPrompts()))(), - 'max_tokens' => $request->maxTokens(), - 'temperature' => $request->temperature(), - 'top_p' => $request->topP(), - 'tools' => ToolMap::map($request->tools()), - 'tool_choice' => ToolChoiceMap::map($request->toolChoice()), - ]) + static::buildHttpRequestPayload($this->request), ); } - /** - * @param array $data - */ - protected function handleToolCalls(array $data, Request $request, ClientResponse $clientResponse): TextResponse + protected function handleToolCalls(): TextResponse { + $data = $this->httpResponse->json(); + $toolResults = $this->callTools( - $request->tools(), + $this->request->tools(), $this->mapToolCalls(data_get($data, 'choices.0.message.tool_calls', []) ?? []), ); - $request->addMessage(new ToolResultMessage($toolResults)); + $this->request->addMessage(new ToolResultMessage($toolResults)); - $this->addStep($data, $request, $clientResponse, FinishReason::ToolCalls, $toolResults); + $this->addStep($toolResults); - if ($this->shouldContinue($request)) { - return $this->handle($request); + if ($this->shouldContinue()) { + return $this->handle(); } return $this->responseBuilder->toResponse(); } - /** - * @param array $data - */ - protected function handleStop(array $data, Request $request, ClientResponse $clientResponse, FinishReason $finishReason): TextResponse + protected function prepareTempResponse(): void { - $this->addStep($data, $request, $clientResponse, $finishReason); - - return $this->responseBuilder->toResponse(); - } + $data = $this->httpResponse->json(); - protected function shouldContinue(Request $request): bool - { - return $this->responseBuilder->steps->count() < $request->maxSteps(); - } - - /** - * @param array $data - * @param ToolResult[] $toolResults - */ - protected function addStep(array $data, Request $request, ClientResponse $clientResponse, FinishReason $finishReason, array $toolResults = []): void - { - $this->responseBuilder->addStep(new Step( + $this->tempResponse = new TextResponse( + steps: new Collection, + responseMessages: new Collection, + messages: new Collection, text: data_get($data, 'choices.0.message.content') ?? '', - finishReason: $finishReason, + finishReason: FinishReasonMap::map(data_get($data, 'choices.0.finish_reason', '')), toolCalls: $this->mapToolCalls(data_get($data, 'choices.0.message.tool_calls', []) ?? []), - toolResults: $toolResults, + toolResults: [], usage: new Usage( data_get($data, 'usage.prompt_tokens'), data_get($data, 'usage.completion_tokens'), @@ -134,17 +94,15 @@ protected function addStep(array $data, Request $request, ClientResponse $client meta: new Meta( id: data_get($data, 'id'), model: data_get($data, 'model'), - rateLimits: $this->processRateLimits($clientResponse), + rateLimits: $this->processRateLimits($this->httpResponse), ), - messages: $request->messages(), - systemPrompts: $request->systemPrompts(), additionalContent: [], - )); + ); } /** - * @param array> $toolCalls - * @return array + * @param array $toolCalls + * @return ToolCall[] */ protected function mapToolCalls(array $toolCalls): array { diff --git a/src/Providers/Mistral/Concerns/ValidatesResponse.php b/src/Providers/Mistral/Concerns/ValidatesResponse.php index de991edd3..223aafa5d 100644 --- a/src/Providers/Mistral/Concerns/ValidatesResponse.php +++ b/src/Providers/Mistral/Concerns/ValidatesResponse.php @@ -10,9 +10,11 @@ trait ValidatesResponse { - protected function validateResponse(Response $response): void + protected Response $httpResponse; + + protected function validateResponse(): void { - $data = $response->json(); + $data = $this->httpResponse->json(); if (! $data || data_get($data, 'object') === 'error') { $message = data_get($data, 'message', 'unknown'); diff --git a/src/Providers/Mistral/Handlers/Embeddings.php b/src/Providers/Mistral/Handlers/Embeddings.php index 8b855a4fa..d07d31344 100644 --- a/src/Providers/Mistral/Handlers/Embeddings.php +++ b/src/Providers/Mistral/Handlers/Embeddings.php @@ -24,7 +24,7 @@ public function handle(Request $request): EmbeddingsResponse { $response = $this->sendRequest($request); - $this->validateResponse($response); + $this->validateResponse(); $data = $response->json(); @@ -41,12 +41,14 @@ public function handle(Request $request): EmbeddingsResponse protected function sendRequest(Request $request): Response { - return $this->client->post( + $this->httpResponse = $this->client->post( 'embeddings', [ 'model' => $request->model(), 'input' => $request->inputs(), ] ); + + return $this->httpResponse; } } diff --git a/src/Providers/Mistral/Handlers/Structured.php b/src/Providers/Mistral/Handlers/Structured.php index bd453ce88..919e9aa2d 100644 --- a/src/Providers/Mistral/Handlers/Structured.php +++ b/src/Providers/Mistral/Handlers/Structured.php @@ -40,7 +40,7 @@ public function handle(Request $request): StructuredResponse $response = $this->sendRequest($request); - $this->validateResponse($response); + $this->validateResponse(); $data = $response->json(); @@ -49,7 +49,7 @@ public function handle(Request $request): StructuredResponse protected function sendRequest(Request $request): ClientResponse { - return $this->client->post( + $this->httpResponse = $this->client->post( 'chat/completions', array_merge([ 'model' => $request->model(), @@ -61,6 +61,8 @@ protected function sendRequest(Request $request): ClientResponse 'response_format' => ['type' => 'json_object'], ])) ); + + return $this->httpResponse; } /** diff --git a/src/Providers/Mistral/Handlers/Text.php b/src/Providers/Mistral/Handlers/Text.php index cb4aea11d..c35fb0716 100644 --- a/src/Providers/Mistral/Handlers/Text.php +++ b/src/Providers/Mistral/Handlers/Text.php @@ -4,116 +4,85 @@ namespace Prism\Prism\Providers\Mistral\Handlers; -use Illuminate\Http\Client\PendingRequest; -use Illuminate\Http\Client\Response as ClientResponse; use Illuminate\Support\Arr; +use Illuminate\Support\Collection; use Prism\Prism\Concerns\CallsTools; -use Prism\Prism\Enums\FinishReason; -use Prism\Prism\Exceptions\PrismException; +use Prism\Prism\Contracts\PrismRequest; use Prism\Prism\Providers\Mistral\Concerns\MapsFinishReason; use Prism\Prism\Providers\Mistral\Concerns\ProcessRateLimits; use Prism\Prism\Providers\Mistral\Concerns\ValidatesResponse; use Prism\Prism\Providers\Mistral\Maps\MessageMap; use Prism\Prism\Providers\Mistral\Maps\ToolChoiceMap; use Prism\Prism\Providers\Mistral\Maps\ToolMap; -use Prism\Prism\Text\Request; +use Prism\Prism\Providers\TextHandler; +use Prism\Prism\Text\Request as TextRequest; use Prism\Prism\Text\Response; -use Prism\Prism\Text\ResponseBuilder; -use Prism\Prism\Text\Step; -use Prism\Prism\ValueObjects\Messages\AssistantMessage; use Prism\Prism\ValueObjects\Messages\ToolResultMessage; use Prism\Prism\ValueObjects\Meta; use Prism\Prism\ValueObjects\ToolCall; -use Prism\Prism\ValueObjects\ToolResult; use Prism\Prism\ValueObjects\Usage; -class Text +class Text extends TextHandler { use CallsTools; use MapsFinishReason; use ProcessRateLimits; use ValidatesResponse; - protected ResponseBuilder $responseBuilder; - - public function __construct(protected PendingRequest $client) - { - $this->responseBuilder = new ResponseBuilder; - } - - public function handle(Request $request): Response + /** + * @param TextRequest $request + */ + #[\Override] + public static function buildHttpRequestPayload(PrismRequest $request): array { - $response = $this->sendRequest($request); - - $this->validateResponse($response); - - $data = $response->json(); - - $responseMessage = new AssistantMessage( - data_get($data, 'choices.0.message.content') ?? '', - $this->mapToolCalls(data_get($data, 'choices.0.message.tool_calls', [])), - ); - - $this->responseBuilder->addResponseMessage($responseMessage); - - $request->addMessage($responseMessage); + if (! $request->is(TextRequest::class)) { + throw new \InvalidArgumentException('Request must be an instance of '.TextRequest::class); + } - return match ($this->mapFinishReason($data)) { - FinishReason::ToolCalls => $this->handleToolCalls($data, $request, $response), - FinishReason::Stop => $this->handleStop($data, $request, $response), - default => throw PrismException::providerResponseError('Invalid tool choice'), - }; + return array_merge([ + 'model' => $request->model(), + 'messages' => (new MessageMap($request->messages(), $request->systemPrompts()))(), + 'max_tokens' => $request->maxTokens(), + ], Arr::whereNotNull([ + 'temperature' => $request->temperature(), + 'top_p' => $request->topP(), + 'tools' => ToolMap::map($request->tools()), + 'tool_choice' => ToolChoiceMap::map($request->toolChoice()), + ])); } - /** - * @param array $data - */ - protected function handleToolCalls(array $data, Request $request, ClientResponse $clientResponse): Response + protected function handleToolCalls(): Response { + $data = $this->httpResponse->json(); + $toolResults = $this->callTools( - $request->tools(), + $this->request->tools(), $this->mapToolCalls(data_get($data, 'choices.0.message.tool_calls', [])), ); - $request->addMessage(new ToolResultMessage($toolResults)); + $this->request->addMessage(new ToolResultMessage($toolResults)); - $this->addStep($data, $request, $clientResponse, $toolResults); + $this->addStep($toolResults); - if ($this->shouldContinue($request)) { - return $this->handle($request); + if ($this->shouldContinue()) { + return $this->handle(); } return $this->responseBuilder->toResponse(); } - /** - * @param array $data - */ - protected function handleStop(array $data, Request $request, ClientResponse $clientResponse): Response - { - $this->addStep($data, $request, $clientResponse); - - return $this->responseBuilder->toResponse(); - } - - protected function shouldContinue(Request $request): bool + protected function prepareTempResponse(): void { - if ($request->maxSteps() === 0) { - return true; - } + $data = $this->httpResponse->json(); - return $this->responseBuilder->steps->count() < $request->maxSteps(); - } - - /** - * @param array $data - * @param ToolResult[] $toolResults - */ - protected function addStep(array $data, Request $request, ClientResponse $clientResponse, array $toolResults = []): void - { - $this->responseBuilder->addStep(new Step( + $this->tempResponse = new Response( + steps: new Collection, + responseMessages: new Collection, + messages: new Collection, text: data_get($data, 'choices.0.message.content') ?? '', finishReason: $this->mapFinishReason($data), + toolCalls: $this->mapToolCalls(data_get($data, 'choices.0.message.tool_calls', [])), + toolResults: [], usage: new Usage( data_get($data, 'usage.prompt_tokens'), data_get($data, 'usage.completion_tokens'), @@ -121,36 +90,23 @@ protected function addStep(array $data, Request $request, ClientResponse $client meta: new Meta( id: data_get($data, 'id'), model: data_get($data, 'model'), - rateLimits: $this->processRateLimits($clientResponse), + rateLimits: $this->processRateLimits($this->httpResponse), ), - messages: $request->messages(), - toolResults: $toolResults, - toolCalls: $this->mapToolCalls(data_get($data, 'choices.0.message.tool_calls', [])), additionalContent: [], - systemPrompts: $request->systemPrompts(), - )); + ); } - protected function sendRequest(Request $request): ClientResponse + protected function sendRequest(): void { - return $this->client->post( + $this->httpResponse = $this->client->post( 'chat/completions', - array_merge([ - 'model' => $request->model(), - 'messages' => (new MessageMap($request->messages(), $request->systemPrompts()))(), - 'max_tokens' => $request->maxTokens(), - ], Arr::whereNotNull([ - 'temperature' => $request->temperature(), - 'top_p' => $request->topP(), - 'tools' => ToolMap::map($request->tools()), - 'tool_choice' => ToolChoiceMap::map($request->toolChoice()), - ])) + static::buildHttpRequestPayload($this->request), ); } /** - * @param array|null $toolCalls - * @return array + * @param array $toolCalls + * @return ToolCall[] */ protected function mapToolCalls(?array $toolCalls): array { diff --git a/src/Providers/Mistral/Mistral.php b/src/Providers/Mistral/Mistral.php index 72cd9ad2c..c9446f1fb 100644 --- a/src/Providers/Mistral/Mistral.php +++ b/src/Providers/Mistral/Mistral.php @@ -41,13 +41,10 @@ public function __construct( #[\Override] public function text(TextRequest $request): TextResponse { - $handler = new Text( - $this->client( - $request->clientOptions(), - $request->clientRetry() - )); - - return $handler->handle($request); + return (new Text( + $this->client($request->clientOptions(), $request->clientRetry()), + $request + ))->handle(); } #[\Override] diff --git a/src/Providers/Ollama/Concerns/MapsFinishReason.php b/src/Providers/Ollama/Concerns/MapsFinishReason.php index dffac3b5b..308ee8ad5 100644 --- a/src/Providers/Ollama/Concerns/MapsFinishReason.php +++ b/src/Providers/Ollama/Concerns/MapsFinishReason.php @@ -14,6 +14,10 @@ trait MapsFinishReason */ protected function mapFinishReason(array $data): FinishReason { + if (! empty(data_get($data, 'message.tool_calls'))) { + return FinishReason::ToolCalls; + } + return FinishReasonMap::map(data_get($data, 'done_reason', '')); } } diff --git a/src/Providers/Ollama/Concerns/ValidatesResponse.php b/src/Providers/Ollama/Concerns/ValidatesResponse.php index fa46b70a0..115291766 100644 --- a/src/Providers/Ollama/Concerns/ValidatesResponse.php +++ b/src/Providers/Ollama/Concerns/ValidatesResponse.php @@ -4,15 +4,17 @@ namespace Prism\Prism\Providers\Ollama\Concerns; +use Illuminate\Http\Client\Response; use Prism\Prism\Exceptions\PrismException; trait ValidatesResponse { - /** - * @param array $data - */ - protected function validateResponse(array $data): void + protected Response $httpResponse; + + protected function validateResponse(): void { + $data = $this->httpResponse->json(); + if (! $data || data_get($data, 'error')) { throw PrismException::providerResponseError(sprintf( 'Ollama Error: %s', diff --git a/src/Providers/Ollama/Handlers/Structured.php b/src/Providers/Ollama/Handlers/Structured.php index 0b95cf7bd..f5c5faddd 100644 --- a/src/Providers/Ollama/Handlers/Structured.php +++ b/src/Providers/Ollama/Handlers/Structured.php @@ -34,7 +34,7 @@ public function handle(Request $request): Response { $data = $this->sendRequest($request); - $this->validateResponse($data); + $this->validateResponse(); $responseMessage = new AssistantMessage( data_get($data, 'message.content') ?? '', @@ -80,7 +80,7 @@ protected function sendRequest(Request $request): array throw new PrismException('Ollama does not support multiple system prompts using withSystemPrompt / withSystemPrompts. However, you can provide additional system prompts by including SystemMessages in with withMessages.'); } - $response = $this->client->post('api/chat', [ + $this->httpResponse = $this->client->post('api/chat', [ 'model' => $request->model(), 'system' => data_get($request->systemPrompts(), '0.content', ''), 'messages' => (new MessageMap($request->messages()))->map(), @@ -93,6 +93,6 @@ protected function sendRequest(Request $request): array ], $request->providerOptions())), ]); - return $response->json(); + return $this->httpResponse->json(); } } diff --git a/src/Providers/Ollama/Handlers/Text.php b/src/Providers/Ollama/Handlers/Text.php index f4777a220..4531a15f2 100644 --- a/src/Providers/Ollama/Handlers/Text.php +++ b/src/Providers/Ollama/Handlers/Text.php @@ -4,151 +4,107 @@ namespace Prism\Prism\Providers\Ollama\Handlers; -use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Arr; +use Illuminate\Support\Collection; use Prism\Prism\Concerns\CallsTools; -use Prism\Prism\Enums\FinishReason; +use Prism\Prism\Contracts\PrismRequest; use Prism\Prism\Exceptions\PrismException; use Prism\Prism\Providers\Ollama\Concerns\MapsFinishReason; use Prism\Prism\Providers\Ollama\Concerns\ValidatesResponse; use Prism\Prism\Providers\Ollama\Maps\MessageMap; use Prism\Prism\Providers\Ollama\Maps\ToolMap; -use Prism\Prism\Text\Request; -use Prism\Prism\Text\Response; -use Prism\Prism\Text\ResponseBuilder; -use Prism\Prism\Text\Step; -use Prism\Prism\ValueObjects\Messages\AssistantMessage; +use Prism\Prism\Providers\TextHandler; +use Prism\Prism\Text\Request as TextRequest; +use Prism\Prism\Text\Response as TextResponse; use Prism\Prism\ValueObjects\Messages\ToolResultMessage; use Prism\Prism\ValueObjects\Meta; use Prism\Prism\ValueObjects\ToolCall; -use Prism\Prism\ValueObjects\ToolResult; use Prism\Prism\ValueObjects\Usage; -class Text +class Text extends TextHandler { use CallsTools; use MapsFinishReason; use ValidatesResponse; - protected ResponseBuilder $responseBuilder; - - public function __construct(protected PendingRequest $client) - { - $this->responseBuilder = new ResponseBuilder; - } - - public function handle(Request $request): Response + /** + * @param TextRequest $request + */ + #[\Override] + public static function buildHttpRequestPayload(PrismRequest $request): array { - $data = $this->sendRequest($request); - - $this->validateResponse($data); - - $responseMessage = new AssistantMessage( - data_get($data, 'message.content') ?? '', - $this->mapToolCalls(data_get($data, 'message.tool_calls', [])), - ); - - $this->responseBuilder->addResponseMessage($responseMessage); - - $request->addMessage($responseMessage); - - // Check for tool calls first, regardless of finish reason - if (! empty(data_get($data, 'message.tool_calls'))) { - return $this->handleToolCalls($data, $request); + if (! $request->is(TextRequest::class)) { + throw new \InvalidArgumentException('Request must be an instance of '.TextRequest::class); } - return match ($this->mapFinishReason($data)) { - FinishReason::Stop => $this->handleStop($data, $request), - default => throw new PrismException('Ollama: unknown finish reason'), - }; + return [ + 'model' => $request->model(), + 'system' => data_get($request->systemPrompts(), '0.content', ''), + 'messages' => (new MessageMap($request->messages()))->map(), + 'tools' => ToolMap::map($request->tools()), + 'stream' => false, + 'options' => Arr::whereNotNull(array_merge([ + 'temperature' => $request->temperature(), + 'num_predict' => $request->maxTokens() ?? 2048, + 'top_p' => $request->topP(), + ], $request->providerOptions())), + ]; } - /** - * @return array - */ - protected function sendRequest(Request $request): array + protected function sendRequest(): void { - if (count($request->systemPrompts()) > 1) { + if (count($this->request->systemPrompts()) > 1) { throw new PrismException('Ollama does not support multiple system prompts using withSystemPrompt / withSystemPrompts. However, you can provide additional system prompts by including SystemMessages in with withMessages.'); } - $response = $this - ->client - ->post('api/chat', [ - 'model' => $request->model(), - 'system' => data_get($request->systemPrompts(), '0.content', ''), - 'messages' => (new MessageMap($request->messages()))->map(), - 'tools' => ToolMap::map($request->tools()), - 'stream' => false, - 'options' => Arr::whereNotNull(array_merge([ - 'temperature' => $request->temperature(), - 'num_predict' => $request->maxTokens() ?? 2048, - 'top_p' => $request->topP(), - ], $request->providerOptions())), - ]); - - return $response->json(); + $this->httpResponse = $this->client->post( + 'api/chat', + static::buildHttpRequestPayload($this->request) + ); } - /** - * @param array $data - */ - protected function handleToolCalls(array $data, Request $request): Response + protected function handleToolCalls(): TextResponse { + $data = $this->httpResponse->json(); + $toolResults = $this->callTools( - $request->tools(), + $this->request->tools(), $this->mapToolCalls(data_get($data, 'message.tool_calls', [])), ); - $request->addMessage(new ToolResultMessage($toolResults)); + $this->request->addMessage(new ToolResultMessage($toolResults)); - $this->addStep($data, $request, $toolResults); + $this->addStep($toolResults); - if ($this->shouldContinue($request)) { - return $this->handle($request); + if ($this->shouldContinue()) { + return $this->handle(); } return $this->responseBuilder->toResponse(); } - /** - * @param array $data - */ - protected function handleStop(array $data, Request $request): Response + protected function prepareTempResponse(): void { - $this->addStep($data, $request); - - return $this->responseBuilder->toResponse(); - } + $data = $this->httpResponse->json(); - protected function shouldContinue(Request $request): bool - { - return $this->responseBuilder->steps->count() < $request->maxSteps(); - } - - /** - * @param array $data - * @param ToolResult[] $toolResults - */ - protected function addStep(array $data, Request $request, array $toolResults = []): void - { - $this->responseBuilder->addStep(new Step( + $this->tempResponse = new TextResponse( + steps: new Collection, + responseMessages: new Collection, + messages: new Collection, text: data_get($data, 'message.content') ?? '', finishReason: $this->mapFinishReason($data), toolCalls: $this->mapToolCalls(data_get($data, 'message.tool_calls', []) ?? []), - toolResults: $toolResults, + toolResults: [], usage: new Usage( data_get($data, 'prompt_eval_count', 0), data_get($data, 'eval_count', 0), ), meta: new Meta( id: '', - model: $request->model(), + model: $this->request->model(), ), - messages: $request->messages(), additionalContent: [], - systemPrompts: $request->systemPrompts(), - )); + ); } /** diff --git a/src/Providers/Ollama/Ollama.php b/src/Providers/Ollama/Ollama.php index a8ff47c34..806d8af7e 100644 --- a/src/Providers/Ollama/Ollama.php +++ b/src/Providers/Ollama/Ollama.php @@ -31,12 +31,10 @@ public function __construct( #[\Override] public function text(TextRequest $request): TextResponse { - $handler = new Text($this->client( - $request->clientOptions(), - $request->clientRetry() - )); - - return $handler->handle($request); + return (new Text( + $this->client($request->clientOptions(), $request->clientRetry()), + $request + ))->handle(); } #[\Override] diff --git a/src/Providers/OpenAI/Concerns/BuildsTools.php b/src/Providers/OpenAI/Concerns/BuildsTools.php index be170c865..085e00e06 100644 --- a/src/Providers/OpenAI/Concerns/BuildsTools.php +++ b/src/Providers/OpenAI/Concerns/BuildsTools.php @@ -11,7 +11,7 @@ trait BuildsTools /** * @return array */ - protected function buildTools(Request $request): array + protected static function buildTools(Request $request): array { $tools = ToolMap::map($request->tools()); diff --git a/src/Providers/OpenAI/Concerns/ValidatesResponse.php b/src/Providers/OpenAI/Concerns/ValidatesResponse.php index a97f22842..d64fb207b 100644 --- a/src/Providers/OpenAI/Concerns/ValidatesResponse.php +++ b/src/Providers/OpenAI/Concerns/ValidatesResponse.php @@ -9,9 +9,11 @@ trait ValidatesResponse { - protected function validateResponse(Response $response): void + protected Response $httpResponse; + + protected function validateResponse(): void { - $data = $response->json(); + $data = $this->httpResponse->json(); if (! $data || data_get($data, 'error')) { throw PrismException::providerResponseError(vsprintf( diff --git a/src/Providers/OpenAI/Handlers/Embeddings.php b/src/Providers/OpenAI/Handlers/Embeddings.php index 7c2c99cbe..15042d01b 100644 --- a/src/Providers/OpenAI/Handlers/Embeddings.php +++ b/src/Providers/OpenAI/Handlers/Embeddings.php @@ -23,7 +23,7 @@ public function handle(Request $request): EmbeddingsResponse { $response = $this->sendRequest($request); - $this->validateResponse($response); + $this->validateResponse(); $data = $response->json(); @@ -39,12 +39,14 @@ public function handle(Request $request): EmbeddingsResponse protected function sendRequest(Request $request): Response { - return $this->client->post( + $this->httpResponse = $this->client->post( 'embeddings', [ 'model' => $request->model(), 'input' => $request->inputs(), ] ); + + return $this->httpResponse; } } diff --git a/src/Providers/OpenAI/Handlers/Images.php b/src/Providers/OpenAI/Handlers/Images.php index b0a762425..91e3c03af 100644 --- a/src/Providers/OpenAI/Handlers/Images.php +++ b/src/Providers/OpenAI/Handlers/Images.php @@ -25,7 +25,7 @@ public function handle(Request $request): Response { $response = $this->sendRequest($request); - $this->validateResponse($response); + $this->validateResponse(); $data = $response->json(); @@ -48,7 +48,9 @@ public function handle(Request $request): Response protected function sendRequest(Request $request): ClientResponse { - return $this->client->post('images/generations', ImageRequestMap::map($request)); + $this->httpResponse = $this->client->post('images/generations', ImageRequestMap::map($request)); + + return $this->httpResponse; } /** diff --git a/src/Providers/OpenAI/Handlers/Stream.php b/src/Providers/OpenAI/Handlers/Stream.php index 731ec4b27..3f41e87f7 100644 --- a/src/Providers/OpenAI/Handlers/Stream.php +++ b/src/Providers/OpenAI/Handlers/Stream.php @@ -374,7 +374,7 @@ protected function sendRequest(Request $request): Response 'temperature' => $request->temperature(), 'top_p' => $request->topP(), 'metadata' => $request->providerOptions('metadata'), - 'tools' => $this->buildTools($request), + 'tools' => static::buildTools($request), 'tool_choice' => ToolChoiceMap::map($request->toolChoice()), 'previous_response_id' => $request->providerOptions('previous_response_id'), 'truncation' => $request->providerOptions('truncation'), diff --git a/src/Providers/OpenAI/Handlers/Structured.php b/src/Providers/OpenAI/Handlers/Structured.php index 7d6af105f..e094d263a 100644 --- a/src/Providers/OpenAI/Handlers/Structured.php +++ b/src/Providers/OpenAI/Handlers/Structured.php @@ -41,7 +41,7 @@ public function handle(Request $request): StructuredResponse }; - $this->validateResponse($response); + $this->validateResponse(); $data = $response->json(); @@ -89,7 +89,7 @@ protected function addStep(array $data, Request $request, ClientResponse $client */ protected function sendRequest(Request $request, array $responseFormat): ClientResponse { - return $this->client->post( + $this->httpResponse = $this->client->post( 'responses', array_merge([ 'model' => $request->model(), @@ -106,6 +106,8 @@ protected function sendRequest(Request $request, array $responseFormat): ClientR ], ])) ); + + return $this->httpResponse; } protected function handleAutoMode(Request $request): ClientResponse diff --git a/src/Providers/OpenAI/Handlers/Text.php b/src/Providers/OpenAI/Handlers/Text.php index 8b089f896..4db675592 100644 --- a/src/Providers/OpenAI/Handlers/Text.php +++ b/src/Providers/OpenAI/Handlers/Text.php @@ -4,136 +4,98 @@ namespace Prism\Prism\Providers\OpenAI\Handlers; -use Illuminate\Http\Client\PendingRequest; -use Illuminate\Http\Client\Response as ClientResponse; use Illuminate\Support\Arr; +use Illuminate\Support\Collection; use Prism\Prism\Concerns\CallsTools; -use Prism\Prism\Enums\FinishReason; -use Prism\Prism\Exceptions\PrismException; +use Prism\Prism\Contracts\PrismRequest; use Prism\Prism\Providers\OpenAI\Concerns\BuildsTools; use Prism\Prism\Providers\OpenAI\Concerns\MapsFinishReason; use Prism\Prism\Providers\OpenAI\Concerns\ValidatesResponse; use Prism\Prism\Providers\OpenAI\Maps\MessageMap; use Prism\Prism\Providers\OpenAI\Maps\ToolCallMap; use Prism\Prism\Providers\OpenAI\Maps\ToolChoiceMap; -use Prism\Prism\Text\Request; -use Prism\Prism\Text\Response; -use Prism\Prism\Text\ResponseBuilder; -use Prism\Prism\Text\Step; -use Prism\Prism\ValueObjects\Messages\AssistantMessage; +use Prism\Prism\Providers\TextHandler; +use Prism\Prism\Text\Request as TextRequest; +use Prism\Prism\Text\Response as TextResponse; use Prism\Prism\ValueObjects\Messages\ToolResultMessage; use Prism\Prism\ValueObjects\Meta; -use Prism\Prism\ValueObjects\ToolResult; use Prism\Prism\ValueObjects\Usage; -class Text +class Text extends TextHandler { use BuildsTools; use CallsTools; use MapsFinishReason; use ValidatesResponse; - protected ResponseBuilder $responseBuilder; - - public function __construct(protected PendingRequest $client) - { - $this->responseBuilder = new ResponseBuilder; - } - - public function handle(Request $request): Response + /** + * @param TextRequest $request + */ + #[\Override] + public static function buildHttpRequestPayload(PrismRequest $request): array { - $response = $this->sendRequest($request); - - $this->validateResponse($response); - - $data = $response->json(); - - $responseMessage = new AssistantMessage( - data_get($data, 'output.{last}.content.0.text') ?? '', - ToolCallMap::map( - array_filter(data_get($data, 'output', []), fn (array $output): bool => $output['type'] === 'function_call'), - array_filter(data_get($data, 'output', []), fn (array $output): bool => $output['type'] === 'reasoning'), - ), - ); - - $this->responseBuilder->addResponseMessage($responseMessage); - - $request->addMessage($responseMessage); + if (! $request->is(TextRequest::class)) { + throw new \InvalidArgumentException('Request must be an instance of '.TextRequest::class); + } - return match ($this->mapFinishReason($data)) { - FinishReason::ToolCalls => $this->handleToolCalls($data, $request, $response), - FinishReason::Stop => $this->handleStop($data, $request, $response), - default => throw new PrismException('OpenAI: unknown finish reason'), - }; + return array_merge([ + 'model' => $request->model(), + 'input' => (new MessageMap($request->messages(), $request->systemPrompts()))(), + 'max_output_tokens' => $request->maxTokens(), + ], Arr::whereNotNull([ + 'temperature' => $request->temperature(), + 'top_p' => $request->topP(), + 'metadata' => $request->providerOptions('metadata'), + 'tools' => static::buildTools($request), + 'tool_choice' => ToolChoiceMap::map($request->toolChoice()), + 'previous_response_id' => $request->providerOptions('previous_response_id'), + 'truncation' => $request->providerOptions('truncation'), + ])); } - /** - * @param array $data - */ - protected function handleToolCalls(array $data, Request $request, ClientResponse $clientResponse): Response + protected function handleToolCalls(): TextResponse { + $data = $this->httpResponse->json(); + $toolResults = $this->callTools( - $request->tools(), + $this->request->tools(), ToolCallMap::map(array_filter(data_get($data, 'output', []), fn (array $output): bool => $output['type'] === 'function_call')), ); - $request->addMessage(new ToolResultMessage($toolResults)); + $this->request->addMessage(new ToolResultMessage($toolResults)); - $this->addStep($data, $request, $clientResponse, $toolResults); + $this->addStep($toolResults); - if ($this->shouldContinue($request)) { - return $this->handle($request); + if ($this->shouldContinue()) { + return $this->handle(); } return $this->responseBuilder->toResponse(); } - /** - * @param array $data - */ - protected function handleStop(array $data, Request $request, ClientResponse $clientResponse): Response - { - $this->addStep($data, $request, $clientResponse); - - return $this->responseBuilder->toResponse(); - } - - protected function shouldContinue(Request $request): bool - { - return $this->responseBuilder->steps->count() < $request->maxSteps(); - } - - protected function sendRequest(Request $request): ClientResponse + protected function sendRequest(): void { - return $this->client->post( + $this->httpResponse = $this->client->post( 'responses', - array_merge([ - 'model' => $request->model(), - 'input' => (new MessageMap($request->messages(), $request->systemPrompts()))(), - 'max_output_tokens' => $request->maxTokens(), - ], Arr::whereNotNull([ - 'temperature' => $request->temperature(), - 'top_p' => $request->topP(), - 'metadata' => $request->providerOptions('metadata'), - 'tools' => $this->buildTools($request), - 'tool_choice' => ToolChoiceMap::map($request->toolChoice()), - 'previous_response_id' => $request->providerOptions('previous_response_id'), - 'truncation' => $request->providerOptions('truncation'), - ])) + static::buildHttpRequestPayload($this->request) ); } - /** - * @param array $data - * @param ToolResult[] $toolResults - */ - protected function addStep(array $data, Request $request, ClientResponse $clientResponse, array $toolResults = []): void + protected function prepareTempResponse(): void { - $this->responseBuilder->addStep(new Step( + $data = $this->httpResponse->json(); + + $this->tempResponse = new TextResponse( + steps: new Collection, + responseMessages: new Collection, + messages: new Collection, text: data_get($data, 'output.{last}.content.0.text') ?? '', finishReason: $this->mapFinishReason($data), - toolCalls: ToolCallMap::map(array_filter(data_get($data, 'output', []), fn (array $output): bool => $output['type'] === 'function_call')), - toolResults: $toolResults, + toolCalls: ToolCallMap::map( + array_filter(data_get($data, 'output', []), fn (array $output): bool => $output['type'] === 'function_call'), + array_filter(data_get($data, 'output', []), fn (array $output): bool => $output['type'] === 'reasoning'), + ), + toolResults: [], usage: new Usage( promptTokens: data_get($data, 'usage.input_tokens', 0) - data_get($data, 'usage.input_tokens_details.cached_tokens', 0), completionTokens: data_get($data, 'usage.output_tokens'), @@ -144,9 +106,7 @@ protected function addStep(array $data, Request $request, ClientResponse $client id: data_get($data, 'id'), model: data_get($data, 'model'), ), - messages: $request->messages(), additionalContent: [], - systemPrompts: $request->systemPrompts(), - )); + ); } } diff --git a/src/Providers/OpenAI/OpenAI.php b/src/Providers/OpenAI/OpenAI.php index d29472317..a569a73ba 100644 --- a/src/Providers/OpenAI/OpenAI.php +++ b/src/Providers/OpenAI/OpenAI.php @@ -42,12 +42,10 @@ public function __construct( #[\Override] public function text(TextRequest $request): TextResponse { - $handler = new Text($this->client( - $request->clientOptions(), - $request->clientRetry() - )); - - return $handler->handle($request); + return (new Text( + $this->client($request->clientOptions(), $request->clientRetry()), + $request + ))->handle(); } #[\Override] diff --git a/src/Providers/OpenRouter/Concerns/ValidatesResponses.php b/src/Providers/OpenRouter/Concerns/ValidatesResponses.php index f330e4323..ab5cbbc68 100644 --- a/src/Providers/OpenRouter/Concerns/ValidatesResponses.php +++ b/src/Providers/OpenRouter/Concerns/ValidatesResponses.php @@ -4,15 +4,17 @@ namespace Prism\Prism\Providers\OpenRouter\Concerns; +use Illuminate\Http\Client\Response; use Prism\Prism\Exceptions\PrismException; trait ValidatesResponses { - /** - * @param array $data - */ - protected function validateResponse(array $data): void + protected Response $httpResponse; + + protected function validateResponse(): void { + $data = $this->httpResponse->json(); + if ($data === []) { throw PrismException::providerResponseError('OpenRouter Error: Empty response'); } diff --git a/src/Providers/OpenRouter/Handlers/Text.php b/src/Providers/OpenRouter/Handlers/Text.php index dd2ff5145..3fc325a2e 100644 --- a/src/Providers/OpenRouter/Handlers/Text.php +++ b/src/Providers/OpenRouter/Handlers/Text.php @@ -4,132 +4,91 @@ namespace Prism\Prism\Providers\OpenRouter\Handlers; -use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Arr; +use Illuminate\Support\Collection; use Prism\Prism\Concerns\CallsTools; -use Prism\Prism\Enums\FinishReason; -use Prism\Prism\Exceptions\PrismException; +use Prism\Prism\Contracts\PrismRequest; use Prism\Prism\Providers\OpenRouter\Concerns\MapsFinishReason; use Prism\Prism\Providers\OpenRouter\Concerns\ValidatesResponses; use Prism\Prism\Providers\OpenRouter\Maps\MessageMap; use Prism\Prism\Providers\OpenRouter\Maps\ToolCallMap; use Prism\Prism\Providers\OpenRouter\Maps\ToolChoiceMap; use Prism\Prism\Providers\OpenRouter\Maps\ToolMap; -use Prism\Prism\Text\Request; +use Prism\Prism\Providers\TextHandler; +use Prism\Prism\Text\Request as TextRequest; use Prism\Prism\Text\Response as TextResponse; -use Prism\Prism\Text\ResponseBuilder; -use Prism\Prism\Text\Step; -use Prism\Prism\ValueObjects\Messages\AssistantMessage; use Prism\Prism\ValueObjects\Messages\ToolResultMessage; use Prism\Prism\ValueObjects\Meta; -use Prism\Prism\ValueObjects\ToolResult; use Prism\Prism\ValueObjects\Usage; -class Text +class Text extends TextHandler { use CallsTools; use MapsFinishReason; use ValidatesResponses; - protected ResponseBuilder $responseBuilder; - - public function __construct(protected PendingRequest $client) - { - $this->responseBuilder = new ResponseBuilder; - } - - public function handle(Request $request): TextResponse + /** + * @param TextRequest $request + */ + #[\Override] + public static function buildHttpRequestPayload(PrismRequest $request): array { - $data = $this->sendRequest($request); - - $this->validateResponse($data); - - $responseMessage = new AssistantMessage( - data_get($data, 'choices.0.message.content') ?? '', - ToolCallMap::map(data_get($data, 'choices.0.message.tool_calls', [])), - [] - ); - - $this->responseBuilder->addResponseMessage($responseMessage); - - $request = $request->addMessage($responseMessage); + if (! $request->is(TextRequest::class)) { + throw new \InvalidArgumentException('Request must be an instance of '.TextRequest::class); + } - return match ($this->mapFinishReason($data)) { - FinishReason::ToolCalls => $this->handleToolCalls($data, $request), - FinishReason::Stop => $this->handleStop($data, $request), - default => throw new PrismException('OpenRouter: unknown finish reason'), - }; + return array_merge([ + 'model' => $request->model(), + 'messages' => (new MessageMap($request->messages(), $request->systemPrompts()))(), + 'max_tokens' => $request->maxTokens(), + ], Arr::whereNotNull([ + 'temperature' => $request->temperature(), + 'top_p' => $request->topP(), + 'tools' => ToolMap::map($request->tools()), + 'tool_choice' => ToolChoiceMap::map($request->toolChoice()), + ])); } - /** - * @param array $data - */ - protected function handleToolCalls(array $data, Request $request): TextResponse + protected function handleToolCalls(): TextResponse { + $data = $this->httpResponse->json(); + $toolResults = $this->callTools( - $request->tools(), + $this->request->tools(), ToolCallMap::map(data_get($data, 'choices.0.message.tool_calls', [])) ); - $request = $request->addMessage(new ToolResultMessage($toolResults)); + $this->request->addMessage(new ToolResultMessage($toolResults)); - $this->addStep($data, $request, $toolResults); + $this->addStep($toolResults); - if ($this->shouldContinue($request)) { - return $this->handle($request); + if ($this->shouldContinue()) { + return $this->handle(); } return $this->responseBuilder->toResponse(); } - /** - * @param array $data - */ - protected function handleStop(array $data, Request $request): TextResponse - { - $this->addStep($data, $request); - - return $this->responseBuilder->toResponse(); - } - - protected function shouldContinue(Request $request): bool - { - return $this->responseBuilder->steps->count() < $request->maxSteps(); - } - - /** - * @return array - */ - protected function sendRequest(Request $request): array + protected function sendRequest(): void { - $response = $this->client->post( + $this->httpResponse = $this->client->post( 'chat/completions', - array_merge([ - 'model' => $request->model(), - 'messages' => (new MessageMap($request->messages(), $request->systemPrompts()))(), - 'max_tokens' => $request->maxTokens(), - ], Arr::whereNotNull([ - 'temperature' => $request->temperature(), - 'top_p' => $request->topP(), - 'tools' => ToolMap::map($request->tools()), - 'tool_choice' => ToolChoiceMap::map($request->toolChoice()), - ])) + static::buildHttpRequestPayload($this->request), ); - - return $response->json(); } - /** - * @param array $data - * @param array $toolResults - */ - protected function addStep(array $data, Request $request, array $toolResults = []): void + protected function prepareTempResponse(): void { - $this->responseBuilder->addStep(new Step( + $data = $this->httpResponse->json(); + + $this->tempResponse = new TextResponse( + steps: new Collection, + responseMessages: new Collection, + messages: new Collection, text: data_get($data, 'choices.0.message.content') ?? '', finishReason: $this->mapFinishReason($data), toolCalls: ToolCallMap::map(data_get($data, 'choices.0.message.tool_calls', [])), - toolResults: $toolResults, + toolResults: [], usage: new Usage( data_get($data, 'usage.prompt_tokens'), data_get($data, 'usage.completion_tokens'), @@ -138,9 +97,7 @@ protected function addStep(array $data, Request $request, array $toolResults = [ id: data_get($data, 'id'), model: data_get($data, 'model'), ), - messages: $request->messages(), additionalContent: [], - systemPrompts: $request->systemPrompts(), - )); + ); } } diff --git a/src/Providers/OpenRouter/OpenRouter.php b/src/Providers/OpenRouter/OpenRouter.php index 75c9dcefe..a885e6b52 100644 --- a/src/Providers/OpenRouter/OpenRouter.php +++ b/src/Providers/OpenRouter/OpenRouter.php @@ -34,12 +34,10 @@ public function __construct( #[\Override] public function text(TextRequest $request): TextResponse { - $handler = new Text($this->client( - $request->clientOptions(), - $request->clientRetry() - )); - - return $handler->handle($request); + return (new Text( + $this->client($request->clientOptions(), $request->clientRetry()), + $request + ))->handle(); } #[\Override] diff --git a/src/Providers/TextHandler.php b/src/Providers/TextHandler.php new file mode 100644 index 000000000..f17c958d7 --- /dev/null +++ b/src/Providers/TextHandler.php @@ -0,0 +1,98 @@ +responseBuilder = new ResponseBuilder; + } + + public function handle(): TextResponse + { + $this->sendRequest(); + + $this->prepareTempResponse(); + + $this->validateResponse(); + + $responseMessage = new AssistantMessage( + $this->tempResponse->text, + $this->tempResponse->toolCalls, + $this->tempResponse->additionalContent, + ); + + $this->responseBuilder->addResponseMessage($responseMessage); + + $this->request->addMessage($responseMessage); + + return match ($this->tempResponse->finishReason) { + FinishReason::ToolCalls => $this->handleToolCalls(), + FinishReason::Stop, FinishReason::Length => $this->handleStop(), + default => throw new PrismException($this->request->provider().': unknown finish reason'), + }; + } + + /** + * @return array + */ + abstract public static function buildHttpRequestPayload(TextRequest $request): array; + + protected function handleStop(): TextResponse + { + $this->addStep(); + + return $this->responseBuilder->toResponse(); + } + + protected function shouldContinue(): bool + { + return $this->responseBuilder->steps->count() < $this->request->maxSteps(); + } + + /** + * @param ToolResult[] $toolResults + */ + protected function addStep(array $toolResults = []): void + { + $this->responseBuilder->addStep(new Step( + text: $this->tempResponse->text, + finishReason: $this->tempResponse->finishReason, + toolCalls: $this->tempResponse->toolCalls, + toolResults: $toolResults, + usage: $this->tempResponse->usage, + meta: $this->tempResponse->meta, + messages: $this->request->messages(), + systemPrompts: $this->request->systemPrompts(), + additionalContent: $this->tempResponse->additionalContent, + )); + } + + protected function validateResponse(): void {} + + abstract protected function handleToolCalls(): TextResponse; + + abstract protected function prepareTempResponse(): void; + + abstract protected function sendRequest(): void; +} diff --git a/src/Providers/XAI/Concerns/ValidatesResponses.php b/src/Providers/XAI/Concerns/ValidatesResponses.php index 7fa4f3c7c..0bc60853e 100644 --- a/src/Providers/XAI/Concerns/ValidatesResponses.php +++ b/src/Providers/XAI/Concerns/ValidatesResponses.php @@ -4,14 +4,13 @@ namespace Prism\Prism\Providers\XAI\Concerns; -use Illuminate\Http\Client\Response; use Prism\Prism\Exceptions\PrismException; trait ValidatesResponses { - protected function validateResponse(Response $response): void + protected function validateResponse(): void { - $data = $response->json(); + $data = $this->httpResponse->json(); if (! $data || data_get($data, 'error')) { throw PrismException::providerResponseError(vsprintf( diff --git a/src/Providers/XAI/Handlers/Text.php b/src/Providers/XAI/Handlers/Text.php index b858b7619..951b8e9ab 100644 --- a/src/Providers/XAI/Handlers/Text.php +++ b/src/Providers/XAI/Handlers/Text.php @@ -4,142 +4,91 @@ namespace Prism\Prism\Providers\XAI\Handlers; -use Illuminate\Http\Client\PendingRequest; -use Illuminate\Http\Client\Response as ClientResponse; use Illuminate\Support\Arr; +use Illuminate\Support\Collection; use Prism\Prism\Concerns\CallsTools; -use Prism\Prism\Enums\FinishReason; -use Prism\Prism\Exceptions\PrismException; +use Prism\Prism\Contracts\PrismRequest; +use Prism\Prism\Providers\TextHandler; use Prism\Prism\Providers\XAI\Concerns\MapsFinishReason; use Prism\Prism\Providers\XAI\Concerns\ValidatesResponses; use Prism\Prism\Providers\XAI\Maps\MessageMap; use Prism\Prism\Providers\XAI\Maps\ToolChoiceMap; use Prism\Prism\Providers\XAI\Maps\ToolMap; -use Prism\Prism\Text\Request; +use Prism\Prism\Text\Request as TextRequest; use Prism\Prism\Text\Response as TextResponse; -use Prism\Prism\Text\ResponseBuilder; -use Prism\Prism\Text\Step; -use Prism\Prism\ValueObjects\Messages\AssistantMessage; use Prism\Prism\ValueObjects\Messages\ToolResultMessage; use Prism\Prism\ValueObjects\Meta; use Prism\Prism\ValueObjects\ToolCall; -use Prism\Prism\ValueObjects\ToolResult; use Prism\Prism\ValueObjects\Usage; -class Text +class Text extends TextHandler { use CallsTools; use MapsFinishReason; use ValidatesResponses; - protected ResponseBuilder $responseBuilder; - - public function __construct(protected PendingRequest $client) - { - $this->responseBuilder = new ResponseBuilder; - } - - public function handle(Request $request): TextResponse + /** + * @param TextRequest $request + */ + #[\Override] + public static function buildHttpRequestPayload(PrismRequest $request): array { - $response = $this->sendRequest($request); - - $this->validateResponse($response); - - $data = $response->json(); - - $responseMessage = new AssistantMessage( - data_get($data, 'choices.0.message.content') ?? '', - $this->mapToolCalls(data_get($data, 'choices.0.message.tool_calls', [])), - ); - - $this->responseBuilder->addResponseMessage($responseMessage); - - $request->addMessage($responseMessage); + if (! $request->is(TextRequest::class)) { + throw new \InvalidArgumentException('Request must be an instance of '.TextRequest::class); + } - return match ($this->mapFinishReason($data)) { - FinishReason::ToolCalls => $this->handleToolCalls($data, $request), - FinishReason::Stop => $this->handleStop($data, $request), - default => throw new PrismException('XAI: unknown finish reason'), - }; + return array_merge([ + 'model' => $request->model(), + 'messages' => (new MessageMap($request->messages(), $request->systemPrompts()))(), + 'max_tokens' => $request->maxTokens() ?? 2048, + ], Arr::whereNotNull([ + 'temperature' => $request->temperature(), + 'top_p' => $request->topP(), + 'tools' => ToolMap::map($request->tools()), + 'tool_choice' => ToolChoiceMap::map($request->toolChoice()), + ])); } - /** - * @param array $data - */ - protected function handleToolCalls(array $data, Request $request): TextResponse + protected function handleToolCalls(): TextResponse { + $data = $this->httpResponse->json(); + $toolResults = $this->callTools( - $request->tools(), + $this->request->tools(), $this->mapToolCalls(data_get($data, 'choices.0.message.tool_calls', [])), ); - $request->addMessage(new ToolResultMessage($toolResults)); + $this->request->addMessage(new ToolResultMessage($toolResults)); - $this->addStep($data, $request, $toolResults); + $this->addStep($toolResults); - if ($this->shouldContinue($request)) { - return $this->handle($request); + if ($this->shouldContinue()) { + return $this->handle(); } return $this->responseBuilder->toResponse(); } - /** - * @param array $data - */ - protected function handleStop(array $data, Request $request): TextResponse - { - $this->addStep($data, $request); - - return $this->responseBuilder->toResponse(); - } - - protected function shouldContinue(Request $request): bool + protected function sendRequest(): void { - return $this->responseBuilder->steps->count() < $request->maxSteps(); - } - - protected function sendRequest(Request $request): ClientResponse - { - return $this->client->post( + $this->httpResponse = $this->client->post( 'chat/completions', - array_merge([ - 'model' => $request->model(), - 'messages' => (new MessageMap($request->messages(), $request->systemPrompts()))(), - 'max_tokens' => $request->maxTokens() ?? 2048, - ], Arr::whereNotNull([ - 'temperature' => $request->temperature(), - 'top_p' => $request->topP(), - 'tools' => ToolMap::map($request->tools()), - 'tool_choice' => ToolChoiceMap::map($request->toolChoice()), - ])) + static::buildHttpRequestPayload($this->request) ); } - /** - * @param array> $toolCalls - * @return array - */ - protected function mapToolCalls(array $toolCalls): array + protected function prepareTempResponse(): void { - return array_map(fn (array $toolCall): ToolCall => new ToolCall( - id: data_get($toolCall, 'id'), - name: data_get($toolCall, 'function.name'), - arguments: data_get($toolCall, 'function.arguments'), - ), $toolCalls); - } + $data = $this->httpResponse->json(); - /** - * @param array $data - * @param array $toolResults - */ - protected function addStep(array $data, Request $request, array $toolResults = []): void - { - $this->responseBuilder->addStep(new Step( + $this->tempResponse = new TextResponse( + steps: new Collection, + responseMessages: new Collection, + messages: new Collection, text: data_get($data, 'choices.0.message.content') ?? '', finishReason: $this->mapFinishReason($data), toolCalls: $this->mapToolCalls(data_get($data, 'choices.0.message.tool_calls', [])), - toolResults: $toolResults, + toolResults: [], usage: new Usage( data_get($data, 'usage.prompt_tokens'), data_get($data, 'usage.completion_tokens'), @@ -148,9 +97,20 @@ protected function addStep(array $data, Request $request, array $toolResults = [ id: data_get($data, 'id'), model: data_get($data, 'model'), ), - messages: $request->messages(), additionalContent: [], - systemPrompts: $request->systemPrompts(), - )); + ); + } + + /** + * @param array> $toolCalls + * @return array + */ + protected function mapToolCalls(array $toolCalls): array + { + return array_map(fn (array $toolCall): ToolCall => new ToolCall( + id: data_get($toolCall, 'id'), + name: data_get($toolCall, 'function.name'), + arguments: data_get($toolCall, 'function.arguments'), + ), $toolCalls); } } diff --git a/src/Providers/XAI/XAI.php b/src/Providers/XAI/XAI.php index f62204b50..d43dc1ad1 100644 --- a/src/Providers/XAI/XAI.php +++ b/src/Providers/XAI/XAI.php @@ -23,9 +23,10 @@ public function __construct( #[\Override] public function text(TextRequest $request): TextResponse { - $handler = new Text($this->client($request->clientOptions(), $request->clientRetry())); - - return $handler->handle($request); + return (new Text( + $this->client($request->clientOptions(), $request->clientRetry()), + $request + ))->handle(); } /** From bac7aead9573963bc0405bfc0f805ad65ff8357f Mon Sep 17 00:00:00 2001 From: MrCrayon Date: Tue, 8 Jul 2025 05:15:07 +0200 Subject: [PATCH 2/2] refactor(providers): rename ValidateResponse to HandleResponseError Make handleResponseError abstract --- src/Providers/Anthropic/Handlers/Text.php | 2 ++ ...sResponses.php => HandleResponseError.php} | 7 +++-- .../DeepSeek/Handlers/Structured.php | 21 ++++----------- src/Providers/DeepSeek/Handlers/Text.php | 4 +-- ...esResponse.php => HandleResponseError.php} | 7 +++-- src/Providers/Gemini/Handlers/Structured.php | 27 ++++--------------- src/Providers/Gemini/Handlers/Text.php | 4 +-- ...teResponse.php => HandleResponseError.php} | 6 +++-- src/Providers/Groq/Handlers/Stream.php | 4 +-- src/Providers/Groq/Handlers/Structured.php | 6 ++--- src/Providers/Groq/Handlers/Text.php | 4 +-- ...esResponse.php => HandleResponseError.php} | 4 +-- src/Providers/Mistral/Handlers/Embeddings.php | 6 ++--- src/Providers/Mistral/Handlers/OCR.php | 2 -- src/Providers/Mistral/Handlers/Stream.php | 4 +-- src/Providers/Mistral/Handlers/Structured.php | 6 ++--- src/Providers/Mistral/Handlers/Text.php | 4 +-- ...esResponse.php => HandleResponseError.php} | 4 +-- src/Providers/Ollama/Handlers/Structured.php | 6 ++--- src/Providers/Ollama/Handlers/Text.php | 4 +-- ...esResponse.php => HandleResponseError.php} | 4 +-- src/Providers/OpenAI/Handlers/Embeddings.php | 6 ++--- src/Providers/OpenAI/Handlers/Images.php | 6 ++--- src/Providers/OpenAI/Handlers/Structured.php | 6 ++--- src/Providers/OpenAI/Handlers/Text.php | 4 +-- ...sResponses.php => HandleResponseError.php} | 4 +-- src/Providers/OpenRouter/Handlers/Stream.php | 2 -- .../OpenRouter/Handlers/Structured.php | 21 ++++----------- src/Providers/OpenRouter/Handlers/Text.php | 4 +-- src/Providers/TextHandler.php | 4 +-- src/Providers/VoyageAI/Embeddings.php | 4 +-- ...sResponses.php => HandleResponseError.php} | 4 +-- src/Providers/XAI/Handlers/Text.php | 4 +-- 33 files changed, 86 insertions(+), 119 deletions(-) rename src/Providers/DeepSeek/Concerns/{ValidatesResponses.php => HandleResponseError.php} (65%) rename src/Providers/Gemini/Concerns/{ValidatesResponse.php => HandleResponseError.php} (77%) rename src/Providers/Groq/Concerns/{ValidateResponse.php => HandleResponseError.php} (86%) rename src/Providers/Mistral/Concerns/{ValidatesResponse.php => HandleResponseError.php} (91%) rename src/Providers/Ollama/Concerns/{ValidatesResponse.php => HandleResponseError.php} (86%) rename src/Providers/OpenAI/Concerns/{ValidatesResponse.php => HandleResponseError.php} (88%) rename src/Providers/OpenRouter/Concerns/{ValidatesResponses.php => HandleResponseError.php} (95%) rename src/Providers/XAI/Concerns/{ValidatesResponses.php => HandleResponseError.php} (87%) diff --git a/src/Providers/Anthropic/Handlers/Text.php b/src/Providers/Anthropic/Handlers/Text.php index c0e166770..0281ca230 100644 --- a/src/Providers/Anthropic/Handlers/Text.php +++ b/src/Providers/Anthropic/Handlers/Text.php @@ -154,4 +154,6 @@ protected function extractToolCalls(array $data): array return $toolCalls; } + + protected function handleResponseError(): void {} } diff --git a/src/Providers/DeepSeek/Concerns/ValidatesResponses.php b/src/Providers/DeepSeek/Concerns/HandleResponseError.php similarity index 65% rename from src/Providers/DeepSeek/Concerns/ValidatesResponses.php rename to src/Providers/DeepSeek/Concerns/HandleResponseError.php index 4e18498f2..d45002c3c 100644 --- a/src/Providers/DeepSeek/Concerns/ValidatesResponses.php +++ b/src/Providers/DeepSeek/Concerns/HandleResponseError.php @@ -4,11 +4,14 @@ namespace Prism\Prism\Providers\DeepSeek\Concerns; +use Illuminate\Http\Client\Response; use Prism\Prism\Exceptions\PrismException; -trait ValidatesResponses +trait HandleResponseError { - protected function validateResponse(): void + protected Response $httpResponse; + + protected function handleResponseError(): void { if ($this->httpResponse->json() === []) { throw PrismException::providerResponseError('DeepSeek Error: Empty response'); diff --git a/src/Providers/DeepSeek/Handlers/Structured.php b/src/Providers/DeepSeek/Handlers/Structured.php index 38f9548e4..105f1d769 100644 --- a/src/Providers/DeepSeek/Handlers/Structured.php +++ b/src/Providers/DeepSeek/Handlers/Structured.php @@ -4,9 +4,8 @@ use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Arr; -use Prism\Prism\Exceptions\PrismException; +use Prism\Prism\Providers\DeepSeek\Concerns\HandleResponseError; use Prism\Prism\Providers\DeepSeek\Concerns\MapsFinishReason; -use Prism\Prism\Providers\DeepSeek\Concerns\ValidatesResponses; use Prism\Prism\Providers\DeepSeek\Maps\FinishReasonMap; use Prism\Prism\Providers\DeepSeek\Maps\MessageMap; use Prism\Prism\Structured\Request; @@ -20,8 +19,8 @@ class Structured { + use HandleResponseError; use MapsFinishReason; - use ValidatesResponses; protected ResponseBuilder $responseBuilder; @@ -36,7 +35,7 @@ public function handle(Request $request): StructuredResponse $data = $this->sendRequest($request); - $this->validateResponse($data); + $this->handleResponseError(); return $this->createResponse($request, $data); } @@ -46,7 +45,7 @@ public function handle(Request $request): StructuredResponse */ protected function sendRequest(Request $request): array { - $response = $this->client->post( + $this->httpResponse = $this->client->post( 'chat/completions', array_merge([ 'model' => $request->model(), @@ -59,17 +58,7 @@ protected function sendRequest(Request $request): array ])) ); - return $response->json(); - } - - /** - * @param array $data - */ - protected function validateResponse(array $data): void - { - if ($data === []) { - throw PrismException::providerResponseError('DeepSeek Error: Empty response'); - } + return $this->httpResponse->json(); } /** diff --git a/src/Providers/DeepSeek/Handlers/Text.php b/src/Providers/DeepSeek/Handlers/Text.php index 810a00ad5..6f6b9d4c6 100644 --- a/src/Providers/DeepSeek/Handlers/Text.php +++ b/src/Providers/DeepSeek/Handlers/Text.php @@ -8,8 +8,8 @@ use Illuminate\Support\Collection; use Prism\Prism\Concerns\CallsTools; use Prism\Prism\Contracts\PrismRequest; +use Prism\Prism\Providers\DeepSeek\Concerns\HandleResponseError; use Prism\Prism\Providers\DeepSeek\Concerns\MapsFinishReason; -use Prism\Prism\Providers\DeepSeek\Concerns\ValidatesResponses; use Prism\Prism\Providers\DeepSeek\Maps\MessageMap; use Prism\Prism\Providers\DeepSeek\Maps\ToolCallMap; use Prism\Prism\Providers\DeepSeek\Maps\ToolChoiceMap; @@ -24,8 +24,8 @@ class Text extends TextHandler { use CallsTools; + use HandleResponseError; use MapsFinishReason; - use ValidatesResponses; /** * @param TextRequest $request diff --git a/src/Providers/Gemini/Concerns/ValidatesResponse.php b/src/Providers/Gemini/Concerns/HandleResponseError.php similarity index 77% rename from src/Providers/Gemini/Concerns/ValidatesResponse.php rename to src/Providers/Gemini/Concerns/HandleResponseError.php index e212f4437..105c0a232 100644 --- a/src/Providers/Gemini/Concerns/ValidatesResponse.php +++ b/src/Providers/Gemini/Concerns/HandleResponseError.php @@ -4,11 +4,14 @@ namespace Prism\Prism\Providers\Gemini\Concerns; +use Illuminate\Http\Client\Response; use Prism\Prism\Exceptions\PrismException; -trait ValidatesResponse +trait HandleResponseError { - protected function validateResponse(): void + protected Response $httpResponse; + + protected function handleResponseError(): void { $data = $this->httpResponse->json(); diff --git a/src/Providers/Gemini/Handlers/Structured.php b/src/Providers/Gemini/Handlers/Structured.php index d8595f093..eb3cfe644 100644 --- a/src/Providers/Gemini/Handlers/Structured.php +++ b/src/Providers/Gemini/Handlers/Structured.php @@ -4,8 +4,7 @@ use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Arr; -use Prism\Prism\Exceptions\PrismException; -use Prism\Prism\Providers\Gemini\Concerns\ValidatesResponse; +use Prism\Prism\Providers\Gemini\Concerns\HandleResponseError; use Prism\Prism\Providers\Gemini\Maps\FinishReasonMap; use Prism\Prism\Providers\Gemini\Maps\MessageMap; use Prism\Prism\Providers\Gemini\Maps\SchemaMap; @@ -19,7 +18,7 @@ class Structured { - use ValidatesResponse; + use HandleResponseError; protected ResponseBuilder $responseBuilder; @@ -32,7 +31,7 @@ public function handle(Request $request): StructuredResponse { $data = $this->sendRequest($request); - $this->validateResponse($data); + $this->handleResponseError(); $responseMessage = new AssistantMessage(data_get($data, 'candidates.0.content.parts.0.text') ?? ''); @@ -52,7 +51,7 @@ public function sendRequest(Request $request): array { $providerOptions = $request->providerOptions(); - $response = $this->client->post( + $this->httpResponse = $this->client->post( "{$request->model()}:generateContent", Arr::whereNotNull([ ...(new MessageMap($request->messages(), $request->systemPrompts()))(), @@ -71,23 +70,7 @@ public function sendRequest(Request $request): array ]) ); - return $response->json(); - } - - /** - * @param array $data - */ - protected function validateResponse(array $data): void - { - if (! $data || data_get($data, 'error')) { - throw PrismException::providerResponseError(vsprintf( - 'Gemini Error: [%s] %s', - [ - data_get($data, 'error.code', 'unknown'), - data_get($data, 'error.message', 'unknown'), - ] - )); - } + return $this->httpResponse->json(); } /** diff --git a/src/Providers/Gemini/Handlers/Text.php b/src/Providers/Gemini/Handlers/Text.php index 84317dc59..d3b20c537 100644 --- a/src/Providers/Gemini/Handlers/Text.php +++ b/src/Providers/Gemini/Handlers/Text.php @@ -10,7 +10,7 @@ use Prism\Prism\Contracts\PrismRequest; use Prism\Prism\Exceptions\PrismException; use Prism\Prism\Providers\Gemini\Concerns\ExtractSearchGroundings; -use Prism\Prism\Providers\Gemini\Concerns\ValidatesResponse; +use Prism\Prism\Providers\Gemini\Concerns\HandleResponseError; use Prism\Prism\Providers\Gemini\Maps\FinishReasonMap; use Prism\Prism\Providers\Gemini\Maps\MessageMap; use Prism\Prism\Providers\Gemini\Maps\ToolCallMap; @@ -27,7 +27,7 @@ class Text extends TextHandler { - use CallsTools, ExtractSearchGroundings, ValidatesResponse; + use CallsTools, ExtractSearchGroundings, HandleResponseError; /** * @param TextRequest $request diff --git a/src/Providers/Groq/Concerns/ValidateResponse.php b/src/Providers/Groq/Concerns/HandleResponseError.php similarity index 86% rename from src/Providers/Groq/Concerns/ValidateResponse.php rename to src/Providers/Groq/Concerns/HandleResponseError.php index 31b9e9a33..03052ba39 100644 --- a/src/Providers/Groq/Concerns/ValidateResponse.php +++ b/src/Providers/Groq/Concerns/HandleResponseError.php @@ -8,9 +8,11 @@ use Prism\Prism\Exceptions\PrismException; use Prism\Prism\ValueObjects\ProviderRateLimit; -trait ValidateResponse +trait HandleResponseError { - protected function validateResponse(): void + protected Response $httpResponse; + + protected function handleResponseError(): void { $data = $this->httpResponse->json(); diff --git a/src/Providers/Groq/Handlers/Stream.php b/src/Providers/Groq/Handlers/Stream.php index 4cf7a0ca4..139ffd800 100644 --- a/src/Providers/Groq/Handlers/Stream.php +++ b/src/Providers/Groq/Handlers/Stream.php @@ -14,8 +14,8 @@ use Prism\Prism\Exceptions\PrismChunkDecodeException; use Prism\Prism\Exceptions\PrismException; use Prism\Prism\Exceptions\PrismRateLimitedException; +use Prism\Prism\Providers\Groq\Concerns\HandleResponseError; use Prism\Prism\Providers\Groq\Concerns\ProcessRateLimits; -use Prism\Prism\Providers\Groq\Concerns\ValidateResponse; use Prism\Prism\Providers\Groq\Maps\FinishReasonMap; use Prism\Prism\Providers\Groq\Maps\MessageMap; use Prism\Prism\Providers\Groq\Maps\ToolChoiceMap; @@ -30,7 +30,7 @@ class Stream { - use CallsTools, ProcessRateLimits, ValidateResponse; + use CallsTools, HandleResponseError, ProcessRateLimits; public function __construct(protected PendingRequest $client) {} diff --git a/src/Providers/Groq/Handlers/Structured.php b/src/Providers/Groq/Handlers/Structured.php index 5f0b2a9fd..03dde9959 100644 --- a/src/Providers/Groq/Handlers/Structured.php +++ b/src/Providers/Groq/Handlers/Structured.php @@ -5,8 +5,8 @@ use Illuminate\Http\Client\PendingRequest; use Illuminate\Http\Client\Response as ClientResponse; use Illuminate\Support\Arr; +use Prism\Prism\Providers\Groq\Concerns\HandleResponseError; use Prism\Prism\Providers\Groq\Concerns\ProcessRateLimits; -use Prism\Prism\Providers\Groq\Concerns\ValidateResponse; use Prism\Prism\Providers\Groq\Maps\FinishReasonMap; use Prism\Prism\Providers\Groq\Maps\MessageMap; use Prism\Prism\Structured\Request; @@ -20,7 +20,7 @@ class Structured { - use ProcessRateLimits, ValidateResponse; + use HandleResponseError, ProcessRateLimits; protected ClientResponse $httpResponse; @@ -37,7 +37,7 @@ public function handle(Request $request): StructuredResponse $response = $this->sendRequest($request); - $this->validateResponse(); + $this->handleResponseError(); $data = $response->json(); diff --git a/src/Providers/Groq/Handlers/Text.php b/src/Providers/Groq/Handlers/Text.php index a389250ee..3d6d5ca9e 100644 --- a/src/Providers/Groq/Handlers/Text.php +++ b/src/Providers/Groq/Handlers/Text.php @@ -8,8 +8,8 @@ use Illuminate\Support\Collection; use Prism\Prism\Concerns\CallsTools; use Prism\Prism\Contracts\PrismRequest; +use Prism\Prism\Providers\Groq\Concerns\HandleResponseError; use Prism\Prism\Providers\Groq\Concerns\ProcessRateLimits; -use Prism\Prism\Providers\Groq\Concerns\ValidateResponse; use Prism\Prism\Providers\Groq\Maps\FinishReasonMap; use Prism\Prism\Providers\Groq\Maps\MessageMap; use Prism\Prism\Providers\Groq\Maps\ToolChoiceMap; @@ -24,7 +24,7 @@ class Text extends TextHandler { - use CallsTools, ProcessRateLimits, ValidateResponse; + use CallsTools, HandleResponseError, ProcessRateLimits; /** * @param TextRequest $request diff --git a/src/Providers/Mistral/Concerns/ValidatesResponse.php b/src/Providers/Mistral/Concerns/HandleResponseError.php similarity index 91% rename from src/Providers/Mistral/Concerns/ValidatesResponse.php rename to src/Providers/Mistral/Concerns/HandleResponseError.php index 223aafa5d..061962c29 100644 --- a/src/Providers/Mistral/Concerns/ValidatesResponse.php +++ b/src/Providers/Mistral/Concerns/HandleResponseError.php @@ -8,11 +8,11 @@ use Prism\Prism\Exceptions\PrismException; use Prism\Prism\ValueObjects\ProviderRateLimit; -trait ValidatesResponse +trait HandleResponseError { protected Response $httpResponse; - protected function validateResponse(): void + protected function handleResponseError(): void { $data = $this->httpResponse->json(); diff --git a/src/Providers/Mistral/Handlers/Embeddings.php b/src/Providers/Mistral/Handlers/Embeddings.php index d07d31344..d21b94094 100644 --- a/src/Providers/Mistral/Handlers/Embeddings.php +++ b/src/Providers/Mistral/Handlers/Embeddings.php @@ -8,15 +8,15 @@ use Illuminate\Http\Client\Response; use Prism\Prism\Embeddings\Request; use Prism\Prism\Embeddings\Response as EmbeddingsResponse; +use Prism\Prism\Providers\Mistral\Concerns\HandleResponseError; use Prism\Prism\Providers\Mistral\Concerns\ProcessRateLimits; -use Prism\Prism\Providers\Mistral\Concerns\ValidatesResponse; use Prism\Prism\ValueObjects\Embedding; use Prism\Prism\ValueObjects\EmbeddingsUsage; use Prism\Prism\ValueObjects\Meta; class Embeddings { - use ProcessRateLimits, ValidatesResponse; + use HandleResponseError, ProcessRateLimits; public function __construct(protected PendingRequest $client) {} @@ -24,7 +24,7 @@ public function handle(Request $request): EmbeddingsResponse { $response = $this->sendRequest($request); - $this->validateResponse(); + $this->handleResponseError(); $data = $response->json(); diff --git a/src/Providers/Mistral/Handlers/OCR.php b/src/Providers/Mistral/Handlers/OCR.php index 4098824cc..69f9677da 100644 --- a/src/Providers/Mistral/Handlers/OCR.php +++ b/src/Providers/Mistral/Handlers/OCR.php @@ -10,7 +10,6 @@ use Prism\Prism\Exceptions\PrismRateLimitedException; use Prism\Prism\Providers\Mistral\Concerns\MapsFinishReason; use Prism\Prism\Providers\Mistral\Concerns\ProcessRateLimits; -use Prism\Prism\Providers\Mistral\Concerns\ValidatesResponse; use Prism\Prism\Providers\Mistral\Maps\DocumentMapper; use Prism\Prism\Providers\Mistral\ValueObjects\OCRResponse; use Prism\Prism\Text\ResponseBuilder; @@ -21,7 +20,6 @@ class OCR use CallsTools; use MapsFinishReason; use ProcessRateLimits; - use ValidatesResponse; protected ResponseBuilder $responseBuilder; diff --git a/src/Providers/Mistral/Handlers/Stream.php b/src/Providers/Mistral/Handlers/Stream.php index fb6033a46..d22da1dcc 100644 --- a/src/Providers/Mistral/Handlers/Stream.php +++ b/src/Providers/Mistral/Handlers/Stream.php @@ -13,9 +13,9 @@ use Prism\Prism\Enums\FinishReason; use Prism\Prism\Exceptions\PrismChunkDecodeException; use Prism\Prism\Exceptions\PrismException; +use Prism\Prism\Providers\Mistral\Concerns\HandleResponseError; use Prism\Prism\Providers\Mistral\Concerns\MapsFinishReason; use Prism\Prism\Providers\Mistral\Concerns\ProcessRateLimits; -use Prism\Prism\Providers\Mistral\Concerns\ValidatesResponse; use Prism\Prism\Providers\Mistral\Maps\MessageMap; use Prism\Prism\Providers\Mistral\Maps\ToolChoiceMap; use Prism\Prism\Providers\Mistral\Maps\ToolMap; @@ -29,7 +29,7 @@ class Stream { - use CallsTools, MapsFinishReason, ProcessRateLimits, ValidatesResponse; + use CallsTools, HandleResponseError, MapsFinishReason, ProcessRateLimits; public function __construct( protected PendingRequest $client, diff --git a/src/Providers/Mistral/Handlers/Structured.php b/src/Providers/Mistral/Handlers/Structured.php index 919e9aa2d..89c8c888e 100644 --- a/src/Providers/Mistral/Handlers/Structured.php +++ b/src/Providers/Mistral/Handlers/Structured.php @@ -7,9 +7,9 @@ use Illuminate\Http\Client\PendingRequest; use Illuminate\Http\Client\Response as ClientResponse; use Illuminate\Support\Arr; +use Prism\Prism\Providers\Mistral\Concerns\HandleResponseError; use Prism\Prism\Providers\Mistral\Concerns\MapsFinishReason; use Prism\Prism\Providers\Mistral\Concerns\ProcessRateLimits; -use Prism\Prism\Providers\Mistral\Concerns\ValidatesResponse; use Prism\Prism\Providers\Mistral\Maps\FinishReasonMap; use Prism\Prism\Providers\Mistral\Maps\MessageMap; use Prism\Prism\Structured\Request; @@ -23,9 +23,9 @@ class Structured { + use HandleResponseError; use MapsFinishReason; use ProcessRateLimits; - use ValidatesResponse; protected ResponseBuilder $responseBuilder; @@ -40,7 +40,7 @@ public function handle(Request $request): StructuredResponse $response = $this->sendRequest($request); - $this->validateResponse(); + $this->handleResponseError(); $data = $response->json(); diff --git a/src/Providers/Mistral/Handlers/Text.php b/src/Providers/Mistral/Handlers/Text.php index c35fb0716..cfbd4720b 100644 --- a/src/Providers/Mistral/Handlers/Text.php +++ b/src/Providers/Mistral/Handlers/Text.php @@ -8,9 +8,9 @@ use Illuminate\Support\Collection; use Prism\Prism\Concerns\CallsTools; use Prism\Prism\Contracts\PrismRequest; +use Prism\Prism\Providers\Mistral\Concerns\HandleResponseError; use Prism\Prism\Providers\Mistral\Concerns\MapsFinishReason; use Prism\Prism\Providers\Mistral\Concerns\ProcessRateLimits; -use Prism\Prism\Providers\Mistral\Concerns\ValidatesResponse; use Prism\Prism\Providers\Mistral\Maps\MessageMap; use Prism\Prism\Providers\Mistral\Maps\ToolChoiceMap; use Prism\Prism\Providers\Mistral\Maps\ToolMap; @@ -25,9 +25,9 @@ class Text extends TextHandler { use CallsTools; + use HandleResponseError; use MapsFinishReason; use ProcessRateLimits; - use ValidatesResponse; /** * @param TextRequest $request diff --git a/src/Providers/Ollama/Concerns/ValidatesResponse.php b/src/Providers/Ollama/Concerns/HandleResponseError.php similarity index 86% rename from src/Providers/Ollama/Concerns/ValidatesResponse.php rename to src/Providers/Ollama/Concerns/HandleResponseError.php index 115291766..7c84ae609 100644 --- a/src/Providers/Ollama/Concerns/ValidatesResponse.php +++ b/src/Providers/Ollama/Concerns/HandleResponseError.php @@ -7,11 +7,11 @@ use Illuminate\Http\Client\Response; use Prism\Prism\Exceptions\PrismException; -trait ValidatesResponse +trait HandleResponseError { protected Response $httpResponse; - protected function validateResponse(): void + protected function handleResponseError(): void { $data = $this->httpResponse->json(); diff --git a/src/Providers/Ollama/Handlers/Structured.php b/src/Providers/Ollama/Handlers/Structured.php index f5c5faddd..8dc4fe1f9 100644 --- a/src/Providers/Ollama/Handlers/Structured.php +++ b/src/Providers/Ollama/Handlers/Structured.php @@ -7,8 +7,8 @@ use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Arr; use Prism\Prism\Exceptions\PrismException; +use Prism\Prism\Providers\Ollama\Concerns\HandleResponseError; use Prism\Prism\Providers\Ollama\Concerns\MapsFinishReason; -use Prism\Prism\Providers\Ollama\Concerns\ValidatesResponse; use Prism\Prism\Providers\Ollama\Maps\MessageMap; use Prism\Prism\Structured\Request; use Prism\Prism\Structured\Response; @@ -20,8 +20,8 @@ class Structured { + use HandleResponseError; use MapsFinishReason; - use ValidatesResponse; protected ResponseBuilder $responseBuilder; @@ -34,7 +34,7 @@ public function handle(Request $request): Response { $data = $this->sendRequest($request); - $this->validateResponse(); + $this->handleResponseError(); $responseMessage = new AssistantMessage( data_get($data, 'message.content') ?? '', diff --git a/src/Providers/Ollama/Handlers/Text.php b/src/Providers/Ollama/Handlers/Text.php index 4531a15f2..cdc8cbb41 100644 --- a/src/Providers/Ollama/Handlers/Text.php +++ b/src/Providers/Ollama/Handlers/Text.php @@ -9,8 +9,8 @@ use Prism\Prism\Concerns\CallsTools; use Prism\Prism\Contracts\PrismRequest; use Prism\Prism\Exceptions\PrismException; +use Prism\Prism\Providers\Ollama\Concerns\HandleResponseError; use Prism\Prism\Providers\Ollama\Concerns\MapsFinishReason; -use Prism\Prism\Providers\Ollama\Concerns\ValidatesResponse; use Prism\Prism\Providers\Ollama\Maps\MessageMap; use Prism\Prism\Providers\Ollama\Maps\ToolMap; use Prism\Prism\Providers\TextHandler; @@ -24,8 +24,8 @@ class Text extends TextHandler { use CallsTools; + use HandleResponseError; use MapsFinishReason; - use ValidatesResponse; /** * @param TextRequest $request diff --git a/src/Providers/OpenAI/Concerns/ValidatesResponse.php b/src/Providers/OpenAI/Concerns/HandleResponseError.php similarity index 88% rename from src/Providers/OpenAI/Concerns/ValidatesResponse.php rename to src/Providers/OpenAI/Concerns/HandleResponseError.php index d64fb207b..5bc224b64 100644 --- a/src/Providers/OpenAI/Concerns/ValidatesResponse.php +++ b/src/Providers/OpenAI/Concerns/HandleResponseError.php @@ -7,11 +7,11 @@ use Illuminate\Http\Client\Response; use Prism\Prism\Exceptions\PrismException; -trait ValidatesResponse +trait HandleResponseError { protected Response $httpResponse; - protected function validateResponse(): void + protected function handleResponseError(): void { $data = $this->httpResponse->json(); diff --git a/src/Providers/OpenAI/Handlers/Embeddings.php b/src/Providers/OpenAI/Handlers/Embeddings.php index 15042d01b..aac3d4c37 100644 --- a/src/Providers/OpenAI/Handlers/Embeddings.php +++ b/src/Providers/OpenAI/Handlers/Embeddings.php @@ -8,14 +8,14 @@ use Illuminate\Http\Client\Response; use Prism\Prism\Embeddings\Request; use Prism\Prism\Embeddings\Response as EmbeddingsResponse; -use Prism\Prism\Providers\OpenAI\Concerns\ValidatesResponse; +use Prism\Prism\Providers\OpenAI\Concerns\HandleResponseError; use Prism\Prism\ValueObjects\Embedding; use Prism\Prism\ValueObjects\EmbeddingsUsage; use Prism\Prism\ValueObjects\Meta; class Embeddings { - use ValidatesResponse; + use HandleResponseError; public function __construct(protected PendingRequest $client) {} @@ -23,7 +23,7 @@ public function handle(Request $request): EmbeddingsResponse { $response = $this->sendRequest($request); - $this->validateResponse(); + $this->handleResponseError(); $data = $response->json(); diff --git a/src/Providers/OpenAI/Handlers/Images.php b/src/Providers/OpenAI/Handlers/Images.php index 91e3c03af..27d72a389 100644 --- a/src/Providers/OpenAI/Handlers/Images.php +++ b/src/Providers/OpenAI/Handlers/Images.php @@ -9,7 +9,7 @@ use Prism\Prism\Images\Request; use Prism\Prism\Images\Response; use Prism\Prism\Images\ResponseBuilder; -use Prism\Prism\Providers\OpenAI\Concerns\ValidatesResponse; +use Prism\Prism\Providers\OpenAI\Concerns\HandleResponseError; use Prism\Prism\Providers\OpenAI\Maps\ImageRequestMap; use Prism\Prism\ValueObjects\GeneratedImage; use Prism\Prism\ValueObjects\Meta; @@ -17,7 +17,7 @@ class Images { - use ValidatesResponse; + use HandleResponseError; public function __construct(protected PendingRequest $client) {} @@ -25,7 +25,7 @@ public function handle(Request $request): Response { $response = $this->sendRequest($request); - $this->validateResponse(); + $this->handleResponseError(); $data = $response->json(); diff --git a/src/Providers/OpenAI/Handlers/Structured.php b/src/Providers/OpenAI/Handlers/Structured.php index e094d263a..d24cd7830 100644 --- a/src/Providers/OpenAI/Handlers/Structured.php +++ b/src/Providers/OpenAI/Handlers/Structured.php @@ -7,8 +7,8 @@ use Illuminate\Support\Arr; use Prism\Prism\Enums\StructuredMode; use Prism\Prism\Exceptions\PrismException; +use Prism\Prism\Providers\OpenAI\Concerns\HandleResponseError; use Prism\Prism\Providers\OpenAI\Concerns\MapsFinishReason; -use Prism\Prism\Providers\OpenAI\Concerns\ValidatesResponse; use Prism\Prism\Providers\OpenAI\Maps\MessageMap; use Prism\Prism\Providers\OpenAI\Support\StructuredModeResolver; use Prism\Prism\Structured\Request; @@ -22,8 +22,8 @@ class Structured { + use HandleResponseError; use MapsFinishReason; - use ValidatesResponse; protected ResponseBuilder $responseBuilder; @@ -41,7 +41,7 @@ public function handle(Request $request): StructuredResponse }; - $this->validateResponse(); + $this->handleResponseError(); $data = $response->json(); diff --git a/src/Providers/OpenAI/Handlers/Text.php b/src/Providers/OpenAI/Handlers/Text.php index 4db675592..72ff7ea43 100644 --- a/src/Providers/OpenAI/Handlers/Text.php +++ b/src/Providers/OpenAI/Handlers/Text.php @@ -9,8 +9,8 @@ use Prism\Prism\Concerns\CallsTools; use Prism\Prism\Contracts\PrismRequest; use Prism\Prism\Providers\OpenAI\Concerns\BuildsTools; +use Prism\Prism\Providers\OpenAI\Concerns\HandleResponseError; use Prism\Prism\Providers\OpenAI\Concerns\MapsFinishReason; -use Prism\Prism\Providers\OpenAI\Concerns\ValidatesResponse; use Prism\Prism\Providers\OpenAI\Maps\MessageMap; use Prism\Prism\Providers\OpenAI\Maps\ToolCallMap; use Prism\Prism\Providers\OpenAI\Maps\ToolChoiceMap; @@ -25,8 +25,8 @@ class Text extends TextHandler { use BuildsTools; use CallsTools; + use HandleResponseError; use MapsFinishReason; - use ValidatesResponse; /** * @param TextRequest $request diff --git a/src/Providers/OpenRouter/Concerns/ValidatesResponses.php b/src/Providers/OpenRouter/Concerns/HandleResponseError.php similarity index 95% rename from src/Providers/OpenRouter/Concerns/ValidatesResponses.php rename to src/Providers/OpenRouter/Concerns/HandleResponseError.php index ab5cbbc68..408506468 100644 --- a/src/Providers/OpenRouter/Concerns/ValidatesResponses.php +++ b/src/Providers/OpenRouter/Concerns/HandleResponseError.php @@ -7,11 +7,11 @@ use Illuminate\Http\Client\Response; use Prism\Prism\Exceptions\PrismException; -trait ValidatesResponses +trait HandleResponseError { protected Response $httpResponse; - protected function validateResponse(): void + protected function handleResponseError(): void { $data = $this->httpResponse->json(); diff --git a/src/Providers/OpenRouter/Handlers/Stream.php b/src/Providers/OpenRouter/Handlers/Stream.php index 4f41cc10a..61540ad89 100644 --- a/src/Providers/OpenRouter/Handlers/Stream.php +++ b/src/Providers/OpenRouter/Handlers/Stream.php @@ -14,7 +14,6 @@ use Prism\Prism\Enums\FinishReason; use Prism\Prism\Exceptions\PrismChunkDecodeException; use Prism\Prism\Providers\OpenRouter\Concerns\MapsFinishReason; -use Prism\Prism\Providers\OpenRouter\Concerns\ValidatesResponses; use Prism\Prism\Providers\OpenRouter\Maps\MessageMap; use Prism\Prism\Providers\OpenRouter\Maps\ToolChoiceMap; use Prism\Prism\Providers\OpenRouter\Maps\ToolMap; @@ -32,7 +31,6 @@ class Stream { use CallsTools; use MapsFinishReason; - use ValidatesResponses; public function __construct(protected PendingRequest $client) {} diff --git a/src/Providers/OpenRouter/Handlers/Structured.php b/src/Providers/OpenRouter/Handlers/Structured.php index 060a3813e..f48ad966a 100644 --- a/src/Providers/OpenRouter/Handlers/Structured.php +++ b/src/Providers/OpenRouter/Handlers/Structured.php @@ -6,9 +6,8 @@ use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Arr; -use Prism\Prism\Exceptions\PrismException; +use Prism\Prism\Providers\OpenRouter\Concerns\HandleResponseError; use Prism\Prism\Providers\OpenRouter\Concerns\MapsFinishReason; -use Prism\Prism\Providers\OpenRouter\Concerns\ValidatesResponses; use Prism\Prism\Providers\OpenRouter\Maps\FinishReasonMap; use Prism\Prism\Providers\OpenRouter\Maps\MessageMap; use Prism\Prism\Structured\Request; @@ -22,8 +21,8 @@ class Structured { + use HandleResponseError; use MapsFinishReason; - use ValidatesResponses; protected ResponseBuilder $responseBuilder; @@ -38,7 +37,7 @@ public function handle(Request $request): StructuredResponse $data = $this->sendRequest($request); - $this->validateResponse($data); + $this->handleResponseError(); return $this->createResponse($request, $data); } @@ -48,7 +47,7 @@ public function handle(Request $request): StructuredResponse */ protected function sendRequest(Request $request): array { - $response = $this->client->post( + $this->httpResponse = $this->client->post( 'chat/completions', array_merge([ 'model' => $request->model(), @@ -61,17 +60,7 @@ protected function sendRequest(Request $request): array ])) ); - return $response->json(); - } - - /** - * @param array $data - */ - protected function validateResponse(array $data): void - { - if ($data === []) { - throw PrismException::providerResponseError('OpenRouter Error: Empty response'); - } + return $this->httpResponse->json(); } /** diff --git a/src/Providers/OpenRouter/Handlers/Text.php b/src/Providers/OpenRouter/Handlers/Text.php index 3fc325a2e..e344af8f6 100644 --- a/src/Providers/OpenRouter/Handlers/Text.php +++ b/src/Providers/OpenRouter/Handlers/Text.php @@ -8,8 +8,8 @@ use Illuminate\Support\Collection; use Prism\Prism\Concerns\CallsTools; use Prism\Prism\Contracts\PrismRequest; +use Prism\Prism\Providers\OpenRouter\Concerns\HandleResponseError; use Prism\Prism\Providers\OpenRouter\Concerns\MapsFinishReason; -use Prism\Prism\Providers\OpenRouter\Concerns\ValidatesResponses; use Prism\Prism\Providers\OpenRouter\Maps\MessageMap; use Prism\Prism\Providers\OpenRouter\Maps\ToolCallMap; use Prism\Prism\Providers\OpenRouter\Maps\ToolChoiceMap; @@ -24,8 +24,8 @@ class Text extends TextHandler { use CallsTools; + use HandleResponseError; use MapsFinishReason; - use ValidatesResponses; /** * @param TextRequest $request diff --git a/src/Providers/TextHandler.php b/src/Providers/TextHandler.php index f17c958d7..f4f7f910f 100644 --- a/src/Providers/TextHandler.php +++ b/src/Providers/TextHandler.php @@ -34,7 +34,7 @@ public function handle(): TextResponse $this->prepareTempResponse(); - $this->validateResponse(); + $this->handleResponseError(); $responseMessage = new AssistantMessage( $this->tempResponse->text, @@ -88,7 +88,7 @@ protected function addStep(array $toolResults = []): void )); } - protected function validateResponse(): void {} + abstract protected function handleResponseError(): void; abstract protected function handleToolCalls(): TextResponse; diff --git a/src/Providers/VoyageAI/Embeddings.php b/src/Providers/VoyageAI/Embeddings.php index 8297eb0e0..f45a3cc89 100644 --- a/src/Providers/VoyageAI/Embeddings.php +++ b/src/Providers/VoyageAI/Embeddings.php @@ -26,7 +26,7 @@ public function handle(EmbeddingsRequest $request): EmbeddingsResponse $this->sendRequest(); - $this->validateResponse(); + $this->handleReponseError(); $data = $this->httpResponse->json(); @@ -54,7 +54,7 @@ protected function sendRequest(): void ])); } - protected function validateResponse(): void + protected function handleReponseError(): void { $data = $this->httpResponse->json(); diff --git a/src/Providers/XAI/Concerns/ValidatesResponses.php b/src/Providers/XAI/Concerns/HandleResponseError.php similarity index 87% rename from src/Providers/XAI/Concerns/ValidatesResponses.php rename to src/Providers/XAI/Concerns/HandleResponseError.php index 0bc60853e..f5cfaa535 100644 --- a/src/Providers/XAI/Concerns/ValidatesResponses.php +++ b/src/Providers/XAI/Concerns/HandleResponseError.php @@ -6,9 +6,9 @@ use Prism\Prism\Exceptions\PrismException; -trait ValidatesResponses +trait HandleResponseError { - protected function validateResponse(): void + protected function handleResponseError(): void { $data = $this->httpResponse->json(); diff --git a/src/Providers/XAI/Handlers/Text.php b/src/Providers/XAI/Handlers/Text.php index 951b8e9ab..2e87949f6 100644 --- a/src/Providers/XAI/Handlers/Text.php +++ b/src/Providers/XAI/Handlers/Text.php @@ -9,8 +9,8 @@ use Prism\Prism\Concerns\CallsTools; use Prism\Prism\Contracts\PrismRequest; use Prism\Prism\Providers\TextHandler; +use Prism\Prism\Providers\XAI\Concerns\HandleResponseError; use Prism\Prism\Providers\XAI\Concerns\MapsFinishReason; -use Prism\Prism\Providers\XAI\Concerns\ValidatesResponses; use Prism\Prism\Providers\XAI\Maps\MessageMap; use Prism\Prism\Providers\XAI\Maps\ToolChoiceMap; use Prism\Prism\Providers\XAI\Maps\ToolMap; @@ -24,8 +24,8 @@ class Text extends TextHandler { use CallsTools; + use HandleResponseError; use MapsFinishReason; - use ValidatesResponses; /** * @param TextRequest $request