├── 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 |
--------------------------------------------------------------------------------
/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 |
9 |
10 |
11 |
12 | by
13 | context window:
14 |
15 |
16 |
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 |
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 |
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 |
55 |
--------------------------------------------------------------------------------
/examples/audio-translations.php:
--------------------------------------------------------------------------------
1 |
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 | Selecione as imagens
52 |
58 |
59 |
60 | Prompt
61 |
63 |
64 |
65 | Enviar
66 |
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 | Model
60 |
61 | DeepSeek R1 Distill Llama 70B
62 | DeepSeek R1 Distill Qwen 32B
63 |
64 |
65 |
66 | System Prompt
67 | (optional)
68 |
71 |
72 |
73 | Reasoning Format
74 |
75 | Raw (with thought tags)
76 | Parsed (separate field)
77 | Hidden (final answer only)
78 |
79 |
80 |
81 | Question or Problem
82 |
85 |
86 |
111 |
113 | Analyze
114 |
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 | Arquivo de Áudio:
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 | Idioma:
110 |
111 | Selecione um idioma
112 | $name): ?>
113 | -
114 |
115 |
116 |
117 | Formato da Resposta:
118 |
119 | json
120 | verbose_json
121 | text
122 |
123 |
124 | Prompt (opcional):
125 |
126 | Transcrever
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: = htmlspecialchars($error) ?>
91 |
92 |
93 |
94 |
95 |
96 |
= htmlspecialchars($success) ?>
97 |
98 |
99 |
100 |
101 |
102 |
Upload JSONL File
103 |
104 |
105 |
106 | Select JSONL File (max 100MB)
107 |
108 |
118 |
119 |
123 | Upload File
124 |
125 |
126 |
127 |
128 |
129 |
130 |
Available Files
131 |
132 |
No files available.
133 |
134 |
135 |
136 |
137 |
138 |
139 |
= htmlspecialchars($file->filename) ?>
140 |
141 |
ID: = htmlspecialchars($file->id) ?>
142 |
Size: = number_format($file->bytes / 1024, 2) ?> KB
143 |
Created: = date('Y-m-d H:i', strtotime($file->created_at)) ?>
144 |
Status:
148 | = htmlspecialchars($file->status) ?>
149 |
150 |
151 |
152 |
153 |
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 | }
--------------------------------------------------------------------------------