Skip to content

Commit 4e69f31

Browse files
committed
feature #284 [Platform] Add Cerebras (junaidbinfarooq)
This PR was squashed before being merged into the main branch. Discussion ---------- [Platform] Add Cerebras | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Docs? | yes | Issues | #16 | License | MIT This PR adds support for the [Cerebras](https://inference-docs.cerebras.ai/) platform Commits ------- a9954c1 [Platform] Add Cerebras
2 parents 22a3555 + a9954c1 commit 4e69f31

File tree

12 files changed

+437
-0
lines changed

12 files changed

+437
-0
lines changed

examples/.env

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,6 @@ NEO4J_PASSWORD=symfonyai
104104
# Typesense
105105
TYPESENSE_HOST=http://127.0.0.1:8108
106106
TYPESENSE_API_KEY=changeMe
107+
108+
# Cerebras
109+
CEREBRAS_API_KEY=

examples/cerebras/chat.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
use Symfony\AI\Agent\Agent;
13+
use Symfony\AI\Platform\Bridge\Cerebras\Model;
14+
use Symfony\AI\Platform\Bridge\Cerebras\PlatformFactory;
15+
use Symfony\AI\Platform\Message\Message;
16+
use Symfony\AI\Platform\Message\MessageBag;
17+
18+
require_once dirname(__DIR__).'/bootstrap.php';
19+
20+
$platform = PlatformFactory::create(env('CEREBRAS_API_KEY'), http_client());
21+
22+
$agent = new Agent($platform, new Model(), logger: logger());
23+
$messages = new MessageBag(
24+
Message::forSystem('You are a helpful assistant.'),
25+
Message::ofUser('How is the weather in Tokyo today?'),
26+
);
27+
$result = $agent->call($messages);
28+
29+
echo $result->getContent().\PHP_EOL;

examples/cerebras/stream.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
use Symfony\AI\Agent\Agent;
13+
use Symfony\AI\Platform\Bridge\Cerebras\Model;
14+
use Symfony\AI\Platform\Bridge\Cerebras\PlatformFactory;
15+
use Symfony\AI\Platform\Message\Message;
16+
use Symfony\AI\Platform\Message\MessageBag;
17+
18+
require_once dirname(__DIR__).'/bootstrap.php';
19+
20+
$platform = PlatformFactory::create(env('CEREBRAS_API_KEY'), http_client());
21+
22+
$agent = new Agent($platform, new Model(), logger: logger());
23+
24+
$messages = new MessageBag(
25+
Message::forSystem('You are an expert in places and geography who always responds concisely.'),
26+
Message::ofUser('What are the top three destinations in France?'),
27+
);
28+
29+
$result = $agent->call($messages, [
30+
'stream' => true,
31+
]);
32+
33+
foreach ($result->getContent() as $word) {
34+
echo $word;
35+
}
36+
echo \PHP_EOL;

src/ai-bundle/src/AiBundle.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
use Symfony\AI\Platform\Bridge\Ollama\PlatformFactory as OllamaPlatformFactory;
3434
use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory as OpenAiPlatformFactory;
3535
use Symfony\AI\Platform\Bridge\OpenRouter\PlatformFactory as OpenRouterPlatformFactory;
36+
use Symfony\AI\Platform\Bridge\Cerebras\PlatformFactory as CerebrasPlatformFactory;
3637
use Symfony\AI\Platform\Model;
3738
use Symfony\AI\Platform\ModelClientInterface;
3839
use Symfony\AI\Platform\Platform;
@@ -316,6 +317,23 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
316317
return;
317318
}
318319

320+
if ('cerebras' === $type && isset($platform['api_key'])) {
321+
$platformId = 'ai.platform.cerebras';
322+
$definition = (new Definition(Platform::class))
323+
->setFactory(CerebrasPlatformFactory::class.'::create')
324+
->setLazy(true)
325+
->addTag('proxy', ['interface' => PlatformInterface::class])
326+
->setArguments([
327+
$platform['api_key'],
328+
new Reference('http_client', ContainerInterface::NULL_ON_INVALID_REFERENCE),
329+
])
330+
->addTag('ai.platform');
331+
332+
$container->setDefinition($platformId, $definition);
333+
334+
return;
335+
}
336+
319337
throw new InvalidArgumentException(\sprintf('Platform "%s" is not supported for configuration via bundle at this point.', $type));
320338
}
321339

