├── art.png ├── tests ├── fixtures │ ├── audio.wav │ ├── australian_shepherd_puppies.png │ ├── batch_file_audio.jsonl │ ├── batch_file_invalid.jsonl │ └── batch_file.jsonl ├── ModelsTest.php ├── TestCase.php ├── ReasoningTest.php ├── AudioTest.php ├── VisionTest.php ├── BatchManagerTest.php ├── ChatTest.php ├── GroqTest.php ├── FileManagerTest.php └── EnvironmentVariablesTest.php ├── examples ├── .env.example ├── .gitignore ├── _input.php ├── models.php ├── composer.json ├── chat.php ├── chat-streaming.php ├── vision-url.php ├── json-mode.php ├── jsonl-processing.php ├── vision-feedback.php ├── vision-simple.php ├── audio-translations.php ├── index.php ├── audio-speech.php ├── vision-multiple.php ├── tool-calling-advanced.php ├── tool-calling.php ├── reasoning.php ├── audio-transcriptions.php └── files.php ├── .gitattributes ├── .env.example ├── .gitignore ├── src ├── Chat.php ├── Audio.php ├── Models.php ├── Stream.php ├── Reasoning.php ├── Vision.php ├── Speech.php ├── Batch.php ├── File.php ├── Translations.php ├── Transcriptions.php ├── Groq.php ├── Completions.php ├── GroqException.php ├── BatchManager.php └── FileManager.php ├── composer.json ├── LICENSE ├── .github └── workflows │ └── tests.yml └── CHANGELOG.md /art.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucianotonet/groq-php/HEAD/art.png -------------------------------------------------------------------------------- /tests/fixtures/audio.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucianotonet/groq-php/HEAD/tests/fixtures/audio.wav -------------------------------------------------------------------------------- /tests/fixtures/australian_shepherd_puppies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucianotonet/groq-php/HEAD/tests/fixtures/australian_shepherd_puppies.png -------------------------------------------------------------------------------- /examples/.env.example: -------------------------------------------------------------------------------- 1 | GROQ_API_KEY= # Get your API key on https://console.groq.com/keys 2 | GROQ_API_BASE=https://api.groq.com/openai/v1 3 | 4 | DEEPGRAM_API_KEY= -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /.yarn/** linguist-vendored 2 | /.yarn/releases/* binary 3 | /.yarn/plugins/**/* binary 4 | /.pnp.* binary linguist-generated 5 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /.php-cs-fixer.cache 3 | /.php-cs-fixer.php 4 | /phpunit.xml 5 | /vendor/ 6 | *.swp 7 | *.swo 8 | playground/* 9 | .idea 10 | .env -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Groq API Key 2 | # Get your API key from https://console.groq.com/keys 3 | GROQ_API_KEY=your_api_key_here 4 | 5 | # Optional: Custom API Base URL 6 | # GROQ_API_BASE=https://api.groq.com/openai/v1 7 | 8 | DEEPGRAM_API_KEY= -------------------------------------------------------------------------------- /examples/_input.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
-------------------------------------------------------------------------------- /tests/fixtures/batch_file_audio.jsonl: -------------------------------------------------------------------------------- 1 | {"custom_id":"audio-request-1","method":"POST","url":"/v1/audio/transcriptions","body":{"model":"whisper-large-v3","language":"en","url":"https://github.com/lucianotonet/groq-php/raw/main/tests/fixtures/audio.wav","response_format":"verbose_json"}} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /.php-cs-fixer.cache 3 | /.php-cs-fixer.php 4 | /composer.lock 5 | /phpunit.xml 6 | /vendor/ 7 | *.swp 8 | *.swo 9 | playground/* 10 | .idea 11 | .env 12 | /examples/.env 13 | /examples/vendor/ 14 | CHANGELOG_GEN.sh 15 | .cursorrules 16 | .cursor* 17 | proj2md.py -------------------------------------------------------------------------------- /tests/ModelsTest.php: -------------------------------------------------------------------------------- 1 | groq->models()->list(); 11 | 12 | $this->assertIsArray($models); 13 | $this->assertNotEmpty($models); 14 | $this->assertArrayHasKey('data', $models); 15 | } 16 | } -------------------------------------------------------------------------------- /src/Chat.php: -------------------------------------------------------------------------------- 1 | groq = $groq; 20 | } 21 | 22 | /** 23 | * @return Completions 24 | */ 25 | public function completions(): Completions 26 | { 27 | return new Completions($this->groq); 28 | } 29 | } -------------------------------------------------------------------------------- /examples/models.php: -------------------------------------------------------------------------------- 1 |
2 |

Available Models:

