mirror of
https://github.com/symfony/ai.git
synced 2026-03-23 23:42:18 +01:00
bug #1711 [Platform] Throw InvalidArgumentException when ModelClient receives a string payload (fabpot)
This PR was squashed before being merged into the main branch.
Discussion
----------
[Platform] Throw InvalidArgumentException when ModelClient receives a string payload
| Q | A
| ------------- | ---
| Bug fix? | yes
| New feature? | no <!-- please update src/**/CHANGELOG.md files -->
| Docs? | no <!-- required for new features -->
| Issues | n/a
| License | MIT
While working on #1644, I realized that passing a string to `ModelClientInterface::request()` was allowed, but most ModelClient implementations use `array_merge($options, $payload)` aithout checking if `$payload` is a string.
Commits
-------
e29a00bd [Platform] Throw InvalidArgumentException when ModelClient receives a string payload
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
|
||||
namespace Symfony\AI\Platform\Bridge\Anthropic;
|
||||
|
||||
use Symfony\AI\Platform\Exception\InvalidArgumentException;
|
||||
use Symfony\AI\Platform\Model;
|
||||
use Symfony\AI\Platform\ModelClientInterface;
|
||||
use Symfony\AI\Platform\Result\RawHttpResult;
|
||||
@@ -38,6 +39,10 @@ final class ModelClient implements ModelClientInterface
|
||||
|
||||
public function request(Model $model, array|string $payload, array $options = []): RawHttpResult
|
||||
{
|
||||
if (\is_string($payload)) {
|
||||
throw new InvalidArgumentException(\sprintf('Payload must be an array, but a string was given to "%s".', self::class));
|
||||
}
|
||||
|
||||
$headers = [
|
||||
'x-api-key' => $this->apiKey,
|
||||
'anthropic-version' => '2023-06-01',
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Symfony\AI\Platform\Bridge\Anthropic\Tests;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\AI\Platform\Bridge\Anthropic\Claude;
|
||||
use Symfony\AI\Platform\Bridge\Anthropic\ModelClient;
|
||||
use Symfony\AI\Platform\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
use Symfony\Component\HttpClient\Response\JsonMockResponse;
|
||||
|
||||
@@ -175,6 +176,16 @@ class ModelClientTest extends TestCase
|
||||
$this->modelClient->request($this->model, ['message' => 'test'], $options);
|
||||
}
|
||||
|
||||
public function testStringPayloadThrowsException()
|
||||
{
|
||||
$this->modelClient = new ModelClient(new MockHttpClient(), 'test-api-key');
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Payload must be an array, but a string was given');
|
||||
|
||||
$this->modelClient->request($this->model, 'string payload');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $headers
|
||||
*
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
namespace Symfony\AI\Platform\Bridge\Azure\Meta;
|
||||
|
||||
use Symfony\AI\Platform\Bridge\Meta\Llama;
|
||||
use Symfony\AI\Platform\Exception\InvalidArgumentException;
|
||||
use Symfony\AI\Platform\Model;
|
||||
use Symfony\AI\Platform\ModelClientInterface;
|
||||
use Symfony\AI\Platform\Result\RawHttpResult;
|
||||
@@ -36,6 +37,10 @@ final class LlamaModelClient implements ModelClientInterface
|
||||
|
||||
public function request(Model $model, array|string $payload, array $options = []): RawHttpResult
|
||||
{
|
||||
if (\is_string($payload)) {
|
||||
throw new InvalidArgumentException(\sprintf('Payload must be an array, but a string was given to "%s".', self::class));
|
||||
}
|
||||
|
||||
$url = \sprintf('https://%s/chat/completions', $this->baseUrl);
|
||||
|
||||
return new RawHttpResult($this->httpClient->request('POST', $url, [
|
||||
|
||||
@@ -58,6 +58,10 @@ final class CompletionsModelClient implements ModelClientInterface
|
||||
|
||||
public function request(Model $model, object|array|string $payload, array $options = []): RawHttpResult
|
||||
{
|
||||
if (!\is_array($payload)) {
|
||||
throw new InvalidArgumentException(\sprintf('Payload must be an array, but a %s was given to "%s".', get_debug_type($payload), self::class));
|
||||
}
|
||||
|
||||
$url = \sprintf('https://%s/openai/deployments/%s/chat/completions', $this->baseUrl, $this->deployment);
|
||||
|
||||
return new RawHttpResult($this->httpClient->request('POST', $url, [
|
||||
|
||||
@@ -59,6 +59,10 @@ final class WhisperModelClient implements ModelClientInterface
|
||||
|
||||
public function request(Model $model, array|string $payload, array $options = []): RawHttpResult
|
||||
{
|
||||
if (\is_string($payload)) {
|
||||
throw new InvalidArgumentException(\sprintf('Payload must be an array, but a string was given to "%s".', self::class));
|
||||
}
|
||||
|
||||
$task = $options['task'] ?? Task::TRANSCRIPTION;
|
||||
$endpoint = Task::TRANSCRIPTION === $task ? 'transcriptions' : 'translations';
|
||||
$url = \sprintf('https://%s/openai/deployments/%s/audio/%s', $this->baseUrl, $this->deployment, $endpoint);
|
||||
|
||||
@@ -15,6 +15,7 @@ use AsyncAws\BedrockRuntime\BedrockRuntimeClient;
|
||||
use AsyncAws\BedrockRuntime\Input\InvokeModelRequest;
|
||||
use Symfony\AI\Platform\Bridge\Anthropic\Claude;
|
||||
use Symfony\AI\Platform\Bridge\Bedrock\RawBedrockResult;
|
||||
use Symfony\AI\Platform\Exception\InvalidArgumentException;
|
||||
use Symfony\AI\Platform\Model;
|
||||
use Symfony\AI\Platform\ModelClientInterface;
|
||||
|
||||
@@ -36,6 +37,10 @@ final class ClaudeModelClient implements ModelClientInterface
|
||||
|
||||
public function request(Model $model, array|string $payload, array $options = []): RawBedrockResult
|
||||
{
|
||||
if (\is_string($payload)) {
|
||||
throw new InvalidArgumentException(\sprintf('Payload must be an array, but a string was given to "%s".', self::class));
|
||||
}
|
||||
|
||||
unset($payload['model']);
|
||||
|
||||
if (isset($options['tools'])) {
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Symfony\AI\Platform\Bridge\Bedrock\Nova;
|
||||
use AsyncAws\BedrockRuntime\BedrockRuntimeClient;
|
||||
use AsyncAws\BedrockRuntime\Input\InvokeModelRequest;
|
||||
use Symfony\AI\Platform\Bridge\Bedrock\RawBedrockResult;
|
||||
use Symfony\AI\Platform\Exception\InvalidArgumentException;
|
||||
use Symfony\AI\Platform\Model;
|
||||
use Symfony\AI\Platform\ModelClientInterface;
|
||||
|
||||
@@ -34,6 +35,10 @@ class NovaModelClient implements ModelClientInterface
|
||||
|
||||
public function request(Model $model, array|string $payload, array $options = []): RawBedrockResult
|
||||
{
|
||||
if (\is_string($payload)) {
|
||||
throw new InvalidArgumentException(\sprintf('Payload must be an array, but a string was given to "%s".', self::class));
|
||||
}
|
||||
|
||||
unset($payload['model']);
|
||||
|
||||
$modelOptions = [];
|
||||
|
||||
@@ -47,6 +47,10 @@ final class ModelClient implements ModelClientInterface
|
||||
|
||||
public function request(BaseModel $model, array|string $payload, array $options = []): RawHttpResult
|
||||
{
|
||||
if (\is_string($payload)) {
|
||||
throw new InvalidArgumentException(\sprintf('Payload must be an array, but a string was given to "%s".', self::class));
|
||||
}
|
||||
|
||||
return new RawHttpResult(
|
||||
$this->httpClient->request(
|
||||
'POST', 'https://api.cerebras.ai/v1/chat/completions',
|
||||
@@ -55,7 +59,7 @@ final class ModelClient implements ModelClientInterface
|
||||
'Content-Type' => 'application/json',
|
||||
'Authorization' => \sprintf('Bearer %s', $this->apiKey),
|
||||
],
|
||||
'json' => \is_array($payload) ? array_merge($payload, $options) : $payload,
|
||||
'json' => array_merge($payload, $options),
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
namespace Symfony\AI\Platform\Bridge\DeepSeek;
|
||||
|
||||
use Symfony\AI\Platform\Exception\InvalidArgumentException;
|
||||
use Symfony\AI\Platform\Model;
|
||||
use Symfony\AI\Platform\ModelClientInterface;
|
||||
use Symfony\AI\Platform\Result\RawHttpResult;
|
||||
@@ -38,6 +39,10 @@ final class ModelClient implements ModelClientInterface
|
||||
|
||||
public function request(Model $model, array|string $payload, array $options = []): RawHttpResult
|
||||
{
|
||||
if (\is_string($payload)) {
|
||||
throw new InvalidArgumentException(\sprintf('Payload must be an array, but a string was given to "%s".', self::class));
|
||||
}
|
||||
|
||||
return new RawHttpResult($this->httpClient->request('POST', 'https://api.deepseek.com/chat/completions', [
|
||||
'auth_bearer' => $this->apiKey,
|
||||
'json' => array_merge($options, $payload),
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
namespace Symfony\AI\Platform\Bridge\DockerModelRunner\Completions;
|
||||
|
||||
use Symfony\AI\Platform\Bridge\DockerModelRunner\Completions;
|
||||
use Symfony\AI\Platform\Exception\InvalidArgumentException;
|
||||
use Symfony\AI\Platform\Model;
|
||||
use Symfony\AI\Platform\ModelClientInterface;
|
||||
use Symfony\AI\Platform\Result\RawHttpResult;
|
||||
@@ -39,6 +40,10 @@ final class ModelClient implements ModelClientInterface
|
||||
|
||||
public function request(Model $model, array|string $payload, array $options = []): RawHttpResult
|
||||
{
|
||||
if (\is_string($payload)) {
|
||||
throw new InvalidArgumentException(\sprintf('Payload must be an array, but a string was given to "%s".', self::class));
|
||||
}
|
||||
|
||||
return new RawHttpResult($this->httpClient->request('POST', \sprintf('%s/engines/v1/chat/completions', $this->hostUrl), [
|
||||
'json' => array_merge($options, $payload),
|
||||
]));
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
namespace Symfony\AI\Platform\Bridge\Gemini\Gemini;
|
||||
|
||||
use Symfony\AI\Platform\Bridge\Gemini\Gemini;
|
||||
use Symfony\AI\Platform\Exception\InvalidArgumentException;
|
||||
use Symfony\AI\Platform\Model;
|
||||
use Symfony\AI\Platform\ModelClientInterface;
|
||||
use Symfony\AI\Platform\Result\RawHttpResult;
|
||||
@@ -44,6 +45,10 @@ final class ModelClient implements ModelClientInterface
|
||||
*/
|
||||
public function request(Model $model, array|string $payload, array $options = []): RawHttpResult
|
||||
{
|
||||
if (\is_string($payload)) {
|
||||
throw new InvalidArgumentException(\sprintf('Payload must be an array, but a string was given to "%s".', self::class));
|
||||
}
|
||||
|
||||
$url = \sprintf(
|
||||
'https://generativelanguage.googleapis.com/v1beta/models/%s:%s',
|
||||
$model->getName(),
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
namespace Symfony\AI\Platform\Bridge\Generic\Completions;
|
||||
|
||||
use Symfony\AI\Platform\Bridge\Generic\CompletionsModel;
|
||||
use Symfony\AI\Platform\Exception\InvalidArgumentException;
|
||||
use Symfony\AI\Platform\Model;
|
||||
use Symfony\AI\Platform\ModelClientInterface;
|
||||
use Symfony\AI\Platform\Result\RawHttpResult;
|
||||
@@ -44,6 +45,10 @@ class ModelClient implements ModelClientInterface
|
||||
|
||||
public function request(Model $model, array|string $payload, array $options = []): RawHttpResult
|
||||
{
|
||||
if (\is_string($payload)) {
|
||||
throw new InvalidArgumentException(\sprintf('Payload must be an array, but a string was given to "%s".', self::class));
|
||||
}
|
||||
|
||||
return new RawHttpResult($this->httpClient->request('POST', $this->baseUrl.$this->path, [
|
||||
'auth_bearer' => $this->apiKey,
|
||||
'headers' => ['Content-Type' => 'application/json'],
|
||||
|
||||
@@ -15,6 +15,7 @@ use PHPUnit\Framework\Attributes\TestWith;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\AI\Platform\Bridge\Generic\Completions\ModelClient;
|
||||
use Symfony\AI\Platform\Bridge\Generic\CompletionsModel;
|
||||
use Symfony\AI\Platform\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface as HttpResponse;
|
||||
@@ -31,6 +32,16 @@ final class ModelClientTest extends TestCase
|
||||
$this->assertTrue($modelClient->supports(new CompletionsModel('gpt-4o')));
|
||||
}
|
||||
|
||||
public function testStringPayloadThrowsException()
|
||||
{
|
||||
$modelClient = new ModelClient(new MockHttpClient(), 'http://localhost:8000');
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Payload must be an array, but a string was given');
|
||||
|
||||
$modelClient->request(new CompletionsModel('gpt-4o'), 'string payload');
|
||||
}
|
||||
|
||||
public function testItIsExecutingTheCorrectRequest()
|
||||
{
|
||||
$resultCallback = static function (string $method, string $url, array $options): HttpResponse {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
namespace Symfony\AI\Platform\Bridge\Mistral\Llm;
|
||||
|
||||
use Symfony\AI\Platform\Bridge\Mistral\Mistral;
|
||||
use Symfony\AI\Platform\Exception\InvalidArgumentException;
|
||||
use Symfony\AI\Platform\Model;
|
||||
use Symfony\AI\Platform\ModelClientInterface;
|
||||
use Symfony\AI\Platform\Result\RawHttpResult;
|
||||
@@ -39,6 +40,10 @@ final class ModelClient implements ModelClientInterface
|
||||
|
||||
public function request(Model $model, array|string $payload, array $options = []): RawHttpResult
|
||||
{
|
||||
if (\is_string($payload)) {
|
||||
throw new InvalidArgumentException(\sprintf('Payload must be an array, but a string was given to "%s".', self::class));
|
||||
}
|
||||
|
||||
return new RawHttpResult($this->httpClient->request('POST', 'https://api.mistral.ai/v1/chat/completions', [
|
||||
'auth_bearer' => $this->apiKey,
|
||||
'headers' => [
|
||||
|
||||
@@ -65,6 +65,10 @@ final class OllamaClient implements ModelClientInterface
|
||||
*/
|
||||
private function doCompletionRequest(array|string $payload, array $options = []): RawHttpResult
|
||||
{
|
||||
if (\is_string($payload)) {
|
||||
throw new InvalidArgumentException(\sprintf('Payload must be an array, but a string was given to "%s".', self::class));
|
||||
}
|
||||
|
||||
// Revert Ollama's default streaming behavior
|
||||
$options['stream'] ??= false;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ 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;
|
||||
use Symfony\AI\Platform\Result\RawHttpResult;
|
||||
@@ -43,6 +44,10 @@ final class ModelClient extends AbstractModelClient implements ModelClientInterf
|
||||
|
||||
public function request(Model $model, array|string $payload, array $options = []): RawHttpResult
|
||||
{
|
||||
if (\is_string($payload)) {
|
||||
throw new InvalidArgumentException(\sprintf('Payload must be an array, but a string was given to "%s".', self::class));
|
||||
}
|
||||
|
||||
if (isset($options[PlatformSubscriber::RESPONSE_FORMAT]['json_schema']['schema'])) {
|
||||
$schema = $options[PlatformSubscriber::RESPONSE_FORMAT]['json_schema'];
|
||||
$options['text']['format'] = $schema;
|
||||
|
||||
@@ -78,6 +78,16 @@ final class ModelClientTest extends TestCase
|
||||
$this->assertTrue($modelClient->supports(new Gpt('gpt-4o')));
|
||||
}
|
||||
|
||||
public function testStringPayloadThrowsException()
|
||||
{
|
||||
$modelClient = new ModelClient(new MockHttpClient(), 'sk-api-key');
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Payload must be an array, but a string was given');
|
||||
|
||||
$modelClient->request(new Gpt('gpt-4o'), 'string payload');
|
||||
}
|
||||
|
||||
public function testItIsExecutingTheCorrectRequest()
|
||||
{
|
||||
$resultCallback = static function (string $method, string $url, array $options): HttpResponse {
|
||||
|
||||
@@ -13,6 +13,7 @@ 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;
|
||||
use Symfony\AI\Platform\Result\RawHttpResult;
|
||||
@@ -38,6 +39,10 @@ final class ModelClient extends AbstractModelClient implements ModelClientInterf
|
||||
|
||||
public function request(Model $model, array|string $payload, array $options = []): RawHttpResult
|
||||
{
|
||||
if (\is_string($payload)) {
|
||||
throw new InvalidArgumentException(\sprintf('Payload must be an array, but a string was given to "%s".', self::class));
|
||||
}
|
||||
|
||||
$task = $options['task'] ?? Task::TRANSCRIPTION;
|
||||
$endpoint = Task::TRANSCRIPTION === $task ? 'transcriptions' : 'translations';
|
||||
unset($options['task']);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
namespace Symfony\AI\Platform\Bridge\OpenResponses;
|
||||
|
||||
use Symfony\AI\Platform\Exception\InvalidArgumentException;
|
||||
use Symfony\AI\Platform\Model;
|
||||
use Symfony\AI\Platform\ModelClientInterface;
|
||||
use Symfony\AI\Platform\Result\RawHttpResult;
|
||||
@@ -41,6 +42,10 @@ class ModelClient implements ModelClientInterface
|
||||
|
||||
public function request(Model $model, array|string $payload, array $options = []): RawHttpResult
|
||||
{
|
||||
if (\is_string($payload)) {
|
||||
throw new InvalidArgumentException(\sprintf('Payload must be an array, but a string was given to "%s".', self::class));
|
||||
}
|
||||
|
||||
if (isset($options[PlatformSubscriber::RESPONSE_FORMAT]['json_schema']['schema'])) {
|
||||
$schema = $options[PlatformSubscriber::RESPONSE_FORMAT]['json_schema'];
|
||||
$options['text']['format'] = $schema;
|
||||
|
||||
@@ -48,6 +48,10 @@ final class ModelClient implements ModelClientInterface
|
||||
|
||||
public function request(Model $model, array|string $payload, array $options = []): RawResultInterface
|
||||
{
|
||||
if (\is_string($payload)) {
|
||||
throw new InvalidArgumentException(\sprintf('Payload must be an array, but a string was given to "%s".', self::class));
|
||||
}
|
||||
|
||||
return new RawHttpResult($this->httpClient->request('POST', 'https://api.perplexity.ai/chat/completions', [
|
||||
'auth_bearer' => $this->apiKey,
|
||||
'json' => array_merge($options, $payload),
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
namespace Symfony\AI\Platform\Bridge\Scaleway\Llm;
|
||||
|
||||
use Symfony\AI\Platform\Bridge\Scaleway\Scaleway;
|
||||
use Symfony\AI\Platform\Exception\InvalidArgumentException;
|
||||
use Symfony\AI\Platform\Model;
|
||||
use Symfony\AI\Platform\ModelClientInterface;
|
||||
use Symfony\AI\Platform\Result\RawHttpResult;
|
||||
@@ -39,6 +40,10 @@ final class ModelClient implements ModelClientInterface
|
||||
|
||||
public function request(Model $model, array|string $payload, array $options = []): RawHttpResult
|
||||
{
|
||||
if (\is_string($payload)) {
|
||||
throw new InvalidArgumentException(\sprintf('Payload must be an array, but a string was given to "%s".', self::class));
|
||||
}
|
||||
|
||||
return new RawHttpResult($this->httpClient->request('POST', 'https://api.scaleway.ai/v1/chat/completions', [
|
||||
'auth_bearer' => $this->apiKey,
|
||||
'json' => array_merge($options, $payload),
|
||||
|
||||
Reference in New Issue
Block a user