-
-
Notifications
You must be signed in to change notification settings - Fork 39
[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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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'; | ||
(new Dotenv())->loadEnv(dirname(__DIR__).'/.env'); | ||
|
||
$platform = PlatformFactory::create($_SERVER['GROQ_API_KEY']); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. when introducing a new env var, please add it to |
||
$model = new Llama(Llama::LLAMA3_70B, [ | ||
'temperature' => 0.5, // default options for the model | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we should use some kind of table for this? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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/ | ||
|
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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could be something more neutral like
Suggested change
since this is not only Llama it's a bit confusing - it's rather a collection of models available on Groq There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||||||
} | ||||||
} |
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), | ||
]); | ||
} | ||
} |
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); | ||
} | ||
} |
There was a problem hiding this comment.
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:
There was a problem hiding this comment.
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 👍
There was a problem hiding this comment.
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!