src/platform/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ CHANGELOG
2121
- HuggingFace (extensive model support with multiple tasks)
2222
- TransformersPHP (local PHP-based transformer models)
2323
- LM Studio (local model hosting)
24+
- Cerebras (language models like Llama 4, Qwen 3, and more)
2425
* Add comprehensive message system with role-based messaging:
2526
- `UserMessage` for user inputs with multi-modal content
2627
- `SystemMessage` for system instructions

src/platform/doc/index.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,8 @@ This allows fast and isolated testing of AI-powered features without relying on
346346

347347
* `Parallel GPT Calls`_
348348
* `Parallel Embeddings Calls`_
349+
* `Cerebras Chat`_
350+
* `Cerebras Streaming`_
349351

350352
.. note::
351353

@@ -392,3 +394,5 @@ This allows fast and isolated testing of AI-powered features without relying on
392394
.. _`Parallel Embeddings Calls`: https://github.com/symfony/ai/blob/main/examples/misc/parallel-embeddings.php
393395
.. _`LM Studio`: https://lmstudio.ai/
394396
.. _`LM Studio Catalog`: https://lmstudio.ai/models
397+
.. _`Cerebras Chat`: https://github.com/symfony/ai/blob/main/examples/cerebras/chat.php
398+
.. _`Cerebras Streaming`: https://github.com/symfony/ai/blob/main/examples/cerebras/stream.php
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace Symfony\AI\Platform\Bridge\Cerebras;
4+
5+
use Symfony\AI\Platform\Capability;
6+
use \Symfony\AI\Platform\Model as BaseModel;
7+
8+
/**
9+
* @author Junaid Farooq <[email protected]>
10+
*/
11+
final class Model extends BaseModel
12+
{
13+
public const LLAMA_4_SCOUT_17B_16E_INSTRUCT = 'llama-4-scout-17b-16e-instruct';
14+
public const LLAMA3_1_8B = 'llama3.1-8b';
15+
public const LLAMA_3_3_70B = 'llama-3.3-70b';
16+
public const LLAMA_4_MAVERICK_17B_128E_INSTRUCT = 'llama-4-maverick-17b-128e-instruct';
17+
public const QWEN_3_32B = 'qwen-3-32b';
18+
public const QWEN_3_235B_A22B_INSTRUCT_2507 = 'qwen-3-235b-a22b-instruct-2507';
19+
public const QWEN_3_235B_A22B_THINKING_2507 = 'qwen-3-235b-a22b-thinking-2507';
20+
public const QWEN_3_CODER_480B = 'qwen-3-coder-480b';
21+
public const GPT_OSS_120B = 'gpt-oss-120b';
22+
23+
public const CAPABILITIES = [
24+
Capability::INPUT_MESSAGES,
25+
Capability::OUTPUT_TEXT,
26+
Capability::OUTPUT_STREAMING,
27+
];
28+
29+
/**
30+
* @see https://inference-docs.cerebras.ai/api-reference/chat-completions for details like options
31+
*/
32+
public function __construct(
33+
string $name = self::LLAMA3_1_8B,
34+
array $capabilities = self::CAPABILITIES,
35+
array $options = [],
36+
) {
37+
parent::__construct($name, $capabilities, $options);
38+
}
39+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\Platform\Bridge\Cerebras;
13+
14+
use Symfony\AI\Platform\Exception\InvalidArgumentException;
15+
use Symfony\AI\Platform\Model as BaseModel;
16+
use Symfony\AI\Platform\ModelClientInterface;
17+
use Symfony\AI\Platform\Result\RawHttpResult;
18+
use Symfony\Component\HttpClient\EventSourceHttpClient;
19+
use Symfony\Contracts\HttpClient\HttpClientInterface;
20+
21+
/**
22+
* @author Junaid Farooq <[email protected]>
23+
*/
24+
final readonly class ModelClient implements ModelClientInterface
25+
{
26+
private EventSourceHttpClient $httpClient;
27+
28+
public function __construct(
29+
HttpClientInterface $httpClient,
30+
#[\SensitiveParameter] private string $apiKey,
31+
) {
32+
if ('' === $apiKey) {
33+
throw new InvalidArgumentException('The API key must not be empty.');
34+
}
35+
36+
if (!str_starts_with($apiKey, 'csk-')) {
37+
throw new InvalidArgumentException('The API key must start with "csk-".');
38+
}
39+
40+
$this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
41+
}
42+
43+
public function supports(BaseModel $model): bool
44+
{
45+
return $model instanceof Model;
46+
}
47+
48+
public function request(BaseModel $model, array|string $payload, array $options = []): RawHttpResult
49+
{
50+
return new RawHttpResult(
51+
$this->httpClient->request(
52+
'POST', 'https://api.cerebras.ai/v1/chat/completions',
53+
[
54+
'headers' => [
55+
'Content-Type' => 'application/json',
56+
'Authorization' => sprintf('Bearer %s', $this->apiKey),
57+
],
58+
'json' => \is_array($payload) ? array_merge($payload, $options) : $payload,
59+
]
60+
)
61+
);
62+
}
63+
}
64+
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\Platform\Bridge\Cerebras;
13+
14+
use Symfony\AI\Platform\Platform;
15+
use Symfony\Component\HttpClient\EventSourceHttpClient;
16+
use Symfony\Contracts\HttpClient\HttpClientInterface;
17+
18+
/**
19+
* @author Junaid Farooq <[email protected]>
20+
*/
21+
final readonly class PlatformFactory
22+
{
23+
public static function create(
24+
#[\SensitiveParameter] string $apiKey,
25+
?HttpClientInterface $httpClient = null,
26+
): Platform {
27+
$httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
28+
29+
return new Platform(
30+
[new ModelClient($httpClient, $apiKey)],
31+
[new ResultConverter()],
32+
);
33+
}
34+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\AI\Platform\Bridge\Cerebras;
13+
14+
use Symfony\AI\Platform\Exception\RuntimeException;
15+
use Symfony\AI\Platform\Model as BaseModel;
16+
use Symfony\AI\Platform\Result\RawHttpResult;
17+
use Symfony\AI\Platform\Result\RawResultInterface;
18+
use Symfony\AI\Platform\Result\ResultInterface;
19+
use Symfony\AI\Platform\Result\StreamResult;
20+
use Symfony\AI\Platform\Result\TextResult;
21+
use Symfony\AI\Platform\ResultConverterInterface;
22+
use Symfony\Component\HttpClient\Chunk\ServerSentEvent;
23+
use Symfony\Component\HttpClient\EventSourceHttpClient;
24+
use Symfony\Component\HttpClient\Exception\JsonException;
25+
use Symfony\Contracts\HttpClient\ResponseInterface as HttpResponse;
26+
27+
/**
28+
* @author Junaid Farooq <[email protected]>
29+
*/
30+
final readonly class ResultConverter implements ResultConverterInterface
31+
{
32+
public function supports(BaseModel $model): bool
33+
{
34+
return $model instanceof Model;
35+
}
36+
37+
public function convert(RawHttpResult|RawResultInterface $result, array $options = []): ResultInterface
38+
{
39+
if ($options['stream'] ?? false) {
40+
return new StreamResult($this->convertStream($result->getObject()));
41+
}
42+
43+
$data = $result->getData();
44+
45+
if (!isset($data['choices'][0]['message']['content'])) {
46+
if (isset($data['type'], $data['message']) && str_ends_with($data['type'], 'error')) {
47+
throw new RuntimeException(sprintf('Cerebras API error: %s', $data['message']));
48+
}
49+
50+
throw new RuntimeException('Response does not contain output.');
51+
}
52+
53+
return new TextResult($data['choices'][0]['message']['content']);
54+
}
55+
56+
private function convertStream(HttpResponse $result): \Generator
57+
{
58+
foreach ((new EventSourceHttpClient())->stream($result) as $chunk) {
59+
if (!$chunk instanceof ServerSentEvent || '[DONE]' === $chunk->getData()) {
60+
continue;
61+
}
62+
63+
try {
64+
$data = $chunk->getArrayData();
65+
} catch (JsonException) {
66+
continue;
67+
}
68+
69+
if (!isset($data['choices'][0]['delta']['content'])) {
70+
continue;
71+
}
72+
73+
yield $data['choices'][0]['delta']['content'];
74+
}
75+
}
76+
}
77+

0 commit comments

Comments
 (0)