Skip to content

Commit 718ee9d

Browse files
committed
refactor(platform): introduce ElevenLabsApiCatalog
1 parent 7adba8f commit 718ee9d

File tree

8 files changed

+294
-72
lines changed

8 files changed

+294
-72
lines changed

src/ai-bundle/config/options.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@
9898
->defaultValue('http_client')
9999
->info('Service ID of the HTTP client to use')
100100
->end()
101+
->booleanNode('api_catalog')
102+
->info('If set, the ElevenLabs API will be used to build the catalog and retrieve models information, using this option leads to additional HTTP calls')
103+
->end()
101104
->end()
102105
->end()
103106
->arrayNode('gemini')

src/ai-bundle/config/services.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
use Symfony\AI\Platform\Contract;
6363
use Symfony\AI\Platform\Contract\JsonSchema\DescriptionParser;
6464
use Symfony\AI\Platform\Contract\JsonSchema\Factory as SchemaFactory;
65+
use Symfony\AI\Platform\ModelCatalog\ModelCatalogInterface;
6566
use Symfony\AI\Platform\Serializer\StructuredOutputSerializer;
6667
use Symfony\AI\Platform\StructuredOutput\PlatformSubscriber;
6768
use Symfony\AI\Platform\StructuredOutput\ResponseFormatFactory;
@@ -98,6 +99,8 @@
9899
->set('ai.platform.model_catalog.deepseek', DeepSeekModelCatalog::class)
99100
->set('ai.platform.model_catalog.dockermodelrunner', DockerModelRunnerModelCatalog::class)
100101
->set('ai.platform.model_catalog.elevenlabs', ElevenLabsModelCatalog::class)
102+
->lazy(true)
103+
->tag('proxy', ['interface' => ModelCatalogInterface::class])
101104
->set('ai.platform.model_catalog.gemini', GeminiModelCatalog::class)
102105
->set('ai.platform.model_catalog.huggingface', HuggingFaceModelCatalog::class)
103106
->set('ai.platform.model_catalog.lmstudio', LmStudioModelCatalog::class)

src/ai-bundle/src/AiBundle.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
use Symfony\AI\Platform\Bridge\Cerebras\PlatformFactory as CerebrasPlatformFactory;
5858
use Symfony\AI\Platform\Bridge\DeepSeek\PlatformFactory as DeepSeekPlatformFactory;
5959
use Symfony\AI\Platform\Bridge\DockerModelRunner\PlatformFactory as DockerModelRunnerPlatformFactory;
60+
use Symfony\AI\Platform\Bridge\ElevenLabs\ElevenLabsApiCatalog;
6061
use Symfony\AI\Platform\Bridge\ElevenLabs\PlatformFactory as ElevenLabsPlatformFactory;
6162
use Symfony\AI\Platform\Bridge\Gemini\PlatformFactory as GeminiPlatformFactory;
6263
use Symfony\AI\Platform\Bridge\Generic\PlatformFactory as GenericPlatformFactory;
@@ -75,6 +76,7 @@
7576
use Symfony\AI\Platform\Capability;
7677
use Symfony\AI\Platform\Exception\RuntimeException;
7778
use Symfony\AI\Platform\Message\Content\File;
79+
use Symfony\AI\Platform\ModelCatalog\ModelCatalogInterface;
7880
use Symfony\AI\Platform\ModelClientInterface;
7981
use Symfony\AI\Platform\Platform;
8082
use Symfony\AI\Platform\PlatformInterface;
@@ -431,6 +433,19 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
431433
}
432434

433435
if ('elevenlabs' === $type) {
436+
if (\array_key_exists('api_catalog', $platform) && $platform['api_catalog']) {
437+
$catalogDefinition = (new Definition(ElevenLabsApiCatalog::class))
438+
->setLazy(true)
439+
->setArguments([
440+
new Reference($platform['http_client']),
441+
$platform['api_key'],
442+
$platform['host'],
443+
])
444+
->addTag('proxy', ['interface' => ModelCatalogInterface::class]);
445+
446+
$container->setDefinition('ai.platform.model_catalog.'.$type, $catalogDefinition);
447+
}
448+
434449
$definition = (new Definition(Platform::class))
435450
->setFactory(ElevenLabsPlatformFactory::class.'::create')
436451
->setLazy(true)
@@ -648,7 +663,9 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
648663
->setArguments([
649664
$platform['host_url'],
650665
new Reference($platform['http_client']),
651-
]);
666+
])
667+
->addTag('proxy', ['interface' => ModelCatalogInterface::class])
668+
;
652669

