Skip to content

Commit ff00f86

Browse files
committed
fix: OpenAI StructuredModeResolver should match versioned model names
StructuredModeResolver::supportsStructuredMode() uses in_array with exact string matching. Versioned model names like gpt-5-mini-2025-08-07 don't match gpt-5-mini, causing them to silently fall into JSON mode (json_object) instead of Structured mode (json_schema + strict). In JSON mode, the schema is embedded as plain text in a system message with no constrained decoding, resulting in significantly more verbose model output. Use a hybrid approach: - Keep exact matching for gpt-4o and o3-mini families where only specific versions support structured output - Use prefix matching for gpt-4.1+, gpt-4.5+, and gpt-5+ families where structured output is a baseline feature across all versions
1 parent 5d6cc65 commit ff00f86

2 files changed

Lines changed: 85 additions & 11 deletions

File tree

src/Providers/OpenAI/Support/StructuredModeResolver.php

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,26 +24,34 @@ public static function forModel(string $model): StructuredMode
2424

2525
protected static function supportsStructuredMode(string $model): bool
2626
{
27-
return in_array($model, [
27+
// Exact matches for models where only specific versions support structured output.
28+
if (in_array($model, [
2829
'gpt-4o-mini',
2930
'gpt-4o-mini-2024-07-18',
3031
'gpt-4o-2024-08-06',
3132
'gpt-4o',
3233
'chatgpt-4o-latest',
3334
'o3-mini',
3435
'o3-mini-2025-01-31',
36+
])) {
37+
return true;
38+
}
39+
40+
// Prefix matching for model families where structured output is a baseline feature.
41+
// All versions of these families support structured mode.
42+
$prefixes = [
3543
'gpt-4.1',
36-
'gpt-4.1-nano',
37-
'gpt-4.1-mini',
38-
'gpt-4.5-preview',
39-
'gpt-4.5-preview-2025-02-27',
44+
'gpt-4.5',
4045
'gpt-5',
41-
'gpt-5-mini',
42-
'gpt-5-nano',
43-
'gpt-5.1',
44-
'gpt-5.2',
45-
'gpt-5.4',
46-
]);
46+
];
47+
48+
foreach ($prefixes as $prefix) {
49+
if (str_starts_with($model, $prefix)) {
50+
return true;
51+
}
52+
}
53+
54+
return false;
4755
}
4856

4957
protected static function supportsJsonMode(string $model): bool
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Providers\OpenAI\Support;
6+
7+
use Prism\Prism\Enums\StructuredMode;
8+
use Prism\Prism\Exceptions\PrismException;
9+
use Prism\Prism\Providers\OpenAI\Support\StructuredModeResolver;
10+
11+
it('resolves structured mode for exact-match models', function (string $model): void {
12+
expect(StructuredModeResolver::forModel($model))->toBe(StructuredMode::Structured);
13+
})->with([
14+
'gpt-4o',
15+
'gpt-4o-mini',
16+
'gpt-4o-mini-2024-07-18',
17+
'gpt-4o-2024-08-06',
18+
'chatgpt-4o-latest',
19+
'o3-mini',
20+
'o3-mini-2025-01-31',
21+
]);
22+
23+
it('resolves structured mode for gpt-4.1+ family models', function (string $model): void {
24+
expect(StructuredModeResolver::forModel($model))->toBe(StructuredMode::Structured);
25+
})->with([
26+
'gpt-4.1',
27+
'gpt-4.1-nano',
28+
'gpt-4.1-mini',
29+
'gpt-4.1-nano-2025-04-14',
30+
'gpt-4.1-mini-2025-04-14',
31+
'gpt-4.5-preview',
32+
'gpt-4.5-preview-2025-02-27',
33+
]);
34+
35+
it('resolves structured mode for gpt-5 family models', function (string $model): void {
36+
expect(StructuredModeResolver::forModel($model))->toBe(StructuredMode::Structured);
37+
})->with([
38+
'gpt-5',
39+
'gpt-5-mini',
40+
'gpt-5-nano',
41+
'gpt-5-mini-2025-08-07',
42+
'gpt-5-nano-2025-08-07',
43+
'gpt-5.1',
44+
'gpt-5.1-2025-10-01',
45+
'gpt-5.2',
46+
'gpt-5.2-2025-12-11',
47+
'gpt-5.4',
48+
]);
49+
50+
it('resolves json mode for models without structured support', function (string $model): void {
51+
expect(StructuredModeResolver::forModel($model))->toBe(StructuredMode::Json);
52+
})->with([
53+
'gpt-4-turbo',
54+
'gpt-4-0125-preview',
55+
'gpt-3.5-turbo',
56+
'some-custom-model',
57+
]);
58+
59+
it('throws for unsupported models', function (string $model): void {
60+
StructuredModeResolver::forModel($model);
61+
})->with([
62+
'o1-mini',
63+
'o1-mini-2024-09-12',
64+
'o1-preview',
65+
'o1-preview-2024-09-12',
66+
])->throws(PrismException::class);

0 commit comments

Comments
 (0)