Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
.gitattributes export-ignore
/.github/ export-ignore
.gitignore export-ignore
/cli.php export-ignore
/*.md export-ignore
/LICENSE.md -export-ignore
/README.md -export-ignore
Expand Down
181 changes: 181 additions & 0 deletions cli.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
<?php
/**
* CLI script for interacting with the AI client.
*
* This script allows users to send prompts to the AI and receive responses.
* It supports named arguments for provider and model selection.
*
* Usage:
* GOOGLE_API_KEY=123456 php cli.php 'Your prompt here' --providerId=google --modelId=gemini-2.5-flash
* OPENAI_API_KEY=123456 php cli.php 'Your prompt here' --providerId=openai
* GOOGLE_API_KEY=123456 OPENAI_API_KEY=123456 php cli.php 'Your prompt here'
*/

declare(strict_types=1);

use WordPress\AiClient\AiClient;
use WordPress\AiClient\Providers\Http\Exception\ResponseException;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;

require_once __DIR__ . '/vendor/autoload.php';

/**
* Prints the output to stdout.
*
* @param string $output The output to print.
*/
function printOutput(string $output): void
{
echo $output . PHP_EOL;
}

/**
* Logs an informational message to stderr.
*
* @param string $message The message to log.
*/
function logInfo(string $message): void
{
fwrite(STDERR, '[INFO] ' . $message . PHP_EOL);
}

/**
* Logs a warning message to stderr.
*
* @param string $message The message to log.
*/
function logWarning(string $message): void
{
fwrite(STDERR, '[WARNING] ' . $message . PHP_EOL);
}

/**
* Logs an error message to stderr and terminates the script.
*
* @param string $message The message to log.
* @param int $exit_code The exit code to use.
*/
function logError(string $message, int $exit_code = 1): void
{
fwrite(STDERR, '[ERROR] ' . $message . PHP_EOL);
exit($exit_code);
}

// --- Argument parsing ---

$positional_args = [];
$named_args = [];

for ($i = 1; $i < $argc; $i++) {
$arg = $argv[$i];
if (str_starts_with($arg, '--')) {
$parts = explode('=', substr($arg, 2), 2);
$key = $parts[0];
$value = $parts[1] ?? true;
if (empty($key)) {
logWarning("Ignoring invalid named argument: {$arg}");
continue;
}
$named_args[$key] = $value;
} else {
$positional_args[] = $arg;
}
}

// --- Input validation ---

if (empty($positional_args[0])) {
logError('Missing required positional argument "prompt input".');
}

// Prompt input. Allow complex input as a JSON string.
$promptInput = $positional_args[0];
if (strpos($promptInput, '{') === 0 || strpos($promptInput, '[') === 0) {
$decodedInput = json_decode($promptInput, true);
if ($decodedInput) {
$promptInput = $decodedInput;
}
}

// Provider ID, model ID, and output format.
$providerId = $named_args['providerId'] ?? null;
$modelId = $named_args['modelId'] ?? null;
$outputFormat = $named_args['outputFormat'] ?? 'message-text';

// Any model configuration options.
$schema = ModelConfig::getJsonSchema()['properties'];
$model_config_data = [];
foreach ($named_args as $key => $value) {
if (!isset($schema[$key])) {
continue;
}

$property_schema = $schema[$key];
$type = $property_schema['type'] ?? null;

$processed_value = $value;
if ($type === 'array' || $type === 'object') {
$decoded = json_decode((string) $value, true);
if (json_last_error() !== JSON_ERROR_NONE) {
logWarning("Invalid JSON for argument --{$key}: " . json_last_error_msg());
continue;
}
$processed_value = $decoded;
} elseif ($type === 'integer') {
$processed_value = (int) $value;
} elseif ($type === 'number') {
$processed_value = (float) $value;
} elseif ($type === 'boolean') {
$processed_value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
if (null === $processed_value) {
logWarning("Invalid boolean for argument --{$key}: {$value}");
continue;
}
}

$model_config_data[$key] = $processed_value;
}

// --- Main logic ---

try {
$modelConfig = ModelConfig::fromArray($model_config_data);

$promptBuilder = AiClient::prompt($promptInput);
$promptBuilder = $promptBuilder->usingModelConfig($modelConfig);
if ($providerId && $modelId) {
$providerClassName = AiClient::defaultRegistry()->getProviderClassName($providerId);
$promptBuilder = $promptBuilder->usingModel($providerClassName::model($modelId));
} elseif ($providerId) {
$promptBuilder = $promptBuilder->usingProvider($providerId);
}
} catch (InvalidArgumentException $e) {
logError('Invalid arguments while trying to set up prompt builder: ' . $e->getMessage());
} catch (ResponseException $e) {
logError('Request failed while trying to set up prompt builder: ' . $e->getMessage());
}

try {
$result = $promptBuilder->generateTextResult();
} catch (InvalidArgumentException $e) {
logError('Invalid arguments while trying to generate text result: ' . $e->getMessage());
} catch (ResponseException $e) {
logError('Request failed while trying to generate text result: ' . $e->getMessage());
}

logInfo("Using provider ID: \"{$result->getProviderMetadata()->getId()}\"");
logInfo("Using model ID: \"{$result->getModelMetadata()->getId()}\"");

switch ($outputFormat) {
case 'result-json':
$output = json_encode($result, JSON_PRETTY_PRINT);
break;
case 'candidates-json':
$output = json_encode($result->getCandidates(), JSON_PRETTY_PRINT);
break;
case 'message-text':
default:
$output = $result->toText();
}

