Skip to content

[Platform] Add Groq support #115

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions examples/groq/chat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

use Symfony\AI\Agent\Agent;
use Symfony\AI\Platform\Bridge\Groq\Llama;
use Symfony\AI\Platform\Bridge\Groq\PlatformFactory;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\Component\Dotenv\Dotenv;

require_once dirname(__DIR__, 2).'/vendor/autoload.php';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 2 can be removed here:

Suggested change
require_once dirname(__DIR__, 2).'/vendor/autoload.php';
require_once dirname(__DIR__).'/vendor/autoload.php';

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

after this change the example works for me 👍

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, thanks. Somehow I got in a bit of a mess with doing a composer install in the monorepo!

(new Dotenv())->loadEnv(dirname(__DIR__).'/.env');

$platform = PlatformFactory::create($_SERVER['GROQ_API_KEY']);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when introducing a new env var, please add it to .env file (without value in this case) and check if the variable is set before - see other examples

$model = new Llama(Llama::LLAMA3_70B, [
'temperature' => 0.5, // default options for the model
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as this is the default, can we remove it in the example?

]);

$agent = new Agent($platform, $model);
$messages = new MessageBag(
Message::forSystem('You are a pirate and you write funny.'),
Message::ofUser('What is the Symfony framework?'),
);
$response = $agent->call($messages, [
'max_tokens' => 500, // specific options just for this call
]);

echo $response->getContent().\PHP_EOL;
15 changes: 14 additions & 1 deletion src/platform/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,16 @@ usually defined by the specific models and their documentation.
* **Language Models**
* `OpenAI's GPT`_ with `OpenAI`_ and `Azure`_ as Platform
* `Anthropic's Claude`_ with `Anthropic`_ and `AWS Bedrock`_ as Platform
* `Meta's Llama`_ with `Azure`_, `Ollama`_, `Replicate`_ and `AWS Bedrock`_ as Platform
* `Meta's Llama`_ with `Azure`_, `Ollama`_, `Replicate`_, `AWS Bedrock`_ and `Groq`_ as Platform
* `Google's Gemma`_ with `Groq`_ as Platform
* `Mistral's Mixtral`_ with `Groq`_ as Platform
* `Alibaba's Qwen`_ with `Groq`_ as Platform
Comment on lines +75 to +78
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should use some kind of table for this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, this doesn't scale well - not necessarily scope of this PR, but I'm in need for better ideas - even feels like a three dimensional table 😆


.. note::

While Groq supports various models, specific API requirements or limitations might apply. Refer to Groq's official documentation for details.
* `Google's Gemini`_ with `Google`_ and `OpenRouter`_ as Platform
* `Groq's Llama`_ with `Groq`_ as Platform
* `DeepSeek's R1`_ with `OpenRouter`_ as Platform
* `Amazon's Nova`_ with `AWS Bedrock`_ as Platform
* `Mistral's Mistral`_ with `Mistral`_ as Platform
Expand Down Expand Up @@ -304,6 +312,11 @@ which can be useful to speed up the processing::
.. _`Google's Gemini`: https://gemini.google.com/
.. _`Google`: https://ai.google.dev/
.. _`OpenRouter`: https://www.openrouter.com/
.. _`Groq's Llama`: https://console.groq.com/docs/overview
.. _`Groq`: https://groq.com/
.. _`Google's Gemma`: https://ai.google.dev/models/gemma
.. _`Mistral's Mixtral`: https://mistral.ai/news/mixtral-8x7b/
.. _`Alibaba's Qwen`: https://qwen.ai
.. _`DeepSeek's R1`: https://www.deepseek.com/
.. _`Amazon's Nova`: https://nova.amazon.com
.. _`Mistral's Mistral`: https://www.mistral.ai/
Expand Down
46 changes: 46 additions & 0 deletions src/platform/src/Bridge/Groq/Llama.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\AI\Platform\Bridge\Groq;

use Symfony\AI\Platform\Capability;
use Symfony\AI\Platform\Model;

