Skip to content

Commit ae371a0

Browse files
committed
Add Docker Model Runner as Platform
1 parent ff08204 commit ae371a0

File tree

15 files changed

+726
-0
lines changed

15 files changed

+726
-0
lines changed

examples/.env

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ REPLICATE_API_KEY=
1818
# For using Ollama
1919
OLLAMA_HOST_URL=
2020

21+
# For using Docker Model Runner
22+
DOCKER_MODEL_RUNNER_HOST_URL=
23+
2124
# For using GPT on Azure
2225
AZURE_OPENAI_BASEURL=
2326
AZURE_OPENAI_KEY=
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\DockerModelRunner\Completions;
14+
use Symfony\AI\Platform\Bridge\DockerModelRunner\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('DOCKER_MODEL_RUNNER_HOST_URL'), http_client());
21+
$model = new Completions();
22+
23+
$agent = new Agent($platform, $model, logger: logger());
24+
$messages = new MessageBag(
25+
Message::forSystem('You are a pirate and you write funny.'),
26+
Message::ofUser('What is the Symfony framework?'),
27+
);
28+
$result = $agent->call($messages, [
29+
'max_tokens' => 500, // specific options just for this call
30+
]);
31+
echo $result->getContent().\PHP_EOL;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Platform\Bridge\DockerModelRunner\Embeddings;
13+
use Symfony\AI\Platform\Bridge\DockerModelRunner\PlatformFactory;
14+
15+
require_once dirname(__DIR__).'/bootstrap.php';
16+
17+
$platform = PlatformFactory::create(env('DOCKER_MODEL_RUNNER_HOST_URL'), http_client());
18+
$response = $platform->invoke(new Embeddings(Embeddings::NOMIC_EMBED_TEXT), <<<TEXT
19+
Once upon a time, there was a country called Japan. It was a beautiful country with a lot of mountains and rivers.
20+
The people of Japan were very kind and hardworking. They loved their country very much and took care of it. The
21+
country was very peaceful and prosperous. The people lived happily ever after.
22+
TEXT);
23+
24+
echo 'Dimensions: '.$response->asVectors()[0]->getDimensions().\PHP_EOL;

