diff --git a/.gitignore b/.gitignore index 0439ad3..650ec58 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ phpunit.*.xml .idea/ /.phpunit.result.cache /.phpunit.cache/ +config.json diff --git a/src/AbstractProvider.php b/src/AbstractProvider.php index a16b654..65b35e7 100644 --- a/src/AbstractProvider.php +++ b/src/AbstractProvider.php @@ -58,7 +58,7 @@ abstract class AbstractProvider implements ProviderInterface * @throws \InvalidArgumentException * @since ___DEPLOY_VERSION___ */ - public function __construct(array $options = [], ?HttpFactory $httpFactory = null) + public function __construct($options = [], ?HttpFactory $httpFactory = null) { // Validate provider is suported if (!\is_array($options) && !($options instanceof \ArrayAccess)) { @@ -175,7 +175,7 @@ protected function makePostRequest(string $url, $data, array $headers = [], $tim /** * Make HTTP DELETE request. * - * @param string $url API endpoint URL + * @param string $url API endpoint URL * @param mixed $data Data to send with DELETE request * @param array $headers HTTP headers * @param integer $timeout Request timeout @@ -190,8 +190,7 @@ protected function makeDeleteRequest(string $url, $data, array $headers = [], $t $response = $this->httpFactory->getHttp([])->delete($url, $headers, $timeout, $data); $this->validateResponse($response); - - } catch (AuthenticationException|RateLimitException|QuotaExceededException $e) { + } catch (AuthenticationException | RateLimitException | QuotaExceededException $e) { throw $e; } catch (ProviderException $e) { throw $e; @@ -341,9 +340,9 @@ protected function getExtensionFromMimeType(string $mimeType): string } /** - * Get audio MIME type from file path. + * Get audio MIME type from the input. * - * @param string $filepath The file path + * @param string $input * * @return string The MIME type * @since __DEPLOY_VERSION__ diff --git a/src/Exception/QuotaExceededException.php b/src/Exception/QuotaExceededException.php index e4beb5d..ff55717 100644 --- a/src/Exception/QuotaExceededException.php +++ b/src/Exception/QuotaExceededException.php @@ -20,14 +20,11 @@ class QuotaExceededException extends AIException * Constructor. * * @param string $provider The AI provider name - * @param string $message The error message - * @param array $context Additional context data * @param int|null $httpStatusCode The actual HTTP status code from response - * @param string|null $providerErrorCode The provider-specific error code * * @since __DEPLOY_VERSION__ */ - public function __construct(string $provider, array $errorData, int $httpStatusCode) + public function __construct(string $provider, array $errorData, $httpStatusCode) { $context = ['error_data' => $errorData]; $providerErrorCode = $errorData['code'] ?? $errorData['error']['code'] ?? null; diff --git a/src/Exception/RateLimitException.php b/src/Exception/RateLimitException.php index b7f343b..48bc519 100644 --- a/src/Exception/RateLimitException.php +++ b/src/Exception/RateLimitException.php @@ -20,14 +20,12 @@ class RateLimitException extends AIException * Constructor. * * @param string $provider The AI provider name - * @param string $message The error message - * @param array $context Additional context data + * @param array $errorData Additional error data * @param int|null $httpStatusCode The actual HTTP status code from response - * @param string|null $providerErrorCode The provider-specific error code * * @since __DEPLOY_VERSION__ */ - public function __construct(string $provider, array $errorData, int $httpStatusCode) + public function __construct(string $provider, array $errorData, $httpStatusCode) { $context = ['error_data' => $errorData]; $providerErrorCode = $errorData['code'] ?? $errorData['error']['code'] ?? null; diff --git a/src/Interface/AudioInterface.php b/src/Interface/AudioInterface.php index 8474563..d38a791 100644 --- a/src/Interface/AudioInterface.php +++ b/src/Interface/AudioInterface.php @@ -22,8 +22,6 @@ interface AudioInterface * Generate speech audio from text input. * * @param string $text The text to convert to speech - * @param string $model The TTS model to use for speech generation - * @param string $voice The voice to use for speech generation * @param array $options Additional options for speech generation * * @return Response @@ -59,7 +57,6 @@ public function getSupportedAudioFormats(): array; * Transcribe audio to text. * * @param string $audioFile Path to the audio file to transcribe - * @param string $model The transcription model to use * @param array $options Additional options for transcription * * @return Response @@ -71,7 +68,6 @@ public function transcribe(string $audioFile, array $options = []): Response; * Translate audio to English text. * * @param string $audioFile Path to audio file to translate - * @param string $model Model to use for translation * @param array $options Additional options * * @return Response diff --git a/src/Provider/AnthropicProvider.php b/src/Provider/AnthropicProvider.php index 821e474..c033ad0 100644 --- a/src/Provider/AnthropicProvider.php +++ b/src/Provider/AnthropicProvider.php @@ -39,15 +39,15 @@ class AnthropicProvider extends AbstractProvider implements ProviderInterface, C * * @param array|\ArrayAccess $options Provider options array. * @param HttpFactory $httpFactory The http factory - * + * * @since __DEPLOY_VERSION__ */ - public function __construct(array $options = [], ?HttpFactory $httpFactory = null) + public function __construct($options = [], ?HttpFactory $httpFactory = null) { parent::__construct($options, $httpFactory); - + $this->baseUrl = $this->getOption('base_url', 'https://api.anthropic.com/v1'); - + // Remove trailing slash if present if (substr($this->baseUrl, -1) === '/') { $this->baseUrl = rtrim($this->baseUrl, '/'); @@ -62,7 +62,7 @@ public function __construct(array $options = [], ?HttpFactory $httpFactory = nul */ public static function isSupported(): bool { - return !empty($_ENV['ANTHROPIC_API_KEY']) || + return !empty($_ENV['ANTHROPIC_API_KEY']) || !empty(getenv('ANTHROPIC_API_KEY')); } @@ -76,7 +76,7 @@ public function getName(): string { return 'Anthropic'; } - + /** * Build HTTP headers for Anthropic API request. * @@ -86,7 +86,7 @@ public function getName(): string private function buildHeaders(): array { $apiKey = $this->getApiKey(); - + return [ 'x-api-key' => $apiKey, 'anthropic-version' => '2023-06-01', // Latest version @@ -103,10 +103,10 @@ private function buildHeaders(): array */ private function getApiKey(): string { - $apiKey = $this->getOption('api_key') ?? - $_ENV['ANTHROPIC_API_KEY'] ?? + $apiKey = $this->getOption('api_key') ?? + $_ENV['ANTHROPIC_API_KEY'] ?? getenv('ANTHROPIC_API_KEY'); - + if (empty($apiKey)) { throw new AuthenticationException( $this->getName(), @@ -114,7 +114,7 @@ private function getApiKey(): string 401 ); } - + return $apiKey; } @@ -143,9 +143,7 @@ private function getModelsEndpoint(): string /** * List available models from Anthropic. * - * @param array $options Optional parameters for the request - * - * @return Response The response containing model list + * @return array * @since __DEPLOY_VERSION__ */ public function getAvailableModels(): array @@ -161,7 +159,7 @@ public function getAvailableModels(): array * Get information about a specific model. * * @param string $modelId The model identifier or alias - * + * * @return Response The response containing model information * @since __DEPLOY_VERSION__ */ @@ -169,7 +167,7 @@ public function getModel(string $modelId): Response { $endpoint = $this->getModelsEndpoint() . '/' . urlencode($modelId); $headers = $this->buildHeaders(); - $httpResponse = $this->makeGetRequest($endpoint, $headers); + $httpResponse = $this->makeGetRequest($endpoint, $headers); $data = $this->parseJsonResponse($httpResponse->getBody()); if (isset($data['error'])) { @@ -189,7 +187,7 @@ public function getModel(string $modelId): Response * * @param string $message The user message to send * @param array $options Additional options - * + * * @return array The request payload * @since __DEPLOY_VERSION__ */ @@ -220,7 +218,7 @@ private function buildChatRequestPayload(string $message, array $options = []): * @param string $message The chat message about the image * @param string $image Image URL or base64 encoded image * @param array $options Additional options - * + * * @return array The request payload * @throws \InvalidArgumentException If model does not support vision capability * @since __DEPLOY_VERSION__ @@ -232,7 +230,7 @@ private function buildVisionRequestPayload(string $message, string $image, array // Determine image format and validate $imageContent = $this->buildImageContent($image); - + $content = [ [ 'type' => 'text', @@ -283,47 +281,47 @@ private function buildVisionRequestPayload(string $message, string $image, array * * @param string $message The message to send * @param array $options Additional options for the request - * + * * @return Response The AI response object * @since __DEPLOY_VERSION__ */ public function chat(string $message, array $options = []): Response { $payload = $this->buildChatRequestPayload($message, $options); - + $headers = $this->buildHeaders(); - + $httpResponse = $this->makePostRequest( $this->getMessagesEndpoint(), json_encode($payload), $headers ); - + return $this->parseAnthropicResponse($httpResponse->getBody()); } - + /** * Generate chat completion with vision capability and return Response. * * @param string $message The chat message about the image. * @param string $image Image URL or base64 encoded image. * @param array $options Additional options for the request. - * + * * @return Response * @since __DEPLOY_VERSION__ */ public function vision(string $message, string $image, array $options = []): Response { $payload = $this->buildVisionRequestPayload($message, $image, $options); - + $headers = $this->buildHeaders(); - + $httpResponse = $this->makePostRequest( $this->getMessagesEndpoint(), json_encode($payload), $headers ); - + return $this->parseAnthropicResponse($httpResponse->getBody()); } @@ -331,7 +329,7 @@ public function vision(string $message, string $image, array $options = []): Res * Parse Anthropic API response into unified Response object. * * @param string $responseBody The JSON response body - * + * * @return Response Unified response object * @since __DEPLOY_VERSION__ */ @@ -342,7 +340,7 @@ private function parseAnthropicResponse(string $responseBody): Response if (isset($data['error'])) { throw new ProviderException($this->getName(), $data); } - + // Get the text content from the first content block $content = ''; if (!empty($data['content'][0]['text'])) { @@ -394,12 +392,12 @@ private function determineAIStatusCode(array $data): int return 200; } } - + /** * Build image content block for Anthropic API. * * @param string $image Image URL or base64 encoded image - * + * * @return array Image content block * @throws \InvalidArgumentException If image format is invalid * @since __DEPLOY_VERSION__ @@ -410,7 +408,7 @@ private function buildImageContent(string $image): array if (filter_var($image, FILTER_VALIDATE_URL)) { $imageData = $this->fetchImageFromUrl($image); $mimeType = $this->detectImageMimeType($imageData); - + return [ 'type' => 'image', 'source' => [ @@ -420,14 +418,14 @@ private function buildImageContent(string $image): array ] ]; } - + // Check if it's already base64 encoded if (preg_match('/^data:image\/([a-zA-Z0-9+\/]+);base64,(.+)$/', $image, $matches)) { $mimeType = 'image/' . $matches[1]; $base64Data = $matches[2]; - + $this->validateImageMimeType($mimeType); - + return [ 'type' => 'image', 'source' => [ @@ -437,12 +435,12 @@ private function buildImageContent(string $image): array ] ]; } - + // If it is a file path if (file_exists($image)) { $imageData = file_get_contents($image); $mimeType = $this->detectImageMimeType($imageData); - + return [ 'type' => 'image', 'source' => [ @@ -452,7 +450,7 @@ private function buildImageContent(string $image): array ] ]; } - + throw InvalidArgumentException::invalidParameter('image', $image, 'anthropic', 'Image must be a valid URL, file path, or base64 encoded data.'); } @@ -460,7 +458,7 @@ private function buildImageContent(string $image): array * Fetch image data from URL. * * @param string $url Image URL - * + * * @return string Image binary data * @throws \Exception If image cannot be fetched * @since __DEPLOY_VERSION__ @@ -468,11 +466,11 @@ private function buildImageContent(string $image): array private function fetchImageFromUrl(string $url): string { $httpResponse = $this->makeGetRequest($url); - + if ($httpResponse->getStatusCode() !== 200) { throw new \Exception("Failed to fetch image from URL: {$url}"); } - + return $httpResponse->getBody(); } @@ -480,14 +478,14 @@ private function fetchImageFromUrl(string $url): string * Validate image MIME type for Anthropic API. * * @param string $mimeType MIME type to validate - * + * * @throws \InvalidArgumentException If MIME type is not supported * @since __DEPLOY_VERSION__ */ private function validateImageMimeType(string $mimeType): void { $supportedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; - + if (!in_array($mimeType, $supportedTypes)) { throw InvalidArgumentException::invalidParameter('image_type', $mimeType, 'anthropic', 'Supported image types: ' . implode(', ', $supportedTypes), ['supported_types' => $supportedTypes]); } diff --git a/src/Provider/OllamaProvider.php b/src/Provider/OllamaProvider.php index c18b088..f0c2e85 100644 --- a/src/Provider/OllamaProvider.php +++ b/src/Provider/OllamaProvider.php @@ -18,7 +18,7 @@ class OllamaProvider extends AbstractProvider { /** * Custom base URL for API requests - * + * * @var string * @since __DEPLOY_VERSION__ */ @@ -29,15 +29,15 @@ class OllamaProvider extends AbstractProvider * * @param array|\ArrayAccess $options Provider options array. * @param HttpFactory $httpFactory The http factory - * + * * @since __DEPLOY_VERSION__ */ - public function __construct(array $options = [], ?HttpFactory $httpFactory = null) + public function __construct($options = [], ?HttpFactory $httpFactory = null) { parent::__construct($options, $httpFactory); - + $this->baseUrl = $this->getOption('base_url', 'http://localhost:11434'); - + // Remove trailing slash if present if (substr($this->baseUrl, -1) === '/') { $this->baseUrl = rtrim($this->baseUrl, '/'); @@ -63,7 +63,7 @@ public static function isSupported(): bool /** * Ensure server is running - * + * * @throws AuthenticationException If the server is not running * @since __DEPLOY_VERSION__ */ @@ -158,14 +158,13 @@ public function getAvailableModels(): array $response = $this->makeGetRequest($this->baseUrl . '/api/tags'); $data = $this->parseJsonResponse($response->getBody()); - + return array_column($data['models'], 'name'); } /** * List models currently loaded into memory (running) and echo their names. * - * @return array Array of running model info * @throws ProviderException If the request fails * @since __DEPLOY_VERSION__ */ @@ -202,12 +201,12 @@ private function checkModelExists(string $modelName, array $availableModels): bo if (in_array($modelName, $availableModels)) { return true; } - + // Check with :latest suffix added if (!str_ends_with($modelName, ':latest') && in_array($modelName . ':latest', $availableModels)) { return true; } - + // Check with :latest suffix removed if (str_ends_with($modelName, ':latest')) { $baseModelName = str_replace(':latest', '', $modelName); @@ -215,7 +214,7 @@ private function checkModelExists(string $modelName, array $availableModels): bo return true; } } - + return false; } @@ -224,7 +223,7 @@ private function checkModelExists(string $modelName, array $availableModels): bo * * @param string $sourceModel The name of the source model to copy * @param string $destinationModel The new name for the copied model - * + * * @return bool True if copy was successful * @since __DEPLOY_VERSION__ */ @@ -270,7 +269,7 @@ public function copyModel(string $sourceModel, string $destinationModel): bool * Delete a model and its data. * * @param string $modelName The name of the model to delete - * + * * @return bool True if deletion was successful * @throws ProviderException If the deletion fails or model does not exist * @since __DEPLOY_VERSION__ @@ -318,8 +317,7 @@ public function deleteModel(string $modelName): bool * @param string $modelName Name of the model to pull * @param bool $stream Whether to stream the response (for progress updates) * @param bool $insecure Allow insecure connections to the library - * - * @return bool True if model was pulled successfully + * * @throws InvalidArgumentException If model doesn't exist in Ollama library * @throws ProviderException If pull fails for other reasons * @since __DEPLOY_VERSION__ @@ -335,7 +333,7 @@ public function pullModel(string $modelName, bool $stream = true, bool $insecure } $endpoint = $this->getPullEndpoint(); - + $requestData = [ 'model' => $modelName ]; @@ -345,7 +343,7 @@ public function pullModel(string $modelName, bool $stream = true, bool $insecure if (!$stream) { $requestData['stream'] = false; } - + try { $jsonData = json_encode($requestData); if (json_last_error() !== JSON_ERROR_NONE) { @@ -361,39 +359,42 @@ public function pullModel(string $modelName, bool $stream = true, bool $insecure $data = $this->parseJsonResponse($response->getBody()); return isset($data['status']) && $data['status'] === 'success'; } - + $body = $response->getBody(); $fullContent = (string) $body; - + $lines = explode("\n", $fullContent); $hasError = false; $errorMessage = ''; - + foreach ($lines as $line) { $line = trim($line); - if (empty($line)) continue; - + if (empty($line)) { + continue; + } + $data = json_decode($line, true); if (json_last_error() !== JSON_ERROR_NONE) { continue; } - + // Check for error in response if (isset($data['error'])) { $errorMessage = $data['error']; - + // Check if this is a "model not found" type error - if (strpos(strtolower($errorMessage), 'file does not exist') !== false || + if ( + strpos(strtolower($errorMessage), 'file does not exist') !== false || strpos(strtolower($errorMessage), 'model') !== false && strpos(strtolower($errorMessage), 'not found') !== false || - strpos(strtolower($errorMessage), 'manifest') !== false && strpos(strtolower($errorMessage), 'not found') !== false) { - + strpos(strtolower($errorMessage), 'manifest') !== false && strpos(strtolower($errorMessage), 'not found') !== false + ) { throw InvalidArgumentException::invalidModel( $modelName, $this->getName(), [] ); } - + // For other errors, throw ProviderException throw new ProviderException( $this->getName(), @@ -401,18 +402,22 @@ public function pullModel(string $modelName, bool $stream = true, bool $insecure ); } } - + // Check if success status exists in the response if (strpos($fullContent, '"status":"success"') !== false) { foreach ($lines as $line) { $line = trim($line); - if (empty($line)) continue; - + if (empty($line)) { + continue; + } + $data = json_decode($line, true); - if (json_last_error() !== JSON_ERROR_NONE) continue; - + if (json_last_error() !== JSON_ERROR_NONE) { + continue; + } + $status = $data['status'] ?? ''; - + if (strpos($status, 'pulling') === 0 && isset($data['digest'])) { $digest = $data['digest']; $total = $data['total'] ?? 0; @@ -429,10 +434,10 @@ public function pullModel(string $modelName, bool $stream = true, bool $insecure echo "\nModel $modelName successfully pulled!\n"; } } - + return true; - } - } catch (InvalidArgumentException $e) { + } + } catch (InvalidArgumentException $e) { throw $e; } catch (ProviderException $e) { throw $e; @@ -470,7 +475,7 @@ private function ensureModelAvailable(string $modelName): bool * * @param string|array $message The user message to send * @param array $options Additional options - * + * * @return array The request payload * @throws \InvalidArgumentException If model does not support chat capability * @since __DEPLOY_VERSION__ @@ -502,7 +507,7 @@ public function buildChatRequestPayload(mixed $message, array $options = []) 'messages' => $messages, 'stream' => false ]; - + if (isset($options['stream'])) { $payload['stream'] = (bool) $options['stream']; } @@ -515,7 +520,7 @@ public function buildChatRequestPayload(mixed $message, array $options = []) * * @param string $message The user message to send * @param array $options Additional options - * + * * @return Response The AI response * @throws ProviderException If the request fails * @since __DEPLOY_VERSION__ @@ -523,9 +528,9 @@ public function buildChatRequestPayload(mixed $message, array $options = []) public function chat(string $message, array $options = []): Response { $payload = $this->buildChatRequestPayload($message, $options); - + $endpoint = $this->getChatEndpoint(); - + $jsonData = json_encode($payload); if (json_last_error() !== JSON_ERROR_NONE) { throw new ProviderException( @@ -533,17 +538,17 @@ public function chat(string $message, array $options = []): Response ['message' => 'Failed to encode request data: ' . json_last_error_msg()] ); } - + $httpResponse = $this->makePostRequest( - $endpoint, + $endpoint, $jsonData, ); - + // Check if this is a streaming response if (isset($payload['stream']) && $payload['stream'] === true) { return $this->parseOllamaStreamingResponse($httpResponse->getBody(), true); } - + return $this->parseOllamaResponse($httpResponse->getBody(), true); } @@ -569,48 +574,48 @@ public function buildGenerateRequestPayload(string $prompt, array $options = []) 'prompt' => $prompt, 'stream' => false ]; - + // Handle optional parameters if (isset($options['stream'])) { $payload['stream'] = (bool) $options['stream']; } - + if (isset($options['suffix'])) { $payload['suffix'] = $options['suffix']; } - + if (isset($options['images']) && is_array($options['images'])) { $payload['images'] = $options['images']; } - + if (isset($options['format'])) { $payload['format'] = $options['format']; } - + if (isset($options['options']) && is_array($options['options'])) { $payload['options'] = $options['options']; } - + if (isset($options['system'])) { $payload['system'] = $options['system']; } - + if (isset($options['template'])) { $payload['template'] = $options['template']; } - + if (isset($options['context'])) { $payload['context'] = $options['context']; } - + if (isset($options['raw'])) { $payload['raw'] = (bool) $options['raw']; } - + if (isset($options['keep_alive'])) { $payload['keep_alive'] = $options['keep_alive']; } - + return $payload; } @@ -619,7 +624,7 @@ public function buildGenerateRequestPayload(string $prompt, array $options = []) * * @param string $prompt The prompt to generate a response for * @param array $options Additional options - * @param callable $callback Optional callback function for streaming responses + * * @return Response The AI response * @throws ProviderException If the request fails * @since __DEPLOY_VERSION__ @@ -627,9 +632,9 @@ public function buildGenerateRequestPayload(string $prompt, array $options = []) public function generate(string $prompt, array $options = []): Response { $payload = $this->buildGenerateRequestPayload($prompt, $options); - + $endpoint = $this->getGenerateEndpoint(); - + $jsonData = json_encode($payload); if (json_last_error() !== JSON_ERROR_NONE) { throw new ProviderException( @@ -637,26 +642,26 @@ public function generate(string $prompt, array $options = []): Response ['message' => 'Failed to encode request data: ' . json_last_error_msg()] ); } - + $httpResponse = $this->makePostRequest( - $endpoint, + $endpoint, $jsonData, ); - + // Check if this is a streaming response if (isset($payload['stream']) && $payload['stream'] === true) { return $this->parseOllamaStreamingResponse($httpResponse->getBody(), false); } - + return $this->parseOllamaResponse($httpResponse->getBody(), false); } /** - * Parse a streaming response from Ollama API + * Parse a streaming response from Ollama API * * @param string $responseBody The raw response body * @param bool $isChat Whether this is a chat response (true) or generate response (false) - * + * * @return Response The parsed response * @since __DEPLOY_VERSION__ */ @@ -665,21 +670,25 @@ private function parseOllamaStreamingResponse(string $responseBody, bool $isChat $lines = explode("\n", $responseBody); $fullContent = ''; $lastMetadata = []; - + foreach ($lines as $line) { $line = trim($line); - if ($line === '') continue; + if ($line === '') { + continue; + } $data = json_decode($line, true); - if (json_last_error() !== JSON_ERROR_NONE) continue; - + if (json_last_error() !== JSON_ERROR_NONE) { + continue; + } + // Accumulate content from each chunk - handle both chat and generate formats if ($isChat && isset($data['message']['content'])) { $fullContent .= $data['message']['content']; } elseif (!$isChat && isset($data['response'])) { $fullContent .= $data['response']; } - + // Keep track of the last metadata if ($data['done'] === true) { $lastMetadata = [ @@ -694,19 +703,19 @@ private function parseOllamaStreamingResponse(string $responseBody, bool $isChat 'eval_count' => $data['eval_count'], 'eval_duration' => $data['eval_duration'] ]; - + // Add chat-specific metadata if ($isChat && isset($data['message']['role'])) { $lastMetadata['role'] = $data['message']['role']; } - + // Add generate-specific metadata if (!$isChat && isset($data['context'])) { $lastMetadata['context'] = $data['context']; } } } - + $statusCode = isset($lastMetadata['done_reason']) ? $this->determineAIStatusCode($lastMetadata) : 200; return new Response( @@ -722,7 +731,7 @@ private function parseOllamaStreamingResponse(string $responseBody, bool $isChat * * @param string $responseBody The raw response body * @param bool $isChat Whether this is a chat response (true) or generate response (false) - * + * * @return Response The parsed response * @throws ProviderException If the response contains an error * @since __DEPLOY_VERSION__ @@ -730,14 +739,14 @@ private function parseOllamaStreamingResponse(string $responseBody, bool $isChat private function parseOllamaResponse(string $responseBody, bool $isChat = true): Response { $data = $this->parseJsonResponse($responseBody); - + if (isset($data['error'])) { throw new ProviderException($this->getName(), $data); } // Extract content based on whether it's a chat or generate response $content = $isChat ? ($data['message']['content'] ?? '') : ($data['response'] ?? ''); - + $statusCode = isset($data['done_reason']) ? $this->determineAIStatusCode($data) : 200; // Build common metadata @@ -753,12 +762,12 @@ private function parseOllamaResponse(string $responseBody, bool $isChat = true): 'eval_count' => $data['eval_count'] ?? 0, 'eval_duration' => $data['eval_duration'] ?? 0 ]; - + // Add chat-specific metadata if ($isChat && isset($data['message']['role'])) { $metadata['role'] = $data['message']['role']; } - + // Add generate-specific metadata if (!$isChat && isset($data['context'])) { $metadata['context'] = $data['context']; @@ -775,21 +784,21 @@ private function parseOllamaResponse(string $responseBody, bool $isChat = true): private function determineAIStatusCode(array $data): int { $finishReason = $data['done_reason']; - + switch ($finishReason) { case 'stop': return 200; - + case 'length': return 206; - + case 'content_filter': return 422; - + case 'tool_calls': case 'function_call': return 202; - + default: return 200; } diff --git a/src/Provider/OpenAIProvider.php b/src/Provider/OpenAIProvider.php index 3460d2d..2027908 100644 --- a/src/Provider/OpenAIProvider.php +++ b/src/Provider/OpenAIProvider.php @@ -124,7 +124,7 @@ class OpenAIProvider extends AbstractProvider implements ChatInterface, ModelInt * * @since __DEPLOY_VERSION__ */ - public function __construct(array $options = [], ?HttpFactory $httpFactory = null) + public function __construct($options = [], ?HttpFactory $httpFactory = null) { parent::__construct($options, $httpFactory); @@ -573,8 +573,6 @@ public function editImage($images, string $prompt, array $options = []): Respons * Generate speech audio from text input. * * @param string $text The text to convert to speech - * @param string $model The model to use for speech synthesis - * @param string $voice The voice to use for speech synthesis * @param array $options Additional options for speech generation * * @return Response @@ -602,7 +600,6 @@ public function speech(string $text, array $options = []): Response * Transcribe audio into text. * * @param string $audioFile Path to audio file - * @param string $model The transcription model to use * @param array $options Additional options for transcription * * @return Response The AI response containing transcribed text @@ -627,7 +624,6 @@ public function transcribe(string $audioFile, array $options = []): Response * Translate audio to English text. * * @param string $audioFile Path to audio file - * @param string $model Model to use for translation * @param array $options Additional options * * @return Response Translation response @@ -722,7 +718,6 @@ public function moderate($input, array $options = []): array * @param array $moderationResult Result from moderate() method * * @return bool - * @throws \InvalidArgumentException * @since __DEPLOY_VERSION__ */ public function isContentFlagged(array $moderationResult): bool @@ -860,7 +855,7 @@ private function buildChatRequestPayload(string $message, array $options = []): private function buildVisionRequestPayload(string $message, string $image, string $capability, array $options = []): array { $model = $options['model'] ?? $this->defaultModel ?? $this->getOption('model', 'gpt-4o-mini'); - + if (!$this->isModelCapable($model, $capability)) { throw InvalidArgumentException::invalidModel($model, 'openai', self::VISION_MODELS, $capability); } @@ -909,7 +904,6 @@ private function buildVisionRequestPayload(string $message, string $image, strin * * @param string $prompt The image generation prompt. * @param array $options Additional options for the request. - * @param string $capability Required capability. * * @return array The request payload. * @since __DEPLOY_VERSION__ @@ -1026,7 +1020,7 @@ private function buildImageRequestPayload(string $prompt, array $options): array private function buildImageVariationPayload(string $imagePath, array $options): array { $model = $options['model'] ?? $this->defaultModel ?? 'dall-e-2'; - + // Only dall-e-2 supports variations if ($model !== 'dall-e-2') { throw InvalidArgumentException::invalidModel($model, 'openai', ['dall-e-2'], 'image variation'); @@ -1194,8 +1188,6 @@ private function buildImageEditPayload($images, string $prompt, array $options): * Build payload for text-to-speech request. * * @param string $text The text to convert to speech - * @param string $model The model to use for speech synthesis - * @param string $voice The voice to use for speech synthesis * @param array $options Additional options for speech generation * * @return array The request payload. @@ -1268,7 +1260,6 @@ private function buildSpeechPayload(string $text, array $options): array * Build payload for transcription request. * * @param string $audioFile The audio file - * @param string $model The transcription model * @param array $options Additional options * * @return array Form data for multipart request @@ -1281,7 +1272,7 @@ private function buildTranscriptionPayload(string $audioFile, array $options): a $this->validateAudioFile($audioFile); $model = $options['model'] ?? $this->defaultModel ?? $this->getOption('model', 'gpt-4o-transcribe'); - + // Validate model if (!in_array($model, self::TRANSCRIPTION_MODELS)) { throw InvalidArgumentException::invalidModel($model, 'openai', self::TRANSCRIPTION_MODELS, 'transcription'); @@ -1378,7 +1369,6 @@ private function buildTranscriptionPayload(string $audioFile, array $options): a * Build payload for translation request. * * @param string $audioFile Path to the audio file - * @param string $model The translation model to use * @param array $options Additional options for translation * * @return array Form data for multipart request @@ -1389,7 +1379,7 @@ private function buildTranslationPayload(string $audioFile, array $options): arr { // Validate audio file $this->validateAudioFile($audioFile); - + $model = $options['model'] ?? $this->defaultModel ?? $this->getOption('model', 'whisper-1'); // Validate model diff --git a/src/Response/Response.php b/src/Response/Response.php index f838009..c2c7030 100644 --- a/src/Response/Response.php +++ b/src/Response/Response.php @@ -12,29 +12,22 @@ use Joomla\Filesystem\File; use Joomla\Filesystem\Folder; use Joomla\Filesystem\Path; +use Joomla\Http\Response as HttpResponse; /** * AI response data object class. * * @since __DEPLOY_VERSION__ */ -class Response +class Response extends HttpResponse { /** - * The content of the response. + * The provider of the response. * * @var string * @since __DEPLOY_VERSION__ */ - private $content; - - /** - * The status code of the response. - * - * @var int - * @since __DEPLOY_VERSION__ - */ - private $statusCode; + private string $provider; /** * The metadata of the response. @@ -42,15 +35,7 @@ class Response * @var array * @since __DEPLOY_VERSION__ */ - private $metadata; - - /** - * The provider of the response. - * - * @var string - * @since __DEPLOY_VERSION__ - */ - private $provider; + private array $metadata; /** * Constructor. @@ -64,21 +49,25 @@ class Response */ public function __construct(string $content, string $provider, array $metadata = [], int $status = 200) { - $this->content = $content; + parent::__construct('php://memory', $status); + + $body = $this->getBody(); + $body->write($content); + $body->rewind(); + $this->provider = $provider; $this->metadata = $metadata; - $this->statusCode = $status; } /** * Get the content of the response. * - * @return string The content of the response. + * @return string * @since __DEPLOY_VERSION__ */ public function getContent(): string { - return $this->content; + return (string) $this->getBody(); } /** @@ -191,55 +180,4 @@ public function getProvider(): string { return $this->provider; } - - /** - * Get the status code of the response. - * - * @return int The status code of the response. - * @since __DEPLOY_VERSION__ - */ - public function getStatusCode(): int - { - return $this->statusCode; - } - - /** - * Magic method to access properties of the response object. - * - * @param string $name The name of the property to get. - * - * @return mixed The value of the property. - * @since __DEPLOY_VERSION__ - */ - public function __get($name) - { - switch (strtolower($name)) { - case 'content': - return $this->getContent(); - - case 'metadata': - return $this->getMetadata(); - - case 'provider': - return $this->getProvider(); - - case 'statuscode': - return $this->getStatusCode(); - - default: - $trace = debug_backtrace(); - - trigger_error( - sprintf( - 'Undefined property via __get(): %s in %s on line %s', - $name, - $trace[0]['file'], - $trace[0]['line'] - ), - E_USER_NOTICE - ); - - break; - } - } }