/**
* @author Christopher Hertel <[email protected]>
* @author Dave Hulbert <[email protected]>
*/
class Llama extends Model
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be something more neutral like

Suggested change
class Llama extends Model
class Model extends BaseModel

since this is not only Llama it's a bit confusing - it's rather a collection of models available on Groq

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Yeah, I'll update. Started with Llama, then I realised they have others now.

{
public const LLAMA3_8B = 'llama3-8b-8192';
public const LLAMA3_70B = 'llama3-70b-8192';
public const LLAMA2_70B = 'llama2-70b-4096';
public const MIXTRAL_8X7B = 'mistral-saba-24b';
public const GEMMA_7B = 'gemma2-9b-it';
public const QWEN_32B = 'qwen/qwen3-32b';

/**
* @param array<mixed> $options The default options for the model usage
*/
public function __construct(
string $name = self::LLAMA3_70B,
array $options = ['temperature' => 1.0],
) {
$capabilities = [
Capability::INPUT_MESSAGES,
Capability::OUTPUT_TEXT,
Capability::OUTPUT_STREAMING,
Capability::TOOL_CALLING,
];

parent::__construct($name, $capabilities, $options);
}
}
52 changes: 52 additions & 0 deletions src/platform/src/Bridge/Groq/Llama/ModelClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\AI\Platform\Bridge\Groq\Llama;

use Symfony\AI\Platform\Bridge\Groq\Llama;
use Symfony\AI\Platform\Model;
use Symfony\AI\Platform\ModelClientInterface as PlatformResponseFactory;
use Symfony\Component\HttpClient\EventSourceHttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Webmozart\Assert\Assert;

/**
* @author Christopher Hertel <[email protected]>
* @author Dave Hulbert <[email protected]>
*/
final readonly class ModelClient implements PlatformResponseFactory
{
private EventSourceHttpClient $httpClient;

public function __construct(
HttpClientInterface $httpClient,
#[\SensitiveParameter]
private string $apiKey,
) {
$this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
Assert::stringNotEmpty($apiKey, 'The API key must not be empty.');
Assert::startsWith($apiKey, 'gsk_', 'The API key must start with "gsk_".');
}

public function supports(Model $model): bool
{
return $model instanceof Llama;
}

public function request(Model $model, array|string $payload, array $options = []): ResponseInterface
{
return $this->httpClient->request('POST', 'https://api.groq.com/openai/v1/chat/completions', [
'auth_bearer' => $this->apiKey,
'json' => array_merge($options, $payload),
]);
}
}
204 changes: 204 additions & 0 deletions src/platform/src/Bridge/Groq/Llama/ResponseConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\AI\Platform\Bridge\Groq\Llama;

use Symfony\AI\Platform\Bridge\Groq\Llama;
use Symfony\AI\Platform\Exception\ContentFilterException;
use Symfony\AI\Platform\Exception\RuntimeException;
use Symfony\AI\Platform\Model;
use Symfony\AI\Platform\Response\Choice;
use Symfony\AI\Platform\Response\ChoiceResponse;
use Symfony\AI\Platform\Response\ResponseInterface as LlmResponse;
use Symfony\AI\Platform\Response\StreamResponse;
use Symfony\AI\Platform\Response\TextResponse;
use Symfony\AI\Platform\Response\ToolCall;
use Symfony\AI\Platform\Response\ToolCallResponse;
use Symfony\AI\Platform\ResponseConverterInterface as PlatformResponseConverter;
use Symfony\Component\HttpClient\Chunk\ServerSentEvent;
use Symfony\Component\HttpClient\EventSourceHttpClient;
use Symfony\Component\HttpClient\Exception\JsonException;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface as HttpResponse;