3 | 4 | models()->list(); 6 | ?> 7 | 8 | 17 |
-------------------------------------------------------------------------------- /examples/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lucianotonet/example-groq-php", 3 | "type": "project", 4 | "authors": [ 5 | { 6 | "name": "Luciano Tonet", 7 | "email": "tonetlds@gmail.com" 8 | } 9 | ], 10 | "require": { 11 | "vlucas/phpdotenv": "^5.6", 12 | "guzzlehttp/guzzle": "^7.9", 13 | "lucianotonet/groq-php": "^0.0.8" 14 | }, 15 | "require-dev": { 16 | "phpunit/phpunit": "^11.0" 17 | }, 18 | "repositories": [ 19 | { 20 | "type": "path", 21 | "url": "../../groq-php", 22 | "options": { 23 | "symlink": true 24 | } 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lucianotonet/groq-php", 3 | "description": "A powerful PHP library for seamless integration with the GroqCloud API", 4 | "type": "package", 5 | "require": { 6 | "php": "^8.1.0", 7 | "guzzlehttp/guzzle": "^7.9", 8 | "vlucas/phpdotenv": "^5.6" 9 | }, 10 | "license": "MIT", 11 | "autoload": { 12 | "psr-4": { 13 | "LucianoTonet\\GroqPHP\\": "src/" 14 | } 15 | }, 16 | "autoload-dev": { 17 | "psr-4": { 18 | "LucianoTonet\\GroqPHP\\Tests\\": "tests/" 19 | } 20 | }, 21 | "authors": [ 22 | { 23 | "name": "Luciano Tonet", 24 | "email": "tonetlds@gmail.com" 25 | } 26 | ], 27 | "require-dev": { 28 | "phpunit/phpunit": "^11.3" 29 | }, 30 | "scripts": { 31 | "test": "phpunit tests/" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | load(); 20 | } catch (\Exception $e) { 21 | $this->markTestSkipped('Environment file not found. Copy .env.example to .env and configure it.'); 22 | } 23 | 24 | $apiKey = getenv('GROQ_API_KEY'); 25 | if (!$apiKey) { 26 | $this->markTestSkipped('GROQ_API_KEY not found in environment variables.'); 27 | } 28 | $this->groq = new Groq($apiKey); 29 | } 30 | } -------------------------------------------------------------------------------- /examples/chat.php: -------------------------------------------------------------------------------- 1 |
2 | user: $message
"; 10 | 11 | try { 12 | $response = $groq->chat()->completions()->create([ 13 | 'model' => 'llama-3.1-8b-instant', 14 | 'messages' => [ 15 | [ 16 | 'role' => 'user', 17 | 'content' => $message 18 | ] 19 | ], 20 | ]); 21 | 22 | echo "assistant: "; 23 | echo $response['choices'][0]['message']['content']; 24 | } catch (GroqException $err) { 25 | echo "assistant:
".$err->getMessage()."
"; 26 | 27 | echo "
";
28 |         print_r($err->getError());
29 | 
30 |         echo "
"; 31 | } 32 | } 33 | ?> 34 |
-------------------------------------------------------------------------------- /tests/fixtures/batch_file_invalid.jsonl: -------------------------------------------------------------------------------- 1 | {"custom_id": "chat-request-1", "method": "POST", "url": "/v1/chat/completions", "model": "llama-3.1-8b-instant", "messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "What is quantum computing?"}]} 2 | {"custom_id": "audio-request-1", "method": "POST", "url": "/v1/audio/transcriptions", "model": "whisper-large-v3", "language": "en", "url": "https://github.com/voxserv/audio_quality_testing_samples/raw/refs/heads/master/testaudio/8000/test01_20s.wav"} 3 | {"custom_id": "chat-request-2", "method": "POST", "url": "/v1/chat/completions", "model": "llama-3.3-70b-versatile", "messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Explain machine learning in simple terms."}]} 4 | {"custom_id":"audio-request-2","method":"POST","url":"/v1/audio/translations","model":"whisper-large-v3","language":"en","url":"https://console.groq.com/audio/batch/sample-zh.wav"} 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Luciano Tonet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/fixtures/batch_file.jsonl: -------------------------------------------------------------------------------- 1 | {"custom_id": "chat-request-1", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "llama-3.1-8b-instant", "messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "What is quantum computing?"}]}} 2 | {"custom_id": "audio-request-1", "method": "POST", "url": "/v1/audio/transcriptions", "body": {"model": "whisper-large-v3", "language": "en", "url": "https://github.com/voxserv/audio_quality_testing_samples/raw/refs/heads/master/testaudio/8000/test01_20s.wav", "response_format": "verbose_json", "timestamp_granularities": ["segment"]}} 3 | {"custom_id": "chat-request-2", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "llama-3.3-70b-versatile", "messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Explain machine learning in simple terms."}]}} 4 | {"custom_id":"audio-request-2","method":"POST","url":"/v1/audio/translations","body":{"model":"whisper-large-v3","language":"en","url":"https://console.groq.com/audio/batch/sample-zh.wav","response_format":"verbose_json","timestamp_granularities":["segment"]}} 5 | -------------------------------------------------------------------------------- /examples/chat-streaming.php: -------------------------------------------------------------------------------- 1 |
2 | user: $message
"; 10 | 11 | try { 12 | $response = $groq->chat()->completions()->create([ 13 | 'model' => 'llama-3.1-8b-instant', 14 | 'messages' => [ 15 | [ 16 | 'role' => 'user', 17 | 'content' => $message 18 | ] 19 | ], 20 | 'stream' => true 21 | ]); 22 | 23 | foreach ($response->chunks() as $chunk) { 24 | if (isset($chunk['choices'][0]['delta']['role'])) { 25 | echo "" . $chunk['choices'][0]['delta']['role'] . ": "; 26 | } 27 | 28 | if (isset($chunk['choices'][0]['delta']['content'])) { 29 | echo $chunk['choices'][0]['delta']['content']; 30 | } 31 | 32 | // Chame ob_flush() e flush() na ordem correta 33 | ob_flush(); // Limpa o buffer de saída 34 | flush(); // Envia os dados para o cliente 35 | } 36 | } catch (\LucianoTonet\GroqPHP\GroqException $err) { 37 | echo "assistant:
Desculpe, ocorreu um erro: " . $err->getMessage() . "
"; 38 | } 39 | } 40 | ?> 41 |
-------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | release: 9 | types: [ published ] 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | # Rodando apenas com PHP 8.2 para economizar créditos da API, 15 | # já que os testes fazem chamadas reais à API do Groq 16 | strategy: 17 | matrix: 18 | php-version: ['8.2'] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Setup PHP 24 | uses: shivammathur/setup-php@v2 25 | with: 26 | php-version: ${{ matrix.php-version }} 27 | extensions: fileinfo, json 28 | coverage: xdebug 29 | 30 | - name: Validate composer.json 31 | run: composer validate --strict 32 | 33 | - name: Cache Composer packages 34 | id: composer-cache 35 | uses: actions/cache@v4 36 | with: 37 | path: vendor 38 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 39 | restore-keys: | 40 | ${{ runner.os }}-php- 41 | 42 | - name: Install dependencies 43 | run: composer install --prefer-dist --no-progress 44 | 45 | - name: Create .env file 46 | run: | 47 | echo "GROQ_API_KEY=${{ secrets.GROQ_API_KEY }}" > .env 48 | echo "GROQ_API_BASE=https://api.groq.com/openai/v1" >> .env 49 | 50 | - name: Run test suite 51 | run: composer test 52 | -------------------------------------------------------------------------------- /src/Audio.php: -------------------------------------------------------------------------------- 1 | groq = $groq; 23 | } 24 | 25 | /** 26 | * Returns a Transcriptions object to work with audio transcriptions. 27 | * 28 | * @return Transcriptions An instance of the Transcriptions class. 29 | */ 30 | public function transcriptions(): Transcriptions 31 | { 32 | return new Transcriptions($this->groq); 33 | } 34 | 35 | /** 36 | * Returns a Translations object to work with audio translations. 37 | * 38 | * @return Translations An instance of the Translations class. 39 | */ 40 | public function translations(): Translations 41 | { 42 | return new Translations($this->groq); 43 | } 44 | 45 | /** 46 | * Returns a Speech object to work with text-to-speech. 47 | * 48 | * @return Speech An instance of the Speech class. 49 | */ 50 | public function speech(): Speech 51 | { 52 | return new Speech($this->groq); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/vision-url.php: -------------------------------------------------------------------------------- 1 |
2 | Prompt: $prompt

"; 8 | 9 | try { 10 | $response = $groq->vision()->analyze($imageUrl, $prompt); 11 | 12 | echo "

Resposta do Modelo:

"; 13 | echo "

" . $response['choices'][0]['message']['content'] . "

"; 14 | } catch (LucianoTonet\GroqPHP\GroqException $err) { 15 | echo "

Erro: " . $err->getMessage() . "

"; 16 | } 17 | } 18 | ?> 19 | 20 |
21 |
22 | 23 | 25 |
26 |
27 | 28 | 30 |
31 | 34 |
35 |
-------------------------------------------------------------------------------- /examples/json-mode.php: -------------------------------------------------------------------------------- 1 |
2 | user: $message
"; 9 | 10 | try { 11 | $response = $groq->chat()->completions()->create([ 12 | 'model' => 'llama3-70b-8192', 13 | 'messages' => [ 14 | [ 15 | 'role' => 'system', 16 | 'content' => "You are an API and shall responde only with valid JSON.", 17 | ], 18 | [ 19 | 'role' => 'user', 20 | 'content' => $message, 21 | ], 22 | ], 23 | 'response_format' => ['type' => 'json_object'] 24 | ]); 25 | echo "assistant:
"; 26 | echo "
";
27 |         echo json_encode(json_decode($response['choices'][0]['message']['content']), JSON_PRETTY_PRINT);
28 |         echo "
"; 29 | } catch (LucianoTonet\GroqPHP\GroqException $e) { 30 | echo "Error:
"; 31 | echo "
";
32 |         echo htmlspecialchars(print_r($e->getMessage(), true));
33 |         echo "

"; 34 | 35 | if($e->getFailedGeneration()) { 36 | echo "Failed Generation (invalid JSON):
"; 37 | echo "
";
38 |             echo htmlspecialchars(print_r($e->getFailedGeneration(), true));
39 |             echo "
"; 40 | } 41 | } 42 | } else { 43 | echo "Ask anythings to simulate an API.
Results will be mocked for demo purposes.


"; 44 | } 45 | ?> 46 |
-------------------------------------------------------------------------------- /src/Models.php: -------------------------------------------------------------------------------- 1 | groq = $groq; 24 | } 25 | 26 | /** 27 | * Fetches the list of models from the GROQ API. 28 | * 29 | * @return array The list of models as an associative array. 30 | * @throws GroqException If there is an error fetching the list of models. 31 | */ 32 | public function list(): array 33 | { 34 | // Create a new GET request with authorization header 35 | $request = new Request('GET', $this->groq->baseUrl() . '/models', [ 36 | 'Authorization' => 'Bearer ' . $this->groq->apiKey() 37 | ]); 38 | 39 | try { 40 | // Make the request and decode the JSON response 41 | $response = $this->groq->makeRequest($request); 42 | return json_decode($response->getBody()->getContents(), true); 43 | } catch (RequestException $e) { 44 | // Handle specific request exceptions 45 | $responseBody = $e->getResponse() ? (string) $e->getResponse()->getBody() : 'No response body available'; 46 | throw new GroqException('Error fetching the list of models: ' . $responseBody, $e->getCode(), 'ListModelsException', []); 47 | } catch (GuzzleException $e) { 48 | // Handle general Guzzle exceptions 49 | throw new GroqException('An unexpected error occurred: ' . $e->getMessage(), $e->getCode(), 'ListModelsException', []); 50 | } catch (\Exception $e) { 51 | // Handle any other unhandled exceptions 52 | throw new GroqException('An unexpected error occurred: ' . $e->getMessage(), $e->getCode(), 'ListModelsException', []); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /examples/jsonl-processing.php: -------------------------------------------------------------------------------- 1 | load(); 10 | 11 | // Initialize Groq client 12 | $groq = new Groq(getenv('GROQ_API_KEY')); 13 | 14 | // Create a sample JSONL file 15 | $jsonlFile = __DIR__ . '/sample-requests.jsonl'; 16 | 17 | // Sample requests 18 | $requests = [ 19 | [ 20 | 'custom_id' => 'request-1', 21 | 'method' => 'POST', 22 | 'url' => '/v1/chat/completions', 23 | 'body' => [ 24 | 'model' => 'llama-3.1-8b-instant', 25 | 'messages' => [ 26 | ['role' => 'system', 'content' => 'You are a helpful assistant.'], 27 | ['role' => 'user', 'content' => 'What is 2+2?'] 28 | ] 29 | ] 30 | ], 31 | [ 32 | 'custom_id' => 'request-2', 33 | 'method' => 'POST', 34 | 'url' => '/v1/chat/completions', 35 | 'body' => [ 36 | 'model' => 'llama-3.1-8b-instant', 37 | 'messages' => [ 38 | ['role' => 'system', 'content' => 'You are a helpful assistant.'], 39 | ['role' => 'user', 'content' => 'What is 3+3?'] 40 | ] 41 | ] 42 | ] 43 | ]; 44 | 45 | // Create JSONL file 46 | $jsonlContent = ''; 47 | foreach ($requests as $request) { 48 | $jsonlContent .= json_encode($request) . "\n"; 49 | } 50 | file_put_contents($jsonlFile, $jsonlContent); 51 | 52 | try { 53 | echo "Uploading JSONL file...\n"; 54 | $file = $groq->files()->upload($jsonlFile, 'jsonl'); 55 | echo "File uploaded successfully. ID: {$file->id}\n"; 56 | 57 | echo "\nListing files...\n"; 58 | $files = $groq->files()->list('jsonl', ['limit' => 10]); 59 | echo "Found " . count($files['data']) . " files\n"; 60 | 61 | echo "\nDownloading file content...\n"; 62 | $content = $groq->files()->download($file->id); 63 | echo "File content:\n$content\n"; 64 | 65 | echo "\nDeleting file...\n"; 66 | $groq->files()->delete($file->id); 67 | echo "File deleted successfully\n"; 68 | 69 | } catch (\LucianoTonet\GroqPHP\GroqException $e) { 70 | echo "Error: " . $e->getMessage() . "\n"; 71 | } finally { 72 | // Clean up the sample file 73 | if (file_exists($jsonlFile)) { 74 | unlink($jsonlFile); 75 | } 76 | } -------------------------------------------------------------------------------- /examples/vision-feedback.php: -------------------------------------------------------------------------------- 1 |
2 | Prompt: $prompt

"; 9 | echo "

Feedback: $feedback

"; 10 | 11 | try { 12 | $response = $groq->vision()->analyze($imagePath, $prompt); 13 | 14 | echo "

Resposta do Modelo:

"; 15 | echo "

" . $response['choices'][0]['message']['content'] . "

"; 16 | 17 | // Aqui você pode processar o feedback, se necessário 18 | } catch (LucianoTonet\GroqPHP\GroqException $err) { 19 | echo "

Erro: " . $err->getMessage() . "

"; 20 | } 21 | } 22 | ?> 23 | 24 |
25 |
26 | 27 | 33 |
34 |
35 | 36 | 38 |
39 |
40 | 41 | 43 |
44 | 47 |
48 |
-------------------------------------------------------------------------------- /examples/vision-simple.php: -------------------------------------------------------------------------------- 1 |
2 | Prompt: $prompt

"; 7 | 8 | try { 9 | $tempDir = sys_get_temp_dir(); 10 | $imagePath = $tempDir . '/' . basename($_FILES['image']['name']); 11 | 12 | if ($_FILES['image']['error'] !== UPLOAD_ERR_OK) { 13 | throw new Exception("Erro no upload: " . $_FILES['image']['error']); 14 | } 15 | 16 | if (!move_uploaded_file($_FILES['image']['tmp_name'], $imagePath)) { 17 | throw new Exception("Falha ao mover o arquivo para o diretório temporário."); 18 | } 19 | 20 | $response = $groq->vision()->analyze($imagePath, $prompt); 21 | 22 | echo "

Resposta do Modelo:

"; 23 | echo "

" . $response['choices'][0]['message']['content'] . "

"; 24 | } catch (LucianoTonet\GroqPHP\GroqException $err) { 25 | echo "

Erro Groq: " . $err->getMessage() . "

"; 26 | } catch (Exception $e) { 27 | echo "

Erro: " . $e->getMessage() . "

"; 28 | } finally { 29 | if (isset($imagePath) && file_exists($imagePath)) { 30 | unlink($imagePath); 31 | } 32 | } 33 | } 34 | ?> 35 | 36 |
37 |
38 | 39 | 45 |
46 |
47 | 48 | 50 |
51 | 54 |
55 |
-------------------------------------------------------------------------------- /examples/audio-translations.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | $newFilePath, 34 | 'model' => 'whisper-large-v3', 35 | 'response_format' => $_POST['response_format'] ?? 'json', 36 | 'temperature' => $_POST['temperature'] ?? 0.0, 37 | ]; 38 | 39 | if (isset($_POST['prompt'])) { 40 | $translationParams['prompt'] = $_POST['prompt']; 41 | } 42 | 43 | $translation = $groq->audio()->translations()->create($translationParams); 44 | 45 | if ($translationParams['response_format'] === 'text') { 46 | echo $translation; // Retorna a resposta em texto diretamente 47 | } else { 48 | echo json_encode($translation, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); // Retorna a resposta em JSON 49 | } 50 | } catch (LucianoTonet\GroqPHP\GroqException $e) { 51 | echo "Error:
" . htmlspecialchars($e->getMessage()) . "
"; 52 | } finally { 53 | if (file_exists($newFilePath)) { 54 | unlink($newFilePath); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /tests/ReasoningTest.php: -------------------------------------------------------------------------------- 1 | "deepseek-r1-distill-llama-70b", 22 | 'reasoning_format' => "raw" 23 | ]; 24 | 25 | try { 26 | $response = $this->groq->reasoning()->analyze($prompt, $options); 27 | } catch (GroqException $e) { 28 | $this->fail("Error in reasoning analysis: " . $e->getMessage()); 29 | } 30 | 31 | $this->assertArrayHasKey('choices', $response); 32 | $this->assertNotEmpty($response['choices']); 33 | $this->assertArrayHasKey('message', $response['choices'][0]); 34 | $this->assertArrayHasKey('content', $response['choices'][0]['message']); 35 | } 36 | 37 | public function testReasoningWithCustomOptions() 38 | { 39 | $prompt = "Explain the process of photosynthesis."; 40 | $options = [ 41 | 'temperature' => 0.6, 42 | 'max_completion_tokens' => 1024, 43 | 'model' => "deepseek-r1-distill-llama-70b", 44 | 'reasoning_format' => "raw" 45 | ]; 46 | 47 | try { 48 | $response = $this->groq->reasoning()->analyze($prompt, $options); 49 | } catch (GroqException $e) { 50 | $this->fail("Error in reasoning with custom options: " . $e->getMessage()); 51 | } 52 | 53 | $this->assertArrayHasKey('choices', $response); 54 | $this->assertNotEmpty($response['choices']); 55 | } 56 | 57 | public function testReasoningWithStreaming() 58 | { 59 | $prompt = "Explain quantum entanglement."; 60 | $options = [ 61 | 'stream' => true, 62 | 'model' => "deepseek-r1-distill-llama-70b", 63 | ]; 64 | 65 | try { 66 | $stream = $this->groq->reasoning()->analyze($prompt, $options); 67 | $this->assertInstanceOf(\LucianoTonet\GroqPHP\Stream::class, $stream); 68 | 69 | $hasContent = false; 70 | foreach ($stream->chunks() as $chunk) { 71 | if (isset($chunk['choices'][0]['delta']['content'])) { 72 | $hasContent = true; 73 | break; 74 | } 75 | } 76 | $this->assertTrue($hasContent); 77 | } catch (GroqException $e) { 78 | $this->fail("Error in reasoning streaming: " . $e->getMessage()); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /examples/index.php: -------------------------------------------------------------------------------- 1 | load(); 10 | 11 | try { 12 | $groq = new Groq(getenv('GROQ_API_KEY'), [ 13 | 'baseUrl' => getenv('GROQ_API_BASE') 14 | ]); 15 | } catch (GroqException $e) { 16 | echo $e->getMessage(); 17 | die(); 18 | } 19 | 20 | $accept = isset($_SERVER['HTTP_ACCEPT']) ? $_SERVER['HTTP_ACCEPT'] : ''; 21 | $accept = explode(',', $accept); 22 | 23 | if (!in_array('text/html', $accept)) { 24 | require __DIR__ . '/' . $_GET['page'] . '.php'; 25 | exit; 26 | } 27 | 28 | ?> 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | Groq PHP Examples 39 | 40 | 41 | 42 | 43 |
44 | 45 |
46 |

Examples:

47 | 62 |
63 |

Advanced:

64 | 67 |
68 | 69 |
70 | 77 |
78 | 79 |
80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /examples/audio-speech.php: -------------------------------------------------------------------------------- 1 | load(); 12 | 13 | // Inicializa o cliente Groq com a chave API 14 | $groq = new LucianoTonet\GroqPHP\Groq([ 15 | 'api_key' => $_ENV['GROQ_API_KEY'], 16 | ]); 17 | 18 | try { 19 | echo "Exemplo de Text-to-Speech (TTS)\n"; 20 | echo "-------------------------------\n\n"; 21 | 22 | // Definir o texto que será convertido em áudio 23 | $text = "Olá! Este é um exemplo de conversão de texto para voz utilizando a API do GroqCloud."; 24 | echo "Texto a ser convertido: \"$text\"\n\n"; 25 | 26 | // Exemplo 1: Salvar o áudio diretamente em um arquivo 27 | echo "Exemplo 1: Salvando o áudio em um arquivo...\n"; 28 | $outputFile = __DIR__ . '/output/speech_example.wav'; 29 | 30 | // Verifica se o diretório de saída existe, senão cria 31 | if (!file_exists(__DIR__ . '/output')) { 32 | mkdir(__DIR__ . '/output', 0755, true); 33 | } 34 | 35 | // Cria o áudio e salva no arquivo 36 | $result = $groq->audio()->speech() 37 | ->model('playai-tts') 38 | ->input($text) 39 | ->voice('Bryan-PlayAI') // Escolhe a voz 40 | ->responseFormat('wav') // Formato de saída 41 | ->save($outputFile); 42 | 43 | if ($result) { 44 | echo "Áudio salvo com sucesso em: $outputFile\n"; 45 | echo "Tamanho do arquivo: " . filesize($outputFile) . " bytes\n\n"; 46 | } else { 47 | echo "Falha ao salvar o áudio.\n\n"; 48 | } 49 | 50 | // Exemplo 2: Obter o conteúdo do áudio como stream 51 | echo "Exemplo 2: Obtendo o conteúdo do áudio como stream...\n"; 52 | $audioStream = $groq->audio()->speech() 53 | ->model('playai-tts') 54 | ->input('This is another example text that will be converted to speech.') 55 | ->voice('Bryan-PlayAI') 56 | ->create(); 57 | 58 | // Você pode processar o stream conforme necessário 59 | // Por exemplo, enviá-lo diretamente para o navegador com os headers apropriados: 60 | /* 61 | header('Content-Type: audio/wav'); 62 | header('Content-Disposition: inline; filename="speech.wav"'); 63 | echo $audioStream; 64 | */ 65 | 66 | echo "Stream de áudio obtido com sucesso!\n"; 67 | 68 | // Exemplo 3: Utilizar voz em árabe 69 | echo "\nExemplo 3: Utilizando o modelo de árabe...\n"; 70 | $arabicText = "مرحبا! هذا مثال على تحويل النص إلى كلام باستخدام واجهة برمجة تطبيقات GroqCloud."; 71 | $outputFileArabic = __DIR__ . '/output/speech_arabic.wav'; 72 | 73 | $result = $groq->audio()->speech() 74 | ->model('playai-tts-arabic') 75 | ->input($arabicText) 76 | ->voice('Arwa-PlayAI') // Voz em árabe 77 | ->save($outputFileArabic); 78 | 79 | if ($result) { 80 | echo "Áudio em árabe salvo com sucesso em: $outputFileArabic\n"; 81 | echo "Tamanho do arquivo: " . filesize($outputFileArabic) . " bytes\n"; 82 | } else { 83 | echo "Falha ao salvar o áudio em árabe.\n"; 84 | } 85 | 86 | } catch (Exception $e) { 87 | echo "Erro: " . $e->getMessage() . "\n"; 88 | } -------------------------------------------------------------------------------- /examples/vision-multiple.php: -------------------------------------------------------------------------------- 1 |
2 | O prompt não pode estar vazio.

"; 10 | } else { 11 | echo "

Prompt para todas as imagens: " . htmlspecialchars($prompt) . "

"; 12 | 13 | foreach ($imagePaths as $index => $imagePath) { 14 | $tempDir = sys_get_temp_dir(); 15 | $tempImagePath = $tempDir . '/' . basename($_FILES['images']['name'][$index]); 16 | 17 | if ($_FILES['images']['error'][$index] !== UPLOAD_ERR_OK) { 18 | echo "

Erro no upload da imagem " . ($index + 1) . ": " . $_FILES['images']['error'][$index] . "

"; 19 | continue; 20 | } 21 | 22 | if (!move_uploaded_file($imagePath, $tempImagePath)) { 23 | echo "

Falha ao mover o arquivo para o diretório temporário da imagem " . ($index + 1) . ".

"; 24 | continue; 25 | } 26 | 27 | $uploadedImagePaths[] = $tempImagePath; 28 | 29 | try { 30 | $response = $groq->vision()->analyze($tempImagePath, $prompt); 31 | echo "

Resposta do Modelo para Imagem " . ($index + 1) . ":

"; 32 | echo "

" . htmlspecialchars($response['choices'][0]['message']['content']) . "

"; 33 | } catch (LucianoTonet\GroqPHP\GroqException $err) { 34 | echo "

Erro na análise da imagem " . ($index + 1) . ": " . htmlspecialchars($err->getMessage()) . "

"; 35 | } catch (Exception $e) { 36 | echo "

Erro: " . htmlspecialchars($e->getMessage()) . "

"; 37 | } 38 | } 39 | } 40 | 41 | foreach ($uploadedImagePaths as $uploadedImagePath) { 42 | if (file_exists($uploadedImagePath)) { 43 | unlink($uploadedImagePath); 44 | } 45 | } 46 | } 47 | ?> 48 | 49 |
50 |
51 | 52 | 58 |
59 |
60 | 61 | 63 |
64 | 67 |
68 |
-------------------------------------------------------------------------------- /tests/AudioTest.php: -------------------------------------------------------------------------------- 1 | testAudioPath = __DIR__ . '/fixtures/audio.wav'; 17 | $this->testOutputPath = __DIR__ . '/fixtures/output.wav'; 18 | } 19 | 20 | protected function tearDown(): void 21 | { 22 | parent::tearDown(); 23 | if (file_exists($this->testOutputPath)) { 24 | unlink($this->testOutputPath); 25 | } 26 | } 27 | 28 | public function testAudioTranscription() 29 | { 30 | $response = $this->groq->audio()->transcriptions()->create([ 31 | 'file' => $this->testAudioPath, 32 | 'model' => 'whisper-large-v3', 33 | 'response_format' => 'json', 34 | ]); 35 | 36 | $this->assertArrayHasKey('text', $response); 37 | $this->assertNotEmpty($response['text']); 38 | 39 | // Verifica se a transcrição contém o texto esperado, ignorando case e espaços extras 40 | $this->assertStringContainsStringIgnoringCase( 41 | trim($this->expectedTranscription), 42 | trim($response['text']) 43 | ); 44 | } 45 | 46 | public function testAudioTranslation() 47 | { 48 | $response = $this->groq->audio()->translations()->create([ 49 | 'file' => $this->testAudioPath, 50 | 'model' => 'whisper-large-v3', 51 | 'response_format' => 'json', 52 | ]); 53 | 54 | $this->assertArrayHasKey('text', $response); 55 | $this->assertNotEmpty($response['text']); 56 | 57 | // Verifica se a tradução contém elementos do texto original 58 | $this->assertStringContainsStringIgnoringCase( 59 | 'help', 60 | $response['text'] 61 | ); 62 | } 63 | 64 | /** 65 | * Este teste requer aceitação dos termos de uso do modelo playai-tts 66 | * no console do Groq (https://console.groq.com/playground?model=playai-tts). 67 | * Por isso, está comentado para não quebrar o build. 68 | */ 69 | /* 70 | public function testSpeechGeneration() 71 | { 72 | $result = $this->groq->audio()->speech() 73 | ->model('playai-tts') 74 | ->input('This is a test of the Groq PHP speech functionality.') 75 | ->voice('Bryan-PlayAI') 76 | ->responseFormat('wav') 77 | ->save($this->testOutputPath); 78 | 79 | $this->assertTrue($result); 80 | $this->assertFileExists($this->testOutputPath); 81 | $this->assertGreaterThan(0, filesize($this->testOutputPath)); 82 | } 83 | */ 84 | 85 | public function testSpeechImplementation() 86 | { 87 | $speech = $this->groq->audio()->speech(); 88 | 89 | // Verificar se os métodos fluentes estão disponíveis 90 | $speech = $speech->model('playai-tts') 91 | ->input('Test text') 92 | ->voice('Bryan-PlayAI') 93 | ->responseFormat('wav'); 94 | 95 | // Verificar se a instância é do tipo correto 96 | $this->assertInstanceOf(Speech::class, $speech); 97 | 98 | // Verificar se os métodos create e save existem 99 | $this->assertTrue(method_exists($speech, 'create'), 'O método create() não existe na classe Speech'); 100 | $this->assertTrue(method_exists($speech, 'save'), 'O método save() não existe na classe Speech'); 101 | } 102 | } -------------------------------------------------------------------------------- /src/Stream.php: -------------------------------------------------------------------------------- 1 | response = clone $response; 27 | } 28 | 29 | /** 30 | * This method returns a generator that yields chunks of data from the response. 31 | * It processes the response line by line and yields the parsed JSON data. 32 | * 33 | * @return \Generator Yields parsed JSON data from the response. 34 | * @throws GroqException If an error occurs during processing. 35 | */ 36 | public function chunks(): \Generator 37 | { 38 | if (!$this->response instanceof ResponseInterface) { 39 | throw new \InvalidArgumentException('Invalid response provided'); 40 | } 41 | 42 | if ($this->response->getStatusCode() >= 400) { 43 | throw new GroqException('Error response received', $this->response->getStatusCode(), 'ResponseError'); 44 | } 45 | 46 | $body = $this->response->getBody(); 47 | 48 | try { 49 | while (!$body->eof()) { 50 | $line = $this->readLine($body); 51 | 52 | if (!str_starts_with($line, 'data:')) { 53 | continue; 54 | } 55 | 56 | $data = trim(substr($line, strlen('data:'))); 57 | 58 | if ($data === '[DONE]') { 59 | break; 60 | } 61 | 62 | $response = json_decode($data, true, flags: JSON_THROW_ON_ERROR); 63 | 64 | if (isset($response['error'])) { 65 | throw new GroqException($response['error'], 0, 'ResponseError'); 66 | } 67 | 68 | yield $response; 69 | } 70 | } catch (\Throwable $e) { 71 | throw new GroqException('Error processing chunks: ' . $e->getMessage(), $e->getCode(), 'ChunksProcessingException'); 72 | } finally { 73 | $body->close(); 74 | } 75 | } 76 | 77 | /** 78 | * Reads a line from the given stream. 79 | * 80 | * @param StreamInterface $stream The stream to read from. 81 | * @return string The line read from the stream. 82 | */ 83 | private function readLine(StreamInterface $stream): string 84 | { 85 | $buffer = ''; 86 | 87 | while (!$stream->eof()) { 88 | $byte = $stream->read(1); 89 | 90 | if ($byte === '') { 91 | return $buffer; 92 | } 93 | 94 | $buffer .= $byte; 95 | 96 | if ($byte === "\n") { 97 | break; 98 | } 99 | } 100 | 101 | return $buffer; 102 | } 103 | 104 | /** 105 | * Retrieves a specific header from the response. 106 | * 107 | * @param string $name The name of the header to retrieve. 108 | * @return string The value of the specified header. 109 | */ 110 | public function getHeader(string $name): string 111 | { 112 | return $this->response->getHeaderLine($name); 113 | } 114 | 115 | /** 116 | * Retrieves all headers from the response. 117 | * 118 | * @return array An associative array of all headers. 119 | */ 120 | public function getHeaders(): array 121 | { 122 | return $this->response->getHeaders(); 123 | } 124 | } -------------------------------------------------------------------------------- /tests/VisionTest.php: -------------------------------------------------------------------------------- 1 | testImagePath = sys_get_temp_dir() . '/test_image.jpg'; 19 | $image = imagecreatetruecolor(100, 100); 20 | imagefill($image, 0, 0, imagecolorallocate($image, 255, 255, 255)); 21 | imagejpeg($image, $this->testImagePath); 22 | imagedestroy($image); 23 | 24 | // Initialize Vision client with default model 25 | $this->groq->vision()->setDefaultModel($this->defaultModel); 26 | } 27 | 28 | public function testVisionAnalysisWithLocalImage() 29 | { 30 | try { 31 | $response = $this->groq->vision()->analyze($this->testImagePath, 'Describe this image'); 32 | 33 | $this->assertArrayHasKey('choices', $response); 34 | $this->assertArrayHasKey('message', $response['choices'][0]); 35 | $this->assertArrayHasKey('content', $response['choices'][0]['message']); 36 | $this->assertNotEmpty($response['choices'][0]['message']['content']); 37 | } catch (GroqException $e) { 38 | $this->fail('Error analyzing local image: ' . $e->getMessage()); 39 | } 40 | } 41 | 42 | public function testVisionAnalysisWithUrlImage() 43 | { 44 | try { 45 | $imageUrl = 'https://raw.githubusercontent.com/lucianotonet/groq-php/main/art.png'; 46 | $response = $this->groq->vision()->analyze($imageUrl, 'Describe this image'); 47 | 48 | $this->assertArrayHasKey('choices', $response); 49 | $this->assertArrayHasKey('message', $response['choices'][0]); 50 | $this->assertArrayHasKey('content', $response['choices'][0]['message']); 51 | $this->assertNotEmpty($response['choices'][0]['message']['content']); 52 | } catch (GroqException $e) { 53 | $this->fail('Error analyzing URL image: ' . $e->getMessage()); 54 | } 55 | } 56 | 57 | public function testVisionAnalysisWithInvalidImage() 58 | { 59 | $prompt = "What do you see in this image?"; 60 | $invalidPath = __DIR__ . '/../../fixtures/nonexistent.png'; 61 | 62 | $this->expectException(GroqException::class); 63 | $this->expectExceptionMessage('Image file not found'); 64 | $this->groq->vision()->analyze($invalidPath, $prompt); 65 | } 66 | 67 | public function testVisionAnalysisWithInvalidUrl() 68 | { 69 | $prompt = "What do you see in this image?"; 70 | $invalidUrl = "https://invalid-url.com/image.png"; 71 | 72 | // Expect only the exception type, not the specific message 73 | // since error messages from external APIs can change 74 | $this->expectException(GroqException::class); 75 | 76 | $this->groq->vision()->analyze($invalidUrl, $prompt); 77 | } 78 | 79 | public function testVisionAnalysisWithCustomOptions() 80 | { 81 | try { 82 | $response = $this->groq->vision() 83 | ->analyze($this->testImagePath, 'What colors do you see in this image?', [ 84 | 'temperature' => 0.7, 85 | 'max_tokens' => 100 86 | ]); 87 | 88 | $this->assertArrayHasKey('choices', $response); 89 | $this->assertArrayHasKey('message', $response['choices'][0]); 90 | $this->assertArrayHasKey('content', $response['choices'][0]['message']); 91 | $this->assertNotEmpty($response['choices'][0]['message']['content']); 92 | } catch (GroqException $e) { 93 | $this->fail('Error analyzing with custom options: ' . $e->getMessage()); 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /src/Reasoning.php: -------------------------------------------------------------------------------- 1 | groq = $groq; 27 | } 28 | 29 | /** 30 | * Performs a reasoning task with step-by-step analysis. 31 | * 32 | * @param string $prompt The question or task to reason about 33 | * @param array $options Additional options for the reasoning 34 | * - model: (string) The model to use (required) 35 | * - temperature: (float) Controls randomness (0.0 to 2.0) 36 | * - max_completion_tokens: (int) Maximum tokens in response 37 | * - stream: (bool) Whether to stream the response 38 | * - top_p: (float) Controls diversity (0.0 to 1.0) 39 | * - frequency_penalty: (float) Penalizes repeated tokens 40 | * - presence_penalty: (float) Penalizes repeated topics 41 | * - stop: (string|array) Stop sequences to end generation 42 | * - system_prompt: (string) Custom system prompt to guide the model's behavior 43 | * - reasoning_format: (string) Controls how model reasoning is presented: 44 | * - "parsed": Separates reasoning into a dedicated field 45 | * - "raw": Includes reasoning within think tags in content (default) 46 | * - "hidden": Returns only the final answer 47 | * Note: Must be "parsed" or "hidden" when using tool calling or JSON mode 48 | * @return array|Stream The reasoning response 49 | * @throws GroqException If there is an error in the reasoning process 50 | */ 51 | public function analyze(string $prompt, array $options = []): array|Stream 52 | { 53 | if (!isset($options['model'])) { 54 | throw new GroqException('The model parameter is required for reasoning tasks', 400, 'invalid_request'); 55 | } 56 | 57 | // Validates reasoning_format if provided 58 | if (isset($options['reasoning_format']) && 59 | !in_array($options['reasoning_format'], ['parsed', 'raw', 'hidden'])) { 60 | throw new GroqException( 61 | 'Invalid reasoning_format. Must be one of: parsed, raw, hidden', 62 | 400, 63 | 'invalid_request' 64 | ); 65 | } 66 | 67 | // Checks if reasoning_format is compatible with json_mode 68 | if (isset($options['json_mode']) && $options['json_mode'] === true && 69 | isset($options['reasoning_format']) && $options['reasoning_format'] === 'raw') { 70 | throw new GroqException( 71 | 'reasoning_format must be "parsed" or "hidden" when using JSON mode', 72 | 400, 73 | 'invalid_request' 74 | ); 75 | } 76 | 77 | $messages = [ 78 | [ 79 | 'role' => 'user', 80 | 'content' => $prompt 81 | ] 82 | ]; 83 | 84 | // Add system prompt only if provided 85 | if (isset($options['system_prompt'])) { 86 | array_unshift($messages, [ 87 | 'role' => 'system', 88 | 'content' => $options['system_prompt'] 89 | ]); 90 | unset($options['system_prompt']); // Remove to avoid interference with the API 91 | } 92 | 93 | $requestOptions = array_merge([ 94 | 'messages' => $messages, 95 | 'reasoning_format' => 'raw' // Default value 96 | ], $options); 97 | 98 | return $this->groq->chat()->completions()->create($requestOptions); 99 | } 100 | } -------------------------------------------------------------------------------- /src/Vision.php: -------------------------------------------------------------------------------- 1 | groq = $groq; 13 | } 14 | 15 | /** 16 | * Analyzes an image and returns the model's response. 17 | * 18 | * @param string $imagePathOrUrl Path or URL of the image. 19 | * @param string $prompt Question or context for the analysis. 20 | * @param array $options Additional options for the analysis. 21 | * @return array Model's response. 22 | */ 23 | public function analyze(string $imagePathOrUrl, string $prompt, array $options = []): array 24 | { 25 | $imageContent = $this->getImageContent($imagePathOrUrl); 26 | 27 | $messages = [ 28 | [ 29 | 'role' => 'user', 30 | 'content' => [ 31 | ['type' => 'text', 'text' => $prompt], 32 | [ 33 | 'type' => 'image_url', 34 | 'image_url' => ['url' => $imageContent], 35 | ], 36 | ], 37 | ], 38 | ]; 39 | 40 | $model = $options['model'] ?? $this->defaultModel; 41 | 42 | $requestOptions = [ 43 | 'model' => $model, 44 | 'messages' => $messages, 45 | ]; 46 | 47 | // Adicionar opções adicionais se fornecidas 48 | if (isset($options['temperature'])) { 49 | $requestOptions['temperature'] = $options['temperature']; 50 | } 51 | if (isset($options['max_completion_tokens'])) { 52 | $requestOptions['max_completion_tokens'] = $options['max_completion_tokens']; 53 | } 54 | 55 | return $this->groq->chat()->completions()->create($requestOptions); 56 | } 57 | 58 | /** 59 | * Retrieves the image content, either from a local file or a URL. 60 | * 61 | * @param string $imagePathOrUrl Path or URL of the image. 62 | * @return string Image content in base64 or original URL. 63 | * @throws GroqException If the image file is not found or exceeds size limits. 64 | */ 65 | private function getImageContent(string $imagePathOrUrl): string 66 | { 67 | if (filter_var($imagePathOrUrl, FILTER_VALIDATE_URL)) { 68 | // Verificar o tamanho da imagem URL (limite de 20MB) 69 | $headers = get_headers($imagePathOrUrl, 1); 70 | $fileSize = isset($headers['Content-Length']) ? (int)$headers['Content-Length'] : 0; 71 | if ($fileSize > 20 * 1024 * 1024) { 72 | throw new GroqException( 73 | "Image URL exceeds 20MB size limit", 74 | 400, 75 | 'ImageSizeLimitExceededException' 76 | ); 77 | } 78 | return $imagePathOrUrl; 79 | } 80 | 81 | if (file_exists($imagePathOrUrl)) { 82 | // Verificar o tamanho do arquivo local (limite de 4MB para base64) 83 | $fileSize = filesize($imagePathOrUrl); 84 | if ($fileSize > 4 * 1024 * 1024) { 85 | throw new GroqException( 86 | "Local image file exceeds 4MB size limit for base64 encoding", 87 | 400, 88 | 'ImageSizeLimitExceededException' 89 | ); 90 | } 91 | $imageData = base64_encode(file_get_contents($imagePathOrUrl)); 92 | $mimeType = mime_content_type($imagePathOrUrl); 93 | return "data:$mimeType;base64," . $imageData; 94 | } 95 | 96 | throw new GroqException( 97 | "Image file not found: $imagePathOrUrl", 98 | 404, 99 | 'FileNotFoundException' 100 | ); 101 | } 102 | 103 | /** 104 | * Sets the default model for vision analysis. 105 | * 106 | * @param string $model The model to use as default. 107 | */ 108 | public function setDefaultModel(string $model): void 109 | { 110 | $this->defaultModel = $model; 111 | } 112 | } -------------------------------------------------------------------------------- /tests/BatchManagerTest.php: -------------------------------------------------------------------------------- 1 | groq = new Groq(getenv('GROQ_API_KEY')); 20 | $this->batchManager = new BatchManager($this->groq); 21 | $this->fileManager = new FileManager($this->groq); 22 | } 23 | 24 | private function retryDelete(string $fileId, int $maxRetries = 5, int $retryDelay = 1): void 25 | { 26 | for ($i = 0; $i < $maxRetries; $i++) { 27 | try { 28 | $this->fileManager->delete($fileId); 29 | return; 30 | } catch (GroqException $e) { 31 | if (str_contains($e->getMessage(), 'file currently in use') && $i < $maxRetries - 1) { 32 | sleep($retryDelay); 33 | } else { 34 | throw $e; 35 | } 36 | } 37 | } 38 | } 39 | 40 | private function cancelBatchSafely(string $batchId): void 41 | { 42 | try { 43 | $this->batchManager->cancel($batchId); 44 | } catch (GroqException $e) { 45 | if (!str_contains($e->getMessage(), 'cannot be cancelled')) { 46 | throw $e; 47 | } 48 | } 49 | } 50 | 51 | public function testCreateBatchForChatCompletions() 52 | { 53 | $file = $this->fileManager->upload(__DIR__ . '/fixtures/batch_file.jsonl', 'batch'); 54 | 55 | $batch = $this->batchManager->create([ 56 | 'input_file_id' => $file->id, 57 | 'endpoint' => '/v1/chat/completions', 58 | 'completion_window' => '24h' 59 | ]); 60 | 61 | $this->assertIsString($batch->id); 62 | $this->assertEquals('/v1/chat/completions', $batch->endpoint); 63 | 64 | // Clean up 65 | $this->cancelBatchSafely($batch->id); 66 | $this->retryDelete($file->id); 67 | } 68 | 69 | public function testCreateBatchForAudioTranscriptions() 70 | { 71 | $file = $this->fileManager->upload(__DIR__ . '/fixtures/batch_file_audio.jsonl', 'batch'); 72 | 73 | $batch = $this->batchManager->create([ 74 | 'input_file_id' => $file->id, 75 | 'endpoint' => '/v1/audio/transcriptions', 76 | 'completion_window' => '24h' 77 | ]); 78 | 79 | $this->assertIsString($batch->id); 80 | $this->assertEquals('/v1/audio/transcriptions', $batch->endpoint); 81 | 82 | // Clean up 83 | $this->cancelBatchSafely($batch->id); 84 | $this->retryDelete($file->id); 85 | } 86 | 87 | public function testRetrieveBatch() 88 | { 89 | $file = $this->fileManager->upload(__DIR__ . '/fixtures/batch_file.jsonl', 'batch'); 90 | 91 | $batch = $this->batchManager->create([ 92 | 'input_file_id' => $file->id, 93 | 'endpoint' => '/v1/chat/completions', 94 | 'completion_window' => '24h' 95 | ]); 96 | 97 | $retrievedBatch = $this->batchManager->retrieve($batch->id); 98 | 99 | $this->assertEquals($batch->id, $retrievedBatch->id); 100 | 101 | // Clean up 102 | $this->cancelBatchSafely($batch->id); 103 | $this->retryDelete($file->id); 104 | } 105 | 106 | public function testListBatches() 107 | { 108 | $batches = $this->batchManager->list(); 109 | 110 | $this->assertIsArray($batches['data']); 111 | } 112 | 113 | public function testCancelBatch() 114 | { 115 | $file = $this->fileManager->upload(__DIR__ . '/fixtures/batch_file.jsonl', 'batch'); 116 | 117 | $batch = $this->batchManager->create([ 118 | 'input_file_id' => $file->id, 119 | 'endpoint' => '/v1/chat/completions', 120 | 'completion_window' => '24h' 121 | ]); 122 | 123 | $canceledBatch = $this->batchManager->cancel($batch->id); 124 | 125 | $this->assertEquals('cancelling', $canceledBatch->status); 126 | 127 | // Clean up 128 | $this->retryDelete($file->id); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Speech.php: -------------------------------------------------------------------------------- 1 | groq = $groq; 31 | $this->model = 'playai-tts'; 32 | $this->input = ''; 33 | $this->voice = ''; 34 | $this->responseFormat = 'wav'; 35 | } 36 | 37 | /** 38 | * Set the TTS model to use. 39 | * 40 | * @param string $model The model to use (play-tts or play-tts-arabic) 41 | * @return $this 42 | */ 43 | public function model(string $model): self 44 | { 45 | $this->model = $model; 46 | return $this; 47 | } 48 | 49 | /** 50 | * Set the text input to convert to speech. 51 | * 52 | * @param string $input The text to convert to speech 53 | * @return $this 54 | */ 55 | public function input(string $input): self 56 | { 57 | $this->input = $input; 58 | return $this; 59 | } 60 | 61 | /** 62 | * Set the voice to use for speech generation. 63 | * 64 | * @param string $voice The voice identifier (e.g., "Bryan-PlayAI") 65 | * @return $this 66 | */ 67 | public function voice(string $voice): self 68 | { 69 | $this->voice = $voice; 70 | return $this; 71 | } 72 | 73 | /** 74 | * Set the response format. 75 | * 76 | * @param string $format The response format (e.g., "wav") 77 | * @return $this 78 | */ 79 | public function responseFormat(string $format): self 80 | { 81 | $this->responseFormat = $format; 82 | return $this; 83 | } 84 | 85 | /** 86 | * Create a speech file from the provided text. 87 | * 88 | * @return resource|string The audio content as a stream resource or string 89 | * @throws GroqException If the request fails 90 | */ 91 | public function create() 92 | { 93 | if (empty($this->input)) { 94 | throw new GroqException('Input text is required', 400, 'validation_error', [], null, null); 95 | } 96 | 97 | if (empty($this->voice)) { 98 | throw new GroqException('Voice is required', 400, 'validation_error', [], null, null); 99 | } 100 | 101 | $payload = [ 102 | 'model' => $this->model, 103 | 'input' => $this->input, 104 | 'voice' => $this->voice, 105 | 'response_format' => $this->responseFormat 106 | ]; 107 | 108 | try { 109 | $client = new Client([ 110 | 'base_uri' => $this->groq->baseUrl(), 111 | 'headers' => [ 112 | 'Authorization' => 'Bearer ' . $this->groq->apiKey(), 113 | 'Content-Type' => 'application/json' 114 | ] 115 | ]); 116 | 117 | $response = $client->post('audio/speech', [ 118 | 'json' => $payload 119 | ]); 120 | 121 | return $response->getBody(); 122 | } catch (RequestException $e) { 123 | $responseBody = $e->getResponse() ? ($e->getResponse()->getBody() ? (string) $e->getResponse()->getBody() : 'Response body is empty') : 'No response body available'; 124 | throw new GroqException('Failed to create speech: ' . $responseBody, $e->getCode(), 'RequestException'); 125 | } catch (GuzzleException $e) { 126 | throw new GroqException('An unexpected error occurred: ' . $e->getMessage(), $e->getCode(), 'GuzzleException'); 127 | } catch (\Exception $e) { 128 | throw new GroqException('An unexpected error occurred: ' . $e->getMessage(), $e->getCode(), 'Exception'); 129 | } 130 | } 131 | 132 | /** 133 | * Save the generated speech to a file. 134 | * 135 | * @param string $filePath The path to save the file 136 | * @return bool True if the file was saved successfully 137 | * @throws GroqException If the request fails 138 | */ 139 | public function save(string $filePath): bool 140 | { 141 | $audioContent = $this->create(); 142 | 143 | if (is_resource($audioContent)) { 144 | $audioData = ''; 145 | while (!feof($audioContent)) { 146 | $audioData .= fread($audioContent, 1024); 147 | } 148 | $result = file_put_contents($filePath, $audioData); 149 | } else { 150 | $result = file_put_contents($filePath, $audioContent); 151 | } 152 | 153 | return $result !== false; 154 | } 155 | } -------------------------------------------------------------------------------- /examples/tool-calling-advanced.php: -------------------------------------------------------------------------------- 1 |
2 | user:
$message
"; 8 | 9 | function getCurrentDateTimeTool(array $parameters = []): string 10 | { 11 | $time_zone = $parameters['time_zone'] ?? 'UTC'; 12 | date_default_timezone_set($time_zone); 13 | return date('Y-m-d H:i:s') . " ($time_zone)"; 14 | } 15 | 16 | function getCurrentWeatherTool(array $parameters = []): string 17 | { 18 | $location = $parameters['location'] ?? 'New York'; 19 | return '30°C, sunny in ' . $location; // Simulation of weather data 20 | } 21 | 22 | $messages = [ 23 | [ 24 | 'role' => 'system', 25 | 'content' => 'You are a helpful AI assistant. Always answer in a concise manner. Execute the functions "getCurrentDateTimeTool" or "getCurrentWeatherTool" to answer the user correctly, if needed.' 26 | ], 27 | [ 28 | 'role' => 'user', 29 | 'content' => $message 30 | ] 31 | ]; 32 | 33 | $tools = [ 34 | [ 35 | "type" => "function", 36 | "function" => [ 37 | "name" => "getCurrentDateTimeTool", 38 | "description" => "Get the current time in any format supported by PHP's date() function, considering the time zone.", 39 | "parameters" => [ 40 | "type" => "object", 41 | "properties" => [ 42 | "time_zone" => [ 43 | "type" => "string", 44 | "description" => "Time zone for which to get the time." 45 | ], 46 | ], 47 | "required" => ["time_zone"], 48 | "default" => ["time_zone" => "UTC"] 49 | ], 50 | ] 51 | ], 52 | [ 53 | "type" => "function", 54 | "function" => [ 55 | "name" => "getCurrentWeatherTool", 56 | "description" => "Get the current weather in a specific location.", 57 | "parameters" => [ 58 | "type" => "object", 59 | "properties" => [ 60 | "location" => [ 61 | "type" => "string", 62 | "description" => "Location for which to get the weather." 63 | ], 64 | ], 65 | "required" => ["location"], 66 | "default" => ["location" => "New York"] 67 | ], 68 | ] 69 | ] 70 | ]; 71 | 72 | try { 73 | $response = $groq->chat()->completions()->create([ 74 | 'model' => 'llama3-groq-70b-8192-tool-use-preview', 75 | 'messages' => $messages, 76 | "temperature" => 0, 77 | "tool_choice" => "auto", 78 | "tools" => $tools, 79 | "parallel_tool_calls" => false 80 | ]); 81 | } catch (\LucianoTonet\GroqPHP\GroqException $err) { 82 | echo $err->getCode() . "
" . $err->getMessage() . "
" . $err->getType() . "
"; 83 | print_r($err->getHeaders()); 84 | echo "assistant:
Sorry, I couldn't understand your request. Please try again.
"; 85 | exit; 86 | } 87 | 88 | echo "" . $response['choices'][0]['message']['role'] . ":
"; 89 | 90 | if (!empty($response['choices'][0]['message']['tool_calls'])) { 91 | foreach ($response['choices'][0]['message']['tool_calls'] as $tool_call) { 92 | if ($tool_call['function']['name']) { 93 | $function_args = json_decode($tool_call['function']['arguments'], true); 94 | echo "> Calling tool...
"; 95 | $function_response = $tool_call['function']['name']($function_args); 96 | echo "> Building response...
"; 97 | 98 | $messages[] = [ 99 | 'tool_call_id' => $tool_call['id'], 100 | 'role' => 'tool', 101 | 'name' => $tool_call['function']['name'], 102 | 'content' => $function_response, 103 | ]; 104 | } 105 | } 106 | 107 | try { 108 | $response = $groq->chat()->completions()->create([ 109 | 'model' => 'llama3-groq-70b-8192-tool-use-preview', 110 | // 'model' => 'llama3-70b-8192', 111 | 'messages' => $messages 112 | ]); 113 | } catch (\LucianoTonet\GroqPHP\GroqException $err) { 114 | echo $err->getCode() . "
" . $err->getMessage() . "
" . $err->getType() . "
"; 115 | print_r($err->getHeaders()); 116 | echo "assistant:
Sorry, I couldn't understand your request. Please try again.
"; 117 | exit; 118 | } 119 | } 120 | 121 | echo $response['choices'][0]['message']['content'] . "
"; 122 | } else { 123 | echo "Ask questions like \"What time is it in UTC-3?\" or \"How is the weather in London?\".
Results will be simulated for demonstration purposes.


"; 124 | } 125 | ?> 126 |
-------------------------------------------------------------------------------- /tests/ChatTest.php: -------------------------------------------------------------------------------- 1 | groq->chat()->completions()->create([ 16 | 'model' => 'llama-3.1-8b-instant', 17 | 'messages' => [ 18 | [ 19 | 'role' => 'user', 20 | 'content' => 'Hello, how are you?' 21 | ] 22 | ] 23 | ]); 24 | 25 | $this->assertArrayHasKey('choices', $response); 26 | $this->assertNotEmpty($response['choices']); 27 | $this->assertArrayHasKey('message', $response['choices'][0]); 28 | $this->assertArrayHasKey('content', $response['choices'][0]['message']); 29 | $this->assertNotEmpty($response['choices'][0]['message']['content']); 30 | } 31 | 32 | /** 33 | * Testa o chat com streaming 34 | * @covers examples/chat-streaming.php 35 | */ 36 | public function testStreamingChatCompletion() 37 | { 38 | $stream = $this->groq->chat()->completions()->create([ 39 | 'model' => 'llama-3.1-8b-instant', 40 | 'messages' => [ 41 | [ 42 | 'role' => 'user', 43 | 'content' => 'Tell me a short story' 44 | ] 45 | ], 46 | 'stream' => true 47 | ]); 48 | 49 | $contentParts = []; 50 | $roleFound = false; 51 | 52 | foreach ($stream->chunks() as $chunk) { 53 | if (isset($chunk['choices'][0]['delta']['role'])) { 54 | $roleFound = true; 55 | $this->assertEquals('assistant', $chunk['choices'][0]['delta']['role']); 56 | } 57 | 58 | if (isset($chunk['choices'][0]['delta']['content'])) { 59 | $contentParts[] = $chunk['choices'][0]['delta']['content']; 60 | } 61 | } 62 | 63 | $this->assertTrue($roleFound, 'Role "assistant" should be present in stream'); 64 | $this->assertNotEmpty($contentParts, 'Stream should contain content chunks'); 65 | 66 | // Junta as partes para verificar se formam uma resposta coerente 67 | $fullContent = implode('', $contentParts); 68 | $this->assertNotEmpty($fullContent); 69 | } 70 | 71 | /** 72 | * Testa o modo JSON 73 | * @covers examples/json-mode.php 74 | */ 75 | public function testJsonModeCompletion() 76 | { 77 | $response = $this->groq->chat()->completions()->create([ 78 | 'model' => 'llama-3.1-8b-instant', 79 | 'messages' => [ 80 | ['role' => 'system', 'content' => 'You are a JSON API. You must respond with valid JSON only.'], 81 | ['role' => 'user', 'content' => 'Return a simple JSON with: name="John", age=30'] 82 | ], 83 | 'response_format' => ['type' => 'json_object'] 84 | ]); 85 | 86 | $this->assertArrayHasKey('choices', $response); 87 | $this->assertArrayHasKey('message', $response['choices'][0]); 88 | $this->assertArrayHasKey('content', $response['choices'][0]['message']); 89 | 90 | // Verificar se o conteúdo é um JSON válido 91 | $content = $response['choices'][0]['message']['content']; 92 | $json = json_decode($content, true); 93 | $this->assertNotNull($json); 94 | $this->assertArrayHasKey('name', $json); 95 | $this->assertArrayHasKey('age', $json); 96 | $this->assertEquals('John', $json['name']); 97 | $this->assertEquals(30, $json['age']); 98 | } 99 | 100 | /** 101 | * Testa o tratamento de erros no chat básico 102 | */ 103 | public function testBasicChatError() 104 | { 105 | $this->expectException(GroqException::class); 106 | 107 | $this->groq->chat()->completions()->create([ 108 | 'model' => 'llama-3.1-8b-instant', 109 | 'messages' => [] // Mensagens vazias devem gerar erro 110 | ]); 111 | } 112 | 113 | /** 114 | * Testa o tratamento de erros no streaming 115 | */ 116 | public function testStreamingError() 117 | { 118 | $this->expectException(GroqException::class); 119 | 120 | $stream = $this->groq->chat()->completions()->create([ 121 | 'model' => 'invalid-model', 122 | 'messages' => [ 123 | ['role' => 'user', 'content' => 'Hello'] 124 | ], 125 | 'stream' => true 126 | ]); 127 | 128 | iterator_to_array($stream->chunks()); // Força a execução do stream 129 | } 130 | 131 | /** 132 | * Testa o controle de buffer no streaming 133 | */ 134 | public function testStreamingBufferControl() 135 | { 136 | $stream = $this->groq->chat()->completions()->create([ 137 | 'model' => 'llama-3.1-8b-instant', 138 | 'messages' => [ 139 | ['role' => 'user', 'content' => 'Count from 1 to 5'] 140 | ], 141 | 'stream' => true 142 | ]); 143 | 144 | $chunks = []; 145 | $fullContent = ''; 146 | 147 | foreach ($stream->chunks() as $chunk) { 148 | $chunks[] = $chunk; 149 | if (isset($chunk['choices'][0]['delta']['content'])) { 150 | $fullContent .= $chunk['choices'][0]['delta']['content']; 151 | } 152 | } 153 | 154 | // Verifica se recebemos chunks 155 | $this->assertNotEmpty($chunks); 156 | 157 | // Verifica a estrutura de cada chunk 158 | foreach ($chunks as $chunk) { 159 | $this->assertArrayHasKey('choices', $chunk); 160 | $this->assertArrayHasKey(0, $chunk['choices']); 161 | $this->assertArrayHasKey('delta', $chunk['choices'][0]); 162 | } 163 | 164 | // Verifica o conteúdo completo montado 165 | $this->assertStringContainsString('1', $fullContent); 166 | $this->assertStringContainsString('5', $fullContent); 167 | 168 | // Verifica se o último chunk indica fim do stream 169 | $lastChunk = end($chunks); 170 | $this->assertArrayHasKey('finish_reason', $lastChunk['choices'][0]); 171 | $this->assertEquals('stop', $lastChunk['choices'][0]['finish_reason']); 172 | } 173 | } -------------------------------------------------------------------------------- /examples/tool-calling.php: -------------------------------------------------------------------------------- 1 |
2 | user:
"; 9 | echo "$message
"; 10 | 11 | function getNbaScore($teamName) 12 | { 13 | // Example dummy function hard coded to return the score of an NBA game 14 | if (strpos(strtolower($teamName), 'warriors') !== false) { 15 | return json_encode([ 16 | "game_id" => "401585601", 17 | "status" => 'Final', 18 | "home_team" => "Los Angeles Lakers", 19 | "home_team_score" => 121, 20 | "away_team" => "Golden State Warriors", 21 | "away_team_score" => 128 22 | ]); 23 | } elseif (strpos(strtolower($teamName), 'lakers') !== false) { 24 | return json_encode([ 25 | "game_id" => "401585601", 26 | "status" => 'Final', 27 | "home_team" => "Los Angeles Lakers", 28 | "home_team_score" => 121, 29 | "away_team" => "Golden State Warriors", 30 | "away_team_score" => 128 31 | ]); 32 | } elseif (strpos(strtolower($teamName), 'nuggets') !== false) { 33 | return json_encode([ 34 | "game_id" => "401585577", 35 | "status" => 'Final', 36 | "home_team" => "Miami Heat", 37 | "home_team_score" => 88, 38 | "away_team" => "Denver Nuggets", 39 | "away_team_score" => 100 40 | ]); 41 | } elseif (strpos(strtolower($teamName), 'heat') !== false) { 42 | return json_encode([ 43 | "game_id" => "401585577", 44 | "status" => 'Final', 45 | "home_team" => "Miami Heat", 46 | "home_team_score" => 88, 47 | "away_team" => "Denver Nuggets", 48 | "away_team_score" => 100 49 | ]); 50 | } else { 51 | return json_encode([ 52 | "team_name" => $teamName, 53 | "score" => "unknown" 54 | ]); 55 | } 56 | } 57 | 58 | $messages = [ 59 | [ 60 | 'role' => 'system', 61 | 'content' => "You shall call the function 'getNbaScore' to answer questions around NBA game scores. Include the team and their opponent in your response." 62 | ], 63 | [ 64 | 'role' => 'user', 65 | 'content' => $message 66 | ] 67 | ]; 68 | 69 | $tools = [ 70 | [ 71 | 'type' => 'function', 72 | 'function' => [ 73 | 'name' => 'getNbaScore', 74 | 'description' => 'Get the score for a given NBA game', 75 | 'parameters' => [ 76 | 'type' => 'object', 77 | 'properties' => [ 78 | "team_name" => [ 79 | "type" => "string", 80 | "description" => "The name of the NBA team (e.g. 'Golden State Warriors')", 81 | ] 82 | ], 83 | "required" => ["team_name"], 84 | ], 85 | ], 86 | ] 87 | ]; 88 | 89 | try { 90 | $response = $groq->chat()->completions()->create([ 91 | 'model' => 'llama3-groq-70b-8192-tool-use-preview', 92 | // 'model' => 'llama3-70b-8192', 93 | 'messages' => $messages, 94 | "tool_choice" => "auto", 95 | "tools" => $tools 96 | ]); 97 | } catch (\LucianoTonet\GroqPHP\GroqException $err) { 98 | echo $err->getCode() . "
" . $err->getMessage() . "
" . $err->getType() . "
"; 99 | print_r($err->getHeaders()); 100 | echo "assistant:
Desculpe, não consegui entender sua solicitação. Tente novamente.
"; 101 | exit; 102 | } 103 | 104 | echo "assistant: " . "
"; 105 | 106 | if (isset($response['choices'][0]['message']['tool_calls'])) { 107 | $tool_calls = $response['choices'][0]['message']['tool_calls']; 108 | foreach ($tool_calls as $tool_call) { 109 | if ($tool_call['function']['name'] == 'getNbaScore') { 110 | $function_args = json_decode($tool_call['function']['arguments'], true); 111 | 112 | echo "> Calling tool..." . "
"; 113 | 114 | $function_response = getNbaScore($function_args['team_name']); 115 | 116 | echo "> Building response..." . "
"; 117 | 118 | $messages[] = [ 119 | 'tool_call_id' => $tool_call['id'], 120 | 'role' => 'tool', 121 | 'name' => 'getNbaScore', 122 | 'content' => $function_response, 123 | ]; 124 | } 125 | } 126 | 127 | try { 128 | $response = $groq->chat()->completions()->create([ 129 | 'model' => 'llama3-groq-70b-8192-tool-use-preview', 130 | // 'model' => 'llama3-70b-8192', 131 | 'messages' => $messages 132 | ]); 133 | } catch (\LucianoTonet\GroqPHP\GroqException $err) { 134 | echo $err->getCode() . "
" . $err->getMessage() . "
" . $err->getType() . "
"; 135 | print_r($err->getHeaders()); 136 | echo "assistant:
Desculpe, não consegui entender sua solicitação. Tente novamente.
"; 137 | exit; 138 | } 139 | } 140 | 141 | echo "" . $response['choices'][0]['message']['role'] . ": " . "
"; 142 | echo $response['choices'][0]['message']['content'] . "
"; 143 | } else { 144 | echo "Ask about NBA game scores.
Results will be mocked for demo purposes.


"; 145 | } 146 | ?> 147 |
-------------------------------------------------------------------------------- /tests/GroqTest.php: -------------------------------------------------------------------------------- 1 | load(); 17 | $this->groq = new Groq($_ENV['GROQ_API_KEY']); 18 | } 19 | 20 | public function testInvalidApiKey() 21 | { 22 | $groq = new Groq('invalid_api_key'); 23 | 24 | $this->expectException(GroqException::class); 25 | $this->expectExceptionCode(0); // Error code will be 0 for invalid API keys 26 | $this->expectExceptionMessage('Invalid API Key'); // Error message will be 'Invalid API Key' 27 | 28 | $groq->chat()->completions()->create([ 29 | 'model' => 'llama3-70b-8192', 30 | 'messages' => [ 31 | ['role' => 'user', 'content' => 'Hello, world!'], 32 | ], 33 | ]); 34 | } 35 | 36 | public function testListModels() 37 | { 38 | $models = $this->groq->models()->list(); 39 | 40 | $this->assertIsArray($models); 41 | $this->assertNotEmpty($models); 42 | $this->assertArrayHasKey('data', $models); // Verify that the 'data' key is present 43 | } 44 | 45 | public function testChatCompletionWithValidApiKey() 46 | { 47 | $response = $this->groq->chat()->completions()->create([ 48 | 'model' => 'llama3-70b-8192', 49 | 'messages' => [ 50 | ['role' => 'user', 'content' => 'Hello, world!'], 51 | ], 52 | ]); 53 | 54 | $this->assertArrayHasKey('choices', $response); 55 | $this->assertNotEmpty($response['choices']); 56 | $this->assertArrayHasKey('message', $response['choices'][0]); 57 | $this->assertArrayHasKey('content', $response['choices'][0]['message']); 58 | } 59 | 60 | // public function testAudioTranscription() 61 | // { 62 | // $audioFile = __DIR__ . '/test_audio.mp3'; 63 | // $response = $this->groq->audio()->transcriptions()->create([ 64 | // 'file' => $audioFile, 65 | // 'model' => 'whisper-large-v3', 66 | // 'response_format' => 'json', 67 | // ]); 68 | 69 | // $this->assertArrayHasKey('text', $response); 70 | // $this->assertNotEmpty($response['text']); 71 | // } 72 | 73 | // public function testAudioTranslation() 74 | // { 75 | // $audioFile = __DIR__ . '/test_audio.mp3'; 76 | // $response = $this->groq->audio()->translations()->create([ 77 | // 'file' => $audioFile, 78 | // 'model' => 'whisper-large-v3', 79 | // 'response_format' => 'json', 80 | // ]); 81 | 82 | // $this->assertArrayHasKey('text', $response); 83 | // $this->assertNotEmpty($response['text']); 84 | // } 85 | 86 | public function testSetOptions() 87 | { 88 | // Setup 89 | $initialApiKey = $_ENV['GROQ_API_KEY']; 90 | $groq = new Groq($initialApiKey); 91 | 92 | // Test setting new options 93 | $newOptions = [ 94 | 'apiKey' => 'new_test_key', 95 | 'baseUrl' => 'https://test-api.groq.com', 96 | 'timeout' => 30000, 97 | 'maxRetries' => 3, 98 | 'headers' => ['X-Custom-Header' => 'test'], 99 | 'proxy' => 'http://proxy.test', 100 | 'verify' => false, 101 | 'debug' => true, 102 | 'stream' => true, 103 | 'responseFormat' => 'json' 104 | ]; 105 | 106 | $groq->setOptions($newOptions); 107 | 108 | // Verify API key was updated 109 | $this->assertEquals('new_test_key', $groq->apiKey()); 110 | 111 | // Get actual options 112 | $actualOptions = $groq->options; 113 | 114 | // Verify all options were set correctly 115 | $this->assertEquals($newOptions['baseUrl'], $groq->baseUrl); 116 | $this->assertEquals($newOptions['timeout'], $actualOptions['timeout']); 117 | $this->assertEquals($newOptions['maxRetries'], $actualOptions['maxRetries']); 118 | $this->assertEquals($newOptions['headers'], $actualOptions['headers']); 119 | $this->assertEquals($newOptions['proxy'], $actualOptions['proxy']); 120 | $this->assertEquals($newOptions['verify'], $actualOptions['verify']); 121 | $this->assertEquals($newOptions['debug'], $actualOptions['debug']); 122 | $this->assertEquals($newOptions['stream'], $actualOptions['stream']); 123 | $this->assertEquals($newOptions['responseFormat'], $actualOptions['responseFormat']); 124 | } 125 | 126 | public function testSetOptionsPartial() 127 | { 128 | // Setup - usar uma chave fixa para teste em vez da variável de ambiente 129 | $mockApiKey = 'test-api-key-' . uniqid(); 130 | $groq = new Groq($mockApiKey, ['timeout' => 10000]); 131 | 132 | // Test setting only some options 133 | $newOptions = [ 134 | 'timeout' => 20000, 135 | 'debug' => true 136 | ]; 137 | 138 | $groq->setOptions($newOptions); 139 | 140 | // Create a reflection class to access private properties 141 | $reflection = new \ReflectionClass($groq); 142 | $optionsProperty = $reflection->getProperty('options'); 143 | $optionsProperty->setAccessible(true); 144 | 145 | // Get actual options 146 | $actualOptions = $optionsProperty->getValue($groq); 147 | 148 | // Verify specific options were updated 149 | $this->assertEquals(20000, $actualOptions['timeout']); 150 | $this->assertEquals(true, $actualOptions['debug']); 151 | 152 | // Verify API key remained unchanged 153 | $this->assertEquals($mockApiKey, $groq->apiKey()); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Batch.php: -------------------------------------------------------------------------------- 1 | validateRequiredFields($data); 41 | $this->validateStatus($data['status']); 42 | $this->validateEndpoint($data['endpoint']); 43 | $this->validateCompletionWindow($data['completion_window']); 44 | 45 | $this->data = $data; 46 | } 47 | 48 | /** 49 | * Checks if the batch is in a terminal state 50 | */ 51 | public function isCompleted(): bool 52 | { 53 | return in_array($this->status, ['completed', 'failed', 'expired', 'cancelled']); 54 | } 55 | 56 | /** 57 | * Checks if the batch is still processing 58 | */ 59 | public function isProcessing(): bool 60 | { 61 | return in_array($this->status, ['validating', 'in_progress', 'finalizing']); 62 | } 63 | 64 | /** 65 | * Gets the progress percentage of the batch 66 | */ 67 | public function getProgress(): float 68 | { 69 | if (!isset($this->data['request_counts'])) { 70 | return 0.0; 71 | } 72 | 73 | $counts = $this->data['request_counts']; 74 | $total = $counts['total'] ?? 0; 75 | 76 | if ($total === 0) { 77 | return 0.0; 78 | } 79 | 80 | $completed = $counts['completed'] ?? 0; 81 | return round(($completed / $total) * 100, 2); 82 | } 83 | 84 | /** 85 | * Gets the error count for the batch 86 | */ 87 | public function getErrorCount(): int 88 | { 89 | return $this->data['request_counts']['failed'] ?? 0; 90 | } 91 | 92 | /** 93 | * Gets the completion time in seconds 94 | */ 95 | public function getCompletionTime(): ?float 96 | { 97 | if (!isset($this->data['completed_at']) || !isset($this->data['created_at'])) { 98 | return null; 99 | } 100 | 101 | return $this->data['completed_at'] - $this->data['created_at']; 102 | } 103 | 104 | /** 105 | * Gets the remaining time before expiration in seconds 106 | */ 107 | public function getTimeRemaining(): ?float 108 | { 109 | if (!isset($this->data['expires_at'])) { 110 | return null; 111 | } 112 | 113 | $now = time(); 114 | $expiresAt = $this->data['expires_at']; 115 | 116 | if ($expiresAt <= $now) { 117 | return 0.0; 118 | } 119 | 120 | return $expiresAt - $now; 121 | } 122 | 123 | private function validateRequiredFields(array $data): void 124 | { 125 | foreach ($this->requiredFields as $field) { 126 | if (!isset($data[$field])) { 127 | throw new GroqException( 128 | "Missing required field: {$field}", 129 | 400, 130 | 'invalid_request' 131 | ); 132 | } 133 | } 134 | } 135 | 136 | private function validateStatus(string $status): void 137 | { 138 | if (!in_array($status, $this->validStatuses)) { 139 | throw new GroqException( 140 | "Invalid batch status: {$status}. Valid statuses are: " . implode(', ', $this->validStatuses), 141 | 400, 142 | 'invalid_request' 143 | ); 144 | } 145 | } 146 | 147 | private function validateEndpoint(string $endpoint): void 148 | { 149 | if (!in_array($endpoint, $this->validEndpoints)) { 150 | throw new GroqException( 151 | 'Invalid endpoint. Only /v1/chat/completions is supported', 152 | 400, 153 | 'invalid_request' 154 | ); 155 | } 156 | } 157 | 158 | private function validateCompletionWindow(string $window): void 159 | { 160 | if (!in_array($window, $this->validCompletionWindows)) { 161 | throw new GroqException( 162 | 'Invalid completion_window. Valid windows are: ' . implode(', ', $this->validCompletionWindows), 163 | 400, 164 | 'invalid_request' 165 | ); 166 | } 167 | } 168 | 169 | public function __get(string $name) 170 | { 171 | return $this->data[$name] ?? null; 172 | } 173 | 174 | public function toArray(): array 175 | { 176 | return $this->data; 177 | } 178 | 179 | /** 180 | * Gets a summary of the batch status 181 | */ 182 | public function getSummary(): array 183 | { 184 | return [ 185 | 'id' => $this->id, 186 | 'status' => $this->status, 187 | 'progress' => $this->getProgress(), 188 | 'error_count' => $this->getErrorCount(), 189 | 'completion_time' => $this->getCompletionTime(), 190 | 'time_remaining' => $this->getTimeRemaining(), 191 | 'request_counts' => $this->data['request_counts'] ?? [ 192 | 'total' => 0, 193 | 'completed' => 0, 194 | 'failed' => 0 195 | ], 196 | 'created_at' => $this->data['created_at'], 197 | 'completed_at' => $this->data['completed_at'] ?? null, 198 | 'expires_at' => $this->data['expires_at'] ?? null 199 | ]; 200 | } 201 | } -------------------------------------------------------------------------------- /tests/FileManagerTest.php: -------------------------------------------------------------------------------- 1 | testJsonlPath = __DIR__ . '/fixtures/batch_file.jsonl'; 18 | $this->testInvalidJsonlPath = __DIR__ . '/fixtures/batch_file_invalid.jsonl'; 19 | } 20 | 21 | public function testUploadFile() 22 | { 23 | $file = $this->groq->files()->upload($this->testJsonlPath, 'batch'); 24 | 25 | $this->assertNotEmpty($file->id); 26 | $this->assertEquals('batch', $file->purpose); 27 | $this->assertNotEmpty($file->filename); 28 | 29 | // Limpar arquivo criado 30 | $this->groq->files()->delete($file->id); 31 | } 32 | 33 | public function testListFiles() 34 | { 35 | $files = $this->groq->files()->list('batch', ['limit' => 10]); 36 | 37 | $this->assertArrayHasKey('data', $files); 38 | $this->assertIsArray($files['data']); 39 | } 40 | 41 | public function testInvalidFileUpload() 42 | { 43 | $this->expectException(GroqException::class); 44 | $this->expectExceptionMessage('File not found'); 45 | $this->groq->files()->upload('/path/to/nonexistent.jsonl', 'batch'); 46 | } 47 | 48 | public function testInvalidJsonlFormat() 49 | { 50 | $this->expectException(GroqException::class); 51 | $this->expectExceptionMessage('Missing or invalid \'body\' field'); 52 | $this->groq->files()->upload($this->testInvalidJsonlPath, 'batch'); 53 | } 54 | 55 | public function testEmptyFile() 56 | { 57 | $emptyFile = sys_get_temp_dir() . '/empty.jsonl'; 58 | file_put_contents($emptyFile, ''); 59 | 60 | try { 61 | $this->expectException(GroqException::class); 62 | $this->expectExceptionMessage('File is empty'); 63 | $this->groq->files()->upload($emptyFile, 'batch'); 64 | } finally { 65 | unlink($emptyFile); 66 | } 67 | } 68 | 69 | public function testInvalidPurpose() 70 | { 71 | $this->expectException(GroqException::class); 72 | $this->expectExceptionMessage('Invalid purpose. Only "batch" is supported'); 73 | $this->groq->files()->upload($this->testJsonlPath, 'jsonl'); 74 | } 75 | 76 | public function testInvalidEndpoint() 77 | { 78 | $invalidEndpointFile = sys_get_temp_dir() . '/invalid_endpoint.jsonl'; 79 | $content = json_encode([ 80 | 'custom_id' => 'test-1', 81 | 'method' => 'POST', 82 | 'url' => '/v1/invalid/endpoint', 83 | 'body' => [ 84 | 'model' => 'llama3-8b-8192', 85 | 'messages' => [['role' => 'user', 'content' => 'test']] 86 | ] 87 | ]) . "\n"; 88 | 89 | file_put_contents($invalidEndpointFile, $content); 90 | 91 | try { 92 | $this->expectException(GroqException::class); 93 | $this->expectExceptionMessage('Invalid endpoint'); 94 | $this->groq->files()->upload($invalidEndpointFile, 'batch'); 95 | } finally { 96 | unlink($invalidEndpointFile); 97 | } 98 | } 99 | 100 | public function testInvalidAudioRequest() 101 | { 102 | $invalidAudioFile = sys_get_temp_dir() . '/invalid_audio.jsonl'; 103 | $content = json_encode([ 104 | 'custom_id' => 'audio-1', 105 | 'method' => 'POST', 106 | 'url' => '/v1/audio/transcriptions', 107 | 'body' => [ 108 | 'model' => 'whisper-large-v3', 109 | 'url' => 'not-a-valid-url' 110 | ] 111 | ]) . "\n"; 112 | 113 | file_put_contents($invalidAudioFile, $content); 114 | 115 | try { 116 | $this->expectException(GroqException::class); 117 | $this->expectExceptionMessage('Missing or invalid audio \'url\' field'); 118 | $this->groq->files()->upload($invalidAudioFile, 'batch'); 119 | } finally { 120 | unlink($invalidAudioFile); 121 | } 122 | } 123 | 124 | public function testMissingLanguageInAudioRequest() 125 | { 126 | $invalidAudioFile = sys_get_temp_dir() . '/missing_language.jsonl'; 127 | $content = json_encode([ 128 | 'custom_id' => 'audio-1', 129 | 'method' => 'POST', 130 | 'url' => '/v1/audio/transcriptions', 131 | 'body' => [ 132 | 'model' => 'whisper-large-v3', 133 | 'url' => 'https://example.com/audio.wav' 134 | ] 135 | ]) . "\n"; 136 | 137 | file_put_contents($invalidAudioFile, $content); 138 | 139 | try { 140 | $this->expectException(GroqException::class); 141 | $this->expectExceptionMessage('Missing required field \'language\''); 142 | $this->groq->files()->upload($invalidAudioFile, 'batch'); 143 | } finally { 144 | unlink($invalidAudioFile); 145 | } 146 | } 147 | 148 | public function testInvalidMessagesFormat() 149 | { 150 | $invalidMessagesFile = sys_get_temp_dir() . '/invalid_messages.jsonl'; 151 | $content = json_encode([ 152 | 'custom_id' => 'chat-1', 153 | 'method' => 'POST', 154 | 'url' => '/v1/chat/completions', 155 | 'body' => [ 156 | 'model' => 'llama3-8b-8192', 157 | 'messages' => [ 158 | ['invalid_field' => 'test'] // Missing role and content 159 | ] 160 | ] 161 | ]) . "\n"; 162 | 163 | file_put_contents($invalidMessagesFile, $content); 164 | 165 | try { 166 | $this->expectException(GroqException::class); 167 | $this->expectExceptionMessage('Message at index 0 is missing required fields'); 168 | $this->groq->files()->upload($invalidMessagesFile, 'batch'); 169 | } finally { 170 | unlink($invalidMessagesFile); 171 | } 172 | } 173 | } -------------------------------------------------------------------------------- /src/File.php: -------------------------------------------------------------------------------- 1 | validateRequiredFields($data); 30 | $this->validatePurpose($data['purpose']); 31 | 32 | if (!isset($data['status'])) { 33 | $data['status'] = 'uploaded'; 34 | } else { 35 | $this->validateStatus($data['status']); 36 | } 37 | 38 | $this->data = $data; 39 | } 40 | 41 | /** 42 | * Checks if the file is ready for use 43 | */ 44 | public function isReady(): bool 45 | { 46 | return $this->status === 'processed'; 47 | } 48 | 49 | /** 50 | * Checks if the file has failed processing 51 | */ 52 | public function hasFailed(): bool 53 | { 54 | return in_array($this->status, ['failed', 'error']); 55 | } 56 | 57 | /** 58 | * Checks if the file is still being processed 59 | */ 60 | public function isProcessing(): bool 61 | { 62 | return $this->status === 'processing'; 63 | } 64 | 65 | /** 66 | * Gets the file size in bytes 67 | */ 68 | public function getSize(): int 69 | { 70 | return $this->bytes; 71 | } 72 | 73 | /** 74 | * Gets the file size in a human-readable format 75 | */ 76 | public function getFormattedSize(): string 77 | { 78 | $bytes = $this->bytes; 79 | $units = ['B', 'KB', 'MB', 'GB']; 80 | $factor = floor((strlen((string) $bytes) - 1) / 3); 81 | 82 | return sprintf("%.2f %s", $bytes / pow(1024, $factor), $units[$factor]); 83 | } 84 | 85 | /** 86 | * Gets the file creation time as a DateTime object 87 | */ 88 | public function getCreatedAt(): \DateTime 89 | { 90 | return new \DateTime('@' . $this->created_at); 91 | } 92 | 93 | /** 94 | * Gets the time elapsed since file creation 95 | */ 96 | public function getTimeElapsed(): string 97 | { 98 | $now = time(); 99 | $elapsed = $now - $this->created_at; 100 | 101 | if ($elapsed < 60) { 102 | return "{$elapsed} seconds ago"; 103 | } 104 | 105 | if ($elapsed < 3600) { 106 | $minutes = floor($elapsed / 60); 107 | return "{$minutes} minute" . ($minutes > 1 ? 's' : '') . " ago"; 108 | } 109 | 110 | if ($elapsed < 86400) { 111 | $hours = floor($elapsed / 3600); 112 | return "{$hours} hour" . ($hours > 1 ? 's' : '') . " ago"; 113 | } 114 | 115 | $days = floor($elapsed / 86400); 116 | return "{$days} day" . ($days > 1 ? 's' : '') . " ago"; 117 | } 118 | 119 | /** 120 | * Gets the file extension 121 | */ 122 | public function getExtension(): string 123 | { 124 | return pathinfo($this->filename, PATHINFO_EXTENSION); 125 | } 126 | 127 | /** 128 | * Gets the file name without extension 129 | */ 130 | public function getBaseName(): string 131 | { 132 | return pathinfo($this->filename, PATHINFO_FILENAME); 133 | } 134 | 135 | private function validateRequiredFields(array $data): void 136 | { 137 | foreach ($this->requiredFields as $field) { 138 | if (!isset($data[$field])) { 139 | throw new GroqException( 140 | "Missing required field: {$field}", 141 | 400, 142 | 'invalid_request' 143 | ); 144 | } 145 | } 146 | 147 | if (!is_int($data['bytes']) || $data['bytes'] < 0) { 148 | throw new GroqException( 149 | 'Invalid bytes value. Must be a non-negative integer.', 150 | 400, 151 | 'invalid_request' 152 | ); 153 | } 154 | 155 | if (!is_int($data['created_at']) || $data['created_at'] <= 0) { 156 | throw new GroqException( 157 | 'Invalid created_at value. Must be a positive integer timestamp.', 158 | 400, 159 | 'invalid_request' 160 | ); 161 | } 162 | } 163 | 164 | private function validateStatus(string $status): void 165 | { 166 | if (!in_array($status, $this->validStatuses)) { 167 | throw new GroqException( 168 | "Invalid file status: {$status}. Valid statuses are: " . implode(', ', $this->validStatuses), 169 | 400, 170 | 'invalid_request' 171 | ); 172 | } 173 | } 174 | 175 | private function validatePurpose(string $purpose): void 176 | { 177 | if (!in_array($purpose, $this->validPurposes)) { 178 | throw new GroqException( 179 | "Invalid file purpose: {$purpose}. Only 'batch' is supported.", 180 | 400, 181 | 'invalid_request' 182 | ); 183 | } 184 | } 185 | 186 | public function __get(string $name) 187 | { 188 | return $this->data[$name] ?? null; 189 | } 190 | 191 | public function toArray(): array 192 | { 193 | return $this->data; 194 | } 195 | 196 | /** 197 | * Gets a summary of the file 198 | */ 199 | public function getSummary(): array 200 | { 201 | return [ 202 | 'id' => $this->id, 203 | 'filename' => $this->filename, 204 | 'size' => $this->getFormattedSize(), 205 | 'status' => $this->status, 206 | 'purpose' => $this->purpose, 207 | 'created' => $this->getTimeElapsed(), 208 | 'extension' => $this->getExtension(), 209 | 'is_ready' => $this->isReady(), 210 | 'has_failed' => $this->hasFailed() 211 | ]; 212 | } 213 | } -------------------------------------------------------------------------------- /examples/reasoning.php: -------------------------------------------------------------------------------- 1 |
2 | $_POST['model'] ?? 'deepseek-r1-distill-llama-70b', 9 | 'temperature' => isset($_POST['temperature']) ? floatval($_POST['temperature']) : null, 10 | 'max_completion_tokens' => isset($_POST['max_completion_tokens']) ? intval($_POST['max_completion_tokens']) : null, 11 | 'top_p' => isset($_POST['top_p']) ? floatval($_POST['top_p']) : null, 12 | 'frequency_penalty' => isset($_POST['frequency_penalty']) ? floatval($_POST['frequency_penalty']) : null, 13 | 'presence_penalty' => isset($_POST['presence_penalty']) ? floatval($_POST['presence_penalty']) : null, 14 | 'reasoning_format' => $_POST['reasoning_format'] ?? 'raw' 15 | ]; 16 | 17 | // Remove undefined options 18 | $options = array_filter($options, fn($value) => !is_null($value)); 19 | 20 | // Add system prompt if provided 21 | if (!empty($_POST['system_prompt'])) { 22 | $options['system_prompt'] = $_POST['system_prompt']; 23 | } 24 | 25 | echo "