src/platform/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ CHANGELOG
2323
- LM Studio (local model hosting)
2424
- Cerebras (language models like Llama 4, Qwen 3, and more)
2525
- Perplexity (Sonar models, supporting search results)
26+
- Docker Model Runner (local model hosting)
2627
* Add comprehensive message system with role-based messaging:
2728
- `UserMessage` for user inputs with multi-modal content
2829
- `SystemMessage` for system instructions
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\DockerModelRunner;
13+
14+
use Symfony\AI\Platform\Capability;
15+
use Symfony\AI\Platform\Model;
16+
17+
/**
18+
* @author Mathieu Santostefano <msantostefano@proton.me>
19+
*/
20+
class Completions extends Model
21+
{
22+
public const GEMMA_3_N = 'ai/gemma3n';
23+
public const GEMMA_3 = 'ai/gemma3';
24+
public const QWEN_2_5 = 'ai/qwen2.5';
25+
public const QWEN_3 = 'ai/qwen3';
26+
public const QWEN_3_CODER = 'ai/qwen3-coder';
27+
public const LLAMA_3_1 = 'ai/llama3.1';
28+
public const LLAMA_3_2 = 'ai/llama3.2';
29+
public const LLAMA_3_3 = 'ai/llama3.3';
30+
public const MISTRAL = 'ai/mistral';
31+
public const MISTRAL_NEMO = 'ai/mistral-nemo';
32+
public const PHI_4 = 'ai/phi4';
33+
public const DEEPSEEK_R_1 = 'ai/deepseek-r1-distill-llama';
34+
public const SEED_OSS = 'ai/seed-oss';
35+
public const GPT_OSS = 'ai/gpt-oss';
36+
public const SMOLLM_2 = 'ai/smollm2';
37+
public const SMOLLM_3 = 'ai/smollm3';
38+
39+
private const TOOL_PATTERNS = [
40+
'/./' => [
41+
Capability::INPUT_MESSAGES,
42+
Capability::OUTPUT_TEXT,
43+
Capability::OUTPUT_STRUCTURED,
44+
],
45+
'/^llama\D*3(\D*\d+)/' => [
46+
Capability::TOOL_CALLING,
47+
],
48+
'/^qwen\d(\.\d)?(-coder)?$/' => [
49+
Capability::TOOL_CALLING,
50+
],
51+
'/^(deepseek|mistral|smollm|seed)/' => [
52+
Capability::TOOL_CALLING,
53+
],
54+
];
55+
56+
public function __construct(
57+
string $name = self::SMOLLM_2,
58+
array $options = [],
59+
) {
60+
$capabilities = [];
61+
62+
foreach (self::TOOL_PATTERNS as $pattern => $possibleCapabilities) {
63+
if (1 === preg_match($pattern, $name)) {
64+
foreach ($possibleCapabilities as $capability) {
65+
$capabilities[] = $capability;
66+
}
67+
}
68+
}
69+
70+
parent::__construct($name, $capabilities, $options);
71+
}
72+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\DockerModelRunner\Completions;
13+
14+
use Symfony\AI\Platform\Bridge\DockerModelRunner\Completions;
15+
use Symfony\AI\Platform\Model;
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 Mathieu Santostefano <msantostefano@proton.me>
23+
*/
24+
final readonly class ModelClient implements ModelClientInterface
25+
{
26+
private EventSourceHttpClient $httpClient;
27+
28+
public function __construct(
29+
HttpClientInterface $httpClient,
30+
private string $hostUrl,
31+
) {
32+
$this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient);
33+
}
34+
35+
public function supports(Model $model): bool
36+
{
37+
return $model instanceof Completions;
38+
}
39+
40+
public function request(Model $model, array|string $payload, array $options = []): RawHttpResult
41+
{
42+
return new RawHttpResult($this->httpClient->request('POST', \sprintf('%s/engines/v1/chat/completions', $this->hostUrl), [
43+
'json' => array_merge($options, $payload),
44+
]));
45+
}
46+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\DockerModelRunner\Completions;
13+
14+
use Symfony\AI\Platform\Bridge\DockerModelRunner\Completions;
15+
use Symfony\AI\Platform\Bridge\OpenAi\Gpt\ResultConverter as OpenAiResponseConverter;
16+
use Symfony\AI\Platform\Model;
17+
use Symfony\AI\Platform\Result\RawResultInterface;
18+
use Symfony\AI\Platform\Result\ResultInterface;
19+
use Symfony\AI\Platform\ResultConverterInterface;
20+
21+
/**
22+
* @author Mathieu Santostefano <msantostefano@proton.me>
23+
*/
24+
final class ResultConverter implements ResultConverterInterface
25+
{
26+
public function __construct(
27+
private readonly OpenAiResponseConverter $gptResponseConverter = new OpenAiResponseConverter(),
28+
) {
29+
}
30+
31+
public function supports(Model $model): bool
32+
{
33+
return $model instanceof Completions;
34+
}
35+
36+
public function convert(RawResultInterface $result, array $options = []): ResultInterface
37+
{
38+
return $this->gptResponseConverter->convert($result, $options);
39+
}
40+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\DockerModelRunner;
13+
14+
use Symfony\AI\Platform\Capability;
15+
use Symfony\AI\Platform\Model;
16+
17+
/**
18+
* @author Mathieu Santostefano <msantostefano@proton.me>
19+
*/
20+
class Embeddings extends Model
21+
{
22+
public const NOMIC_EMBED_TEXT = 'ai/nomic-embed-text-v1.5';
23+
public const MXBAI_EMBED_LARGE = 'ai/mxbai-embed-large';
24+
public const EMBEDDING_GEMMA = 'ai/embeddinggemma';
25+
public const GRANITE_EMBEDDING_MULTI = 'ai/granite-embedding-multilingual';
26+
27+
private const TOOL_PATTERNS = [
28+
'/./' => [
29+
Capability::INPUT_MESSAGES,
30+
Capability::OUTPUT_TEXT,
31+
Capability::OUTPUT_STRUCTURED,
32+
],
33+
'/^(nomic).*/' => [
34+
Capability::INPUT_MULTIPLE,
35+
],
36+
];
37+
38+
public function __construct(
39+
string $name = self::NOMIC_EMBED_TEXT,
40+
array $options = [],
41+
) {
42+
$capabilities = [];
43+
44+
foreach (self::TOOL_PATTERNS as $pattern => $possibleCapabilities) {
45+
if (1 === preg_match($pattern, $name)) {
46+
foreach ($possibleCapabilities as $capability) {
47+
$capabilities[] = $capability;
48+
}
49+
}
50+
}
51+
52+
parent::__construct($name, $capabilities, $options);
53+
}
54+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\DockerModelRunner\Embeddings;
13+
14+
use Symfony\AI\Platform\Bridge\DockerModelRunner\Embeddings;
15+
use Symfony\AI\Platform\Model;
16+
use Symfony\AI\Platform\ModelClientInterface;
17+
use Symfony\AI\Platform\Result\RawHttpResult;
18+
use Symfony\Contracts\HttpClient\HttpClientInterface;
19+
20+
/**
21+
* @author Mathieu Santostefano <msantostefano@proton.me>
22+
*/
23+
final readonly class ModelClient implements ModelClientInterface
24+
{
25+
public function __construct(
26+
private HttpClientInterface $httpClient,
27+
private string $hostUrl,
28+
) {
29+
}
30+
31+
public function supports(Model $model): bool
32+
{
33+
return $model instanceof Embeddings;
34+
}
35+
36+
public function request(Model $model, array|string $payload, array $options = []): RawHttpResult
37+
{
38+
return new RawHttpResult($this->httpClient->request('POST', \sprintf('%s/engines/v1/embeddings', $this->hostUrl), [
39+
'json' => array_merge($options, [
40+
'model' => $model->getName(),
41+
'input' => $payload,
42+
]),
43+
]));
44+
}
45+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\DockerModelRunner\Embeddings;
13+
14+
use Symfony\AI\Platform\Bridge\DockerModelRunner\Embeddings;
15+
use Symfony\AI\Platform\Exception\RuntimeException;
16+
use Symfony\AI\Platform\Model;
17+
use Symfony\AI\Platform\Result\RawResultInterface;
18+
use Symfony\AI\Platform\Result\VectorResult;
19+
use Symfony\AI\Platform\ResultConverterInterface;
20+
use Symfony\AI\Platform\Vector\Vector;
21+
22+
/**
23+
* @author Mathieu Santostefano <msantostefano@proton.me>
24+
*/
25+
final class ResultConverter implements ResultConverterInterface
26+
{
27+
public function supports(Model $model): bool
28+
{
29+
return $model instanceof Embeddings;
30+
}
31+
32+
public function convert(RawResultInterface $result, array $options = []): VectorResult
33+
{
34+
$data = $result->getData();
35+
36+
if (!isset($data['data'])) {
37+
throw new RuntimeException('Response does not contain data.');
38+
}
39+
40+
return new VectorResult(
41+
...array_map(
42+
static fn (array $item): Vector => new Vector($item['embedding']),
43+
$data['data']
44+
),
45+
);
46+
}
47+
}

0 commit comments

Comments
 (0)