Skip to content

Commit 395156d

Browse files
committed
feature #116 [Platform] Add lmstudio (lubiana)
This PR was squashed before being merged into the main branch. Discussion ---------- [Platform] Add lmstudio | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Docs? | yes | Issues | | License | MIT Add Support for [LM Studio](https://lmstudio.ai/docs/app) as Platform. As it is mostly OpenAI compatible I picked the parts from that platform and copied them into the LM Studio namespace with some adjustments. What I am unsure about is if this is the best way to implement this. Basically LM Studio ist mostly API compatible with the OpenAI API, so instead of copying the relevant parts from the OpenAI platform into the LM Studio one another approach could be to to allow setting a different url for the OpenAI platform. But in that case the model classes there might need to be not final, so a user can extend those and add capabilities for those models. On the other hand it might be better to keep this approach, as the OpenAI and LM Studio APis might diverge further in the future. As this is my first contribution to symfony i would be happy to get some feedback. I am especially unsure if my approach of wrapping the GPT Responseconverter in `src/platform/src/Bridge/LMStudio/Completions/ResponseConverter.php` is a wise choice. Commits ------- 7ec20d8 [Platform] Add lmstudio
2 parents 8ffcb19 + 7ec20d8 commit 395156d

File tree

19 files changed

+734
-1
lines changed

19 files changed

+734
-1
lines changed

examples/.env

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,6 @@ MARIADB_URI=pdo-mysql://[email protected]:3309/my_database
7878
# Meilisearch
7979
MEILISEARCH_HOST=http://127.0.0.1:7700
8080
MEILISEARCH_API_KEY=changeMe
81+
82+
# For using LMStudio
83+
LMSTUDIO_HOST_URL=http://127.0.0.1:1234

examples/lmstudio/chat.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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\LMStudio\Completions;
14+
use Symfony\AI\Platform\Bridge\LMStudio\PlatformFactory;
15+
use Symfony\AI\Platform\Message\Message;
16+
use Symfony\AI\Platform\Message\MessageBag;
17+
use Symfony\Component\Dotenv\Dotenv;
18+
19+
require_once dirname(__DIR__).'/vendor/autoload.php';
20+
(new Dotenv())->loadEnv(dirname(__DIR__).'/.env');
21+
22+
if (!isset($_SERVER['LMSTUDIO_HOST_URL'])) {
23+
echo 'Please set the LMSTUDIO_HOST_URL environment variable.'.\PHP_EOL;
24+
exit(1);
25+
}
26+
27+
$platform = PlatformFactory::create($_SERVER['LMSTUDIO_HOST_URL']);
28+
$model = new Completions('gemma-3-4b-it-qat');
29+
30+
$agent = new Agent($platform, $model);
31+
$messages = new MessageBag(
32+
Message::forSystem('You are a pirate and you write funny.'),
33+
Message::ofUser('What is the Symfony framework?'),
34+
);
35+
$response = $agent->call($messages, [
36+
'max_tokens' => 500, // specific options just for this call
37+
]);
38+
39+
echo $response->getContent().\PHP_EOL;
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 <[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\LMStudio\Completions;
14+
use Symfony\AI\Platform\Bridge\LMStudio\PlatformFactory;
15+
use Symfony\AI\Platform\Capability;
16+
use Symfony\AI\Platform\Message\Content\Image;
17+
use Symfony\AI\Platform\Message\Message;
18+
use Symfony\AI\Platform\Message\MessageBag;
19+
use Symfony\Component\Dotenv\Dotenv;
20+
21+
require_once dirname(__DIR__).'/vendor/autoload.php';
22+
(new Dotenv())->loadEnv(dirname(__DIR__).'/.env');
23+
24+
if (!isset($_SERVER['LMSTUDIO_HOST_URL'])) {
25+
echo 'Please set the LMSTUDIO_HOST_URL environment variable.'.\PHP_EOL;
26+
exit(1);
27+
}
28+
29+
$platform = PlatformFactory::create($_SERVER['LMSTUDIO_HOST_URL']);
30+
$model = new Completions(
31+
name: 'gemma-3-4b-it-qat',
32+
capabilities: [...Completions::DEFAULT_CAPABILITIES, Capability::INPUT_IMAGE]
33+
);
34+
35+
$agent = new Agent($platform, $model);
36+
$messages = new MessageBag(
37+
Message::forSystem('You are an image analyzer bot that helps identify the content of images.'),
38+
Message::ofUser(
39+
'Describe the image as a comedian would do it.',
40+
Image::fromFile(dirname(__DIR__, 2).'/fixtures/image.jpg'),
41+
),
42+
);
43+
$response = $agent->call($messages);
44+
45+
echo $response->getContent().\PHP_EOL;

src/ai-bundle/config/options.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@
5757
->scalarNode('api_key')->isRequired()->end()
5858
->end()
5959
->end()
60+
->arrayNode('lmstudio')
61+
->children()
62+
->scalarNode('host_url')->defaultValue('http://127.0.0.1:1234')->end()
63+
->end()
64+
->end()
6065
->end()
6166
->end()
6267
->arrayNode('agent')

src/ai-bundle/src/AIBundle.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use Symfony\AI\Platform\Bridge\Anthropic\PlatformFactory as AnthropicPlatformFactory;
3333
use Symfony\AI\Platform\Bridge\Azure\OpenAI\PlatformFactory as AzureOpenAIPlatformFactory;
3434
use Symfony\AI\Platform\Bridge\Google\PlatformFactory as GooglePlatformFactory;
35+
use Symfony\AI\Platform\Bridge\LMStudio\PlatformFactory as LMStudioPlatformFactory;
3536
use Symfony\AI\Platform\Bridge\Mistral\PlatformFactory as MistralPlatformFactory;
3637
use Symfony\AI\Platform\Bridge\OpenAI\PlatformFactory as OpenAIPlatformFactory;
3738
use Symfony\AI\Platform\Bridge\OpenRouter\PlatformFactory as OpenRouterPlatformFactory;
@@ -256,6 +257,21 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
256257
return;
257258
}
258259

260+
if ('lmstudio' === $type) {
261+
$platformId = 'symfony_ai.platform.lmstudio';
262+
$definition = (new Definition(Platform::class))
263+
->setFactory(LMStudioPlatformFactory::class.'::create')
264+
->setAutowired(true)
265+
->setLazy(true)
266+
->addTag('proxy', ['interface' => PlatformInterface::class])
267+
->setArguments(['$hostUrl' => $platform['host_url']])
268+
->addTag('symfony_ai.platform');
269+
270+
$container->setDefinition($platformId, $definition);
271+
272+
return;
273+
}
274+
259275
throw new InvalidArgumentException(\sprintf('Platform "%s" is not supported for configuration via bundle at this point.', $type));
260276
}
261277