Prompt: " . htmlspecialchars($prompt) . "

"; 26 | 27 | try { 28 | $response = $groq->reasoning()->analyze($prompt, $options); 29 | 30 | $content = $response['choices'][0]['message']['content']; 31 | 32 | if ($options['reasoning_format'] == 'parsed' && isset($response['choices'][0]['message']['reasoning'])) { 33 | $reasoning = $response['choices'][0]['message']['reasoning']; 34 | echo "

Reasoning:

"; 35 | echo "
"; 36 | echo '
'; 37 | echo nl2br($reasoning); 38 | echo '
'; 39 | echo "
"; 40 | } else { 41 | $content = preg_replace( 42 | '/(.*?<\/think>)/s', 43 | '<think>$1</think>', 44 | $content 45 | ); 46 | } 47 | 48 | echo "

Answer:

"; 49 | echo nl2br($content); 50 | echo "
"; 51 | } catch (LucianoTonet\GroqPHP\GroqException $err) { 52 | echo "

Error: " . htmlspecialchars($err->getMessage()) . "

"; 53 | } 54 | } 55 | ?> 56 | 57 |
58 |
59 | 60 | 64 |
65 |
66 | 68 | 71 |
72 |
73 | 74 | 79 |
80 |
81 | 82 | 85 |
86 |
87 |
88 | 90 | 92 |
93 |
94 | 96 | 98 |
99 |
100 | 101 | 103 |
104 |
105 | 107 | 109 |
110 |
111 | 115 |
116 | -------------------------------------------------------------------------------- /src/Translations.php: -------------------------------------------------------------------------------- 1 | groq = $groq; 30 | } 31 | 32 | /** 33 | * Translation Usage 34 | * This method translates spoken words in audio or video files to the specified language. 35 | * 36 | * Optional Parameters: 37 | * - prompt: Provides context or specifies the spelling of unknown words. 38 | * - response_format: Defines the format of the response. The default is "json". 39 | * Use "verbose_json" to receive timestamps for audio segments. 40 | * Use "text" to return a text response. 41 | * vtt and srt formats are not supported. 42 | * - temperature: Specifies a value between 0 and 1 to control the variability of the translation output. 43 | * 44 | * @param array $params 45 | * @return array|string|Stream 46 | * @throws \InvalidArgumentException 47 | */ 48 | public function create(array $params): array|string|Stream 49 | { 50 | $this->validateParams($params); 51 | $client = new Client(); 52 | $multipart = $this->buildMultipart($params); 53 | 54 | try { 55 | $response = $client->request('POST', $this->groq->baseUrl() . '/audio/translations', [ 56 | 'headers' => [ 57 | 'Authorization' => 'Bearer ' . $this->groq->apiKey() 58 | ], 59 | 'multipart' => $multipart 60 | ]); 61 | 62 | return $this->handleResponse($response, $params['response_format'] ?? 'json'); 63 | } catch (GuzzleException $e) { 64 | if ($e instanceof RequestException && $e->hasResponse()) { 65 | $responseBody = $e->getResponse()->getBody(); 66 | $responseBodyString = $responseBody ? (string) $responseBody : 'Response body is empty or null'; 67 | if (trim($responseBodyString) === '') { 68 | $responseBodyString = 'Response body is empty or null'; 69 | } 70 | throw GroqException::createFromResponse($e->getResponse()); 71 | } else { 72 | throw new GroqException('Unexpected error while creating translations: ' . $e->getMessage(), $e->getCode(), 'UnexpectedException', []); 73 | } 74 | } 75 | } 76 | 77 | /** 78 | * Validates the input parameters. 79 | * 80 | * @param array $params 81 | * @throws \InvalidArgumentException 82 | */ 83 | private function validateParams(array $params): void 84 | { 85 | if (empty($params['file'])) { 86 | throw new \InvalidArgumentException('The "file" parameter is required.'); 87 | } 88 | if (!file_exists($params['file'])) { 89 | throw new \InvalidArgumentException('The specified file does not exist.'); 90 | } 91 | } 92 | 93 | /** 94 | * Builds the multipart structure for the request. 95 | * 96 | * @param array $params 97 | * @return array 98 | */ 99 | private function buildMultipart(array $params): array 100 | { 101 | $multipart = [ 102 | [ 103 | 'name' => 'file', 104 | 'contents' => fopen($params['file'], 'r') 105 | ], 106 | [ 107 | 'name' => 'model', 108 | 'contents' => $params['model'] ?? 'whisper-large-v3' 109 | ], 110 | [ 111 | 'name' => 'temperature', 112 | 'contents' => $params['temperature'] ?? 0.0 113 | ], 114 | ]; 115 | 116 | if (!empty($params['prompt'])) { 117 | $multipart[] = [ 118 | 'name' => 'prompt', 119 | 'contents' => $params['prompt'] 120 | ]; 121 | } 122 | 123 | if (!empty($params['response_format'])) { 124 | $multipart[] = [ 125 | 'name' => 'response_format', 126 | 'contents' => $params['response_format'] 127 | ]; 128 | } 129 | 130 | return $multipart; 131 | } 132 | 133 | /** 134 | * Handles the response of the request. 135 | * 136 | * @param ResponseInterface $response 137 | * @param string $responseFormat 138 | * @return array|string|Stream 139 | */ 140 | private function handleResponse(ResponseInterface $response, string $responseFormat): array|string|Stream 141 | { 142 | $body = $response->getBody()->getContents(); 143 | 144 | if ($responseFormat === 'text') { 145 | return $body; // Returns the body of the response directly 146 | } 147 | 148 | $data = json_decode($body, true); 149 | 150 | if (json_last_error() !== JSON_ERROR_NONE) { 151 | throw new GroqException('Error decoding the JSON response: ' . json_last_error_msg(), 0, 'JsonDecodeError'); 152 | } 153 | 154 | return $data; 155 | } 156 | 157 | /** 158 | * Streams the response from the request. 159 | * 160 | * @param Request $request 161 | * @param array $options 162 | * @return Stream 163 | */ 164 | private function streamResponse(Request $request, array $options): Stream 165 | { 166 | try { 167 | $client = new Client(); 168 | $response = $client->send($request, array_merge($options, ['stream' => true])); 169 | return new Stream($response); 170 | } catch (RequestException $e) { 171 | $responseBody = $e->getResponse() ? $e->getResponse()->getBody() : null; 172 | $responseBodyString = $responseBody ? (string) $responseBody : 'No response body available'; 173 | throw new GroqException('Failed to stream the response: ' . $responseBodyString, $e->getCode(), 'RequestException', []); 174 | } catch (GuzzleException $e) { 175 | throw new GroqException('An unexpected error occurred: ' . $e->getMessage(), $e->getCode(), 'UnexpectedException', []); 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /examples/audio-transcriptions.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 'Chinese', 8 | 'pt' => 'Portuguese', 9 | 'ta' => 'Tamil', 10 | 'sw' => 'Swahili', 11 | 'km' => 'Khmer', 12 | 'jv' => 'Javanese', 13 | 'nl' => 'Dutch', 14 | 'vi' => 'Vietnamese', 15 | 'br' => 'Breton', 16 | 'sq' => 'Albanian', 17 | 'si' => 'Sinhala', 18 | 'my' => 'Burmese', 19 | 'kk' => 'Kazakh', 20 | 'ka' => 'Georgian', 21 | 'bo' => 'Tibetan', 22 | 'tg' => 'Tajik', 23 | 'yi' => 'Yiddish', 24 | 'hi' => 'Hindi', 25 | 'th' => 'Thai', 26 | 'ur' => 'Urdu', 27 | 'hr' => 'Croatian', 28 | 'ne' => 'Nepali', 29 | 'so' => 'Somali', 30 | 'fo' => 'Faroese', 31 | 'su' => 'Sundanese', 32 | 'yue' => 'Cantonese', 33 | 'sr' => 'Serbian', 34 | 'tl' => 'Tagalog', 35 | 'he' => 'Hebrew', 36 | 'mn' => 'Mongolian', 37 | 'oc' => 'Occitan', 38 | 'tk' => 'Turkmen', 39 | 'mg' => 'Malagasy', 40 | 'la' => 'Latin', 41 | 'sl' => 'Slovenian', 42 | 'ca' => 'Catalan', 43 | 'id' => 'Indonesian', 44 | 'uk' => 'Ukrainian', 45 | 'el' => 'Greek', 46 | 'da' => 'Danish', 47 | 'no' => 'Norwegian', 48 | 'kn' => 'Kannada', 49 | 'et' => 'Estonian', 50 | 'eu' => 'Basque', 51 | 'nn' => 'Norwegian Nynorsk', 52 | 'haw' => 'Hawaiian', 53 | 'ar' => 'Arabic', 54 | 'lv' => 'Latvian', 55 | 'bn' => 'Bengali', 56 | 'sn' => 'Shona', 57 | 'yo' => 'Yoruba', 58 | 'am' => 'Amharic', 59 | 'ms' => 'Malay', 60 | 'bg' => 'Bulgarian', 61 | 'ml' => 'Malayalam', 62 | 'cy' => 'Welsh', 63 | 'ps' => 'Pashto', 64 | 'sd' => 'Sindhi', 65 | 'gu' => 'Gujarati', 66 | 'ru' => 'Russian', 67 | 'ko' => 'Korean', 68 | 'ro' => 'Romanian', 69 | 'sk' => 'Slovak', 70 | 'te' => 'Telugu', 71 | 'hy' => 'Armenian', 72 | 'lb' => 'Luxembourgish', 73 | 'as' => 'Assamese', 74 | 'ln' => 'Lingala', 75 | 'en' => 'English', 76 | 'de' => 'German', 77 | 'sv' => 'Swedish', 78 | 'fi' => 'Finnish', 79 | 'gl' => 'Galician', 80 | 'af' => 'Afrikaans', 81 | 'lt' => 'Lithuanian', 82 | 'bs' => 'Bosnian', 83 | 'pl' => 'Polish', 84 | 'fa' => 'Persian', 85 | 'az' => 'Azerbaijani', 86 | 'pa' => 'Punjabi', 87 | 'ha' => 'Hausa', 88 | 'ja' => 'Japanese', 89 | 'tr' => 'Turkish', 90 | 'be' => 'Belarusian', 91 | 'lo' => 'Lao', 92 | 'ht' => 'Haitian Creole', 93 | 'ba' => 'Bashkir', 94 | 'uz' => 'Uzbek', 95 | 'mt' => 'Maltese', 96 | 'es' => 'Spanish', 97 | 'it' => 'Italian', 98 | 'hu' => 'Hungarian', 99 | 'mk' => 'Macedonian', 100 | 'is' => 'Icelandic', 101 | 'mr' => 'Marathi', 102 | 'fr' => 'French', 103 | 'cs' => 'Czech', 104 | 'mi' => 'Maori', 105 | 'sa' => 'Sanskrit', 106 | 'tt' => 'Tatar' 107 | ]; 108 | ?> 109 | 110 | 116 | 117 | 118 | 123 | 124 | 125 | 126 | 127 |
128 | 129 | $newFilePath, 140 | 'model' => 'whisper-large-v3', 141 | 'response_format' => $_POST['response_format'] ?? 'json', 142 | 'temperature' => $_POST['temperature'] ?? 0.0, 143 | ]; 144 | 145 | if (isset($_POST['language'])) { 146 | $transcriptionParams['language'] = $_POST['language'] ?? 'en'; 147 | } 148 | 149 | if (isset($_POST['prompt'])) { 150 | $transcriptionParams['prompt'] = $_POST['prompt']; 151 | } 152 | 153 | $transcription = $groq->audio()->transcriptions()->create($transcriptionParams); 154 | 155 | if ($_POST['response_format'] === 'verbose_json') { 156 | // Aqui você pode processar a transcrição para incluir timestamps, se necessário 157 | echo '
';
158 |             echo json_encode($transcription, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
159 |             echo '
'; 160 | } elseif ($_POST['response_format'] === 'text') { 161 | // Retorna apenas o texto da transcrição 162 | echo '

'; 163 | echo $transcription ?? ''; 164 | echo '

'; 165 | } else { 166 | // Formato padrão é json 167 | echo '
';
168 |             echo json_encode($transcription, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
169 |             echo '
'; 170 | } 171 | } catch (LucianoTonet\GroqPHP\GroqException $e) { 172 | echo "Error:
" . htmlspecialchars($e->getMessage()) . "
"; 173 | } finally { 174 | if (file_exists($newFilePath)) { 175 | unlink($newFilePath); 176 | } 177 | } 178 | } -------------------------------------------------------------------------------- /examples/files.php: -------------------------------------------------------------------------------- 1 | files()->delete($fileId); 11 | if ($result['deleted']) { 12 | $success = "File deleted successfully."; 13 | } 14 | break; 15 | 16 | case 'download': 17 | if (!isset($_REQUEST['file_id']) || !preg_match('/^[a-zA-Z0-9\-_]+$/', $_REQUEST['file_id'])) { 18 | die('File ID is required'); 19 | } 20 | 21 | $fileId = $_REQUEST['file_id']; 22 | $content = $groq->files()->download($fileId); 23 | 24 | header('Content-Type: application/x-jsonlines'); 25 | header('Content-Disposition: attachment; filename="download.jsonl"'); 26 | header('Content-Length: ' . strlen($content)); 27 | // Prevent browser caching of sensitive data 28 | header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); 29 | header('Pragma: no-cache'); 30 | 31 | echo $content; 32 | exit; 33 | } 34 | } catch (GroqException $e) { 35 | $error = $e->getMessage(); 36 | } 37 | } 38 | 39 | // Handle file upload 40 | if (isset($_FILES['jsonl_file'])) { 41 | try { 42 | // Validate file size (e.g., 100MB limit) 43 | if ($_FILES['jsonl_file']['size'] > 100 * 1024 * 1024) { 44 | throw new GroqException('File size exceeds limit', 400, 'invalid_request'); 45 | } 46 | 47 | // Validate file type 48 | $finfo = finfo_open(FILEINFO_MIME_TYPE); 49 | $mimeType = finfo_file($finfo, $_FILES['jsonl_file']['tmp_name']); 50 | finfo_close($finfo); 51 | if ($mimeType !== 'application/x-ndjson' && $mimeType !== 'text/plain') { 52 | throw new GroqException('Invalid file type', 400, 'invalid_request'); 53 | } 54 | 55 | $tempFile = $_FILES['jsonl_file']['tmp_name']; 56 | $originalName = $_FILES['jsonl_file']['name']; 57 | 58 | $newTempFile = tempnam(sys_get_temp_dir(), 'groq_') . '.jsonl'; 59 | try { 60 | if (!copy($tempFile, $newTempFile)) { 61 | throw new GroqException('Failed to process upload file', 400, 'invalid_request'); 62 | } 63 | 64 | // Upload file 65 | $file = $groq->files()->upload($newTempFile, 'batch'); 66 | $success = "File uploaded successfully!"; 67 | } finally { 68 | // Always clean up temporary file 69 | if (file_exists($newTempFile)) { 70 | unlink($newTempFile); 71 | } 72 | } 73 | } catch (GroqException $e) { 74 | $error = $e->getMessage(); 75 | } 76 | } 77 | 78 | // Get list of files 79 | try { 80 | $filesList = $groq->files()->list('batch', ['limit' => 10]); 81 | $files = $filesList['data']; 82 | } catch (GroqException $e) { 83 | $error = $e->getMessage(); 84 | } 85 | ?> 86 | 87 |
88 | 89 |
90 |

Error:

91 |
92 | 93 | 94 | 95 |
96 |

97 |
98 | 99 | 100 | 101 |
102 |

Upload JSONL File

103 |
104 |
105 | 108 | 118 |
119 | 125 |
126 |
127 | 128 | 129 |
130 |

Available Files

131 | 132 |

No files available.

133 | 134 |
135 | 136 |
137 |
138 |
139 |

filename) ?>

140 |
141 |

ID: id) ?>

142 |

Size: bytes / 1024, 2) ?> KB

143 |

Created: created_at)) ?>

