diff --git a/src/Builders/PromptBuilder.php b/src/Builders/PromptBuilder.php index 42dbe085..8273aca9 100644 --- a/src/Builders/PromptBuilder.php +++ b/src/Builders/PromptBuilder.php @@ -18,7 +18,6 @@ use WordPress\AiClient\Providers\Models\DTO\ModelRequirements; use WordPress\AiClient\Providers\Models\DTO\RequiredOption; use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum; -use WordPress\AiClient\Providers\Models\Enums\OptionEnum; use WordPress\AiClient\Providers\Models\ImageGeneration\Contracts\ImageGenerationModelInterface; use WordPress\AiClient\Providers\Models\SpeechGeneration\Contracts\SpeechGenerationModelInterface; use WordPress\AiClient\Providers\Models\TextGeneration\Contracts\TextGenerationModelInterface; @@ -510,79 +509,6 @@ public function asJsonResponse(?array $schema = null): self return $this; } - /** - * Gets the inferred model requirements based on prompt features. - * - * @since 0.1.0 - * - * @param CapabilityEnum $capability The capability the model must support. - * @return ModelRequirements The inferred requirements. - */ - private function getModelRequirements(CapabilityEnum $capability): ModelRequirements - { - $capabilities = [$capability]; - $inputModalities = []; - - // Check if we have chat history (multiple messages) - if (count($this->messages) > 1) { - $capabilities[] = CapabilityEnum::chatHistory(); - } - - // Analyze all messages to determine required input modalities - $hasFunctionMessageParts = false; - foreach ($this->messages as $message) { - foreach ($message->getParts() as $part) { - // Check for text input - if ($part->getType()->isText()) { - $inputModalities[] = ModalityEnum::text(); - } - - // Check for file inputs - if ($part->getType()->isFile()) { - $file = $part->getFile(); - - if ($file !== null) { - if ($file->isImage()) { - $inputModalities[] = ModalityEnum::image(); - } elseif ($file->isAudio()) { - $inputModalities[] = ModalityEnum::audio(); - } elseif ($file->isVideo()) { - $inputModalities[] = ModalityEnum::video(); - } elseif ($file->isDocument() || $file->isText()) { - $inputModalities[] = ModalityEnum::document(); - } - } - } - - // Check for function calls/responses (these might require special capabilities) - if ($part->getType()->isFunctionCall() || $part->getType()->isFunctionResponse()) { - $hasFunctionMessageParts = true; - } - } - } - - // Build required options from ModelConfig - $requiredOptions = $this->modelConfig->toRequiredOptions(); - - if ($hasFunctionMessageParts) { - // Add function declarations option if we have function calls/responses - $requiredOptions = $this->includeInRequiredOptions( - $requiredOptions, - new RequiredOption(OptionEnum::functionDeclarations(), true) - ); - } - - // Add input modalities if we have any inputs - $requiredOptions = $this->includeInRequiredOptions( - $requiredOptions, - new RequiredOption(OptionEnum::inputModalities(), $inputModalities) - ); - - return new ModelRequirements( - $capabilities, - $requiredOptions - ); - } /** * Infers the capability from configured output modalities. @@ -671,11 +597,11 @@ private function isSupported(?CapabilityEnum $intendedCapability = null): bool } // Build requirements with the specified capability - $requirements = $this->getModelRequirements($intendedCapability); + $requirements = ModelRequirements::fromPromptData($intendedCapability, $this->messages, $this->modelConfig); // If the model has been set, check if it meets the requirements if ($this->model !== null) { - return $this->model->metadata()->meetsRequirements($requirements); + return $requirements->areMetBy($this->model->metadata()); } try { @@ -1112,7 +1038,7 @@ protected function appendPartToMessages(MessagePart $part): void */ private function getConfiguredModel(CapabilityEnum $capability): ModelInterface { - $requirements = $this->getModelRequirements($capability); + $requirements = ModelRequirements::fromPromptData($capability, $this->messages, $this->modelConfig); // If a model has been explicitly set, return it if ($this->model !== null) { @@ -1333,20 +1259,6 @@ private function isMessagesList($value): bool * @param RequiredOption $option The option to potentially add. * @return list The updated list of required options. */ - private function includeInRequiredOptions(array $options, RequiredOption $option): array - { - // Check if an option with the same name already exists - foreach ($options as $existingOption) { - if ($existingOption->getName()->equals($option->getName())) { - // Option already exists, return unchanged list - return $options; - } - } - - // Add the new option - $options[] = $option; - return $options; - } /** * Includes output modalities if not already present. diff --git a/src/Providers/Models/DTO/ModelConfig.php b/src/Providers/Models/DTO/ModelConfig.php index 6b27d40c..4bcac66e 100644 --- a/src/Providers/Models/DTO/ModelConfig.php +++ b/src/Providers/Models/DTO/ModelConfig.php @@ -9,7 +9,6 @@ use WordPress\AiClient\Files\Enums\FileTypeEnum; use WordPress\AiClient\Files\Enums\MediaOrientationEnum; use WordPress\AiClient\Messages\Enums\ModalityEnum; -use WordPress\AiClient\Providers\Models\Enums\OptionEnum; use WordPress\AiClient\Tools\DTO\FunctionDeclaration; use WordPress\AiClient\Tools\DTO\WebSearch; @@ -962,138 +961,6 @@ static function (FunctionDeclaration $function_declaration): array { return $data; } - /** - * Converts the model configuration to required options. - * - * @since 0.1.0 - * - * @return list The required options. - */ - public function toRequiredOptions(): array - { - $requiredOptions = []; - - // Map properties that have corresponding OptionEnum values - if ($this->outputModalities !== null) { - $requiredOptions[] = new RequiredOption( - OptionEnum::outputModalities(), - $this->outputModalities - ); - } - - if ($this->systemInstruction !== null) { - $requiredOptions[] = new RequiredOption( - OptionEnum::systemInstruction(), - $this->systemInstruction - ); - } - - if ($this->candidateCount !== null) { - $requiredOptions[] = new RequiredOption( - OptionEnum::candidateCount(), - $this->candidateCount - ); - } - - if ($this->maxTokens !== null) { - $requiredOptions[] = new RequiredOption( - OptionEnum::maxTokens(), - $this->maxTokens - ); - } - - if ($this->temperature !== null) { - $requiredOptions[] = new RequiredOption( - OptionEnum::temperature(), - $this->temperature - ); - } - - if ($this->topP !== null) { - $requiredOptions[] = new RequiredOption( - OptionEnum::topP(), - $this->topP - ); - } - - if ($this->topK !== null) { - $requiredOptions[] = new RequiredOption( - OptionEnum::topK(), - $this->topK - ); - } - - if ($this->outputMimeType !== null) { - $requiredOptions[] = new RequiredOption( - OptionEnum::outputMimeType(), - $this->outputMimeType - ); - } - - if ($this->outputSchema !== null) { - $requiredOptions[] = new RequiredOption( - OptionEnum::outputSchema(), - $this->outputSchema - ); - } - - // Handle properties without OptionEnum values as custom options - // These would need to be handled specially by providers - if ($this->stopSequences !== null) { - $requiredOptions[] = new RequiredOption(OptionEnum::stopSequences(), $this->stopSequences); - } - - if ($this->presencePenalty !== null) { - $requiredOptions[] = new RequiredOption(OptionEnum::presencePenalty(), $this->presencePenalty); - } - - if ($this->frequencyPenalty !== null) { - $requiredOptions[] = new RequiredOption(OptionEnum::frequencyPenalty(), $this->frequencyPenalty); - } - - if ($this->logprobs !== null) { - $requiredOptions[] = new RequiredOption(OptionEnum::logprobs(), $this->logprobs); - } - - if ($this->topLogprobs !== null) { - $requiredOptions[] = new RequiredOption(OptionEnum::topLogprobs(), $this->topLogprobs); - } - - if ($this->functionDeclarations !== null) { - $requiredOptions[] = new RequiredOption(OptionEnum::functionDeclarations(), true); - } - - if ($this->webSearch !== null) { - $requiredOptions[] = new RequiredOption(OptionEnum::webSearch(), true); - } - - if ($this->outputFileType !== null) { - $requiredOptions[] = new RequiredOption(OptionEnum::outputFileType(), $this->outputFileType); - } - - if ($this->outputMediaOrientation !== null) { - $requiredOptions[] = new RequiredOption( - OptionEnum::outputMediaOrientation(), - $this->outputMediaOrientation - ); - } - - if ($this->outputMediaAspectRatio !== null) { - $requiredOptions[] = new RequiredOption( - OptionEnum::outputMediaAspectRatio(), - $this->outputMediaAspectRatio - ); - } - - // Add custom options as individual RequiredOptions - // Custom options don't have predefined OptionEnum values, so we use the customOptions enum - // with the actual key-value pair as the value - foreach ($this->customOptions as $key => $value) { - $requiredOptions[] = new RequiredOption(OptionEnum::customOptions(), [$key => $value]); - } - - return $requiredOptions; - } /** * {@inheritDoc} diff --git a/src/Providers/Models/DTO/ModelMetadata.php b/src/Providers/Models/DTO/ModelMetadata.php index ecdc7bf4..749061b0 100644 --- a/src/Providers/Models/DTO/ModelMetadata.php +++ b/src/Providers/Models/DTO/ModelMetadata.php @@ -54,15 +54,6 @@ class ModelMetadata extends AbstractDataTransferObject */ protected array $supportedOptions; - /** - * @var array Map of supported capabilities for O(1) lookups. - */ - private array $capabilitiesMap = []; - - /** - * @var array Map of supported options by name for O(1) lookups. - */ - private array $optionsMap = []; /** * Constructor. @@ -90,16 +81,6 @@ public function __construct(string $id, string $name, array $supportedCapabiliti $this->name = $name; $this->supportedCapabilities = $supportedCapabilities; $this->supportedOptions = $supportedOptions; - - // Build capability map for efficient lookups - foreach ($supportedCapabilities as $capability) { - $this->capabilitiesMap[$capability->value] = true; - } - - // Build options map for efficient lookups - foreach ($supportedOptions as $option) { - $this->optionsMap[$option->getName()->value] = $option; - } } /** @@ -209,42 +190,6 @@ public function toArray(): array ]; } - /** - * Checks whether this model meets the specified requirements. - * - * @since 0.1.0 - * - * @param ModelRequirements $requirements The requirements to check against. - * @return bool True if the model meets all requirements, false otherwise. - */ - public function meetsRequirements(ModelRequirements $requirements): bool - { - // Check if all required capabilities are supported using map lookup - foreach ($requirements->getRequiredCapabilities() as $requiredCapability) { - if (!isset($this->capabilitiesMap[$requiredCapability->value])) { - return false; - } - } - - // Check if all required options are supported with the specified values - foreach ($requirements->getRequiredOptions() as $requiredOption) { - // Use map lookup instead of linear search - if (!isset($this->optionsMap[$requiredOption->getName()->value])) { - return false; - } - - $supportedOption = $this->optionsMap[$requiredOption->getName()->value]; - - // Check if the required value is supported by this option - if (!$supportedOption->isSupportedValue($requiredOption->getValue())) { - return false; - } - } - - return true; - } - - /** * {@inheritDoc} * diff --git a/src/Providers/Models/DTO/ModelRequirements.php b/src/Providers/Models/DTO/ModelRequirements.php index fc8e37d4..f798e282 100644 --- a/src/Providers/Models/DTO/ModelRequirements.php +++ b/src/Providers/Models/DTO/ModelRequirements.php @@ -6,7 +6,10 @@ use InvalidArgumentException; use WordPress\AiClient\Common\AbstractDataTransferObject; +use WordPress\AiClient\Messages\DTO\Message; +use WordPress\AiClient\Messages\Enums\ModalityEnum; use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum; +use WordPress\AiClient\Providers\Models\Enums\OptionEnum; /** * Represents requirements that implementing code has for AI model selection. @@ -88,6 +91,290 @@ public function getRequiredOptions(): array return $this->requiredOptions; } + /** + * Checks whether the given model metadata meets these requirements. + * + * @since n.e.x.t + * + * @param ModelMetadata $metadata The model metadata to check against. + * @return bool True if the model meets all requirements, false otherwise. + */ + public function areMetBy(ModelMetadata $metadata): bool + { + // Create lookup maps for better performance (instead of nested foreach loops) + $capabilitiesMap = []; + foreach ($metadata->getSupportedCapabilities() as $capability) { + $capabilitiesMap[$capability->value] = $capability; + } + + $optionsMap = []; + foreach ($metadata->getSupportedOptions() as $option) { + $optionsMap[$option->getName()->value] = $option; + } + + // Check if all required capabilities are supported using map lookup + foreach ($this->requiredCapabilities as $requiredCapability) { + if (!isset($capabilitiesMap[$requiredCapability->value])) { + return false; + } + } + + // Check if all required options are supported with the specified values + foreach ($this->requiredOptions as $requiredOption) { + // Use map lookup instead of linear search + if (!isset($optionsMap[$requiredOption->getName()->value])) { + return false; + } + + $supportedOption = $optionsMap[$requiredOption->getName()->value]; + + // Check if the required value is supported by this option + if (!$supportedOption->isSupportedValue($requiredOption->getValue())) { + return false; + } + } + + return true; + } + + /** + * Creates ModelRequirements from prompt data and model configuration. + * + * @since n.e.x.t + * + * @param CapabilityEnum $capability The capability the model must support. + * @param list $messages The messages in the conversation. + * @param ModelConfig $modelConfig The model configuration. + * @return self The created requirements. + */ + public static function fromPromptData(CapabilityEnum $capability, array $messages, ModelConfig $modelConfig): self + { + // Start with base capability + $capabilities = [$capability]; + $inputModalities = []; + + // Check if we have chat history (multiple messages) + if (count($messages) > 1) { + $capabilities[] = CapabilityEnum::chatHistory(); + } + + // Analyze all messages to determine required input modalities + $hasFunctionMessageParts = false; + foreach ($messages as $message) { + foreach ($message->getParts() as $part) { + // Check for text input + if ($part->getType()->isText()) { + $inputModalities[] = ModalityEnum::text(); + } + + // Check for file inputs + if ($part->getType()->isFile()) { + $file = $part->getFile(); + + if ($file !== null) { + if ($file->isImage()) { + $inputModalities[] = ModalityEnum::image(); + } elseif ($file->isAudio()) { + $inputModalities[] = ModalityEnum::audio(); + } elseif ($file->isVideo()) { + $inputModalities[] = ModalityEnum::video(); + } elseif ($file->isDocument() || $file->isText()) { + $inputModalities[] = ModalityEnum::document(); + } + } + } + + // Check for function calls/responses (these might require special capabilities) + if ($part->getType()->isFunctionCall() || $part->getType()->isFunctionResponse()) { + $hasFunctionMessageParts = true; + } + } + } + + // Convert ModelConfig to RequiredOptions + $requiredOptions = self::toRequiredOptions($modelConfig); + + // Add additional options based on message analysis + if ($hasFunctionMessageParts) { + $requiredOptions = self::includeInRequiredOptions( + $requiredOptions, + new RequiredOption(OptionEnum::functionDeclarations(), true) + ); + } + + // Add input modalities if we have any inputs + if (!empty($inputModalities)) { + // Remove duplicates + $inputModalities = array_unique($inputModalities, SORT_REGULAR); + $requiredOptions = self::includeInRequiredOptions( + $requiredOptions, + new RequiredOption(OptionEnum::inputModalities(), array_values($inputModalities)) + ); + } + + // Step 6: Return new ModelRequirements + return new self($capabilities, $requiredOptions); + } + + /** + * Converts ModelConfig to an array of RequiredOptions. + * + * @since n.e.x.t + * + * @param ModelConfig $modelConfig The model configuration. + * @return list The required options. + */ + private static function toRequiredOptions(ModelConfig $modelConfig): array + { + $requiredOptions = []; + + // Map properties that have corresponding OptionEnum values + if ($modelConfig->getOutputModalities() !== null) { + $requiredOptions[] = new RequiredOption( + OptionEnum::outputModalities(), + $modelConfig->getOutputModalities() + ); + } + + if ($modelConfig->getSystemInstruction() !== null) { + $requiredOptions[] = new RequiredOption( + OptionEnum::systemInstruction(), + $modelConfig->getSystemInstruction() + ); + } + + if ($modelConfig->getCandidateCount() !== null) { + $requiredOptions[] = new RequiredOption( + OptionEnum::candidateCount(), + $modelConfig->getCandidateCount() + ); + } + + if ($modelConfig->getMaxTokens() !== null) { + $requiredOptions[] = new RequiredOption( + OptionEnum::maxTokens(), + $modelConfig->getMaxTokens() + ); + } + + if ($modelConfig->getTemperature() !== null) { + $requiredOptions[] = new RequiredOption( + OptionEnum::temperature(), + $modelConfig->getTemperature() + ); + } + + if ($modelConfig->getTopP() !== null) { + $requiredOptions[] = new RequiredOption( + OptionEnum::topP(), + $modelConfig->getTopP() + ); + } + + if ($modelConfig->getTopK() !== null) { + $requiredOptions[] = new RequiredOption( + OptionEnum::topK(), + $modelConfig->getTopK() + ); + } + + if ($modelConfig->getOutputMimeType() !== null) { + $requiredOptions[] = new RequiredOption( + OptionEnum::outputMimeType(), + $modelConfig->getOutputMimeType() + ); + } + + if ($modelConfig->getOutputSchema() !== null) { + $requiredOptions[] = new RequiredOption( + OptionEnum::outputSchema(), + $modelConfig->getOutputSchema() + ); + } + + // Handle properties without OptionEnum values as custom options + if ($modelConfig->getStopSequences() !== null) { + $requiredOptions[] = new RequiredOption(OptionEnum::stopSequences(), $modelConfig->getStopSequences()); + } + + if ($modelConfig->getPresencePenalty() !== null) { + $requiredOptions[] = new RequiredOption(OptionEnum::presencePenalty(), $modelConfig->getPresencePenalty()); + } + + if ($modelConfig->getFrequencyPenalty() !== null) { + $requiredOptions[] = new RequiredOption( + OptionEnum::frequencyPenalty(), + $modelConfig->getFrequencyPenalty() + ); + } + + if ($modelConfig->getLogprobs() !== null) { + $requiredOptions[] = new RequiredOption(OptionEnum::logprobs(), $modelConfig->getLogprobs()); + } + + if ($modelConfig->getTopLogprobs() !== null) { + $requiredOptions[] = new RequiredOption(OptionEnum::topLogprobs(), $modelConfig->getTopLogprobs()); + } + + if ($modelConfig->getFunctionDeclarations() !== null) { + $requiredOptions[] = new RequiredOption(OptionEnum::functionDeclarations(), true); + } + + if ($modelConfig->getWebSearch() !== null) { + $requiredOptions[] = new RequiredOption(OptionEnum::webSearch(), true); + } + + if ($modelConfig->getOutputFileType() !== null) { + $requiredOptions[] = new RequiredOption(OptionEnum::outputFileType(), $modelConfig->getOutputFileType()); + } + + if ($modelConfig->getOutputMediaOrientation() !== null) { + $requiredOptions[] = new RequiredOption( + OptionEnum::outputMediaOrientation(), + $modelConfig->getOutputMediaOrientation() + ); + } + + if ($modelConfig->getOutputMediaAspectRatio() !== null) { + $requiredOptions[] = new RequiredOption( + OptionEnum::outputMediaAspectRatio(), + $modelConfig->getOutputMediaAspectRatio() + ); + } + + // Add custom options as individual RequiredOptions + foreach ($modelConfig->getCustomOptions() as $key => $value) { + $requiredOptions[] = new RequiredOption(OptionEnum::customOptions(), [$key => $value]); + } + + return $requiredOptions; + } + + /** + * Includes a RequiredOption in the array, ensuring no duplicates based on option name. + * + * @since n.e.x.t + * + * @param list $requiredOptions The existing required options. + * @param RequiredOption $newOption The new option to include. + * @return list The updated required options array. + */ + private static function includeInRequiredOptions(array $requiredOptions, RequiredOption $newOption): array + { + // Check if we already have this option name + foreach ($requiredOptions as $index => $existingOption) { + if ($existingOption->getName()->equals($newOption->getName())) { + // Replace existing option with new one + $requiredOptions[$index] = $newOption; + return $requiredOptions; + } + } + + // Option not found, add it + $requiredOptions[] = $newOption; + return $requiredOptions; + } + /** * {@inheritDoc} * diff --git a/src/Providers/ProviderRegistry.php b/src/Providers/ProviderRegistry.php index 0e0db98c..86e2dd11 100644 --- a/src/Providers/ProviderRegistry.php +++ b/src/Providers/ProviderRegistry.php @@ -236,7 +236,7 @@ public function findProviderModelsMetadataForSupport( // Filter models that meet requirements $matchingModels = []; foreach ($modelMetadataDirectory->listModelMetadata() as $modelMetadata) { - if ($modelMetadata->meetsRequirements($modelRequirements)) { + if ($modelRequirements->areMetBy($modelMetadata)) { $matchingModels[] = $modelMetadata; } } diff --git a/tests/unit/Builders/PromptBuilderTest.php b/tests/unit/Builders/PromptBuilderTest.php index 8d5085b6..b2d699a6 100644 --- a/tests/unit/Builders/PromptBuilderTest.php +++ b/tests/unit/Builders/PromptBuilderTest.php @@ -23,6 +23,9 @@ use WordPress\AiClient\Providers\Models\DTO\ModelConfig; use WordPress\AiClient\Providers\Models\DTO\ModelMetadata; use WordPress\AiClient\Providers\Models\DTO\ModelRequirements; +use WordPress\AiClient\Providers\Models\DTO\SupportedOption; +use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum; +use WordPress\AiClient\Providers\Models\Enums\OptionEnum; use WordPress\AiClient\Providers\Models\SpeechGeneration\Contracts\SpeechGenerationModelInterface; use WordPress\AiClient\Providers\Models\TextGeneration\Contracts\TextGenerationModelInterface; use WordPress\AiClient\Providers\Models\TextToSpeechConversion\Contracts\TextToSpeechConversionModelInterface; @@ -1135,7 +1138,6 @@ public function testGenerateResultWithTextModality(): void $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createMockTextGenerationModel($result, $metadata); @@ -1166,7 +1168,6 @@ public function testGenerateResultWithImageModality(): void $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createMockImageGenerationModel($result, $metadata); @@ -1198,7 +1199,6 @@ public function testGenerateResultWithAudioModality(): void $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createSpeechGenerationModel($metadata, $result); @@ -1227,7 +1227,6 @@ public function testGenerateResultWithMultimodalOutput(): void $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createMockTextGenerationModel($result, $metadata); @@ -1248,7 +1247,6 @@ public function testGenerateResultThrowsExceptionForUnsupportedModality(): void { $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); // Model that only implements ModelInterface, not TextGenerationModelInterface $model = $this->createMock(ModelInterface::class); @@ -1272,7 +1270,6 @@ public function testGenerateResultThrowsExceptionForUnsupportedOutputModality(): { $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createMock(ModelInterface::class); $model->method('metadata')->willReturn($metadata); @@ -1304,7 +1301,6 @@ public function testGenerateTextResult(): void $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createMockTextGenerationModel($result, $metadata); @@ -1346,7 +1342,6 @@ public function testGenerateImageResult(): void $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createMockImageGenerationModel($result, $metadata); @@ -1388,7 +1383,6 @@ public function testGenerateSpeechResult(): void $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createSpeechGenerationModel($metadata, $result); @@ -1430,7 +1424,6 @@ public function testConvertTextToSpeechResult(): void $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createTextToSpeechModel($metadata, $result); @@ -1461,7 +1454,6 @@ public function testConvertTextToSpeechResultThrowsExceptionForUnsupportedModel( { $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); // Model that doesn't implement TextToSpeechConversionModelInterface $model = $this->createMock(ModelInterface::class); @@ -1497,7 +1489,6 @@ public function testGenerateText(): void $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createMockTextGenerationModel($result, $metadata); @@ -1519,7 +1510,6 @@ public function testGenerateTextThrowsExceptionWhenNoCandidates(): void // we need to create a mock that throws an exception or test a different scenario $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $providerMetadata = new ProviderMetadata( 'mock-provider', @@ -1604,7 +1594,6 @@ public function testGenerateTextThrowsExceptionWhenNoParts(): void $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createMockTextGenerationModel($result, $metadata); @@ -1639,7 +1628,6 @@ public function testGenerateTextThrowsExceptionWhenPartHasNoText(): void $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createMockTextGenerationModel($result, $metadata); @@ -1684,7 +1672,6 @@ public function testGenerateTexts(): void $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createMockTextGenerationModel($result, $metadata); @@ -1717,7 +1704,6 @@ public function testGenerateTextsThrowsExceptionWhenNoTextGenerated(): void { $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $providerMetadata = new ProviderMetadata( 'mock-provider', @@ -1804,7 +1790,6 @@ public function testGenerateImage(): void $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createMockImageGenerationModel($result, $metadata); @@ -1836,7 +1821,6 @@ public function testGenerateImageThrowsExceptionWhenNoFile(): void $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createMockImageGenerationModel($result, $metadata); @@ -1879,7 +1863,6 @@ public function testGenerateImages(): void $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createMockImageGenerationModel($result, $metadata); @@ -1915,7 +1898,6 @@ public function testConvertTextToSpeech(): void $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createTextToSpeechModel($metadata, $result); @@ -1956,7 +1938,6 @@ public function testConvertTextToSpeeches(): void $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createTextToSpeechModel($metadata, $result); @@ -1992,7 +1973,6 @@ public function testGenerateSpeech(): void $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createSpeechGenerationModel($metadata, $result); @@ -2035,7 +2015,6 @@ public function testGenerateSpeeches(): void $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createSpeechGenerationModel($metadata, $result); @@ -2243,7 +2222,6 @@ public function testIncludeOutputModalityPreservesExisting(): void $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createMockTextGenerationModel($result, $metadata); @@ -2387,7 +2365,6 @@ public function testGenerateImageResultCreatesProperOperation(): void $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createMockImageGenerationModel($result, $metadata); @@ -2452,7 +2429,6 @@ public function testGenerateImageReturnsFileDirectly(): void $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createMockImageGenerationModel($result, $metadata); @@ -2516,7 +2492,6 @@ public function testGenerateTextWithNoCandidatesThrowsException(): void $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createMockTextGenerationModel($result, $metadata); @@ -2552,7 +2527,6 @@ public function testGenerateTextWithNonStringPartThrowsException(): void $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createMockTextGenerationModel($result, $metadata); @@ -2585,7 +2559,15 @@ public function testIsSupportedForText(): void { $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('text-model'); - $metadata->method('meetsRequirements')->willReturn(true); + $metadata->method('getSupportedCapabilities')->willReturn([ + CapabilityEnum::textGeneration() + ]); + $metadata->method('getSupportedOptions')->willReturn([ + new SupportedOption(OptionEnum::inputModalities(), [ + [ModalityEnum::text()], + [ModalityEnum::text(), ModalityEnum::image()] + ]) + ]); $result = new GenerativeAiResult('test-id', [ new Candidate( @@ -2659,7 +2641,15 @@ public function testIsSupportedForSpeechGeneration(): void { $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('speech-model'); - $metadata->method('meetsRequirements')->willReturn(true); + $metadata->method('getSupportedCapabilities')->willReturn([ + CapabilityEnum::speechGeneration() + ]); + $metadata->method('getSupportedOptions')->willReturn([ + new SupportedOption(OptionEnum::inputModalities(), [ + [ModalityEnum::text()], + [ModalityEnum::text(), ModalityEnum::image()] + ]) + ]); $result = new GenerativeAiResult('test-id', [ new Candidate( @@ -2687,7 +2677,6 @@ public function testGenerateResultWithProvider(): void $modelMetadata = $this->createMock(ModelMetadata::class); $modelMetadata->method('getId')->willReturn('provider-model'); - $modelMetadata->method('meetsRequirements')->willReturn(true); $model = $this->createMockTextGenerationModel($result, $modelMetadata); @@ -2742,7 +2731,6 @@ public function testModelTakesPrecedenceOverProvider(): void $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('explicit-model'); - $metadata->method('meetsRequirements')->willReturn(true); $model = $this->createMockTextGenerationModel($result, $metadata); diff --git a/tests/unit/Providers/Models/DTO/ModelConfigTest.php b/tests/unit/Providers/Models/DTO/ModelConfigTest.php index e99f0585..e875f993 100644 --- a/tests/unit/Providers/Models/DTO/ModelConfigTest.php +++ b/tests/unit/Providers/Models/DTO/ModelConfigTest.php @@ -12,7 +12,6 @@ use WordPress\AiClient\Files\Enums\MediaOrientationEnum; use WordPress\AiClient\Messages\Enums\ModalityEnum; use WordPress\AiClient\Providers\Models\DTO\ModelConfig; -use WordPress\AiClient\Providers\Models\Enums\OptionEnum; use WordPress\AiClient\Tools\DTO\FunctionDeclaration; use WordPress\AiClient\Tools\DTO\WebSearch; @@ -704,246 +703,4 @@ public function testSetCustomOption(): void $restored = ModelConfig::fromArray($array); $this->assertEquals($customOptions, $restored->getCustomOptions()); } - - /** - * Tests toRequiredOptions method with all properties. - * - * @return void - */ - public function testToRequiredOptionsWithAllProperties(): void - { - $config = new ModelConfig(); - - // Set all properties that map to RequiredOptions - $config->setOutputModalities([ModalityEnum::text(), ModalityEnum::image()]); - $config->setSystemInstruction('Be helpful'); - $config->setCandidateCount(2); - $config->setMaxTokens(1000); - $config->setTemperature(0.7); - $config->setTopP(0.9); - $config->setTopK(40); - $config->setStopSequences(['STOP', 'END']); - $config->setPresencePenalty(0.5); - $config->setFrequencyPenalty(0.3); - $config->setLogprobs(true); - $config->setTopLogprobs(5); - $config->setFunctionDeclarations([$this->createSampleFunctionDeclaration()]); - $config->setWebSearch($this->createSampleWebSearch()); - $config->setOutputFileType(FileTypeEnum::inline()); - $config->setOutputMimeType('application/json'); - $config->setOutputSchema(['type' => 'object']); - $config->setOutputMediaOrientation(MediaOrientationEnum::landscape()); - $config->setOutputMediaAspectRatio('16:9'); - $config->setCustomOptions(['key1' => 'value1', 'key2' => 'value2']); - - $requiredOptions = $config->toRequiredOptions(); - - $this->assertIsArray($requiredOptions); - $this->assertNotEmpty($requiredOptions); - - // Helper function to find option by name - /** - * @param list<\WordPress\AiClient\Providers\Models\DTO\RequiredOption> $options - * @param OptionEnum $name - * @return \WordPress\AiClient\Providers\Models\DTO\RequiredOption|null - */ - $findOption = function ( - array $options, - OptionEnum $name - ): ?\WordPress\AiClient\Providers\Models\DTO\RequiredOption { - foreach ($options as $option) { - if ($option->getName()->equals($name)) { - return $option; - } - } - return null; - }; - - // Test output modalities - $outputModalitiesOption = $findOption($requiredOptions, OptionEnum::outputModalities()); - $this->assertNotNull($outputModalitiesOption); - $this->assertEquals([ModalityEnum::text(), ModalityEnum::image()], $outputModalitiesOption->getValue()); - - // Test system instruction - $systemInstructionOption = $findOption($requiredOptions, OptionEnum::systemInstruction()); - $this->assertNotNull($systemInstructionOption); - $this->assertEquals('Be helpful', $systemInstructionOption->getValue()); - - // Test candidate count - $candidateCountOption = $findOption($requiredOptions, OptionEnum::candidateCount()); - $this->assertNotNull($candidateCountOption); - $this->assertEquals(2, $candidateCountOption->getValue()); - - // Test max tokens - $maxTokensOption = $findOption($requiredOptions, OptionEnum::maxTokens()); - $this->assertNotNull($maxTokensOption); - $this->assertEquals(1000, $maxTokensOption->getValue()); - - // Test temperature - $temperatureOption = $findOption($requiredOptions, OptionEnum::temperature()); - $this->assertNotNull($temperatureOption); - $this->assertEquals(0.7, $temperatureOption->getValue()); - - // Test top-p - $topPOption = $findOption($requiredOptions, OptionEnum::topP()); - $this->assertNotNull($topPOption); - $this->assertEquals(0.9, $topPOption->getValue()); - - // Test top-k - $topKOption = $findOption($requiredOptions, OptionEnum::topK()); - $this->assertNotNull($topKOption); - $this->assertEquals(40, $topKOption->getValue()); - - // Test stop sequences - $stopSequencesOption = $findOption($requiredOptions, OptionEnum::stopSequences()); - $this->assertNotNull($stopSequencesOption); - $this->assertEquals(['STOP', 'END'], $stopSequencesOption->getValue()); - - // Test presence penalty - $presencePenaltyOption = $findOption($requiredOptions, OptionEnum::presencePenalty()); - $this->assertNotNull($presencePenaltyOption); - $this->assertEquals(0.5, $presencePenaltyOption->getValue()); - - // Test frequency penalty - $frequencyPenaltyOption = $findOption($requiredOptions, OptionEnum::frequencyPenalty()); - $this->assertNotNull($frequencyPenaltyOption); - $this->assertEquals(0.3, $frequencyPenaltyOption->getValue()); - - // Test logprobs - $logprobsOption = $findOption($requiredOptions, OptionEnum::logprobs()); - $this->assertNotNull($logprobsOption); - $this->assertTrue($logprobsOption->getValue()); - - // Test top logprobs - $topLogprobsOption = $findOption($requiredOptions, OptionEnum::topLogprobs()); - $this->assertNotNull($topLogprobsOption); - $this->assertEquals(5, $topLogprobsOption->getValue()); - - // Test function declarations (should be boolean true) - $functionDeclarationsOption = $findOption($requiredOptions, OptionEnum::functionDeclarations()); - $this->assertNotNull($functionDeclarationsOption); - $this->assertTrue($functionDeclarationsOption->getValue()); - - // Test web search (should be boolean true) - $webSearchOption = $findOption($requiredOptions, OptionEnum::webSearch()); - $this->assertNotNull($webSearchOption); - $this->assertTrue($webSearchOption->getValue()); - - // Test output file type - IMPORTANT: Should be the enum object, not the string value - $outputFileTypeOption = $findOption($requiredOptions, OptionEnum::outputFileType()); - $this->assertNotNull($outputFileTypeOption); - $this->assertInstanceOf(FileTypeEnum::class, $outputFileTypeOption->getValue()); - $this->assertSame(FileTypeEnum::inline(), $outputFileTypeOption->getValue()); - - // Test output MIME type - $outputMimeTypeOption = $findOption($requiredOptions, OptionEnum::outputMimeType()); - $this->assertNotNull($outputMimeTypeOption); - $this->assertEquals('application/json', $outputMimeTypeOption->getValue()); - - // Test output schema - $outputSchemaOption = $findOption($requiredOptions, OptionEnum::outputSchema()); - $this->assertNotNull($outputSchemaOption); - $this->assertEquals(['type' => 'object'], $outputSchemaOption->getValue()); - - // Test output media orientation - IMPORTANT: Should be the enum object, not the string value - $outputMediaOrientationOption = $findOption($requiredOptions, OptionEnum::outputMediaOrientation()); - $this->assertNotNull($outputMediaOrientationOption); - $this->assertInstanceOf(MediaOrientationEnum::class, $outputMediaOrientationOption->getValue()); - $this->assertSame(MediaOrientationEnum::landscape(), $outputMediaOrientationOption->getValue()); - - // Test output media aspect ratio - $outputMediaAspectRatioOption = $findOption($requiredOptions, OptionEnum::outputMediaAspectRatio()); - $this->assertNotNull($outputMediaAspectRatioOption); - $this->assertEquals('16:9', $outputMediaAspectRatioOption->getValue()); - - // Test custom options - each custom option should be a separate RequiredOption - $customOptions = array_filter($requiredOptions, function ($option) { - return $option->getName()->equals(OptionEnum::customOptions()); - }); - $this->assertCount(2, $customOptions); // We set 2 custom options - - // Verify custom option values - $customOptionValues = array_map(function ($option) { - return $option->getValue(); - }, $customOptions); - $this->assertContains(['key1' => 'value1'], $customOptionValues); - $this->assertContains(['key2' => 'value2'], $customOptionValues); - } - - /** - * Tests toRequiredOptions method with no properties set. - * - * @return void - */ - public function testToRequiredOptionsWithNoProperties(): void - { - $config = new ModelConfig(); - $requiredOptions = $config->toRequiredOptions(); - - $this->assertIsArray($requiredOptions); - $this->assertEmpty($requiredOptions); - } - - /** - * Tests toRequiredOptions method with partial properties. - * - * @return void - */ - public function testToRequiredOptionsWithPartialProperties(): void - { - $config = new ModelConfig(); - - // Only set a few properties - $config->setTemperature(0.8); - $config->setMaxTokens(500); - $config->setOutputFileType(FileTypeEnum::remote()); - $config->setOutputMediaOrientation(MediaOrientationEnum::portrait()); - - $requiredOptions = $config->toRequiredOptions(); - - $this->assertIsArray($requiredOptions); - $this->assertCount(4, $requiredOptions); - - // Helper function to find option by name - /** - * @param list<\WordPress\AiClient\Providers\Models\DTO\RequiredOption> $options - * @param OptionEnum $name - * @return \WordPress\AiClient\Providers\Models\DTO\RequiredOption|null - */ - $findOption = function ( - array $options, - OptionEnum $name - ): ?\WordPress\AiClient\Providers\Models\DTO\RequiredOption { - foreach ($options as $option) { - if ($option->getName()->equals($name)) { - return $option; - } - } - return null; - }; - - // Test temperature - $temperatureOption = $findOption($requiredOptions, OptionEnum::temperature()); - $this->assertNotNull($temperatureOption); - $this->assertEquals(0.8, $temperatureOption->getValue()); - - // Test max tokens - $maxTokensOption = $findOption($requiredOptions, OptionEnum::maxTokens()); - $this->assertNotNull($maxTokensOption); - $this->assertEquals(500, $maxTokensOption->getValue()); - - // Test output file type - Should be the enum object - $outputFileTypeOption = $findOption($requiredOptions, OptionEnum::outputFileType()); - $this->assertNotNull($outputFileTypeOption); - $this->assertInstanceOf(FileTypeEnum::class, $outputFileTypeOption->getValue()); - $this->assertSame(FileTypeEnum::remote(), $outputFileTypeOption->getValue()); - $this->assertTrue($outputFileTypeOption->getValue()->isRemote()); - - // Test output media orientation - Should be the enum object - $outputMediaOrientationOption = $findOption($requiredOptions, OptionEnum::outputMediaOrientation()); - $this->assertNotNull($outputMediaOrientationOption); - $this->assertInstanceOf(MediaOrientationEnum::class, $outputMediaOrientationOption->getValue()); - $this->assertSame(MediaOrientationEnum::portrait(), $outputMediaOrientationOption->getValue()); - $this->assertTrue($outputMediaOrientationOption->getValue()->isPortrait()); - } } diff --git a/tests/unit/Providers/Models/DTO/ModelRequirementsTest.php b/tests/unit/Providers/Models/DTO/ModelRequirementsTest.php index 75ec6c35..1cb7897e 100644 --- a/tests/unit/Providers/Models/DTO/ModelRequirementsTest.php +++ b/tests/unit/Providers/Models/DTO/ModelRequirementsTest.php @@ -8,8 +8,15 @@ use PHPUnit\Framework\TestCase; use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface; use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface; +use WordPress\AiClient\Files\DTO\File; +use WordPress\AiClient\Messages\DTO\MessagePart; +use WordPress\AiClient\Messages\DTO\UserMessage; +use WordPress\AiClient\Messages\Enums\ModalityEnum; +use WordPress\AiClient\Providers\Models\DTO\ModelConfig; +use WordPress\AiClient\Providers\Models\DTO\ModelMetadata; use WordPress\AiClient\Providers\Models\DTO\ModelRequirements; use WordPress\AiClient\Providers\Models\DTO\RequiredOption; +use WordPress\AiClient\Providers\Models\DTO\SupportedOption; use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum; use WordPress\AiClient\Providers\Models\Enums\OptionEnum; @@ -392,4 +399,237 @@ public function testImplementsCorrectInterfaces(): void $requirements ); } + + /** + * Tests areMetBy method with matching capabilities and options. + * + * @return void + */ + public function testAreMetByWithMatchingRequirements(): void + { + $requirements = new ModelRequirements( + [CapabilityEnum::textGeneration(), CapabilityEnum::chatHistory()], + [ + new RequiredOption(OptionEnum::temperature(), 0.7), + new RequiredOption(OptionEnum::maxTokens(), 1000) + ] + ); + + $metadata = $this->createMock(ModelMetadata::class); + $metadata->method('getSupportedCapabilities')->willReturn([ + CapabilityEnum::textGeneration(), + CapabilityEnum::chatHistory(), + CapabilityEnum::imageGeneration() + ]); + $metadata->method('getSupportedOptions')->willReturn([ + new SupportedOption(OptionEnum::temperature(), [0.1, 0.7, 1.0]), + new SupportedOption(OptionEnum::maxTokens(), [500, 1000, 2000]) + ]); + + $this->assertTrue($requirements->areMetBy($metadata)); + } + + /** + * Tests areMetBy method with missing required capability. + * + * @return void + */ + public function testAreMetByWithMissingCapability(): void + { + $requirements = new ModelRequirements( + [CapabilityEnum::textGeneration(), CapabilityEnum::imageGeneration()], + [] + ); + + $metadata = $this->createMock(ModelMetadata::class); + $metadata->method('getSupportedCapabilities')->willReturn([ + CapabilityEnum::textGeneration() + ]); + $metadata->method('getSupportedOptions')->willReturn([]); + + $this->assertFalse($requirements->areMetBy($metadata)); + } + + /** + * Tests areMetBy method with unsupported option value. + * + * @return void + */ + public function testAreMetByWithUnsupportedOptionValue(): void + { + $requirements = new ModelRequirements( + [CapabilityEnum::textGeneration()], + [new RequiredOption(OptionEnum::temperature(), 0.5)] + ); + + $metadata = $this->createMock(ModelMetadata::class); + $metadata->method('getSupportedCapabilities')->willReturn([ + CapabilityEnum::textGeneration() + ]); + $metadata->method('getSupportedOptions')->willReturn([ + new SupportedOption(OptionEnum::temperature(), [0.1, 0.7, 1.0]) + ]); + + $this->assertFalse($requirements->areMetBy($metadata)); + } + + /** + * Tests areMetBy method with no requirements. + * + * @return void + */ + public function testAreMetByWithNoRequirements(): void + { + $requirements = new ModelRequirements([], []); + + $metadata = $this->createMock(ModelMetadata::class); + $metadata->method('getSupportedCapabilities')->willReturn([ + CapabilityEnum::textGeneration() + ]); + $metadata->method('getSupportedOptions')->willReturn([ + new SupportedOption(OptionEnum::temperature(), [0.7]) + ]); + + $this->assertTrue($requirements->areMetBy($metadata)); + } + + /** + * Tests fromPromptData method with simple text generation. + * + * @return void + */ + public function testFromPromptDataWithSimpleTextGeneration(): void + { + $messages = [new UserMessage([new MessagePart('Hello, world!')])]; + $modelConfig = new ModelConfig(); + $modelConfig->setTemperature(0.7); + $modelConfig->setMaxTokens(1000); + + $requirements = ModelRequirements::fromPromptData( + CapabilityEnum::textGeneration(), + $messages, + $modelConfig + ); + + $capabilities = $requirements->getRequiredCapabilities(); + $this->assertContains(CapabilityEnum::textGeneration(), $capabilities); + + // Check for input modalities option containing text + $inputModalityOptions = array_filter( + $requirements->getRequiredOptions(), + fn($opt) => $opt->getName()->isInputModalities() + ); + $this->assertNotEmpty($inputModalityOptions); + + $modalityValues = array_values($inputModalityOptions)[0]->getValue(); + + // The array contains ModalityEnum objects, not strings + $this->assertContains(ModalityEnum::text(), $modalityValues); + } + + /** + * Tests fromPromptData method with chat history. + * + * @return void + */ + public function testFromPromptDataWithChatHistory(): void + { + $messages = [ + new UserMessage([new MessagePart('First message')]), + new UserMessage([new MessagePart('Second message')]) + ]; + $modelConfig = new ModelConfig(); + + $requirements = ModelRequirements::fromPromptData( + CapabilityEnum::textGeneration(), + $messages, + $modelConfig + ); + + $capabilities = $requirements->getRequiredCapabilities(); + $this->assertContains(CapabilityEnum::textGeneration(), $capabilities); + $this->assertContains(CapabilityEnum::chatHistory(), $capabilities); + } + + /** + * Tests fromPromptData method with image input. + * + * @return void + */ + public function testFromPromptDataWithImageInput(): void + { + $b64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=='; + $imageFile = new File('data:image/png;base64,' . $b64); + $messages = [ + new UserMessage([ + new MessagePart('Describe this image'), + new MessagePart($imageFile) + ]) + ]; + $modelConfig = new ModelConfig(); + + $requirements = ModelRequirements::fromPromptData( + CapabilityEnum::textGeneration(), + $messages, + $modelConfig + ); + + $inputModalityOptions = array_filter( + $requirements->getRequiredOptions(), + fn($opt) => $opt->getName()->isInputModalities() + ); + $this->assertNotEmpty($inputModalityOptions); + + $modalityValues = array_values($inputModalityOptions)[0]->getValue(); + $this->assertContains(ModalityEnum::text(), $modalityValues); + $this->assertContains(ModalityEnum::image(), $modalityValues); + } + + /** + * Tests fromPromptData method with model configuration options. + * + * @return void + */ + public function testFromPromptDataWithModelConfigOptions(): void + { + $messages = [new UserMessage([new MessagePart('Test')])]; + $modelConfig = new ModelConfig(); + $modelConfig->setTemperature(0.9); + $modelConfig->setMaxTokens(2000); + $modelConfig->setTopP(0.95); + $modelConfig->setStopSequences(['END']); + + $requirements = ModelRequirements::fromPromptData( + CapabilityEnum::textGeneration(), + $messages, + $modelConfig + ); + + $options = $requirements->getRequiredOptions(); + $this->assertNotEmpty($options); + + // Check that we have the expected options based on ModelConfig settings + $hasTemperature = false; + $hasMaxTokens = false; + $hasTopP = false; + + foreach ($options as $option) { + if ($option->getName()->isTemperature()) { + $hasTemperature = true; + $this->assertEquals(0.9, $option->getValue()); + } + if ($option->getName()->isMaxTokens()) { + $hasMaxTokens = true; + $this->assertEquals(2000, $option->getValue()); + } + if ($option->getName()->isTopP()) { + $hasTopP = true; + $this->assertEquals(0.95, $option->getValue()); + } + } + + $this->assertTrue($hasTemperature, 'Temperature option should be present'); + $this->assertTrue($hasMaxTokens, 'Max tokens option should be present'); + $this->assertTrue($hasTopP, 'Top P option should be present'); + } }