Skip to content

Commit 3156d4c

Browse files
junaidbinfarooqchr-hertel
authored andcommitted
[Platform] Add Cerebras
1 parent 68803d6 commit 3156d4c

File tree

8 files changed

+351
-0
lines changed

8 files changed

+351
-0
lines changed

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

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

src/Bridge/Cerebras/Model.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+
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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\Tests\Bridge\Cerebras;
13+
14+
use PHPUnit\Framework\Attributes\CoversClass;
15+
use PHPUnit\Framework\Attributes\Small;
16+
use PHPUnit\Framework\Attributes\TestWith;
17+
use PHPUnit\Framework\Attributes\UsesClass;
18+
use PHPUnit\Framework\TestCase;
19+
use Symfony\AI\Platform\Bridge\Cerebras\Model;
20+
use Symfony\AI\Platform\Bridge\Cerebras\ModelClient;
21+
use Symfony\AI\Platform\Exception\InvalidArgumentException;
22+
use Symfony\Component\HttpClient\MockHttpClient;
23+
use Symfony\Component\HttpClient\Response\JsonMockResponse;
24+
25+
/**
26+
* @author Junaid Farooq <[email protected]>
27+
*/
28+
#[CoversClass(ModelClient::class)]
29+
#[UsesClass(Model::class)]
30+
#[Small]
31+
class ModelClientTest extends TestCase
32+
{
33+
public function testItDoesNotAllowAnEmptyKey()
34+
{
35+
$this->expectException(InvalidArgumentException::class);
36+
$this->expectExceptionMessage('The API key must not be empty.');
37+
38+
new ModelClient(new MockHttpClient(), '');
39+
}
40+
41+
#[TestWith(['api-key-without-prefix'])]
42+
#[TestWith(['pk-api-key'])]
43+
#[TestWith(['SK-api-key'])]
44+
#[TestWith(['skapikey'])]
45+
#[TestWith(['sk api-key'])]
46+
#[TestWith(['sk'])]
47+
public function testItVerifiesIfTheKeyStartsWithCsk(string $invalidApiKey)
48+
{
49+
$this->expectException(InvalidArgumentException::class);
50+
$this->expectExceptionMessage('The API key must start with "csk-".');
51+
52+
new ModelClient(new MockHttpClient(), $invalidApiKey);
53+
}
54+
55+
public function testItSupportsTheCorrectModel()
56+
{
57+
$client = new ModelClient(new MockHttpClient(), 'csk-1234567890abcdef');
58+
59+
self::assertTrue($client->supports(new Model(Model::GPT_OSS_120B)));
60+
}
61+
62+
public function testItSuccessfullyInvokesTheModel()
63+
{
64+
$expectedResponse = [
65+
'model' => 'llama-3.3-70b',
66+
'input' => [
67+
'messages' => [
68+
['role' => 'user', 'content' => 'Hello, world!'],
69+
],
70+
],
71+
'temperature' => 0.5,
72+
];
73+
$httpClient = new MockHttpClient(
74+
new JsonMockResponse($expectedResponse),
75+
);
76+
77+
$client = new ModelClient($httpClient, 'csk-1234567890abcdef');
78+
79+
$payload = [
80+
'messages' => [
81+
['role' => 'user', 'content' => 'Hello, world!'],
82+
],
83+
];
84+
85+
$result = $client->request(new Model(Model::LLAMA_3_3_70B), $payload);
86+
$data = $result->getData();
87+
$info = $result->getObject()->getInfo();
88+
89+
self::assertNotEmpty($data);
90+
self::assertNotEmpty($info);
91+
self::assertSame('POST', $info['http_method']);
92+
self::assertSame('https://api.cerebras.ai/v1/chat/completions', $info['url']);
93+
self::assertSame($expectedResponse, $data);
94+
}
95+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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\Tests\Bridge\Cerebras;
13+
14+
use PHPUnit\Framework\Attributes\CoversClass;
15+
use PHPUnit\Framework\Attributes\Small;
16+
use PHPUnit\Framework\Attributes\UsesClass;
17+
use PHPUnit\Framework\TestCase;
18+
use Symfony\AI\Platform\Bridge\Cerebras\Model;
19+
use Symfony\AI\Platform\Bridge\Cerebras\ModelClient;
20+
use Symfony\AI\Platform\Bridge\Cerebras\ResultConverter;
21+
use Symfony\Component\HttpClient\MockHttpClient;
22+
23+
/**
24+
* @author Junaid Farooq <[email protected]>
25+
*/
26+
#[CoversClass(ResultConverter::class)]
27+
#[UsesClass(Model::class)]
28+
#[Small]
29+
class ResultConverterTest extends TestCase
30+
{
31+
public function testItSupportsTheCorrectModel()
32+
{
33+
$client = new ModelClient(new MockHttpClient(), 'csk-1234567890abcdef');
34+
35+
$this->assertTrue($client->supports(new Model(Model::GPT_OSS_120B)));
36+
}
37+
}

0 commit comments

Comments
 (0)