Skip to content

Commit 2a011bf

Browse files
committed
refactor(platform): introduce ElevenLabsApiCatalog
1 parent 92134c3 commit 2a011bf

File tree

9 files changed

+290
-1588
lines changed

9 files changed

+290
-1588
lines changed

demo/config/reference.php

Lines changed: 0 additions & 1520 deletions
This file was deleted.

src/ai-bundle/config/options.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@
110110
->defaultValue('http_client')
111111
->info('Service ID of the HTTP client to use')
112112
->end()
113+
->booleanNode('api_catalog')
114+
->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')
115+
->end()
113116
->end()
114117
->end()
115118
->arrayNode('gemini')

src/ai-bundle/config/services.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
use Symfony\AI\Platform\Contract;
6464
use Symfony\AI\Platform\Contract\JsonSchema\DescriptionParser;
6565
use Symfony\AI\Platform\Contract\JsonSchema\Factory as SchemaFactory;
66+
use Symfony\AI\Platform\ModelCatalog\ModelCatalogInterface;
6667
use Symfony\AI\Platform\Serializer\StructuredOutputSerializer;
6768
use Symfony\AI\Platform\StructuredOutput\PlatformSubscriber;
6869
use Symfony\AI\Platform\StructuredOutput\ResponseFormatFactory;
@@ -100,6 +101,8 @@
100101
->set('ai.platform.model_catalog.deepseek', DeepSeekModelCatalog::class)
101102
->set('ai.platform.model_catalog.dockermodelrunner', DockerModelRunnerModelCatalog::class)
102103
->set('ai.platform.model_catalog.elevenlabs', ElevenLabsModelCatalog::class)
104+
->lazy(true)
105+
->tag('proxy', ['interface' => ModelCatalogInterface::class])
103106
->set('ai.platform.model_catalog.gemini', GeminiModelCatalog::class)
104107
->set('ai.platform.model_catalog.huggingface', HuggingFaceModelCatalog::class)
105108
->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
@@ -58,6 +58,7 @@
5858
use Symfony\AI\Platform\Bridge\Decart\PlatformFactory as DecartPlatformFactory;
5959
use Symfony\AI\Platform\Bridge\DeepSeek\PlatformFactory as DeepSeekPlatformFactory;
6060
use Symfony\AI\Platform\Bridge\DockerModelRunner\PlatformFactory as DockerModelRunnerPlatformFactory;
61+
use Symfony\AI\Platform\Bridge\ElevenLabs\ElevenLabsApiCatalog;
6162
use Symfony\AI\Platform\Bridge\ElevenLabs\PlatformFactory as ElevenLabsPlatformFactory;
6263
use Symfony\AI\Platform\Bridge\Gemini\PlatformFactory as GeminiPlatformFactory;
6364
use Symfony\AI\Platform\Bridge\Generic\PlatformFactory as GenericPlatformFactory;
@@ -76,6 +77,7 @@
7677
use Symfony\AI\Platform\Capability;
7778
use Symfony\AI\Platform\Exception\RuntimeException;
7879
use Symfony\AI\Platform\Message\Content\File;
80+
use Symfony\AI\Platform\ModelCatalog\ModelCatalogInterface;
7981
use Symfony\AI\Platform\ModelClientInterface;
8082
use Symfony\AI\Platform\Platform;
8183
use Symfony\AI\Platform\PlatformInterface;
@@ -453,6 +455,19 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
453455
}
454456