653670
$container->setDefinition('ai.platform.model_catalog.ollama', $catalogDefinition);
654671
}

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

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,13 @@
2727
use Symfony\AI\Chat\ChatInterface;
2828
use Symfony\AI\Chat\ManagedStoreInterface as ManagedMessageStoreInterface;
2929
use Symfony\AI\Chat\MessageStoreInterface;
30-
use Symfony\AI\Platform\Bridge\ElevenLabs\PlatformFactory;
30+
use Symfony\AI\Platform\Bridge\ElevenLabs\ElevenLabsApiCatalog;
31+
use Symfony\AI\Platform\Bridge\ElevenLabs\ModelCatalog as ElevenLabsModelCatalog;
32+
use Symfony\AI\Platform\Bridge\ElevenLabs\PlatformFactory as ElevenLabsPlatformFactory;
3133
use Symfony\AI\Platform\Bridge\Ollama\OllamaApiCatalog;
3234
use Symfony\AI\Platform\Capability;
3335
use Symfony\AI\Platform\Model;
36+
use Symfony\AI\Platform\ModelCatalog\ModelCatalogInterface;
3437
use Symfony\AI\Platform\PlatformInterface;
3538
use Symfony\AI\Store\Bridge\AzureSearch\SearchStore as AzureStore;
3639
use Symfony\AI\Store\Bridge\Cache\Store as CacheStore;
@@ -3785,7 +3788,7 @@ public function testElevenLabsPlatformCanBeRegistered()
37853788
$definition = $container->getDefinition('ai.platform.elevenlabs');
37863789

37873790
$this->assertTrue($definition->isLazy());
3788-
$this->assertSame([PlatformFactory::class, 'create'], $definition->getFactory());
3791+
$this->assertSame([ElevenLabsPlatformFactory::class, 'create'], $definition->getFactory());
37893792

37903793
$this->assertCount(6, $definition->getArguments());
37913794
$this->assertSame('foo', $definition->getArgument(0));
@@ -3805,6 +3808,14 @@ public function testElevenLabsPlatformCanBeRegistered()
38053808

38063809
$this->assertTrue($container->hasAlias('Symfony\AI\Platform\PlatformInterface $elevenlabs'));
38073810
$this->assertTrue($container->hasAlias('Symfony\AI\Platform\PlatformInterface'));
3811+
3812+
$modelCatalogDefinition = $container->getDefinition('ai.platform.model_catalog.elevenlabs');
3813+
3814+
$this->assertSame(ElevenLabsModelCatalog::class, $modelCatalogDefinition->getClass());
3815+
$this->assertTrue($modelCatalogDefinition->isLazy());
3816+
3817+
$this->assertTrue($modelCatalogDefinition->hasTag('proxy'));
3818+
$this->assertSame([['interface' => ModelCatalogInterface::class]], $modelCatalogDefinition->getTag('proxy'));
38083819
}
38093820

38103821
public function testElevenLabsPlatformWithCustomEndpointCanBeRegistered()
@@ -3825,7 +3836,7 @@ public function testElevenLabsPlatformWithCustomEndpointCanBeRegistered()
38253836
$definition = $container->getDefinition('ai.platform.elevenlabs');
38263837

38273838
$this->assertTrue($definition->isLazy());
3828-
$this->assertSame([PlatformFactory::class, 'create'], $definition->getFactory());
3839+
$this->assertSame([ElevenLabsPlatformFactory::class, 'create'], $definition->getFactory());
38293840

38303841
$this->assertCount(6, $definition->getArguments());
38313842
$this->assertSame('foo', $definition->getArgument(0));
@@ -3845,6 +3856,14 @@ public function testElevenLabsPlatformWithCustomEndpointCanBeRegistered()
38453856

38463857
$this->assertTrue($container->hasAlias('Symfony\AI\Platform\PlatformInterface $elevenlabs'));
38473858
$this->assertTrue($container->hasAlias('Symfony\AI\Platform\PlatformInterface'));
3859+
3860+
$modelCatalogDefinition = $container->getDefinition('ai.platform.model_catalog.elevenlabs');
3861+
3862+
$this->assertSame(ElevenLabsModelCatalog::class, $modelCatalogDefinition->getClass());
3863+
$this->assertTrue($modelCatalogDefinition->isLazy());
3864+
3865+
$this->assertTrue($modelCatalogDefinition->hasTag('proxy'));
3866+
$this->assertSame([['interface' => ModelCatalogInterface::class]], $modelCatalogDefinition->getTag('proxy'));
38483867
}
38493868

38503869
public function testElevenLabsPlatformWithCustomHttpClientCanBeRegistered()
@@ -3865,7 +3884,7 @@ public function testElevenLabsPlatformWithCustomHttpClientCanBeRegistered()
38653884
$definition = $container->getDefinition('ai.platform.elevenlabs');
38663885