/**
* @author Christopher Hertel <[email protected]>
* @author Dave Hulbert <[email protected]>
*/
final class ResponseConverter implements PlatformResponseConverter
{
public function supports(Model $model): bool
{
return $model instanceof Llama;
}

public function convert(HttpResponse $response, array $options = []): LlmResponse
{
if ($options['stream'] ?? false) {
return new StreamResponse($this->convertStream($response));
}

try {
$data = $response->toArray();
} catch (ClientExceptionInterface $e) {
$data = $response->toArray(throw: false);

if (isset($data['error']['code']) && 'content_filter' === $data['error']['code']) {
throw new ContentFilterException(message: $data['error']['message'], previous: $e);
}

throw $e;
}

if (!isset($data['choices'])) {
throw new RuntimeException('Response does not contain choices');
}

/** @var Choice[] $choices */
$choices = array_map($this->convertChoice(...), $data['choices']);

if (1 !== \count($choices)) {
return new ChoiceResponse(...$choices);
}

if ($choices[0]->hasToolCall()) {
return new ToolCallResponse(...$choices[0]->getToolCalls());
}

return new TextResponse($choices[0]->getContent());
}

private function convertStream(HttpResponse $response): \Generator
{
$toolCalls = [];
foreach ((new EventSourceHttpClient())->stream($response) as $chunk) {
if (!$chunk instanceof ServerSentEvent || '[DONE]' === $chunk->getData()) {
continue;
}

try {
$data = $chunk->getArrayData();
} catch (JsonException) {
// try catch only needed for Symfony 6.4
continue;
}

if ($this->streamIsToolCall($data)) {
$toolCalls = $this->convertStreamToToolCalls($toolCalls, $data);
}

if ([] !== $toolCalls && $this->isToolCallsStreamFinished($data)) {
yield new ToolCallResponse(...array_map($this->convertToolCall(...), $toolCalls));
}

if (!isset($data['choices'][0]['delta']['content'])) {
continue;
}

yield $data['choices'][0]['delta']['content'];
}
}

/**
* @param array<string, mixed> $toolCalls
* @param array<string, mixed> $data
*
* @return array<string, mixed>
*/
private function convertStreamToToolCalls(array $toolCalls, array $data): array
{
if (!isset($data['choices'][0]['delta']['tool_calls'])) {
return $toolCalls;
}

foreach ($data['choices'][0]['delta']['tool_calls'] as $i => $toolCall) {
if (isset($toolCall['id'])) {
// initialize tool call
$toolCalls[$i] = [
'id' => $toolCall['id'],
'function' => $toolCall['function'],
];
continue;
}

// add arguments delta to tool call
$toolCalls[$i]['function']['arguments'] .= $toolCall['function']['arguments'];
}

return $toolCalls;
}

/**
* @param array<string, mixed> $data
*/
private function streamIsToolCall(array $data): bool
{
return isset($data['choices'][0]['delta']['tool_calls']);
}

/**
* @param array<string, mixed> $data
*/
private function isToolCallsStreamFinished(array $data): bool
{
return isset($data['choices'][0]['finish_reason']) && 'tool_calls' === $data['choices'][0]['finish_reason'];
}

/**
* @param array{
* index: integer,
* message: array{
* role: 'assistant',
* content: ?string,
* tool_calls: array{
* id: string,
* type: 'function',
* function: array{
* name: string,
* arguments: string
* },
* },
* refusal: ?mixed
* },
* logprobs: string,
* finish_reason: 'stop'|'length'|'tool_calls'|'content_filter',
* } $choice
*/
private function convertChoice(array $choice): Choice
{
if ('tool_calls' === $choice['finish_reason']) {
return new Choice(toolCalls: array_map([$this, 'convertToolCall'], $choice['message']['tool_calls']));
}

if (\in_array($choice['finish_reason'], ['stop', 'length'], true)) {
return new Choice($choice['message']['content']);
}

throw new RuntimeException(\sprintf('Unsupported finish reason "%s".', $choice['finish_reason']));
}

/**
* @param array{
* id: string,
* type: 'function',
* function: array{
* name: string,
* arguments: string
* }
* } $toolCall
*/
private function convertToolCall(array $toolCall): ToolCall
{
$arguments = json_decode($toolCall['function']['arguments'], true, \JSON_THROW_ON_ERROR);

return new ToolCall($toolCall['id'], $toolCall['function']['name'], $arguments);
}
}
Loading