455457
if ('elevenlabs' === $type) {
458+
if (\array_key_exists('api_catalog', $platform) && $platform['api_catalog']) {
459+
$catalogDefinition = (new Definition(ElevenLabsApiCatalog::class))
460+
->setLazy(true)
461+
->setArguments([
462+
new Reference($platform['http_client']),
463+
$platform['api_key'],
464+
$platform['host'],
465+
])
466+
->addTag('proxy', ['interface' => ModelCatalogInterface::class]);
467+
468+
$container->setDefinition('ai.platform.model_catalog.'.$type, $catalogDefinition);
469+
}
470+
456471
$definition = (new Definition(Platform::class))
457472
->setFactory(ElevenLabsPlatformFactory::class.'::create')
458473
->setLazy(true)
@@ -670,7 +685,9 @@ private function processPlatformConfig(string $type, array $platform, ContainerB
670685
->setArguments([
671686
$platform['host_url'],
672687
new Reference($platform['http_client']),
673-
]);
688+
])
689+
->addTag('proxy', ['interface' => ModelCatalogInterface::class])
690+
;
674691

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

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

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,13 @@
2828
use Symfony\AI\Chat\ManagedStoreInterface as ManagedMessageStoreInterface;
2929
use Symfony\AI\Chat\MessageStoreInterface;
3030
use Symfony\AI\Platform\Bridge\Decart\PlatformFactory as DecartPlatformFactory;
31+
use Symfony\AI\Platform\Bridge\ElevenLabs\ElevenLabsApiCatalog;
32+
use Symfony\AI\Platform\Bridge\ElevenLabs\ModelCatalog as ElevenLabsModelCatalog;
3133
use Symfony\AI\Platform\Bridge\ElevenLabs\PlatformFactory as ElevenLabsPlatformFactory;
3234
use Symfony\AI\Platform\Bridge\Ollama\OllamaApiCatalog;
3335
use Symfony\AI\Platform\Capability;
3436
use Symfony\AI\Platform\Model;
37+
use Symfony\AI\Platform\ModelCatalog\ModelCatalogInterface;
3538
use Symfony\AI\Platform\PlatformInterface;
3639
use Symfony\AI\Store\Bridge\AzureSearch\SearchStore as AzureStore;
3740
use Symfony\AI\Store\Bridge\Cache\Store as CacheStore;
@@ -3849,6 +3852,14 @@ public function testElevenLabsPlatformCanBeRegistered()
38493852

38503853
$this->assertTrue($container->hasAlias('Symfony\AI\Platform\PlatformInterface $elevenlabs'));
38513854
$this->assertTrue($container->hasAlias('Symfony\AI\Platform\PlatformInterface'));
3855+
3856+
$modelCatalogDefinition = $container->getDefinition('ai.platform.model_catalog.elevenlabs');
3857+
3858+
$this->assertSame(ElevenLabsModelCatalog::class, $modelCatalogDefinition->getClass());
3859+
$this->assertTrue($modelCatalogDefinition->isLazy());
3860+
3861+
$this->assertTrue($modelCatalogDefinition->hasTag('proxy'));
3862+
$this->assertSame([['interface' => ModelCatalogInterface::class]], $modelCatalogDefinition->getTag('proxy'));
38523863
}
38533864

38543865
public function testElevenLabsPlatformWithCustomEndpointCanBeRegistered()
@@ -3889,6 +3900,14 @@ public function testElevenLabsPlatformWithCustomEndpointCanBeRegistered()
38893900

38903901
$this->assertTrue($container->hasAlias('Symfony\AI\Platform\PlatformInterface $elevenlabs'));
38913902
$this->assertTrue($container->hasAlias('Symfony\AI\Platform\PlatformInterface'));
3903+
3904+
$modelCatalogDefinition = $container->getDefinition('ai.platform.model_catalog.elevenlabs');
3905+
3906+
$this->assertSame(ElevenLabsModelCatalog::class, $modelCatalogDefinition->getClass());
3907+
$this->assertTrue($modelCatalogDefinition->isLazy());
3908+
3909+
$this->assertTrue($modelCatalogDefinition->hasTag('proxy'));
3910+
$this->assertSame([['interface' => ModelCatalogInterface::class]], $modelCatalogDefinition->getTag('proxy'));
38923911
}
38933912

38943913
public function testElevenLabsPlatformWithCustomHttpClientCanBeRegistered()
@@ -3929,6 +3948,68 @@ public function testElevenLabsPlatformWithCustomHttpClientCanBeRegistered()
39293948

