Skip to content

Commit b50bce3

Browse files
committed
bug #706 [Platform] Fix integer options (OskarStark)
This PR was squashed before being merged into the main branch. Discussion ---------- [Platform] Fix integer options | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | Docs? | no | Issues | -- | License | MIT <img width="1940" height="76" alt="CleanShot 2025-09-30 at 11 00 47@2x" src="https://github.com/user-attachments/assets/6fdc7495-5857-452e-a730-684815b6c2a6" /> Commits ------- ae24e55 [Platform] Fix integer options
2 parents 23d4b59 + ae24e55 commit b50bce3

File tree

4 files changed

+195
-2
lines changed

4 files changed

+195
-2
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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\Platform\Bridge\OpenAi\PlatformFactory;
13+
use Symfony\AI\Platform\Message\Message;
14+
use Symfony\AI\Platform\Message\MessageBag;
15+
16+
require_once dirname(__DIR__).'/bootstrap.php';
17+
18+
$platform = PlatformFactory::create(env('OPENAI_API_KEY'), http_client());
19+
20+
$messages = new MessageBag(
21+
Message::forSystem('You are a pirate and you write funny.'),
22+
Message::ofUser('What is the Symfony framework?'),
23+
);
24+
$result = $platform->invoke('gpt-4o-mini?max_tokens=7', $messages);
25+
26+
echo $result->getResult()->getContent().\PHP_EOL;

src/platform/src/ModelCatalog/AbstractModelCatalog.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,34 @@ protected static function parseModelName(string $modelName): array
8383
}
8484

8585
parse_str($queryString, $options);
86+
87+
$options = self::convertNumericStrings($options);
8688
}
8789

8890
return [
8991
'name' => $actualModelName,
9092
'options' => $options,
9193
];
9294
}
95+
96+
/**
97+
* Recursively converts numeric strings to integers or floats in an array.
98+
*
99+
* @param array<string, mixed> $data The array to process
100+
*
101+
* @return array<string, mixed> The array with numeric strings converted to appropriate numeric types
102+
*/
103+
private static function convertNumericStrings(array $data): array
104+
{
105+
foreach ($data as $key => $value) {
106+
if (\is_array($value)) {
107+
$data[$key] = self::convertNumericStrings($value);
108+
} elseif (is_numeric($value) && \is_string($value)) {
109+
// Convert to int if it's a whole number, otherwise to float
110+
$data[$key] = str_contains($value, '.') ? (float) $value : (int) $value;
111+
}
112+
}
113+
114+
return $data;
115+
}
93116
}

src/platform/tests/DynamicModelCatalogTest.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,10 @@ public function testGetModelWithOptions()
4545
$this->assertSame('test-model', $model->getName());
4646

4747
$options = $model->getOptions();
48-
$this->assertSame('0.7', $options['temperature']);
49-
$this->assertSame('1000', $options['max_tokens']);
48+
$this->assertSame(0.7, $options['temperature']);
49+
$this->assertIsFloat($options['temperature']);
50+
$this->assertSame(1000, $options['max_tokens']);
51+
$this->assertIsInt($options['max_tokens']);
5052
}
5153

