From 0a0b6bc1d0405add8e73f2113e31a148272f84b4 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Tue, 2 Sep 2025 21:13:09 +0200 Subject: [PATCH] [Platform][OpenAI] Add `region` option --- src/ai-bundle/config/options.php | 9 +++ src/ai-bundle/src/AiBundle.php | 1 + .../DependencyInjection/AiBundleTest.php | 67 ++++++++++++++++++- .../LmStudio/Completions/ModelClient.php | 4 +- .../LmStudio/Embeddings/ModelClient.php | 4 +- .../src/Bridge/OpenAi/AbstractModelClient.php | 41 ++++++++++++ .../src/Bridge/OpenAi/DallE/ModelClient.php | 14 ++-- .../Bridge/OpenAi/Embeddings/ModelClient.php | 16 ++--- .../src/Bridge/OpenAi/Gpt/ModelClient.php | 16 ++--- .../src/Bridge/OpenAi/PlatformFactory.php | 12 ++-- .../src/Bridge/OpenAi/Whisper/ModelClient.php | 16 ++--- .../Azure/OpenAi/WhisperModelClientTest.php | 17 +---- .../Bridge/OpenAi/DallE/ModelClientTest.php | 17 +++++ .../OpenAi/Embeddings/ModelClientTest.php | 17 +++++ .../Bridge/OpenAi/Gpt/ModelClientTest.php | 17 +++++ .../Bridge/OpenAi/Whisper/ModelClientTest.php | 40 +++++++++++ 16 files changed, 246 insertions(+), 62 deletions(-) create mode 100644 src/platform/src/Bridge/OpenAi/AbstractModelClient.php diff --git a/src/ai-bundle/config/options.php b/src/ai-bundle/config/options.php index cafafc09e..18fa1649b 100644 --- a/src/ai-bundle/config/options.php +++ b/src/ai-bundle/config/options.php @@ -14,6 +14,7 @@ use Codewithkyrian\ChromaDB\Client as ChromaDbClient; use MongoDB\Client as MongoDbClient; use Probots\Pinecone\Client as PineconeClient; +use Symfony\AI\Platform\Bridge\OpenAi\PlatformFactory; use Symfony\AI\Platform\PlatformInterface; use Symfony\AI\Store\StoreInterface; @@ -59,6 +60,14 @@ ->arrayNode('openai') ->children() ->scalarNode('api_key')->isRequired()->end() + ->scalarNode('region') + ->defaultNull() + ->validate() + ->ifNotInArray([null, PlatformFactory::REGION_EU, PlatformFactory::REGION_US]) + ->thenInvalid('The region must be either "EU" (https://eu.api.openai.com), "US" (https://us.api.openai.com) or null (https://api.openai.com)') + ->end() + ->info('The region for OpenAI API (EU, US, or null for default)') + ->end() ->end() ->end() ->arrayNode('mistral') diff --git a/src/ai-bundle/src/AiBundle.php b/src/ai-bundle/src/AiBundle.php index 4679f77d0..bc21de8bd 100644 --- a/src/ai-bundle/src/AiBundle.php +++ b/src/ai-bundle/src/AiBundle.php @@ -316,6 +316,7 @@ private function processPlatformConfig(string $type, array $platform, ContainerB $platform['api_key'], new Reference('http_client', ContainerInterface::NULL_ON_INVALID_REFERENCE), new Reference('ai.platform.contract.openai'), + $platform['region'] ?? null, ]) ->addTag('ai.platform'); diff --git a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php index 4906e0da9..8dc71b696 100644 --- a/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php +++ b/src/ai-bundle/tests/DependencyInjection/AiBundleTest.php @@ -459,7 +459,7 @@ public function testTokenUsageProcessorTags() 'ai' => [ 'platform' => [ 'openai' => [ - 'api_key' => 'test_key', + 'api_key' => 'sk-test_key', ], ], 'agent' => [ @@ -489,6 +489,71 @@ public function testTokenUsageProcessorTags() $this->assertTrue($foundTag, 'Token usage processor should have output tag with full agent ID'); } + public function testOpenAiPlatformWithDefaultRegion() + { + $container = $this->buildContainer([ + 'ai' => [ + 'platform' => [ + 'openai' => [ + 'api_key' => 'sk-test-key', + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.platform.openai')); + + $definition = $container->getDefinition('ai.platform.openai'); + $arguments = $definition->getArguments(); + + $this->assertCount(4, $arguments); + $this->assertSame('sk-test-key', $arguments[0]); + $this->assertNull($arguments[3]); // region should be null by default + } + + #[TestWith(['EU'])] + #[TestWith(['US'])] + #[TestWith([null])] + public function testOpenAiPlatformWithRegion(?string $region) + { + $container = $this->buildContainer([ + 'ai' => [ + 'platform' => [ + 'openai' => [ + 'api_key' => 'sk-test-key', + 'region' => $region, + ], + ], + ], + ]); + + $this->assertTrue($container->hasDefinition('ai.platform.openai')); + + $definition = $container->getDefinition('ai.platform.openai'); + $arguments = $definition->getArguments(); + + $this->assertCount(4, $arguments); + $this->assertSame('sk-test-key', $arguments[0]); + $this->assertSame($region, $arguments[3]); + } + + public function testOpenAiPlatformWithInvalidRegion() + { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('The region must be either "EU" (https://eu.api.openai.com), "US" (https://us.api.openai.com) or null (https://api.openai.com)'); + + $this->buildContainer([ + 'ai' => [ + 'platform' => [ + 'openai' => [ + 'api_key' => 'sk-test-key', + 'region' => 'INVALID', + ], + ], + ], + ]); + } + private function buildContainer(array $configuration): ContainerBuilder { $container = new ContainerBuilder(); diff --git a/src/platform/src/Bridge/LmStudio/Completions/ModelClient.php b/src/platform/src/Bridge/LmStudio/Completions/ModelClient.php index dc422e25c..80abef08c 100644 --- a/src/platform/src/Bridge/LmStudio/Completions/ModelClient.php +++ b/src/platform/src/Bridge/LmStudio/Completions/ModelClient.php @@ -13,7 +13,7 @@ use Symfony\AI\Platform\Bridge\LmStudio\Completions; use Symfony\AI\Platform\Model; -use Symfony\AI\Platform\ModelClientInterface as PlatformResponseFactory; +use Symfony\AI\Platform\ModelClientInterface; use Symfony\AI\Platform\Result\RawHttpResult; use Symfony\Component\HttpClient\EventSourceHttpClient; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -21,7 +21,7 @@ /** * @author André Lubian */ -final readonly class ModelClient implements PlatformResponseFactory +final readonly class ModelClient implements ModelClientInterface { private EventSourceHttpClient $httpClient; diff --git a/src/platform/src/Bridge/LmStudio/Embeddings/ModelClient.php b/src/platform/src/Bridge/LmStudio/Embeddings/ModelClient.php index c705c8a71..f6208e437 100644 --- a/src/platform/src/Bridge/LmStudio/Embeddings/ModelClient.php +++ b/src/platform/src/Bridge/LmStudio/Embeddings/ModelClient.php @@ -13,7 +13,7 @@ use Symfony\AI\Platform\Bridge\LmStudio\Embeddings; use Symfony\AI\Platform\Model; -use Symfony\AI\Platform\ModelClientInterface as PlatformResponseFactory; +use Symfony\AI\Platform\ModelClientInterface; use Symfony\AI\Platform\Result\RawHttpResult; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -21,7 +21,7 @@ * @author Christopher Hertel * @author André Lubian */ -final readonly class ModelClient implements PlatformResponseFactory +final readonly class ModelClient implements ModelClientInterface { public function __construct( private HttpClientInterface $httpClient, diff --git a/src/platform/src/Bridge/OpenAi/AbstractModelClient.php b/src/platform/src/Bridge/OpenAi/AbstractModelClient.php new file mode 100644 index 000000000..748ae5a20 --- /dev/null +++ b/src/platform/src/Bridge/OpenAi/AbstractModelClient.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Bridge\OpenAi; + +use Symfony\AI\Platform\Exception\InvalidArgumentException; + +/** + * @author Oskar Stark + */ +abstract readonly class AbstractModelClient +{ + protected static function getBaseUrl(?string $region): string + { + return match ($region) { + null => 'https://api.openai.com', + PlatformFactory::REGION_EU => 'https://eu.api.openai.com', + PlatformFactory::REGION_US => 'https://us.api.openai.com', + default => throw new InvalidArgumentException(\sprintf('Invalid region "%s". Valid options are: "%s", "%s", or null.', $region, PlatformFactory::REGION_EU, PlatformFactory::REGION_US)), + }; + } + + protected static function validateApiKey(string $apiKey): void + { + if ('' === $apiKey) { + throw new InvalidArgumentException('The API key must not be empty.'); + } + + if (!str_starts_with($apiKey, 'sk-')) { + throw new InvalidArgumentException('The API key must start with "sk-".'); + } + } +} diff --git a/src/platform/src/Bridge/OpenAi/DallE/ModelClient.php b/src/platform/src/Bridge/OpenAi/DallE/ModelClient.php index 408e010cd..7fc719f0d 100644 --- a/src/platform/src/Bridge/OpenAi/DallE/ModelClient.php +++ b/src/platform/src/Bridge/OpenAi/DallE/ModelClient.php @@ -11,8 +11,8 @@ namespace Symfony\AI\Platform\Bridge\OpenAi\DallE; +use Symfony\AI\Platform\Bridge\OpenAi\AbstractModelClient; use Symfony\AI\Platform\Bridge\OpenAi\DallE; -use Symfony\AI\Platform\Exception\InvalidArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface; use Symfony\AI\Platform\Result\RawHttpResult; @@ -23,18 +23,14 @@ * * @author Denis Zunke */ -final readonly class ModelClient implements ModelClientInterface +final readonly class ModelClient extends AbstractModelClient implements ModelClientInterface { public function __construct( private HttpClientInterface $httpClient, #[\SensitiveParameter] private string $apiKey, + private ?string $region = null, ) { - if ('' === $apiKey) { - throw new InvalidArgumentException('The API key must not be empty.'); - } - if (!str_starts_with($apiKey, 'sk-')) { - throw new InvalidArgumentException('The API key must start with "sk-".'); - } + self::validateApiKey($apiKey); } public function supports(Model $model): bool @@ -44,7 +40,7 @@ public function supports(Model $model): bool public function request(Model $model, array|string $payload, array $options = []): RawHttpResult { - return new RawHttpResult($this->httpClient->request('POST', 'https://api.openai.com/v1/images/generations', [ + return new RawHttpResult($this->httpClient->request('POST', self::getBaseUrl($this->region).'/v1/images/generations', [ 'auth_bearer' => $this->apiKey, 'json' => array_merge($options, [ 'model' => $model->getName(), diff --git a/src/platform/src/Bridge/OpenAi/Embeddings/ModelClient.php b/src/platform/src/Bridge/OpenAi/Embeddings/ModelClient.php index 8d62cdd80..dc71020ee 100644 --- a/src/platform/src/Bridge/OpenAi/Embeddings/ModelClient.php +++ b/src/platform/src/Bridge/OpenAi/Embeddings/ModelClient.php @@ -11,28 +11,24 @@ namespace Symfony\AI\Platform\Bridge\OpenAi\Embeddings; +use Symfony\AI\Platform\Bridge\OpenAi\AbstractModelClient; use Symfony\AI\Platform\Bridge\OpenAi\Embeddings; -use Symfony\AI\Platform\Exception\InvalidArgumentException; use Symfony\AI\Platform\Model; -use Symfony\AI\Platform\ModelClientInterface as PlatformResponseFactory; +use Symfony\AI\Platform\ModelClientInterface; use Symfony\AI\Platform\Result\RawHttpResult; use Symfony\Contracts\HttpClient\HttpClientInterface; /** * @author Christopher Hertel */ -final readonly class ModelClient implements PlatformResponseFactory +final readonly class ModelClient extends AbstractModelClient implements ModelClientInterface { public function __construct( private HttpClientInterface $httpClient, #[\SensitiveParameter] private string $apiKey, + private ?string $region = null, ) { - if ('' === $apiKey) { - throw new InvalidArgumentException('The API key must not be empty.'); - } - if (!str_starts_with($apiKey, 'sk-')) { - throw new InvalidArgumentException('The API key must start with "sk-".'); - } + self::validateApiKey($apiKey); } public function supports(Model $model): bool @@ -42,7 +38,7 @@ public function supports(Model $model): bool public function request(Model $model, array|string $payload, array $options = []): RawHttpResult { - return new RawHttpResult($this->httpClient->request('POST', 'https://api.openai.com/v1/embeddings', [ + return new RawHttpResult($this->httpClient->request('POST', self::getBaseUrl($this->region).'/v1/embeddings', [ 'auth_bearer' => $this->apiKey, 'json' => array_merge($options, [ 'model' => $model->getName(), diff --git a/src/platform/src/Bridge/OpenAi/Gpt/ModelClient.php b/src/platform/src/Bridge/OpenAi/Gpt/ModelClient.php index 6ab91358a..94034c1c1 100644 --- a/src/platform/src/Bridge/OpenAi/Gpt/ModelClient.php +++ b/src/platform/src/Bridge/OpenAi/Gpt/ModelClient.php @@ -11,10 +11,10 @@ namespace Symfony\AI\Platform\Bridge\OpenAi\Gpt; +use Symfony\AI\Platform\Bridge\OpenAi\AbstractModelClient; use Symfony\AI\Platform\Bridge\OpenAi\Gpt; -use Symfony\AI\Platform\Exception\InvalidArgumentException; use Symfony\AI\Platform\Model; -use Symfony\AI\Platform\ModelClientInterface as PlatformResponseFactory; +use Symfony\AI\Platform\ModelClientInterface; use Symfony\AI\Platform\Result\RawHttpResult; use Symfony\Component\HttpClient\EventSourceHttpClient; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -22,21 +22,17 @@ /** * @author Christopher Hertel */ -final readonly class ModelClient implements PlatformResponseFactory +final readonly class ModelClient extends AbstractModelClient implements ModelClientInterface { private EventSourceHttpClient $httpClient; public function __construct( HttpClientInterface $httpClient, #[\SensitiveParameter] private string $apiKey, + private ?string $region = null, ) { $this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); - if ('' === $apiKey) { - throw new InvalidArgumentException('The API key must not be empty.'); - } - if (!str_starts_with($apiKey, 'sk-')) { - throw new InvalidArgumentException('The API key must start with "sk-".'); - } + self::validateApiKey($apiKey); } public function supports(Model $model): bool @@ -46,7 +42,7 @@ public function supports(Model $model): bool public function request(Model $model, array|string $payload, array $options = []): RawHttpResult { - return new RawHttpResult($this->httpClient->request('POST', 'https://api.openai.com/v1/chat/completions', [ + return new RawHttpResult($this->httpClient->request('POST', self::getBaseUrl($this->region).'/v1/chat/completions', [ 'auth_bearer' => $this->apiKey, 'json' => array_merge($options, $payload), ])); diff --git a/src/platform/src/Bridge/OpenAi/PlatformFactory.php b/src/platform/src/Bridge/OpenAi/PlatformFactory.php index 4cf0cf118..3f91585f1 100644 --- a/src/platform/src/Bridge/OpenAi/PlatformFactory.php +++ b/src/platform/src/Bridge/OpenAi/PlatformFactory.php @@ -24,19 +24,23 @@ */ final readonly class PlatformFactory { + public const REGION_EU = 'EU'; + public const REGION_US = 'US'; + public static function create( #[\SensitiveParameter] string $apiKey, ?HttpClientInterface $httpClient = null, ?Contract $contract = null, + ?string $region = null, ): Platform { $httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); return new Platform( [ - new Gpt\ModelClient($httpClient, $apiKey), - new Embeddings\ModelClient($httpClient, $apiKey), - new DallE\ModelClient($httpClient, $apiKey), - new WhisperModelClient($httpClient, $apiKey), + new Gpt\ModelClient($httpClient, $apiKey, $region), + new Embeddings\ModelClient($httpClient, $apiKey, $region), + new DallE\ModelClient($httpClient, $apiKey, $region), + new WhisperModelClient($httpClient, $apiKey, $region), ], [ new Gpt\ResultConverter(), diff --git a/src/platform/src/Bridge/OpenAi/Whisper/ModelClient.php b/src/platform/src/Bridge/OpenAi/Whisper/ModelClient.php index de945ffb9..88db5fac7 100644 --- a/src/platform/src/Bridge/OpenAi/Whisper/ModelClient.php +++ b/src/platform/src/Bridge/OpenAi/Whisper/ModelClient.php @@ -11,28 +11,24 @@ namespace Symfony\AI\Platform\Bridge\OpenAi\Whisper; +use Symfony\AI\Platform\Bridge\OpenAi\AbstractModelClient; use Symfony\AI\Platform\Bridge\OpenAi\Whisper; -use Symfony\AI\Platform\Exception\InvalidArgumentException; use Symfony\AI\Platform\Model; -use Symfony\AI\Platform\ModelClientInterface as BaseModelClient; +use Symfony\AI\Platform\ModelClientInterface; use Symfony\AI\Platform\Result\RawHttpResult; use Symfony\Contracts\HttpClient\HttpClientInterface; /** * @author Christopher Hertel */ -final readonly class ModelClient implements BaseModelClient +final readonly class ModelClient extends AbstractModelClient implements ModelClientInterface { public function __construct( private HttpClientInterface $httpClient, #[\SensitiveParameter] private string $apiKey, + private ?string $region = null, ) { - if ('' === $apiKey) { - throw new InvalidArgumentException('The API key must not be empty.'); - } - if (!str_starts_with($apiKey, 'sk-')) { - throw new InvalidArgumentException('The API key must start with "sk-".'); - } + self::validateApiKey($apiKey); } public function supports(Model $model): bool @@ -46,7 +42,7 @@ public function request(Model $model, array|string $payload, array $options = [] $endpoint = Task::TRANSCRIPTION === $task ? 'transcriptions' : 'translations'; unset($options['task']); - return new RawHttpResult($this->httpClient->request('POST', \sprintf('https://api.openai.com/v1/audio/%s', $endpoint), [ + return new RawHttpResult($this->httpClient->request('POST', \sprintf('%s/v1/audio/%s', self::getBaseUrl($this->region), $endpoint), [ 'auth_bearer' => $this->apiKey, 'headers' => ['Content-Type' => 'multipart/form-data'], 'body' => array_merge($options, $payload, ['model' => $model->getName()]), diff --git a/src/platform/tests/Bridge/Azure/OpenAi/WhisperModelClientTest.php b/src/platform/tests/Bridge/Azure/OpenAi/WhisperModelClientTest.php index 450d694f6..28bea6340 100644 --- a/src/platform/tests/Bridge/Azure/OpenAi/WhisperModelClientTest.php +++ b/src/platform/tests/Bridge/Azure/OpenAi/WhisperModelClientTest.php @@ -95,10 +95,7 @@ function ($method, $url): MockResponse { ]); $client = new WhisperModelClient($httpClient, 'test.azure.com', 'whspr', '2023-12', 'test-key'); - $model = new Whisper(); - $payload = ['file' => 'audio-data']; - - $client->request($model, $payload); + $client->request(new Whisper(), ['file' => 'audio-data']); $this->assertSame(1, $httpClient->getRequestsCount()); } @@ -115,11 +112,7 @@ function ($method, $url): MockResponse { ]); $client = new WhisperModelClient($httpClient, 'test.azure.com', 'whspr', '2023-12', 'test-key'); - $model = new Whisper(); - $payload = ['file' => 'audio-data']; - $options = ['task' => Task::TRANSCRIPTION]; - - $client->request($model, $payload, $options); + $client->request(new Whisper(), ['file' => 'audio-data'], ['task' => Task::TRANSCRIPTION]); $this->assertSame(1, $httpClient->getRequestsCount()); } @@ -136,11 +129,7 @@ function ($method, $url): MockResponse { ]); $client = new WhisperModelClient($httpClient, 'test.azure.com', 'whspr', '2023-12', 'test-key'); - $model = new Whisper(); - $payload = ['file' => 'audio-data']; - $options = ['task' => Task::TRANSLATION]; - - $client->request($model, $payload, $options); + $client->request(new Whisper(), ['file' => 'audio-data'], ['task' => Task::TRANSLATION]); $this->assertSame(1, $httpClient->getRequestsCount()); } diff --git a/src/platform/tests/Bridge/OpenAi/DallE/ModelClientTest.php b/src/platform/tests/Bridge/OpenAi/DallE/ModelClientTest.php index d866c299b..c75ca7bfd 100644 --- a/src/platform/tests/Bridge/OpenAi/DallE/ModelClientTest.php +++ b/src/platform/tests/Bridge/OpenAi/DallE/ModelClientTest.php @@ -78,4 +78,21 @@ public function testItIsExecutingTheCorrectRequest() $modelClient = new ModelClient($httpClient, 'sk-api-key'); $modelClient->request(new DallE(), 'foo', ['n' => 1, 'response_format' => 'url']); } + + #[TestWith(['EU', 'https://eu.api.openai.com/v1/images/generations'])] + #[TestWith(['US', 'https://us.api.openai.com/v1/images/generations'])] + #[TestWith([null, 'https://api.openai.com/v1/images/generations'])] + public function testItUsesCorrectBaseUrl(?string $region, string $expectedUrl) + { + $resultCallback = static function (string $method, string $url, array $options) use ($expectedUrl): HttpResponse { + self::assertSame('POST', $method); + self::assertSame($expectedUrl, $url); + self::assertSame('Authorization: Bearer sk-api-key', $options['normalized_headers']['authorization'][0]); + + return new MockResponse(); + }; + $httpClient = new MockHttpClient([$resultCallback]); + $modelClient = new ModelClient($httpClient, 'sk-api-key', $region); + $modelClient->request(new DallE(), 'foo', ['n' => 1, 'response_format' => 'url']); + } } diff --git a/src/platform/tests/Bridge/OpenAi/Embeddings/ModelClientTest.php b/src/platform/tests/Bridge/OpenAi/Embeddings/ModelClientTest.php index 33c4924ef..b502df891 100644 --- a/src/platform/tests/Bridge/OpenAi/Embeddings/ModelClientTest.php +++ b/src/platform/tests/Bridge/OpenAi/Embeddings/ModelClientTest.php @@ -111,4 +111,21 @@ public function testItIsExecutingTheCorrectRequestWithArrayInput() $modelClient = new ModelClient($httpClient, 'sk-api-key'); $modelClient->request(new Embeddings(), ['text1', 'text2', 'text3'], []); } + + #[TestWith(['EU', 'https://eu.api.openai.com/v1/embeddings'])] + #[TestWith(['US', 'https://us.api.openai.com/v1/embeddings'])] + #[TestWith([null, 'https://api.openai.com/v1/embeddings'])] + public function testItUsesCorrectBaseUrl(?string $region, string $expectedUrl) + { + $resultCallback = static function (string $method, string $url, array $options) use ($expectedUrl): HttpResponse { + self::assertSame('POST', $method); + self::assertSame($expectedUrl, $url); + self::assertSame('Authorization: Bearer sk-api-key', $options['normalized_headers']['authorization'][0]); + + return new MockResponse(); + }; + $httpClient = new MockHttpClient([$resultCallback]); + $modelClient = new ModelClient($httpClient, 'sk-api-key', $region); + $modelClient->request(new Embeddings(), 'test input', []); + } } diff --git a/src/platform/tests/Bridge/OpenAi/Gpt/ModelClientTest.php b/src/platform/tests/Bridge/OpenAi/Gpt/ModelClientTest.php index f72581adf..99bde7aa9 100644 --- a/src/platform/tests/Bridge/OpenAi/Gpt/ModelClientTest.php +++ b/src/platform/tests/Bridge/OpenAi/Gpt/ModelClientTest.php @@ -113,4 +113,21 @@ public function testItIsExecutingTheCorrectRequestWithArrayPayload() $modelClient = new ModelClient($httpClient, 'sk-api-key'); $modelClient->request(new Gpt(), ['model' => 'gpt-4o', 'messages' => [['role' => 'user', 'content' => 'Hello']]], ['temperature' => 0.7]); } + + #[TestWith(['EU', 'https://eu.api.openai.com/v1/chat/completions'])] + #[TestWith(['US', 'https://us.api.openai.com/v1/chat/completions'])] + #[TestWith([null, 'https://api.openai.com/v1/chat/completions'])] + public function testItUsesCorrectBaseUrl(?string $region, string $expectedUrl) + { + $resultCallback = static function (string $method, string $url, array $options) use ($expectedUrl): HttpResponse { + self::assertSame('POST', $method); + self::assertSame($expectedUrl, $url); + self::assertSame('Authorization: Bearer sk-api-key', $options['normalized_headers']['authorization'][0]); + + return new MockResponse(); + }; + $httpClient = new MockHttpClient([$resultCallback]); + $modelClient = new ModelClient($httpClient, 'sk-api-key', $region); + $modelClient->request(new Gpt(), ['messages' => []], []); + } } diff --git a/src/platform/tests/Bridge/OpenAi/Whisper/ModelClientTest.php b/src/platform/tests/Bridge/OpenAi/Whisper/ModelClientTest.php index 238c3bcec..0f641c5db 100644 --- a/src/platform/tests/Bridge/OpenAi/Whisper/ModelClientTest.php +++ b/src/platform/tests/Bridge/OpenAi/Whisper/ModelClientTest.php @@ -111,4 +111,44 @@ function ($method, $url): MockResponse { $this->assertSame(1, $httpClient->getRequestsCount()); } + + #[TestWith(['EU', 'https://eu.api.openai.com/v1/audio/transcriptions'])] + #[TestWith(['US', 'https://us.api.openai.com/v1/audio/transcriptions'])] + #[TestWith([null, 'https://api.openai.com/v1/audio/transcriptions'])] + public function testItUsesCorrectRegionUrlForTranscription(?string $region, string $expectedUrl) + { + $httpClient = new MockHttpClient([ + function ($method, $url) use ($expectedUrl): MockResponse { + self::assertSame('POST', $method); + self::assertSame($expectedUrl, $url); + + return new MockResponse('{"text": "Hello World"}'); + }, + ]); + + $client = new ModelClient($httpClient, 'sk-test-key', $region); + $client->request(new Whisper(), ['file' => 'audio-data']); + + $this->assertSame(1, $httpClient->getRequestsCount()); + } + + #[TestWith(['EU', 'https://eu.api.openai.com/v1/audio/translations'])] + #[TestWith(['US', 'https://us.api.openai.com/v1/audio/translations'])] + #[TestWith([null, 'https://api.openai.com/v1/audio/translations'])] + public function testItUsesCorrectRegionUrlForTranslation(?string $region, string $expectedUrl) + { + $httpClient = new MockHttpClient([ + function ($method, $url) use ($expectedUrl): MockResponse { + self::assertSame('POST', $method); + self::assertSame($expectedUrl, $url); + + return new MockResponse('{"text": "Hello World"}'); + }, + ]); + + $client = new ModelClient($httpClient, 'sk-test-key', $region); + $client->request(new Whisper(), ['file' => 'audio-data'], ['task' => Task::TRANSLATION]); + + $this->assertSame(1, $httpClient->getRequestsCount()); + } }