diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a134fe6f..4f9e9bc6 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -69,6 +69,28 @@ $texts = AiClient::generateTextResult( )->toTexts(); ``` +#### Generate multiple text candidates using an xAI model + +##### Fluent API + +```php +$texts = AiClient::prompt('Write a 2-verse poem about PHP.') + ->usingModel(XAi::model('grok-3-mini')) + ->generateTexts(4); +``` + +##### Traditional API + +```php +$texts = AiClient::generateTextResult( + 'Write a 2-verse poem about PHP.', + XAi::model( + 'grok-3-mini', + [OptionEnum::CANDIDATE_COUNT => 4] + ) +)->toTexts(); +``` + #### Generate an image using any suitable OpenAI model ##### Fluent API @@ -93,6 +115,25 @@ $imageFile = AiClient::generateImageResult( )->toImageFile(); ``` +#### Generate an image using an xAI model + +##### Fluent API + +```php +$imageFile = AiClient::prompt('Generate an illustration of the PHP elephant in the Caribbean sea.') + ->usingModel(XAi::model('grok-2-image-1212')) + ->generateImage(); +``` + +##### Traditional API + +```php +$imageFile = AiClient::generateImageResult( + 'Generate an illustration of the PHP elephant in the Caribbean sea.', + XAi::model('grok-2-image-1212') +)->toImageFile(); +``` + #### Generate an image using any suitable model from any provider ##### Fluent API @@ -806,7 +847,7 @@ sequenceDiagram participant HttpTransporter participant PSR17Factory participant PSR18Client - + Model->>HttpTransporter: send(Request) HttpTransporter->>PSR17Factory: createRequest(Request) PSR17Factory-->>HttpTransporter: PSR-7 Request diff --git a/src/AiClient.php b/src/AiClient.php index 67bf36c8..25d122a1 100644 --- a/src/AiClient.php +++ b/src/AiClient.php @@ -8,6 +8,7 @@ use WordPress\AiClient\ProviderImplementations\Anthropic\AnthropicProvider; use WordPress\AiClient\ProviderImplementations\Google\GoogleProvider; use WordPress\AiClient\ProviderImplementations\OpenAi\OpenAiProvider; +use WordPress\AiClient\ProviderImplementations\XAi\XAiProvider; use WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface; use WordPress\AiClient\Providers\Http\HttpTransporterFactory; use WordPress\AiClient\Providers\Models\Contracts\ModelInterface; @@ -104,6 +105,7 @@ public static function defaultRegistry(): ProviderRegistry $registry->registerProvider(AnthropicProvider::class); $registry->registerProvider(GoogleProvider::class); $registry->registerProvider(OpenAiProvider::class); + $registry->registerProvider(XAiProvider::class); self::$defaultRegistry = $registry; } diff --git a/src/ProviderImplementations/XAi/XAiImageGenerationModel.php b/src/ProviderImplementations/XAi/XAiImageGenerationModel.php new file mode 100644 index 00000000..6dcb8392 --- /dev/null +++ b/src/ProviderImplementations/XAi/XAiImageGenerationModel.php @@ -0,0 +1,30 @@ + + * } + */ +class XAiModelMetadataDirectory extends AbstractOpenAiCompatibleModelMetadataDirectory +{ + /** + * {@inheritDoc} + * + * @since n.e.x.t + */ + protected function createRequest(HttpMethodEnum $method, string $path, array $headers = [], $data = null): Request + { + return new Request( + $method, + XAiProvider::BASE_URI . '/' . ltrim($path, '/'), + $headers, + $data + ); + } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + */ + protected function parseResponseToModelMetadataList(Response $response): array + { + /** @var ModelsResponseData $responseData */ + $responseData = $response->getData(); + if (!isset($responseData['data']) || !$responseData['data']) { + throw new RuntimeException( + 'Unexpected API response: Missing the data key.' + ); + } + + // Unfortunately, the xAI API does not return model capabilities, so we have to hardcode them here. + $xaiCapabilities = [ + CapabilityEnum::textGeneration(), + CapabilityEnum::chatHistory(), + ]; + $xaiBaseOptions = [ + new SupportedOption(OptionEnum::systemInstruction()), + new SupportedOption(OptionEnum::candidateCount()), + new SupportedOption(OptionEnum::maxTokens()), + new SupportedOption(OptionEnum::temperature()), + new SupportedOption(OptionEnum::topP()), + new SupportedOption(OptionEnum::stopSequences()), + new SupportedOption(OptionEnum::presencePenalty()), + new SupportedOption(OptionEnum::frequencyPenalty()), + new SupportedOption(OptionEnum::logprobs()), + new SupportedOption(OptionEnum::topLogprobs()), + new SupportedOption(OptionEnum::outputMimeType(), ['text/plain', 'application/json']), + new SupportedOption(OptionEnum::outputSchema()), + new SupportedOption(OptionEnum::functionDeclarations()), + new SupportedOption(OptionEnum::webSearch()), + new SupportedOption(OptionEnum::customOptions()), + ]; + $xaiOptions = array_merge($xaiBaseOptions, [ + new SupportedOption(OptionEnum::inputModalities(), [[ModalityEnum::text()]]), + new SupportedOption(OptionEnum::outputModalities(), [[ModalityEnum::text()]]), + ]); + $xaiMultimodalInputOptions = array_merge($xaiBaseOptions, [ + new SupportedOption( + OptionEnum::inputModalities(), + [ + [ModalityEnum::text()], + [ModalityEnum::text(), ModalityEnum::image()], + ] + ), + new SupportedOption(OptionEnum::outputModalities(), [[ModalityEnum::text()]]), + ]); + $imageCapabilities = [ + CapabilityEnum::imageGeneration(), + ]; + $imageOptions = [ + new SupportedOption(OptionEnum::inputModalities(), [[ModalityEnum::text()]]), + new SupportedOption(OptionEnum::outputModalities(), [[ModalityEnum::image()]]), + new SupportedOption(OptionEnum::candidateCount()), + new SupportedOption(OptionEnum::outputMimeType(), ['image/jpeg']), + new SupportedOption(OptionEnum::outputFileType(), [FileTypeEnum::inline()]), + new SupportedOption(OptionEnum::customOptions()), + ]; + + $modelsData = (array) $responseData['data']; + + return array_values( + array_map( + static function (array $modelData) use ( + $xaiCapabilities, + $xaiOptions, + $xaiMultimodalInputOptions, + $imageCapabilities, + $imageOptions + ): ModelMetadata { + $modelId = $modelData['id']; + if (str_contains($modelId, '-image-')) { + $modelCaps = $imageCapabilities; + $modelOptions = $imageOptions; + } elseif (str_contains($modelId, '-vision-')) { + $modelCaps = $xaiCapabilities; + $modelOptions = $xaiMultimodalInputOptions; + } elseif ( + str_starts_with($modelId, 'grok-') && + !str_contains($modelId, '-code') + ) { + $modelCaps = $xaiCapabilities; + $modelOptions = $xaiOptions; + } else { + $modelCaps = []; + $modelOptions = []; + } + + return new ModelMetadata( + $modelId, + $modelId, // The xAI API does not return a display name. + $modelCaps, + $modelOptions + ); + }, + $modelsData + ) + ); + } +} diff --git a/src/ProviderImplementations/XAi/XAiProvider.php b/src/ProviderImplementations/XAi/XAiProvider.php new file mode 100644 index 00000000..c006dcaf --- /dev/null +++ b/src/ProviderImplementations/XAi/XAiProvider.php @@ -0,0 +1,86 @@ +getSupportedCapabilities(); + foreach ($capabilities as $capability) { + if ($capability->isTextGeneration()) { + return new XAiTextGenerationModel($modelMetadata, $providerMetadata); + } + if ($capability->isImageGeneration()) { + return new XAiImageGenerationModel($modelMetadata, $providerMetadata); + } + } + + throw new RuntimeException( + 'Unsupported model capabilities: ' . implode(', ', $capabilities) + ); + } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + */ + protected static function createProviderMetadata(): ProviderMetadata + { + return new ProviderMetadata( + 'xai', + 'xAI', + ProviderTypeEnum::cloud() + ); + } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + */ + protected static function createProviderAvailability(): ProviderAvailabilityInterface + { + // Check valid API access by attempting to list models. + return new ListModelsApiBasedProviderAvailability( + static::modelMetadataDirectory() + ); + } + + /** + * {@inheritDoc} + * + * @since n.e.x.t + */ + protected static function createModelMetadataDirectory(): ModelMetadataDirectoryInterface + { + return new XAiModelMetadataDirectory(); + } +} diff --git a/src/ProviderImplementations/XAi/XAiTextGenerationModel.php b/src/ProviderImplementations/XAi/XAiTextGenerationModel.php new file mode 100644 index 00000000..3205eeee --- /dev/null +++ b/src/ProviderImplementations/XAi/XAiTextGenerationModel.php @@ -0,0 +1,32 @@ +