38673886
$this->assertTrue($definition->isLazy());
3868-
$this->assertSame([PlatformFactory::class, 'create'], $definition->getFactory());
3887+
$this->assertSame([ElevenLabsPlatformFactory::class, 'create'], $definition->getFactory());
38693888

38703889
$this->assertCount(6, $definition->getArguments());
38713890
$this->assertSame('foo', $definition->getArgument(0));
@@ -3885,6 +3904,68 @@ public function testElevenLabsPlatformWithCustomHttpClientCanBeRegistered()
38853904

38863905
$this->assertTrue($container->hasAlias('Symfony\AI\Platform\PlatformInterface $elevenlabs'));
38873906
$this->assertTrue($container->hasAlias('Symfony\AI\Platform\PlatformInterface'));
3907+
3908+
$modelCatalogDefinition = $container->getDefinition('ai.platform.model_catalog.elevenlabs');
3909+
3910+
$this->assertSame(ElevenLabsModelCatalog::class, $modelCatalogDefinition->getClass());
3911+
$this->assertTrue($modelCatalogDefinition->isLazy());
3912+
3913+
$this->assertTrue($modelCatalogDefinition->hasTag('proxy'));
3914+
$this->assertSame([['interface' => ModelCatalogInterface::class]], $modelCatalogDefinition->getTag('proxy'));
3915+
}
3916+
3917+
public function testElevenLabsPlatformWithApiCatalogCanBeRegistered()
3918+
{
3919+
$container = $this->buildContainer([
3920+
'ai' => [
3921+
'platform' => [
3922+
'elevenlabs' => [
3923+
'api_key' => 'foo',
3924+
'api_catalog' => true,
3925+
],
3926+
],
3927+
],
3928+
]);
3929+
3930+
$this->assertTrue($container->hasDefinition('ai.platform.elevenlabs'));
3931+
$this->assertTrue($container->hasDefinition('ai.platform.model_catalog.elevenlabs'));
3932+
3933+
$definition = $container->getDefinition('ai.platform.elevenlabs');
3934+
3935+
$this->assertTrue($definition->isLazy());
3936+
$this->assertSame([ElevenLabsPlatformFactory::class, 'create'], $definition->getFactory());
3937+
3938+
$this->assertCount(6, $definition->getArguments());
3939+
$this->assertSame('foo', $definition->getArgument(0));
3940+
$this->assertSame('https://api.elevenlabs.io/v1', $definition->getArgument(1));
3941+
$this->assertInstanceOf(Reference::class, $definition->getArgument(2));
3942+
$this->assertSame('http_client', (string) $definition->getArgument(2));
3943+
$this->assertInstanceOf(Reference::class, $definition->getArgument(3));
3944+
$this->assertSame('ai.platform.model_catalog.elevenlabs', (string) $definition->getArgument(3));
3945+
$this->assertNull($definition->getArgument(4));
3946+
$this->assertInstanceOf(Reference::class, $definition->getArgument(5));
3947+
$this->assertSame('event_dispatcher', (string) $definition->getArgument(5));
3948+
3949+
$this->assertTrue($definition->hasTag('proxy'));
3950+
$this->assertSame([['interface' => PlatformInterface::class]], $definition->getTag('proxy'));
3951+
$this->assertTrue($definition->hasTag('ai.platform'));
3952+
$this->assertSame([['name' => 'elevenlabs']], $definition->getTag('ai.platform'));
3953+
3954+
$this->assertTrue($container->hasAlias('Symfony\AI\Platform\PlatformInterface $elevenlabs'));
3955+
$this->assertTrue($container->hasAlias('Symfony\AI\Platform\PlatformInterface'));
3956+
3957+
$modelCatalogDefinition = $container->getDefinition('ai.platform.model_catalog.elevenlabs');
3958+
3959+
$this->assertSame(ElevenLabsApiCatalog::class, $modelCatalogDefinition->getClass());
3960+
$this->assertTrue($modelCatalogDefinition->isLazy());
3961+
$this->assertCount(3, $modelCatalogDefinition->getArguments());
3962+
$this->assertInstanceOf(Reference::class, $modelCatalogDefinition->getArgument(0));
3963+
$this->assertSame('http_client', (string) $modelCatalogDefinition->getArgument(0));
3964+
$this->assertSame('foo', $modelCatalogDefinition->getArgument(1));
3965+
$this->assertSame('https://api.elevenlabs.io/v1', $modelCatalogDefinition->getArgument(2));
3966+
3967+
$this->assertTrue($modelCatalogDefinition->hasTag('proxy'));
3968+
$this->assertSame([['interface' => ModelCatalogInterface::class]], $modelCatalogDefinition->getTag('proxy'));
38883969
}
38893970