printOutput($output);
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -893,7 +893,7 @@ direction LR
+send(Request $request) Response
}
class RequestAuthenticationInterface {
+authenticate(Request $request) void
+authenticateRequest(Request $request) Request
+getJsonSchema() array< string, mixed >$
}
class WithHttpTransporterInterface {
Expand Down
15 changes: 9 additions & 6 deletions src/AiClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
namespace WordPress\AiClient;

use WordPress\AiClient\Builders\PromptBuilder;
use WordPress\AiClient\ProviderImplementations\Anthropic\AnthropicProvider;
use WordPress\AiClient\ProviderImplementations\Google\GoogleProvider;
use WordPress\AiClient\ProviderImplementations\OpenAi\OpenAiProvider;
use WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface;
use WordPress\AiClient\Providers\Http\HttpTransporterFactory;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\ProviderRegistry;
Expand Down Expand Up @@ -95,12 +99,11 @@ public static function defaultRegistry(): ProviderRegistry
if (self::$defaultRegistry === null) {
$registry = new ProviderRegistry();

// Provider registration will be enabled once concrete provider implementations are available.
// This follows the pattern established in the provider registry architecture.
//$registry->setHttpTransporter(HttpTransporterFactory::createTransporter());
//$registry->registerProvider(AnthropicProvider::class);
//$registry->registerProvider(GoogleProvider::class);
//$registry->registerProvider(OpenAiProvider::class);
// Set up default HTTP transporter and register built-in providers.
$registry->setHttpTransporter(HttpTransporterFactory::createTransporter());
$registry->registerProvider(AnthropicProvider::class);
$registry->registerProvider(GoogleProvider::class);
$registry->registerProvider(OpenAiProvider::class);

self::$defaultRegistry = $registry;
}
Expand Down
12 changes: 12 additions & 0 deletions src/Builders/PromptBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,9 @@ public function withHistory(Message ...$messages): self
/**
* Sets the model to use for generation.
*
* The model's configuration will be merged with the builder's configuration,
* with the builder's configuration taking precedence for any overlapping settings.
*
* @since n.e.x.t
*
* @param ModelInterface $model The model to use.
Expand All @@ -199,6 +202,14 @@ public function withHistory(Message ...$messages): self
public function usingModel(ModelInterface $model): self
{
$this->model = $model;

// Merge model's config with builder's config, with builder's config taking precedence
$modelConfigArray = $model->getConfig()->toArray();
$builderConfigArray = $this->modelConfig->toArray();
$mergedConfigArray = array_merge($modelConfigArray, $builderConfigArray);

$this->modelConfig = ModelConfig::fromArray($mergedConfigArray);

return $this;
}

Expand Down Expand Up @@ -1009,6 +1020,7 @@ private function getConfiguredModel(CapabilityEnum $capability): ModelInterface
// If a model has been explicitly set, return it
if ($this->model !== null) {
$this->model->setConfig($this->modelConfig);
$this->registry->bindModelDependencies($this->model);
return $this->model;
}

Expand Down
16 changes: 15 additions & 1 deletion src/Common/AbstractEnum.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use BadMethodCallException;
use InvalidArgumentException;
use JsonSerializable;
use ReflectionClass;
use RuntimeException;

Expand Down Expand Up @@ -36,7 +37,7 @@
*
* @since n.e.x.t
*/
abstract class AbstractEnum
abstract class AbstractEnum implements JsonSerializable
{
/**
* @var string The value of the enum instance.
Expand Down Expand Up @@ -393,4 +394,17 @@ final public function __toString(): string
{
return $this->value;
}

/**
* Converts the enum to a JSON-serializable format.
*
* @since n.e.x.t
*
* @return string The enum value.
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->value;
}
}
22 changes: 22 additions & 0 deletions src/Files/ValueObjects/MimeType.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ final class MimeType
'ogg' => 'audio/ogg',
'flac' => 'audio/flac',
'm4a' => 'audio/m4a',
'aac' => 'audio/aac',

// Video
'mp4' => 'video/mp4',
Expand Down Expand Up @@ -130,6 +131,27 @@ public function __construct(string $value)
$this->value = strtolower($value);
}

/**
* Gets the primary known file extension for this MIME type.
*
* @since n.e.x.t
*
* @return string The file extension (without the dot).
* @throws InvalidArgumentException If no known extension exists for this MIME type.
*/
public function toExtension(): string
{
// Reverse lookup for the MIME type to find the extension.
$extension = array_search($this->value, self::$extensionMap, true);
if ($extension === false) {
throw new InvalidArgumentException(
sprintf('No known extension for MIME type: %s', $this->value)
);
}

return $extension;
}

/**
* Creates a MimeType from a file extension.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace WordPress\AiClient\ProviderImplementations\Anthropic;

use WordPress\AiClient\Providers\Http\DTO\ApiKeyRequestAuthentication;
use WordPress\AiClient\Providers\Http\DTO\Request;

/**
* Class for HTTP request authentication using an API key in a Anthropic API compliant way.
*
* @since n.e.x.t
*/
class AnthropicApiKeyRequestAuthentication extends ApiKeyRequestAuthentication
{
public const ANTHROPIC_API_VERSION = '2023-06-01';

/**
* {@inheritDoc}
*
* @since n.e.x.t
*/
public function authenticateRequest(Request $request): Request
{
// Anthropic requires this header to be set for all requests.
$request = $request->withHeader('anthropic-version', self::ANTHROPIC_API_VERSION);

// Add the API key to the request headers.
return $request->withHeader('x-api-key', $this->apiKey);
}
}
Loading