From aac973e207123113fa8733ff3e398905a15892a1 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Tue, 17 Jun 2025 20:28:12 -0700 Subject: [PATCH 01/39] Outline proposed architecture based on requirements. --- README.md | 1 + docs/ARCHITECTURE.md | 673 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 674 insertions(+) create mode 100644 docs/ARCHITECTURE.md diff --git a/README.md b/README.md index ca5de81f..73963af5 100644 --- a/README.md +++ b/README.md @@ -14,5 +14,6 @@ For more information on the requirements and guiding principles, please review: * [Glossary](./docs/GLOSSARY.md) * [Requirements](./docs/REQUIREMENTS.md) +* [Architecture](./docs/ARCHITECTURE.md) See the [contributing documentation](./CONTRIBUTING.md) for more information on how to get involved. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 00000000..5a644644 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,673 @@ +# Architecture + +This document outlines the architecture for the PHP AI Client. It is critical that it meets all [requirements](./REQUIREMENTS.md). + +## High-level API design + +The API design at a high level is heavily inspired by the [Vercel AI SDK](https://github.com/vercel/ai), which is widely used in the NodeJS ecosystem and one of the very few comprehensive AI client SDKs available. + +The main additional aspect that the Vercel AI SDK does not cater for easily is for a developer to use AI in a way that the choice of provider remains with the user. To clarify with an example: Instead of "Generate text with Google's model `gemini-2.5-flash`", go with "Generate text using any provider model that supports text generation and multimodal input". In other words, there needs to be a mechanism that allows finding any configured model that supports the given set of required AI features and capabilities. + +### Code examples + +The following examples indicate how this SDK could eventually be used. + +#### Generate text using a Google model + +```php +$text = Ai::generateText( + 'Write a 2-verse poem about PHP.', + Google::model('gemini-2.5-flash') +); +``` + +#### Generate multiple text candidates using an Anthropic model + +```php +$result = Ai::generateTextResult( + 'Write a 2-verse poem about PHP.', + Anthropic::model( + 'claude-3.7-sonnet', + [TextGenerationConfig::CANDIDATE_COUNT => 4] + ) +); +$texts = CandidatesUtil::toTexts( + $result->getCandidates() +); +``` + +#### Generate an image using any suitable OpenAI model + +```php +$modelsMetadata = Ai::defaultRegistry()->findProviderModelsMetadataForSupport( + 'openai', + AiFeature::IMAGE_GENERATION +); +$imageFile = Ai::generateImage( + 'Generate an illustration of the PHP elephant in the Carribean sea.', + Ai::defaultRegistry()->getProviderModel( + 'openai', + $modelsMetadata[0]->getId() + ) +); +``` + +#### Generate an image using any suitable model from any provider + +```php +$providerModelsMetadata = Ai::defaultRegistry()->findModelsMetadataForSupport( + AiFeature::IMAGE_GENERATION +); +$imageFile = Ai::generateImage( + 'Generate an illustration of the PHP elephant in the Carribean sea.', + Ai::defaultRegistry()->getProviderModel( + $providerModelsMetadata[0]->getProvider()->getId(), + $providerModelsMetadata[0]->getModels()[0]->getId() + ) +); +``` + +#### Generate embeddings using any suitable model from any provider + +```php +$providerModelsMetadata = Ai::defaultRegistry()->findModelsMetadataForSupport( + AiFeature::EMBEDDING_GENERATION +); +$embeddings = Ai::generateEmbeddings( + [ + 'A very long text.', + 'Another very long text.', + 'More long text.', + ], + Ai::defaultRegistry()->getProviderModel( + $providerModelsMetadata[0]->getProvider()->getId(), + $providerModelsMetadata[0]->getModels()[0]->getId() + ) +); +``` + +#### Generate text with JSON output using any suitable model from any provider + +```php +$providerModelsMetadata = Ai::defaultRegistry()->findModelsMetadataForSupport( + AiFeature::TEXT_GENERATION, + [ + // Make sure the model supports JSON output as well as following a given schema. + TextGenerationConfig::OUTPUT_MIME_TYPE => 'application/json', + TextGenerationConfig::OUTPUT_SCHEMA => true, + ] +); +$imageFile = Ai::generateText( + 'Transform the following CSV content into a JSON array of row data.', + Ai::defaultRegistry()->getProviderModel( + $providerModelsMetadata[0]->getProvider()->getId(), + $providerModelsMetadata[0]->getModels()[0]->getId(), + [ + AiModelConfig::GENERATION_CONFIG => [ + TextGenerationConfig::OUTPUT_MIME_TYPE => 'application/json', + TextGenerationConfig::OUTPUT_SCHEMA => [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'name' => [ + 'type' => 'string', + ], + 'age' => [ + 'type' => 'integer', + ], + ], + ], + ], + ], + ] + ) +); +``` + +## Class diagrams + +This section shows comprehensive class diagrams for the proposed architecture. For explanation on specific terms, see the [glossary](./GLOSSARY.md). + +**Note:** The class diagrams are not meant to be entirely comprehensive in terms of which AI features and capabilities are or will be supported. For now, they simply use "text generation", "image generation", "text to speech", "speech generation", and "embedding generation" for illustrative purposes. Other features like "music generation" or "video generation" etc. would work similarly. + +**Note:** The class diagrams are also not meant to be comprehensive in terms of any specific configuration keys or parameters which are or will be supported. For now, the relevant definitions don't include any specific parameter names or constants. + +### Zoomed out view + +Below you find the zoomed out overview class diagram, looking at the two entrypoints for the largely decoupled APIs for: + +- Consuming AI capabilities. + - This is what the vast majority of developers will use. +- Registering and implementing AI providers. + - This is what only developers that implement additional models or custom providers will use. + +Zoomed in views with detailed specifications for both of the APIs are found in the subsequent sections. + +```mermaid +--- +config: + class: + hideEmptyMembersBox: true +--- +classDiagram +direction LR + namespace Ai { + class AiEntrypoint { + +defaultRegistry() AiProviderRegistry + +isConfigured(AiProviderAvailability $availability) bool$ + +generateText(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) string$ + +streamGenerateText(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) Generator< string >$ + +generateImage(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) File$ + +textToSpeech(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) File$ + +generateSpeech(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) File$ + +generateEmbeddings(Message[] $input, AiModel $model) Embedding[]$ + +generateResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ + +generateOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ + +generateTextResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ + +streamGenerateTextResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) Generator< GenerativeAiResult >$ + +generateImageResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ + +textToSpeechResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ + +generateSpeechResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ + +generateEmbeddingsResult(string[]|Message[] $input, AiModel $model) EmbeddingResult$ + +generateTextOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ + +generateImageOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ + +textToSpeechOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ + +generateSpeechOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ + +generateEmbeddingsOperation(string[]|Message[] $input, AiModel $model) EmbeddingOperation$ + } + } + namespace Ai.Providers { + class AiProviderRegistry { + +registerProvider(string $className) void + +hasProvider(string $idOrClassName) bool + +getProviderClassName(string $id) string + +isProviderConfigured(string $idOrClassName) bool + +getProviderModel(string $idOrClassName, string $modelId, AiModelConfig|array $modelConfig) AiModel + +findProviderModelsMetadataForSupport(string $idOrClassName, AiFeature $feature, array $capabilities) AiModelMetadata[] + +findModelsMetadataForSupport(AiFeature $feature, array $capabilities) AiProviderModelMetadata[] + } + } + + AiEntrypoint "1" o-- "1..*" AiProviderRegistry +``` + +### Class diagram zoomed in on AI consumption + +```mermaid +--- +config: + class: + hideEmptyMembersBox: true +--- +classDiagram +direction LR + namespace Ai { + class AiEntrypoint { + +defaultRegistry() AiProviderRegistry + +isConfigured(AiProviderAvailability $availability) bool$ + +generateText(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) string$ + +streamGenerateText(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) Generator< string >$ + +generateImage(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) File$ + +textToSpeech(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) File$ + +generateSpeech(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) File$ + +generateEmbeddings(Message[] $input, AiModel $model) Embedding[]$ + +generateResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ + +generateOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ + +generateTextResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ + +streamGenerateTextResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) Generator< GenerativeAiResult >$ + +generateImageResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ + +textToSpeechResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ + +generateSpeechResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ + +generateEmbeddingsResult(string[]|Message[] $input, AiModel $model) EmbeddingResult$ + +generateTextOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ + +generateImageOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ + +textToSpeechOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ + +generateSpeechOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ + +generateEmbeddingsOperation(string[]|Message[] $input, AiModel $model) EmbeddingOperation$ + } + } + namespace Ai.Types { + class Message { + +getRole() MessageRole + +getParts() MessagePart[] + +getJsonSchema() array< string, mixed >$ + } + class MessagePart { + +getType() MessagePartType + +getText() string? + +getInlineFile() InlineFile? + +getRemoteFile() RemoteFile? + +getFunctionCall() FunctionCall? + +getFunctionResponse() FunctionResponse? + +getJsonSchema() array< string, mixed >$ + } + class File { + } + class InlineFile { + +getMimeType() string + +getBase64Data() string + +getJsonSchema() array< string, mixed >$ + } + class RemoteFile { + +getMimeType() string + +getUrl() string + +getJsonSchema() array< string, mixed >$ + } + class LocalFile { + +getMimeType() string + +getPath() string + +getJsonSchema() array< string, mixed >$ + } + class FunctionCall { + +getId() string + +getName() string + +getArgs() array< string, mixed > + +getJsonSchema() array< string, mixed >$ + } + class FunctionResponse { + +getId() string + +getName() string + +getResponse() mixed + +getJsonSchema() array< string, mixed >$ + } + class Embedding { + +getVector() float[] + +getDimension() int + } + class Operation { + +getId() string + +getState() OperationState + +getJsonSchema() array< string, mixed >$ + } + class GenerativeAiOperation { + +getId() string + +getState() OperationState + +getResult() GenerativeAiResult + +getJsonSchema() array< string, mixed >$ + } + class EmbeddingOperation { + +getId() string + +getState() OperationState + +getResult() EmbeddingResult + +getJsonSchema() array< string, mixed >$ + } + class Result { + +getId() string + +getUsage() TokenUsage + +getProviderMetadata() array< string, mixed > + +getJsonSchema() array< string, mixed >$ + } + class GenerativeAiResult { + +getId() string + +getCandidates() Candidate[] + +getUsage() TokenUsage + +getProviderMetadata() array< string, mixed > + +getJsonSchema() array< string, mixed >$ + } + class EmbeddingResult { + +getId() string + +getEmbeddings() Embedding[] + +getUsage() TokenUsage + +getProviderMetadata() array< string, mixed > + +getJsonSchema() array< string, mixed >$ + } + class Candidate { + +getMessage() Message + +getFinishReason() FinishReason + +getTokenCount() int + +getJsonSchema() array< string, mixed >$ + } + class TokenUsage { + +getPromptTokens() int + +getCompletionTokens() int + +getTotalTokens() int + +getJsonSchema() array< string, mixed >$ + } + } + namespace Ai.Types.Enums { + class MessageRole { + USER + MODEL + SYSTEM + } + class MessagePartType { + TEXT + INLINE_FILE + REMOTE_FILE + FUNCTION_CALL + FUNCTION_RESPONSE + } + class FinishReason { + STOP + LENGTH + CONTENT_FILTER + TOOL_CALLS + ERROR + } + class OperationState { + STARTING + PROCESSING + SUCCEEDED + FAILED + CANCELED + } + class AiModality { + TEXT + DOCUMENT + IMAGE + AUDIO + VIDEO + } + } + namespace Ai.Util { + class MessageUtil { + +toText(Message $message) string$ + +toImageFile(Message $message) File$ + +toAudioFile(Message $message) File$ + +toVideoFile(Message $message) File$ + } + class CandidatesUtil { + +toTexts(Candidate[] $candidates) string[]$ + +toImageFiles(Candidate[] $candidates) File[]$ + +toAudioFiles(Candidate[] $candidates) File[]$ + +toVideoFiles(Candidate[] $candidates) File[]$ + +toFirstText(Candidate[] $candidates) string$ + +toFirstImageFile(Candidate[] $candidates) File$ + +toFirstAudioFile(Candidate[] $candidates) File$ + +toFirstVideoFile(Candidate[] $candidates) File$ + } + } + + <> File + <> Operation + <> Result + <> MessageRole + <> MessagePartType + <> FinishReason + <> OperationState + <> AiModality + + AiEntrypoint .. Message : receives + AiEntrypoint .. MessagePart : receives + AiEntrypoint .. GenerativeAiResult : creates + AiEntrypoint .. EmbeddingResult : creates + AiEntrypoint .. GenerativeAiOperation : creates + AiEntrypoint .. EmbeddingOperation : creates + Message "1" *-- "1..*" MessagePart + MessagePart "1" o-- "0..1" InlineFile + MessagePart "1" o-- "0..1" RemoteFile + MessagePart "1" o-- "0..1" FunctionCall + MessagePart "1" o-- "0..1" FunctionResponse + GenerativeAiOperation "1" o-- "0..1" GenerativeAiResult + EmbeddingOperation "1" o-- "0..1" EmbeddingResult + GenerativeAiResult "1" o-- "1..*" Candidate + GenerativeAiResult "1" o-- "1" TokenUsage + EmbeddingResult "1" o-- "1..*" Embedding + EmbeddingResult "1" o-- "1" TokenUsage + Candidate "1" o-- "1" Message + Message ..> MessageRole + MessagePart ..> MessagePartType + Operation ..> OperationState + GenerativeAiOperation ..> OperationState + Candidate ..> FinishReason + File <|-- InlineFile + File <|-- RemoteFile + File <|-- LocalFile + Operation <|-- GenerativeAiOperation + Operation <|-- EmbeddingOperation + Result <|-- GenerativeAiResult + Result <|-- EmbeddingResult +``` + +### Class diagram zoomed in on AI provider registration and implementation + +```mermaid +--- +config: + class: + hideEmptyMembersBox: true +--- +classDiagram +direction LR + namespace Ai.Providers { + class AiProviderRegistry { + +registerProvider(string $className) void + +hasProvider(string $idOrClassName) bool + +getProviderClassName(string $id) string + +isProviderConfigured(string $idOrClassName) bool + +getProviderModel(string $idOrClassName, string $modelId, AiModelConfig|array $modelConfig) AiModel + +findProviderModelsMetadataForSupport(string $idOrClassName, AiFeature $feature, array $capabilities) AiModelMetadata[] + +findModelsMetadataForSupport(AiFeature $feature, array $capabilities) AiProviderModelMetadata[] + } + } + namespace Ai.Providers.Contracts { + class AiProvider { + +metadata() AiProviderMetadata$ + +model(string $modelId, AiModelConfig|array< string, mixed > $modelConfig) AiModel$ + +availability() AiProviderAvailability$ + +modelMetadataDirectory() AiModelMetadataDirectory$ + } + class AiModel { + +metadata() AiModelMetadata + +setConfig(AiModelConfig $config) void + +getConfig() AiModelConfig + +getSupportedCapabilities() AiCapability[]$ + } + class AiProviderAvailability { + +isConfigured() bool + } + class AiModelMetadataDirectory { + +listModelMetadata() AiModelMetadata[] + +hasModelMetadata(string $modelId) bool + +getModelMetadata(string $modelId) AiModelMetadata + } + class WithGenerativeAiOperations { + +getOperation(string $operationId) GenerativeAiOperation + } + class WithEmbeddingOperations { + +getOperation(string $operationId) EmbeddingOperation + } + class AiTextGenerationModel { + +generateTextResult(Message[] $prompt) GenerativeAiResult + +streamGenerateTextResult(Message[] $prompt) Generator< GenerativeAiResult > + } + class AiImageGenerationModel { + +generateImageResult(Message[] $prompt) GenerativeAiResult + } + class AiTextToSpeechModel { + +textToSpeechResult(Message[] $prompt) GenerativeAiResult + } + class AiSpeechGenerationModel { + +generateSpeechResult(Message[] $prompt) GenerativeAiResult + } + class AiEmbeddingGenerationModel { + +generateEmbeddingsResult(Message[] $input) EmbeddingResult + } + class AiTextGenerationOperationModel { + +generateTextOperation(Message[] $prompt) GenerativeAiOperation + } + class AiImageGenerationOperationModel { + +generateImageOperation(Message[] $prompt) GenerativeAiOperation + } + class AiTextToSpeechOperationModel { + +textToSpeechOperation(Message[] $prompt) GenerativeAiOperation + } + class AiSpeechGenerationOperationModel { + +generateSpeechOperation(Message[] $prompt) GenerativeAiOperation + } + class AiEmbeddingGenerationOperationModel { + +generateEmbeddingsOperation(Message[] $input) EmbeddingOperation + } + class WithHttpClient { + +setHttpClient(HttpClient $client) void + +getHttpClient() HttpClient + } + class HttpClient { + +send(RequestInterface $request, array< string, mixed > $options) ResponseInterface + +request(string $method, string $uri, array< string, mixed > $options) ResponseInterface + } + class WithAuthentication { + +setAuthentication(Authentication $authentication) void + +getAuthentication() Authentication + } + class Authentication { + +authenticate(RequestInterface $request) void + +getJsonSchema() array< string, mixed >$ + } + } + namespace Ai.Providers.Types { + class AiProviderMetadata { + +getId() string + +getName() string + +getType() AiProviderType + +getJsonSchema() array< string, mixed >$ + } + class AiModelMetadata { + +getId() string + +getName() string + +getSupportedFeatures() AiFeature[] + +getSupportedCapabilities() AiCapability[] + +getJsonSchema() array< string, mixed >$ + } + class AiProviderModelsMetadata { + +getProvider() AiProviderMetadata + +getModels() AiModelMetadata[] + +getJsonSchema() array< string, mixed >$ + } + class AiModelConfig { + +setSystemInstruction(string|MessagePart|MessagePart[]|Message $systemInstruction) void + +getSystemInstruction() Message? + +setGenerationConfig(GenerationConfig $config) void + +getGenerationConfig() GenerationConfig? + +setTools(Tool[] $tools) void + +getTools() Tool[] + +getJsonSchema() array< string, mixed >$ + } + class GenerationConfig { + +setValue(string $key, mixed $value) void + +getValue(string $key) mixed + +getValues() array< string, mixed > + +getAdditionalValues() array< string, mixed > + +getJsonSchema() array< string, mixed >$ + } + class TextGenerationConfig { + } + class ImageGenerationConfig { + } + class TextToSpeechConfig { + } + class SpeechGenerationConfig { + } + class EmbeddingGenerationConfig { + } + class Tool { + +getType() ToolType + +getFunctionDeclarations() FunctionDeclaration[]? + +getWebSearch() WebSearch? + +getJsonSchema() array< string, mixed >$ + } + class FunctionDeclaration { + +getName() string + +getDescription() string + +getParameters() mixed + +getJsonSchema() array< string, mixed >$ + } + class WebSearch { + +getAllowedDomains() string[] + +getDisallowedDomains() string[] + +getJsonSchema() array< string, mixed >$ + } + class AiCapability { + +isSupported() bool + +isSupportedValue(mixed $value) bool + +getSupportedValues() mixed[] + +getJsonSchema() array< string, mixed >$ + } + } + namespace Ai.Providers.Enums { + class AiProviderType { + CLOUD + SERVER + CLIENT + } + class ToolType { + FUNCTION_DECLARATIONS + WEB_SEARCH + } + class AiFeature { + TEXT_GENERATION + IMAGE_GENERATION + TEXT_TO_SPEECH + SPEECH_GENERATION + MUSIC_GENERATION + VIDEO_GENERATION + EMBEDDING_GENERATION + } + } + namespace Ai.Providers.Util { + class AiFeaturesUtil { + +getSupportedFeatures(AiModel|string $modelClass) AiFeature[]$ + +getSupportedCapabilities(AiModel|string $modelClass) AiCapability[]$ + } + } + + <> AiProvider + <> AiModel + <> AiProviderAvailability + <> AiModelMetadataDirectory + <> WithGenerativeAiOperations + <> WithEmbeddingOperations + <> AiTextGenerationModel + <> AiImageGenerationModel + <> AiTextToSpeechModel + <> AiSpeechGenerationModel + <> AiEmbeddingGenerationModel + <> AiTextGenerationOperationModel + <> AiImageGenerationOperationModel + <> AiTextToSpeechOperationModel + <> AiSpeechGenerationOperationModel + <> AiEmbeddingGenerationOperationModel + <> WithHttpClient + <> HttpClient + <> WithAuthentication + <> Authentication + <> GenerationConfig + <> AiFeature + <> AiProviderType + + AiProvider .. AiModel : creates + AiProvider "1" *-- "1" AiProviderMetadata + AiProvider "1" *-- "1" AiProviderAvailability + AiProvider "1" *-- "1" AiModelMetadataDirectory + AiModel "1" *-- "1" AiModelMetadata + AiModel "1" *-- "1" AiModelConfig + AiProviderModelsMetadata "1" o-- "1" AiProviderMetadata + AiProviderModelsMetadata "1" o-- "1..*" AiModelMetadata + AiProviderRegistry "1" o-- "0..*" AiProvider + AiProviderRegistry "1" o-- "0..*" AiProviderMetadata + AiModelMetadataDirectory "1" o-- "1..*" AiModelMetadata + AiModelMetadata "1" o-- "1..*" AiFeature + AiModelMetadata "1" o-- "0..*" AiCapability + AiModelConfig "1" o-- "0..1" GenerationConfig + AiModelConfig "1" o-- "0..*" Tool + Tool "1" o-- "0..*" FunctionDeclaration + Tool "1" o-- "0..1" WebSearch + AiProviderMetadata ..> AiProviderType + AiModelMetadata ..> AiFeature + AiModel <|-- AiTextGenerationModel + AiModel <|-- AiImageGenerationModel + AiModel <|-- AiTextToSpeechModel + AiModel <|-- AiSpeechGenerationModel + AiModel <|-- AiEmbeddingGenerationModel + AiModel <|-- AiTextGenerationOperationModel + AiModel <|-- AiImageGenerationOperationModel + AiModel <|-- AiTextToSpeechOperationModel + AiModel <|-- AiSpeechGenerationOperationModel + AiModel <|-- AiEmbeddingGenerationOperationModel + GenerationConfig <|-- TextGenerationConfig + GenerationConfig <|-- ImageGenerationConfig + GenerationConfig <|-- TextToSpeechConfig + GenerationConfig <|-- SpeechGenerationConfig + GenerationConfig <|-- EmbeddingGenerationConfig +``` From 42c1a83549d78c69bf284f959a3e1a54380e75ce Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Tue, 1 Jul 2025 15:16:02 -0700 Subject: [PATCH 02/39] Use utility methods on result object instead of declarative methods on entry point. --- docs/ARCHITECTURE.md | 50 ++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 5a644644..b8b55436 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -15,25 +15,22 @@ The following examples indicate how this SDK could eventually be used. #### Generate text using a Google model ```php -$text = Ai::generateText( +$text = Ai::generateTextResult( 'Write a 2-verse poem about PHP.', Google::model('gemini-2.5-flash') -); +)->toText(); ``` #### Generate multiple text candidates using an Anthropic model ```php -$result = Ai::generateTextResult( +$texts = Ai::generateTextResult( 'Write a 2-verse poem about PHP.', Anthropic::model( 'claude-3.7-sonnet', [TextGenerationConfig::CANDIDATE_COUNT => 4] ) -); -$texts = CandidatesUtil::toTexts( - $result->getCandidates() -); +)->toTexts(); ``` #### Generate an image using any suitable OpenAI model @@ -43,13 +40,13 @@ $modelsMetadata = Ai::defaultRegistry()->findProviderModelsMetadataForSupport( 'openai', AiFeature::IMAGE_GENERATION ); -$imageFile = Ai::generateImage( +$imageFile = Ai::generateImageResult( 'Generate an illustration of the PHP elephant in the Carribean sea.', Ai::defaultRegistry()->getProviderModel( 'openai', $modelsMetadata[0]->getId() ) -); +)->toImageFile(); ``` #### Generate an image using any suitable model from any provider @@ -58,13 +55,13 @@ $imageFile = Ai::generateImage( $providerModelsMetadata = Ai::defaultRegistry()->findModelsMetadataForSupport( AiFeature::IMAGE_GENERATION ); -$imageFile = Ai::generateImage( +$imageFile = Ai::generateImageResult( 'Generate an illustration of the PHP elephant in the Carribean sea.', Ai::defaultRegistry()->getProviderModel( $providerModelsMetadata[0]->getProvider()->getId(), $providerModelsMetadata[0]->getModels()[0]->getId() ) -); +)->toImageFile(); ``` #### Generate embeddings using any suitable model from any provider @@ -73,7 +70,7 @@ $imageFile = Ai::generateImage( $providerModelsMetadata = Ai::defaultRegistry()->findModelsMetadataForSupport( AiFeature::EMBEDDING_GENERATION ); -$embeddings = Ai::generateEmbeddings( +$embeddings = Ai::generateEmbeddingsResult( [ 'A very long text.', 'Another very long text.', @@ -83,7 +80,7 @@ $embeddings = Ai::generateEmbeddings( $providerModelsMetadata[0]->getProvider()->getId(), $providerModelsMetadata[0]->getModels()[0]->getId() ) -); +)->getEmbeddings(); ``` #### Generate text with JSON output using any suitable model from any provider @@ -97,7 +94,7 @@ $providerModelsMetadata = Ai::defaultRegistry()->findModelsMetadataForSupport( TextGenerationConfig::OUTPUT_SCHEMA => true, ] ); -$imageFile = Ai::generateText( +$jsonString = Ai::generateTextResult( 'Transform the following CSV content into a JSON array of row data.', Ai::defaultRegistry()->getProviderModel( $providerModelsMetadata[0]->getProvider()->getId(), @@ -122,7 +119,7 @@ $imageFile = Ai::generateText( ], ] ) -); +)->toText(); ``` ## Class diagrams @@ -156,12 +153,6 @@ direction LR class AiEntrypoint { +defaultRegistry() AiProviderRegistry +isConfigured(AiProviderAvailability $availability) bool$ - +generateText(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) string$ - +streamGenerateText(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) Generator< string >$ - +generateImage(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) File$ - +textToSpeech(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) File$ - +generateSpeech(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) File$ - +generateEmbeddings(Message[] $input, AiModel $model) Embedding[]$ +generateResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ +generateOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ +generateTextResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ @@ -206,12 +197,6 @@ direction LR class AiEntrypoint { +defaultRegistry() AiProviderRegistry +isConfigured(AiProviderAvailability $availability) bool$ - +generateText(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) string$ - +streamGenerateText(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) Generator< string >$ - +generateImage(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) File$ - +textToSpeech(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) File$ - +generateSpeech(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) File$ - +generateEmbeddings(Message[] $input, AiModel $model) Embedding[]$ +generateResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ +generateOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ +generateTextResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ @@ -304,6 +289,17 @@ direction LR +getUsage() TokenUsage +getProviderMetadata() array< string, mixed > +getJsonSchema() array< string, mixed >$ + %% The following utility methods transform the result candidates into a specific shape. + +toText() string + +toImageFile() File + +toAudioFile() File + +toVideoFile() File + +toMessage() Message + +toTexts() string[] + +toImageFiles() File[] + +toAudioFiles() File[] + +toVideoFiles() File[] + +toMessages() Message[] } class EmbeddingResult { +getId() string From ad14f143850fa04df361ea5ca1cd02d15e769a41 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Wed, 9 Jul 2025 11:35:36 -0700 Subject: [PATCH 03/39] Include additional code examples for some simpler and some more complex use-cases, with additional explanations. --- docs/ARCHITECTURE.md | 86 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 77 insertions(+), 9 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b8b55436..a26b21a9 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -12,6 +12,14 @@ The main additional aspect that the Vercel AI SDK does not cater for easily is f The following examples indicate how this SDK could eventually be used. +#### Generate text using any suitable model from any provider (most basic example) + +```php +$text = Ai::generateTextResult( + 'Write a 2-verse poem about PHP.' +)->toText(); +``` + #### Generate text using a Google model ```php @@ -64,27 +72,68 @@ $imageFile = Ai::generateImageResult( )->toImageFile(); ``` -#### Generate embeddings using any suitable model from any provider +#### Generate text using any suitable model from any provider + +_Note: This does effectively the exact same as [the first code example](#generate-text-using-any-suitable-model-from-any-provider-most-basic-example), but more verbosely. In other words, if you omit the model parameter, the SDK will do this internally._ ```php $providerModelsMetadata = Ai::defaultRegistry()->findModelsMetadataForSupport( - AiFeature::EMBEDDING_GENERATION + AiFeature::TEXT_GENERATION ); -$embeddings = Ai::generateEmbeddingsResult( - [ - 'A very long text.', - 'Another very long text.', - 'More long text.', - ], +$text = Ai::generateTextResult( + 'Write a 2-verse poem about PHP.', Ai::defaultRegistry()->getProviderModel( $providerModelsMetadata[0]->getProvider()->getId(), $providerModelsMetadata[0]->getModels()[0]->getId() ) -)->getEmbeddings(); +)->toText(); +``` + +#### Generate text with an image as additional input using any suitable model from any provider + +_Note: Since this omits the model parameter, the SDK will automatically determine which models are suitable and use any of them, similar to [the first code example](#generate-text-using-any-suitable-model-from-any-provider-most-basic-example). Since it knows the input includes an image, it can internally infer that the model needs to not only support `AiFeature::TEXT_GENERATION`, but also `AiCapability::INPUT_MODALITIES => ['text', 'image']`._ + +```php +$text = Ai::generateTextResult( + [ + [ + 'text' => 'Generate alternative text for this image.', + ], + [ + 'mimeType' => 'image/png', + 'base64Data' => '...', // Base64-encoded data blob. + ], + ] +)->toText(); +``` + +#### Generate text with chat history using any suitable model from any provider + +_Note: Similarly to the previous example, even without specifying the model here, the SDK will be able to infer required model capabilities because it can detect that multiple chat messages are passed. Therefore it will internally only consider models that support `AiFeature::TEXT_GENERATION` as well as `AiCapability::CHAT_HISTORY`._ + +```php +$text = Ai::generateTextResult( + [ + [ + 'role' => MessageRole::USER, + 'parts' => ['text' => 'Do you spell it WordPress or Wordpress?'], + ], + [ + 'role' => MessageRole::MODEL, + 'parts' => ['text' => 'The correct spelling is WordPress.'], + ], + [ + 'role' => MessageRole::USER, + 'parts' => ['text' => 'Can you repeat that please?'], + ], + ] +)->toText(); ``` #### Generate text with JSON output using any suitable model from any provider +_Note: Unlike the previous two examples, to require JSON output it is necessary to go the verbose route, since it is impossible for the SDK to detect whether you require JSON output purely from the prompt input. Therefore this code example contains the logic to manually search for suitable models and then use one of them for the task._ + ```php $providerModelsMetadata = Ai::defaultRegistry()->findModelsMetadataForSupport( AiFeature::TEXT_GENERATION, @@ -122,6 +171,25 @@ $jsonString = Ai::generateTextResult( )->toText(); ``` +#### Generate embeddings using any suitable model from any provider + +```php +$providerModelsMetadata = Ai::defaultRegistry()->findModelsMetadataForSupport( + AiFeature::EMBEDDING_GENERATION +); +$embeddings = Ai::generateEmbeddingsResult( + [ + 'A very long text.', + 'Another very long text.', + 'More long text.', + ], + Ai::defaultRegistry()->getProviderModel( + $providerModelsMetadata[0]->getProvider()->getId(), + $providerModelsMetadata[0]->getModels()[0]->getId() + ) +)->getEmbeddings(); +``` + ## Class diagrams This section shows comprehensive class diagrams for the proposed architecture. For explanation on specific terms, see the [glossary](./GLOSSARY.md). From 9b718951302038e77378de93cf73abd3e8d16b4d Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Wed, 9 Jul 2025 11:40:25 -0700 Subject: [PATCH 04/39] Fix incorrect class references. --- docs/ARCHITECTURE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a26b21a9..1add0488 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -91,7 +91,7 @@ $text = Ai::generateTextResult( #### Generate text with an image as additional input using any suitable model from any provider -_Note: Since this omits the model parameter, the SDK will automatically determine which models are suitable and use any of them, similar to [the first code example](#generate-text-using-any-suitable-model-from-any-provider-most-basic-example). Since it knows the input includes an image, it can internally infer that the model needs to not only support `AiFeature::TEXT_GENERATION`, but also `AiCapability::INPUT_MODALITIES => ['text', 'image']`._ +_Note: Since this omits the model parameter, the SDK will automatically determine which models are suitable and use any of them, similar to [the first code example](#generate-text-using-any-suitable-model-from-any-provider-most-basic-example). Since it knows the input includes an image, it can internally infer that the model needs to not only support `AiFeature::TEXT_GENERATION`, but also `TextGenerationConfig::INPUT_MODALITIES => ['text', 'image']`._ ```php $text = Ai::generateTextResult( @@ -109,7 +109,7 @@ $text = Ai::generateTextResult( #### Generate text with chat history using any suitable model from any provider -_Note: Similarly to the previous example, even without specifying the model here, the SDK will be able to infer required model capabilities because it can detect that multiple chat messages are passed. Therefore it will internally only consider models that support `AiFeature::TEXT_GENERATION` as well as `AiCapability::CHAT_HISTORY`._ +_Note: Similarly to the previous example, even without specifying the model here, the SDK will be able to infer required model capabilities because it can detect that multiple chat messages are passed. Therefore it will internally only consider models that support `AiFeature::TEXT_GENERATION` as well as `TextGenerationConfig::CHAT_HISTORY`._ ```php $text = Ai::generateTextResult( From b13e0705f2bef98de882eaa1d3de0f292094cec5 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Wed, 9 Jul 2025 22:06:40 -0700 Subject: [PATCH 05/39] Revise class diagrams to include fluent API infrastructure, and reorganize overview diagrams for easier understanding. --- docs/ARCHITECTURE.md | 169 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 157 insertions(+), 12 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 1add0488..ba205172 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -198,16 +198,83 @@ This section shows comprehensive class diagrams for the proposed architecture. F **Note:** The class diagrams are also not meant to be comprehensive in terms of any specific configuration keys or parameters which are or will be supported. For now, the relevant definitions don't include any specific parameter names or constants. -### Zoomed out view +### Overview: Fluent API for AI consumption -Below you find the zoomed out overview class diagram, looking at the two entrypoints for the largely decoupled APIs for: +This is a subset of the overall class diagram, purely focused on the fluent API for AI consumption. -- Consuming AI capabilities. - - This is what the vast majority of developers will use. -- Registering and implementing AI providers. - - This is what only developers that implement additional models or custom providers will use. +```mermaid +--- +config: + class: + hideEmptyMembersBox: true +--- +classDiagram +direction LR + namespace Ai { + class AiEntrypoint { + +prompt(?string $text) PromptFacade$ + +message(?string $text) MessageFacade$ + } + + class PromptFacade { + +withText(string $text) self + +withImageFile(File $file) self + +withAudioFile(File $file) self + +withVideoFile(File $file) self + +withFunctionResponse(FunctionResponse $functionResponse) self + +withMessageParts(...MessagePart $part) self + +withHistory(Message[] $messages) self + +usingModel(AiModel $model) self + +usingSystemInstruction(string|MessagePart[]|Message $systemInstruction) self + +usingTemperature(float $temperature) self + +usingTopP(float $topP) self + +usingTopK(int $topK) self + +usingStopSequences(...string $stopSequences) self + +usingCandidateCount(int $candidateCount) self + +usingOutputMime(string $mimeType) self + +usingOutputSchema(array< string, mixed > $schema) self + +usingOutputModalities(...AiModality $modalities) self + +generateResult() GenerativeAiResult + +generateOperation() GenerativeAiOperation + +generateTextResult() GenerativeAiResult + +streamGenerateTextResult() Generator< GenerativeAiResult > + +generateImageResult() GenerativeAiResult + +textToSpeechResult() GenerativeAiResult + +generateSpeechResult() GenerativeAiResult + +generateEmbeddingsResult() EmbeddingResult + +generateTextOperation() GenerativeAiOperation + +generateImageOperation() GenerativeAiOperation + +textToSpeechOperation() GenerativeAiOperation + +generateSpeechOperation() GenerativeAiOperation + +generateEmbeddingsOperation() EmbeddingOperation + +generateText() string + +streamGenerateText() Generator< string > + +generateImage() File + +textToSpeech() File + +generateSpeech() File + +generateEmbeddings() Embedding[] + } + + class MessageFacade { + +usingRole(MessageRole $role) self + +withText(string $text) self + +withImageFile(File $file) self + +withAudioFile(File $file) self + +withVideoFile(File $file) self + +withFunctionCall(FunctionCall $functionCall) self + +withFunctionResponse(FunctionResponse $functionResponse) self + +withMessageParts(...MessagePart $part) self + +get() Message + } + } + + AiEntrypoint .. PromptFacade : creates + AiEntrypoint .. MessageFacade : creates +``` -Zoomed in views with detailed specifications for both of the APIs are found in the subsequent sections. +### Overview: Traditional method call API for AI consumption + +This is a subset of the overall class diagram, purely focused on the traditional method call API for AI consumption. ```mermaid --- @@ -219,8 +286,6 @@ classDiagram direction LR namespace Ai { class AiEntrypoint { - +defaultRegistry() AiProviderRegistry - +isConfigured(AiProviderAvailability $availability) bool$ +generateResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ +generateOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ +generateTextResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ @@ -236,6 +301,26 @@ direction LR +generateEmbeddingsOperation(string[]|Message[] $input, AiModel $model) EmbeddingOperation$ } } +``` + +### Overview: API for provider registration and implementation + +This is a subset of the overall class diagram, purely focused on the API for provider registration and implementation. + +```mermaid +--- +config: + class: + hideEmptyMembersBox: true +--- +classDiagram +direction LR + namespace Ai { + class AiEntrypoint { + +defaultRegistry() AiProviderRegistry$ + +isConfigured(AiProviderAvailability $availability) bool$ + } + } namespace Ai.Providers { class AiProviderRegistry { +registerProvider(string $className) void @@ -251,7 +336,7 @@ direction LR AiEntrypoint "1" o-- "1..*" AiProviderRegistry ``` -### Class diagram zoomed in on AI consumption +### Details: Class diagram for AI consumption ```mermaid --- @@ -263,7 +348,9 @@ classDiagram direction LR namespace Ai { class AiEntrypoint { - +defaultRegistry() AiProviderRegistry + +prompt(?string $text) PromptFacade$ + +message(?string $text) MessageFacade$ + +defaultRegistry() AiProviderRegistry$ +isConfigured(AiProviderAvailability $availability) bool$ +generateResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ +generateOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ @@ -279,6 +366,57 @@ direction LR +generateSpeechOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ +generateEmbeddingsOperation(string[]|Message[] $input, AiModel $model) EmbeddingOperation$ } + + class PromptFacade { + +withText(string $text) self + +withImageFile(File $file) self + +withAudioFile(File $file) self + +withVideoFile(File $file) self + +withFunctionResponse(FunctionResponse $functionResponse) self + +withMessageParts(...MessagePart $part) self + +withHistory(Message[] $messages) self + +usingModel(AiModel $model) self + +usingSystemInstruction(string|MessagePart[]|Message $systemInstruction) self + +usingTemperature(float $temperature) self + +usingTopP(float $topP) self + +usingTopK(int $topK) self + +usingStopSequences(...string $stopSequences) self + +usingCandidateCount(int $candidateCount) self + +usingOutputMime(string $mimeType) self + +usingOutputSchema(array< string, mixed > $schema) self + +usingOutputModalities(...AiModality $modalities) self + +generateResult() GenerativeAiResult + +generateOperation() GenerativeAiOperation + +generateTextResult() GenerativeAiResult + +streamGenerateTextResult() Generator< GenerativeAiResult > + +generateImageResult() GenerativeAiResult + +textToSpeechResult() GenerativeAiResult + +generateSpeechResult() GenerativeAiResult + +generateEmbeddingsResult() EmbeddingResult + +generateTextOperation() GenerativeAiOperation + +generateImageOperation() GenerativeAiOperation + +textToSpeechOperation() GenerativeAiOperation + +generateSpeechOperation() GenerativeAiOperation + +generateEmbeddingsOperation() EmbeddingOperation + +generateText() string + +streamGenerateText() Generator< string > + +generateImage() File + +textToSpeech() File + +generateSpeech() File + +generateEmbeddings() Embedding[] + } + + class MessageFacade { + +usingRole(MessageRole $role) self + +withText(string $text) self + +withImageFile(File $file) self + +withAudioFile(File $file) self + +withVideoFile(File $file) self + +withFunctionCall(FunctionCall $functionCall) self + +withFunctionResponse(FunctionResponse $functionResponse) self + +withMessageParts(...MessagePart $part) self + +get() Message + } } namespace Ai.Types { class Message { @@ -454,10 +592,17 @@ direction LR AiEntrypoint .. Message : receives AiEntrypoint .. MessagePart : receives + AiEntrypoint .. PromptFacade : creates + AiEntrypoint .. MessageFacade : creates AiEntrypoint .. GenerativeAiResult : creates AiEntrypoint .. EmbeddingResult : creates AiEntrypoint .. GenerativeAiOperation : creates AiEntrypoint .. EmbeddingOperation : creates + PromptFacade .. GenerativeAiResult : creates + PromptFacade .. EmbeddingResult : creates + PromptFacade .. GenerativeAiOperation : creates + PromptFacade .. EmbeddingOperation : creates + MessageFacade .. Message : creates Message "1" *-- "1..*" MessagePart MessagePart "1" o-- "0..1" InlineFile MessagePart "1" o-- "0..1" RemoteFile @@ -484,7 +629,7 @@ direction LR Result <|-- EmbeddingResult ``` -### Class diagram zoomed in on AI provider registration and implementation +### Details: Class diagram for AI provider registration and implementation ```mermaid --- From 24285c40526a6d73d50804052d251def8ea5c725 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Thu, 10 Jul 2025 08:57:25 -0700 Subject: [PATCH 06/39] Rename Facade classes to Builder classes, as more suitable name despite not formally using the builder pattern. --- docs/ARCHITECTURE.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ba205172..b7654c63 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -212,11 +212,11 @@ classDiagram direction LR namespace Ai { class AiEntrypoint { - +prompt(?string $text) PromptFacade$ - +message(?string $text) MessageFacade$ + +prompt(?string $text) PromptBuilder$ + +message(?string $text) MessageBuilder$ } - class PromptFacade { + class PromptBuilder { +withText(string $text) self +withImageFile(File $file) self +withAudioFile(File $file) self @@ -255,7 +255,7 @@ direction LR +generateEmbeddings() Embedding[] } - class MessageFacade { + class MessageBuilder { +usingRole(MessageRole $role) self +withText(string $text) self +withImageFile(File $file) self @@ -268,8 +268,8 @@ direction LR } } - AiEntrypoint .. PromptFacade : creates - AiEntrypoint .. MessageFacade : creates + AiEntrypoint .. PromptBuilder : creates + AiEntrypoint .. MessageBuilder : creates ``` ### Overview: Traditional method call API for AI consumption @@ -348,8 +348,8 @@ classDiagram direction LR namespace Ai { class AiEntrypoint { - +prompt(?string $text) PromptFacade$ - +message(?string $text) MessageFacade$ + +prompt(?string $text) PromptBuilder$ + +message(?string $text) MessageBuilder$ +defaultRegistry() AiProviderRegistry$ +isConfigured(AiProviderAvailability $availability) bool$ +generateResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ @@ -367,7 +367,7 @@ direction LR +generateEmbeddingsOperation(string[]|Message[] $input, AiModel $model) EmbeddingOperation$ } - class PromptFacade { + class PromptBuilder { +withText(string $text) self +withImageFile(File $file) self +withAudioFile(File $file) self @@ -406,7 +406,7 @@ direction LR +generateEmbeddings() Embedding[] } - class MessageFacade { + class MessageBuilder { +usingRole(MessageRole $role) self +withText(string $text) self +withImageFile(File $file) self @@ -592,17 +592,17 @@ direction LR AiEntrypoint .. Message : receives AiEntrypoint .. MessagePart : receives - AiEntrypoint .. PromptFacade : creates - AiEntrypoint .. MessageFacade : creates + AiEntrypoint .. PromptBuilder : creates + AiEntrypoint .. MessageBuilder : creates AiEntrypoint .. GenerativeAiResult : creates AiEntrypoint .. EmbeddingResult : creates AiEntrypoint .. GenerativeAiOperation : creates AiEntrypoint .. EmbeddingOperation : creates - PromptFacade .. GenerativeAiResult : creates - PromptFacade .. EmbeddingResult : creates - PromptFacade .. GenerativeAiOperation : creates - PromptFacade .. EmbeddingOperation : creates - MessageFacade .. Message : creates + PromptBuilder .. GenerativeAiResult : creates + PromptBuilder .. EmbeddingResult : creates + PromptBuilder .. GenerativeAiOperation : creates + PromptBuilder .. EmbeddingOperation : creates + MessageBuilder .. Message : creates Message "1" *-- "1..*" MessagePart MessagePart "1" o-- "0..1" InlineFile MessagePart "1" o-- "0..1" RemoteFile From 920db773ee3cffc54cf4869d0f8059ce8f485d6f Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Thu, 10 Jul 2025 17:14:17 -0700 Subject: [PATCH 07/39] Use variadic parameter for messages. --- docs/ARCHITECTURE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b7654c63..cb3febd6 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -223,7 +223,7 @@ direction LR +withVideoFile(File $file) self +withFunctionResponse(FunctionResponse $functionResponse) self +withMessageParts(...MessagePart $part) self - +withHistory(Message[] $messages) self + +withHistory(...Message $messages) self +usingModel(AiModel $model) self +usingSystemInstruction(string|MessagePart[]|Message $systemInstruction) self +usingTemperature(float $temperature) self @@ -374,7 +374,7 @@ direction LR +withVideoFile(File $file) self +withFunctionResponse(FunctionResponse $functionResponse) self +withMessageParts(...MessagePart $part) self - +withHistory(Message[] $messages) self + +withHistory(...Message $messages) self +usingModel(AiModel $model) self +usingSystemInstruction(string|MessagePart[]|Message $systemInstruction) self +usingTemperature(float $temperature) self From 8fa8b210be7f90f26f5fbb5f8098b0732b37dbf5 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 11 Jul 2025 11:21:28 -0700 Subject: [PATCH 08/39] Rename `textToSpeech` methods to `convertTextToSpeech` and relevant interfaces to use `TextToSpeechConversion`. --- docs/ARCHITECTURE.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index cb3febd6..3b4b5f64 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -239,18 +239,18 @@ direction LR +generateTextResult() GenerativeAiResult +streamGenerateTextResult() Generator< GenerativeAiResult > +generateImageResult() GenerativeAiResult - +textToSpeechResult() GenerativeAiResult + +convertTextToSpeechResult() GenerativeAiResult +generateSpeechResult() GenerativeAiResult +generateEmbeddingsResult() EmbeddingResult +generateTextOperation() GenerativeAiOperation +generateImageOperation() GenerativeAiOperation - +textToSpeechOperation() GenerativeAiOperation + +convertTextToSpeechOperation() GenerativeAiOperation +generateSpeechOperation() GenerativeAiOperation +generateEmbeddingsOperation() EmbeddingOperation +generateText() string +streamGenerateText() Generator< string > +generateImage() File - +textToSpeech() File + +convertTextToSpeech() File +generateSpeech() File +generateEmbeddings() Embedding[] } @@ -291,12 +291,12 @@ direction LR +generateTextResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ +streamGenerateTextResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) Generator< GenerativeAiResult >$ +generateImageResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ - +textToSpeechResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ + +convertTextToSpeechResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ +generateSpeechResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ +generateEmbeddingsResult(string[]|Message[] $input, AiModel $model) EmbeddingResult$ +generateTextOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ +generateImageOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ - +textToSpeechOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ + +convertTextToSpeechOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ +generateSpeechOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ +generateEmbeddingsOperation(string[]|Message[] $input, AiModel $model) EmbeddingOperation$ } @@ -357,12 +357,12 @@ direction LR +generateTextResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ +streamGenerateTextResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) Generator< GenerativeAiResult >$ +generateImageResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ - +textToSpeechResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ + +convertTextToSpeechResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ +generateSpeechResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ +generateEmbeddingsResult(string[]|Message[] $input, AiModel $model) EmbeddingResult$ +generateTextOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ +generateImageOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ - +textToSpeechOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ + +convertTextToSpeechOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ +generateSpeechOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ +generateEmbeddingsOperation(string[]|Message[] $input, AiModel $model) EmbeddingOperation$ } @@ -390,18 +390,18 @@ direction LR +generateTextResult() GenerativeAiResult +streamGenerateTextResult() Generator< GenerativeAiResult > +generateImageResult() GenerativeAiResult - +textToSpeechResult() GenerativeAiResult + +convertTextToSpeechResult() GenerativeAiResult +generateSpeechResult() GenerativeAiResult +generateEmbeddingsResult() EmbeddingResult +generateTextOperation() GenerativeAiOperation +generateImageOperation() GenerativeAiOperation - +textToSpeechOperation() GenerativeAiOperation + +convertTextToSpeechOperation() GenerativeAiOperation +generateSpeechOperation() GenerativeAiOperation +generateEmbeddingsOperation() EmbeddingOperation +generateText() string +streamGenerateText() Generator< string > +generateImage() File - +textToSpeech() File + +convertTextToSpeech() File +generateSpeech() File +generateEmbeddings() Embedding[] } @@ -684,8 +684,8 @@ direction LR class AiImageGenerationModel { +generateImageResult(Message[] $prompt) GenerativeAiResult } - class AiTextToSpeechModel { - +textToSpeechResult(Message[] $prompt) GenerativeAiResult + class AiTextToSpeechConversionModel { + +convertTextToSpeechResult(Message[] $prompt) GenerativeAiResult } class AiSpeechGenerationModel { +generateSpeechResult(Message[] $prompt) GenerativeAiResult @@ -699,8 +699,8 @@ direction LR class AiImageGenerationOperationModel { +generateImageOperation(Message[] $prompt) GenerativeAiOperation } - class AiTextToSpeechOperationModel { - +textToSpeechOperation(Message[] $prompt) GenerativeAiOperation + class AiTextToSpeechConversionOperationModel { + +convertTextToSpeechOperation(Message[] $prompt) GenerativeAiOperation } class AiSpeechGenerationOperationModel { +generateSpeechOperation(Message[] $prompt) GenerativeAiOperation @@ -764,7 +764,7 @@ direction LR } class ImageGenerationConfig { } - class TextToSpeechConfig { + class TextToSpeechConversionConfig { } class SpeechGenerationConfig { } @@ -829,12 +829,12 @@ direction LR <> WithEmbeddingOperations <> AiTextGenerationModel <> AiImageGenerationModel - <> AiTextToSpeechModel + <> AiTextToSpeechConversionModel <> AiSpeechGenerationModel <> AiEmbeddingGenerationModel <> AiTextGenerationOperationModel <> AiImageGenerationOperationModel - <> AiTextToSpeechOperationModel + <> AiTextToSpeechConversionOperationModel <> AiSpeechGenerationOperationModel <> AiEmbeddingGenerationOperationModel <> WithHttpClient @@ -866,17 +866,17 @@ direction LR AiModelMetadata ..> AiFeature AiModel <|-- AiTextGenerationModel AiModel <|-- AiImageGenerationModel - AiModel <|-- AiTextToSpeechModel + AiModel <|-- AiTextToSpeechConversionModel AiModel <|-- AiSpeechGenerationModel AiModel <|-- AiEmbeddingGenerationModel AiModel <|-- AiTextGenerationOperationModel AiModel <|-- AiImageGenerationOperationModel - AiModel <|-- AiTextToSpeechOperationModel + AiModel <|-- AiTextToSpeechConversionOperationModel AiModel <|-- AiSpeechGenerationOperationModel AiModel <|-- AiEmbeddingGenerationOperationModel GenerationConfig <|-- TextGenerationConfig GenerationConfig <|-- ImageGenerationConfig - GenerationConfig <|-- TextToSpeechConfig + GenerationConfig <|-- TextToSpeechConversionConfig GenerationConfig <|-- SpeechGenerationConfig GenerationConfig <|-- EmbeddingGenerationConfig ``` From 3346bdd89ca3d1c2147a13b9164bcbfa904629af Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 11 Jul 2025 15:35:09 -0700 Subject: [PATCH 09/39] Rename getUsage() to getTokenUsage(). --- docs/ARCHITECTURE.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3b4b5f64..93ad418d 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -485,14 +485,14 @@ direction LR } class Result { +getId() string - +getUsage() TokenUsage + +getTokenUsage() TokenUsage +getProviderMetadata() array< string, mixed > +getJsonSchema() array< string, mixed >$ } class GenerativeAiResult { +getId() string +getCandidates() Candidate[] - +getUsage() TokenUsage + +getTokenUsage() TokenUsage +getProviderMetadata() array< string, mixed > +getJsonSchema() array< string, mixed >$ %% The following utility methods transform the result candidates into a specific shape. @@ -510,7 +510,7 @@ direction LR class EmbeddingResult { +getId() string +getEmbeddings() Embedding[] - +getUsage() TokenUsage + +getTokenUsage() TokenUsage +getProviderMetadata() array< string, mixed > +getJsonSchema() array< string, mixed >$ } From eef847e87b8ec5e2cbdcf8e7ae058ffba08cdde8 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 11 Jul 2025 16:14:17 -0700 Subject: [PATCH 10/39] Rename features/capabilities to capabilities/options and simplify model discovery via AiModelRequirements objects that can be automatically inferred from message and config objects. --- docs/ARCHITECTURE.md | 110 +++++++++++++++++++++++++++---------------- docs/GLOSSARY.md | 2 + 2 files changed, 71 insertions(+), 41 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 93ad418d..9cc4aee1 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -6,7 +6,7 @@ This document outlines the architecture for the PHP AI Client. It is critical th The API design at a high level is heavily inspired by the [Vercel AI SDK](https://github.com/vercel/ai), which is widely used in the NodeJS ecosystem and one of the very few comprehensive AI client SDKs available. -The main additional aspect that the Vercel AI SDK does not cater for easily is for a developer to use AI in a way that the choice of provider remains with the user. To clarify with an example: Instead of "Generate text with Google's model `gemini-2.5-flash`", go with "Generate text using any provider model that supports text generation and multimodal input". In other words, there needs to be a mechanism that allows finding any configured model that supports the given set of required AI features and capabilities. +The main additional aspect that the Vercel AI SDK does not cater for easily is for a developer to use AI in a way that the choice of provider remains with the user. To clarify with an example: Instead of "Generate text with Google's model `gemini-2.5-flash`", go with "Generate text using any provider model that supports text generation and multimodal input". In other words, there needs to be a mechanism that allows finding any configured model that supports the given set of required AI capabilities and options. ### Code examples @@ -36,7 +36,7 @@ $texts = Ai::generateTextResult( 'Write a 2-verse poem about PHP.', Anthropic::model( 'claude-3.7-sonnet', - [TextGenerationConfig::CANDIDATE_COUNT => 4] + [AiOption::CANDIDATE_COUNT => 4] ) )->toTexts(); ``` @@ -46,7 +46,7 @@ $texts = Ai::generateTextResult( ```php $modelsMetadata = Ai::defaultRegistry()->findProviderModelsMetadataForSupport( 'openai', - AiFeature::IMAGE_GENERATION + AiCapability::IMAGE_GENERATION ); $imageFile = Ai::generateImageResult( 'Generate an illustration of the PHP elephant in the Carribean sea.', @@ -61,7 +61,7 @@ $imageFile = Ai::generateImageResult( ```php $providerModelsMetadata = Ai::defaultRegistry()->findModelsMetadataForSupport( - AiFeature::IMAGE_GENERATION + AiCapability::IMAGE_GENERATION ); $imageFile = Ai::generateImageResult( 'Generate an illustration of the PHP elephant in the Carribean sea.', @@ -78,7 +78,7 @@ _Note: This does effectively the exact same as [the first code example](#generat ```php $providerModelsMetadata = Ai::defaultRegistry()->findModelsMetadataForSupport( - AiFeature::TEXT_GENERATION + AiCapability::TEXT_GENERATION ); $text = Ai::generateTextResult( 'Write a 2-verse poem about PHP.', @@ -91,7 +91,7 @@ $text = Ai::generateTextResult( #### Generate text with an image as additional input using any suitable model from any provider -_Note: Since this omits the model parameter, the SDK will automatically determine which models are suitable and use any of them, similar to [the first code example](#generate-text-using-any-suitable-model-from-any-provider-most-basic-example). Since it knows the input includes an image, it can internally infer that the model needs to not only support `AiFeature::TEXT_GENERATION`, but also `TextGenerationConfig::INPUT_MODALITIES => ['text', 'image']`._ +_Note: Since this omits the model parameter, the SDK will automatically determine which models are suitable and use any of them, similar to [the first code example](#generate-text-using-any-suitable-model-from-any-provider-most-basic-example). Since it knows the input includes an image, it can internally infer that the model needs to not only support `AiCapability::TEXT_GENERATION`, but also `AiOption::INPUT_MODALITIES => ['text', 'image']`._ ```php $text = Ai::generateTextResult( @@ -109,7 +109,7 @@ $text = Ai::generateTextResult( #### Generate text with chat history using any suitable model from any provider -_Note: Similarly to the previous example, even without specifying the model here, the SDK will be able to infer required model capabilities because it can detect that multiple chat messages are passed. Therefore it will internally only consider models that support `AiFeature::TEXT_GENERATION` as well as `TextGenerationConfig::CHAT_HISTORY`._ +_Note: Similarly to the previous example, even without specifying the model here, the SDK will be able to infer required model capabilities because it can detect that multiple chat messages are passed. Therefore it will internally only consider models that support `AiCapability::TEXT_GENERATION` as well as `AiCapability::CHAT_HISTORY`._ ```php $text = Ai::generateTextResult( @@ -136,11 +136,11 @@ _Note: Unlike the previous two examples, to require JSON output it is necessary ```php $providerModelsMetadata = Ai::defaultRegistry()->findModelsMetadataForSupport( - AiFeature::TEXT_GENERATION, + AiCapability::TEXT_GENERATION, [ // Make sure the model supports JSON output as well as following a given schema. - TextGenerationConfig::OUTPUT_MIME_TYPE => 'application/json', - TextGenerationConfig::OUTPUT_SCHEMA => true, + AiOption::OUTPUT_MIME_TYPE => 'application/json', + AiOption::OUTPUT_SCHEMA => true, ] ); $jsonString = Ai::generateTextResult( @@ -149,19 +149,17 @@ $jsonString = Ai::generateTextResult( $providerModelsMetadata[0]->getProvider()->getId(), $providerModelsMetadata[0]->getModels()[0]->getId(), [ - AiModelConfig::GENERATION_CONFIG => [ - TextGenerationConfig::OUTPUT_MIME_TYPE => 'application/json', - TextGenerationConfig::OUTPUT_SCHEMA => [ - 'type' => 'array', - 'items' => [ - 'type' => 'object', - 'properties' => [ - 'name' => [ - 'type' => 'string', - ], - 'age' => [ - 'type' => 'integer', - ], + AiOption::OUTPUT_MIME_TYPE => 'application/json', + AiOption::OUTPUT_SCHEMA => [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'name' => [ + 'type' => 'string', + ], + 'age' => [ + 'type' => 'integer', ], ], ], @@ -175,7 +173,7 @@ $jsonString = Ai::generateTextResult( ```php $providerModelsMetadata = Ai::defaultRegistry()->findModelsMetadataForSupport( - AiFeature::EMBEDDING_GENERATION + AiCapability::EMBEDDING_GENERATION ); $embeddings = Ai::generateEmbeddingsResult( [ @@ -253,6 +251,8 @@ direction LR +convertTextToSpeech() File +generateSpeech() File +generateEmbeddings() Embedding[] + +getModelRequirements() AiModelRequirements + +isSupported() bool } class MessageBuilder { @@ -327,9 +327,9 @@ direction LR +hasProvider(string $idOrClassName) bool +getProviderClassName(string $id) string +isProviderConfigured(string $idOrClassName) bool - +getProviderModel(string $idOrClassName, string $modelId, AiModelConfig|array $modelConfig) AiModel - +findProviderModelsMetadataForSupport(string $idOrClassName, AiFeature $feature, array $capabilities) AiModelMetadata[] - +findModelsMetadataForSupport(AiFeature $feature, array $capabilities) AiProviderModelMetadata[] + +getProviderModel(string $idOrClassName, string $modelId, AiModelConfig|array< string, mixed > $modelConfig) AiModel + +findProviderModelsMetadataForSupport(string $idOrClassName, AiModelRequirements $modelRequirements) AiModelMetadata[] + +findModelsMetadataForSupport(AiModelRequirements $modelRequirements) AiProviderModelMetadata[] } } @@ -404,6 +404,8 @@ direction LR +convertTextToSpeech() File +generateSpeech() File +generateEmbeddings() Embedding[] + +getModelRequirements() AiModelRequirements + +isSupported() bool } class MessageBuilder { @@ -579,6 +581,9 @@ direction LR +toFirstAudioFile(Candidate[] $candidates) File$ +toFirstVideoFile(Candidate[] $candidates) File$ } + class RequirementsUtil { + +inferRequirements(Message[] $messages, AiModelConfig $modelConfig) AiModelRequirements$ + } } <> File @@ -645,9 +650,9 @@ direction LR +hasProvider(string $idOrClassName) bool +getProviderClassName(string $id) string +isProviderConfigured(string $idOrClassName) bool - +getProviderModel(string $idOrClassName, string $modelId, AiModelConfig|array $modelConfig) AiModel - +findProviderModelsMetadataForSupport(string $idOrClassName, AiFeature $feature, array $capabilities) AiModelMetadata[] - +findModelsMetadataForSupport(AiFeature $feature, array $capabilities) AiProviderModelMetadata[] + +getProviderModel(string $idOrClassName, string $modelId, AiModelConfig|array< string, mixed > $modelConfig) AiModel + +findProviderModelsMetadataForSupport(string $idOrClassName, AiModelRequirements $modelRequirements) AiModelMetadata[] + +findModelsMetadataForSupport(AiModelRequirements $modelRequirements) AiProviderModelMetadata[] } } namespace Ai.Providers.Contracts { @@ -661,7 +666,6 @@ direction LR +metadata() AiModelMetadata +setConfig(AiModelConfig $config) void +getConfig() AiModelConfig - +getSupportedCapabilities() AiCapability[]$ } class AiProviderAvailability { +isConfigured() bool @@ -735,8 +739,8 @@ direction LR class AiModelMetadata { +getId() string +getName() string - +getSupportedFeatures() AiFeature[] +getSupportedCapabilities() AiCapability[] + +getSupportedOptions() AiSupportedOption[] +getJsonSchema() array< string, mixed >$ } class AiProviderModelsMetadata { @@ -744,6 +748,10 @@ direction LR +getModels() AiModelMetadata[] +getJsonSchema() array< string, mixed >$ } + class AiModelRequirements { + getRequiredCapabilities() AiCapability[] + getRequiredOptions() AiRequiredOption[] + } class AiModelConfig { +setSystemInstruction(string|MessagePart|MessagePart[]|Message $systemInstruction) void +getSystemInstruction() Message? @@ -787,12 +795,17 @@ direction LR +getDisallowedDomains() string[] +getJsonSchema() array< string, mixed >$ } - class AiCapability { - +isSupported() bool + class AiSupportedOption { + +getName() string +isSupportedValue(mixed $value) bool +getSupportedValues() mixed[] +getJsonSchema() array< string, mixed >$ } + class AiRequiredOption { + +getName() string + +getValue() mixed + +getJsonSchema() array< string, mixed >$ + } } namespace Ai.Providers.Enums { class AiProviderType { @@ -804,7 +817,7 @@ direction LR FUNCTION_DECLARATIONS WEB_SEARCH } - class AiFeature { + class AiCapability { TEXT_GENERATION IMAGE_GENERATION TEXT_TO_SPEECH @@ -812,12 +825,23 @@ direction LR MUSIC_GENERATION VIDEO_GENERATION EMBEDDING_GENERATION + CHAT_HISTORY + } + class AiOption { + INPUT_MODALITIES + OUTPUT_MODALITIES + CANDIDATE_COUNT + TEMPERATURE + TOP_K + TOP_P + OUTPUT_MIME_TYPE + OUTPUT_SCHEMA } } namespace Ai.Providers.Util { - class AiFeaturesUtil { - +getSupportedFeatures(AiModel|string $modelClass) AiFeature[]$ + class AiCapabilitiesUtil { +getSupportedCapabilities(AiModel|string $modelClass) AiCapability[]$ + +getSupportedOptions(AiModel|string $modelClass) AiSupportedOption[]$ } } @@ -842,7 +866,8 @@ direction LR <> WithAuthentication <> Authentication <> GenerationConfig - <> AiFeature + <> AiCapability + <> AiOption <> AiProviderType AiProvider .. AiModel : creates @@ -856,14 +881,17 @@ direction LR AiProviderRegistry "1" o-- "0..*" AiProvider AiProviderRegistry "1" o-- "0..*" AiProviderMetadata AiModelMetadataDirectory "1" o-- "1..*" AiModelMetadata - AiModelMetadata "1" o-- "1..*" AiFeature - AiModelMetadata "1" o-- "0..*" AiCapability + AiModelMetadata "1" o-- "1..*" AiCapability + AiModelMetadata "1" o-- "0..*" AiSupportedOption + AiModelRequirements "1" o-- "1..*" AiCapability + AiModelRequirements "1" o-- "0..*" AiRequiredOption AiModelConfig "1" o-- "0..1" GenerationConfig AiModelConfig "1" o-- "0..*" Tool Tool "1" o-- "0..*" FunctionDeclaration Tool "1" o-- "0..1" WebSearch AiProviderMetadata ..> AiProviderType - AiModelMetadata ..> AiFeature + AiModelMetadata ..> AiCapability + AiModelMetadata ..> AiSupportedOption AiModel <|-- AiTextGenerationModel AiModel <|-- AiImageGenerationModel AiModel <|-- AiTextToSpeechConversionModel diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index 0968ec8d..a26390cb 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -3,6 +3,8 @@ This glossary defines common terms relevant for the PHP AI Client and related projects. * **Agent**: An autonomous system that can perceive its environment, make decisions, and take actions to achieve specific goals, often leveraging AI models. +* **AI Capability**: A single string that denotes an AI feature, such as text generation or image generation. +* **AI Option**: A parameter that can be passed to a model, such as temperature or output MIME type. * **Generative AI**: Overaching term describing AI models that generate content as requested in a prompt. * **MCP**: The "Model Context Protocol", a proposed standard for connecting AI assistants to the systems where data lives. * **Message**: A single message, either a user prompt, a model response, or a system prompt—optionally containing of multiple message parts. From 4b9a6c84af0ebf71c1160cbe5e83aa6fece0a28c Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 11 Jul 2025 16:16:27 -0700 Subject: [PATCH 11/39] Consistently use AI implementers and extenders term. --- docs/ARCHITECTURE.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 9cc4aee1..c7b0afc0 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -196,9 +196,9 @@ This section shows comprehensive class diagrams for the proposed architecture. F **Note:** The class diagrams are also not meant to be comprehensive in terms of any specific configuration keys or parameters which are or will be supported. For now, the relevant definitions don't include any specific parameter names or constants. -### Overview: Fluent API for AI consumption +### Overview: Fluent API for AI implementers -This is a subset of the overall class diagram, purely focused on the fluent API for AI consumption. +This is a subset of the overall class diagram, purely focused on the fluent API for AI implementers. ```mermaid --- @@ -272,9 +272,9 @@ direction LR AiEntrypoint .. MessageBuilder : creates ``` -### Overview: Traditional method call API for AI consumption +### Overview: Traditional method call API for AI implementers -This is a subset of the overall class diagram, purely focused on the traditional method call API for AI consumption. +This is a subset of the overall class diagram, purely focused on the traditional method call API for AI implementers. ```mermaid --- @@ -303,9 +303,9 @@ direction LR } ``` -### Overview: API for provider registration and implementation +### Overview: API for AI extenders -This is a subset of the overall class diagram, purely focused on the API for provider registration and implementation. +This is a subset of the overall class diagram, purely focused on the API for AI extenders. ```mermaid --- @@ -336,7 +336,7 @@ direction LR AiEntrypoint "1" o-- "1..*" AiProviderRegistry ``` -### Details: Class diagram for AI consumption +### Details: Class diagram for AI implementers ```mermaid --- @@ -634,7 +634,7 @@ direction LR Result <|-- EmbeddingResult ``` -### Details: Class diagram for AI provider registration and implementation +### Details: Class diagram for AI extenders ```mermaid --- From 0f6e21f141ccec560304933c3f5a21e3797f0179 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 11 Jul 2025 16:18:12 -0700 Subject: [PATCH 12/39] Include max_tokens as another illustrative AI option. --- docs/ARCHITECTURE.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c7b0afc0..f8185309 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -224,6 +224,7 @@ direction LR +withHistory(...Message $messages) self +usingModel(AiModel $model) self +usingSystemInstruction(string|MessagePart[]|Message $systemInstruction) self + +usingMaxTokens(int $maxTokens) self +usingTemperature(float $temperature) self +usingTopP(float $topP) self +usingTopK(int $topK) self @@ -377,6 +378,7 @@ direction LR +withHistory(...Message $messages) self +usingModel(AiModel $model) self +usingSystemInstruction(string|MessagePart[]|Message $systemInstruction) self + +usingMaxTokens(int $maxTokens) self +usingTemperature(float $temperature) self +usingTopP(float $topP) self +usingTopK(int $topK) self @@ -831,6 +833,7 @@ direction LR INPUT_MODALITIES OUTPUT_MODALITIES CANDIDATE_COUNT + MAX_TOKENS TEMPERATURE TOP_K TOP_P From af606a12693eae5362e307e64ca1e23dfa9a985f Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 11 Jul 2025 20:15:38 -0700 Subject: [PATCH 13/39] Get rid of unnecessary GenerationConfig classes in favor of storing options directly on AiModelConfig. --- docs/ARCHITECTURE.md | 46 +++++++++++++++++++------------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f8185309..5fa38e08 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -755,31 +755,31 @@ direction LR getRequiredOptions() AiRequiredOption[] } class AiModelConfig { + +setOutputModalities(AiModality[] $modalities) void + +getOutputModalities() AiModality[] +setSystemInstruction(string|MessagePart|MessagePart[]|Message $systemInstruction) void +getSystemInstruction() Message? - +setGenerationConfig(GenerationConfig $config) void - +getGenerationConfig() GenerationConfig? + +setCandidateCount(int $candidateCount) void + +getCandidateCount() int + +setMaxTokens(int $maxTokens) void + +getMaxTokens() int + +setTemperature(float $temperature) void + +getTemperature() float + +setTopK(int $topK) void + +getTopK() int + +setTopP(float $topP) void + +getTopP() float + +setOutputMimeType(string $outputMimeType) void + +getOutputMimeType() string + +setOutputSchema(array< string, mixed > $outputSchema) void + +getOutputSchema() array< string, mixed > + +setCustomOption(string $key, mixed $value) void + +getCustomOption(string $key) mixed + +getCustomOptions() array< string, mixed > +setTools(Tool[] $tools) void +getTools() Tool[] +getJsonSchema() array< string, mixed >$ } - class GenerationConfig { - +setValue(string $key, mixed $value) void - +getValue(string $key) mixed - +getValues() array< string, mixed > - +getAdditionalValues() array< string, mixed > - +getJsonSchema() array< string, mixed >$ - } - class TextGenerationConfig { - } - class ImageGenerationConfig { - } - class TextToSpeechConversionConfig { - } - class SpeechGenerationConfig { - } - class EmbeddingGenerationConfig { - } class Tool { +getType() ToolType +getFunctionDeclarations() FunctionDeclaration[]? @@ -832,6 +832,7 @@ direction LR class AiOption { INPUT_MODALITIES OUTPUT_MODALITIES + SYSTEM_INSTRUCTION CANDIDATE_COUNT MAX_TOKENS TEMPERATURE @@ -868,7 +869,6 @@ direction LR <> HttpClient <> WithAuthentication <> Authentication - <> GenerationConfig <> AiCapability <> AiOption <> AiProviderType @@ -888,7 +888,6 @@ direction LR AiModelMetadata "1" o-- "0..*" AiSupportedOption AiModelRequirements "1" o-- "1..*" AiCapability AiModelRequirements "1" o-- "0..*" AiRequiredOption - AiModelConfig "1" o-- "0..1" GenerationConfig AiModelConfig "1" o-- "0..*" Tool Tool "1" o-- "0..*" FunctionDeclaration Tool "1" o-- "0..1" WebSearch @@ -905,9 +904,4 @@ direction LR AiModel <|-- AiTextToSpeechConversionOperationModel AiModel <|-- AiSpeechGenerationOperationModel AiModel <|-- AiEmbeddingGenerationOperationModel - GenerationConfig <|-- TextGenerationConfig - GenerationConfig <|-- ImageGenerationConfig - GenerationConfig <|-- TextToSpeechConversionConfig - GenerationConfig <|-- SpeechGenerationConfig - GenerationConfig <|-- EmbeddingGenerationConfig ``` From 9206a9f13bd6f68f10d7f42ee86abcf972a7b889 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 11 Jul 2025 20:32:19 -0700 Subject: [PATCH 14/39] Mention the two alternative implementer API surfaces in the introduction. --- docs/ARCHITECTURE.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 5fa38e08..c0d68707 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -4,10 +4,15 @@ This document outlines the architecture for the PHP AI Client. It is critical th ## High-level API design -The API design at a high level is heavily inspired by the [Vercel AI SDK](https://github.com/vercel/ai), which is widely used in the NodeJS ecosystem and one of the very few comprehensive AI client SDKs available. +The architecture at a high level is heavily inspired by the [Vercel AI SDK](https://github.com/vercel/ai), which is widely used in the NodeJS ecosystem and one of the very few comprehensive AI client SDKs available. The main additional aspect that the Vercel AI SDK does not cater for easily is for a developer to use AI in a way that the choice of provider remains with the user. To clarify with an example: Instead of "Generate text with Google's model `gemini-2.5-flash`", go with "Generate text using any provider model that supports text generation and multimodal input". In other words, there needs to be a mechanism that allows finding any configured model that supports the given set of required AI capabilities and options. +For the implementer facing API surface, two alternative APIs are available: + +* A fluent API is used as the primary means of using the AI client SDK, for easy-to-read code by chaining declarative methods. +* A traditional method based API inspired by the Vercel AI SDK, which is more aligned with traditional WordPress patterns such as passing an array of arguments. + ### Code examples The following examples indicate how this SDK could eventually be used. From 6f8fe1939444295b7eec4982700f4f7deffba506 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 11 Jul 2025 20:33:10 -0700 Subject: [PATCH 15/39] Rename AiEntrypoint to AiClient and fix name in code examples. --- docs/ARCHITECTURE.md | 70 ++++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c0d68707..f5b75be0 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -20,7 +20,7 @@ The following examples indicate how this SDK could eventually be used. #### Generate text using any suitable model from any provider (most basic example) ```php -$text = Ai::generateTextResult( +$text = AiClient::generateTextResult( 'Write a 2-verse poem about PHP.' )->toText(); ``` @@ -28,7 +28,7 @@ $text = Ai::generateTextResult( #### Generate text using a Google model ```php -$text = Ai::generateTextResult( +$text = AiClient::generateTextResult( 'Write a 2-verse poem about PHP.', Google::model('gemini-2.5-flash') )->toText(); @@ -37,7 +37,7 @@ $text = Ai::generateTextResult( #### Generate multiple text candidates using an Anthropic model ```php -$texts = Ai::generateTextResult( +$texts = AiClient::generateTextResult( 'Write a 2-verse poem about PHP.', Anthropic::model( 'claude-3.7-sonnet', @@ -49,13 +49,13 @@ $texts = Ai::generateTextResult( #### Generate an image using any suitable OpenAI model ```php -$modelsMetadata = Ai::defaultRegistry()->findProviderModelsMetadataForSupport( +$modelsMetadata = AiClient::defaultRegistry()->findProviderModelsMetadataForSupport( 'openai', AiCapability::IMAGE_GENERATION ); -$imageFile = Ai::generateImageResult( +$imageFile = AiClient::generateImageResult( 'Generate an illustration of the PHP elephant in the Carribean sea.', - Ai::defaultRegistry()->getProviderModel( + AiClient::defaultRegistry()->getProviderModel( 'openai', $modelsMetadata[0]->getId() ) @@ -65,12 +65,12 @@ $imageFile = Ai::generateImageResult( #### Generate an image using any suitable model from any provider ```php -$providerModelsMetadata = Ai::defaultRegistry()->findModelsMetadataForSupport( +$providerModelsMetadata = AiClient::defaultRegistry()->findModelsMetadataForSupport( AiCapability::IMAGE_GENERATION ); -$imageFile = Ai::generateImageResult( +$imageFile = AiClient::generateImageResult( 'Generate an illustration of the PHP elephant in the Carribean sea.', - Ai::defaultRegistry()->getProviderModel( + AiClient::defaultRegistry()->getProviderModel( $providerModelsMetadata[0]->getProvider()->getId(), $providerModelsMetadata[0]->getModels()[0]->getId() ) @@ -82,12 +82,12 @@ $imageFile = Ai::generateImageResult( _Note: This does effectively the exact same as [the first code example](#generate-text-using-any-suitable-model-from-any-provider-most-basic-example), but more verbosely. In other words, if you omit the model parameter, the SDK will do this internally._ ```php -$providerModelsMetadata = Ai::defaultRegistry()->findModelsMetadataForSupport( +$providerModelsMetadata = AiClient::defaultRegistry()->findModelsMetadataForSupport( AiCapability::TEXT_GENERATION ); -$text = Ai::generateTextResult( +$text = AiClient::generateTextResult( 'Write a 2-verse poem about PHP.', - Ai::defaultRegistry()->getProviderModel( + AiClient::defaultRegistry()->getProviderModel( $providerModelsMetadata[0]->getProvider()->getId(), $providerModelsMetadata[0]->getModels()[0]->getId() ) @@ -99,7 +99,7 @@ $text = Ai::generateTextResult( _Note: Since this omits the model parameter, the SDK will automatically determine which models are suitable and use any of them, similar to [the first code example](#generate-text-using-any-suitable-model-from-any-provider-most-basic-example). Since it knows the input includes an image, it can internally infer that the model needs to not only support `AiCapability::TEXT_GENERATION`, but also `AiOption::INPUT_MODALITIES => ['text', 'image']`._ ```php -$text = Ai::generateTextResult( +$text = AiClient::generateTextResult( [ [ 'text' => 'Generate alternative text for this image.', @@ -117,7 +117,7 @@ $text = Ai::generateTextResult( _Note: Similarly to the previous example, even without specifying the model here, the SDK will be able to infer required model capabilities because it can detect that multiple chat messages are passed. Therefore it will internally only consider models that support `AiCapability::TEXT_GENERATION` as well as `AiCapability::CHAT_HISTORY`._ ```php -$text = Ai::generateTextResult( +$text = AiClient::generateTextResult( [ [ 'role' => MessageRole::USER, @@ -140,7 +140,7 @@ $text = Ai::generateTextResult( _Note: Unlike the previous two examples, to require JSON output it is necessary to go the verbose route, since it is impossible for the SDK to detect whether you require JSON output purely from the prompt input. Therefore this code example contains the logic to manually search for suitable models and then use one of them for the task._ ```php -$providerModelsMetadata = Ai::defaultRegistry()->findModelsMetadataForSupport( +$providerModelsMetadata = AiClient::defaultRegistry()->findModelsMetadataForSupport( AiCapability::TEXT_GENERATION, [ // Make sure the model supports JSON output as well as following a given schema. @@ -148,9 +148,9 @@ $providerModelsMetadata = Ai::defaultRegistry()->findModelsMetadataForSupport( AiOption::OUTPUT_SCHEMA => true, ] ); -$jsonString = Ai::generateTextResult( +$jsonString = AiClient::generateTextResult( 'Transform the following CSV content into a JSON array of row data.', - Ai::defaultRegistry()->getProviderModel( + AiClient::defaultRegistry()->getProviderModel( $providerModelsMetadata[0]->getProvider()->getId(), $providerModelsMetadata[0]->getModels()[0]->getId(), [ @@ -177,16 +177,16 @@ $jsonString = Ai::generateTextResult( #### Generate embeddings using any suitable model from any provider ```php -$providerModelsMetadata = Ai::defaultRegistry()->findModelsMetadataForSupport( +$providerModelsMetadata = AiClient::defaultRegistry()->findModelsMetadataForSupport( AiCapability::EMBEDDING_GENERATION ); -$embeddings = Ai::generateEmbeddingsResult( +$embeddings = AiClient::generateEmbeddingsResult( [ 'A very long text.', 'Another very long text.', 'More long text.', ], - Ai::defaultRegistry()->getProviderModel( + AiClient::defaultRegistry()->getProviderModel( $providerModelsMetadata[0]->getProvider()->getId(), $providerModelsMetadata[0]->getModels()[0]->getId() ) @@ -214,7 +214,7 @@ config: classDiagram direction LR namespace Ai { - class AiEntrypoint { + class AiClient { +prompt(?string $text) PromptBuilder$ +message(?string $text) MessageBuilder$ } @@ -274,8 +274,8 @@ direction LR } } - AiEntrypoint .. PromptBuilder : creates - AiEntrypoint .. MessageBuilder : creates + AiClient .. PromptBuilder : creates + AiClient .. MessageBuilder : creates ``` ### Overview: Traditional method call API for AI implementers @@ -291,7 +291,7 @@ config: classDiagram direction LR namespace Ai { - class AiEntrypoint { + class AiClient { +generateResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ +generateOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ +generateTextResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ @@ -322,7 +322,7 @@ config: classDiagram direction LR namespace Ai { - class AiEntrypoint { + class AiClient { +defaultRegistry() AiProviderRegistry$ +isConfigured(AiProviderAvailability $availability) bool$ } @@ -339,7 +339,7 @@ direction LR } } - AiEntrypoint "1" o-- "1..*" AiProviderRegistry + AiClient "1" o-- "1..*" AiProviderRegistry ``` ### Details: Class diagram for AI implementers @@ -353,7 +353,7 @@ config: classDiagram direction LR namespace Ai { - class AiEntrypoint { + class AiClient { +prompt(?string $text) PromptBuilder$ +message(?string $text) MessageBuilder$ +defaultRegistry() AiProviderRegistry$ @@ -602,14 +602,14 @@ direction LR <> OperationState <> AiModality - AiEntrypoint .. Message : receives - AiEntrypoint .. MessagePart : receives - AiEntrypoint .. PromptBuilder : creates - AiEntrypoint .. MessageBuilder : creates - AiEntrypoint .. GenerativeAiResult : creates - AiEntrypoint .. EmbeddingResult : creates - AiEntrypoint .. GenerativeAiOperation : creates - AiEntrypoint .. EmbeddingOperation : creates + AiClient .. Message : receives + AiClient .. MessagePart : receives + AiClient .. PromptBuilder : creates + AiClient .. MessageBuilder : creates + AiClient .. GenerativeAiResult : creates + AiClient .. EmbeddingResult : creates + AiClient .. GenerativeAiOperation : creates + AiClient .. EmbeddingOperation : creates PromptBuilder .. GenerativeAiResult : creates PromptBuilder .. EmbeddingResult : creates PromptBuilder .. GenerativeAiOperation : creates From 6142c5351aeda93aa89b0732775c6b95102b7876 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 11 Jul 2025 20:37:24 -0700 Subject: [PATCH 16/39] Fix lack of AiModelRequirements in code examples. --- docs/ARCHITECTURE.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f5b75be0..8c12e936 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -51,7 +51,7 @@ $texts = AiClient::generateTextResult( ```php $modelsMetadata = AiClient::defaultRegistry()->findProviderModelsMetadataForSupport( 'openai', - AiCapability::IMAGE_GENERATION + new AiModelRequirements([AiCapability::IMAGE_GENERATION]) ); $imageFile = AiClient::generateImageResult( 'Generate an illustration of the PHP elephant in the Carribean sea.', @@ -66,7 +66,7 @@ $imageFile = AiClient::generateImageResult( ```php $providerModelsMetadata = AiClient::defaultRegistry()->findModelsMetadataForSupport( - AiCapability::IMAGE_GENERATION + new AiModelRequirements([AiCapability::IMAGE_GENERATION]) ); $imageFile = AiClient::generateImageResult( 'Generate an illustration of the PHP elephant in the Carribean sea.', @@ -83,7 +83,7 @@ _Note: This does effectively the exact same as [the first code example](#generat ```php $providerModelsMetadata = AiClient::defaultRegistry()->findModelsMetadataForSupport( - AiCapability::TEXT_GENERATION + new AiModelRequirements([AiCapability::TEXT_GENERATION]) ); $text = AiClient::generateTextResult( 'Write a 2-verse poem about PHP.', @@ -141,12 +141,14 @@ _Note: Unlike the previous two examples, to require JSON output it is necessary ```php $providerModelsMetadata = AiClient::defaultRegistry()->findModelsMetadataForSupport( - AiCapability::TEXT_GENERATION, - [ - // Make sure the model supports JSON output as well as following a given schema. - AiOption::OUTPUT_MIME_TYPE => 'application/json', - AiOption::OUTPUT_SCHEMA => true, - ] + new AiModelRequirements( + [AiCapability::TEXT_GENERATION], + [ + // Make sure the model supports JSON output as well as following a given schema. + AiOption::OUTPUT_MIME_TYPE => 'application/json', + AiOption::OUTPUT_SCHEMA => true, + ] + ) ); $jsonString = AiClient::generateTextResult( 'Transform the following CSV content into a JSON array of row data.', @@ -178,7 +180,7 @@ $jsonString = AiClient::generateTextResult( ```php $providerModelsMetadata = AiClient::defaultRegistry()->findModelsMetadataForSupport( - AiCapability::EMBEDDING_GENERATION + new AiModelRequirements([AiCapability::EMBEDDING_GENERATION]) ); $embeddings = AiClient::generateEmbeddingsResult( [ From 3d7990a7e8b5288a9885597ba8925eb374478cd7 Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Sat, 12 Jul 2025 09:50:54 -0400 Subject: [PATCH 17/39] Add fluent API code examples to architecture documentation This proposed set of changes to #2 and is doing a few things: 1. Adding Fluent API code examples (similar to those suggested by @JasonTheAdams [here](https://github.com/WordPress/php-ai-client/pull/2#issuecomment-3054047920)) alongside the Traditional API examples. 2. Proposing a handful of helper methods to simplify the usage of Candidate Count and avoid implementers from needing to understand what _Candidate Count_ means. 3. Representing @JasonTheAdams' suggestion of `AiClient::withImage(string $mimeType, string $base64Blob)` to mimic the input simplicity that the Traditional API supports. 4. Adding the `AiClient::asJsonResponse()` and `AiClient::asArrayResponse()` as a helper methods for `usingOutputMime('application/json')` 5. Adding helper methods for specifying models that support a certain capability or option. --- docs/ARCHITECTURE.md | 163 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 159 insertions(+), 4 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8c12e936..5c76327c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -19,6 +19,13 @@ The following examples indicate how this SDK could eventually be used. #### Generate text using any suitable model from any provider (most basic example) +##### Fluent API +```php +$text = AiClient::prompt('Write a 2-verse poem about PHP.') + ->generateText(); +``` + +##### Traditional API ```php $text = AiClient::generateTextResult( 'Write a 2-verse poem about PHP.' @@ -27,6 +34,14 @@ $text = AiClient::generateTextResult( #### Generate text using a Google model +##### Fluent API +```php +$text = AiClient::prompt('Write a 2-verse poem about PHP.') + ->usingModel('gemini-2.5-flash') + ->generateText(); +``` + +##### Traditional API ```php $text = AiClient::generateTextResult( 'Write a 2-verse poem about PHP.', @@ -36,6 +51,14 @@ $text = AiClient::generateTextResult( #### Generate multiple text candidates using an Anthropic model +##### Fluent API +```php +$texts = AiClient::prompt('Write a 2-verse poem about PHP.') + ->usingModel('claude-3.7-sonnet') + ->generateTexts(4); +``` + +##### Traditional API ```php $texts = AiClient::generateTextResult( 'Write a 2-verse poem about PHP.', @@ -48,6 +71,15 @@ $texts = AiClient::generateTextResult( #### Generate an image using any suitable OpenAI model +##### Fluent API +```php +$imageFile = AiClient::prompt('Generate an illustration of the PHP elephant in the Carribean sea.') + ->usingProvider('openai') + ->usingModelSupportingImages() // Optional. + ->generateImage(); +``` + +##### Traditional API ```php $modelsMetadata = AiClient::defaultRegistry()->findProviderModelsMetadataForSupport( 'openai', @@ -64,6 +96,14 @@ $imageFile = AiClient::generateImageResult( #### Generate an image using any suitable model from any provider +##### Fluent API +```php +$imageFile = AiClient::prompt('Generate an illustration of the PHP elephant in the Carribean sea.') + ->usingModelSupportingImages() // Optional. + ->generateImage(); +``` + +##### Traditional API ```php $providerModelsMetadata = AiClient::defaultRegistry()->findModelsMetadataForSupport( new AiModelRequirements([AiCapability::IMAGE_GENERATION]) @@ -81,6 +121,14 @@ $imageFile = AiClient::generateImageResult( _Note: This does effectively the exact same as [the first code example](#generate-text-using-any-suitable-model-from-any-provider-most-basic-example), but more verbosely. In other words, if you omit the model parameter, the SDK will do this internally._ +##### Fluent API +```php +$text = AiClient::prompt('Write a 2-verse poem about PHP.') + ->usingModelSupportingText() // Optional. + ->generateText(); +``` + +##### Traditional API ```php $providerModelsMetadata = AiClient::defaultRegistry()->findModelsMetadataForSupport( new AiModelRequirements([AiCapability::TEXT_GENERATION]) @@ -98,6 +146,14 @@ $text = AiClient::generateTextResult( _Note: Since this omits the model parameter, the SDK will automatically determine which models are suitable and use any of them, similar to [the first code example](#generate-text-using-any-suitable-model-from-any-provider-most-basic-example). Since it knows the input includes an image, it can internally infer that the model needs to not only support `AiCapability::TEXT_GENERATION`, but also `AiOption::INPUT_MODALITIES => ['text', 'image']`._ +##### Fluent API +```php +$text = AiClient::prompt('Generate alternative text for this image.') + ->withImage('image/png', $base64blob) + ->generateText(); +``` + +##### Traditional API ```php $text = AiClient::generateTextResult( [ @@ -116,6 +172,17 @@ $text = AiClient::generateTextResult( _Note: Similarly to the previous example, even without specifying the model here, the SDK will be able to infer required model capabilities because it can detect that multiple chat messages are passed. Therefore it will internally only consider models that support `AiCapability::TEXT_GENERATION` as well as `AiCapability::CHAT_HISTORY`._ +##### Fluent API +```php +$text = AiClient::prompt('Can you repeat that please?') + ->withHistory( + new UserMessage('Do you spell it WordPress or Wordpress?'), + new AgentMessage('The correct spelling is WordPress.') + ) + ->generateText(); +``` + +##### Traditional API ```php $text = AiClient::generateTextResult( [ @@ -139,6 +206,21 @@ $text = AiClient::generateTextResult( _Note: Unlike the previous two examples, to require JSON output it is necessary to go the verbose route, since it is impossible for the SDK to detect whether you require JSON output purely from the prompt input. Therefore this code example contains the logic to manually search for suitable models and then use one of them for the task._ +##### Fluent API +```php +// Verbose. +$text = AiClient::prompt('Transform the following CSV content into a JSON array of row data.') + ->asJsonResponse() + ->usingOutputSchema(['name' => 'string', 'age' => 'integer']) + ->generateText(); + +// Simple. +$text = AiClient::prompt('Transform the following CSV content into a JSON array of row data.') + ->asJsonResponse(['name' => 'string', 'age' => 'integer']) + ->generateText(); +``` + +##### Traditional API ```php $providerModelsMetadata = AiClient::defaultRegistry()->findModelsMetadataForSupport( new AiModelRequirements( @@ -178,6 +260,13 @@ $jsonString = AiClient::generateTextResult( #### Generate embeddings using any suitable model from any provider +##### Fluent API +```php +$embeddings = AiClient::prompt('A very long text.', 'Another very long text.', 'More long text.') + ->generateEmbeddings(); +``` + +##### Traditional API ```php $providerModelsMetadata = AiClient::defaultRegistry()->findModelsMetadataForSupport( new AiModelRequirements([AiCapability::EMBEDDING_GENERATION]) @@ -217,12 +306,13 @@ classDiagram direction LR namespace Ai { class AiClient { - +prompt(?string $text) PromptBuilder$ - +message(?string $text) MessageBuilder$ + +prompt(...string $text) PromptBuilder$ + +message(...string $text) MessageBuilder$ } class PromptBuilder { +withText(string $text) self + +withImage(string $mimeType, string $base64Blob) self +withImageFile(File $file) self +withAudioFile(File $file) self +withVideoFile(File $file) self @@ -230,6 +320,20 @@ direction LR +withMessageParts(...MessagePart $part) self +withHistory(...Message $messages) self +usingModel(AiModel $model) self + +usingModelSupporting(...AiCapability|AiOption $aiCapabilityOrOption) self + +usingModelSupportingCapability(...AiCapability $aiCapability) self + +usingModelSupportingOption(...AiOption $aiOption) self + +usingModelSupportingAudio() self + +usingModelSupportingHistory() self + +usingModelSupportingEmbeddings() self + +usingModelSupportingImages() self + +usingModelSupportingJsonOutput() self + +usingModelSupportingMusic() self + +usingModelSupportingOutputSchema() self + +usingModelSupportingSpeech() self + +usingModelSupportingText() self + +usingModelSupportingTextToSpeech() self + +usingModelSupportingVideo() self +usingSystemInstruction(string|MessagePart[]|Message $systemInstruction) self +usingMaxTokens(int $maxTokens) self +usingTemperature(float $temperature) self @@ -240,24 +344,41 @@ direction LR +usingOutputMime(string $mimeType) self +usingOutputSchema(array< string, mixed > $schema) self +usingOutputModalities(...AiModality $modalities) self + +asArrayResponse(?array< string, mixed > $schema) self + +asJsonResponse(?array< string, mixed > $schema) self +generateResult() GenerativeAiResult + +generateResults(int $candidateCount) GenerativeAiResult[] +generateOperation() GenerativeAiOperation + +generateOperations(int $candidateCount) GenerativeAiOperation[] +generateTextResult() GenerativeAiResult + +generateTextResults(int $candidateCount) GenerativeAiResult[] +streamGenerateTextResult() Generator< GenerativeAiResult > +generateImageResult() GenerativeAiResult + +generateImageResults(int $candidateCount) GenerativeAiResult[] +convertTextToSpeechResult() GenerativeAiResult + +convertTextToSpeechResults(int $candidateCount) GenerativeAiResult[] +generateSpeechResult() GenerativeAiResult + +generateSpeechResults(int $candidateCount) GenerativeAiResult[] +generateEmbeddingsResult() EmbeddingResult + +generateEmbeddingsResults(int $candidateCount) EmbeddingResult[] +generateTextOperation() GenerativeAiOperation + +generateTextOperations(int $candidateCount) GenerativeAiOperation[] +generateImageOperation() GenerativeAiOperation + +generateImageOperations(int $candidateCount) GenerativeAiOperation[] +convertTextToSpeechOperation() GenerativeAiOperation +generateSpeechOperation() GenerativeAiOperation + +generateSpeechOperations(int $candidateCount) GenerativeAiOperation[] +generateEmbeddingsOperation() EmbeddingOperation + +generateEmbeddingsOperations(int $candidateCount) EmbeddingOperation[] +generateText() string + +generateTexts(int $candidateCount) string[] +streamGenerateText() Generator< string > +generateImage() File + +generateImages(int $candidateCount) File[] +convertTextToSpeech() File + +convertTextToSpeeches(int $candidateCount) File[] +generateSpeech() File + +generateSpeeches(int $candidateCount) File[] +generateEmbeddings() Embedding[] +getModelRequirements() AiModelRequirements +isSupported() bool @@ -266,6 +387,7 @@ direction LR class MessageBuilder { +usingRole(MessageRole $role) self +withText(string $text) self + +withImage(string $mimeType, string $base64Blob) self +withImageFile(File $file) self +withAudioFile(File $file) self +withVideoFile(File $file) self @@ -356,8 +478,8 @@ classDiagram direction LR namespace Ai { class AiClient { - +prompt(?string $text) PromptBuilder$ - +message(?string $text) MessageBuilder$ + +prompt(...string $text) PromptBuilder$ + +message(...string $text) MessageBuilder$ +defaultRegistry() AiProviderRegistry$ +isConfigured(AiProviderAvailability $availability) bool$ +generateResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ @@ -377,6 +499,7 @@ direction LR class PromptBuilder { +withText(string $text) self + +withImage(string $mimeType, string $base64Blob) self +withImageFile(File $file) self +withAudioFile(File $file) self +withVideoFile(File $file) self @@ -384,6 +507,20 @@ direction LR +withMessageParts(...MessagePart $part) self +withHistory(...Message $messages) self +usingModel(AiModel $model) self + +usingModelSupporting(...AiCapability|AiOption $aiCapabilityOrOption) self + +usingModelSupportingCapability(...AiCapability $aiCapability) self + +usingModelSupportingOption(...AiOption $aiOption) self + +usingModelSupportingAudio() self + +usingModelSupportingHistory() self + +usingModelSupportingEmbeddings() self + +usingModelSupportingImages() self + +usingModelSupportingJsonOutput() self + +usingModelSupportingMusic() self + +usingModelSupportingOutputSchema() self + +usingModelSupportingSpeech() self + +usingModelSupportingText() self + +usingModelSupportingTextToSpeech() self + +usingModelSupportingVideo() self +usingSystemInstruction(string|MessagePart[]|Message $systemInstruction) self +usingMaxTokens(int $maxTokens) self +usingTemperature(float $temperature) self @@ -394,24 +531,41 @@ direction LR +usingOutputMime(string $mimeType) self +usingOutputSchema(array< string, mixed > $schema) self +usingOutputModalities(...AiModality $modalities) self + +asArrayResponse(?array< string, mixed > $schema) self + +asJsonResponse(?array< string, mixed > $schema) self +generateResult() GenerativeAiResult + +generateResults(int $candidateCount) GenerativeAiResult[] +generateOperation() GenerativeAiOperation + +generateOperations(int $candidateCount) GenerativeAiOperation[] +generateTextResult() GenerativeAiResult + +generateTextResults(int $candidateCount) GenerativeAiResult[] +streamGenerateTextResult() Generator< GenerativeAiResult > +generateImageResult() GenerativeAiResult + +generateImageResults(int $candidateCount) GenerativeAiResult[] +convertTextToSpeechResult() GenerativeAiResult + +convertTextToSpeechResults(int $candidateCount) GenerativeAiResult[] +generateSpeechResult() GenerativeAiResult + +generateSpeechResults(int $candidateCount) GenerativeAiResult[] +generateEmbeddingsResult() EmbeddingResult + +generateEmbeddingsResults(int $candidateCount) EmbeddingResult[] +generateTextOperation() GenerativeAiOperation + +generateTextOperations(int $candidateCount) GenerativeAiOperation[] +generateImageOperation() GenerativeAiOperation + +generateImageOperations(int $candidateCount) GenerativeAiOperation[] +convertTextToSpeechOperation() GenerativeAiOperation +generateSpeechOperation() GenerativeAiOperation + +generateSpeechOperations(int $candidateCount) GenerativeAiOperation[] +generateEmbeddingsOperation() EmbeddingOperation + +generateEmbeddingsOperations(int $candidateCount) EmbeddingOperation[] +generateText() string + +generateTexts(int $candidateCount) string[] +streamGenerateText() Generator< string > +generateImage() File + +generateImages(int $candidateCount) File[] +convertTextToSpeech() File + +convertTextToSpeeches(int $candidateCount) File[] +generateSpeech() File + +generateSpeeches(int $candidateCount) File[] +generateEmbeddings() Embedding[] +getModelRequirements() AiModelRequirements +isSupported() bool @@ -420,6 +574,7 @@ direction LR class MessageBuilder { +usingRole(MessageRole $role) self +withText(string $text) self + +withImage(string $mimeType, string $base64Blob) self +withImageFile(File $file) self +withAudioFile(File $file) self +withVideoFile(File $file) self From aa21ba8dae3a8cde89376094d52d69dc74a3b162 Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Sat, 12 Jul 2025 17:54:05 -0400 Subject: [PATCH 18/39] Explicitly declare which provider the model is coming from Co-authored-by: Felix Arntz --- docs/ARCHITECTURE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 5c76327c..8dc95590 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -37,7 +37,7 @@ $text = AiClient::generateTextResult( ##### Fluent API ```php $text = AiClient::prompt('Write a 2-verse poem about PHP.') - ->usingModel('gemini-2.5-flash') + ->usingModel(Google::model('gemini-2.5-flash')) ->generateText(); ``` @@ -54,7 +54,7 @@ $text = AiClient::generateTextResult( ##### Fluent API ```php $texts = AiClient::prompt('Write a 2-verse poem about PHP.') - ->usingModel('claude-3.7-sonnet') + ->usingModel(Anthropic::model('claude-3.7-sonnet')) ->generateTexts(4); ``` From b3a0d9cf18db1302b53baa57ce1ee4ab935647c7 Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Sat, 12 Jul 2025 18:31:56 -0400 Subject: [PATCH 19/39] Remove the superfluous usingModelSupporting*() methods --- docs/ARCHITECTURE.md | 41 ++++++++++------------------------------- 1 file changed, 10 insertions(+), 31 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8dc95590..cf03a37c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -75,7 +75,6 @@ $texts = AiClient::generateTextResult( ```php $imageFile = AiClient::prompt('Generate an illustration of the PHP elephant in the Carribean sea.') ->usingProvider('openai') - ->usingModelSupportingImages() // Optional. ->generateImage(); ``` @@ -99,7 +98,6 @@ $imageFile = AiClient::generateImageResult( ##### Fluent API ```php $imageFile = AiClient::prompt('Generate an illustration of the PHP elephant in the Carribean sea.') - ->usingModelSupportingImages() // Optional. ->generateImage(); ``` @@ -123,8 +121,17 @@ _Note: This does effectively the exact same as [the first code example](#generat ##### Fluent API ```php +$providerModelsMetadata = AiClient::defaultRegistry()->findModelsMetadataForSupport( + new AiModelRequirements([AiCapability::TEXT_GENERATION]) +); + $text = AiClient::prompt('Write a 2-verse poem about PHP.') - ->usingModelSupportingText() // Optional. + ->withModel( + AiClient::defaultRegistry()->getProviderModel( + $providerModelsMetadata[0]->getProvider()->getId(), + $providerModelsMetadata[0]->getModels()[0]->getId() + ) + ) ->generateText(); ``` @@ -320,20 +327,6 @@ direction LR +withMessageParts(...MessagePart $part) self +withHistory(...Message $messages) self +usingModel(AiModel $model) self - +usingModelSupporting(...AiCapability|AiOption $aiCapabilityOrOption) self - +usingModelSupportingCapability(...AiCapability $aiCapability) self - +usingModelSupportingOption(...AiOption $aiOption) self - +usingModelSupportingAudio() self - +usingModelSupportingHistory() self - +usingModelSupportingEmbeddings() self - +usingModelSupportingImages() self - +usingModelSupportingJsonOutput() self - +usingModelSupportingMusic() self - +usingModelSupportingOutputSchema() self - +usingModelSupportingSpeech() self - +usingModelSupportingText() self - +usingModelSupportingTextToSpeech() self - +usingModelSupportingVideo() self +usingSystemInstruction(string|MessagePart[]|Message $systemInstruction) self +usingMaxTokens(int $maxTokens) self +usingTemperature(float $temperature) self @@ -507,20 +500,6 @@ direction LR +withMessageParts(...MessagePart $part) self +withHistory(...Message $messages) self +usingModel(AiModel $model) self - +usingModelSupporting(...AiCapability|AiOption $aiCapabilityOrOption) self - +usingModelSupportingCapability(...AiCapability $aiCapability) self - +usingModelSupportingOption(...AiOption $aiOption) self - +usingModelSupportingAudio() self - +usingModelSupportingHistory() self - +usingModelSupportingEmbeddings() self - +usingModelSupportingImages() self - +usingModelSupportingJsonOutput() self - +usingModelSupportingMusic() self - +usingModelSupportingOutputSchema() self - +usingModelSupportingSpeech() self - +usingModelSupportingText() self - +usingModelSupportingTextToSpeech() self - +usingModelSupportingVideo() self +usingSystemInstruction(string|MessagePart[]|Message $systemInstruction) self +usingMaxTokens(int $maxTokens) self +usingTemperature(float $temperature) self From 1163c9f76fa60ad47823c60b29b4a9768dea45a7 Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Sat, 12 Jul 2025 18:34:47 -0400 Subject: [PATCH 20/39] Use the full schema declaration for JSON --- docs/ARCHITECTURE.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index cf03a37c..c8939079 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -215,15 +215,22 @@ _Note: Unlike the previous two examples, to require JSON output it is necessary ##### Fluent API ```php -// Verbose. $text = AiClient::prompt('Transform the following CSV content into a JSON array of row data.') ->asJsonResponse() - ->usingOutputSchema(['name' => 'string', 'age' => 'integer']) - ->generateText(); - -// Simple. -$text = AiClient::prompt('Transform the following CSV content into a JSON array of row data.') - ->asJsonResponse(['name' => 'string', 'age' => 'integer']) + ->usingOutputSchema([ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'name' => [ + 'type' => 'string', + ], + 'age' => [ + 'type' => 'integer', + ], + ], + ], + ]) ->generateText(); ``` From 523e81117e6805230ab2e0c65ef983404d1754df Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Sat, 12 Jul 2025 18:45:39 -0400 Subject: [PATCH 21/39] Remove asArrayResponse() --- docs/ARCHITECTURE.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c8939079..9f907403 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -344,7 +344,6 @@ direction LR +usingOutputMime(string $mimeType) self +usingOutputSchema(array< string, mixed > $schema) self +usingOutputModalities(...AiModality $modalities) self - +asArrayResponse(?array< string, mixed > $schema) self +asJsonResponse(?array< string, mixed > $schema) self +generateResult() GenerativeAiResult +generateResults(int $candidateCount) GenerativeAiResult[] @@ -517,7 +516,6 @@ direction LR +usingOutputMime(string $mimeType) self +usingOutputSchema(array< string, mixed > $schema) self +usingOutputModalities(...AiModality $modalities) self - +asArrayResponse(?array< string, mixed > $schema) self +asJsonResponse(?array< string, mixed > $schema) self +generateResult() GenerativeAiResult +generateResults(int $candidateCount) GenerativeAiResult[] From 871fc066d0b1ebc4711cdd9b11e3c50140c967a4 Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Sat, 12 Jul 2025 18:53:47 -0400 Subject: [PATCH 22/39] Remove invalid generate methods and ensure remaining non Result & Operation methods have a singular return type. --- docs/ARCHITECTURE.md | 38 ++++++++------------------------------ 1 file changed, 8 insertions(+), 30 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 9f907403..0dbb87e3 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -346,38 +346,27 @@ direction LR +usingOutputModalities(...AiModality $modalities) self +asJsonResponse(?array< string, mixed > $schema) self +generateResult() GenerativeAiResult - +generateResults(int $candidateCount) GenerativeAiResult[] +generateOperation() GenerativeAiOperation - +generateOperations(int $candidateCount) GenerativeAiOperation[] +generateTextResult() GenerativeAiResult - +generateTextResults(int $candidateCount) GenerativeAiResult[] +streamGenerateTextResult() Generator< GenerativeAiResult > +generateImageResult() GenerativeAiResult - +generateImageResults(int $candidateCount) GenerativeAiResult[] +convertTextToSpeechResult() GenerativeAiResult - +convertTextToSpeechResults(int $candidateCount) GenerativeAiResult[] +generateSpeechResult() GenerativeAiResult - +generateSpeechResults(int $candidateCount) GenerativeAiResult[] +generateEmbeddingsResult() EmbeddingResult - +generateEmbeddingsResults(int $candidateCount) EmbeddingResult[] +generateTextOperation() GenerativeAiOperation - +generateTextOperations(int $candidateCount) GenerativeAiOperation[] +generateImageOperation() GenerativeAiOperation - +generateImageOperations(int $candidateCount) GenerativeAiOperation[] +convertTextToSpeechOperation() GenerativeAiOperation +generateSpeechOperation() GenerativeAiOperation - +generateSpeechOperations(int $candidateCount) GenerativeAiOperation[] +generateEmbeddingsOperation() EmbeddingOperation - +generateEmbeddingsOperations(int $candidateCount) EmbeddingOperation[] +generateText() string - +generateTexts(int $candidateCount) string[] + +generateTexts(int $candidateCount) string +streamGenerateText() Generator< string > +generateImage() File - +generateImages(int $candidateCount) File[] + +generateImages(int $candidateCount) File +convertTextToSpeech() File - +convertTextToSpeeches(int $candidateCount) File[] + +convertTextToSpeeches(int $candidateCount) File +generateSpeech() File - +generateSpeeches(int $candidateCount) File[] + +generateSpeeches(int $candidateCount) File +generateEmbeddings() Embedding[] +getModelRequirements() AiModelRequirements +isSupported() bool @@ -518,38 +507,27 @@ direction LR +usingOutputModalities(...AiModality $modalities) self +asJsonResponse(?array< string, mixed > $schema) self +generateResult() GenerativeAiResult - +generateResults(int $candidateCount) GenerativeAiResult[] +generateOperation() GenerativeAiOperation - +generateOperations(int $candidateCount) GenerativeAiOperation[] +generateTextResult() GenerativeAiResult - +generateTextResults(int $candidateCount) GenerativeAiResult[] +streamGenerateTextResult() Generator< GenerativeAiResult > +generateImageResult() GenerativeAiResult - +generateImageResults(int $candidateCount) GenerativeAiResult[] +convertTextToSpeechResult() GenerativeAiResult - +convertTextToSpeechResults(int $candidateCount) GenerativeAiResult[] +generateSpeechResult() GenerativeAiResult - +generateSpeechResults(int $candidateCount) GenerativeAiResult[] +generateEmbeddingsResult() EmbeddingResult - +generateEmbeddingsResults(int $candidateCount) EmbeddingResult[] +generateTextOperation() GenerativeAiOperation - +generateTextOperations(int $candidateCount) GenerativeAiOperation[] +generateImageOperation() GenerativeAiOperation - +generateImageOperations(int $candidateCount) GenerativeAiOperation[] +convertTextToSpeechOperation() GenerativeAiOperation +generateSpeechOperation() GenerativeAiOperation - +generateSpeechOperations(int $candidateCount) GenerativeAiOperation[] +generateEmbeddingsOperation() EmbeddingOperation - +generateEmbeddingsOperations(int $candidateCount) EmbeddingOperation[] +generateText() string - +generateTexts(int $candidateCount) string[] + +generateTexts(int $candidateCount) string +streamGenerateText() Generator< string > +generateImage() File - +generateImages(int $candidateCount) File[] + +generateImages(int $candidateCount) File +convertTextToSpeech() File - +convertTextToSpeeches(int $candidateCount) File[] + +convertTextToSpeeches(int $candidateCount) File +generateSpeech() File - +generateSpeeches(int $candidateCount) File[] + +generateSpeeches(int $candidateCount) File +generateEmbeddings() Embedding[] +getModelRequirements() AiModelRequirements +isSupported() bool From 1d0c10f53c63301ecf300a2f3f488109dc1ea681 Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Sat, 12 Jul 2025 18:57:56 -0400 Subject: [PATCH 23/39] Use `withImageFile()` along with `new InlineFile()` for specifying inline images --- docs/ARCHITECTURE.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 0dbb87e3..9c2058ce 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -156,7 +156,7 @@ _Note: Since this omits the model parameter, the SDK will automatically determin ##### Fluent API ```php $text = AiClient::prompt('Generate alternative text for this image.') - ->withImage('image/png', $base64blob) + ->withImageFile(new InlineFile('image/png', $base64blob)) ->generateText(); ``` @@ -326,7 +326,6 @@ direction LR class PromptBuilder { +withText(string $text) self - +withImage(string $mimeType, string $base64Blob) self +withImageFile(File $file) self +withAudioFile(File $file) self +withVideoFile(File $file) self @@ -375,7 +374,6 @@ direction LR class MessageBuilder { +usingRole(MessageRole $role) self +withText(string $text) self - +withImage(string $mimeType, string $base64Blob) self +withImageFile(File $file) self +withAudioFile(File $file) self +withVideoFile(File $file) self @@ -487,7 +485,6 @@ direction LR class PromptBuilder { +withText(string $text) self - +withImage(string $mimeType, string $base64Blob) self +withImageFile(File $file) self +withAudioFile(File $file) self +withVideoFile(File $file) self @@ -536,7 +533,6 @@ direction LR class MessageBuilder { +usingRole(MessageRole $role) self +withText(string $text) self - +withImage(string $mimeType, string $base64Blob) self +withImageFile(File $file) self +withAudioFile(File $file) self +withVideoFile(File $file) self From b86d6d83329b452fd2caaf5e33f577995e5f3c4d Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Sat, 12 Jul 2025 19:00:21 -0400 Subject: [PATCH 24/39] Make the chat history example read better top-to-bottom --- docs/ARCHITECTURE.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 9c2058ce..bd971933 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -181,11 +181,13 @@ _Note: Similarly to the previous example, even without specifying the model here ##### Fluent API ```php +$history = [ + new UserMessage('Do you spell it WordPress or Wordpress?'), + new AgentMessage('The correct spelling is WordPress.'), +]; + $text = AiClient::prompt('Can you repeat that please?') - ->withHistory( - new UserMessage('Do you spell it WordPress or Wordpress?'), - new AgentMessage('The correct spelling is WordPress.') - ) + ->withHistory(...$history) ->generateText(); ``` From f5873f70d89983a98df446b6fba4b4b18e2e99b7 Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Sat, 12 Jul 2025 19:42:42 -0400 Subject: [PATCH 25/39] Fix order of variadic dots --- docs/ARCHITECTURE.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index bd971933..5b3424a8 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -322,8 +322,8 @@ classDiagram direction LR namespace Ai { class AiClient { - +prompt(...string $text) PromptBuilder$ - +message(...string $text) MessageBuilder$ + +prompt(string ...$text) PromptBuilder$ + +message(string ...$text) MessageBuilder$ } class PromptBuilder { @@ -466,8 +466,8 @@ classDiagram direction LR namespace Ai { class AiClient { - +prompt(...string $text) PromptBuilder$ - +message(...string $text) MessageBuilder$ + +prompt(string ...$text) PromptBuilder$ + +message(string ...$text) MessageBuilder$ +defaultRegistry() AiProviderRegistry$ +isConfigured(AiProviderAvailability $availability) bool$ +generateResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ From 6df92b73f7f060aaba8e1da047f7c2a84d7bf848 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Sun, 13 Jul 2025 15:32:43 -0700 Subject: [PATCH 26/39] Rename namespace for consistency. --- docs/ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8c12e936..71179a15 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -816,7 +816,7 @@ direction LR +getJsonSchema() array< string, mixed >$ } } - namespace Ai.Providers.Enums { + namespace Ai.Providers.Types.Enums { class AiProviderType { CLOUD SERVER From b72d636fcaee67d1bbefef42f4a1c6f93624b640 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Sun, 13 Jul 2025 15:33:27 -0700 Subject: [PATCH 27/39] Fix TEXT_TO_SPEECH enum value for consistency. --- docs/ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 71179a15..08b43f8e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -829,7 +829,7 @@ direction LR class AiCapability { TEXT_GENERATION IMAGE_GENERATION - TEXT_TO_SPEECH + TEXT_TO_SPEECH_CONVERSION SPEECH_GENERATION MUSIC_GENERATION VIDEO_GENERATION From 2d981a680b7020d0931cba21dd1a60d8294738f9 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Sun, 13 Jul 2025 16:07:47 -0700 Subject: [PATCH 28/39] Align root namespace with project decision. --- docs/ARCHITECTURE.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 08b43f8e..8d753216 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -215,7 +215,7 @@ config: --- classDiagram direction LR - namespace Ai { + namespace AiClient { class AiClient { +prompt(?string $text) PromptBuilder$ +message(?string $text) MessageBuilder$ @@ -292,7 +292,7 @@ config: --- classDiagram direction LR - namespace Ai { + namespace AiClient { class AiClient { +generateResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ +generateOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ @@ -323,13 +323,13 @@ config: --- classDiagram direction LR - namespace Ai { + namespace AiClient { class AiClient { +defaultRegistry() AiProviderRegistry$ +isConfigured(AiProviderAvailability $availability) bool$ } } - namespace Ai.Providers { + namespace AiClient.Providers { class AiProviderRegistry { +registerProvider(string $className) void +hasProvider(string $idOrClassName) bool @@ -354,7 +354,7 @@ config: --- classDiagram direction LR - namespace Ai { + namespace AiClient { class AiClient { +prompt(?string $text) PromptBuilder$ +message(?string $text) MessageBuilder$ @@ -429,7 +429,7 @@ direction LR +get() Message } } - namespace Ai.Types { + namespace AiClient.Types { class Message { +getRole() MessageRole +getParts() MessagePart[] @@ -538,7 +538,7 @@ direction LR +getJsonSchema() array< string, mixed >$ } } - namespace Ai.Types.Enums { + namespace AiClient.Types.Enums { class MessageRole { USER MODEL @@ -573,7 +573,7 @@ direction LR VIDEO } } - namespace Ai.Util { + namespace AiClient.Util { class MessageUtil { +toText(Message $message) string$ +toImageFile(Message $message) File$ @@ -653,7 +653,7 @@ config: --- classDiagram direction LR - namespace Ai.Providers { + namespace AiClient.Providers { class AiProviderRegistry { +registerProvider(string $className) void +hasProvider(string $idOrClassName) bool @@ -664,7 +664,7 @@ direction LR +findModelsMetadataForSupport(AiModelRequirements $modelRequirements) AiProviderModelMetadata[] } } - namespace Ai.Providers.Contracts { + namespace AiClient.Providers.Contracts { class AiProvider { +metadata() AiProviderMetadata$ +model(string $modelId, AiModelConfig|array< string, mixed > $modelConfig) AiModel$ @@ -738,7 +738,7 @@ direction LR +getJsonSchema() array< string, mixed >$ } } - namespace Ai.Providers.Types { + namespace AiClient.Providers.Types { class AiProviderMetadata { +getId() string +getName() string @@ -816,7 +816,7 @@ direction LR +getJsonSchema() array< string, mixed >$ } } - namespace Ai.Providers.Types.Enums { + namespace AiClient.Providers.Types.Enums { class AiProviderType { CLOUD SERVER @@ -849,7 +849,7 @@ direction LR OUTPUT_SCHEMA } } - namespace Ai.Providers.Util { + namespace AiClient.Providers.Util { class AiCapabilitiesUtil { +getSupportedCapabilities(AiModel|string $modelClass) AiCapability[]$ +getSupportedOptions(AiModel|string $modelClass) AiSupportedOption[]$ From 985cd4232849d48adac2e8399709cc625f868581 Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Mon, 14 Jul 2025 08:49:08 -0400 Subject: [PATCH 29/39] Adding image convenience methods --- docs/ARCHITECTURE.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 5b3424a8..1b7044d5 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -156,7 +156,7 @@ _Note: Since this omits the model parameter, the SDK will automatically determin ##### Fluent API ```php $text = AiClient::prompt('Generate alternative text for this image.') - ->withImageFile(new InlineFile('image/png', $base64blob)) + ->withInlineImage($base64Blob, 'image/png') ->generateText(); ``` @@ -328,6 +328,9 @@ direction LR class PromptBuilder { +withText(string $text) self + +withInlineImage(string $base64Blob, string $mimeType) + +withLocalImage(string $path, string $mimeType) + +withRemoteImage(string $uri, string $mimeType) +withImageFile(File $file) self +withAudioFile(File $file) self +withVideoFile(File $file) self @@ -487,6 +490,9 @@ direction LR class PromptBuilder { +withText(string $text) self + +withInlineImage(string $base64Blob, string $mimeType) + +withLocalImage(string $path, string $mimeType) + +withRemoteImage(string $uri, string $mimeType) +withImageFile(File $file) self +withAudioFile(File $file) self +withVideoFile(File $file) self From fb446e4884f4065075b17b67a966cafbd61b30ee Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Mon, 14 Jul 2025 08:51:35 -0400 Subject: [PATCH 30/39] Use a different prompt structure for chat history to show flexibility --- docs/ARCHITECTURE.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 1b7044d5..54ea9bc8 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -181,13 +181,12 @@ _Note: Similarly to the previous example, even without specifying the model here ##### Fluent API ```php -$history = [ - new UserMessage('Do you spell it WordPress or Wordpress?'), - new AgentMessage('The correct spelling is WordPress.'), -]; - -$text = AiClient::prompt('Can you repeat that please?') - ->withHistory(...$history) +$text = AiClient::prompt() + ->withHistory( + new UserMessage('Do you spell it WordPress or Wordpress?'), + new AgentMessage('The correct spelling is WordPress.'), + ) + ->withText('Can you repeat that please?') ->generateText(); ``` From 45712370832045001218760a16d19ea5258f086a Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Mon, 14 Jul 2025 08:53:45 -0400 Subject: [PATCH 31/39] Return multiples when there are multiple candidates --- docs/ARCHITECTURE.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 54ea9bc8..35a2b4ca 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -362,14 +362,14 @@ direction LR +generateSpeechOperation() GenerativeAiOperation +generateEmbeddingsOperation() EmbeddingOperation +generateText() string - +generateTexts(int $candidateCount) string + +generateTexts(int $candidateCount) string[] +streamGenerateText() Generator< string > +generateImage() File - +generateImages(int $candidateCount) File + +generateImages(int $candidateCount) File[] +convertTextToSpeech() File - +convertTextToSpeeches(int $candidateCount) File + +convertTextToSpeeches(int $candidateCount) File[] +generateSpeech() File - +generateSpeeches(int $candidateCount) File + +generateSpeeches(int $candidateCount) File[] +generateEmbeddings() Embedding[] +getModelRequirements() AiModelRequirements +isSupported() bool @@ -524,14 +524,14 @@ direction LR +generateSpeechOperation() GenerativeAiOperation +generateEmbeddingsOperation() EmbeddingOperation +generateText() string - +generateTexts(int $candidateCount) string + +generateTexts(int $candidateCount) string[] +streamGenerateText() Generator< string > +generateImage() File - +generateImages(int $candidateCount) File + +generateImages(int $candidateCount) File[] +convertTextToSpeech() File - +convertTextToSpeeches(int $candidateCount) File + +convertTextToSpeeches(int $candidateCount) File[] +generateSpeech() File - +generateSpeeches(int $candidateCount) File + +generateSpeeches(int $candidateCount) File[] +generateEmbeddings() Embedding[] +getModelRequirements() AiModelRequirements +isSupported() bool From 7e0cbfd7345dfbc35eaef25bcf4ab17164c79df6 Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Mon, 14 Jul 2025 08:56:40 -0400 Subject: [PATCH 32/39] Set prompt() arg to string|Message. Remove variadic from message() --- docs/ARCHITECTURE.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 35a2b4ca..c5c73928 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -321,8 +321,8 @@ classDiagram direction LR namespace Ai { class AiClient { - +prompt(string ...$text) PromptBuilder$ - +message(string ...$text) MessageBuilder$ + +prompt(string|Message ...$text) PromptBuilder$ + +message(string $text) MessageBuilder$ } class PromptBuilder { @@ -468,8 +468,8 @@ classDiagram direction LR namespace Ai { class AiClient { - +prompt(string ...$text) PromptBuilder$ - +message(string ...$text) MessageBuilder$ + +prompt(string|Message ...$text) PromptBuilder$ + +message(string $text) MessageBuilder$ +defaultRegistry() AiProviderRegistry$ +isConfigured(AiProviderAvailability $availability) bool$ +generateResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ From 8a1a2b877d585409d99ec6772073eba7ab017316 Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Mon, 14 Jul 2025 12:57:46 -0400 Subject: [PATCH 33/39] Keep message() arg optional --- docs/ARCHITECTURE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c5c73928..f1dbfd30 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -322,7 +322,7 @@ direction LR namespace Ai { class AiClient { +prompt(string|Message ...$text) PromptBuilder$ - +message(string $text) MessageBuilder$ + +message(?string $text) MessageBuilder$ } class PromptBuilder { @@ -469,7 +469,7 @@ direction LR namespace Ai { class AiClient { +prompt(string|Message ...$text) PromptBuilder$ - +message(string $text) MessageBuilder$ + +message(?string $text) MessageBuilder$ +defaultRegistry() AiProviderRegistry$ +isConfigured(AiProviderAvailability $availability) bool$ +generateResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ From b234aa1ce81d018a1bf089a33694def01e4bf554 Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Mon, 14 Jul 2025 13:00:33 -0400 Subject: [PATCH 34/39] Make $candidateCount optional for plural generate termination methods --- docs/ARCHITECTURE.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f1dbfd30..ecc642fd 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -362,14 +362,14 @@ direction LR +generateSpeechOperation() GenerativeAiOperation +generateEmbeddingsOperation() EmbeddingOperation +generateText() string - +generateTexts(int $candidateCount) string[] + +generateTexts(?int $candidateCount) string[] +streamGenerateText() Generator< string > +generateImage() File - +generateImages(int $candidateCount) File[] + +generateImages(?int $candidateCount) File[] +convertTextToSpeech() File - +convertTextToSpeeches(int $candidateCount) File[] + +convertTextToSpeeches(?int $candidateCount) File[] +generateSpeech() File - +generateSpeeches(int $candidateCount) File[] + +generateSpeeches(?int $candidateCount) File[] +generateEmbeddings() Embedding[] +getModelRequirements() AiModelRequirements +isSupported() bool @@ -524,14 +524,14 @@ direction LR +generateSpeechOperation() GenerativeAiOperation +generateEmbeddingsOperation() EmbeddingOperation +generateText() string - +generateTexts(int $candidateCount) string[] + +generateTexts(?int $candidateCount) string[] +streamGenerateText() Generator< string > +generateImage() File - +generateImages(int $candidateCount) File[] + +generateImages(?int $candidateCount) File[] +convertTextToSpeech() File - +convertTextToSpeeches(int $candidateCount) File[] + +convertTextToSpeeches(?int $candidateCount) File[] +generateSpeech() File - +generateSpeeches(int $candidateCount) File[] + +generateSpeeches(?int $candidateCount) File[] +generateEmbeddings() Embedding[] +getModelRequirements() AiModelRequirements +isSupported() bool From c046ece9d6db9ef4206b1d93f3071c7e8720ce24 Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Mon, 14 Jul 2025 13:30:34 -0400 Subject: [PATCH 35/39] Remove embedding examples - too soon to know what we want --- docs/ARCHITECTURE.md | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ecc642fd..3b134a89 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -273,32 +273,6 @@ $jsonString = AiClient::generateTextResult( )->toText(); ``` -#### Generate embeddings using any suitable model from any provider - -##### Fluent API -```php -$embeddings = AiClient::prompt('A very long text.', 'Another very long text.', 'More long text.') - ->generateEmbeddings(); -``` - -##### Traditional API -```php -$providerModelsMetadata = AiClient::defaultRegistry()->findModelsMetadataForSupport( - new AiModelRequirements([AiCapability::EMBEDDING_GENERATION]) -); -$embeddings = AiClient::generateEmbeddingsResult( - [ - 'A very long text.', - 'Another very long text.', - 'More long text.', - ], - AiClient::defaultRegistry()->getProviderModel( - $providerModelsMetadata[0]->getProvider()->getId(), - $providerModelsMetadata[0]->getModels()[0]->getId() - ) -)->getEmbeddings(); -``` - ## Class diagrams This section shows comprehensive class diagrams for the proposed architecture. For explanation on specific terms, see the [glossary](./GLOSSARY.md). From 1187c107f827ad9419d9ba6bb2434eb3fe024d6e Mon Sep 17 00:00:00 2001 From: Matthew Batchelder Date: Mon, 14 Jul 2025 13:30:53 -0400 Subject: [PATCH 36/39] Make prompt() non-variadic --- docs/ARCHITECTURE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3b134a89..776a4e7d 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -295,7 +295,7 @@ classDiagram direction LR namespace Ai { class AiClient { - +prompt(string|Message ...$text) PromptBuilder$ + +prompt(string|Message|null $text = null) PromptBuilder$ +message(?string $text) MessageBuilder$ } @@ -442,7 +442,7 @@ classDiagram direction LR namespace Ai { class AiClient { - +prompt(string|Message ...$text) PromptBuilder$ + +prompt(string|Message|null $text = null) PromptBuilder$ +message(?string $text) MessageBuilder$ +defaultRegistry() AiProviderRegistry$ +isConfigured(AiProviderAvailability $availability) bool$ From 639d6271a18919e46b14d3e26a1673a079c81a4c Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Mon, 14 Jul 2025 10:47:07 -0700 Subject: [PATCH 37/39] Fix AiClient namespace just to cater for Mermaid problem. --- docs/ARCHITECTURE.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index db74ee6c..16d3f436 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -293,7 +293,7 @@ config: --- classDiagram direction LR - namespace AiClient { + namespace AiClientNamespace { class AiClient { +prompt(string|Message|null $text = null) PromptBuilder$ +message(?string $text) MessageBuilder$ @@ -378,7 +378,7 @@ config: --- classDiagram direction LR - namespace AiClient { + namespace AiClientNamespace { class AiClient { +generateResult(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiResult$ +generateOperation(string|MessagePart|MessagePart[]|Message|Message[] $prompt, AiModel $model) GenerativeAiOperation$ @@ -409,13 +409,13 @@ config: --- classDiagram direction LR - namespace AiClient { + namespace AiClientNamespace { class AiClient { +defaultRegistry() AiProviderRegistry$ +isConfigured(AiProviderAvailability $availability) bool$ } } - namespace AiClient.Providers { + namespace AiClientNamespace.Providers { class AiProviderRegistry { +registerProvider(string $className) void +hasProvider(string $idOrClassName) bool @@ -440,7 +440,7 @@ config: --- classDiagram direction LR - namespace AiClient { + namespace AiClientNamespace { class AiClient { +prompt(string|Message|null $text = null) PromptBuilder$ +message(?string $text) MessageBuilder$ @@ -523,7 +523,7 @@ direction LR +get() Message } } - namespace AiClient.Types { + namespace AiClientNamespace.Types { class Message { +getRole() MessageRole +getParts() MessagePart[] @@ -632,7 +632,7 @@ direction LR +getJsonSchema() array< string, mixed >$ } } - namespace AiClient.Types.Enums { + namespace AiClientNamespace.Types.Enums { class MessageRole { USER MODEL @@ -667,7 +667,7 @@ direction LR VIDEO } } - namespace AiClient.Util { + namespace AiClientNamespace.Util { class MessageUtil { +toText(Message $message) string$ +toImageFile(Message $message) File$ @@ -747,7 +747,7 @@ config: --- classDiagram direction LR - namespace AiClient.Providers { + namespace AiClientNamespace.Providers { class AiProviderRegistry { +registerProvider(string $className) void +hasProvider(string $idOrClassName) bool @@ -758,7 +758,7 @@ direction LR +findModelsMetadataForSupport(AiModelRequirements $modelRequirements) AiProviderModelMetadata[] } } - namespace AiClient.Providers.Contracts { + namespace AiClientNamespace.Providers.Contracts { class AiProvider { +metadata() AiProviderMetadata$ +model(string $modelId, AiModelConfig|array< string, mixed > $modelConfig) AiModel$ @@ -832,7 +832,7 @@ direction LR +getJsonSchema() array< string, mixed >$ } } - namespace AiClient.Providers.Types { + namespace AiClientNamespace.Providers.Types { class AiProviderMetadata { +getId() string +getName() string @@ -910,7 +910,7 @@ direction LR +getJsonSchema() array< string, mixed >$ } } - namespace AiClient.Providers.Types.Enums { + namespace AiClientNamespace.Providers.Types.Enums { class AiProviderType { CLOUD SERVER @@ -943,7 +943,7 @@ direction LR OUTPUT_SCHEMA } } - namespace AiClient.Providers.Util { + namespace AiClientNamespace.Providers.Util { class AiCapabilitiesUtil { +getSupportedCapabilities(AiModel|string $modelClass) AiCapability[]$ +getSupportedOptions(AiModel|string $modelClass) AiSupportedOption[]$ From f7e8f69965383da74d185bea331367d46373644e Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Mon, 14 Jul 2025 10:50:43 -0700 Subject: [PATCH 38/39] Added child message classes in class diagrams that were missing before. --- docs/ARCHITECTURE.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 16d3f436..d76beaa7 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -529,6 +529,12 @@ direction LR +getParts() MessagePart[] +getJsonSchema() array< string, mixed >$ } + class UserMessage { + } + class ModelMessage { + } + class SystemMessage { + } class MessagePart { +getType() MessagePartType +getText() string? @@ -731,6 +737,9 @@ direction LR File <|-- InlineFile File <|-- RemoteFile File <|-- LocalFile + Message <|-- UserMessage + Message <|-- ModelMessage + Message <|-- SystemMessage Operation <|-- GenerativeAiOperation Operation <|-- EmbeddingOperation Result <|-- GenerativeAiResult From 3ff376eceff38ba5c0740cea74a13b8e9a7067e5 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Mon, 14 Jul 2025 10:52:05 -0700 Subject: [PATCH 39/39] Fix AgentMessage name. --- docs/ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d76beaa7..aae59d1c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -184,7 +184,7 @@ _Note: Similarly to the previous example, even without specifying the model here $text = AiClient::prompt() ->withHistory( new UserMessage('Do you spell it WordPress or Wordpress?'), - new AgentMessage('The correct spelling is WordPress.'), + new ModelMessage('The correct spelling is WordPress.'), ) ->withText('Can you repeat that please?') ->generateText();