diff --git a/src/platform/src/Action.php b/src/platform/src/Action.php new file mode 100644 index 000000000..8e2ba49c7 --- /dev/null +++ b/src/platform/src/Action.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform; + +/** + * @author Joshua Behrens + */ +enum Action: string +{ + case CHAT = 'chat'; + case CALCULATE_EMBEDDINGS = 'embeddings'; + case COMPLETE_CHAT = 'chat-completion'; +} diff --git a/src/platform/src/Bridge/Albert/EmbeddingsModelClient.php b/src/platform/src/Bridge/Albert/EmbeddingsModelClient.php index 2c754db5f..a6a057dd6 100644 --- a/src/platform/src/Bridge/Albert/EmbeddingsModelClient.php +++ b/src/platform/src/Bridge/Albert/EmbeddingsModelClient.php @@ -11,7 +11,9 @@ namespace Symfony\AI\Platform\Bridge\Albert; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\OpenAi\Embeddings; +use Symfony\AI\Platform\Exception\InvalidActionArgumentException; use Symfony\AI\Platform\Exception\InvalidArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface; @@ -33,13 +35,21 @@ public function __construct( '' !== $baseUrl || throw new InvalidArgumentException('The base URL must not be empty.'); } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::CALCULATE_EMBEDDINGS !== $action) { + return false; + } + return $model instanceof Embeddings; } - public function request(Model $model, array|string $payload, array $options = []): RawResultInterface + public function request(Model $model, Action $action, array|string $payload, array $options = []): RawResultInterface { + if (Action::CALCULATE_EMBEDDINGS !== $action) { + return throw new InvalidActionArgumentException($model, $action, [Action::CALCULATE_EMBEDDINGS]); + } + return new RawHttpResult($this->httpClient->request('POST', \sprintf('%s/embeddings', $this->baseUrl), [ 'auth_bearer' => $this->apiKey, 'json' => \is_array($payload) ? array_merge($payload, $options) : $payload, diff --git a/src/platform/src/Bridge/Albert/GptModelClient.php b/src/platform/src/Bridge/Albert/GptModelClient.php index 311eee2ae..6d8a75629 100644 --- a/src/platform/src/Bridge/Albert/GptModelClient.php +++ b/src/platform/src/Bridge/Albert/GptModelClient.php @@ -11,7 +11,9 @@ namespace Symfony\AI\Platform\Bridge\Albert; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\OpenAi\Gpt; +use Symfony\AI\Platform\Exception\InvalidActionArgumentException; use Symfony\AI\Platform\Exception\InvalidArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface; @@ -38,13 +40,21 @@ public function __construct( '' !== $baseUrl || throw new InvalidArgumentException('The base URL must not be empty.'); } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return false; + } + return $model instanceof Gpt; } - public function request(Model $model, array|string $payload, array $options = []): RawResultInterface + public function request(Model $model, Action $action, array|string $payload, array $options = []): RawResultInterface { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return throw new InvalidActionArgumentException($model, $action, [Action::CHAT, Action::COMPLETE_CHAT]); + } + return new RawHttpResult($this->httpClient->request('POST', \sprintf('%s/chat/completions', $this->baseUrl), [ 'auth_bearer' => $this->apiKey, 'json' => \is_array($payload) ? array_merge($payload, $options) : $payload, diff --git a/src/platform/src/Bridge/Anthropic/ModelClient.php b/src/platform/src/Bridge/Anthropic/ModelClient.php index 978887b33..9de8f82fb 100644 --- a/src/platform/src/Bridge/Anthropic/ModelClient.php +++ b/src/platform/src/Bridge/Anthropic/ModelClient.php @@ -11,6 +11,8 @@ namespace Symfony\AI\Platform\Bridge\Anthropic; +use Symfony\AI\Platform\Action; +use Symfony\AI\Platform\Exception\InvalidActionArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface; use Symfony\AI\Platform\Result\RawHttpResult; @@ -32,13 +34,21 @@ public function __construct( $this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::CHAT !== $action) { + return false; + } + return $model instanceof Claude; } - public function request(Model $model, array|string $payload, array $options = []): RawHttpResult + public function request(Model $model, Action $action, array|string $payload, array $options = []): RawHttpResult { + if (Action::CHAT !== $action) { + return throw new InvalidActionArgumentException($model, $action, [Action::CHAT]); + } + if (isset($options['tools'])) { $options['tool_choice'] = ['type' => 'auto']; } diff --git a/src/platform/src/Bridge/Anthropic/ResultConverter.php b/src/platform/src/Bridge/Anthropic/ResultConverter.php index 92edde313..07ccad45b 100644 --- a/src/platform/src/Bridge/Anthropic/ResultConverter.php +++ b/src/platform/src/Bridge/Anthropic/ResultConverter.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\Anthropic; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\Result\RawHttpResult; @@ -31,8 +32,12 @@ */ class ResultConverter implements ResultConverterInterface { - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return false; + } + return $model instanceof Claude; } diff --git a/src/platform/src/Bridge/Azure/Meta/LlamaModelClient.php b/src/platform/src/Bridge/Azure/Meta/LlamaModelClient.php index b5f1b7eba..f87ddb8a0 100644 --- a/src/platform/src/Bridge/Azure/Meta/LlamaModelClient.php +++ b/src/platform/src/Bridge/Azure/Meta/LlamaModelClient.php @@ -11,7 +11,9 @@ namespace Symfony\AI\Platform\Bridge\Azure\Meta; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Meta\Llama; +use Symfony\AI\Platform\Exception\InvalidActionArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface; use Symfony\AI\Platform\Result\RawHttpResult; @@ -29,13 +31,21 @@ public function __construct( ) { } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return false; + } + return $model instanceof Llama; } - public function request(Model $model, array|string $payload, array $options = []): RawHttpResult + public function request(Model $model, Action $action, array|string $payload, array $options = []): RawHttpResult { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return throw new InvalidActionArgumentException($model, $action, [Action::CHAT, Action::COMPLETE_CHAT]); + } + $url = \sprintf('https://%s/chat/completions', $this->baseUrl); return new RawHttpResult($this->httpClient->request('POST', $url, [ diff --git a/src/platform/src/Bridge/Azure/Meta/LlamaResultConverter.php b/src/platform/src/Bridge/Azure/Meta/LlamaResultConverter.php index 28d1a4d0b..700b68ea9 100644 --- a/src/platform/src/Bridge/Azure/Meta/LlamaResultConverter.php +++ b/src/platform/src/Bridge/Azure/Meta/LlamaResultConverter.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\Azure\Meta; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Meta\Llama; use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Model; @@ -23,8 +24,12 @@ */ final readonly class LlamaResultConverter implements ResultConverterInterface { - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return false; + } + return $model instanceof Llama; } diff --git a/src/platform/src/Bridge/Azure/OpenAi/EmbeddingsModelClient.php b/src/platform/src/Bridge/Azure/OpenAi/EmbeddingsModelClient.php index 002d09171..e0ef9200d 100644 --- a/src/platform/src/Bridge/Azure/OpenAi/EmbeddingsModelClient.php +++ b/src/platform/src/Bridge/Azure/OpenAi/EmbeddingsModelClient.php @@ -11,7 +11,9 @@ namespace Symfony\AI\Platform\Bridge\Azure\OpenAi; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\OpenAi\Embeddings; +use Symfony\AI\Platform\Exception\InvalidActionArgumentException; use Symfony\AI\Platform\Exception\InvalidArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface; @@ -41,13 +43,21 @@ public function __construct( '' !== $apiKey || throw new InvalidArgumentException('The API key must not be empty.'); } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::CALCULATE_EMBEDDINGS !== $action) { + return false; + } + return $model instanceof Embeddings; } - public function request(Model $model, array|string $payload, array $options = []): RawHttpResult + public function request(Model $model, Action $action, array|string $payload, array $options = []): RawHttpResult { + if (Action::CALCULATE_EMBEDDINGS !== $action) { + return throw new InvalidActionArgumentException($model, $action, [Action::CALCULATE_EMBEDDINGS]); + } + $url = \sprintf('https://%s/openai/deployments/%s/embeddings', $this->baseUrl, $this->deployment); return new RawHttpResult($this->httpClient->request('POST', $url, [ diff --git a/src/platform/src/Bridge/Azure/OpenAi/GptModelClient.php b/src/platform/src/Bridge/Azure/OpenAi/GptModelClient.php index 504380469..a14fd7ace 100644 --- a/src/platform/src/Bridge/Azure/OpenAi/GptModelClient.php +++ b/src/platform/src/Bridge/Azure/OpenAi/GptModelClient.php @@ -11,7 +11,9 @@ namespace Symfony\AI\Platform\Bridge\Azure\OpenAi; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\OpenAi\Gpt; +use Symfony\AI\Platform\Exception\InvalidActionArgumentException; use Symfony\AI\Platform\Exception\InvalidArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface; @@ -41,13 +43,21 @@ public function __construct( '' !== $apiKey || throw new InvalidArgumentException('The API key must not be empty.'); } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return false; + } + return $model instanceof Gpt; } - public function request(Model $model, object|array|string $payload, array $options = []): RawHttpResult + public function request(Model $model, Action $action, object|array|string $payload, array $options = []): RawHttpResult { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return throw new InvalidActionArgumentException($model, $action, [Action::CHAT, Action::COMPLETE_CHAT]); + } + $url = \sprintf('https://%s/openai/deployments/%s/chat/completions', $this->baseUrl, $this->deployment); return new RawHttpResult($this->httpClient->request('POST', $url, [ diff --git a/src/platform/src/Bridge/Azure/OpenAi/WhisperModelClient.php b/src/platform/src/Bridge/Azure/OpenAi/WhisperModelClient.php index a5490c3bc..acfcb9590 100644 --- a/src/platform/src/Bridge/Azure/OpenAi/WhisperModelClient.php +++ b/src/platform/src/Bridge/Azure/OpenAi/WhisperModelClient.php @@ -11,8 +11,10 @@ namespace Symfony\AI\Platform\Bridge\Azure\OpenAi; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\OpenAi\Whisper; use Symfony\AI\Platform\Bridge\OpenAi\Whisper\Task; +use Symfony\AI\Platform\Exception\InvalidActionArgumentException; use Symfony\AI\Platform\Exception\InvalidArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface; @@ -42,13 +44,21 @@ public function __construct( '' !== $apiKey || throw new InvalidArgumentException('The API key must not be empty.'); } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::CHAT !== $action) { + return false; + } + return $model instanceof Whisper; } - public function request(Model $model, array|string $payload, array $options = []): RawHttpResult + public function request(Model $model, Action $action, array|string $payload, array $options = []): RawHttpResult { + if (Action::CHAT !== $action) { + return throw new InvalidActionArgumentException($model, $action, [Action::CHAT]); + } + $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); diff --git a/src/platform/src/Bridge/Bedrock/Anthropic/ClaudeModelClient.php b/src/platform/src/Bridge/Bedrock/Anthropic/ClaudeModelClient.php index a2be5356a..c526a46f6 100644 --- a/src/platform/src/Bridge/Bedrock/Anthropic/ClaudeModelClient.php +++ b/src/platform/src/Bridge/Bedrock/Anthropic/ClaudeModelClient.php @@ -14,8 +14,10 @@ use AsyncAws\BedrockRuntime\BedrockRuntimeClient; use AsyncAws\BedrockRuntime\Input\InvokeModelRequest; use AsyncAws\BedrockRuntime\Result\InvokeModelResponse; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Anthropic\Claude; use Symfony\AI\Platform\Bridge\Bedrock\RawBedrockResult; +use Symfony\AI\Platform\Exception\InvalidActionArgumentException; use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface; @@ -34,13 +36,21 @@ public function __construct( ) { } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return false; + } + return $model instanceof Claude; } - public function request(Model $model, array|string $payload, array $options = []): RawBedrockResult + public function request(Model $model, Action $action, array|string $payload, array $options = []): RawBedrockResult { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return throw new InvalidActionArgumentException($model, $action, [Action::CHAT, Action::COMPLETE_CHAT]); + } + unset($payload['model']); if (isset($options['tools'])) { diff --git a/src/platform/src/Bridge/Bedrock/Anthropic/ClaudeResultConverter.php b/src/platform/src/Bridge/Bedrock/Anthropic/ClaudeResultConverter.php index 4fe27ad8d..65d6fd567 100644 --- a/src/platform/src/Bridge/Bedrock/Anthropic/ClaudeResultConverter.php +++ b/src/platform/src/Bridge/Bedrock/Anthropic/ClaudeResultConverter.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\Bedrock\Anthropic; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Anthropic\Claude; use Symfony\AI\Platform\Bridge\Bedrock\RawBedrockResult; use Symfony\AI\Platform\Exception\RuntimeException; @@ -26,8 +27,12 @@ */ final readonly class ClaudeResultConverter implements ResultConverterInterface { - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return false; + } + return $model instanceof Claude; } diff --git a/src/platform/src/Bridge/Bedrock/Meta/LlamaModelClient.php b/src/platform/src/Bridge/Bedrock/Meta/LlamaModelClient.php index 2607b83e3..0a2c77f84 100644 --- a/src/platform/src/Bridge/Bedrock/Meta/LlamaModelClient.php +++ b/src/platform/src/Bridge/Bedrock/Meta/LlamaModelClient.php @@ -13,8 +13,10 @@ use AsyncAws\BedrockRuntime\BedrockRuntimeClient; use AsyncAws\BedrockRuntime\Input\InvokeModelRequest; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Bedrock\RawBedrockResult; use Symfony\AI\Platform\Bridge\Meta\Llama; +use Symfony\AI\Platform\Exception\InvalidActionArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface; @@ -28,13 +30,21 @@ public function __construct( ) { } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return false; + } + return $model instanceof Llama; } - public function request(Model $model, array|string $payload, array $options = []): RawBedrockResult + public function request(Model $model, Action $action, array|string $payload, array $options = []): RawBedrockResult { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return throw new InvalidActionArgumentException($model, $action, [Action::CHAT, Action::COMPLETE_CHAT]); + } + return new RawBedrockResult($this->bedrockRuntimeClient->invokeModel(new InvokeModelRequest([ 'modelId' => $this->getModelId($model), 'contentType' => 'application/json', diff --git a/src/platform/src/Bridge/Bedrock/Meta/LlamaResultConverter.php b/src/platform/src/Bridge/Bedrock/Meta/LlamaResultConverter.php index a1e0e5cf5..28a27ada2 100644 --- a/src/platform/src/Bridge/Bedrock/Meta/LlamaResultConverter.php +++ b/src/platform/src/Bridge/Bedrock/Meta/LlamaResultConverter.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\Bedrock\Meta; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Bedrock\RawBedrockResult; use Symfony\AI\Platform\Bridge\Meta\Llama; use Symfony\AI\Platform\Exception\RuntimeException; @@ -24,8 +25,12 @@ */ class LlamaResultConverter implements ResultConverterInterface { - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return false; + } + return $model instanceof Llama; } diff --git a/src/platform/src/Bridge/Bedrock/Nova/NovaModelClient.php b/src/platform/src/Bridge/Bedrock/Nova/NovaModelClient.php index a1990da62..a20ae0373 100644 --- a/src/platform/src/Bridge/Bedrock/Nova/NovaModelClient.php +++ b/src/platform/src/Bridge/Bedrock/Nova/NovaModelClient.php @@ -13,7 +13,9 @@ use AsyncAws\BedrockRuntime\BedrockRuntimeClient; use AsyncAws\BedrockRuntime\Input\InvokeModelRequest; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Bedrock\RawBedrockResult; +use Symfony\AI\Platform\Exception\InvalidActionArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface; @@ -27,13 +29,21 @@ public function __construct( ) { } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return false; + } + return $model instanceof Nova; } - public function request(Model $model, array|string $payload, array $options = []): RawBedrockResult + public function request(Model $model, Action $action, array|string $payload, array $options = []): RawBedrockResult { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return throw new InvalidActionArgumentException($model, $action, [Action::CHAT, Action::COMPLETE_CHAT]); + } + $modelOptions = []; if (isset($options['tools'])) { $modelOptions['toolConfig']['tools'] = $options['tools']; diff --git a/src/platform/src/Bridge/Bedrock/Nova/NovaResultConverter.php b/src/platform/src/Bridge/Bedrock/Nova/NovaResultConverter.php index bf2c1169d..f6f5aa94f 100644 --- a/src/platform/src/Bridge/Bedrock/Nova/NovaResultConverter.php +++ b/src/platform/src/Bridge/Bedrock/Nova/NovaResultConverter.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\Bedrock\Nova; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Bedrock\RawBedrockResult; use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Model; @@ -25,8 +26,12 @@ */ class NovaResultConverter implements ResultConverterInterface { - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return false; + } + return $model instanceof Nova; } diff --git a/src/platform/src/Bridge/Cerebras/ModelClient.php b/src/platform/src/Bridge/Cerebras/ModelClient.php index 2ff201b5b..506fd547b 100644 --- a/src/platform/src/Bridge/Cerebras/ModelClient.php +++ b/src/platform/src/Bridge/Cerebras/ModelClient.php @@ -11,6 +11,8 @@ namespace Symfony\AI\Platform\Bridge\Cerebras; +use Symfony\AI\Platform\Action; +use Symfony\AI\Platform\Exception\InvalidActionArgumentException; use Symfony\AI\Platform\Exception\InvalidArgumentException; use Symfony\AI\Platform\Model as BaseModel; use Symfony\AI\Platform\ModelClientInterface; @@ -40,13 +42,21 @@ public function __construct( $this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); } - public function supports(BaseModel $model): bool + public function supports(BaseModel $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return false; + } + return $model instanceof Model; } - public function request(BaseModel $model, array|string $payload, array $options = []): RawHttpResult + public function request(BaseModel $model, Action $action, array|string $payload, array $options = []): RawHttpResult { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return throw new InvalidActionArgumentException($model, $action, [Action::CHAT, Action::COMPLETE_CHAT]); + } + return new RawHttpResult( $this->httpClient->request( 'POST', 'https://api.cerebras.ai/v1/chat/completions', diff --git a/src/platform/src/Bridge/Cerebras/ResultConverter.php b/src/platform/src/Bridge/Cerebras/ResultConverter.php index d6310ba48..6e6b197ab 100644 --- a/src/platform/src/Bridge/Cerebras/ResultConverter.php +++ b/src/platform/src/Bridge/Cerebras/ResultConverter.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\Cerebras; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Model as BaseModel; use Symfony\AI\Platform\Result\RawHttpResult; @@ -29,8 +30,12 @@ */ final readonly class ResultConverter implements ResultConverterInterface { - public function supports(BaseModel $model): bool + public function supports(BaseModel $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return false; + } + return $model instanceof Model; } diff --git a/src/platform/src/Bridge/Gemini/Embeddings/ModelClient.php b/src/platform/src/Bridge/Gemini/Embeddings/ModelClient.php index 237a1d693..6134bad2d 100644 --- a/src/platform/src/Bridge/Gemini/Embeddings/ModelClient.php +++ b/src/platform/src/Bridge/Gemini/Embeddings/ModelClient.php @@ -11,7 +11,9 @@ namespace Symfony\AI\Platform\Bridge\Gemini\Embeddings; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Gemini\Embeddings; +use Symfony\AI\Platform\Exception\InvalidActionArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface; use Symfony\AI\Platform\Result\RawHttpResult; @@ -29,13 +31,21 @@ public function __construct( ) { } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::CALCULATE_EMBEDDINGS !== $action) { + return false; + } + return $model instanceof Embeddings; } - public function request(Model $model, array|string $payload, array $options = []): RawHttpResult + public function request(Model $model, Action $action, array|string $payload, array $options = []): RawHttpResult { + if (Action::CALCULATE_EMBEDDINGS !== $action) { + return throw new InvalidActionArgumentException($model, $action, [Action::CALCULATE_EMBEDDINGS]); + } + $url = \sprintf('https://generativelanguage.googleapis.com/v1beta/models/%s:%s', $model->getName(), 'batchEmbedContents'); $modelOptions = $model->getOptions(); diff --git a/src/platform/src/Bridge/Gemini/Embeddings/ResultConverter.php b/src/platform/src/Bridge/Gemini/Embeddings/ResultConverter.php index 518c2ccff..95045b70b 100644 --- a/src/platform/src/Bridge/Gemini/Embeddings/ResultConverter.php +++ b/src/platform/src/Bridge/Gemini/Embeddings/ResultConverter.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\Gemini\Embeddings; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Gemini\Embeddings; use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Model; @@ -24,8 +25,12 @@ */ final readonly class ResultConverter implements ResultConverterInterface { - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::CALCULATE_EMBEDDINGS !== $action) { + return false; + } + return $model instanceof Embeddings; } diff --git a/src/platform/src/Bridge/Gemini/Gemini/ModelClient.php b/src/platform/src/Bridge/Gemini/Gemini/ModelClient.php index b40a1bbcb..e1bcb8810 100644 --- a/src/platform/src/Bridge/Gemini/Gemini/ModelClient.php +++ b/src/platform/src/Bridge/Gemini/Gemini/ModelClient.php @@ -11,7 +11,9 @@ namespace Symfony\AI\Platform\Bridge\Gemini\Gemini; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Gemini\Gemini; +use Symfony\AI\Platform\Exception\InvalidActionArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface; use Symfony\AI\Platform\Result\RawHttpResult; @@ -33,16 +35,25 @@ public function __construct( $this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return false; + } + return $model instanceof Gemini; } /** + * @param Action $action * @throws TransportExceptionInterface When the HTTP request fails due to network issues */ - public function request(Model $model, array|string $payload, array $options = []): RawHttpResult + public function request(Model $model, Action $action, array|string $payload, array $options = []): RawHttpResult { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return throw new InvalidActionArgumentException($model, $action, [Action::CHAT, Action::COMPLETE_CHAT]); + } + $url = \sprintf( 'https://generativelanguage.googleapis.com/v1beta/models/%s:%s', $model->getName(), diff --git a/src/platform/src/Bridge/Gemini/Gemini/ResultConverter.php b/src/platform/src/Bridge/Gemini/Gemini/ResultConverter.php index da8fe5c23..b00e93ca9 100644 --- a/src/platform/src/Bridge/Gemini/Gemini/ResultConverter.php +++ b/src/platform/src/Bridge/Gemini/Gemini/ResultConverter.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\Gemini\Gemini; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Gemini\Gemini; use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Model; @@ -31,8 +32,12 @@ */ final readonly class ResultConverter implements ResultConverterInterface { - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return false; + } + return $model instanceof Gemini; } diff --git a/src/platform/src/Bridge/HuggingFace/ModelClient.php b/src/platform/src/Bridge/HuggingFace/ModelClient.php index 783e76ab9..5f11fe7bd 100644 --- a/src/platform/src/Bridge/HuggingFace/ModelClient.php +++ b/src/platform/src/Bridge/HuggingFace/ModelClient.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\HuggingFace; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface as PlatformModelClient; use Symfony\AI\Platform\Result\RawHttpResult; @@ -33,15 +34,16 @@ public function __construct( $this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { return true; } /** * The difference in HuggingFace here is that we treat the payload as the options for the request not only the body. + * @param Action $action */ - public function request(Model $model, array|string $payload, array $options = []): RawHttpResult + public function request(Model $model, Action $action, array|string $payload, array $options = []): RawHttpResult { // Extract task from options if provided $task = $options['task'] ?? null; diff --git a/src/platform/src/Bridge/HuggingFace/ResultConverter.php b/src/platform/src/Bridge/HuggingFace/ResultConverter.php index 44829509b..a8b9ca861 100644 --- a/src/platform/src/Bridge/HuggingFace/ResultConverter.php +++ b/src/platform/src/Bridge/HuggingFace/ResultConverter.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\HuggingFace; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\HuggingFace\Output\ClassificationResult; use Symfony\AI\Platform\Bridge\HuggingFace\Output\FillMaskResult; use Symfony\AI\Platform\Bridge\HuggingFace\Output\ImageSegmentationResult; @@ -38,7 +39,7 @@ */ final readonly class ResultConverter implements PlatformResponseConverter { - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { return true; } diff --git a/src/platform/src/Bridge/LmStudio/Completions/ModelClient.php b/src/platform/src/Bridge/LmStudio/Completions/ModelClient.php index dc422e25c..c70ecec37 100644 --- a/src/platform/src/Bridge/LmStudio/Completions/ModelClient.php +++ b/src/platform/src/Bridge/LmStudio/Completions/ModelClient.php @@ -11,7 +11,9 @@ namespace Symfony\AI\Platform\Bridge\LmStudio\Completions; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\LmStudio\Completions; +use Symfony\AI\Platform\Exception\InvalidActionArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface as PlatformResponseFactory; use Symfony\AI\Platform\Result\RawHttpResult; @@ -32,13 +34,21 @@ public function __construct( $this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action) { + return false; + } + return $model instanceof Completions; } - public function request(Model $model, array|string $payload, array $options = []): RawHttpResult + public function request(Model $model, Action $action, array|string $payload, array $options = []): RawHttpResult { + if (Action::COMPLETE_CHAT !== $action) { + return throw new InvalidActionArgumentException($model, $action, [Action::CHAT, Action::COMPLETE_CHAT]); + } + return new RawHttpResult($this->httpClient->request('POST', \sprintf('%s/v1/chat/completions', $this->hostUrl), [ 'json' => array_merge($options, $payload), ])); diff --git a/src/platform/src/Bridge/LmStudio/Completions/ResultConverter.php b/src/platform/src/Bridge/LmStudio/Completions/ResultConverter.php index b9c7ad373..531165631 100644 --- a/src/platform/src/Bridge/LmStudio/Completions/ResultConverter.php +++ b/src/platform/src/Bridge/LmStudio/Completions/ResultConverter.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\LmStudio\Completions; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\LmStudio\Completions; use Symfony\AI\Platform\Bridge\OpenAi\Gpt\ResultConverter as OpenAiResponseConverter; use Symfony\AI\Platform\Model; @@ -28,8 +29,12 @@ public function __construct( ) { } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action) { + return false; + } + return $model instanceof Completions; } diff --git a/src/platform/src/Bridge/LmStudio/Embeddings/ModelClient.php b/src/platform/src/Bridge/LmStudio/Embeddings/ModelClient.php index c705c8a71..fda938bad 100644 --- a/src/platform/src/Bridge/LmStudio/Embeddings/ModelClient.php +++ b/src/platform/src/Bridge/LmStudio/Embeddings/ModelClient.php @@ -11,7 +11,9 @@ namespace Symfony\AI\Platform\Bridge\LmStudio\Embeddings; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\LmStudio\Embeddings; +use Symfony\AI\Platform\Exception\InvalidActionArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface as PlatformResponseFactory; use Symfony\AI\Platform\Result\RawHttpResult; @@ -29,13 +31,21 @@ public function __construct( ) { } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::CALCULATE_EMBEDDINGS !== $action) { + return false; + } + return $model instanceof Embeddings; } - public function request(Model $model, array|string $payload, array $options = []): RawHttpResult + public function request(Model $model, Action $action, array|string $payload, array $options = []): RawHttpResult { + if (Action::CALCULATE_EMBEDDINGS !== $action) { + return throw new InvalidActionArgumentException($model, $action, [Action::CALCULATE_EMBEDDINGS]); + } + return new RawHttpResult($this->httpClient->request('POST', \sprintf('%s/v1/embeddings', $this->hostUrl), [ 'json' => array_merge($options, [ 'model' => $model->getName(), diff --git a/src/platform/src/Bridge/LmStudio/Embeddings/ResultConverter.php b/src/platform/src/Bridge/LmStudio/Embeddings/ResultConverter.php index cf06c55bf..8bcddc15b 100644 --- a/src/platform/src/Bridge/LmStudio/Embeddings/ResultConverter.php +++ b/src/platform/src/Bridge/LmStudio/Embeddings/ResultConverter.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\LmStudio\Embeddings; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\LmStudio\Embeddings; use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Model; @@ -25,8 +26,12 @@ */ final class ResultConverter implements ResultConverterInterface { - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::CALCULATE_EMBEDDINGS !== $action) { + return false; + } + return $model instanceof Embeddings; } diff --git a/src/platform/src/Bridge/Mistral/Embeddings/ModelClient.php b/src/platform/src/Bridge/Mistral/Embeddings/ModelClient.php index 5a2be7f91..8c21a1859 100644 --- a/src/platform/src/Bridge/Mistral/Embeddings/ModelClient.php +++ b/src/platform/src/Bridge/Mistral/Embeddings/ModelClient.php @@ -11,7 +11,9 @@ namespace Symfony\AI\Platform\Bridge\Mistral\Embeddings; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Mistral\Embeddings; +use Symfony\AI\Platform\Exception\InvalidActionArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface; use Symfony\AI\Platform\Result\RawHttpResult; @@ -33,13 +35,21 @@ public function __construct( $this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::CALCULATE_EMBEDDINGS !== $action) { + return false; + } + return $model instanceof Embeddings; } - public function request(Model $model, array|string $payload, array $options = []): RawHttpResult + public function request(Model $model, Action $action, array|string $payload, array $options = []): RawHttpResult { + if (Action::CALCULATE_EMBEDDINGS !== $action) { + return throw new InvalidActionArgumentException($model, $action, [Action::CALCULATE_EMBEDDINGS]); + } + return new RawHttpResult($this->httpClient->request('POST', 'https://api.mistral.ai/v1/embeddings', [ 'auth_bearer' => $this->apiKey, 'headers' => [ diff --git a/src/platform/src/Bridge/Mistral/Embeddings/ResultConverter.php b/src/platform/src/Bridge/Mistral/Embeddings/ResultConverter.php index 3f838d1ac..1f5bf1273 100644 --- a/src/platform/src/Bridge/Mistral/Embeddings/ResultConverter.php +++ b/src/platform/src/Bridge/Mistral/Embeddings/ResultConverter.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\Mistral\Embeddings; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Mistral\Embeddings; use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Model; @@ -25,8 +26,12 @@ */ final readonly class ResultConverter implements ResultConverterInterface { - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::CALCULATE_EMBEDDINGS !== $action) { + return false; + } + return $model instanceof Embeddings; } diff --git a/src/platform/src/Bridge/Mistral/Llm/ModelClient.php b/src/platform/src/Bridge/Mistral/Llm/ModelClient.php index a6b872336..f9276e8a1 100644 --- a/src/platform/src/Bridge/Mistral/Llm/ModelClient.php +++ b/src/platform/src/Bridge/Mistral/Llm/ModelClient.php @@ -11,7 +11,9 @@ namespace Symfony\AI\Platform\Bridge\Mistral\Llm; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Mistral\Mistral; +use Symfony\AI\Platform\Exception\InvalidActionArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface; use Symfony\AI\Platform\Result\RawHttpResult; @@ -33,13 +35,21 @@ public function __construct( $this->httpClient = $httpClient instanceof EventSourceHttpClient ? $httpClient : new EventSourceHttpClient($httpClient); } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return false; + } + return $model instanceof Mistral; } - public function request(Model $model, array|string $payload, array $options = []): RawHttpResult + public function request(Model $model, Action $action, array|string $payload, array $options = []): RawHttpResult { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return throw new InvalidActionArgumentException($model, $action, [Action::CHAT, Action::COMPLETE_CHAT]); + } + return new RawHttpResult($this->httpClient->request('POST', 'https://api.mistral.ai/v1/chat/completions', [ 'auth_bearer' => $this->apiKey, 'headers' => [ diff --git a/src/platform/src/Bridge/Mistral/Llm/ResultConverter.php b/src/platform/src/Bridge/Mistral/Llm/ResultConverter.php index a6b84026b..e727bbb2e 100644 --- a/src/platform/src/Bridge/Mistral/Llm/ResultConverter.php +++ b/src/platform/src/Bridge/Mistral/Llm/ResultConverter.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\Mistral\Llm; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Mistral\Mistral; use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Model; @@ -33,8 +34,12 @@ */ final readonly class ResultConverter implements ResultConverterInterface { - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return false; + } + return $model instanceof Mistral; } diff --git a/src/platform/src/Bridge/Ollama/OllamaClient.php b/src/platform/src/Bridge/Ollama/OllamaClient.php index 9fdf7b43e..73e5f3ed6 100644 --- a/src/platform/src/Bridge/Ollama/OllamaClient.php +++ b/src/platform/src/Bridge/Ollama/OllamaClient.php @@ -11,6 +11,8 @@ namespace Symfony\AI\Platform\Bridge\Ollama; +use Symfony\AI\Platform\Action; +use Symfony\AI\Platform\Exception\InvalidActionArgumentException; use Symfony\AI\Platform\Exception\InvalidArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface; @@ -28,12 +30,28 @@ public function __construct( ) { } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { - return $model instanceof Ollama; + $response = $this->httpClient->request('POST', \sprintf('%s/api/show', $this->hostUrl), [ + 'json' => [ + 'model' => $model->getName(), + ], + ]); + + $capabilities = $response->toArray()['capabilities'] ?? null; + + if (null === $capabilities) { + return false; + } + + return match (true) { + \in_array('completion', $capabilities, true) => Action::CHAT === $action, + \in_array('embedding', $capabilities, true) => Action::CALCULATE_EMBEDDINGS === $action, + default => false, + }; } - public function request(Model $model, array|string $payload, array $options = []): RawHttpResult + public function request(Model $model, Action $action, array|string $payload, array $options = []): RawHttpResult { $response = $this->httpClient->request('POST', \sprintf('%s/api/show', $this->hostUrl), [ 'json' => [ @@ -50,7 +68,7 @@ public function request(Model $model, array|string $payload, array $options = [] return match (true) { \in_array('completion', $capabilities, true) => $this->doCompletionRequest($payload, $options), \in_array('embedding', $capabilities, true) => $this->doEmbeddingsRequest($model, $payload, $options), - default => throw new InvalidArgumentException(\sprintf('Unsupported model "%s": "%s".', $model::class, $model->getName())), + default => throw new InvalidActionArgumentException($model, $action, [Action::CHAT, Action::COMPLETE_CHAT, Action::CALCULATE_EMBEDDINGS], new InvalidArgumentException(\sprintf('Unsupported model "%s": "%s".', $model::class, $model->getName()))), }; } diff --git a/src/platform/src/Bridge/Ollama/OllamaResultConverter.php b/src/platform/src/Bridge/Ollama/OllamaResultConverter.php index 30e40c8db..1a6460c06 100644 --- a/src/platform/src/Bridge/Ollama/OllamaResultConverter.php +++ b/src/platform/src/Bridge/Ollama/OllamaResultConverter.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\Ollama; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\Result\RawResultInterface; @@ -27,8 +28,12 @@ */ final readonly class OllamaResultConverter implements ResultConverterInterface { - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action && Action::CALCULATE_EMBEDDINGS !== $action) { + return false; + } + return $model instanceof Ollama; } diff --git a/src/platform/src/Bridge/OpenAi/DallE/ModelClient.php b/src/platform/src/Bridge/OpenAi/DallE/ModelClient.php index 2a8715f81..d322f2b35 100644 --- a/src/platform/src/Bridge/OpenAi/DallE/ModelClient.php +++ b/src/platform/src/Bridge/OpenAi/DallE/ModelClient.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\OpenAi\DallE; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\OpenAi\DallE; use Symfony\AI\Platform\Exception\InvalidArgumentException; use Symfony\AI\Platform\Model; @@ -34,12 +35,12 @@ public function __construct( str_starts_with($apiKey, 'sk-') || throw new InvalidArgumentException('The API key must start with "sk-".'); } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { return $model instanceof DallE; } - public function request(Model $model, array|string $payload, array $options = []): RawHttpResult + public function request(Model $model, Action $action, array|string $payload, array $options = []): RawHttpResult { return new RawHttpResult($this->httpClient->request('POST', 'https://api.openai.com/v1/images/generations', [ 'auth_bearer' => $this->apiKey, diff --git a/src/platform/src/Bridge/OpenAi/DallE/ResultConverter.php b/src/platform/src/Bridge/OpenAi/DallE/ResultConverter.php index 21e239e2a..339ed2a8b 100644 --- a/src/platform/src/Bridge/OpenAi/DallE/ResultConverter.php +++ b/src/platform/src/Bridge/OpenAi/DallE/ResultConverter.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\OpenAi\DallE; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\OpenAi\DallE; use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Model; @@ -25,7 +26,7 @@ */ final readonly class ResultConverter implements ResultConverterInterface { - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { return $model instanceof DallE; } diff --git a/src/platform/src/Bridge/OpenAi/Embeddings/ModelClient.php b/src/platform/src/Bridge/OpenAi/Embeddings/ModelClient.php index 66368f141..cf2d9fe37 100644 --- a/src/platform/src/Bridge/OpenAi/Embeddings/ModelClient.php +++ b/src/platform/src/Bridge/OpenAi/Embeddings/ModelClient.php @@ -11,7 +11,9 @@ namespace Symfony\AI\Platform\Bridge\OpenAi\Embeddings; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\OpenAi\Embeddings; +use Symfony\AI\Platform\Exception\InvalidActionArgumentException; use Symfony\AI\Platform\Exception\InvalidArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface as PlatformResponseFactory; @@ -32,13 +34,21 @@ public function __construct( str_starts_with($apiKey, 'sk-') || throw new InvalidArgumentException('The API key must start with "sk-".'); } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::CALCULATE_EMBEDDINGS !== $action) { + return false; + } + return $model instanceof Embeddings; } - public function request(Model $model, array|string $payload, array $options = []): RawHttpResult + public function request(Model $model, Action $action, array|string $payload, array $options = []): RawHttpResult { + if (Action::CALCULATE_EMBEDDINGS !== $action) { + return throw new InvalidActionArgumentException($model, $action, [Action::CALCULATE_EMBEDDINGS]); + } + return new RawHttpResult($this->httpClient->request('POST', 'https://api.openai.com/v1/embeddings', [ 'auth_bearer' => $this->apiKey, 'json' => array_merge($options, [ diff --git a/src/platform/src/Bridge/OpenAi/Embeddings/ResultConverter.php b/src/platform/src/Bridge/OpenAi/Embeddings/ResultConverter.php index cae4321fd..13c09ce91 100644 --- a/src/platform/src/Bridge/OpenAi/Embeddings/ResultConverter.php +++ b/src/platform/src/Bridge/OpenAi/Embeddings/ResultConverter.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\OpenAi\Embeddings; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\OpenAi\Embeddings; use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Model; @@ -25,8 +26,12 @@ */ final class ResultConverter implements ResultConverterInterface { - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::CALCULATE_EMBEDDINGS !== $action) { + return false; + } + return $model instanceof Embeddings; } diff --git a/src/platform/src/Bridge/OpenAi/Gpt/ModelClient.php b/src/platform/src/Bridge/OpenAi/Gpt/ModelClient.php index 086b2014e..bb22da743 100644 --- a/src/platform/src/Bridge/OpenAi/Gpt/ModelClient.php +++ b/src/platform/src/Bridge/OpenAi/Gpt/ModelClient.php @@ -11,7 +11,9 @@ namespace Symfony\AI\Platform\Bridge\OpenAi\Gpt; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\OpenAi\Gpt; +use Symfony\AI\Platform\Exception\InvalidActionArgumentException; use Symfony\AI\Platform\Exception\InvalidArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface as PlatformResponseFactory; @@ -36,13 +38,21 @@ public function __construct( str_starts_with($apiKey, 'sk-') || throw new InvalidArgumentException('The API key must start with "sk-".'); } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return false; + } + return $model instanceof Gpt; } - public function request(Model $model, array|string $payload, array $options = []): RawHttpResult + public function request(Model $model, Action $action, array|string $payload, array $options = []): RawHttpResult { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return throw new InvalidActionArgumentException($model, $action, [Action::CHAT, Action::COMPLETE_CHAT]); + } + return new RawHttpResult($this->httpClient->request('POST', 'https://api.openai.com/v1/chat/completions', [ 'auth_bearer' => $this->apiKey, 'json' => array_merge($options, $payload), diff --git a/src/platform/src/Bridge/OpenAi/Gpt/ResultConverter.php b/src/platform/src/Bridge/OpenAi/Gpt/ResultConverter.php index 72714e817..901f7e5b3 100644 --- a/src/platform/src/Bridge/OpenAi/Gpt/ResultConverter.php +++ b/src/platform/src/Bridge/OpenAi/Gpt/ResultConverter.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\OpenAi\Gpt; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\OpenAi\Gpt; use Symfony\AI\Platform\Exception\ContentFilterException; use Symfony\AI\Platform\Exception\RuntimeException; @@ -35,8 +36,12 @@ */ final class ResultConverter implements PlatformResponseConverter { - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return false; + } + return $model instanceof Gpt; } diff --git a/src/platform/src/Bridge/OpenAi/Whisper/ModelClient.php b/src/platform/src/Bridge/OpenAi/Whisper/ModelClient.php index 31c75a0a1..9d9214fea 100644 --- a/src/platform/src/Bridge/OpenAi/Whisper/ModelClient.php +++ b/src/platform/src/Bridge/OpenAi/Whisper/ModelClient.php @@ -11,7 +11,9 @@ namespace Symfony\AI\Platform\Bridge\OpenAi\Whisper; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\OpenAi\Whisper; +use Symfony\AI\Platform\Exception\InvalidActionArgumentException; use Symfony\AI\Platform\Exception\InvalidArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface as BaseModelClient; @@ -31,13 +33,21 @@ public function __construct( '' !== $apiKey || throw new InvalidArgumentException('The API key must not be empty.'); } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return false; + } + return $model instanceof Whisper; } - public function request(Model $model, array|string $payload, array $options = []): RawHttpResult + public function request(Model $model, Action $action, array|string $payload, array $options = []): RawHttpResult { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return throw new InvalidActionArgumentException($model, $action, [Action::CHAT, Action::COMPLETE_CHAT]); + } + $task = $options['task'] ?? Task::TRANSCRIPTION; $endpoint = Task::TRANSCRIPTION === $task ? 'transcriptions' : 'translations'; unset($options['task']); diff --git a/src/platform/src/Bridge/OpenAi/Whisper/ResultConverter.php b/src/platform/src/Bridge/OpenAi/Whisper/ResultConverter.php index 97ad909ba..92c9f6b1b 100644 --- a/src/platform/src/Bridge/OpenAi/Whisper/ResultConverter.php +++ b/src/platform/src/Bridge/OpenAi/Whisper/ResultConverter.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\OpenAi\Whisper; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\OpenAi\Whisper; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\Result\RawResultInterface; @@ -23,8 +24,12 @@ */ final class ResultConverter implements BaseResponseConverter { - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return false; + } + return $model instanceof Whisper; } diff --git a/src/platform/src/Bridge/OpenRouter/ModelClient.php b/src/platform/src/Bridge/OpenRouter/ModelClient.php index 913e1955a..efb1f9c2e 100644 --- a/src/platform/src/Bridge/OpenRouter/ModelClient.php +++ b/src/platform/src/Bridge/OpenRouter/ModelClient.php @@ -11,6 +11,8 @@ namespace Symfony\AI\Platform\Bridge\OpenRouter; +use Symfony\AI\Platform\Action; +use Symfony\AI\Platform\Exception\InvalidActionArgumentException; use Symfony\AI\Platform\Exception\InvalidArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface; @@ -34,13 +36,21 @@ public function __construct( str_starts_with($apiKey, 'sk-') || throw new InvalidArgumentException('The API key must start with "sk-".'); } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return false; + } + return true; } - public function request(Model $model, array|string $payload, array $options = []): RawHttpResult + public function request(Model $model, Action $action, array|string $payload, array $options = []): RawHttpResult { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return throw new InvalidActionArgumentException($model, $action, [Action::CHAT, Action::COMPLETE_CHAT]); + } + return new RawHttpResult($this->httpClient->request('POST', 'https://openrouter.ai/api/v1/chat/completions', [ 'auth_bearer' => $this->apiKey, 'json' => array_merge($options, $payload), diff --git a/src/platform/src/Bridge/OpenRouter/ResultConverter.php b/src/platform/src/Bridge/OpenRouter/ResultConverter.php index 79ab8b8c3..aa54ada2f 100644 --- a/src/platform/src/Bridge/OpenRouter/ResultConverter.php +++ b/src/platform/src/Bridge/OpenRouter/ResultConverter.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\OpenRouter; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\Result\RawResultInterface; @@ -23,8 +24,12 @@ */ final readonly class ResultConverter implements ResultConverterInterface { - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return false; + } + return true; } diff --git a/src/platform/src/Bridge/Replicate/LlamaModelClient.php b/src/platform/src/Bridge/Replicate/LlamaModelClient.php index 53af5d0e4..3383fe034 100644 --- a/src/platform/src/Bridge/Replicate/LlamaModelClient.php +++ b/src/platform/src/Bridge/Replicate/LlamaModelClient.php @@ -11,7 +11,9 @@ namespace Symfony\AI\Platform\Bridge\Replicate; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Meta\Llama; +use Symfony\AI\Platform\Exception\InvalidActionArgumentException; use Symfony\AI\Platform\Exception\InvalidArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface; @@ -27,13 +29,21 @@ public function __construct( ) { } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return false; + } + return $model instanceof Llama; } - public function request(Model $model, array|string $payload, array $options = []): RawHttpResult + public function request(Model $model, Action $action, array|string $payload, array $options = []): RawHttpResult { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return throw new InvalidActionArgumentException($model, $action, [Action::CHAT, Action::COMPLETE_CHAT]); + } + $model instanceof Llama || throw new InvalidArgumentException(\sprintf('The model must be an instance of "%s".', Llama::class)); return new RawHttpResult( diff --git a/src/platform/src/Bridge/Replicate/LlamaResultConverter.php b/src/platform/src/Bridge/Replicate/LlamaResultConverter.php index 18bb24abb..5b9af55c1 100644 --- a/src/platform/src/Bridge/Replicate/LlamaResultConverter.php +++ b/src/platform/src/Bridge/Replicate/LlamaResultConverter.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\Replicate; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Meta\Llama; use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Model; @@ -24,8 +25,12 @@ */ final readonly class LlamaResultConverter implements ResultConverterInterface { - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::CHAT !== $action) { + return false; + } + return $model instanceof Llama; } diff --git a/src/platform/src/Bridge/TransformersPhp/ModelClient.php b/src/platform/src/Bridge/TransformersPhp/ModelClient.php index 1650dca5f..d644e23e3 100644 --- a/src/platform/src/Bridge/TransformersPhp/ModelClient.php +++ b/src/platform/src/Bridge/TransformersPhp/ModelClient.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\TransformersPhp; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Exception\InvalidArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface; @@ -19,12 +20,12 @@ final readonly class ModelClient implements ModelClientInterface { - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { return true; } - public function request(Model $model, array|string $payload, array $options = []): RawPipelineResult + public function request(Model $model, Action $action, array|string $payload, array $options = []): RawPipelineResult { if (null === $task = $options['task'] ?? null) { throw new InvalidArgumentException('The task option is required.'); diff --git a/src/platform/src/Bridge/TransformersPhp/ResultConverter.php b/src/platform/src/Bridge/TransformersPhp/ResultConverter.php index 587844dbd..18205d174 100644 --- a/src/platform/src/Bridge/TransformersPhp/ResultConverter.php +++ b/src/platform/src/Bridge/TransformersPhp/ResultConverter.php @@ -12,6 +12,7 @@ namespace Symfony\AI\Platform\Bridge\TransformersPhp; use Codewithkyrian\Transformers\Pipelines\Task; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\Result\ObjectResult; use Symfony\AI\Platform\Result\RawResultInterface; @@ -20,8 +21,12 @@ final readonly class ResultConverter implements ResultConverterInterface { - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::COMPLETE_CHAT !== $action && Action::CHAT !== $action) { + return false; + } + return true; } diff --git a/src/platform/src/Bridge/Voyage/ModelClient.php b/src/platform/src/Bridge/Voyage/ModelClient.php index 3da2a5f66..827a27f17 100644 --- a/src/platform/src/Bridge/Voyage/ModelClient.php +++ b/src/platform/src/Bridge/Voyage/ModelClient.php @@ -11,6 +11,8 @@ namespace Symfony\AI\Platform\Bridge\Voyage; +use Symfony\AI\Platform\Action; +use Symfony\AI\Platform\Exception\InvalidActionArgumentException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface; use Symfony\AI\Platform\Result\RawHttpResult; @@ -27,13 +29,21 @@ public function __construct( ) { } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::CALCULATE_EMBEDDINGS !== $action) { + return false; + } + return $model instanceof Voyage; } - public function request(Model $model, object|string|array $payload, array $options = []): RawHttpResult + public function request(Model $model, Action $action, object|string|array $payload, array $options = []): RawHttpResult { + if (Action::CALCULATE_EMBEDDINGS !== $action) { + return throw new InvalidActionArgumentException($model, $action, [Action::CALCULATE_EMBEDDINGS]); + } + return new RawHttpResult($this->httpClient->request('POST', 'https://api.voyageai.com/v1/embeddings', [ 'auth_bearer' => $this->apiKey, 'json' => [ diff --git a/src/platform/src/Bridge/Voyage/ResultConverter.php b/src/platform/src/Bridge/Voyage/ResultConverter.php index a3b912506..b7112ad91 100644 --- a/src/platform/src/Bridge/Voyage/ResultConverter.php +++ b/src/platform/src/Bridge/Voyage/ResultConverter.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Platform\Bridge\Voyage; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Exception\RuntimeException; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\Result\RawResultInterface; @@ -24,8 +25,12 @@ */ final readonly class ResultConverter implements ResultConverterInterface { - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { + if (Action::CALCULATE_EMBEDDINGS !== $action) { + return false; + } + return $model instanceof Voyage; } diff --git a/src/platform/src/Exception/InvalidActionArgumentException.php b/src/platform/src/Exception/InvalidActionArgumentException.php new file mode 100644 index 000000000..41b476ede --- /dev/null +++ b/src/platform/src/Exception/InvalidActionArgumentException.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\AI\Platform\Exception; + +use Symfony\AI\Platform\Action; +use Symfony\AI\Platform\Model; + +/** + * @author Joshua Behrens + */ +class InvalidActionArgumentException extends InvalidArgumentException +{ + /** + * @param list $expectedActions + */ + public function __construct( + public readonly Model $model, + public readonly Action $invalidAction, + public readonly array $expectedActions, + ?\Throwable $previous = null, + int $code = 0, + ) { + $expectedAsString = implode(', ', array_map(static fn (Action $action): string => $action->name, $this->expectedActions)); + parent::__construct( + 'Tried invalid action ' . $this->invalidAction->name . ' on model ' . $this->model->getName() . ' where actions ' . $expectedAsString . ' where expected', + $code, + $previous, + ); + } +} diff --git a/src/platform/src/ModelClientInterface.php b/src/platform/src/ModelClientInterface.php index 877bbd7e4..85d2b6112 100644 --- a/src/platform/src/ModelClientInterface.php +++ b/src/platform/src/ModelClientInterface.php @@ -18,11 +18,11 @@ */ interface ModelClientInterface { - public function supports(Model $model): bool; + public function supports(Model $model, Action $action): bool; /** * @param array $payload * @param array $options */ - public function request(Model $model, array|string $payload, array $options = []): RawResultInterface; + public function request(Model $model, Action $action, array|string $payload, array $options = []): RawResultInterface; } diff --git a/src/platform/src/Platform.php b/src/platform/src/Platform.php index de5a16dc9..094c7b3aa 100644 --- a/src/platform/src/Platform.php +++ b/src/platform/src/Platform.php @@ -44,7 +44,7 @@ public function __construct( $this->resultConverters = $resultConverters instanceof \Traversable ? iterator_to_array($resultConverters) : $resultConverters; } - public function invoke(Model $model, array|string|object $input, array $options = []): ResultPromise + public function invoke(Model $model, array|string|object $input, array $options = [], Action $action = Action::CHAT): ResultPromise { $payload = $this->contract->createRequestPayload($model, $input); $options = array_merge($model->getOptions(), $options); @@ -53,20 +53,20 @@ public function invoke(Model $model, array|string|object $input, array $options $options['tools'] = $this->contract->createToolOption($options['tools'], $model); } - $result = $this->doInvoke($model, $payload, $options); + $result = $this->doInvoke($model, $action, $payload, $options); - return $this->convertResult($model, $result, $options); + return $this->convertResult($model, $action, $result, $options); } /** * @param array $payload * @param array $options */ - private function doInvoke(Model $model, array|string $payload, array $options = []): RawResultInterface + private function doInvoke(Model $model, Action $action, array|string $payload, array $options = []): RawResultInterface { foreach ($this->modelClients as $modelClient) { - if ($modelClient->supports($model)) { - return $modelClient->request($model, $payload, $options); + if ($modelClient->supports($model, $action)) { + return $modelClient->request($model, $action, $payload, $options); } } @@ -76,10 +76,10 @@ private function doInvoke(Model $model, array|string $payload, array $options = /** * @param array $options */ - private function convertResult(Model $model, RawResultInterface $result, array $options): ResultPromise + private function convertResult(Model $model, Action $action, RawResultInterface $result, array $options): ResultPromise { foreach ($this->resultConverters as $resultConverter) { - if ($resultConverter->supports($model)) { + if ($resultConverter->supports($model, $action)) { return new ResultPromise($resultConverter->convert(...), $result, $options); } } diff --git a/src/platform/src/ResultConverterInterface.php b/src/platform/src/ResultConverterInterface.php index b7a5ffba8..7a0c996cc 100644 --- a/src/platform/src/ResultConverterInterface.php +++ b/src/platform/src/ResultConverterInterface.php @@ -19,7 +19,7 @@ */ interface ResultConverterInterface { - public function supports(Model $model): bool; + public function supports(Model $model, Action $action): bool; /** * @param array $options diff --git a/src/platform/tests/Bridge/Albert/EmbeddingsModelClientTest.php b/src/platform/tests/Bridge/Albert/EmbeddingsModelClientTest.php index a915a7c8a..ce9ca4609 100644 --- a/src/platform/tests/Bridge/Albert/EmbeddingsModelClientTest.php +++ b/src/platform/tests/Bridge/Albert/EmbeddingsModelClientTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Albert\EmbeddingsModelClient; use Symfony\AI\Platform\Bridge\OpenAi\Embeddings; use Symfony\AI\Platform\Bridge\OpenAi\Gpt; @@ -59,7 +60,7 @@ public function testSupportsEmbeddingsModel() ); $embeddingsModel = new Embeddings('text-embedding-ada-002'); - $this->assertTrue($client->supports($embeddingsModel)); + $this->assertTrue($client->supports($embeddingsModel, Action::CALCULATE_EMBEDDINGS)); } public function testDoesNotSupportNonEmbeddingsModel() @@ -71,7 +72,7 @@ public function testDoesNotSupportNonEmbeddingsModel() ); $gptModel = new Gpt('gpt-3.5-turbo'); - $this->assertFalse($client->supports($gptModel)); + $this->assertFalse($client->supports($gptModel, Action::CALCULATE_EMBEDDINGS)); } #[DataProvider('providePayloadToJson')] @@ -91,7 +92,7 @@ public function testRequestSendsCorrectHttpRequest(array|string $payload, array ); $model = new Embeddings('text-embedding-ada-002'); - $result = $client->request($model, $payload, $options); + $result = $client->request($model, Action::CALCULATE_EMBEDDINGS, $payload, $options); $this->assertNotNull($capturedRequest); $this->assertSame('POST', $capturedRequest['method']); @@ -152,7 +153,7 @@ public function testRequestHandlesBaseUrlWithoutTrailingSlash() ); $model = new Embeddings('text-embedding-ada-002'); - $client->request($model, ['input' => 'test']); + $client->request($model, Action::CALCULATE_EMBEDDINGS, ['input' => 'test']); $this->assertSame('https://albert.example.com/v1/embeddings', $capturedUrl); } @@ -173,7 +174,7 @@ public function testRequestHandlesBaseUrlWithTrailingSlash() ); $model = new Embeddings('text-embedding-ada-002'); - $client->request($model, ['input' => 'test']); + $client->request($model, Action::CALCULATE_EMBEDDINGS, ['input' => 'test']); $this->assertSame('https://albert.example.com/v1/embeddings', $capturedUrl); } diff --git a/src/platform/tests/Bridge/Albert/GptModelClientTest.php b/src/platform/tests/Bridge/Albert/GptModelClientTest.php index 06c18a341..8fa423f28 100644 --- a/src/platform/tests/Bridge/Albert/GptModelClientTest.php +++ b/src/platform/tests/Bridge/Albert/GptModelClientTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Albert\GptModelClient; use Symfony\AI\Platform\Bridge\OpenAi\Embeddings; use Symfony\AI\Platform\Bridge\OpenAi\Gpt; @@ -69,7 +70,7 @@ public function testConstructorWrapsHttpClientInEventSourceHttpClient() $mockHttpClient->setResponseFactory([$mockResponse]); $model = new Gpt('gpt-3.5-turbo'); - $client->request($model, ['messages' => []]); + $client->request($model, Action::CHAT, ['messages' => []]); } public function testConstructorAcceptsEventSourceHttpClient() @@ -90,7 +91,7 @@ public function testConstructorAcceptsEventSourceHttpClient() $mockHttpClient->setResponseFactory([$mockResponse]); $model = new Gpt('gpt-3.5-turbo'); - $client->request($model, ['messages' => []]); + $client->request($model, Action::CHAT, ['messages' => []]); } public function testSupportsGptModel() @@ -102,7 +103,7 @@ public function testSupportsGptModel() ); $gptModel = new Gpt('gpt-3.5-turbo'); - $this->assertTrue($client->supports($gptModel)); + $this->assertTrue($client->supports($gptModel, Action::CHAT)); } public function testDoesNotSupportNonGptModel() @@ -114,7 +115,7 @@ public function testDoesNotSupportNonGptModel() ); $embeddingsModel = new Embeddings('text-embedding-ada-002'); - $this->assertFalse($client->supports($embeddingsModel)); + $this->assertFalse($client->supports($embeddingsModel, Action::CALCULATE_EMBEDDINGS)); } #[DataProvider('providePayloadToJson')] @@ -134,7 +135,7 @@ public function testRequestSendsCorrectHttpRequest(array|string $payload, array ); $model = new Gpt('gpt-3.5-turbo'); - $result = $client->request($model, $payload, $options); + $result = $client->request($model, Action::COMPLETE_CHAT, $payload, $options); $this->assertNotNull($capturedRequest); $this->assertSame('POST', $capturedRequest['method']); @@ -201,7 +202,7 @@ public function testRequestHandlesBaseUrlWithoutTrailingSlash() ); $model = new Gpt('gpt-3.5-turbo'); - $client->request($model, ['messages' => []]); + $client->request($model, Action::COMPLETE_CHAT, ['messages' => []]); $this->assertSame('https://albert.example.com/v1/chat/completions', $capturedUrl); } @@ -222,7 +223,7 @@ public function testRequestHandlesBaseUrlWithTrailingSlash() ); $model = new Gpt('gpt-3.5-turbo'); - $client->request($model, ['messages' => []]); + $client->request($model, Action::COMPLETE_CHAT, ['messages' => []]); $this->assertSame('https://albert.example.com/v1/chat/completions', $capturedUrl); } diff --git a/src/platform/tests/Bridge/Azure/OpenAi/EmbeddingsModelClientTest.php b/src/platform/tests/Bridge/Azure/OpenAi/EmbeddingsModelClientTest.php index f3cbc4f4c..11ff3ff74 100644 --- a/src/platform/tests/Bridge/Azure/OpenAi/EmbeddingsModelClientTest.php +++ b/src/platform/tests/Bridge/Azure/OpenAi/EmbeddingsModelClientTest.php @@ -16,6 +16,7 @@ use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Azure\OpenAi\EmbeddingsModelClient; use Symfony\AI\Platform\Bridge\OpenAi\Embeddings; use Symfony\AI\Platform\Exception\InvalidArgumentException; @@ -77,7 +78,7 @@ public function testItIsSupportingTheCorrectModel() { $client = new EmbeddingsModelClient(new MockHttpClient(), 'test.azure.com', 'deployment', '2023-12-01', 'api-key'); - $this->assertTrue($client->supports(new Embeddings())); + $this->assertTrue($client->supports(new Embeddings(), Action::CALCULATE_EMBEDDINGS)); } public function testItIsExecutingTheCorrectRequest() @@ -93,6 +94,6 @@ public function testItIsExecutingTheCorrectRequest() $httpClient = new MockHttpClient([$resultCallback]); $client = new EmbeddingsModelClient($httpClient, 'test.azure.com', 'embeddings-deployment', '2023-12-01', 'test-api-key'); - $client->request(new Embeddings(), 'Hello, world!'); + $client->request(new Embeddings(), Action::CALCULATE_EMBEDDINGS, 'Hello, world!'); } } diff --git a/src/platform/tests/Bridge/Azure/OpenAi/GptModelClientTest.php b/src/platform/tests/Bridge/Azure/OpenAi/GptModelClientTest.php index a51abffd8..40094e459 100644 --- a/src/platform/tests/Bridge/Azure/OpenAi/GptModelClientTest.php +++ b/src/platform/tests/Bridge/Azure/OpenAi/GptModelClientTest.php @@ -16,6 +16,7 @@ use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Azure\OpenAi\GptModelClient; use Symfony\AI\Platform\Bridge\OpenAi\Gpt; use Symfony\AI\Platform\Exception\InvalidArgumentException; @@ -77,7 +78,7 @@ public function testItIsSupportingTheCorrectModel() { $client = new GptModelClient(new MockHttpClient(), 'test.azure.com', 'deployment', '2023-12-01', 'api-key'); - $this->assertTrue($client->supports(new Gpt())); + $this->assertTrue($client->supports(new Gpt(), Action::CHAT)); } public function testItIsExecutingTheCorrectRequest() @@ -93,6 +94,6 @@ public function testItIsExecutingTheCorrectRequest() $httpClient = new MockHttpClient([$resultCallback]); $client = new GptModelClient($httpClient, 'test.azure.com', 'gpt-deployment', '2023-12-01', 'test-api-key'); - $client->request(new Gpt(), ['messages' => [['role' => 'user', 'content' => 'Hello']]]); + $client->request(new Gpt(), Action::COMPLETE_CHAT, ['messages' => [['role' => 'user', 'content' => 'Hello']]]); } } diff --git a/src/platform/tests/Bridge/Azure/OpenAi/WhisperModelClientTest.php b/src/platform/tests/Bridge/Azure/OpenAi/WhisperModelClientTest.php index 450d694f6..e855b40d4 100644 --- a/src/platform/tests/Bridge/Azure/OpenAi/WhisperModelClientTest.php +++ b/src/platform/tests/Bridge/Azure/OpenAi/WhisperModelClientTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Azure\OpenAi\WhisperModelClient; use Symfony\AI\Platform\Bridge\OpenAi\Whisper; use Symfony\AI\Platform\Bridge\OpenAi\Whisper\Task; @@ -80,7 +81,7 @@ public function testItSupportsWhisperModel() ); $model = new Whisper(); - $this->assertTrue($client->supports($model)); + $this->assertTrue($client->supports($model, Action::CHAT)); } public function testItUsesTranscriptionEndpointByDefault() @@ -98,7 +99,7 @@ function ($method, $url): MockResponse { $model = new Whisper(); $payload = ['file' => 'audio-data']; - $client->request($model, $payload); + $client->request($model, Action::CHAT, $payload); $this->assertSame(1, $httpClient->getRequestsCount()); } @@ -119,7 +120,7 @@ function ($method, $url): MockResponse { $payload = ['file' => 'audio-data']; $options = ['task' => Task::TRANSCRIPTION]; - $client->request($model, $payload, $options); + $client->request($model, Action::CHAT, $payload, $options); $this->assertSame(1, $httpClient->getRequestsCount()); } @@ -140,7 +141,7 @@ function ($method, $url): MockResponse { $payload = ['file' => 'audio-data']; $options = ['task' => Task::TRANSLATION]; - $client->request($model, $payload, $options); + $client->request($model, Action::CHAT, $payload, $options); $this->assertSame(1, $httpClient->getRequestsCount()); } diff --git a/src/platform/tests/Bridge/Cerebras/ModelClientTest.php b/src/platform/tests/Bridge/Cerebras/ModelClientTest.php index 14e97762b..e5d80234a 100644 --- a/src/platform/tests/Bridge/Cerebras/ModelClientTest.php +++ b/src/platform/tests/Bridge/Cerebras/ModelClientTest.php @@ -16,6 +16,7 @@ use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Cerebras\Model; use Symfony\AI\Platform\Bridge\Cerebras\ModelClient; use Symfony\AI\Platform\Exception\InvalidArgumentException; @@ -56,7 +57,7 @@ public function testItSupportsTheCorrectModel() { $client = new ModelClient(new MockHttpClient(), 'csk-1234567890abcdef'); - self::assertTrue($client->supports(new Model(Model::GPT_OSS_120B))); + self::assertTrue($client->supports(new Model(Model::GPT_OSS_120B), Action::CHAT)); } public function testItSuccessfullyInvokesTheModel() @@ -82,7 +83,7 @@ public function testItSuccessfullyInvokesTheModel() ], ]; - $result = $client->request(new Model(Model::LLAMA_3_3_70B), $payload); + $result = $client->request(new Model(Model::LLAMA_3_3_70B), Action::COMPLETE_CHAT, $payload); $data = $result->getData(); $info = $result->getObject()->getInfo(); diff --git a/src/platform/tests/Bridge/Cerebras/ResultConverterTest.php b/src/platform/tests/Bridge/Cerebras/ResultConverterTest.php index a7185a137..b72bb2de8 100644 --- a/src/platform/tests/Bridge/Cerebras/ResultConverterTest.php +++ b/src/platform/tests/Bridge/Cerebras/ResultConverterTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Cerebras\Model; use Symfony\AI\Platform\Bridge\Cerebras\ModelClient; use Symfony\AI\Platform\Bridge\Cerebras\ResultConverter; @@ -32,6 +33,6 @@ public function testItSupportsTheCorrectModel() { $client = new ModelClient(new MockHttpClient(), 'csk-1234567890abcdef'); - $this->assertTrue($client->supports(new Model(Model::GPT_OSS_120B))); + $this->assertTrue($client->supports(new Model(Model::GPT_OSS_120B), Action::CHAT)); } } diff --git a/src/platform/tests/Bridge/Gemini/Embeddings/ModelClientTest.php b/src/platform/tests/Bridge/Gemini/Embeddings/ModelClientTest.php index 5eda6889c..9b8fd416c 100644 --- a/src/platform/tests/Bridge/Gemini/Embeddings/ModelClientTest.php +++ b/src/platform/tests/Bridge/Gemini/Embeddings/ModelClientTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Gemini\Embeddings; use Symfony\AI\Platform\Bridge\Gemini\Embeddings\ModelClient; use Symfony\AI\Platform\Result\VectorResult; @@ -66,7 +67,7 @@ public function testItMakesARequestWithCorrectPayload() $model = new Embeddings(Embeddings::GEMINI_EMBEDDING_EXP_03_07, ['dimensions' => 1536, 'task_type' => 'CLASSIFICATION']); - $result = (new ModelClient($httpClient, 'test'))->request($model, ['payload1', 'payload2']); + $result = (new ModelClient($httpClient, 'test'))->request($model, Action::CALCULATE_EMBEDDINGS, ['payload1', 'payload2']); $this->assertSame(json_decode($this->getEmbeddingStub(), true), $result->getData()); } diff --git a/src/platform/tests/Bridge/LmStudio/Completions/ModelClientTest.php b/src/platform/tests/Bridge/LmStudio/Completions/ModelClientTest.php index b543f29ac..cb1c673d1 100644 --- a/src/platform/tests/Bridge/LmStudio/Completions/ModelClientTest.php +++ b/src/platform/tests/Bridge/LmStudio/Completions/ModelClientTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\LmStudio\Completions; use Symfony\AI\Platform\Bridge\LmStudio\Completions\ModelClient; use Symfony\Component\HttpClient\EventSourceHttpClient; @@ -31,7 +32,7 @@ public function testItIsSupportingTheCorrectModel() { $client = new ModelClient(new MockHttpClient(), 'http://localhost:1234'); - $this->assertTrue($client->supports(new Completions('test-model'))); + $this->assertTrue($client->supports(new Completions('test-model'), Action::CHAT)); } public function testItIsExecutingTheCorrectRequest() @@ -57,7 +58,7 @@ public function testItIsExecutingTheCorrectRequest() ], ]; - $client->request(new Completions('test-model'), $payload); + $client->request(new Completions('test-model'), Action::COMPLETE_CHAT, $payload); } public function testItMergesOptionsWithPayload() @@ -83,7 +84,7 @@ public function testItMergesOptionsWithPayload() ], ]; - $client->request(new Completions('test-model'), $payload, ['temperature' => 0.7]); + $client->request(new Completions('test-model'), Action::COMPLETE_CHAT, $payload, ['temperature' => 0.7]); } public function testItUsesEventSourceHttpClient() diff --git a/src/platform/tests/Bridge/LmStudio/Completions/ResultConverterTest.php b/src/platform/tests/Bridge/LmStudio/Completions/ResultConverterTest.php index 4e7c1c4b8..470ded198 100644 --- a/src/platform/tests/Bridge/LmStudio/Completions/ResultConverterTest.php +++ b/src/platform/tests/Bridge/LmStudio/Completions/ResultConverterTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\LmStudio\Completions; use Symfony\AI\Platform\Bridge\LmStudio\Completions\ResultConverter; use Symfony\AI\Platform\Bridge\OpenAi\Gpt\ResultConverter as OpenAiResultConverter; @@ -29,6 +30,6 @@ public function testItSupportsCompletionsModel() { $converter = new ResultConverter(); - $this->assertTrue($converter->supports(new Completions('test-model'))); + $this->assertTrue($converter->supports(new Completions('test-model'), Action::COMPLETE_CHAT)); } } diff --git a/src/platform/tests/Bridge/LmStudio/Embeddings/ModelClientTest.php b/src/platform/tests/Bridge/LmStudio/Embeddings/ModelClientTest.php index 1ab245d18..5af598aac 100644 --- a/src/platform/tests/Bridge/LmStudio/Embeddings/ModelClientTest.php +++ b/src/platform/tests/Bridge/LmStudio/Embeddings/ModelClientTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\LmStudio\Embeddings; use Symfony\AI\Platform\Bridge\LmStudio\Embeddings\ModelClient; use Symfony\Component\HttpClient\MockHttpClient; @@ -29,7 +30,7 @@ public function testItIsSupportingTheCorrectModel() { $client = new ModelClient(new MockHttpClient(), 'http://localhost:1234'); - $this->assertTrue($client->supports(new Embeddings('test-model'))); + $this->assertTrue($client->supports(new Embeddings('test-model'), Action::CALCULATE_EMBEDDINGS)); } public function testItIsExecutingTheCorrectRequest() @@ -47,7 +48,7 @@ public function testItIsExecutingTheCorrectRequest() $model = new Embeddings('test-model'); - $client->request($model, 'Hello, world!'); + $client->request($model, Action::CALCULATE_EMBEDDINGS, 'Hello, world!'); } public function testItMergesOptionsWithPayload() @@ -68,7 +69,7 @@ public function testItMergesOptionsWithPayload() $model = new Embeddings('test-model'); - $client->request($model, 'Hello, world!', ['custom_option' => 'value']); + $client->request($model, Action::CALCULATE_EMBEDDINGS, 'Hello, world!', ['custom_option' => 'value']); } public function testItHandlesArrayInput() @@ -86,6 +87,6 @@ public function testItHandlesArrayInput() $model = new Embeddings('test-model'); - $client->request($model, ['Hello', 'world']); + $client->request($model, Action::CALCULATE_EMBEDDINGS, ['Hello', 'world']); } } diff --git a/src/platform/tests/Bridge/LmStudio/Embeddings/ResultConverterTest.php b/src/platform/tests/Bridge/LmStudio/Embeddings/ResultConverterTest.php index 16b6b25d8..3a2e8bf48 100644 --- a/src/platform/tests/Bridge/LmStudio/Embeddings/ResultConverterTest.php +++ b/src/platform/tests/Bridge/LmStudio/Embeddings/ResultConverterTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\LmStudio\Embeddings; use Symfony\AI\Platform\Bridge\LmStudio\Embeddings\ResultConverter; use Symfony\AI\Platform\Exception\RuntimeException; @@ -84,6 +85,6 @@ public function testItSupportsEmbeddingsModel() { $converter = new ResultConverter(); - $this->assertTrue($converter->supports(new Embeddings('test-model'))); + $this->assertTrue($converter->supports(new Embeddings('test-model'), Action::CALCULATE_EMBEDDINGS)); } } diff --git a/src/platform/tests/Bridge/Ollama/OllamaResultConverterTest.php b/src/platform/tests/Bridge/Ollama/OllamaResultConverterTest.php index 791206200..035655c05 100644 --- a/src/platform/tests/Bridge/Ollama/OllamaResultConverterTest.php +++ b/src/platform/tests/Bridge/Ollama/OllamaResultConverterTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\Ollama\Ollama; use Symfony\AI\Platform\Bridge\Ollama\OllamaResultConverter; use Symfony\AI\Platform\Exception\RuntimeException; @@ -42,8 +43,8 @@ public function testSupportsLlamaModel() { $converter = new OllamaResultConverter(); - $this->assertTrue($converter->supports(new Ollama())); - $this->assertFalse($converter->supports(new Model('any-model'))); + $this->assertTrue($converter->supports(new Ollama(), Action::CHAT)); + $this->assertFalse($converter->supports(new Model('any-model'), Action::CHAT)); } public function testConvertTextResponse() diff --git a/src/platform/tests/Bridge/OpenAi/DallE/ModelClientTest.php b/src/platform/tests/Bridge/OpenAi/DallE/ModelClientTest.php index d866c299b..d4f514aaa 100644 --- a/src/platform/tests/Bridge/OpenAi/DallE/ModelClientTest.php +++ b/src/platform/tests/Bridge/OpenAi/DallE/ModelClientTest.php @@ -16,6 +16,7 @@ use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\OpenAi\DallE; use Symfony\AI\Platform\Bridge\OpenAi\DallE\ModelClient; use Symfony\AI\Platform\Exception\InvalidArgumentException; @@ -61,7 +62,7 @@ public function testItIsSupportingTheCorrectModel() { $modelClient = new ModelClient(new MockHttpClient(), 'sk-api-key'); - $this->assertTrue($modelClient->supports(new DallE())); + $this->assertTrue($modelClient->supports(new DallE(), Action::CHAT)); } public function testItIsExecutingTheCorrectRequest() @@ -76,6 +77,6 @@ public function testItIsExecutingTheCorrectRequest() }; $httpClient = new MockHttpClient([$resultCallback]); $modelClient = new ModelClient($httpClient, 'sk-api-key'); - $modelClient->request(new DallE(), 'foo', ['n' => 1, 'response_format' => 'url']); + $modelClient->request(new DallE(), Action::CHAT, 'foo', ['n' => 1, 'response_format' => 'url']); } } diff --git a/src/platform/tests/Bridge/OpenAi/Whisper/ModelClientTest.php b/src/platform/tests/Bridge/OpenAi/Whisper/ModelClientTest.php index d34e73ec6..0193a766c 100644 --- a/src/platform/tests/Bridge/OpenAi/Whisper/ModelClientTest.php +++ b/src/platform/tests/Bridge/OpenAi/Whisper/ModelClientTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Small; use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Bridge\OpenAi\Whisper; use Symfony\AI\Platform\Bridge\OpenAi\Whisper\ModelClient; use Symfony\AI\Platform\Bridge\OpenAi\Whisper\Task; @@ -29,7 +30,7 @@ public function testItSupportsWhisperModel() $client = new ModelClient(new MockHttpClient(), 'test-key'); $model = new Whisper(); - $this->assertTrue($client->supports($model)); + $this->assertTrue($client->supports($model, Action::CHAT)); } public function testItUsesTranscriptionEndpointByDefault() @@ -47,7 +48,7 @@ function ($method, $url): MockResponse { $model = new Whisper(); $payload = ['file' => 'audio-data']; - $client->request($model, $payload); + $client->request($model, Action::CHAT, $payload); $this->assertSame(1, $httpClient->getRequestsCount()); } @@ -68,7 +69,7 @@ function ($method, $url): MockResponse { $payload = ['file' => 'audio-data']; $options = ['task' => Task::TRANSCRIPTION]; - $client->request($model, $payload, $options); + $client->request($model, Action::CHAT, $payload, $options); $this->assertSame(1, $httpClient->getRequestsCount()); } @@ -89,7 +90,7 @@ function ($method, $url): MockResponse { $payload = ['file' => 'audio-data']; $options = ['task' => Task::TRANSLATION]; - $client->request($model, $payload, $options); + $client->request($model, Action::CHAT, $payload, $options); $this->assertSame(1, $httpClient->getRequestsCount()); } diff --git a/src/store/tests/Double/PlatformTestHandler.php b/src/store/tests/Double/PlatformTestHandler.php index 0a9406f35..363a77e3b 100644 --- a/src/store/tests/Double/PlatformTestHandler.php +++ b/src/store/tests/Double/PlatformTestHandler.php @@ -11,6 +11,7 @@ namespace Symfony\AI\Store\Tests\Double; +use Symfony\AI\Platform\Action; use Symfony\AI\Platform\Model; use Symfony\AI\Platform\ModelClientInterface; use Symfony\AI\Platform\Platform; @@ -38,12 +39,12 @@ public static function createPlatform(?ResultInterface $create = null): Platform return new Platform([$handler], [$handler]); } - public function supports(Model $model): bool + public function supports(Model $model, Action $action): bool { return true; } - public function request(Model $model, array|string|object $payload, array $options = []): RawHttpResult + public function request(Model $model, Action $action, array|string|object $payload, array $options = []): RawHttpResult { ++$this->createCalls;