5254
#[TestWith(['gpt-4'])]
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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\Tests\ModelCatalog;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\AI\Platform\Capability;
16+
use Symfony\AI\Platform\Exception\InvalidArgumentException;
17+
use Symfony\AI\Platform\Model;
18+
use Symfony\AI\Platform\ModelCatalog\AbstractModelCatalog;
19+
20+
final class AbstractModelCatalogTest extends TestCase
21+
{
22+
public function testGetModelWithoutQueryParameters()
23+
{
24+
$catalog = $this->createTestCatalog();
25+
$model = $catalog->getModel('test-model');
26+
27+
$this->assertSame('test-model', $model->getName());
28+
$this->assertSame([], $model->getOptions());
29+
}
30+
31+
public function testGetModelWithStringQueryParameter()
32+
{
33+
$catalog = $this->createTestCatalog();
34+
$model = $catalog->getModel('test-model?param=value');
35+
36+
$this->assertSame('test-model', $model->getName());
37+
$this->assertSame(['param' => 'value'], $model->getOptions());
38+
}
39+
40+
public function testGetModelWithIntegerQueryParameter()
41+
{
42+
$catalog = $this->createTestCatalog();
43+
$model = $catalog->getModel('test-model?max_tokens=500');
44+
45+
$this->assertSame('test-model', $model->getName());
46+
$options = $model->getOptions();
47+
$this->assertArrayHasKey('max_tokens', $options);
48+
$this->assertIsInt($options['max_tokens']);
49+
$this->assertSame(500, $options['max_tokens']);
50+
}
51+
52+
public function testGetModelWithMultipleQueryParameters()
53+
{
54+
$catalog = $this->createTestCatalog();
55+
$model = $catalog->getModel('test-model?max_tokens=500&temperature=0.7&stream=true');
56+
57+
$this->assertSame('test-model', $model->getName());
58+
$options = $model->getOptions();
59+
60+
$this->assertArrayHasKey('max_tokens', $options);
61+
$this->assertIsInt($options['max_tokens']);
62+
$this->assertSame(500, $options['max_tokens']);
63+
64+
$this->assertArrayHasKey('temperature', $options);
65+
$this->assertIsFloat($options['temperature']);
66+
$this->assertSame(0.7, $options['temperature']);
67+
68+
$this->assertArrayHasKey('stream', $options);
69+
$this->assertSame('true', $options['stream']);
70+
}
71+
72+
public function testGetModelWithNestedArrayQueryParameters()
73+
{
74+
$catalog = $this->createTestCatalog();
75+
$model = $catalog->getModel('test-model?options[max_tokens]=500&options[temperature]=0.7&options[metadata][version]=1');
76+
77+
$this->assertSame('test-model', $model->getName());
78+
$options = $model->getOptions();
79+
80+
$this->assertIsArray($options['options']);
81+
$this->assertSame(500, $options['options']['max_tokens']);
82+
$this->assertIsInt($options['options']['max_tokens']);
83+
$this->assertSame(0.7, $options['options']['temperature']);
84+
$this->assertIsFloat($options['options']['temperature']);
85+
$this->assertIsArray($options['options']['metadata']);
86+
$this->assertSame(1, $options['options']['metadata']['version']);
87+
$this->assertIsInt($options['options']['metadata']['version']);
88+
}
89+
90+
public function testGetModelWithEmptyModelNameThrowsException()
91+
{
92+
$catalog = $this->createTestCatalog();
93+
94+
$this->expectException(InvalidArgumentException::class);
95+
$this->expectExceptionMessage('Model name cannot be empty.');
96+
97+
/* @phpstan-ignore argument.type */
98+
$catalog->getModel('');
99+
}
100+
101+
public function testGetModelWithOnlyQueryStringThrowsException()
102+
{
103+
$catalog = $this->createTestCatalog();
104+
105+
$this->expectException(InvalidArgumentException::class);
106+
$this->expectExceptionMessage('Model name cannot be empty.');
107+
108+
$catalog->getModel('?max_tokens=500');
109+
}
110+
111+
public function testNumericStringsAreConvertedRecursively()
112+
{
113+
$catalog = $this->createTestCatalog();
114+
$model = $catalog->getModel('test-model?a[b][c]=123&a[b][d]=text&a[e]=456');
115+
116+
$options = $model->getOptions();
117+
118+
$this->assertIsArray($options['a']);
119+
$this->assertIsArray($options['a']['b']);
120+
$this->assertSame(123, $options['a']['b']['c']);
121+
$this->assertIsInt($options['a']['b']['c']);
122+
$this->assertSame('text', $options['a']['b']['d']);
123+
$this->assertIsString($options['a']['b']['d']);
124+
$this->assertSame(456, $options['a']['e']);
125+
$this->assertIsInt($options['a']['e']);
126+
}
127+
128+
private function createTestCatalog(): AbstractModelCatalog
129+
{
130+
return new class extends AbstractModelCatalog {
131+
public function __construct()
132+
{
133+
$this->models = [
134+
'test-model' => [
135+
'class' => Model::class,
136+
'capabilities' => [Capability::INPUT_TEXT],
137+
],
138+
];
139+
}
140+
};
141+
}
142+
}

0 commit comments

Comments
 (0)