38903971
#[TestDox('Token usage processor tags use the correct agent ID')]
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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\ElevenLabs;
13+
14+
use Symfony\AI\Platform\Capability;
15+
use Symfony\AI\Platform\Exception\InvalidArgumentException;
16+
use Symfony\AI\Platform\ModelCatalog\ModelCatalogInterface;
17+
use Symfony\Contracts\HttpClient\HttpClientInterface;
18+
19+
/**
20+
* @author Guillaume Loulier <[email protected]>
21+
*/
22+
final class ElevenLabsApiCatalog implements ModelCatalogInterface
23+
{
24+
public function __construct(
25+
private readonly HttpClientInterface $httpClient,
26+
#[\SensitiveParameter] private readonly string $apiKey,
27+
private readonly string $hostUrl = 'https://api.elevenlabs.io/v1',
28+
) {
29+
}
30+
31+
public function getModel(string $modelName): ElevenLabs
32+
{
33+
$models = $this->getModels();
34+
35+
if (!\array_key_exists($modelName, $models)) {
36+
throw new InvalidArgumentException(\sprintf('The model "%s" cannot be retrieve from the API.', $modelName));
37+
}
38+
39+
return new ElevenLabs($modelName, $models[$modelName]['capabilities']);
40+
}
41+
42+
public function getModels(): array
43+
{
44+
$response = $this->httpClient->request('GET', \sprintf('%s/models', $this->hostUrl), [
45+
'headers' => [
46+
'x-api-key' => $this->apiKey,
47+
],
48+
]);
49+
50+
$models = $response->toArray();
51+
52+
$capabilities = fn (array $model): array => match (true) {
53+
$model['can_do_text_to_speech'] => [
54+
Capability::TEXT_TO_SPEECH,
55+
Capability::INPUT_TEXT,
56+
Capability::OUTPUT_AUDIO,
57+
],
58+
$model['can_do_voice_conversation'] => [
59+
Capability::SPEECH_TO_TEXT,
60+
Capability::INPUT_AUDIO,
61+
Capability::OUTPUT_TEXT,
62+
],
63+
default => throw new InvalidArgumentException(\sprintf('The model "%s" is not supported, please check the ElevenLabs API.', $model['name'])),
64+
};
65+
66+
return array_merge(...array_map(
67+
static fn (array $model): array => [
68+
$model['name'] => [
69+
'class' => ElevenLabs::class,
70+
'capabilities' => $capabilities($model),
71+
],
72+
],
73+
$models,
74+
));
75+
}
76+
}

src/platform/src/Bridge/ElevenLabs/ElevenLabsClient.php

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,14 @@ public function request(Model $model, array|string $payload, array $options = []
4646
return $this->doSpeechToTextRequest($model, $payload);
4747
}
4848

49-
$capabilities = $this->retrieveCapabilities($model);
50-
51-
if (!$capabilities['can_do_text_to_speech']) {
52-
throw new InvalidArgumentException(\sprintf('The model "%s" does not support text-to-speech.', $model->getName()));
49+
if ($model->supports(Capability::TEXT_TO_SPEECH)) {
50+
return $this->doTextToSpeechRequest($model, $payload, [
51+
...$options,
52+
...$model->getOptions(),
53+
]);
5354
}
5455

55-
return $this->doTextToSpeechRequest($model, $payload, array_merge($options, $model->getOptions()));
56+
throw new InvalidArgumentException(\sprintf('The model "%s" does not support text-to-speech or speech-to-text, please check the model information.', $model->getName()));
5657
}
5758

5859
/**
@@ -102,26 +103,4 @@ private function doTextToSpeechRequest(Model $model, array|string $payload, arra
102103
],
103104
]));
104105
}
105-
106-
/**
107-
* @return array<string, mixed>
108-
*/
109-
private function retrieveCapabilities(Model $model): array
110-
{
111-
$capabilityResponse = $this->httpClient->request('GET', \sprintf('%s/models', $this->hostUrl), [
112-
'headers' => [
113-
'xi-api-key' => $this->apiKey,
114-
],
115-
]);
116-
117-
$models = $capabilityResponse->toArray();
118-
119-
$currentModelConfiguration = array_filter($models, static fn (array $information): bool => $information['model_id'] === $model->getName());
120-
121-
if ([] === $currentModelConfiguration) {
122-
throw new InvalidArgumentException('The model information could not be retrieved from the ElevenLabs API. Your model might not be supported. Try to use another one.');
123-
}
124-
125-
return reset($currentModelConfiguration);
126-
}
127106
}

0 commit comments

Comments
 (0)