Skip to content

Commit 663750f

Browse files
OskarStarkclaude
andcommitted
Implement ModelCatalog system with YAML configuration support
- Replaced individual model constants with centralized ModelCatalog system - Added ModelCatalogInterface with getModel(), getModels(), and getModelConfig() methods - Created AbstractModelCatalog base class for common functionality - Implemented platform-specific ModelCatalog services (e.g., ai.model_catalog.openai) - Updated Platform::invoke() to accept string model names instead of Model objects - Added YAML configuration support for custom models with capability validation - Removed constructors from Gpt, Embeddings, Whisper, and DallE classes - Consolidated all OpenAI models (GPT, Embeddings, Whisper, DALL-E) into ModelCatalog - Updated OpenAI examples to use string model names - Added test coverage for model name normalization and ModelCatalog functionality - Configured AiBundle to create model catalog services from YAML configuration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 72e3ee7 commit 663750f

21 files changed

+127
-100
lines changed

examples/openai/additional-model.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,6 @@
4444
),
4545
);
4646

47-
$result = $platform->invoke($modelCatalog->getModel('gpt-4o-mini-transcribe'), $messages);
47+
$result = $platform->invoke('gpt-4o-mini-transcribe', $messages);
4848

4949
echo $result->getResult()->getContent().\PHP_EOL;

examples/openai/audio-transcript.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@
1616
require_once dirname(__DIR__).'/bootstrap.php';
1717

1818
$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client());
19-
$model = new Whisper(Whisper::WHISPER_1);
2019
$file = Audio::fromFile(dirname(__DIR__, 2).'/fixtures/audio.mp3');
2120

22-
$result = $platform->invoke($model, $file);
21+
$result = $platform->invoke('whisper-1', $file);
2322

2423
echo $result->asText().\PHP_EOL;

examples/openai/chat.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,12 @@
1717
require_once dirname(__DIR__).'/bootstrap.php';
1818

1919
$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client());
20-
$model = new Gpt(Gpt::GPT_4O_MINI);
2120

