Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
9d1c48c
Remove tokenCount from Candidate DTO.
felixarntz Aug 5, 2025
3962f5b
Add initial base classes for API based and OpenAI API compatible prov…
felixarntz Aug 5, 2025
9bf93ad
Merge branch 'trunk' into provider-base-and-implementation
felixarntz Aug 6, 2025
fa8774d
Merge branch 'remove-candidate-token-count' into provider-base-and-im…
felixarntz Aug 6, 2025
170fce4
Merge branch 'trunk' into provider-base-and-implementation
felixarntz Aug 9, 2025
7201212
Integrate base class infra with the provider and model interfaces and…
felixarntz Aug 9, 2025
c2a7c3e
Move abstract model classes to Models namespace.
felixarntz Aug 9, 2025
d9b32fc
Implement OpenAI specific logic for parsing model metadata from the API.
felixarntz Aug 9, 2025
e7cb666
Merge branch 'trunk' into provider-base-and-implementation
felixarntz Aug 14, 2025
cd53963
Fix unnecessary use statements.
felixarntz Aug 14, 2025
30e63ba
Use new ModelConfig constants for supported options.
felixarntz Aug 14, 2025
5cc0e03
Implement OpenAI compatible response message parsing.
felixarntz Aug 14, 2025
48e8365
Merge branch 'trunk' into provider-base-and-implementation
felixarntz Aug 15, 2025
f9f147d
Merge branch 'trunk' into provider-base-and-implementation
felixarntz Aug 15, 2025
50c3c0e
Integrate with actual Http classes now that they're implemented.
felixarntz Aug 15, 2025
c73e812
Fix missing parameter.
felixarntz Aug 15, 2025
90f187c
Fix remaining PHPStan problems in abstract text generation model class.
felixarntz Aug 15, 2025
8bd9ee9
Properly implement OpenAI API compatible request creation.
felixarntz Aug 15, 2025
83f3268
Fix PHPStan problems in OpenAiModelMetadataDirectory.
felixarntz Aug 16, 2025
3c53649
Allow getting extension for (common) MIME types and use it where need…
felixarntz Aug 16, 2025
faec411
Implement support for handling remaining OpenAI compatible text gener…
felixarntz Aug 16, 2025
eb1192e
Implement remaining constants and logic for model support discovery.
felixarntz Aug 16, 2025
01a470b
Merge branch 'trunk' into provider-base-and-implementation
felixarntz Aug 16, 2025
78bf887
Implement provider registry logic to hook up providers with HTTP tran…
felixarntz Aug 16, 2025
7ca7bc8
Properly implement request authentication infrastructure and include …
felixarntz Aug 17, 2025
b4e2bcc
Fix remaining PHPStan errors related to request authentication infra.
felixarntz Aug 18, 2025
03cf0f7
Actually use request authentication instance to authenticate requests.
felixarntz Aug 18, 2025
ff8ce6b
Fix logic bug when setting up default request authentication.
felixarntz Aug 18, 2025
34f279a
Ensure models returned from provider registry are properly hooked up …
felixarntz Aug 18, 2025
f75289a
Implement simple ResponseUtil class for an easy way to handle unsucce…
felixarntz Aug 18, 2025
dc0b26d
Fix OpenAI compatible POST request by setting correct Content-Type he…
felixarntz Aug 18, 2025
b04c637
Implement MessageUtil class to make it easy to parse messages from va…
felixarntz Aug 18, 2025
62fd97c
Remove non-functional demo code in favor of TODO comment to implement…
felixarntz Aug 18, 2025
ffdccd3
Implement provider classes for Google.
felixarntz Aug 18, 2025
6fa22e8
Implement provider classes for Anthropic.
felixarntz Aug 18, 2025
97ef2cd
Implement very basic CLI tool to test the SDK (experimental, not part…
felixarntz Aug 18, 2025
5992015
Ensure ModelConfig::KEY_CUSTOM_OPTIONS is marked as supported by the …
felixarntz Aug 20, 2025
2bf6951
Enhance CLI tool to allow passing any model config parameters.
felixarntz Aug 20, 2025
9d307f6
Handle missing model for required options scenario gracefully in CLI …
felixarntz Aug 20, 2025
6c53d89
Add test coverage for class changes.
felixarntz Aug 20, 2025
56c9178
Add more test coverage and fix fully specified imports in test code.
felixarntz Aug 20, 2025
073a767
Remove NullRequestAuthentication in favor of only considering models …
felixarntz Aug 20, 2025
7757d33
Add tests for AbstractProvider.
felixarntz Aug 20, 2025
8788280
Fix PHPCS violations.
felixarntz Aug 21, 2025
0e0ac80
Merge branch 'trunk' into provider-base-and-implementation
felixarntz Aug 23, 2025
e6848b6
Update provider code after removal of system message.
felixarntz Aug 23, 2025
45a8d50
Fix data formatting bug for OpenAI compatible APIs.
felixarntz Aug 25, 2025
20858ba
Merge branch 'trunk' into provider-base-and-implementation
felixarntz Aug 26, 2025
ecc3a53
Add missing value to OptionEnum.
felixarntz Aug 26, 2025
2169172
Fix failing test for SupportedOption.
felixarntz Aug 26, 2025
9b4f09b
Allow passing ModelConfig to PromptBuilder via constructor.
felixarntz Aug 26, 2025
f5fbe89
Update PromptBuilder tests to include ProviderMetadata in mock model …
felixarntz Aug 26, 2025
4c58f96
Fix provider model metadata to use OptionEnum values instead of Model…
felixarntz Aug 26, 2025
6322f96
Fix enum base implementation to allow JSON encoding.
felixarntz Aug 26, 2025
2a0e5f0
Fix provider model metadata directory implementations to always expre…
felixarntz Aug 26, 2025
f192b63
Update the CLI test tool to use PromptBuilder.
felixarntz Aug 26, 2025
853bb55
Merge branch 'trunk' into provider-base-and-implementation
felixarntz Aug 26, 2025
657d754
Fix code after GenerativeAiResult update.
felixarntz Aug 26, 2025
e4bc2d7
Reinstate CLI tool code to show provider and model used for the query.
felixarntz Aug 26, 2025
4499d45
Moved abstract classes and other provider/model implementations to th…
felixarntz Aug 26, 2025
bbffb4a
Update outdated references.
felixarntz Aug 26, 2025
0f03598
Remove now unused MessageUtil.
felixarntz Aug 27, 2025
39edb57
Simplify hasModelMetadata method.
felixarntz Aug 27, 2025
c6575f5
Update inheritdoc blocks to still include since annotations.
felixarntz Aug 27, 2025
7fb9267
Update return type doc.
felixarntz Aug 27, 2025
1c53f39
Centrally defined ModelsResponseData PHPStan types.
felixarntz Aug 27, 2025
a8dfd8a
feat: adds bindModelDependencies public method
JasonTheAdams Aug 27, 2025
aa89253
feat: merges the configs in usingModel
JasonTheAdams Aug 27, 2025
5044796
Add since annotations for methods with inheritDoc.
felixarntz Aug 27, 2025
e87d4f3
refactor: add array data types
JasonTheAdams Aug 27, 2025
50850b6
Clarify in comment why we ignore exception.
felixarntz Aug 27, 2025
ccd62f1
Merge branch 'provider-base-and-implementation' of github.com:WordPre…
felixarntz Aug 27, 2025
36df670
Avoid unnecessary middleman functions.
felixarntz Aug 27, 2025
005fed1
Fix fatal error and clarify throwIfNotSuccessful method presence.
felixarntz Aug 27, 2025
f00d34a
Merge branch 'trunk' into provider-base-and-implementation
felixarntz Aug 27, 2025
a23d42d
Remove ModelConfig from PromptBuilder constructor again in favor of u…
felixarntz Aug 27, 2025
11ccd68
refactor: uses passthrough isRemote method
JasonTheAdams Aug 28, 2025
3051148
Merge branch 'trunk' into provider-base-and-implementation
felixarntz Aug 28, 2025
702f4fb
Properly wire up default registry in AiClient.
felixarntz Aug 28, 2025
e92b8ee
Use AiClient in CLI test tool.
felixarntz Aug 28, 2025
0845206
Fix errors in unit tests.
felixarntz Aug 28, 2025
f802aeb
Reuse MockModelCreationTrait instead of duplicating methods.
felixarntz Aug 28, 2025
61ac6d3
Fix incorrect provider reference in comment.
felixarntz Aug 28, 2025
31f3e05
Make sure manually provided model is bound.
felixarntz Aug 28, 2025
d6c3ab2
Include additional supported audio formats for OpenAI text-to-speech …
felixarntz Aug 29, 2025
ef3b0ef
Add TODO about OpenAI system vs developer message role.
felixarntz Aug 29, 2025
296040a
Clarify purpose of specific ProviderAvailability implementations.
felixarntz Aug 29, 2025
3186fbc
Move sort call out of loop.
felixarntz Aug 29, 2025
eea2c67
test: fixes failing tests due to missing provider
JasonTheAdams Aug 29, 2025
3815355
test: removes trailing commas breaking 7.4
JasonTheAdams Aug 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace WordPress\AiClient\ProviderImplementations\OpenAi;

use WordPress\AiClient\Providers\AbstractOpenAiCompatibleModelMetadataDirectory;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;

/**
* Class for the OpenAI model metadata directory.
*
* @since n.e.x.t
*/
class OpenAiModelMetadataDirectory extends AbstractOpenAiCompatibleModelMetadataDirectory
{
/**
* @inheritDoc
*/
protected function createRequest(string $path): RequestInterface

Check failure on line 20 in src/ProviderImplementations/OpenAi/OpenAiModelMetadataDirectory.php

View workflow job for this annotation

GitHub Actions / PHP

Return type WordPress\AiClient\ProviderImplementations\OpenAi\RequestInterface of method WordPress\AiClient\ProviderImplementations\OpenAi\OpenAiModelMetadataDirectory::createRequest() is not covariant with return type WordPress\AiClient\Providers\RequestInterface of method WordPress\AiClient\Providers\AbstractOpenAiCompatibleModelMetadataDirectory::createRequest().

Check failure on line 20 in src/ProviderImplementations/OpenAi/OpenAiModelMetadataDirectory.php

View workflow job for this annotation

GitHub Actions / PHP

Method WordPress\AiClient\ProviderImplementations\OpenAi\OpenAiModelMetadataDirectory::createRequest() has invalid return type WordPress\AiClient\ProviderImplementations\OpenAi\RequestInterface.
{
// Something like this.
return new OpenAiCompatibleRequest('https://api.openai.com/v1', $path);

Check failure on line 23 in src/ProviderImplementations/OpenAi/OpenAiModelMetadataDirectory.php

View workflow job for this annotation

GitHub Actions / PHP

Method WordPress\AiClient\ProviderImplementations\OpenAi\OpenAiModelMetadataDirectory::createRequest() should return WordPress\AiClient\ProviderImplementations\OpenAi\RequestInterface but returns WordPress\AiClient\ProviderImplementations\OpenAi\OpenAiCompatibleRequest.

Check failure on line 23 in src/ProviderImplementations/OpenAi/OpenAiModelMetadataDirectory.php

View workflow job for this annotation

GitHub Actions / PHP

Instantiated class WordPress\AiClient\ProviderImplementations\OpenAi\OpenAiCompatibleRequest not found.
}

/**
* @inheritDoc
*/
protected function parseResponseToModelMetadataList(ResponseInterface $response): array

Check failure on line 29 in src/ProviderImplementations/OpenAi/OpenAiModelMetadataDirectory.php

View workflow job for this annotation

GitHub Actions / PHP

Parameter $response of method WordPress\AiClient\ProviderImplementations\OpenAi\OpenAiModelMetadataDirectory::parseResponseToModelMetadataList() has invalid type WordPress\AiClient\Providers\ResponseInterface.

Check failure on line 29 in src/ProviderImplementations/OpenAi/OpenAiModelMetadataDirectory.php

View workflow job for this annotation

GitHub Actions / PHP

Parameter $response of method WordPress\AiClient\ProviderImplementations\OpenAi\OpenAiModelMetadataDirectory::parseResponseToModelMetadataList() has invalid type WordPress\AiClient\ProviderImplementations\OpenAi\ResponseInterface.

Check failure on line 29 in src/ProviderImplementations/OpenAi/OpenAiModelMetadataDirectory.php

View workflow job for this annotation

GitHub Actions / PHP

Parameter #1 $response (WordPress\AiClient\ProviderImplementations\OpenAi\ResponseInterface) of method WordPress\AiClient\ProviderImplementations\OpenAi\OpenAiModelMetadataDirectory::parseResponseToModelMetadataList() is not contravariant with parameter #1 $response (WordPress\AiClient\Providers\ResponseInterface) of method WordPress\AiClient\Providers\AbstractOpenAiCompatibleModelMetadataDirectory::parseResponseToModelMetadataList().
{
$responseData = $response->getData();

Check failure on line 31 in src/ProviderImplementations/OpenAi/OpenAiModelMetadataDirectory.php

View workflow job for this annotation

GitHub Actions / PHP

Call to method getData() on an unknown class WordPress\AiClient\ProviderImplementations\OpenAi\ResponseInterface.
if (!isset($responseData['data']) || !$responseData['data']) {

Check failure on line 32 in src/ProviderImplementations/OpenAi/OpenAiModelMetadataDirectory.php

View workflow job for this annotation

GitHub Actions / PHP

Cannot access offset 'data' on mixed.
throw new RuntimeException(

Check failure on line 33 in src/ProviderImplementations/OpenAi/OpenAiModelMetadataDirectory.php

View workflow job for this annotation

GitHub Actions / PHP

Instantiated class WordPress\AiClient\ProviderImplementations\OpenAi\RuntimeException not found.
'Unexpected API response: Missing the data key.'
);
}
return array_map(
static function (array $modelData): ModelMetadata {
// TODO: Create ModelMetadata object from API data.
},
$responseData['data']
);
}
}
82 changes: 82 additions & 0 deletions src/ProviderImplementations/OpenAi/OpenAiProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

namespace WordPress\AiClient\ProviderImplementations\OpenAi;

use WordPress\AiClient\Providers\AbstractProvider;
use WordPress\AiClient\Providers\Contracts\ModelMetadataDirectoryInterface;
use WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface;
use WordPress\AiClient\Providers\Contracts\ProviderInterface;
use WordPress\AiClient\Providers\DTO\ProviderMetadata;
use WordPress\AiClient\Providers\Enums\ProviderTypeEnum;
use WordPress\AiClient\Providers\ListModelsApiBasedProviderAvailability;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;

/**
* Class for the OpenAI provider.
*
* @since n.e.x.t
*/
class OpenAiProvider extends AbstractProvider
{
/**
* @inheritDoc
*/
protected static function createModel(
ModelMetadata $modelMetadata,
ProviderMetadata $providerMetadata
): ModelInterface {
$capabilities = $modelMetadata->getCapabilities();
foreach ($capabilities as $capability) {
if ($capability->isTextGeneration()) {
return new OpenAiTextGenerationModel($modelMetadata, $providerMetadata);
}
if ($capability->isImageGeneration()) {
// TODO: Implement OpenAiImageGenerationModel.
return new OpenAiImageGenerationModel($modelMetadata, $providerMetadata);
}
if ($capability->isTextToSpeechConversion()) {
// TODO: Implement OpenAiTextToSpeechConversionModel.
return new OpenAiTextToSpeechConversionModel($modelMetadata, $providerMetadata);
}
}

throw new RuntimeException(
'Unsupported model capabilities: ' . implode(', ', $capabilities)
);
}

/**
* @inheritDoc
*/
protected static function createProviderMetadata(): ProviderMetadata
{
return new ProviderMetadata(
'openai',
'OpenAI',
ProviderTypeEnum::cloud()
);
}

/**
* @inheritDoc
*/
protected static function createProviderAvailability(): ProviderAvailabilityInterface
{
// Check valid API access by attempting to list models.
return new ListModelsApiBasedProviderAvailability(
static::modelMetadataDirectory()
);
}

/**
* @inheritDoc
*/
protected static function createModelMetadataDirectory(): ModelMetadataDirectoryInterface
{
return new OpenAiModelMetadataDirectory();
}
}
25 changes: 25 additions & 0 deletions src/ProviderImplementations/OpenAi/OpenAiTextGenerationModel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace WordPress\AiClient\ProviderImplementations\OpenAi;

use WordPress\AiClient\Providers\AbstractOpenAiCompatibleTextGenerationModel;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;

/**
* Class for an OpenAI text generation model.
*
* @since n.e.x.t
*/
class OpenAiTextGenerationModel extends AbstractOpenAiCompatibleTextGenerationModel
{
/**
* @inheritDoc
*/
protected function createRequest(string $path, array $params): RequestInterface
{
// Something like this.
return new OpenAiCompatibleRequest('https://api.openai.com/v1', $path);
}
}
103 changes: 103 additions & 0 deletions src/Providers/AbstractApiBasedModel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

declare(strict_types=1);

namespace WordPress\AiClient\Providers;

use InvalidArgumentException;
use WordPress\AiClient\Providers\DTO\ProviderMetadata;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\Contracts\WithHttpTransporterInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
use WordPress\AiClient\Providers\Models\Traits\WithHttpTransporterTrait;

/**
* Base class for an API-based model for a provider.
*
* @since n.e.x.t
*/
abstract class AbstractApiBasedModel implements
ModelInterface,
WithHttpTransporterInterface
{
use WithHttpTransporterTrait;

/**
* @var ModelMetadata The metadata for the model.
*/
private ModelMetadata $metadata;

/**
* @var ProviderMetadata The metadata for the model's provider.
*/
private ProviderMetadata $providerMetadata;

/**
* @var ModelConfig The configuration for the model.
*/
private ModelConfig $config;

/**
* Constructor.
*
* @since n.e.x.t
*
* @param ModelMetadata $metadata The metadata for the model.
* @param ProviderMetadata $providerMetadata The metadata for the model's provider.
*/
public function __construct(ModelMetadata $metadata, ProviderMetadata $providerMetadata)
{
$this->metadata = $metadata;
$this->providerMetadata = $providerMetadata;
$this->config = ModelConfig::fromArray([]);
}

/**
* Returns the metadata for the model.
*
* @since n.e.x.t
*
* @return ModelMetadata The model metadata.
*/
public function metadata(): ModelMetadata
{
return $this->metadata;
}

/**
* Returns the metadata for the model's provider.
*
* @since n.e.x.t
*
* @return ProviderMetadata The provider metadata.
*/
public function providerMetadata(): ProviderMetadata
{
return $this->providerMetadata;
}

/**
* Sets the configuration for the model.
*
* @since n.e.x.t
*
* @param ModelConfig $config The configuration for the model.
*/
public function setConfig(ModelConfig $config): void
{
$this->config = $config;
}

/**
* Returns the configuration for the model.
*
* @since n.e.x.t
*
* @return ModelConfig The model configuration.
*/
public function getConfig(): ModelConfig
{
return $this->config;
}
}
105 changes: 105 additions & 0 deletions src/Providers/AbstractApiBasedModelMetadataDirectory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

declare(strict_types=1);

namespace WordPress\AiClient\Providers;

use InvalidArgumentException;
use WordPress\AiClient\Providers\Contracts\ModelMetadataDirectoryInterface;
use WordPress\AiClient\Providers\Models\Contracts\WithHttpTransporterInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
use WordPress\AiClient\Providers\Models\Traits\WithHttpTransporterTrait;

/**
* Base class for an API-based model metadata directory for a provider.
*
* @since n.e.x.t
*/
abstract class AbstractApiBasedModelMetadataDirectory implements
ModelMetadataDirectoryInterface,
WithHttpTransporterInterface
{
use WithHttpTransporterTrait;

/**
* @var ?array<string, ModelMetadata> Map of model ID to model metadata, effectively for caching.
*/
private ?array $modelMetadataMap = null;

/**
* Lists the metadata for all models from the provider.
*
* @since n.e.x.t
*
* @return list<ModelMetadata> List of model metadata objects.
*/
final public function listModelMetadata(): array
{
$modelsMetadata = $this->getModelMetadataMap();
return array_values($modelsMetadata);
}

/**
* Checks whether model metadata for the given model ID exists.
*
* This is effectively a check for whether the given model ID is for a valid model from the provider.
*
* @since n.e.x.t
*
* @param string $modelId The model ID.
* @return bool True if there is metadata for the model.
*/
final public function hasModelMetadata(string $modelId): bool
{
try {
$this->getModelMetadata();
} catch (InvalidArgumentException $e) {
return false;
}
return true;
}

/**
* Gets the model metadata for the given model ID.
*
* @since n.e.x.t
*
* @param string $modelId The model ID.
* @return ModelMetadata The model metadata.
* @throws InvalidArgumentException If the model for the given ID does not exist.
*/
final public function getModelMetadata(string $modelId): ModelMetadata
{
$modelsMetadata = $this->getModelMetadataMap();
if (!isset($modelsMetadata[$modelId])) {
throw new InvalidArgumentException(
sprintf('No model with ID %s was found in the provider', $modelId)
);
}
return $modelsMetadata[$modelId];
Copy link
Member

@JasonTheAdams JasonTheAdams Aug 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we simplify the has function, per my last comment, then we can simplify this, too:

if (!$this->has($modelId)) {
	throw new InvalidArgumentException(
		sprintf('No model with ID %s was found in the provider', $modelId)
	);
}

return $this->getModelMetadataMap()[$modelId];

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would push back on this: If you argue above that calling get() to determine has() is overhead (which I agree with!), then calling has() to determine get() is equally overhead. I think handling it just internally of the method is most pragmatic from that perspective.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My issue wasn't with methods referencing one another; I think that's just reducing redundancy. I more view get() as a higher order function than has(). That is, it makes sense for get() to check whether it has the value in order to return it. But having has() infer itself from a failed get feels weird.

That said, it's really not a big deal. It's such a simple check. 😄

}

/**
* Returns the map of model ID to model metadata for all models from the provider.
*
* @since n.e.x.t
*
* @return array<string, ModelMetadata> Map of model ID to model metadata.
*/
private function getModelMetadataMap(): array
{
if ($this->modelMetadataMap === null) {
$this->modelMetadataMap = $this->sendListModelsRequest();
}
return $this->modelMetadataMap;
}

/**
* Sends the API request to list models from the provider and returns the map of model ID to model metadata.
*
* @since n.e.x.t
*
* @return array<string, ModelMetadata> Map of model ID to model metadata.
*/
abstract protected function sendListModelsRequest(): array;
}
Loading