39303949
$this->assertTrue($container->hasAlias('Symfony\AI\Platform\PlatformInterface $elevenlabs'));
39313950
$this->assertTrue($container->hasAlias('Symfony\AI\Platform\PlatformInterface'));
3951+
3952+
$modelCatalogDefinition = $container->getDefinition('ai.platform.model_catalog.elevenlabs');
3953+
3954+
$this->assertSame(ElevenLabsModelCatalog::class, $modelCatalogDefinition->getClass());
3955+
$this->assertTrue($modelCatalogDefinition->isLazy());
3956+
3957+
$this->assertTrue($modelCatalogDefinition->hasTag('proxy'));
3958+
$this->assertSame([['interface' => ModelCatalogInterface::class]], $modelCatalogDefinition->getTag('proxy'));
3959+
}
3960+
3961+
public function testElevenLabsPlatformWithApiCatalogCanBeRegistered()
3962+
{
3963+
$container = $this->buildContainer([
3964+
'ai' => [
3965+
'platform' => [
3966+
'elevenlabs' => [
3967+
'api_key' => 'foo',
3968+
'api_catalog' => true,
3969+
],
3970+
],
3971+
],
3972+
]);
3973+
3974+
$this->assertTrue($container->hasDefinition('ai.platform.elevenlabs'));
3975+
$this->assertTrue($container->hasDefinition('ai.platform.model_catalog.elevenlabs'));
3976+
3977+
$definition = $container->getDefinition('ai.platform.elevenlabs');
3978+
3979+
$this->assertTrue($definition->isLazy());
3980+
$this->assertSame([ElevenLabsPlatformFactory::class, 'create'], $definition->getFactory());
3981+
3982+
$this->assertCount(6, $definition->getArguments());
3983+
$this->assertSame('foo', $definition->getArgument(0));
3984+
$this->assertSame('https://api.elevenlabs.io/v1', $definition->getArgument(1));
3985+
$this->assertInstanceOf(Reference::class, $definition->getArgument(2));
3986+
$this->assertSame('http_client', (string) $definition->getArgument(2));
3987+
$this->assertInstanceOf(Reference::class, $definition->getArgument(3));
3988+
$this->assertSame('ai.platform.model_catalog.elevenlabs', (string) $definition->getArgument(3));
3989+
$this->assertNull($definition->getArgument(4));
3990+
$this->assertInstanceOf(Reference::class, $definition->getArgument(5));
3991+
$this->assertSame('event_dispatcher', (string) $definition->getArgument(5));
3992+
3993+
$this->assertTrue($definition->hasTag('proxy'));
3994+
$this->assertSame([['interface' => PlatformInterface::class]], $definition->getTag('proxy'));
3995+
$this->assertTrue($definition->hasTag('ai.platform'));
3996+
$this->assertSame([['name' => 'elevenlabs']], $definition->getTag('ai.platform'));
3997+
3998+
$this->assertTrue($container->hasAlias('Symfony\AI\Platform\PlatformInterface $elevenlabs'));
3999+
$this->assertTrue($container->hasAlias('Symfony\AI\Platform\PlatformInterface'));
4000+
4001+
$modelCatalogDefinition = $container->getDefinition('ai.platform.model_catalog.elevenlabs');
4002+
4003+
$this->assertSame(ElevenLabsApiCatalog::class, $modelCatalogDefinition->getClass());
4004+
$this->assertTrue($modelCatalogDefinition->isLazy());
4005+
$this->assertCount(3, $modelCatalogDefinition->getArguments());
4006+
$this->assertInstanceOf(Reference::class, $modelCatalogDefinition->getArgument(0));
4007+
$this->assertSame('http_client', (string) $modelCatalogDefinition->getArgument(0));
4008+
$this->assertSame('foo', $modelCatalogDefinition->getArgument(1));
4009+
$this->assertSame('https://api.elevenlabs.io/v1', $modelCatalogDefinition->getArgument(2));
4010+
4011+
$this->assertTrue($modelCatalogDefinition->hasTag('proxy'));
4012+
$this->assertSame([['interface' => ModelCatalogInterface::class]], $modelCatalogDefinition->getTag('proxy'));
39324013
}
39334014

39344015
#[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)