src/ai-bundle/tests/DependencyInjection/AIBundleTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ private function getFullConfig(): array
7272
'openrouter' => [
7373
'api_key' => 'openrouter_key_full',
7474
],
75+
'lmstudio' => [
76+
'host_url' => 'http://127.0.0.1:1234',
77+
],
7578
],
7679
'agent' => [
7780
'my_chat_agent' => [

src/platform/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ CHANGELOG
2020
- Voyage AI (specialized embeddings)
2121
- HuggingFace (extensive model support with multiple tasks)
2222
- TransformersPHP (local PHP-based transformer models)
23+
- LM Studio (local model hosting)
2324
* Add comprehensive message system with role-based messaging:
2425
- `UserMessage` for user inputs with multi-modal content
2526
- `SystemMessage` for system instructions
@@ -54,4 +55,4 @@ CHANGELOG
5455
* Add temperature and parameter controls
5556
* Add exception handling with specific error types
5657
* Add support for embeddings generation across multiple providers
57-
* Add response promises for async operations
58+
* Add response promises for async operations

src/platform/doc/index.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ usually defined by the specific models and their documentation.
8686
* **Other Models**
8787
* `OpenAI's Dall·E`_ with `OpenAI`_ as Platform
8888
* `OpenAI's Whisper`_ with `OpenAI`_ and `Azure`_ as Platform
89+
* `LM Studio Catalog`_ and `HuggingFace`_ Models with `LM Studio`_ as Platform.
8990
* All models provided by `HuggingFace`_ can be listed with a command in the examples folder,
9091
and also filtered, e.g. ``php examples/huggingface/_model-listing.php --provider=hf-inference --task=object-detection``
9192

@@ -332,3 +333,5 @@ which can be useful to speed up the processing::
332333
.. _`Embeddings with Mistral`: https://github.com/symfony/ai/blob/main/examples/mistral/embeddings.php
333334
.. _`Parallel GPT Calls`: https://github.com/symfony/ai/blob/main/examples/misc/parallel-chat-gpt.php
334335
.. _`Parallel Embeddings Calls`: https://github.com/symfony/ai/blob/main/examples/misc/parallel-embeddings.php
336+
.. _`LM Studio`: https://lmstudio.ai/
337+
.. _`LM Studio Catalog`: https://lmstudio.ai/models
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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\LMStudio;
13+
14+
use Symfony\AI\Platform\Capability;
15+
use Symfony\AI\Platform\Model;
16+
17+
/**
18+
* @author André Lubian <[email protected]>
19+
*/
20+
class Completions extends Model
21+
{
22+
public const DEFAULT_CAPABILITIES = [
23+
Capability::INPUT_MESSAGES,
24+
Capability::OUTPUT_TEXT,
25+
Capability::OUTPUT_STREAMING,
26+
];
27+
28+
public function __construct(
29+
string $name,
30+
array $options = ['temperature' => 0.7],
31+
array $capabilities = self::DEFAULT_CAPABILITIES,
32+
) {
33+
parent::__construct($name, $capabilities, $options);
34+
}
35+
}
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 <[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\LMStudio\Completions;
13+
14+
use Symfony\AI\Platform\Bridge\LMStudio\Completions;
15+
use Symfony\AI\Platform\Model;
16+
use Symfony\AI\Platform\ModelClientInterface as PlatformResponseFactory;
17+
use Symfony\Component\HttpClient\EventSourceHttpClient;
18+
use Symfony\Contracts\HttpClient\HttpClientInterface;
19+
use Symfony\Contracts\HttpClient\ResponseInterface;
20+
21+
/**
22+
* @author André Lubian <[email protected]>
23+
*/
24+
final readonly class ModelClient implements PlatformResponseFactory
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 = []): ResponseInterface
41+
{
42+
return $this->httpClient->request('POST', \sprintf('%s/v1/chat/completions', $this->hostUrl), [
43+
'json' => array_merge($options, $payload),
44+
]);
45+
}
46+
}

0 commit comments

Comments
 (0)