2221
$messages = new MessageBag(
2322
Message::forSystem('You are a pirate and you write funny.'),
2423
Message::ofUser('What is the Symfony framework?'),
2524
);
26-
$result = $platform->invoke($model, $messages, [
25+
$result = $platform->invoke('gpt-4o-mini', $messages, [
2726
'max_tokens' => 500, // specific options just for this call
2827
]);
2928

examples/openai/embeddings.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@
1515
require_once dirname(__DIR__).'/bootstrap.php';
1616

1717
$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client());
18-
$embeddings = new Embeddings(Embeddings::TEXT_3_SMALL);
1918

20-
$result = $platform->invoke($embeddings, <<<TEXT
19+
$result = $platform->invoke('text-embedding-3-small', <<<TEXT
2120
Once upon a time, there was a country called Japan. It was a beautiful country with a lot of mountains and rivers.
2221
The people of Japan were very kind and hardworking. They loved their country very much and took care of it. The
2322
country was very peaceful and prosperous. The people lived happily ever after.

examples/openai/image-output-dall-e-2.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client());
1818

1919
$result = $platform->invoke(
20-
model: new DallE(DallE::DALL_E_2),
20+
model: 'dall-e-2',
2121
input: 'A cartoon-style elephant with a long trunk and large ears.',
2222
options: [
2323
'response_format' => 'url', // Generate response as URL

examples/openai/image-output-dall-e-3.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client());
1919

2020
$result = $platform->invoke(
21-
model: new DallE(name: DallE::DALL_E_3),
21+
model: 'dall-e-3',
2222
input: 'A cartoon-style elephant with a long trunk and large ears.',
2323
options: [
2424
'response_format' => 'url', // Generate response as URL

src/ai-bundle/config/options.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,10 @@
189189
->enumPrototype(Capability::class)
190190
->end()
191191
->defaultValue([])
192+
->validate()
193+
->ifEmpty()
194+
->thenInvalid('At least one capability must be specified for each model.')
195+
->end()
192196
->end()
193197
->end()
194198
->end()

src/ai-bundle/src/AiBundle.php

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,21 +1170,16 @@ private function processModelCatalogConfig(string $platformName, array $models,
11701170
foreach ($models as $modelName => $modelConfig) {
11711171
$capabilities = $modelConfig['capabilities'] ?? [];
11721172

1173-
// If no capabilities provided, add all capabilities from Capability enum
1174-
if (empty($capabilities)) {
1175-
$capabilityEnums = Capability::cases();
1176-
} else {
1177-
// Convert string capabilities to Capability enums
1178-
if (is_string($capabilities)) {
1179-
$capabilities = explode(',', $capabilities);
1180-
}
1181-
1182-
$capabilityEnums = [];
1183-
foreach ($capabilities as $capability) {
1184-
$capability = trim($capability);
1185-
if ($capability !== '') {
1186-
$capabilityEnums[] = Capability::from($capability);
1187-
}
1173+
// Convert string capabilities to Capability enums
1174+
if (is_string($capabilities)) {
1175+
$capabilities = explode(',', $capabilities);
1176+
}
1177+
1178+
$capabilityEnums = [];
1179+
foreach ($capabilities as $capability) {
1180+
$capability = trim($capability);
1181+
if ($capability !== '') {
1182+
$capabilityEnums[] = Capability::from($capability);
11881183
}
11891184
}
11901185

@@ -1195,9 +1190,7 @@ private function processModelCatalogConfig(string $platformName, array $models,
11951190
}
11961191

11971192
// Create platform-specific model catalog service
1198-
$catalogServiceId = 'ai.model_catalog.' . $platformName;
1199-
$catalogDefinition = new Definition(ModelCatalog::class, [$modelDefinitions]);
1200-
$container->setDefinition($catalogServiceId, $catalogDefinition);
1193+
$container->setDefinition('ai.model_catalog.' . $platformName, new Definition(ModelCatalog::class, [$modelDefinitions]));
12011194
}
12021195

12031196
}

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1946,6 +1946,50 @@ public function testIndexerWithSourceFiltersAndTransformers()
19461946
$this->assertSame('logger', (string) $arguments[6]);
19471947
}
19481948

1949+
public function testModelNamesAreNotNormalized()
1950+
{
1951+
$container = $this->buildContainer([
1952+
'ai' => [
1953+
'model' => [
1954+
'openai' => [
1955+
'foo-bar' => [
1956+
'class' => Gpt::class,
1957+
'capabilities' => ['INPUT_TEXT', 'OUTPUT_TEXT'],
1958+
],
1959+
'my_custom-model.v2' => [
1960+
'class' => Embeddings::class,
1961+
'capabilities' => ['INPUT_TEXT'],
1962+
],
1963+
'gpt-4o-mini-transcribe' => [
1964+
'class' => Gpt::class,
1965+
'capabilities' => ['INPUT_AUDIO', 'INPUT_TEXT', 'OUTPUT_TEXT'],
1966+
],
1967+
],
1968+
],
1969+
],
1970+
]);
1971+
1972+
// Verify that model catalog service is created for the platform
1973+
$this->assertTrue($container->hasDefinition('ai.model_catalog.openai'));
1974+
1975+
// Get the model catalog definition and check that model names are preserved exactly
1976+
$catalogDefinition = $container->getDefinition('ai.model_catalog.openai');
1977+
$arguments = $catalogDefinition->getArguments();
1978+
1979+
$this->assertCount(1, $arguments);
1980+
$modelDefinitions = $arguments[0];
1981+
1982+
// Verify that model names are not normalized and maintain their original format
1983+
$this->assertArrayHasKey('foo-bar', $modelDefinitions);
1984+
$this->assertArrayHasKey('my_custom-model.v2', $modelDefinitions);
1985+
$this->assertArrayHasKey('gpt-4o-mini-transcribe', $modelDefinitions);
1986+
1987+
// Verify model configurations are correct
1988+
$this->assertSame(Gpt::class, $modelDefinitions['foo-bar']['class']);
1989+
$this->assertSame(Embeddings::class, $modelDefinitions['my_custom-model.v2']['class']);
1990+
$this->assertSame(Gpt::class, $modelDefinitions['gpt-4o-mini-transcribe']['class']);
1991+
}
1992+
19491993
private function buildContainer(array $configuration): ContainerBuilder
19501994
{
19511995
$container = new ContainerBuilder();

src/platform/src/AbstractModelCatalog.php

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,24 @@
2020
abstract class AbstractModelCatalog implements ModelCatalogInterface
2121
{
2222
/**
23-
* @var array<string, array{class: string, platform: string, capabilities: list<Capability>}>
23+
* @var array<string, array{class: string, capabilities: list<Capability>}>
2424
*/
2525
protected readonly array $models;
2626

2727
public function getModel(string $modelName): Model
2828
{
29-
$modelConfig = $this->getModelConfig($modelName);
30-
31-
if (null === $modelConfig) {
29+
if (!isset($this->models[$modelName])) {
3230
throw ModelNotFoundException::forModelName($modelName);
3331
}
3432

33+
$modelConfig = $this->models[$modelName];
3534
$modelClass = $modelConfig['class'];
35+
3636
if (!class_exists($modelClass)) {
3737
throw new InvalidArgumentException(\sprintf('Model class "%s" does not exist.', $modelClass));
3838
}
3939

40-
$model = new $modelClass();
40+
$model = new $modelClass($modelName, $modelConfig['capabilities']);
4141
if (!$model instanceof Model) {
4242
throw new InvalidArgumentException(\sprintf('Model class "%s" must extend %s.', $modelClass, Model::class));
4343
}
@@ -46,18 +46,23 @@ public function getModel(string $modelName): Model
4646
}
4747

4848
/**
49-
* @return list<string>
49+
* @return array<string, array{class: string, capabilities: list<Capability>}>
5050
*/
5151
public function getModels(): array
5252
{
53-
return array_keys($this->models);
53+
return $this->models;
5454
}
5555

5656
/**
57-
* @return array{class: string, platform: string, capabilities: list<Capability>}|null
57+
* @return array{class: string, capabilities: list<Capability>}
5858
*/
59-
private function getModelConfig(string $name): ?array
59+
public function getModelConfig(string $modelName): array
6060
{
61-
return $this->models[$name] ?? null;
61+
if (!isset($this->models[$modelName])) {
62+
throw ModelNotFoundException::forModelName($modelName);
63+
}
64+
65+
return $this->models[$modelName];
6266
}
67+
6368
}

0 commit comments

Comments
 (0)