From ddd2c3d731e55d834d819407404755e44be6beea Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 26 Aug 2025 14:31:57 -0700 Subject: [PATCH 1/3] feat: adds support for specifying provider in PromptBuilder --- src/Builders/PromptBuilder.php | 73 +++++++++--- tests/unit/Builders/PromptBuilderTest.php | 136 ++++++++++++++++++++++ 2 files changed, 193 insertions(+), 16 deletions(-) diff --git a/src/Builders/PromptBuilder.php b/src/Builders/PromptBuilder.php index 83721788..d42a936c 100644 --- a/src/Builders/PromptBuilder.php +++ b/src/Builders/PromptBuilder.php @@ -57,6 +57,11 @@ class PromptBuilder */ protected ?ModelInterface $model = null; + /** + * @var string|null The provider ID or class name. + */ + protected ?string $providerIdOrClassName = null; + /** * @var ModelConfig The model configuration. */ @@ -198,6 +203,20 @@ public function usingModel(ModelInterface $model): self return $this; } + /** + * Sets the provider to use for generation. + * + * @since n.e.x.t + * + * @param string $providerIdOrClassName The provider ID or class name. + * @return self + */ + public function usingProvider(string $providerIdOrClassName): self + { + $this->providerIdOrClassName = $providerIdOrClassName; + return $this; + } + /** * Sets the system instruction. * @@ -930,28 +949,50 @@ private function getConfiguredModel(CapabilityEnum $capability): ModelInterface } // Find a suitable model based on requirements - $modelsMetadata = $this->registry->findModelsMetadataForSupport($requirements); + if ($this->providerIdOrClassName === null) { + $providerModelsMetadata = $this->registry->findModelsMetadataForSupport($requirements); - if (empty($modelsMetadata)) { - throw new InvalidArgumentException( - 'No models found that support the required capabilities and options for this prompt. ' . - 'Required capabilities: ' . implode(', ', array_map(function ($cap) { - return $cap->value; - }, $requirements->getRequiredCapabilities())) . - '. Required options: ' . implode(', ', array_map(function ($opt) { - return $opt->getName()->value . '=' . json_encode($opt->getValue()); - }, $requirements->getRequiredOptions())) + if (empty($providerModelsMetadata)) { + throw new InvalidArgumentException( + 'No models found that support the required capabilities and options for this prompt. ' . + 'Required capabilities: ' . implode(', ', array_map(function ($cap) { + return $cap->value; + }, $requirements->getRequiredCapabilities())) . + '. Required options: ' . implode(', ', array_map(function ($opt) { + return $opt->getName()->value . '=' . json_encode($opt->getValue()); + }, $requirements->getRequiredOptions())) + ); + } + + $firstProviderModels = $providerModelsMetadata[0]; + $provider = $firstProviderModels->getProvider()->getId(); + $modelMetadata = $firstProviderModels->getModels()[0]; + } else { + $modelsMetadata = $this->registry->findProviderModelsMetadataForSupport( + $this->providerIdOrClassName, + $requirements ); - } - // Get the first available model from the first provider - $firstProviderModels = $modelsMetadata[0]; - $firstModelMetadata = $firstProviderModels->getModels()[0]; + if (empty($modelsMetadata)) { + throw new InvalidArgumentException( + 'No models found that support the required capabilities and options for this prompt. ' . + 'Required capabilities: ' . implode(', ', array_map(function ($cap) { + return $cap->value; + }, $requirements->getRequiredCapabilities())) . + '. Required options: ' . implode(', ', array_map(function ($opt) { + return $opt->getName()->value . '=' . json_encode($opt->getValue()); + }, $requirements->getRequiredOptions())) + ); + } + + $provider = $this->providerIdOrClassName; + $modelMetadata = $modelsMetadata[0]; + } // Get the model instance from the provider return $this->registry->getProviderModel( - $firstProviderModels->getProvider()->getId(), - $firstModelMetadata->getId(), + $provider, + $modelMetadata->getId(), $this->modelConfig ); } diff --git a/tests/unit/Builders/PromptBuilderTest.php b/tests/unit/Builders/PromptBuilderTest.php index 0740c2ac..decaea39 100644 --- a/tests/unit/Builders/PromptBuilderTest.php +++ b/tests/unit/Builders/PromptBuilderTest.php @@ -19,6 +19,7 @@ use WordPress\AiClient\Providers\Models\Contracts\ModelInterface; 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\ImageGeneration\Contracts\ImageGenerationModelInterface; use WordPress\AiClient\Providers\Models\SpeechGeneration\Contracts\SpeechGenerationModelInterface; use WordPress\AiClient\Providers\Models\TextGeneration\Contracts\TextGenerationModelInterface; @@ -613,6 +614,26 @@ public function testUsingModel(): void $this->assertSame($model, $actualModel); } + /** + * Tests usingProvider method. + * + * @return void + */ + public function testUsingProvider(): void + { + $builder = new PromptBuilder($this->registry); + $result = $builder->usingProvider('test-provider'); + + $this->assertSame($builder, $result); + + $reflection = new \ReflectionClass($builder); + $providerProperty = $reflection->getProperty('providerIdOrClassName'); + $providerProperty->setAccessible(true); + + $actualProvider = $providerProperty->getValue($builder); + $this->assertEquals('test-provider', $actualProvider); + } + /** * Tests usingSystemInstruction method. * @@ -2462,4 +2483,119 @@ public function testIsSupportedForSpeechGeneration(): void $this->assertTrue($builder->isSupportedForSpeechGeneration()); } + + /** + * Tests generateResult with provider specified. + * + * @return void + */ + public function testGenerateResultWithProvider(): void + { + $result = $this->createMock(GenerativeAiResult::class); + + $modelMetadata = $this->createMock(ModelMetadata::class); + $modelMetadata->method('getId')->willReturn('provider-model'); + $modelMetadata->method('meetsRequirements')->willReturn(true); + + $model = $this->createTextGenerationModel($modelMetadata, $result); + + // Mock the registry to return the model when provider is specified + $this->registry->expects($this->once()) + ->method('findProviderModelsMetadataForSupport') + ->with('test-provider', $this->isInstanceOf(ModelRequirements::class)) + ->willReturn([$modelMetadata]); + + $this->registry->expects($this->once()) + ->method('getProviderModel') + ->with('test-provider', 'provider-model', $this->isInstanceOf(ModelConfig::class)) + ->willReturn($model); + + $builder = new PromptBuilder($this->registry, 'Test prompt'); + $builder->usingProvider('test-provider'); + + $actualResult = $builder->generateResult(); + $this->assertSame($result, $actualResult); + } + + /** + * Tests generateResult with provider but no suitable models. + * + * @return void + */ + public function testGenerateResultWithProviderNoModelsThrowsException(): void + { + // Mock the registry to return empty array when provider is specified + $this->registry->expects($this->once()) + ->method('findProviderModelsMetadataForSupport') + ->with('test-provider', $this->isInstanceOf(ModelRequirements::class)) + ->willReturn([]); + + $builder = new PromptBuilder($this->registry, 'Test prompt'); + $builder->usingProvider('test-provider'); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('No models found that support the required capabilities'); + + $builder->generateResult(); + } + + /** + * Tests that provider takes precedence when both provider and model are set. + * + * @return void + */ + public function testModelTakesPrecedenceOverProvider(): void + { + $result = $this->createMock(GenerativeAiResult::class); + + $metadata = $this->createMock(ModelMetadata::class); + $metadata->method('getId')->willReturn('explicit-model'); + $metadata->method('meetsRequirements')->willReturn(true); + + $model = $this->createTextGenerationModel($metadata, $result); + + // Registry should not be called when model is explicitly set + $this->registry->expects($this->never()) + ->method('findProviderModelsMetadataForSupport'); + $this->registry->expects($this->never()) + ->method('getProviderModel'); + + $builder = new PromptBuilder($this->registry, 'Test prompt'); + $builder->usingProvider('test-provider'); + $builder->usingModel($model); // Model overrides provider + + $actualResult = $builder->generateResult(); + $this->assertSame($result, $actualResult); + } + + /** + * Tests fluent interface with provider. + * + * @return void + */ + public function testFluentInterfaceWithProvider(): void + { + $builder = new PromptBuilder($this->registry, 'Initial text'); + + $result = $builder + ->usingProvider('my-provider') + ->withText(' Additional text') + ->usingMaxTokens(500) + ->usingTemperature(0.7); + + $this->assertSame($builder, $result); + + $reflection = new \ReflectionClass($builder); + + $providerProperty = $reflection->getProperty('providerIdOrClassName'); + $providerProperty->setAccessible(true); + $this->assertEquals('my-provider', $providerProperty->getValue($builder)); + + $configProperty = $reflection->getProperty('modelConfig'); + $configProperty->setAccessible(true); + /** @var ModelConfig $config */ + $config = $configProperty->getValue($builder); + $this->assertEquals(500, $config->getMaxTokens()); + $this->assertEquals(0.7, $config->getTemperature()); + } } From b05b215be16d943288499a31711322dd13c26025 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 26 Aug 2025 15:35:12 -0700 Subject: [PATCH 2/3] refactor: adds provider and model metadata to ai result --- src/Results/Contracts/ResultInterface.php | 22 ++- src/Results/DTO/GenerativeAiResult.php | 98 ++++++++-- tests/unit/Builders/PromptBuilderTest.php | 168 +++++++++++++++--- .../DTO/GenerativeAiOperationTest.php | 58 +++++- .../Results/DTO/GenerativeAiResultTest.php | 167 +++++++++++++---- 5 files changed, 436 insertions(+), 77 deletions(-) diff --git a/src/Results/Contracts/ResultInterface.php b/src/Results/Contracts/ResultInterface.php index 09d57812..c1965c20 100644 --- a/src/Results/Contracts/ResultInterface.php +++ b/src/Results/Contracts/ResultInterface.php @@ -4,6 +4,8 @@ namespace WordPress\AiClient\Results\Contracts; +use WordPress\AiClient\Providers\DTO\ProviderMetadata; +use WordPress\AiClient\Providers\Models\DTO\ModelMetadata; use WordPress\AiClient\Results\DTO\TokenUsage; /** @@ -34,6 +36,24 @@ public function getId(): string; */ public function getTokenUsage(): TokenUsage; + /** + * Gets the provider metadata. + * + * @since n.e.x.t + * + * @return ProviderMetadata The provider metadata. + */ + public function getProviderMetadata(): ProviderMetadata; + + /** + * Gets the model metadata. + * + * @since n.e.x.t + * + * @return ModelMetadata The model metadata. + */ + public function getModelMetadata(): ModelMetadata; + /** * Gets provider-specific metadata. * @@ -41,5 +61,5 @@ public function getTokenUsage(): TokenUsage; * * @return array Provider metadata. */ - public function getProviderMetadata(): array; + public function getAdditionalData(): array; } diff --git a/src/Results/DTO/GenerativeAiResult.php b/src/Results/DTO/GenerativeAiResult.php index 229099a2..c8925fad 100644 --- a/src/Results/DTO/GenerativeAiResult.php +++ b/src/Results/DTO/GenerativeAiResult.php @@ -9,6 +9,8 @@ use WordPress\AiClient\Common\AbstractDataTransferObject; use WordPress\AiClient\Files\DTO\File; use WordPress\AiClient\Messages\DTO\Message; +use WordPress\AiClient\Providers\DTO\ProviderMetadata; +use WordPress\AiClient\Providers\Models\DTO\ModelMetadata; use WordPress\AiClient\Results\Contracts\ResultInterface; /** @@ -21,12 +23,16 @@ * * @phpstan-import-type CandidateArrayShape from Candidate * @phpstan-import-type TokenUsageArrayShape from TokenUsage + * @phpstan-import-type ProviderMetadataArrayShape from ProviderMetadata + * @phpstan-import-type ModelMetadataArrayShape from ModelMetadata * * @phpstan-type GenerativeAiResultArrayShape array{ * id: string, * candidates: array, * tokenUsage: TokenUsageArrayShape, - * providerMetadata?: array + * providerMetadata: ProviderMetadataArrayShape, + * modelMetadata: ModelMetadataArrayShape, + * additionalData?: array * } * * @extends AbstractDataTransferObject @@ -37,6 +43,8 @@ class GenerativeAiResult extends AbstractDataTransferObject implements ResultInt public const KEY_CANDIDATES = 'candidates'; public const KEY_TOKEN_USAGE = 'tokenUsage'; public const KEY_PROVIDER_METADATA = 'providerMetadata'; + public const KEY_MODEL_METADATA = 'modelMetadata'; + public const KEY_ADDITIONAL_DATA = 'additionalData'; /** * @var string Unique identifier for this result. */ @@ -53,9 +61,19 @@ class GenerativeAiResult extends AbstractDataTransferObject implements ResultInt private TokenUsage $tokenUsage; /** - * @var array Provider-specific metadata. + * @var ProviderMetadata Provider metadata. */ - private array $providerMetadata; + private ProviderMetadata $providerMetadata; + + /** + * @var ModelMetadata Model metadata. + */ + private ModelMetadata $modelMetadata; + + /** + * @var array Additional data. + */ + private array $additionalData; /** * Constructor. @@ -65,11 +83,19 @@ class GenerativeAiResult extends AbstractDataTransferObject implements ResultInt * @param string $id Unique identifier for this result. * @param Candidate[] $candidates The generated candidates. * @param TokenUsage $tokenUsage Token usage statistics. - * @param array $providerMetadata Provider-specific metadata. + * @param ProviderMetadata $providerMetadata Provider metadata. + * @param ModelMetadata $modelMetadata Model metadata. + * @param array $additionalData Additional data. * @throws InvalidArgumentException If no candidates provided. */ - public function __construct(string $id, array $candidates, TokenUsage $tokenUsage, array $providerMetadata = []) - { + public function __construct( + string $id, + array $candidates, + TokenUsage $tokenUsage, + ProviderMetadata $providerMetadata, + ModelMetadata $modelMetadata, + array $additionalData = [] + ) { if (empty($candidates)) { throw new InvalidArgumentException('At least one candidate must be provided'); } @@ -78,6 +104,8 @@ public function __construct(string $id, array $candidates, TokenUsage $tokenUsag $this->candidates = $candidates; $this->tokenUsage = $tokenUsage; $this->providerMetadata = $providerMetadata; + $this->modelMetadata = $modelMetadata; + $this->additionalData = $additionalData; } /** @@ -113,15 +141,39 @@ public function getTokenUsage(): TokenUsage } /** - * {@inheritDoc} + * Gets the provider metadata. * * @since n.e.x.t + * + * @return ProviderMetadata The provider metadata. */ - public function getProviderMetadata(): array + public function getProviderMetadata(): ProviderMetadata { return $this->providerMetadata; } + /** + * Gets the model metadata. + * + * @since n.e.x.t + * + * @return ModelMetadata The model metadata. + */ + public function getModelMetadata(): ModelMetadata + { + return $this->modelMetadata; + } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + */ + public function getAdditionalData(): array + { + return $this->additionalData; + } + /** * Gets the total number of candidates. * @@ -387,13 +439,21 @@ public static function getJsonSchema(): array 'description' => 'The generated candidates.', ], self::KEY_TOKEN_USAGE => TokenUsage::getJsonSchema(), - self::KEY_PROVIDER_METADATA => [ + self::KEY_PROVIDER_METADATA => ProviderMetadata::getJsonSchema(), + self::KEY_MODEL_METADATA => ModelMetadata::getJsonSchema(), + self::KEY_ADDITIONAL_DATA => [ 'type' => 'object', 'additionalProperties' => true, - 'description' => 'Provider-specific metadata.', + 'description' => 'Additional data included in the API response.', ], ], - 'required' => [self::KEY_ID, self::KEY_CANDIDATES, self::KEY_TOKEN_USAGE], + 'required' => [ + self::KEY_ID, + self::KEY_CANDIDATES, + self::KEY_TOKEN_USAGE, + self::KEY_PROVIDER_METADATA, + self::KEY_MODEL_METADATA + ], ]; } @@ -410,7 +470,9 @@ public function toArray(): array self::KEY_ID => $this->id, self::KEY_CANDIDATES => array_map(fn(Candidate $candidate) => $candidate->toArray(), $this->candidates), self::KEY_TOKEN_USAGE => $this->tokenUsage->toArray(), - self::KEY_PROVIDER_METADATA => $this->providerMetadata, + self::KEY_PROVIDER_METADATA => $this->providerMetadata->toArray(), + self::KEY_MODEL_METADATA => $this->modelMetadata->toArray(), + self::KEY_ADDITIONAL_DATA => $this->additionalData, ]; } @@ -421,7 +483,13 @@ public function toArray(): array */ public static function fromArray(array $array): self { - static::validateFromArrayData($array, [self::KEY_ID, self::KEY_CANDIDATES, self::KEY_TOKEN_USAGE]); + static::validateFromArrayData($array, [ + self::KEY_ID, + self::KEY_CANDIDATES, + self::KEY_TOKEN_USAGE, + self::KEY_PROVIDER_METADATA, + self::KEY_MODEL_METADATA + ]); $candidates = array_map( fn(array $candidateData) => Candidate::fromArray($candidateData), @@ -432,7 +500,9 @@ public static function fromArray(array $array): self $array[self::KEY_ID], $candidates, TokenUsage::fromArray($array[self::KEY_TOKEN_USAGE]), - $array[self::KEY_PROVIDER_METADATA] ?? [] + ProviderMetadata::fromArray($array[self::KEY_PROVIDER_METADATA]), + ModelMetadata::fromArray($array[self::KEY_MODEL_METADATA]), + $array[self::KEY_ADDITIONAL_DATA] ?? [] ); } } diff --git a/tests/unit/Builders/PromptBuilderTest.php b/tests/unit/Builders/PromptBuilderTest.php index decaea39..be636d68 100644 --- a/tests/unit/Builders/PromptBuilderTest.php +++ b/tests/unit/Builders/PromptBuilderTest.php @@ -16,10 +16,13 @@ use WordPress\AiClient\Messages\DTO\UserMessage; use WordPress\AiClient\Messages\Enums\MessageRoleEnum; use WordPress\AiClient\Messages\Enums\ModalityEnum; +use WordPress\AiClient\Providers\DTO\ProviderMetadata; +use WordPress\AiClient\Providers\Enums\ProviderTypeEnum; use WordPress\AiClient\Providers\Models\Contracts\ModelInterface; 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\Enums\CapabilityEnum; use WordPress\AiClient\Providers\Models\ImageGeneration\Contracts\ImageGenerationModelInterface; use WordPress\AiClient\Providers\Models\SpeechGeneration\Contracts\SpeechGenerationModelInterface; use WordPress\AiClient\Providers\Models\TextGeneration\Contracts\TextGenerationModelInterface; @@ -41,6 +44,31 @@ class PromptBuilderTest extends TestCase */ private ProviderRegistry $registry; + /** + * Creates a test provider metadata instance. + * + * @return ProviderMetadata + */ + private function createTestProviderMetadata(): ProviderMetadata + { + return new ProviderMetadata('test-provider', 'Test Provider', ProviderTypeEnum::cloud()); + } + + /** + * Creates a test model metadata instance. + * + * @return ModelMetadata + */ + private function createTestModelMetadata(): ModelMetadata + { + return new ModelMetadata( + 'test-model', + 'Test Model', + [CapabilityEnum::textGeneration()], + [] + ); + } + /** * Creates a mock model that implements both ModelInterface and TextGenerationModelInterface. * @@ -1095,7 +1123,9 @@ public function testGenerateResultWithImageModality(): void new ModelMessage([new MessagePart(new File('data:image/png;base64,iVBORw0KGgo=', 'image/png'))]), FinishReasonEnum::stop() )], - new TokenUsage(100, 50, 150) + new TokenUsage(100, 50, 150), + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $metadata = $this->createMock(ModelMetadata::class); @@ -1125,7 +1155,9 @@ public function testGenerateResultWithAudioModality(): void new ModelMessage([new MessagePart(new File('data:audio/wav;base64,UklGRigE=', 'audio/wav'))]), FinishReasonEnum::stop() )], - new TokenUsage(100, 50, 150) + new TokenUsage(100, 50, 150), + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $metadata = $this->createMock(ModelMetadata::class); @@ -1152,7 +1184,9 @@ public function testGenerateResultWithMultimodalOutput(): void $result = new GenerativeAiResult( 'test-result', [new Candidate(new ModelMessage([new MessagePart('Generated text')]), FinishReasonEnum::stop())], - new TokenUsage(100, 50, 150) + new TokenUsage(100, 50, 150), + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $metadata = $this->createMock(ModelMetadata::class); @@ -1227,7 +1261,9 @@ public function testGenerateTextResult(): void $result = new GenerativeAiResult( 'test-result', [new Candidate(new ModelMessage([new MessagePart('Generated text')]), FinishReasonEnum::stop())], - new TokenUsage(100, 50, 150) + new TokenUsage(100, 50, 150), + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $metadata = $this->createMock(ModelMetadata::class); @@ -1267,7 +1303,9 @@ public function testGenerateImageResult(): void new ModelMessage([new MessagePart(new File('data:image/png;base64,iVBORw0KGgo=', 'image/png'))]), FinishReasonEnum::stop() )], - new TokenUsage(100, 50, 150) + new TokenUsage(100, 50, 150), + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $metadata = $this->createMock(ModelMetadata::class); @@ -1307,7 +1345,9 @@ public function testGenerateSpeechResult(): void new ModelMessage([new MessagePart(new File('data:audio/wav;base64,UklGRigE=', 'audio/wav'))]), FinishReasonEnum::stop() )], - new TokenUsage(100, 50, 150) + new TokenUsage(100, 50, 150), + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $metadata = $this->createMock(ModelMetadata::class); @@ -1347,7 +1387,9 @@ public function testConvertTextToSpeechResult(): void new ModelMessage([new MessagePart(new File('data:audio/wav;base64,UklGRigE=', 'audio/wav'))]), FinishReasonEnum::stop() )], - new TokenUsage(100, 50, 150) + new TokenUsage(100, 50, 150), + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $metadata = $this->createMock(ModelMetadata::class); @@ -1409,7 +1451,13 @@ public function testGenerateText(): void $message = new ModelMessage([$messagePart]); $candidate = new Candidate($message, FinishReasonEnum::stop()); - $result = new GenerativeAiResult('test-result', [$candidate], new TokenUsage(100, 50, 150)); + $result = new GenerativeAiResult( + 'test-result', + [$candidate], + new TokenUsage(100, 50, 150), + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() + ); $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); @@ -1492,7 +1540,13 @@ public function testGenerateTextThrowsExceptionWhenNoParts(): void $message = new ModelMessage([]); $candidate = new Candidate($message, FinishReasonEnum::stop()); - $result = new GenerativeAiResult('test-result', [$candidate], new TokenUsage(100, 50, 150)); + $result = new GenerativeAiResult( + 'test-result', + [$candidate], + new TokenUsage(100, 50, 150), + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() + ); $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); @@ -1521,7 +1575,13 @@ public function testGenerateTextThrowsExceptionWhenPartHasNoText(): void $message = new ModelMessage([$messagePart]); $candidate = new Candidate($message, FinishReasonEnum::stop()); - $result = new GenerativeAiResult('test-result', [$candidate], new TokenUsage(100, 50, 150)); + $result = new GenerativeAiResult( + 'test-result', + [$candidate], + new TokenUsage(100, 50, 150), + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() + ); $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); @@ -1560,7 +1620,13 @@ public function testGenerateTexts(): void ) ]; - $result = new GenerativeAiResult('test-result-id', $candidates, new TokenUsage(100, 50, 150)); + $result = new GenerativeAiResult( + 'test-result-id', + $candidates, + new TokenUsage(100, 50, 150), + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() + ); $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); @@ -1656,7 +1722,13 @@ public function testGenerateImage(): void $message = new ModelMessage([$messagePart]); $candidate = new Candidate($message, FinishReasonEnum::stop()); - $result = new GenerativeAiResult('test-result', [$candidate], new TokenUsage(100, 50, 150)); + $result = new GenerativeAiResult( + 'test-result', + [$candidate], + new TokenUsage(100, 50, 150), + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() + ); $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); @@ -1682,7 +1754,13 @@ public function testGenerateImageThrowsExceptionWhenNoFile(): void $message = new ModelMessage([$messagePart]); $candidate = new Candidate($message, FinishReasonEnum::stop()); - $result = new GenerativeAiResult('test-result', [$candidate], new TokenUsage(100, 50, 150)); + $result = new GenerativeAiResult( + 'test-result', + [$candidate], + new TokenUsage(100, 50, 150), + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() + ); $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); @@ -1719,7 +1797,13 @@ public function testGenerateImages(): void ); } - $result = new GenerativeAiResult('test-result-id', $candidates, new TokenUsage(100, 50, 150)); + $result = new GenerativeAiResult( + 'test-result-id', + $candidates, + new TokenUsage(100, 50, 150), + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() + ); $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); @@ -1749,7 +1833,13 @@ public function testConvertTextToSpeech(): void $message = new Message(MessageRoleEnum::model(), [$messagePart]); $candidate = new Candidate($message, FinishReasonEnum::stop()); - $result = new GenerativeAiResult('test-result', [$candidate], new TokenUsage(100, 50, 150)); + $result = new GenerativeAiResult( + 'test-result', + [$candidate], + new TokenUsage(100, 50, 150), + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() + ); $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); @@ -1784,7 +1874,13 @@ public function testConvertTextToSpeeches(): void ); } - $result = new GenerativeAiResult('test-result-id', $candidates, new TokenUsage(100, 50, 150)); + $result = new GenerativeAiResult( + 'test-result-id', + $candidates, + new TokenUsage(100, 50, 150), + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() + ); $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); @@ -1814,7 +1910,13 @@ public function testGenerateSpeech(): void $message = new Message(MessageRoleEnum::model(), [$messagePart]); $candidate = new Candidate($message, FinishReasonEnum::stop()); - $result = new GenerativeAiResult('test-result', [$candidate], new TokenUsage(100, 50, 150)); + $result = new GenerativeAiResult( + 'test-result', + [$candidate], + new TokenUsage(100, 50, 150), + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() + ); $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); @@ -1854,7 +1956,9 @@ public function testGenerateSpeeches(): void $result = new GenerativeAiResult( 'test-result-id', $candidates, - new TokenUsage(100, 50, 150) + new TokenUsage(100, 50, 150), + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $metadata = $this->createMock(ModelMetadata::class); @@ -2060,7 +2164,9 @@ public function testIncludeOutputModalityPreservesExisting(): void $result = new GenerativeAiResult( 'test-result', [new Candidate(new ModelMessage([new MessagePart('Generated text')]), FinishReasonEnum::stop())], - new TokenUsage(100, 50, 150) + new TokenUsage(100, 50, 150), + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $metadata = $this->createMock(ModelMetadata::class); @@ -2202,7 +2308,9 @@ public function testGenerateImageResultCreatesProperOperation(): void new ModelMessage([new MessagePart(new File('data:image/png;base64,iVBORw0KGgo=', 'image/png'))]), FinishReasonEnum::stop() )], - new TokenUsage(100, 50, 150) + new TokenUsage(100, 50, 150), + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $metadata = $this->createMock(ModelMetadata::class); @@ -2262,7 +2370,13 @@ public function testGenerateImageReturnsFileDirectly(): void FinishReasonEnum::stop() ); - $result = new GenerativeAiResult('test-result', [$candidate], new TokenUsage(100, 50, 150)); + $result = new GenerativeAiResult( + 'test-result', + [$candidate], + new TokenUsage(100, 50, 150), + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() + ); $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); @@ -2356,7 +2470,13 @@ public function testGenerateTextWithNonStringPartThrowsException(): void FinishReasonEnum::stop() ); - $result = new GenerativeAiResult('test-result', [$candidate], new TokenUsage(100, 50, 150)); + $result = new GenerativeAiResult( + 'test-result', + [$candidate], + new TokenUsage(100, 50, 150), + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() + ); $metadata = $this->createMock(ModelMetadata::class); $metadata->method('getId')->willReturn('test-model'); @@ -2400,7 +2520,7 @@ public function testIsSupportedForText(): void new ModelMessage([new MessagePart('Test')]), FinishReasonEnum::stop() ) - ], new TokenUsage(10, 5, 15)); + ], new TokenUsage(10, 5, 15), $this->createTestProviderMetadata(), $this->createTestModelMetadata()); $model = $this->createTextGenerationModel($metadata, $result); @@ -2474,7 +2594,7 @@ public function testIsSupportedForSpeechGeneration(): void new ModelMessage([new MessagePart(new File('https://example.com/speech.mp3', 'audio/mp3'))]), FinishReasonEnum::stop() ) - ], new TokenUsage(10, 5, 15)); + ], new TokenUsage(10, 5, 15), $this->createTestProviderMetadata(), $this->createTestModelMetadata()); $model = $this->createSpeechGenerationModel($metadata, $result); diff --git a/tests/unit/Operations/DTO/GenerativeAiOperationTest.php b/tests/unit/Operations/DTO/GenerativeAiOperationTest.php index 31485305..c663b045 100644 --- a/tests/unit/Operations/DTO/GenerativeAiOperationTest.php +++ b/tests/unit/Operations/DTO/GenerativeAiOperationTest.php @@ -11,6 +11,10 @@ use WordPress\AiClient\Messages\Enums\MessageRoleEnum; use WordPress\AiClient\Operations\DTO\GenerativeAiOperation; use WordPress\AiClient\Operations\Enums\OperationStateEnum; +use WordPress\AiClient\Providers\DTO\ProviderMetadata; +use WordPress\AiClient\Providers\Enums\ProviderTypeEnum; +use WordPress\AiClient\Providers\Models\DTO\ModelMetadata; +use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum; use WordPress\AiClient\Results\DTO\Candidate; use WordPress\AiClient\Results\DTO\GenerativeAiResult; use WordPress\AiClient\Results\DTO\TokenUsage; @@ -24,6 +28,35 @@ class GenerativeAiOperationTest extends TestCase { use ArrayTransformationTestTrait; + /** + * Creates a test provider metadata instance. + * + * @return ProviderMetadata + */ + private function createTestProviderMetadata(): ProviderMetadata + { + return new ProviderMetadata( + 'test-provider', + 'Test Provider', + ProviderTypeEnum::cloud() + ); + } + + /** + * Creates a test model metadata instance. + * + * @return ModelMetadata + */ + private function createTestModelMetadata(): ModelMetadata + { + return new ModelMetadata( + 'test-model', + 'Test Model', + [CapabilityEnum::textGeneration()], + [] + ); + } + /** * Tests creating operation in starting state. * @@ -78,6 +111,8 @@ public function testCreateInSucceededStateWithResult(): void 'result_123', [$candidate], $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata(), ['provider' => 'test'] ); @@ -200,7 +235,9 @@ public function testStateTransitions(): void $result = new GenerativeAiResult( 'result_transition', [new Candidate($modelMessage, FinishReasonEnum::stop(), 10)], - $tokenUsage + $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $operation2 = new GenerativeAiOperation( 'op_transition_2', @@ -337,7 +374,9 @@ public function testToArraySucceededState(): void $result = new GenerativeAiResult( 'result_success', [$candidate], - $tokenUsage + $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $operation = new GenerativeAiOperation( @@ -408,6 +447,17 @@ public function testFromArraySucceededState(): void TokenUsage::KEY_PROMPT_TOKENS => 10, TokenUsage::KEY_COMPLETION_TOKENS => 30, TokenUsage::KEY_TOTAL_TOKENS => 40 + ], + GenerativeAiResult::KEY_PROVIDER_METADATA => [ + ProviderMetadata::KEY_ID => 'test-provider', + ProviderMetadata::KEY_NAME => 'Test Provider', + ProviderMetadata::KEY_TYPE => ProviderTypeEnum::cloud()->value + ], + GenerativeAiResult::KEY_MODEL_METADATA => [ + ModelMetadata::KEY_ID => 'test-model', + ModelMetadata::KEY_NAME => 'Test Model', + ModelMetadata::KEY_SUPPORTED_CAPABILITIES => ['text_generation'], + ModelMetadata::KEY_SUPPORTED_OPTIONS => [] ] ] ]; @@ -460,7 +510,9 @@ public function testArrayRoundTripSucceededState(): void $result = new GenerativeAiResult( 'result_roundtrip', [$candidate], - $tokenUsage + $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $this->assertArrayRoundTrip( diff --git a/tests/unit/Results/DTO/GenerativeAiResultTest.php b/tests/unit/Results/DTO/GenerativeAiResultTest.php index e58f43a5..ddde10e3 100644 --- a/tests/unit/Results/DTO/GenerativeAiResultTest.php +++ b/tests/unit/Results/DTO/GenerativeAiResultTest.php @@ -13,6 +13,10 @@ use WordPress\AiClient\Messages\DTO\ModelMessage; use WordPress\AiClient\Messages\Enums\MessagePartTypeEnum; use WordPress\AiClient\Messages\Enums\MessageRoleEnum; +use WordPress\AiClient\Providers\DTO\ProviderMetadata; +use WordPress\AiClient\Providers\Enums\ProviderTypeEnum; +use WordPress\AiClient\Providers\Models\DTO\ModelMetadata; +use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum; use WordPress\AiClient\Results\DTO\Candidate; use WordPress\AiClient\Results\DTO\GenerativeAiResult; use WordPress\AiClient\Results\DTO\TokenUsage; @@ -27,6 +31,31 @@ class GenerativeAiResultTest extends TestCase { use ArrayTransformationTestTrait; + /** + * Creates a test provider metadata instance. + * + * @return ProviderMetadata + */ + private function createTestProviderMetadata(): ProviderMetadata + { + return new ProviderMetadata('test-provider', 'Test Provider', ProviderTypeEnum::cloud()); + } + + /** + * Creates a test model metadata instance. + * + * @return ModelMetadata + */ + private function createTestModelMetadata(): ModelMetadata + { + return new ModelMetadata( + 'test-model', + 'Test Model', + [CapabilityEnum::textGeneration()], + [] + ); + } + /** * Tests creating result with single candidate. * @@ -43,14 +72,16 @@ public function testCreateWithSingleCandidate(): void $result = new GenerativeAiResult( 'result_123', [$candidate], - $tokenUsage + $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $this->assertEquals('result_123', $result->getId()); $this->assertCount(1, $result->getCandidates()); $this->assertSame($candidate, $result->getCandidates()[0]); $this->assertSame($tokenUsage, $result->getTokenUsage()); - $this->assertEquals([], $result->getProviderMetadata()); + $this->assertEquals([], $result->getAdditionalData()); } /** @@ -72,7 +103,9 @@ public function testCreateWithMultipleCandidates(): void $result = new GenerativeAiResult( 'result_multi', $candidates, - $tokenUsage + $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $this->assertCount(3, $result->getCandidates()); @@ -81,11 +114,11 @@ public function testCreateWithMultipleCandidates(): void } /** - * Tests creating result with provider metadata. + * Tests creating result with additional data. * * @return void */ - public function testCreateWithProviderMetadata(): void + public function testCreateWithAdditionalData(): void { $message = new ModelMessage([new MessagePart('Response')]); $candidate = new Candidate($message, FinishReasonEnum::stop(), 5); @@ -101,10 +134,12 @@ public function testCreateWithProviderMetadata(): void 'result_meta', [$candidate], $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata(), $metadata ); - $this->assertEquals($metadata, $result->getProviderMetadata()); + $this->assertEquals($metadata, $result->getAdditionalData()); } /** @@ -119,7 +154,13 @@ public function testRejectsEmptyCandidatesArray(): void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('At least one candidate must be provided'); - new GenerativeAiResult('result_empty', [], $tokenUsage); + new GenerativeAiResult( + 'result_empty', + [], + $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() + ); } /** @@ -139,7 +180,9 @@ public function testToText(): void $result = new GenerativeAiResult( 'result_text', [$candidate], - $tokenUsage + $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $this->assertEquals($text, $result->toText()); @@ -162,7 +205,9 @@ public function testToTextThrowsExceptionWhenNoTextContent(): void $result = new GenerativeAiResult( 'result_no_text', [$candidate], - $tokenUsage + $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $this->expectException(RuntimeException::class); @@ -193,7 +238,9 @@ public function testToFile(): void $result = new GenerativeAiResult( 'result_file', [$candidate], - $tokenUsage + $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $this->assertSame($file, $result->toFile()); @@ -215,7 +262,9 @@ public function testToFileThrowsExceptionWhenNoFileContent(): void $result = new GenerativeAiResult( 'result_no_file', [$candidate], - $tokenUsage + $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $this->expectException(RuntimeException::class); @@ -241,7 +290,9 @@ public function testToImageFile(): void $result = new GenerativeAiResult( 'result_image', [$candidate], - $tokenUsage + $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $this->assertSame($imageFile, $result->toImageFile()); @@ -264,7 +315,9 @@ public function testToImageFileThrowsExceptionForNonImageFile(): void $result = new GenerativeAiResult( 'result_pdf', [$candidate], - $tokenUsage + $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $this->expectException(RuntimeException::class); @@ -290,7 +343,9 @@ public function testToAudioFile(): void $result = new GenerativeAiResult( 'result_audio', [$candidate], - $tokenUsage + $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $this->assertSame($audioFile, $result->toAudioFile()); @@ -313,7 +368,9 @@ public function testToVideoFile(): void $result = new GenerativeAiResult( 'result_video', [$candidate], - $tokenUsage + $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $this->assertSame($videoFile, $result->toVideoFile()); @@ -335,7 +392,9 @@ public function testToMessage(): void $result = new GenerativeAiResult( 'result_msg', [$candidate], - $tokenUsage + $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $this->assertSame($message, $result->toMessage()); @@ -362,7 +421,9 @@ public function testToTextsWithMultipleCandidates(): void $result = new GenerativeAiResult( 'result_texts', $candidates, - $tokenUsage + $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $this->assertEquals($texts, $result->toTexts()); @@ -392,7 +453,9 @@ public function testToFilesWithMultipleCandidates(): void $result = new GenerativeAiResult( 'result_files', $candidates, - $tokenUsage + $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $files = $result->toFiles(); @@ -423,7 +486,9 @@ public function testToImageFilesFiltersOnlyImages(): void $result = new GenerativeAiResult( 'result_mixed', $candidates, - $tokenUsage + $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $images = $result->toImageFiles(); @@ -453,7 +518,9 @@ public function testToAudioFilesFiltersOnlyAudio(): void $result = new GenerativeAiResult( 'result_audio_mix', $candidates, - $tokenUsage + $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $audioFiles = $result->toAudioFiles(); @@ -483,7 +550,9 @@ public function testToVideoFilesFiltersOnlyVideo(): void $result = new GenerativeAiResult( 'result_video_mix', $candidates, - $tokenUsage + $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $videoFiles = $result->toVideoFiles(); @@ -514,7 +583,9 @@ public function testToMessages(): void $result = new GenerativeAiResult( 'result_messages', $candidates, - $tokenUsage + $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $extractedMessages = $result->toMessages(); @@ -542,6 +613,8 @@ public function testJsonSchema(): void $this->assertArrayHasKey(GenerativeAiResult::KEY_CANDIDATES, $schema['properties']); $this->assertArrayHasKey(GenerativeAiResult::KEY_TOKEN_USAGE, $schema['properties']); $this->assertArrayHasKey(GenerativeAiResult::KEY_PROVIDER_METADATA, $schema['properties']); + $this->assertArrayHasKey(GenerativeAiResult::KEY_MODEL_METADATA, $schema['properties']); + $this->assertArrayHasKey(GenerativeAiResult::KEY_ADDITIONAL_DATA, $schema['properties']); // Check id property $this->assertEquals('string', $schema['properties'][GenerativeAiResult::KEY_ID]['type']); @@ -552,7 +625,7 @@ public function testJsonSchema(): void $this->assertEquals(1, $candidatesSchema['minItems']); // Check providerMetadata property - $metadataSchema = $schema['properties'][GenerativeAiResult::KEY_PROVIDER_METADATA]; + $metadataSchema = $schema['properties'][GenerativeAiResult::KEY_ADDITIONAL_DATA]; $this->assertEquals('object', $metadataSchema['type']); $this->assertTrue($metadataSchema['additionalProperties']); @@ -561,7 +634,9 @@ public function testJsonSchema(): void $this->assertContains(GenerativeAiResult::KEY_ID, $schema['required']); $this->assertContains(GenerativeAiResult::KEY_CANDIDATES, $schema['required']); $this->assertContains(GenerativeAiResult::KEY_TOKEN_USAGE, $schema['required']); - $this->assertNotContains(GenerativeAiResult::KEY_PROVIDER_METADATA, $schema['required']); + $this->assertContains(GenerativeAiResult::KEY_PROVIDER_METADATA, $schema['required']); + $this->assertContains(GenerativeAiResult::KEY_MODEL_METADATA, $schema['required']); + $this->assertNotContains(GenerativeAiResult::KEY_ADDITIONAL_DATA, $schema['required']); } /** @@ -578,7 +653,9 @@ public function testImplementsResultInterface(): void $result = new GenerativeAiResult( 'result_interface', [$candidate], - $tokenUsage + $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $this->assertInstanceOf( @@ -601,7 +678,9 @@ public function testHasMultipleCandidatesReturnsFalseForSingle(): void $result = new GenerativeAiResult( 'result_single', [$candidate], - $tokenUsage + $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $this->assertFalse($result->hasMultipleCandidates()); @@ -627,6 +706,8 @@ public function testToArray(): void 'result_json_123', [$candidate], $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata(), $metadata ); @@ -638,14 +719,16 @@ public function testToArray(): void GenerativeAiResult::KEY_ID, GenerativeAiResult::KEY_CANDIDATES, GenerativeAiResult::KEY_TOKEN_USAGE, - GenerativeAiResult::KEY_PROVIDER_METADATA + GenerativeAiResult::KEY_PROVIDER_METADATA, + GenerativeAiResult::KEY_MODEL_METADATA, + GenerativeAiResult::KEY_ADDITIONAL_DATA ] ); $this->assertEquals('result_json_123', $json[GenerativeAiResult::KEY_ID]); $this->assertIsArray($json[GenerativeAiResult::KEY_CANDIDATES]); $this->assertCount(1, $json[GenerativeAiResult::KEY_CANDIDATES]); $this->assertIsArray($json[GenerativeAiResult::KEY_TOKEN_USAGE]); - $this->assertEquals($metadata, $json[GenerativeAiResult::KEY_PROVIDER_METADATA]); + $this->assertEquals($metadata, $json[GenerativeAiResult::KEY_ADDITIONAL_DATA]); } /** @@ -680,7 +763,9 @@ public function testFromArray(): void TokenUsage::KEY_COMPLETION_TOKENS => 20, TokenUsage::KEY_TOTAL_TOKENS => 28 ], - GenerativeAiResult::KEY_PROVIDER_METADATA => ['provider' => 'test'] + GenerativeAiResult::KEY_PROVIDER_METADATA => $this->createTestProviderMetadata()->toArray(), + GenerativeAiResult::KEY_MODEL_METADATA => $this->createTestModelMetadata()->toArray(), + GenerativeAiResult::KEY_ADDITIONAL_DATA => ['provider' => 'test'] ]; $result = GenerativeAiResult::fromArray($json); @@ -691,7 +776,7 @@ public function testFromArray(): void $this->assertEquals(8, $result->getTokenUsage()->getPromptTokens()); $this->assertEquals(20, $result->getTokenUsage()->getCompletionTokens()); $this->assertEquals(28, $result->getTokenUsage()->getTotalTokens()); - $this->assertEquals(['provider' => 'test'], $result->getProviderMetadata()); + $this->assertEquals(['provider' => 'test'], $result->getAdditionalData()); } /** @@ -715,6 +800,8 @@ public function testArrayRoundTripWithMultipleCandidates(): void 'result_roundtrip', $candidates, new TokenUsage(30, 75, 105), + $this->createTestProviderMetadata(), + $this->createTestModelMetadata(), ['test_meta' => true] ), function ($original, $restored) { @@ -724,7 +811,7 @@ function ($original, $restored) { $original->getTokenUsage()->getTotalTokens(), $restored->getTokenUsage()->getTotalTokens() ); - $this->assertEquals($original->getProviderMetadata(), $restored->getProviderMetadata()); + $this->assertEquals($original->getAdditionalData(), $restored->getAdditionalData()); // Check first candidate details $originalFirst = $original->getCandidates()[0]; @@ -755,7 +842,9 @@ public function testToArrayWithoutProviderMetadata(): void $result = new GenerativeAiResult( 'result_no_meta', [$candidate], - $tokenUsage + $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() ); $json = $this->assertToArrayReturnsArray($result); @@ -766,10 +855,12 @@ public function testToArrayWithoutProviderMetadata(): void GenerativeAiResult::KEY_ID, GenerativeAiResult::KEY_CANDIDATES, GenerativeAiResult::KEY_TOKEN_USAGE, - GenerativeAiResult::KEY_PROVIDER_METADATA + GenerativeAiResult::KEY_PROVIDER_METADATA, + GenerativeAiResult::KEY_MODEL_METADATA, + GenerativeAiResult::KEY_ADDITIONAL_DATA ] ); - $this->assertEquals([], $json[GenerativeAiResult::KEY_PROVIDER_METADATA]); + $this->assertEquals([], $json[GenerativeAiResult::KEY_ADDITIONAL_DATA]); } /** @@ -783,7 +874,13 @@ public function testImplementsWithArrayTransformationInterface(): void $candidate = new Candidate($message, FinishReasonEnum::stop(), 1); $tokenUsage = new TokenUsage(1, 1, 2); - $result = new GenerativeAiResult('test', [$candidate], $tokenUsage); + $result = new GenerativeAiResult( + 'test', + [$candidate], + $tokenUsage, + $this->createTestProviderMetadata(), + $this->createTestModelMetadata() + ); $this->assertImplementsArrayTransformation($result); } } From cce0fb397923f8a65646858bca397a1c3c9c072a Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 26 Aug 2025 15:55:24 -0700 Subject: [PATCH 3/3] refactor: improves invalid errors --- src/Builders/PromptBuilder.php | 35 ++++++++++++++--------- tests/unit/Builders/PromptBuilderTest.php | 2 +- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/Builders/PromptBuilder.php b/src/Builders/PromptBuilder.php index d42a936c..a6fb8cad 100644 --- a/src/Builders/PromptBuilder.php +++ b/src/Builders/PromptBuilder.php @@ -954,13 +954,16 @@ private function getConfiguredModel(CapabilityEnum $capability): ModelInterface if (empty($providerModelsMetadata)) { throw new InvalidArgumentException( - 'No models found that support the required capabilities and options for this prompt. ' . - 'Required capabilities: ' . implode(', ', array_map(function ($cap) { - return $cap->value; - }, $requirements->getRequiredCapabilities())) . - '. Required options: ' . implode(', ', array_map(function ($opt) { - return $opt->getName()->value . '=' . json_encode($opt->getValue()); - }, $requirements->getRequiredOptions())) + sprintf( + 'No models found that support the required capabilities and options for this prompt. ' . + 'Required capabilities: %s. Required options: %s', + implode(', ', array_map(function ($cap) { + return $cap->value; + }, $requirements->getRequiredCapabilities())), + implode(', ', array_map(function ($opt) { + return $opt->getName()->value . '=' . json_encode($opt->getValue()); + }, $requirements->getRequiredOptions())) + ) ); } @@ -975,13 +978,17 @@ private function getConfiguredModel(CapabilityEnum $capability): ModelInterface if (empty($modelsMetadata)) { throw new InvalidArgumentException( - 'No models found that support the required capabilities and options for this prompt. ' . - 'Required capabilities: ' . implode(', ', array_map(function ($cap) { - return $cap->value; - }, $requirements->getRequiredCapabilities())) . - '. Required options: ' . implode(', ', array_map(function ($opt) { - return $opt->getName()->value . '=' . json_encode($opt->getValue()); - }, $requirements->getRequiredOptions())) + sprintf( + 'No models found for %s that support the required capabilities and options for this prompt. ' . + 'Required capabilities: %s. Required options: %s', + $this->providerIdOrClassName, + implode(', ', array_map(function ($cap) { + return $cap->value; + }, $requirements->getRequiredCapabilities())), + implode(', ', array_map(function ($opt) { + return $opt->getName()->value . '=' . json_encode($opt->getValue()); + }, $requirements->getRequiredOptions())) + ) ); } diff --git a/tests/unit/Builders/PromptBuilderTest.php b/tests/unit/Builders/PromptBuilderTest.php index be636d68..c2324ef3 100644 --- a/tests/unit/Builders/PromptBuilderTest.php +++ b/tests/unit/Builders/PromptBuilderTest.php @@ -2654,7 +2654,7 @@ public function testGenerateResultWithProviderNoModelsThrowsException(): void $builder->usingProvider('test-provider'); $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('No models found that support the required capabilities'); + $this->expectExceptionMessage('No models found for test-provider that support the required capabilities'); $builder->generateResult(); }