144 |

Status: 148 | status) ?> 149 |

150 |
151 |
152 | 153 |
154 | 155 | Download 156 | 157 | 158 |
160 | 161 | 162 | 167 |
168 |
169 |
170 |
171 | 172 |
173 | 174 |
175 |
-------------------------------------------------------------------------------- /src/Transcriptions.php: -------------------------------------------------------------------------------- 1 | groq = $groq; 30 | } 31 | 32 | /** 33 | * Audio Transcription 34 | * This method transcribes spoken words in audio or video files. 35 | * 36 | * Optional Parameters: 37 | * - prompt: Provides context or specifies the spelling of unknown words. 38 | * - response_format: Defines the format of the response. The default is "json". 39 | * Use "verbose_json" to receive timestamps for audio segments. 40 | * Use "text" to return a plain text response. 41 | * vtt and srt formats are not supported. 42 | * - temperature: Specifies a value between 0 and 1 to control the variability of the transcription. 43 | * - language: Specifies the language for the transcription (optional; Whisper will automatically detect if not specified). 44 | * Use ISO 639-1 language codes (e.g., "en" for English, "fr" for French, etc.). 45 | * Specifying a language can improve the accuracy and speed of the transcription. 46 | * - timestamp_granularities[] is not supported. 47 | * 48 | * @param array $params 49 | * @return array|string|Stream 50 | */ 51 | public function create(array $params): array|string|Stream 52 | { 53 | $this->validateParams($params); // Validate parameters 54 | $client = new Client(); 55 | $multipart = $this->buildMultipart($params); 56 | 57 | try { 58 | $response = $client->request('POST', $this->groq->baseUrl() . '/audio/transcriptions', [ 59 | 'headers' => [ 60 | 'Authorization' => 'Bearer ' . $this->groq->apiKey() 61 | ], 62 | 'multipart' => $multipart 63 | ]); 64 | 65 | return $this->handleResponse($response, $params['response_format'] ?? 'json'); 66 | } catch (RequestException $e) { 67 | $response = $e->getResponse(); 68 | $responseBody = $response && $response->getBody() ? (string) $response->getBody() : 'No response body available'; 69 | throw new GroqException('Error transcribing audio: ' . $responseBody, $e->getCode(), 'RequestException'); 70 | } catch (GuzzleException $e) { 71 | throw new GroqException('An unexpected error occurred: ' . $e->getMessage(), $e->getCode(), 'GuzzleException'); 72 | } catch (\Exception $e) { 73 | throw new GroqException('An unexpected error occurred: ' . $e->getMessage(), $e->getCode(), 'Exception'); 74 | } 75 | } 76 | 77 | /** 78 | * Validates the input parameters. 79 | * 80 | * @param array $params 81 | * @throws \InvalidArgumentException 82 | */ 83 | private function validateParams(array $params): void 84 | { 85 | if (empty($params['file'])) { 86 | throw new \InvalidArgumentException('The "file" parameter is required.'); 87 | } 88 | 89 | if (!file_exists($params['file'])) { 90 | throw new \InvalidArgumentException('The specified file does not exist.'); 91 | } 92 | 93 | if (isset($params['temperature']) && ($params['temperature'] < 0 || $params['temperature'] > 1)) { 94 | throw new \InvalidArgumentException('The "temperature" parameter must be between 0 and 1.'); 95 | } 96 | } 97 | 98 | /** 99 | * Builds the multipart structure for the request. 100 | * 101 | * @param array $params 102 | * @return array 103 | */ 104 | private function buildMultipart(array $params): array 105 | { 106 | $multipart = [ 107 | [ 108 | 'name' => 'file', 109 | 'contents' => fopen($params['file'], 'r') 110 | ], 111 | [ 112 | 'name' => 'model', 113 | 'contents' => $params['model'] ?? 'whisper-large-v3' 114 | ], 115 | ]; 116 | 117 | if (isset($params['temperature'])) { 118 | $multipart[] = [ 119 | 'name' => 'temperature', 120 | 'contents' => $params['temperature'] 121 | ]; 122 | } 123 | 124 | if (isset($params['language'])) { 125 | $multipart[] = [ 126 | 'name' => 'language', 127 | 'contents' => $params['language'] 128 | ]; 129 | } 130 | 131 | if (isset($params['prompt'])) { 132 | $multipart[] = [ 133 | 'name' => 'prompt', 134 | 'contents' => $params['prompt'] 135 | ]; 136 | } 137 | 138 | if (isset($params['response_format'])) { 139 | $multipart[] = [ 140 | 'name' => 'response_format', 141 | 'contents' => $params['response_format'] 142 | ]; 143 | } 144 | 145 | return $multipart; 146 | } 147 | 148 | /** 149 | * Handles the response from the request. 150 | * 151 | * @param ResponseInterface $response 152 | * @param string $responseFormat 153 | * @return array|string|Stream 154 | */ 155 | private function handleResponse(ResponseInterface $response, string $responseFormat): array|string|Stream 156 | { 157 | $body = $response->getBody()->getContents(); 158 | 159 | if ($responseFormat === 'text') { 160 | return $body; // Return the body of the response directly 161 | } 162 | 163 | $data = json_decode($body, true); 164 | 165 | if (json_last_error() !== JSON_ERROR_NONE) { 166 | throw new GroqException('Error decoding the JSON response: ' . json_last_error_msg(), 0, 'JsonDecodeError'); 167 | } 168 | 169 | return $data; 170 | } 171 | 172 | /** 173 | * Streams the response from the request. 174 | * 175 | * @param Request $request 176 | * @param array $options 177 | * @return Stream 178 | */ 179 | private function streamResponse(Request $request, array $options): Stream 180 | { 181 | try { 182 | $client = new Client(); 183 | $response = $client->send($request, array_merge($options, ['stream' => true])); 184 | return new Stream($response); 185 | } catch (RequestException $e) { 186 | $responseBody = $e->getResponse() ? ($e->getResponse()->getBody() ? (string) $e->getResponse()->getBody() : 'Response body is empty') : 'No response body available'; 187 | throw new GroqException('Failed to stream the response: ' . $responseBody, $e->getCode(), 'RequestException'); 188 | } catch (GuzzleException $e) { 189 | throw new GroqException('An unexpected error occurred: ' . $e->getMessage(), $e->getCode(), 'GuzzleException'); 190 | } catch (\Exception $e) { 191 | throw new GroqException('An unexpected error occurred: ' . $e->getMessage(), $e->getCode(), 'Exception'); 192 | } 193 | } 194 | } -------------------------------------------------------------------------------- /src/Groq.php: -------------------------------------------------------------------------------- 1 | apiKey = $apiKey; // Set the API key 46 | $this->options = $options; // Set the options 47 | 48 | // Get base URL and ensure it ends with a forward slash 49 | $baseUrl = $options['baseUrl'] ?? $_ENV['GROQ_API_BASE'] ?? getenv('GROQ_API_BASE'); 50 | 51 | if (empty($baseUrl) || !is_string($baseUrl) || in_array($baseUrl, ['null', 'false'], true)) { 52 | $baseUrl = 'https://api.groq.com/openai/v1'; 53 | } 54 | 55 | $this->baseUrl = rtrim($baseUrl, '/') . '/'; // Ensure trailing slash 56 | } 57 | 58 | /** 59 | * Sets additional options for the Groq instance. 60 | * 61 | * @param array $options Options to be merged with existing options 62 | * 63 | * Authentication: 64 | * - apiKey: (string) The API key for authentication 65 | * - baseUrl: (string) The base URL for API requests (default: https://api.groq.com/openai/v1) 66 | * 67 | * Request Configuration: 68 | * - timeout: (int) Request timeout in milliseconds 69 | * 70 | * Model Parameters: 71 | * - model: (string) ID of the model to use 72 | * - temperature: (float) Sampling temperature between 0 and 2 (default: 1) 73 | * - max_completion_tokens: (int) Maximum tokens to generate 74 | * - top_p: (float) Nucleus sampling between 0 and 1 (default: 1) 75 | * - frequency_penalty: (float) Number between -2.0 and 2.0 (default: 0) 76 | * - presence_penalty: (float) Number between -2.0 and 2.0 (default: 0) 77 | * 78 | * Response Options: 79 | * - stream: (bool) Enable streaming responses (default: false) 80 | * - response_format: (array) Format specification for model output 81 | * Example: ['type' => 'json_object'] for JSON mode 82 | * 83 | * Tool Options: 84 | * - tool_choice: (string|array) Tool selection mode (auto|none|specific) 85 | * - parallel_tool_calls: (bool) Enable parallel tool calls (default: true) 86 | * - tools: (array) List of tools the model may use 87 | * 88 | * Additional Options: 89 | * - seed: (int|null) Integer for deterministic sampling 90 | * - stop: (string|array|null) Up to 4 sequences where generation should stop 91 | * - user: (string|null) Unique identifier for end-user tracking 92 | * - service_tier: (string|null) Service tier to use (auto|flex) 93 | */ 94 | public function setOptions(array $options): void 95 | { 96 | // Update API key if provided 97 | if (isset($options['apiKey'])) { 98 | $this->apiKey = $options['apiKey']; 99 | } 100 | 101 | // Update base URL if provided 102 | if (isset($options['baseUrl'])) { 103 | $this->baseUrl = $options['baseUrl']; 104 | } 105 | 106 | // Merge new options with existing ones 107 | $this->options = array_merge($this->options, $options); 108 | } 109 | 110 | /** 111 | * Creates a new Chat instance. 112 | * 113 | * @return Chat A new instance of the Chat class 114 | */ 115 | public function chat(): Chat 116 | { 117 | return new Chat($this); // Return a new Chat instance 118 | } 119 | 120 | /** 121 | * Creates a new Audio instance. 122 | * 123 | * @return Audio A new instance of the Audio class 124 | */ 125 | public function audio(): Audio 126 | { 127 | return new Audio($this); // Return a new Audio instance 128 | } 129 | 130 | /** 131 | * Creates a new Models instance. 132 | * 133 | * @return Models A new instance of the Models class 134 | */ 135 | public function models(): Models 136 | { 137 | return new Models($this); // Return a new Models instance 138 | } 139 | 140 | /** 141 | * Sends an HTTP request using the Guzzle client and returns the response. 142 | * 143 | * @param Request $request The HTTP request to be sent 144 | * @return ResponseInterface The response from the API 145 | */ 146 | public function makeRequest(Request $request): ResponseInterface 147 | { 148 | $client = new Client([ 149 | 'base_uri' => $this->baseUrl, 150 | 'headers' => [ 151 | 'Authorization' => 'Bearer ' . $this->apiKey 152 | ] 153 | ]); // Create a new Guzzle client 154 | 155 | $response = $client->send($request); // Send the request and return the response 156 | 157 | return $response; 158 | } 159 | 160 | /** 161 | * Retrieves the base URL for the API. 162 | * 163 | * @return string The base URL 164 | */ 165 | public function baseUrl(): string 166 | { 167 | return $this->baseUrl; // Return the base URL 168 | } 169 | 170 | /** 171 | * Retrieves the API key used for authentication. 172 | * 173 | * @return string The API key 174 | */ 175 | public function apiKey(): string 176 | { 177 | return $this->apiKey; // Return the API key 178 | } 179 | 180 | /** 181 | * Creates a new Vision instance. 182 | * 183 | * @return Vision A new instance of the Vision class 184 | */ 185 | public function vision(): Vision 186 | { 187 | return new Vision($this); // Return a new Vision instance 188 | } 189 | 190 | /** 191 | * Creates a new Reasoning instance. 192 | * 193 | * @return Reasoning A new instance of the Reasoning class 194 | */ 195 | public function reasoning(): Reasoning 196 | { 197 | return new Reasoning($this); // Return a new Reasoning instance 198 | } 199 | 200 | /** 201 | * Creates a new Speech instance. 202 | * 203 | * @return Speech A new instance of the Speech class 204 | */ 205 | public function speech(): Speech 206 | { 207 | return new Speech($this); 208 | } 209 | 210 | /** 211 | * Creates a new Files instance. 212 | * 213 | * @return FileManager A new instance of the FileManager class 214 | */ 215 | public function files(): FileManager 216 | { 217 | return new FileManager($this); 218 | } 219 | 220 | /** 221 | * Creates a new Batches instance. 222 | * 223 | * @return BatchManager A new instance of the BatchManager class 224 | */ 225 | public function batches(): BatchManager 226 | { 227 | return new BatchManager($this); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /tests/EnvironmentVariablesTest.php: -------------------------------------------------------------------------------- 1 | load(); 31 | } 32 | 33 | // Save original environment values 34 | $this->originalApiKey = getenv('GROQ_API_KEY'); 35 | $this->originalApiBase = getenv('GROQ_API_BASE'); 36 | } 37 | 38 | /** 39 | * Restore original environment values after tests 40 | */ 41 | protected function tearDown(): void 42 | { 43 | // Restore API key if it was previously set 44 | if ($this->originalApiKey !== false) { 45 | putenv("GROQ_API_KEY={$this->originalApiKey}"); 46 | } else { 47 | putenv('GROQ_API_KEY'); // Unset the variable 48 | } 49 | 50 | // Restore API base URL if it was previously set 51 | if ($this->originalApiBase !== false) { 52 | putenv("GROQ_API_BASE={$this->originalApiBase}"); 53 | } else { 54 | putenv('GROQ_API_BASE'); // Unset the variable 55 | } 56 | } 57 | 58 | /** 59 | * Tests that an exception is thrown when GROQ_API_KEY is not set 60 | */ 61 | public function testExceptionWhenApiKeyNotSet() 62 | { 63 | // Unset the API key environment variable 64 | putenv('GROQ_API_KEY'); 65 | unset($_ENV['GROQ_API_KEY']); 66 | 67 | // Expect an exception to be thrown when creating a Groq instance without an API key 68 | $this->expectException(GroqException::class); 69 | $this->expectExceptionMessage('The API key is not set. Please provide an API key when initializing the Groq client or set the environment variable GROQ_API_KEY.'); 70 | 71 | new Groq(); 72 | } 73 | 74 | /** 75 | * Tests that an exception is thrown when GROQ_API_KEY is set to an empty string 76 | */ 77 | public function testExceptionWhenApiKeyEmpty() 78 | { 79 | // Set the API key environment variable to an empty string 80 | putenv('GROQ_API_KEY='); 81 | 82 | // Expect an exception to be thrown 83 | $this->expectException(GroqException::class); 84 | $this->expectExceptionMessage('The API key is not set. Please provide an API key when initializing the Groq client or set the environment variable GROQ_API_KEY.'); 85 | 86 | new Groq(); 87 | } 88 | 89 | /** 90 | * Tests that API key can be passed directly to the constructor 91 | */ 92 | public function testApiKeyFromConstructor() 93 | { 94 | // Unset the API key environment variable 95 | putenv('GROQ_API_KEY'); 96 | 97 | // Create a Groq instance with the API key passed to the constructor 98 | $groq = new Groq('test-api-key'); 99 | 100 | // Verify the API key was set correctly 101 | $this->assertEquals('test-api-key', $groq->apiKey()); 102 | } 103 | 104 | /** 105 | * Tests constructor behavior when GROQ_API_BASE is not defined 106 | */ 107 | public function testConstructWithoutApiBaseUrl() 108 | { 109 | // Unset the API base URL environment variable 110 | putenv('GROQ_API_BASE'); 111 | 112 | // Create a Groq instance with a test API key 113 | $groq = new Groq('test-api-key'); 114 | 115 | // Verify the base URL was set to the default value 116 | $this->assertEquals('https://api.groq.com/openai/v1/', $groq->baseUrl); 117 | } 118 | 119 | /** 120 | * Tests constructor behavior when GROQ_API_BASE is set to an empty string 121 | */ 122 | public function testConstructWithEmptyStringApiBaseUrl() 123 | { 124 | // Set the API base URL environment variable to an empty string 125 | putenv('GROQ_API_BASE='); 126 | 127 | // Create a Groq instance with a test API key 128 | $groq = new Groq('test-api-key'); 129 | 130 | // Verify the base URL was set to the default value 131 | $this->assertEquals('https://api.groq.com/openai/v1/', $groq->baseUrl); 132 | } 133 | 134 | /** 135 | * Tests constructor behavior when GROQ_API_BASE is set to the string literal "null" 136 | */ 137 | public function testConstructWithNullStringApiBaseUrl() 138 | { 139 | // Set the API base URL environment variable to the string "null" 140 | putenv('GROQ_API_BASE=null'); 141 | 142 | // Create a Groq instance with a test API key 143 | $groq = new Groq('test-api-key'); 144 | 145 | // Verify the base URL was set to the default value 146 | // This works correctly due to the ?: operator treating "null" as falsy 147 | $this->assertEquals('https://api.groq.com/openai/v1/', $groq->baseUrl); 148 | } 149 | 150 | /** 151 | * Tests constructor behavior when GROQ_API_BASE is set to the string literal "false" 152 | */ 153 | public function testConstructWithFalseStringApiBaseUrl() 154 | { 155 | // Set the API base URL environment variable to the string "false" 156 | putenv('GROQ_API_BASE=false'); 157 | 158 | // Create a Groq instance with a test API key 159 | $groq = new Groq('test-api-key'); 160 | 161 | // Verify the base URL was set to the default value 162 | // This works correctly due to the ?: operator treating "false" as falsy 163 | $this->assertEquals('https://api.groq.com/openai/v1/', $groq->baseUrl); 164 | } 165 | 166 | /** 167 | * Tests that custom base URL is used when set 168 | */ 169 | public function testCustomApiBaseUrl() 170 | { 171 | // Limpa eventuais valores na variável superglobal $_ENV 172 | unset($_ENV['GROQ_API_BASE']); 173 | 174 | // Set the API base URL environment variable to a custom value 175 | putenv('GROQ_API_BASE=https://custom-api.groq.com/v2'); 176 | 177 | // Create a Groq instance with a test API key 178 | $groq = new Groq('test-api-key'); 179 | 180 | // Verify the base URL was set to the custom value with a trailing slash 181 | $this->assertEquals('https://custom-api.groq.com/v2/', $groq->baseUrl); 182 | } 183 | 184 | /** 185 | * Tests that base URL from options overrides environment variable 186 | */ 187 | public function testBaseUrlFromOptions() 188 | { 189 | // Set the API base URL environment variable to a value that should be overridden 190 | putenv('GROQ_API_BASE=https://env-api.groq.com/v1'); 191 | 192 | // Create a Groq instance with the base URL in options 193 | $groq = new Groq('test-api-key', [ 194 | 'baseUrl' => 'https://options-api.groq.com/v2' 195 | ]); 196 | 197 | // Verify the base URL from options was used 198 | $this->assertEquals('https://options-api.groq.com/v2/', $groq->baseUrl); 199 | } 200 | 201 | /** 202 | * Tests that base URL from $_ENV overrides getenv() 203 | */ 204 | public function testBaseUrlFromEnvSuperglobal() 205 | { 206 | // Set the API base URL with getenv() 207 | putenv('GROQ_API_BASE=https://getenv-api.groq.com/v1'); 208 | 209 | // Set the API base URL in $_ENV (which should take precedence) 210 | $_ENV['GROQ_API_BASE'] = 'https://env-superglobal-api.groq.com/v1'; 211 | 212 | // Create a Groq instance with a test API key 213 | $groq = new Groq('test-api-key'); 214 | 215 | // Verify the base URL from $_ENV was used 216 | $this->assertEquals('https://env-superglobal-api.groq.com/v1/', $groq->baseUrl); 217 | 218 | // Clean up 219 | unset($_ENV['GROQ_API_BASE']); 220 | } 221 | } -------------------------------------------------------------------------------- /src/Completions.php: -------------------------------------------------------------------------------- 1 | groq = $groq; 28 | } 29 | 30 | /** 31 | * Creates a completion based on the provided parameters. 32 | * 33 | * @param array $params Parameters for creating the completion. 34 | * @return array|Stream The response from the API or a stream. 35 | * @throws GroqException If an error occurs during the request. 36 | */ 37 | public function create(array $params = []): array|Stream 38 | { 39 | $this->validateParams($params); 40 | $this->prepareParams($params); 41 | 42 | $request = $this->createRequest($params); 43 | 44 | try { 45 | return isset($params['stream']) && $params['stream'] === true 46 | ? $this->streamResponse($request) 47 | : $this->handleResponse($this->groq->makeRequest($request)); 48 | } catch (RequestException $e) { 49 | throw $this->createGroqExceptionFromRequestException($e); 50 | } catch (GuzzleException $e) { 51 | throw new GroqException('Unexpected error while creating the completion: ' . $e->getMessage(), $e->getCode(), 'api_error'); 52 | } catch (\Exception $e) { 53 | throw new GroqException('Unexpected error: ' . $e->getMessage(), $e->getCode(), 'unknown_error'); 54 | } 55 | } 56 | 57 | /** 58 | * Validates the required parameters for the completion request. 59 | * 60 | * @param array $params The parameters to validate. 61 | * @throws GroqException If required parameters are missing. 62 | */ 63 | private function validateParams(array $params): void 64 | { 65 | if (empty($params['model'])) { 66 | throw new GroqException('Missing required parameter: model', 400, 'invalid_request'); 67 | } 68 | if (empty($params['messages'])) { 69 | throw new GroqException('Missing required parameter: messages', 400, 'invalid_request'); 70 | } 71 | } 72 | 73 | /** 74 | * Prepares the parameters for the API request. 75 | * 76 | * @param array $params The parameters to prepare. 77 | */ 78 | private function prepareParams(array &$params): void 79 | { 80 | // Remove unnecessary parameters 81 | if (isset($params['response_format'], $params['tools'])) { 82 | unset($params['response_format']); 83 | } 84 | $this->processImageContent($params['messages']); 85 | } 86 | 87 | /** 88 | * Processes image content within the messages. 89 | * 90 | * @param array $messages The messages containing image content. 91 | */ 92 | private function processImageContent(array &$messages): void 93 | { 94 | foreach ($messages as &$message) { 95 | if (isset($message['content']) && is_array($message['content'])) { 96 | foreach ($message['content'] as &$content) { 97 | // Verifica se $content['image_url'] é um array e tem a chave 'url' 98 | if (isset($content['type']) && $content['type'] === 'image_url' && 99 | isset($content['image_url']) && is_array($content['image_url']) && 100 | isset($content['image_url']['url'])) { 101 | $this->processImageUrl($content['image_url']['url']); 102 | } 103 | } 104 | } 105 | } 106 | } 107 | 108 | /** 109 | * Processes the image URL and converts it to base64 if necessary. 110 | * 111 | * @param string &$url The image URL to process. 112 | */ 113 | private function processImageUrl(string &$url): void 114 | { 115 | if (strpos($url, 'data:image/') === 0) { 116 | return; // Already in base64 format 117 | } 118 | if (file_exists($url)) { 119 | $imageData = base64_encode(file_get_contents($url)); 120 | $url = 'data:image/jpeg;base64,' . $imageData; 121 | } 122 | } 123 | 124 | /** 125 | * Creates a request object for the API. 126 | * 127 | * @param array $params The parameters for the request. 128 | * @return Request The created request object. 129 | */ 130 | private function createRequest(array $params): Request 131 | { 132 | $body = json_encode(array_filter([ 133 | 'model' => $params['model'], 134 | 'messages' => $params['messages'], 135 | 'stream' => $params['stream'] ?? false, 136 | 'stream_options' => $params['stream_options'] ?? null, 137 | 'response_format' => $params['response_format'] ?? null, 138 | 'tools' => $params['tools'] ?? null, 139 | 'tool_choice' => $params['tool_choice'] ?? null, 140 | 'max_completion_tokens' => $params['max_completion_tokens'] ?? null, 141 | 'temperature' => $params['temperature'] ?? null, 142 | 'top_p' => $params['top_p'] ?? null, 143 | 'stop' => $params['stop'] ?? null, 144 | 'seed' => $params['seed'] ?? null, 145 | 'parallel_tool_calls' => $params['parallel_tool_calls'] ?? null, 146 | 'frequency_penalty' => $params['frequency_penalty'] ?? 0, 147 | 'presence_penalty' => $params['presence_penalty'] ?? 0, 148 | 'n' => $params['n'] ?? null, 149 | 'logprobs' => $params['logprobs'] ?? false, 150 | 'logit_bias' => $params['logit_bias'] ?? null, 151 | 'top_logprobs' => $params['top_logprobs'] ?? null, 152 | 'reasoning_format' => $params['reasoning_format'] ?? null, 153 | 'service_tier' => $params['service_tier'] ?? null, 154 | 'user' => $params['user'] ?? null, 155 | ], function ($value) { 156 | return $value !== null; 157 | })); 158 | 159 | return new Request( 160 | 'POST', 161 | $this->groq->baseUrl() . '/chat/completions', 162 | [ 163 | "Content-Type" => "application/json", 164 | "Authorization" => "Bearer " . $this->groq->apiKey(), 165 | ], 166 | $body 167 | ); 168 | } 169 | 170 | /** 171 | * Handles the API response and converts it to an array. 172 | * 173 | * @param ResponseInterface $response The response from the API. 174 | * @return array The decoded response data. 175 | */ 176 | private function handleResponse(ResponseInterface $response): array 177 | { 178 | return json_decode($response->getBody()->getContents(), true); 179 | } 180 | 181 | /** 182 | * Creates a GroqException from a RequestException. 183 | * 184 | * @param RequestException $e The original request exception. 185 | * @return GroqException The created GroqException. 186 | */ 187 | private function createGroqExceptionFromRequestException(RequestException $e): GroqException 188 | { 189 | $responseBody = $e->getResponse() ? (string) $e->getResponse()->getBody() : 'Response body not available'; 190 | $errorData = json_decode($responseBody); 191 | 192 | if (json_last_error() === JSON_ERROR_NONE && isset($errorData->error)) { 193 | return new GroqException( 194 | $errorData->error->message ?? 'Unknown error', 195 | (int) ($errorData->error->code ?? 0), 196 | $errorData->error->type ?? 'api_error' 197 | ); 198 | } 199 | 200 | return new GroqException('Unknown error', 0, 'unknown_error'); 201 | } 202 | 203 | /** 204 | * Streams the response from the API. 205 | * 206 | * @param Request $request The HTTP request to send. 207 | * @return Stream The streamed response. 208 | * @throws GroqException If an error occurs during streaming. 209 | */ 210 | private function streamResponse(Request $request): Stream 211 | { 212 | try { 213 | $client = new Client(); 214 | $response = $client->send($request, ['stream' => true]); 215 | return new Stream($response); 216 | } catch (RequestException $e) { 217 | $responseBody = $e->getResponse() ? (string) $e->getResponse()->getBody() : 'Response body not available'; 218 | throw new GroqException('Failed to stream the response: ' . $responseBody, $e->getCode(), 'stream_error'); 219 | } catch (GuzzleException $e) { 220 | throw new GroqException('Unexpected error while trying to stream the response: ' . $e->getMessage(), $e->getCode(), 'api_error'); 221 | } catch (\Exception $e) { 222 | throw new GroqException('An unexpected error occurred: ' . $e->getMessage(), $e->getCode(), 'generic_error'); 223 | } 224 | } 225 | } -------------------------------------------------------------------------------- /src/GroqException.php: -------------------------------------------------------------------------------- 1 | 'Invalid Request', 18 | 'api_error' => 'API Error', 19 | 'authentication_error' => 'Authentication Error', 20 | 'rate_limit_error' => 'Rate Limit Error', 21 | 'chat_completion_error' => 'Chat Completion Error', 22 | 'transcription_error' => 'Transcription Error', 23 | 'not_found_error' => 'Not Found Error', 24 | 'unprocessable_entity' => 'Unprocessable Entity', 25 | 'timeout_error' => 'Timeout Error', 26 | 'service_unavailable' => 'Service Unavailable', 27 | 'invalid_request_error' => 'Invalid Request Error', 28 | 'invalid_api_key' => 'Invalid API Key', 29 | 'network_error' => 'Network Error', 30 | 'failed_generation' => 'Failed Generation', // Added new error type 31 | ]; 32 | 33 | // Map error types to HTTP status codes 34 | const HTTP_STATUS_CODES = [ 35 | self::ERROR_TYPES['invalid_request'] => 400, 36 | self::ERROR_TYPES['api_error'] => 500, 37 | self::ERROR_TYPES['authentication_error'] => 401, 38 | self::ERROR_TYPES['rate_limit_error'] => 429, 39 | self::ERROR_TYPES['not_found_error'] => 404, 40 | self::ERROR_TYPES['unprocessable_entity'] => 422, 41 | self::ERROR_TYPES['timeout_error'] => 504, 42 | self::ERROR_TYPES['service_unavailable'] => 503, 43 | self::ERROR_TYPES['invalid_request_error'] => 400, 44 | self::ERROR_TYPES['invalid_api_key'] => 0, // Mapped to status code 0 45 | self::ERROR_TYPES['network_error'] => 503, 46 | self::ERROR_TYPES['failed_generation'] => 400, // Mapped to status code 400 47 | ]; 48 | 49 | protected string $type; // Type of the error 50 | protected array $headers; // Response headers 51 | protected ?stdClass $responseBody; // Response body 52 | protected ?string $failedGeneration; // Field for failed_generation 53 | 54 | /** 55 | * Constructor for GroqException. 56 | * @param string $message Error message 57 | * @param int $code Error code 58 | * @param string $type Type of the error 59 | * @param string[] $headers Response headers 60 | * @param string|null $failedGeneration Field for failed_generation 61 | * @param stdClass|null $responseBody Response body 62 | */ 63 | public function __construct(string $message, int $code, string $type, array $headers = [], ?stdClass $responseBody = null, ?string $failedGeneration = null) 64 | { 65 | parent::__construct($message, $code); 66 | $this->type = $type; 67 | $this->headers = $headers; 68 | $this->responseBody = $responseBody; 69 | $this->failedGeneration = $failedGeneration; // Assigning the failed_generation field 70 | } 71 | 72 | public function getType(): string 73 | { 74 | return $this->type; 75 | } 76 | 77 | /** 78 | * Alias for getType() for backward compatibility 79 | */ 80 | public function getErrorType(): string 81 | { 82 | return $this->getType(); 83 | } 84 | 85 | public function getHeaders(): array 86 | { 87 | return $this->headers; 88 | } 89 | 90 | public function getResponseBody(): ?stdClass 91 | { 92 | return $this->responseBody; 93 | } 94 | 95 | public function getFailedGeneration(): ?string 96 | { 97 | return $this->failedGeneration ?? null; // Method to return the failed_generation field 98 | } 99 | 100 | /** 101 | * Converts the exception details to JSON format. 102 | * @return string 103 | */ 104 | public function toJson(): string 105 | { 106 | $errorDetails = [ 107 | 'message' => $this->getMessage(), 108 | 'type' => $this->getType(), 109 | 'code' => $this->getCode(), 110 | ]; 111 | 112 | if ($this->getFailedGeneration() !== null) { 113 | $errorDetails['failed_generation'] = $this->getFailedGeneration(); // Using the new getFailedGeneration method 114 | } 115 | 116 | return json_encode([ 117 | 'error' => $errorDetails, 118 | 'headers' => $this->getHeaders(), 119 | 'responseBody' => $this->responseBody ? json_decode(json_encode($this->responseBody), true) : null, 120 | ]); 121 | } 122 | 123 | public function toArray(): array 124 | { 125 | return json_decode($this->toJson(), true); 126 | } 127 | 128 | public function getError(): array 129 | { 130 | $errorDetails = [ 131 | 'message' => $this->getMessage(), 132 | 'type' => $this->getType(), 133 | 'code' => $this->getCode(), 134 | ]; 135 | 136 | if ($this->getFailedGeneration() !== null) { 137 | $errorDetails['failed_generation'] = $this->getFailedGeneration(); // Using the new getFailedGeneration method 138 | } 139 | 140 | return $errorDetails; 141 | } 142 | 143 | /** 144 | * Creates a GroqException from a response. 145 | * @param ResponseInterface $response 146 | * @return self 147 | * @throws self 148 | */ 149 | public static function createFromResponse(ResponseInterface $response): self 150 | { 151 | $body = json_decode($response->getBody()->getContents(), true); 152 | if (json_last_error() !== JSON_ERROR_NONE) { 153 | throw new self('Invalid JSON response', 500, self::ERROR_TYPES['api_error'], $response->getHeaders()); 154 | } 155 | 156 | $errorMessage = $body['error']['message'] ?? 'Unknown error'; 157 | $errorType = $body['error']['type'] ?? self::ERROR_TYPES['api_error']; 158 | $errorCode = $response->getStatusCode(); 159 | $failedGeneration = $body['error']['failed_generation'] ?? null; // Captura o campo failed_generation, se disponível 160 | 161 | // Handle specific error types using the factory pattern 162 | return match ($errorType) { 163 | 'invalid_request_error' => self::invalidRequest($errorMessage), 164 | 'invalid_api_key' => self::authenticationError('Invalid API key provided.'), 165 | self::ERROR_TYPES['invalid_request'] => self::invalidRequest($errorMessage), 166 | self::ERROR_TYPES['authentication_error'] => self::authenticationError($errorMessage), 167 | self::ERROR_TYPES['not_found_error'] => self::notFoundError($errorMessage), 168 | self::ERROR_TYPES['unprocessable_entity'] => self::unprocessableEntity($errorMessage), 169 | self::ERROR_TYPES['rate_limit_error'] => self::rateLimitError($errorMessage), 170 | self::ERROR_TYPES['timeout_error'] => new self($errorMessage, 504, $errorType, $response->getHeaders()), 171 | self::ERROR_TYPES['service_unavailable'] => new self($errorMessage, 503, $errorType, $response->getHeaders()), 172 | self::ERROR_TYPES['network_error'] => new self('A network error occurred', 503, self::ERROR_TYPES['network_error'], $response->getHeaders()), 173 | self::ERROR_TYPES['failed_generation'] => new self($errorMessage, 400, self::ERROR_TYPES['failed_generation'], $response->getHeaders(), $failedGeneration), // Handling for failed_generation 174 | default => new self($errorMessage, $errorCode, $errorType, $response->getHeaders(), json_decode(json_encode($body))), 175 | }; 176 | } 177 | 178 | public static function invalidRequest(string $message): self 179 | { 180 | return new self($message, self::HTTP_STATUS_CODES['invalid_request'], self::ERROR_TYPES['invalid_request']); 181 | } 182 | 183 | public static function authenticationError(string $message): self 184 | { 185 | return new self($message, self::HTTP_STATUS_CODES['authentication_error'], self::ERROR_TYPES['authentication_error']); 186 | } 187 | 188 | public static function notFoundError(string $message): self 189 | { 190 | return new self($message, self::HTTP_STATUS_CODES['not_found_error'], self::ERROR_TYPES['not_found_error']); 191 | } 192 | 193 | public static function unprocessableEntity(string $message): self 194 | { 195 | return new self($message, self::HTTP_STATUS_CODES['unprocessable_entity'], self::ERROR_TYPES['unprocessable_entity']); 196 | } 197 | 198 | public static function rateLimitError(string $message): self 199 | { 200 | return new self($message, self::HTTP_STATUS_CODES['rate_limit_error'], self::ERROR_TYPES['rate_limit_error']); 201 | } 202 | 203 | public static function internalServerError(string $message): self 204 | { 205 | return new self($message, self::HTTP_STATUS_CODES['api_error'], self::ERROR_TYPES['api_error']); 206 | } 207 | 208 | public static function apiKeyNotSet(): self 209 | { 210 | return new self( 211 | 'The API key is not set. Please provide an API key when initializing the Groq client or set the environment variable GROQ_API_KEY.', 212 | 400, 213 | self::ERROR_TYPES['authentication_error'] 214 | ); 215 | } 216 | } -------------------------------------------------------------------------------- /src/BatchManager.php: -------------------------------------------------------------------------------- 1 | [ 31 | 'temperature' => 0.7, 32 | 'max_tokens' => 1000, 33 | 'top_p' => 1.0, 34 | 'frequency_penalty' => 0, 35 | 'presence_penalty' => 0 36 | ], 37 | '/v1/audio/transcriptions' => [ 38 | 'language' => 'en', 39 | 'prompt' => '', 40 | 'response_format' => 'json', 41 | 'temperature' => 0.7 42 | ], 43 | '/v1/audio/translations' => [ 44 | 'prompt' => '', 45 | 'response_format' => 'json', 46 | 'temperature' => 0.7 47 | ] 48 | ]; 49 | 50 | public function __construct(Groq $groq) 51 | { 52 | $this->groq = $groq; 53 | } 54 | 55 | /** 56 | * Creates a new batch for asynchronous processing 57 | * 58 | * @param array $params Batch parameters 59 | * @throws GroqException 60 | */ 61 | public function create(array $params): Batch 62 | { 63 | $this->validateCreateParams($params); 64 | 65 | $payload = [ 66 | 'input_file_id' => $params['input_file_id'], 67 | 'endpoint' => $params['endpoint'], 68 | 'completion_window' => $params['completion_window'] 69 | ]; 70 | 71 | if (isset($params['metadata'])) { 72 | $this->validateMetadata($params['metadata']); 73 | $payload['metadata'] = $params['metadata']; 74 | } 75 | 76 | try { 77 | $request = new Request('POST', 'batches', [], json_encode($payload)); 78 | $response = $this->groq->makeRequest($request); 79 | 80 | $data = json_decode($response->getBody()->getContents(), true); 81 | return new Batch($data); 82 | } catch (\GuzzleHttp\Exception\ClientException $e) { 83 | $this->handleClientException($e); 84 | } 85 | } 86 | 87 | /** 88 | * Retrieves a specific batch 89 | * 90 | * @param string $batchId Batch ID 91 | * @throws GroqException 92 | */ 93 | public function retrieve(string $batchId): Batch 94 | { 95 | try { 96 | $request = new Request('GET', "batches/{$batchId}"); 97 | $response = $this->groq->makeRequest($request); 98 | $data = json_decode($response->getBody()->getContents(), true); 99 | return new Batch($data); 100 | } catch (\GuzzleHttp\Exception\ClientException $e) { 101 | $this->handleClientException($e); 102 | } 103 | } 104 | 105 | /** 106 | * Lists existing batches with optional filtering and pagination 107 | * 108 | * @param array $params Pagination and sorting parameters 109 | * @throws GroqException 110 | */ 111 | public function list(array $params = []): array 112 | { 113 | $query = array_filter([ 114 | 'limit' => $params['limit'] ?? 20, 115 | 'after' => $params['after'] ?? null, 116 | 'order' => $params['order'] ?? 'desc', 117 | 'status' => $params['status'] ?? null, 118 | 'created_after' => $params['created_after'] ?? null, 119 | 'created_before' => $params['created_before'] ?? null 120 | ]); 121 | 122 | try { 123 | $request = new Request('GET', 'batches', [ 124 | 'query' => $query 125 | ]); 126 | $response = $this->groq->makeRequest($request); 127 | 128 | $data = json_decode($response->getBody()->getContents(), true); 129 | 130 | $data['data'] = array_map(function ($item) { 131 | return new Batch($item); 132 | }, $data['data']); 133 | 134 | return $data; 135 | } catch (\GuzzleHttp\Exception\ClientException $e) { 136 | $this->handleClientException($e); 137 | } 138 | } 139 | 140 | /** 141 | * Cancels a running batch 142 | * 143 | * @param string $batchId Batch ID 144 | * @throws GroqException 145 | */ 146 | public function cancel(string $batchId): Batch 147 | { 148 | try { 149 | $request = new Request('POST', "batches/{$batchId}/cancel"); 150 | $response = $this->groq->makeRequest($request); 151 | $data = json_decode($response->getBody()->getContents(), true); 152 | return new Batch($data); 153 | } catch (\GuzzleHttp\Exception\ClientException $e) { 154 | $this->handleClientException($e); 155 | } 156 | } 157 | 158 | /** 159 | * Validates required parameters for batch creation 160 | * 161 | * @param array $params Parameters to validate 162 | * @throws GroqException 163 | */ 164 | private function validateCreateParams(array $params): void 165 | { 166 | foreach ($this->requiredCreateParams as $param) { 167 | if (!isset($params[$param])) { 168 | throw new GroqException( 169 | "Missing required parameter: {$param}", 170 | 400, 171 | 'invalid_request' 172 | ); 173 | } 174 | } 175 | 176 | if (!in_array($params['endpoint'], $this->validEndpoints)) { 177 | throw new GroqException( 178 | 'Invalid endpoint. Only /v1/chat/completions is supported', 179 | 400, 180 | 'invalid_request' 181 | ); 182 | } 183 | 184 | if (!in_array($params['completion_window'], $this->validCompletionWindows)) { 185 | throw new GroqException( 186 | 'Invalid completion_window. Supported values are: ' . implode(', ', $this->validCompletionWindows), 187 | 400, 188 | 'invalid_request' 189 | ); 190 | } 191 | 192 | if (isset($params['config'])) { 193 | $this->validateConfig($params['config'], $params['endpoint']); 194 | } 195 | } 196 | 197 | /** 198 | * Validates batch configuration parameters 199 | * 200 | * @param array $config Configuration to validate 201 | * @param string $endpoint Target endpoint 202 | * @throws GroqException 203 | */ 204 | private function validateConfig(array $config, string $endpoint): void 205 | { 206 | $defaultConfig = $this->defaultConfig[$endpoint] ?? []; 207 | $allowedParams = array_keys($defaultConfig); 208 | 209 | foreach ($config as $param => $value) { 210 | if (!in_array($param, $allowedParams)) { 211 | throw new GroqException( 212 | "Invalid configuration parameter: {$param} for endpoint {$endpoint}", 213 | 400, 214 | 'invalid_request' 215 | ); 216 | } 217 | 218 | // Validate parameter values 219 | switch ($param) { 220 | case 'temperature': 221 | case 'top_p': 222 | if (!is_numeric($value) || $value < 0 || $value > 1) { 223 | throw new GroqException( 224 | "{$param} must be a number between 0 and 1", 225 | 400, 226 | 'invalid_request' 227 | ); 228 | } 229 | break; 230 | case 'max_tokens': 231 | if (!is_int($value) || $value < 1) { 232 | throw new GroqException( 233 | "max_tokens must be a positive integer", 234 | 400, 235 | 'invalid_request' 236 | ); 237 | } 238 | break; 239 | case 'frequency_penalty': 240 | case 'presence_penalty': 241 | if (!is_numeric($value) || $value < -2 || $value > 2) { 242 | throw new GroqException( 243 | "{$param} must be a number between -2 and 2", 244 | 400, 245 | 'invalid_request' 246 | ); 247 | } 248 | break; 249 | } 250 | } 251 | } 252 | 253 | /** 254 | * Validates metadata parameter 255 | * 256 | * @param mixed $metadata The metadata to validate 257 | * @throws GroqException 258 | */ 259 | private function validateMetadata(mixed $metadata): void 260 | { 261 | if (!is_null($metadata) && !is_array($metadata)) { 262 | throw new GroqException( 263 | 'Metadata must be an object or null', 264 | 400, 265 | 'invalid_request' 266 | ); 267 | } 268 | 269 | // Validate metadata size 270 | $jsonSize = strlen(json_encode($metadata)); 271 | if ($jsonSize > 8192) { // 8KB limit 272 | throw new GroqException( 273 | 'Metadata size exceeds maximum limit of 8KB', 274 | 400, 275 | 'invalid_request' 276 | ); 277 | } 278 | } 279 | 280 | /** 281 | * Handles client exceptions and throws appropriate GroqException 282 | * 283 | * @param \GuzzleHttp\Exception\ClientException $e 284 | * @throws GroqException 285 | */ 286 | private function handleClientException(\GuzzleHttp\Exception\ClientException $e): never 287 | { 288 | $response = $e->getResponse(); 289 | $errorBody = json_decode($response->getBody()->getContents(), true); 290 | 291 | if ($response->getStatusCode() === 403 && 292 | isset($errorBody['error']['type']) && 293 | $errorBody['error']['type'] === 'permissions_error') { 294 | throw new GroqException( 295 | 'Batch processing is not available in your current Groq plan. Please upgrade your plan to use this feature.', 296 | 403, 297 | 'permissions_error' 298 | ); 299 | } 300 | 301 | if ($response->getStatusCode() === 429) { 302 | throw new GroqException( 303 | 'Rate limit exceeded. Please try again later.', 304 | 429, 305 | 'rate_limit_error' 306 | ); 307 | } 308 | 309 | throw new GroqException( 310 | $errorBody['error']['message'] ?? 'Unknown error occurred', 311 | $response->getStatusCode(), 312 | $errorBody['error']['type'] ?? 'api_error' 313 | ); 314 | } 315 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [v1.3.0] - 2025-03-23 11 | 12 | ### Breaking Changes in Files/Batches Module 13 | - Changed JSONL file format requirements for batch processing 14 | - Restricted file extensions to `.jsonl` only (removed support for `.json`, `.txt`, `.ndjson`) 15 | - Added stricter validation for file contents and structure 16 | 17 | ### Changed 18 | - Updated Files API validation to properly handle 'batch' purpose 19 | - Added support for additional MIME types: application/x-jsonlines, application/jsonl, application/x-ndjson, application/x-ndjason 20 | - Updated JSONL file format documentation and examples 21 | - Updated tests to use new JSONL format and purpose 22 | - Improved file validation order in FileManager.php 23 | - Fixed JSONL file validation in FileManager to properly check content format 24 | - Enhanced error messages for file type validation 25 | - Improved test coverage for file upload functionality 26 | 27 | ### Added 28 | - New example file: examples/batch-processing.php 29 | - Additional test cases for JSONL file validation 30 | - Improved error messages for file validation 31 | - Test results: 39 tests, 218 assertions (all passing) 32 | - Added more comprehensive validation for JSONL file contents 33 | - Support for audio transcription and translation in batch processing 34 | - Support for chat completions in batch processing 35 | 36 | ### Fixed 37 | - Issue with file type validation in FileManager::validateFile method 38 | - Improved MIME type detection for JSONL files 39 | - Fixed test case for file upload to use correct JSONL format 40 | - Corrected JSONL test file structure to match API requirements 41 | 42 | ### Migration Guide for Files/Batches Module 43 | If you are using the Files/Batches module, you will need to: 44 | 1. Update your JSONL files to include required fields: 45 | - `custom_id`: Your unique identifier for tracking 46 | - `method`: Must be "POST" 47 | - `url`: One of: /v1/chat/completions, /v1/audio/transcriptions, or /v1/audio/translations 48 | - `body`: Request parameters matching the endpoint format 49 | 2. Ensure all files use `.jsonl` extension 50 | 3. Update file content to match new validation requirements 51 | 4. Review the updated documentation for complete format specifications 52 | 53 | ## v1.0.0 54 | * [23/03/2025](https://github.com/lucianotonet/groq-php/commits/77391f0c32a9a7602906a2dc1dc31d1313afc858) test: Add EnvironmentVariablesTest for GROQ_API_KEY and GROQ_API_BASE 55 | * [23/03/2025](https://github.com/lucianotonet/groq-php/commits/2ac16eb1b048f11f97642dbea4b0cc900b51715f) fix: prevent TypeError from unset GROQ_API_BASE environment variable 56 | * [23/03/2025](https://github.com/lucianotonet/groq-php/commits/b1d9dd28ff29d5a3feaf6ca6e4238ceac578271d) refactor: Update depracated models on examples and tests to use Llama 3 57 | * [27/02/2025](https://github.com/lucianotonet/groq-php/commits/7335963893c722150a80b25f0a9de5dfffd586a2) docs: Update README 58 | * [23/02/2025](https://github.com/lucianotonet/groq-php/commits/a4b1cdaa984feb9f2c985541f46a51dd06a3af6e) fix: Fix general warning - 'The version field is present, it is recommended to leave it out if the package is published on Packagist.' 59 | * [23/02/2025](https://github.com/lucianotonet/groq-php/commits/26d11f41ae7084d9df155d80c9ab752afa2e9369) Add GitHub Actions workflow for running tests 60 | 61 | ## v0.1.2 62 | * [23/02/2025](https://github.com/lucianotonet/groq-php/commits/511bbb0b763085cec0c5b129e9a512cd642f449d) chore: Bump version to v0.1.2 63 | * [23/02/2025](https://github.com/lucianotonet/groq-php/commits/4b29c9c4446c13ff082678ea24415644c171800b) Merge pull request #11 from lucianotonet/feat/files-and-batches 64 | * [23/02/2025](https://github.com/lucianotonet/groq-php/commits/3b3cf6492c09aacb03f906665309b8ab2570e943) Apply suggestions from code review 65 | * [23/02/2025](https://github.com/lucianotonet/groq-php/commits/44d0211f257b990cad15e19d9c6fff20608519e6) Apply suggestions from code review 66 | * [23/02/2025](https://github.com/lucianotonet/groq-php/commits/7fafc8187286c7f8c23ae81a1aea5ca1aba6e363) Add comprehensive test suite for Groq PHP library 67 | * [23/02/2025](https://github.com/lucianotonet/groq-php/commits/3244576e28f1811bca6ca2fb046fa5f95936fe01) Adds reasoning format options to example 68 | * [23/02/2025](https://github.com/lucianotonet/groq-php/commits/21166c741ffe8d649f64048bbfe3d56e08b155df) Adds file and batch processing capabilities 69 | 70 | ## v0.1.1 71 | * [20/02/2025](https://github.com/lucianotonet/groq-php/commits/048cb936e0eeabc6c350dac03fb02e9a96ff291a) chore: Bump version to v0.1.1 72 | * [20/02/2025](https://github.com/lucianotonet/groq-php/commits/fa075afb5fdab7b57f81170f6dfb25b7247453cf) Improves API key retrieval 73 | 74 | ## v0.1.0 75 | * [18/02/2025](https://github.com/lucianotonet/groq-php/commits/380e3722ba741dbef5647aaac718dac7c399327b) chore: bump version to 0.1.0 76 | * [18/02/2025](https://github.com/lucianotonet/groq-php/commits/1ac4a35f87ce2882f7ab4daa18172634c81a13f4) Merge pull request #10 from lucianotonet/develop 77 | * [18/02/2025](https://github.com/lucianotonet/groq-php/commits/fcebfec4f6034f12db02abd32debdaca3c52972b) Update temperature input range in reasoning example 78 | * [18/02/2025](https://github.com/lucianotonet/groq-php/commits/f733627e6f32279ceecf3b72ad243ea0e4d99d0c) Update README with comprehensive configuration options and new reasoning/tool calling documentation 79 | * [18/02/2025](https://github.com/lucianotonet/groq-php/commits/93e46d82c8883ef508899ab264d99dc978b818f3) Renames max_tokens to max_completion_tokens 80 | * [18/02/2025](https://github.com/lucianotonet/groq-php/commits/bb700c1acb87d560ea71ea0316c006ca57b44e93) Adds reasoning feature 81 | * [18/02/2025](https://github.com/lucianotonet/groq-php/commits/c999f8f734f3db5b680cf225abd970bdc6228207) Merge pull request #9 from lguichard/main 82 | * [18/02/2025](https://github.com/lucianotonet/groq-php/commits/e944b6551c245bbb18ad7883fee228c68649ba34) Fixes 83 | * [14/02/2025](https://github.com/lucianotonet/groq-php/commits/f2330a23c6ce5cc01530c6fb3e4950ab061698f3) Fix - Update completions parameters 84 | 85 | ## v0.0.10 86 | * [29/10/2024](https://github.com/lucianotonet/groq-php/commits/c36c5a471d4caf0cec642887371203c6339d9738) Update version to 0.0.10 in composer.json, add API key options in README. 87 | * [29/10/2024](https://github.com/lucianotonet/groq-php/commits/87fae1976b1827ebafcc69986cd729b9ce63cee1) Merge pull request #8 from lucianotonet/develop 88 | * [29/10/2024](https://github.com/lucianotonet/groq-php/commits/db48f4475badca9e51aef9f9dacf5892913188ae) Merge branch 'hotfix/test-vision-with-url' into main 89 | * [29/10/2024](https://github.com/lucianotonet/groq-php/commits/5f04b293dea29381b90b099a488b1b50ed50b36e) Merge branch 'hotfix/test-vision-with-url' into develop 90 | * [29/10/2024](https://github.com/lucianotonet/groq-php/commits/632b93cf05dc7328f366ba5a1a7a6079cb68ee40) Translate messages 91 | * [29/10/2024](https://github.com/lucianotonet/groq-php/commits/49ba8cfddeb40d76e5de78ea4dff36ff8a93201e) hotfix: Update image URL in VisionTest 92 | * [29/10/2024](https://github.com/lucianotonet/groq-php/commits/e9796cbf8a94cfce642a1b42cb4df6c7e6a658d9) Merge branch 'feature/apikey-on-runtime' into develop 93 | * [29/10/2024](https://github.com/lucianotonet/groq-php/commits/6c17c9146b4097fddda5eb2d81984e0002cbe054) feat: Update Groq class properties and setOptions method. 94 | * [19/09/2024](https://github.com/lucianotonet/groq-php/commits/96062eb2ac9fb40ac3088cd3220825f532778367) feat: Ensure API key is explicitly set during initialization 95 | * [19/09/2024](https://github.com/lucianotonet/groq-php/commits/9ce4c02e4328023e9038c134a3bfeb5ed82a3211) feat(Vision): add default model and enhance analyze method 96 | * [19/09/2024](https://github.com/lucianotonet/groq-php/commits/a50320ea121d4cb9862a53a4d8490e30509e5c5a) feat: add parallel tool calls and additional parameters for flexibility 97 | * [08/09/2024](https://github.com/lucianotonet/groq-php/commits/2f7094aa675f13841046e3bbea1f11a5f4856a6c) Update image URL in README for Groq PHP project 98 | 99 | ## v0.0.9 100 | * [06/09/2024](https://github.com/lucianotonet/groq-php/commits/d8263d8b0c831d489c69e67554ddfaf98b3e4d8c) Merge remote-tracking branch 'origin/main' 101 | * [06/09/2024](https://github.com/lucianotonet/groq-php/commits/a6939768fd4048de6f264e5d2d86bf2ae57c5f25) Fix: Re-added missing parameters on request 102 | * [06/09/2024](https://github.com/lucianotonet/groq-php/commits/2e790ae361c6499f90cde6a80183bea0c7f973a4) Update README.md 103 | * [05/09/2024](https://github.com/lucianotonet/groq-php/commits/ed15619457b09cd1712c95738889f9e67bf047f6) Update package URL in README for consistency and clarity. 104 | * [05/09/2024](https://github.com/lucianotonet/groq-php/commits/94eec8615b2dfe81072078433d1582fac8abe0cf) Add image to README for Groq PHP project 105 | * [04/09/2024](https://github.com/lucianotonet/groq-php/commits/de8186902da024a163dee5b8aa9163f7eedf867f) Update README.md 106 | * [04/09/2024](https://github.com/lucianotonet/groq-php/commits/0a066d80b719b54e054e5d79052faec8b380ab22) Merge tag 'v0.0.9' and add Vision functionality for image analysis. - Merge tag 'v0.0.9' - Add Vision functionality for image analysis and examples 107 | * [04/09/2024](https://github.com/lucianotonet/groq-php/commits/899f755f5ddc81007f074f3f37229e54f08fa34b) Merge tag 'v0.0.9' 108 | * [04/09/2024](https://github.com/lucianotonet/groq-php/commits/af7a725e45c8b26ec515e7ec0d7d64e83e82bd4f) Merge pull request #6 from tgeorgel/main 109 | * [04/09/2024](https://github.com/lucianotonet/groq-php/commits/948455b251b56e51ae1e1ef172471064771b36ea) Add Vision functionality for image analysis and examples: 110 | * [03/09/2024](https://github.com/lucianotonet/groq-php/commits/cca3e7e073cbe38a5b4cf121fd9ad83761f9eb45) fix: Prevent the language to be forced on the transcript endpoint 111 | * [02/08/2024](https://github.com/lucianotonet/groq-php/commits/6f3ed06528ef3f2da49153ac32bef97568c3fac2) Update Groq PHP package version to 0.0.8 112 | 113 | ## v0.0.8 114 | * [02/08/2024](https://github.com/lucianotonet/groq-php/commits/5b1ee7018ac4f2f5a521120bfcfbb0f6a72dc011) Update Groq PHP package version to 0.0.8 115 | * [02/08/2024](https://github.com/lucianotonet/groq-php/commits/bc86d82a4c64be6d7118101b49fa3e5fe5779870) feat: Update examples 116 | * [02/08/2024](https://github.com/lucianotonet/groq-php/commits/2a118e3cdbe1ef8305a942c1ca7be2441fb48faf) feat: Enhance error handling and improve API response management 117 | * [30/07/2024](https://github.com/lucianotonet/groq-php/commits/220d8e9b84c95a7dc2ceec9afcc65600df86d292) chore: Update badges in README.md 118 | 119 | ## v0.0.7 120 | * [23/07/2024](https://github.com/lucianotonet/groq-php/commits/24ef05e3b3e0cee44468a3459ddecd6c998b09f9) chore: Bump version to 0.0.7 121 | * [23/07/2024](https://github.com/lucianotonet/groq-php/commits/e94092448e8e18f9f88066d617d360f5ab173e46) Test: Validate API key and integrate model listing 122 | * [23/07/2024](https://github.com/lucianotonet/groq-php/commits/0692c0be5be92570b953918a7704dab5154866d1) Refactor: Enhance Error Handling and Improve Code Quality 123 | * [23/07/2024](https://github.com/lucianotonet/groq-php/commits/0a063da770ba4764625de376338db1689c58316b) docs: Update Changelog 124 | * [23/07/2024](https://github.com/lucianotonet/groq-php/commits/0f4c5f021f701e3c9ffaf00283467f98ccf5872d) feat: Add list models feature 125 | * [23/07/2024](https://github.com/lucianotonet/groq-php/commits/dca5448832b32fd065b73f1591606aa44374db2d) chore: Update to use $_ENV instead of getenv() for improved reliability 126 | * [23/07/2024](https://github.com/lucianotonet/groq-php/commits/3b585af351edf39897389ed4491b53d5e22ca092) chore: Change GROQ_API_BASE_URL env var to GROQ_API_BASE 127 | 128 | ## v0.0.6 129 | * [19/07/2024](https://github.com/lucianotonet/groq-php/commits/7c5d736e806bbf8ed6315ae6909670174de9d091) feat: Add speech-to-text transcription and translation features 130 | 131 | ## v0.0.5 132 | * [04/07/2024](https://github.com/lucianotonet/groq-php/commits/043778f8ec7341789f21e821690555d6dbcd7055) Merge pull request #4 from lucianotonet/versionfix 133 | * [04/07/2024](https://github.com/lucianotonet/groq-php/commits/279d9e8197327f8519aa6ca726d53d81861bff52) Fix version mismatch 134 | * [04/07/2024](https://github.com/lucianotonet/groq-php/commits/8b81ee2fed7644fb756e4baf61507d5a62bf7a04) Update composer.json 135 | * [20/06/2024](https://github.com/lucianotonet/groq-php/commits/eec092f5161f2b59aa59acf9743eaec7c5e42d43) Merge pull request #3 from JosephGabito/main 136 | * [21/06/2024](https://github.com/lucianotonet/groq-php/commits/d072b0906c86686fa75ac458e8af074caf6c17d9) Update README.md 137 | * [13/06/2024](https://github.com/lucianotonet/groq-php/commits/2c7e1fbd8d84db7e71ba0fd21578edd69de4c34d) Docs: Mark Models endpoint as completed (1c9a24d) 138 | * [13/06/2024](https://github.com/lucianotonet/groq-php/commits/2b7707e8ade7ad18713870dda85b8b30dcef7543) Remove unrelated scripts 139 | * [13/06/2024](https://github.com/lucianotonet/groq-php/commits/29d5798121c4eeb5808a575aa3459fe8bf0ebbe5) Docs: Remove max retries from readme 140 | * [13/06/2024](https://github.com/lucianotonet/groq-php/commits/29e854a011cd3bbcc6c8665ba7de1a977c109cf6) Refatoração: Melhora a legibilidade do código em exemplos 141 | * [13/06/2024](https://github.com/lucianotonet/groq-php/commits/8aa9e29a6098f59b6d8449c970f024aab55dd64b) feat: Remove Llama2 references as it is not available anymore 142 | * [18/05/2024](https://github.com/lucianotonet/groq-php/commits/8e0f9f3ad7d48dbcfd2f6eacb3a60137c954af59) Refactored error handling and added documentation examples for Groq PHP library. 143 | * [17/05/2024](https://github.com/lucianotonet/groq-php/commits/941a1d38a70b800be9442a1f59661476eef0cf76) Refactor script filenames for brevity and clarity. Rename CHANGELOG_GENERATOR.sh to CHANGELOG_GEN.sh and CHANGELOG_GENERATOR_AI.sh to CHANGELOG_GEN_AI.sh for concise and consistent naming conventions. 144 | * [17/05/2024](https://github.com/lucianotonet/groq-php/commits/3d833d48267d4940ad34bc00b6a7295116275830) Add Groq API key and base URL to .env.example file 145 | * [16/05/2024](https://github.com/lucianotonet/groq-php/commits/1c9a24dc8f87b1f981f3bc3a11894104b0b41771) Add Models class and list method for fetching models from API 146 | 147 | ## v0.0.4 148 | * [20/04/2024](https://github.com/lucianotonet/groq-php/commits/3bfb93700e93f51f5d0f393d3f4011015fb63d6a) Add examples folder + some improvements: - Add support for response streaming and JSON mode. - Improve error handling with more descriptive errors. - Add usage examples. - Fix bugs related to streaming and JSON mode. - Update minimum PHP version. - Add unit tests. 149 | * [13/04/2024](https://github.com/lucianotonet/groq-php/commits/aea491db97605365b1128a5c9a6419d5b46f403d) Fix: Cannot assign bool to property LucianoTonet\GroqPHP\Groq::$baseUrl of type string 150 | * [30/03/2024](https://github.com/lucianotonet/groq-php/commits/d90f0c3c1780dc2ee1fd390e3151097a165dc762) Update README 151 | 152 | ## v0.0.3 153 | * [30/03/2024](https://github.com/lucianotonet/groq-php/commits/98a08261a84c9131b1ad4ce0f75700f5b6939a4f) Add stream funcionality + some refactor 154 | * [30/03/2024](https://github.com/lucianotonet/groq-php/commits/6d42703ee21fd4710ff8514be2ddcced6a27c380) Add stream funcionality + some refactor 155 | * [28/03/2024](https://github.com/lucianotonet/groq-php/commits/e7b608b3a596126785eccc1a902b52a558315548) Add Changelog 156 | 157 | ## v0.0.2 158 | * [28/03/2024](https://github.com/lucianotonet/groq-php/commits/3c97462ae25b225ced5dd22fc2b92474105279e2) Add some examples and improvements. 159 | * [28/03/2024](https://github.com/lucianotonet/groq-php/commits/b3eaaf6605c7e0bc4572681fc99f3d686ea40ae6) Add license file 160 | 161 | ## v0.0.1 162 | * [22/03/2024](https://github.com/lucianotonet/groq-php/commits/8fea7dff55eca796dfbc6e1c340ae82c7af0a60f) First commit -------------------------------------------------------------------------------- /src/FileManager.php: -------------------------------------------------------------------------------- 1 | groq = $groq; 41 | if ($cacheDir !== null) { 42 | $this->setCacheDir($cacheDir); 43 | } 44 | } 45 | 46 | /** 47 | * Sets the cache directory for downloaded files 48 | */ 49 | public function setCacheDir(string $dir): void 50 | { 51 | if (!is_dir($dir)) { 52 | if (!mkdir($dir, 0755, true)) { 53 | throw new GroqException( 54 | "Failed to create cache directory: {$dir}", 55 | 500, 56 | 'internal_error' 57 | ); 58 | } 59 | } 60 | 61 | if (!is_writable($dir)) { 62 | throw new GroqException( 63 | "Cache directory is not writable: {$dir}", 64 | 500, 65 | 'internal_error' 66 | ); 67 | } 68 | 69 | $this->cacheDir = rtrim($dir, '/'); 70 | } 71 | 72 | /** 73 | * Uploads a file for batch processing 74 | * 75 | * @param string $filePath Path to the file 76 | * @param string $purpose Purpose of the file (only 'batch' is supported) 77 | * @throws GroqException 78 | */ 79 | public function upload(string $filePath, string $purpose): File 80 | { 81 | if ($purpose !== 'batch') { 82 | throw new GroqException( 83 | 'Invalid purpose. Only "batch" is supported', 84 | 400, 85 | 'invalid_request' 86 | ); 87 | } 88 | 89 | $this->validateFileType($filePath); 90 | 91 | $this->validateFileContent($filePath); 92 | 93 | try { 94 | $response = $this->groq->makeRequest(new Request( 95 | 'POST', 96 | 'files', 97 | [], 98 | new \GuzzleHttp\Psr7\MultipartStream([ 99 | [ 100 | 'name' => 'purpose', 101 | 'contents' => $purpose 102 | ], 103 | [ 104 | 'name' => 'file', 105 | 'contents' => fopen($filePath, 'r'), 106 | 'filename' => basename($filePath) 107 | ] 108 | ]) 109 | )); 110 | 111 | $data = json_decode($response->getBody()->getContents(), true); 112 | return new File($data); 113 | } catch (\GuzzleHttp\Exception\ClientException $e) { 114 | $this->handleApiError($e); 115 | } 116 | } 117 | 118 | /** 119 | * Lists files with optional filtering 120 | * 121 | * @param string|null $purpose Filter by purpose 122 | * @param array $params Additional parameters (limit, after, order) 123 | * @throws GroqException 124 | */ 125 | public function list(?string $purpose = null, ?array $params = []): array 126 | { 127 | $query = array_filter([ 128 | 'purpose' => $purpose, 129 | 'limit' => $params['limit'] ?? 20, 130 | 'after' => $params['after'] ?? null, 131 | 'order' => $params['order'] ?? 'desc', 132 | 'created_after' => $params['created_after'] ?? null, 133 | 'created_before' => $params['created_before'] ?? null 134 | ]); 135 | 136 | try { 137 | $response = $this->groq->makeRequest(new Request('GET', 'files', [ 138 | 'query' => $query 139 | ])); 140 | 141 | $data = json_decode($response->getBody()->getContents(), true); 142 | $data['data'] = array_map(function ($item) { 143 | return new File($item); 144 | }, $data['data']); 145 | 146 | return $data; 147 | } catch (\GuzzleHttp\Exception\ClientException $e) { 148 | $this->handleApiError($e); 149 | } 150 | } 151 | 152 | /** 153 | * Retrieves file information 154 | * 155 | * @param string $fileId File ID 156 | * @throws GroqException 157 | */ 158 | public function retrieve(string $fileId): File 159 | { 160 | try { 161 | $response = $this->groq->makeRequest(new Request('GET', "files/{$fileId}")); 162 | $data = json_decode($response->getBody()->getContents(), true); 163 | return new File($data); 164 | } catch (\GuzzleHttp\Exception\ClientException $e) { 165 | $this->handleApiError($e); 166 | } 167 | } 168 | 169 | /** 170 | * Deletes a file 171 | * 172 | * @param string $fileId File ID 173 | * @throws GroqException 174 | */ 175 | public function delete(string $fileId): array 176 | { 177 | try { 178 | $response = $this->groq->makeRequest(new Request('DELETE', "files/{$fileId}")); 179 | return json_decode($response->getBody()->getContents(), true); 180 | } catch (\GuzzleHttp\Exception\ClientException $e) { 181 | $this->handleApiError($e); 182 | } 183 | } 184 | 185 | /** 186 | * Downloads a file's content 187 | * 188 | * @param string $fileId File ID 189 | * @param bool $useCache Whether to use cached content if available 190 | * @throws GroqException 191 | */ 192 | public function download(string $fileId, bool $useCache = true): string 193 | { 194 | if ($useCache && $this->cacheDir !== null) { 195 | $cachedContent = $this->getFromCache($fileId); 196 | if ($cachedContent !== null) { 197 | return $cachedContent; 198 | } 199 | } 200 | 201 | try { 202 | $response = $this->groq->makeRequest(new Request('GET', "files/{$fileId}/content")); 203 | $content = $response->getBody()->getContents(); 204 | 205 | if ($this->cacheDir !== null) { 206 | $this->saveToCache($fileId, $content); 207 | } 208 | 209 | return $content; 210 | } catch (\GuzzleHttp\Exception\ClientException $e) { 211 | $this->handleApiError($e); 212 | } 213 | } 214 | 215 | /** 216 | * Validates a file before upload 217 | * 218 | * @param string $filePath Path to the file 219 | * @throws GroqException 220 | */ 221 | private function validateFileType(string $filePath): void 222 | { 223 | if (!file_exists($filePath)) { 224 | throw new GroqException( 225 | 'File not found', 226 | 400, 227 | 'invalid_request' 228 | ); 229 | } 230 | 231 | if (filesize($filePath) === 0) { 232 | throw new GroqException( 233 | 'File is empty', 234 | 400, 235 | 'invalid_request' 236 | ); 237 | } 238 | 239 | if (filesize($filePath) > 100 * 1024 * 1024) { 240 | throw new GroqException( 241 | 'File size exceeds maximum limit of 100MB', 242 | 400, 243 | 'invalid_request' 244 | ); 245 | } 246 | 247 | $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); 248 | if (!in_array($extension, $this->allowedExtensions)) { 249 | throw new GroqException( 250 | 'Invalid file extension. Supported extensions are: ' . implode(', ', $this->allowedExtensions), 251 | 400, 252 | 'invalid_request' 253 | ); 254 | } 255 | 256 | $fileInfo = new \finfo(FILEINFO_MIME_TYPE); 257 | $mimeType = $fileInfo->file($filePath); 258 | 259 | if (!in_array($mimeType, $this->allowedMimeTypes)) { 260 | throw new GroqException( 261 | 'Invalid file type. File must be a valid JSONL file. You provided: ' . $mimeType, 262 | 400, 263 | 'invalid_request' 264 | ); 265 | } 266 | } 267 | 268 | /** 269 | * Validates JSONL file content 270 | * 271 | * @param string $filePath Path to the file 272 | * @throws GroqException 273 | */ 274 | private function validateFileContent(string $filePath): void 275 | { 276 | $handle = fopen($filePath, 'r'); 277 | 278 | if ($handle === false) { 279 | throw new GroqException( 280 | 'Unable to read file', 281 | 400, 282 | 'invalid_request' 283 | ); 284 | } 285 | 286 | $lineNumber = 0; 287 | while (($line = fgets($handle)) !== false) { 288 | $lineNumber++; 289 | $line = trim($line); 290 | 291 | if (empty($line)) { 292 | continue; 293 | } 294 | 295 | $decoded = json_decode($line, true); 296 | if ($decoded === null) { 297 | throw new GroqException( 298 | "Invalid JSON on line {$lineNumber}: " . json_last_error_msg(), 299 | 400, 300 | 'invalid_request' 301 | ); 302 | } 303 | 304 | // Validate required fields for batch requests 305 | if (!isset($decoded['custom_id']) || !is_string($decoded['custom_id'])) { 306 | throw new GroqException( 307 | "Missing or invalid 'custom_id' field on line {$lineNumber}", 308 | 400, 309 | 'invalid_request' 310 | ); 311 | } 312 | 313 | if (!isset($decoded['method']) || $decoded['method'] !== 'POST') { 314 | throw new GroqException( 315 | "Missing or invalid 'method' field on line {$lineNumber}. Only POST method is supported.", 316 | 400, 317 | 'invalid_request' 318 | ); 319 | } 320 | 321 | if (!isset($decoded['url']) || !str_starts_with($decoded['url'], '/v1/')) { 322 | throw new GroqException( 323 | "Missing or invalid 'url' field on line {$lineNumber}. URL must start with '/v1/'", 324 | 400, 325 | 'invalid_request' 326 | ); 327 | } 328 | 329 | // Validate endpoint 330 | if (!in_array($decoded['url'], $this->supportedEndpoints)) { 331 | throw new GroqException( 332 | "Invalid endpoint '{$decoded['url']}' on line {$lineNumber}. Supported endpoints are: " . implode(', ', $this->supportedEndpoints), 333 | 400, 334 | 'invalid_request' 335 | ); 336 | } 337 | 338 | if (!isset($decoded['body']) || !is_array($decoded['body'])) { 339 | throw new GroqException( 340 | "Missing or invalid 'body' field on line {$lineNumber}", 341 | 400, 342 | 'invalid_request' 343 | ); 344 | } 345 | 346 | if (!isset($decoded['body']['model']) || !is_string($decoded['body']['model'])) { 347 | throw new GroqException( 348 | "Missing or invalid 'model' field in body on line {$lineNumber}", 349 | 400, 350 | 'invalid_request' 351 | ); 352 | } 353 | 354 | // Validate specific endpoints 355 | if ($decoded['url'] === '/v1/chat/completions') { 356 | if (!isset($decoded['body']['messages']) || !is_array($decoded['body']['messages'])) { 357 | throw new GroqException( 358 | "Missing or invalid 'messages' field for chat completion on line {$lineNumber}", 359 | 400, 360 | 'invalid_request' 361 | ); 362 | } 363 | 364 | // Validate messages array is not empty and has required fields 365 | if (empty($decoded['body']['messages'])) { 366 | throw new GroqException( 367 | "The 'messages' array cannot be empty on line {$lineNumber}", 368 | 400, 369 | 'invalid_request' 370 | ); 371 | } 372 | 373 | foreach ($decoded['body']['messages'] as $index => $message) { 374 | if (!isset($message['role']) || !isset($message['content'])) { 375 | throw new GroqException( 376 | "Message at index {$index} is missing required fields 'role' or 'content' on line {$lineNumber}", 377 | 400, 378 | 'invalid_request' 379 | ); 380 | } 381 | } 382 | } elseif (str_contains($decoded['url'], '/v1/audio/')) { 383 | if (!isset($decoded['body']['url']) || !filter_var($decoded['body']['url'], FILTER_VALIDATE_URL)) { 384 | throw new GroqException( 385 | "Missing or invalid audio 'url' field on line {$lineNumber}", 386 | 400, 387 | 'invalid_request' 388 | ); 389 | } 390 | 391 | // Validate audio-specific fields 392 | if (!isset($decoded['body']['language'])) { 393 | throw new GroqException( 394 | "Missing required field 'language' for audio request on line {$lineNumber}", 395 | 400, 396 | 'invalid_request' 397 | ); 398 | } 399 | } 400 | } 401 | 402 | fclose($handle); 403 | } 404 | 405 | /** 406 | * Gets file content from cache 407 | * 408 | * @param string $fileId File ID 409 | */ 410 | private function getFromCache(string $fileId): ?string 411 | { 412 | if ($this->cacheDir === null) { 413 | return null; 414 | } 415 | 416 | $cachePath = "{$this->cacheDir}/{$fileId}"; 417 | if (!file_exists($cachePath)) { 418 | return null; 419 | } 420 | 421 | return file_get_contents($cachePath); 422 | } 423 | 424 | /** 425 | * Saves file content to cache 426 | * 427 | * @param string $fileId File ID 428 | * @param string $content File content 429 | */ 430 | private function saveToCache(string $fileId, string $content): void 431 | { 432 | if ($this->cacheDir === null) { 433 | return; 434 | } 435 | 436 | $cachePath = "{$this->cacheDir}/{$fileId}"; 437 | file_put_contents($cachePath, $content); 438 | } 439 | 440 | /** 441 | * Handles API errors 442 | * 443 | * @param \GuzzleHttp\Exception\ClientException $e 444 | * @throws GroqException 445 | */ 446 | private function handleApiError(\GuzzleHttp\Exception\ClientException $e): never 447 | { 448 | $response = $e->getResponse(); 449 | $errorBody = json_decode($response->getBody()->getContents(), true); 450 | 451 | if ( 452 | $response->getStatusCode() === 403 && 453 | isset($errorBody['error']['type']) && 454 | $errorBody['error']['type'] === 'permissions_error' 455 | ) { 456 | throw new GroqException( 457 | 'Files API is not available in your current Groq plan. Please upgrade your plan to use this feature.', 458 | 403, 459 | 'permissions_error' 460 | ); 461 | } 462 | 463 | if ($response->getStatusCode() === 429) { 464 | throw new GroqException( 465 | 'Rate limit exceeded. Please try again later.', 466 | 429, 467 | 'rate_limit_error' 468 | ); 469 | } 470 | 471 | throw new GroqException( 472 | $errorBody['error']['message'] ?? 'Unknown error occurred', 473 | $response->getStatusCode(), 474 | $errorBody['error']['type'] ?? 'api_error' 475 | ); 476 | } 477 | } --------------------------------------------------------------------------------