Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion src/ProviderImplementations/Anthropic/AnthropicProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface;
use WordPress\AiClient\Providers\DTO\ProviderMetadata;
use WordPress\AiClient\Providers\Enums\ProviderTypeEnum;
use WordPress\AiClient\Providers\Http\Enums\RequestAuthenticationMethod;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;

Expand Down Expand Up @@ -63,7 +64,8 @@ protected static function createProviderMetadata(): ProviderMetadata
'anthropic',
'Anthropic',
ProviderTypeEnum::cloud(),
'https://console.anthropic.com/settings/keys'
'https://console.anthropic.com/settings/keys',
RequestAuthenticationMethod::apiKey()
);
}

Expand Down
4 changes: 3 additions & 1 deletion src/ProviderImplementations/Google/GoogleProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface;
use WordPress\AiClient\Providers\DTO\ProviderMetadata;
use WordPress\AiClient\Providers\Enums\ProviderTypeEnum;
use WordPress\AiClient\Providers\Http\Enums\RequestAuthenticationMethod;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;

Expand Down Expand Up @@ -66,7 +67,8 @@ protected static function createProviderMetadata(): ProviderMetadata
'google',
'Google',
ProviderTypeEnum::cloud(),
'https://aistudio.google.com/app/api-keys'
'https://aistudio.google.com/app/api-keys',
RequestAuthenticationMethod::apiKey()
);
}

Expand Down
4 changes: 3 additions & 1 deletion src/ProviderImplementations/OpenAi/OpenAiProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface;
use WordPress\AiClient\Providers\DTO\ProviderMetadata;
use WordPress\AiClient\Providers\Enums\ProviderTypeEnum;
use WordPress\AiClient\Providers\Http\Enums\RequestAuthenticationMethod;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;

Expand Down Expand Up @@ -72,7 +73,8 @@ protected static function createProviderMetadata(): ProviderMetadata
'openai',
'OpenAI',
ProviderTypeEnum::cloud(),
'https://platform.openai.com/api-keys'
'https://platform.openai.com/api-keys',
RequestAuthenticationMethod::apiKey()
);
}

Expand Down
44 changes: 40 additions & 4 deletions src/Providers/DTO/ProviderMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Providers\Enums\ProviderTypeEnum;
use WordPress\AiClient\Providers\Http\Enums\RequestAuthenticationMethod;

/**
* Represents metadata about an AI provider.
Expand All @@ -19,7 +20,8 @@
* id: string,
* name: string,
* type: string,
* credentialsUrl?: ?string
* credentialsUrl?: ?string,
* authenticationMethod?: ?string
* }
*
* @extends AbstractDataTransferObject<ProviderMetadataArrayShape>
Expand All @@ -30,6 +32,7 @@ class ProviderMetadata extends AbstractDataTransferObject
public const KEY_NAME = 'name';
public const KEY_TYPE = 'type';
public const KEY_CREDENTIALS_URL = 'credentialsUrl';
public const KEY_AUTHENTICATION_METHOD = 'authenticationMethod';

/**
* @var string The provider's unique identifier.
Expand All @@ -51,6 +54,11 @@ class ProviderMetadata extends AbstractDataTransferObject
*/
protected ?string $credentialsUrl;

/**
* @var RequestAuthenticationMethod|null The authentication method.
*/
protected ?RequestAuthenticationMethod $authenticationMethod;

/**
* Constructor.
*
Expand All @@ -60,13 +68,20 @@ class ProviderMetadata extends AbstractDataTransferObject
* @param string $name The provider's display name.
* @param ProviderTypeEnum $type The provider type.
* @param string|null $credentialsUrl The URL where users can get credentials.
* @param RequestAuthenticationMethod|null $authenticationMethod The authentication method.
*/
public function __construct(string $id, string $name, ProviderTypeEnum $type, ?string $credentialsUrl = null)
{
public function __construct(
string $id,
string $name,
ProviderTypeEnum $type,
?string $credentialsUrl = null,
?RequestAuthenticationMethod $authenticationMethod = null
) {
$this->id = $id;
$this->name = $name;
$this->type = $type;
$this->credentialsUrl = $credentialsUrl;
$this->authenticationMethod = $authenticationMethod;
}

/**
Expand Down Expand Up @@ -117,6 +132,18 @@ public function getCredentialsUrl(): ?string
return $this->credentialsUrl;
}

/**
* Gets the authentication method.
*
* @since n.e.x.t
*
* @return RequestAuthenticationMethod|null The authentication method.
*/
public function getAuthenticationMethod(): ?RequestAuthenticationMethod
{
return $this->authenticationMethod;
}

/**
* {@inheritDoc}
*
Expand Down Expand Up @@ -144,6 +171,11 @@ public static function getJsonSchema(): array
'type' => 'string',
'description' => 'The URL where users can get credentials.',
],
self::KEY_AUTHENTICATION_METHOD => [
'type' => ['string', 'null'],
'enum' => array_merge(RequestAuthenticationMethod::getValues(), [null]),
'description' => 'The authentication method.',
],
],
'required' => [self::KEY_ID, self::KEY_NAME, self::KEY_TYPE],
];
Expand All @@ -163,6 +195,7 @@ public function toArray(): array
self::KEY_NAME => $this->name,
self::KEY_TYPE => $this->type->value,
self::KEY_CREDENTIALS_URL => $this->credentialsUrl,
self::KEY_AUTHENTICATION_METHOD => $this->authenticationMethod ? $this->authenticationMethod->value : null,
];
}

Expand All @@ -179,7 +212,10 @@ public static function fromArray(array $array): self
$array[self::KEY_ID],
$array[self::KEY_NAME],
ProviderTypeEnum::from($array[self::KEY_TYPE]),
$array[self::KEY_CREDENTIALS_URL] ?? null
$array[self::KEY_CREDENTIALS_URL] ?? null,
isset($array[self::KEY_AUTHENTICATION_METHOD])
? RequestAuthenticationMethod::from($array[self::KEY_AUTHENTICATION_METHOD])
: null
);
}
}
42 changes: 42 additions & 0 deletions src/Providers/Http/Enums/RequestAuthenticationMethod.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace WordPress\AiClient\Providers\Http\Enums;

use WordPress\AiClient\Common\AbstractEnum;
use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface;
use WordPress\AiClient\Providers\Http\Contracts\RequestAuthenticationInterface;
use WordPress\AiClient\Providers\Http\DTO\ApiKeyRequestAuthentication;

/**
* Enum for request authentication methods.
*
* @since n.e.x.t
*
* @method static self apiKey() Creates an instance for API_KEY method.
* @method bool isApiKey() Checks if the method is API_KEY.
*/
class RequestAuthenticationMethod extends AbstractEnum
{
/**
* API key authentication.
*/
public const API_KEY = 'api_key';

/**
* Gets the implementation class for the authentication method.
*
* @since n.e.x.t
*
* @return class-string<RequestAuthenticationInterface&WithArrayTransformationInterface> The implementation class.
*
* @phpstan-ignore missingType.generics
*/
public function getImplementationClass(): string
{
// At the moment, this is the only supported method.
// Once more methods are available, add conditionals here for each method.
return ApiKeyRequestAuthentication::class;
}
}
49 changes: 40 additions & 9 deletions src/Providers/ProviderRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
use WordPress\AiClient\Providers\Http\Contracts\RequestAuthenticationInterface;
use WordPress\AiClient\Providers\Http\Contracts\WithHttpTransporterInterface;
use WordPress\AiClient\Providers\Http\Contracts\WithRequestAuthenticationInterface;
use WordPress\AiClient\Providers\Http\DTO\ApiKeyRequestAuthentication;
use WordPress\AiClient\Providers\Http\Traits\WithHttpTransporterTrait;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
Expand Down Expand Up @@ -443,11 +442,36 @@ private function setHttpTransporterForProvider(
*
* @param class-string<ProviderInterface> $className The provider class name.
* @param RequestAuthenticationInterface $requestAuthentication The authentication instance.
*
* @throws InvalidArgumentException If the authentication instance is not of the expected type.
*/
private function setRequestAuthenticationForProvider(
string $className,
RequestAuthenticationInterface $requestAuthentication
): void {
$authenticationMethod = $className::metadata()->getAuthenticationMethod();
if ($authenticationMethod === null) {
throw new InvalidArgumentException(
sprintf(
'Provider %s does not expect any authentication, but got %s.',
$className,
get_class($requestAuthentication)
)
);
}
Copy link
Member

Choose a reason for hiding this comment

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

I'm a bit confused by this logic. We get the authentication method enum from the provider class, then we get the authentication class derived from the enum, null if the enum is null, and then we throw an exception if the authentication class is null saying that it does not expect authentication but has an authentication method enum.

The only way this would get here would be if the getImplementationClass() method returned null, but that's logically impossible (see my other comment).

I also notice this down below, too. I guess it all hinges on whether the getImplementationClass() return type is in fact nullable or not.

Copy link
Member Author

Choose a reason for hiding this comment

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

This clause is there because one shouldn't attempt to set a RequestAuthentication instance for a provider that doesn't need any authentication (i.e. where authenticationMethod is null).

I agree with your feedback that the getImplementationClass() return type shouldn't be nullable, so I'll update, and then the logic here has to be adjusted accordingly. But at its core the condition will still be needed - it'll instead just check whether the authenticationMethod is null instead of whether the expectedClass is null. Makes sense?


$expectedClass = $authenticationMethod->getImplementationClass();
if (!$requestAuthentication instanceof $expectedClass) {
throw new InvalidArgumentException(
sprintf(
'Provider %s expects authentication of type %s, but got %s.',
$className,
$expectedClass,
get_class($requestAuthentication)
)
);
}

$availability = $className::availability();
if ($availability instanceof WithRequestAuthenticationInterface) {
$availability->setRequestAuthentication($requestAuthentication);
Expand Down Expand Up @@ -478,14 +502,19 @@ private function setRequestAuthenticationForProvider(
private function createDefaultProviderRequestAuthentication(
string $className
): ?RequestAuthenticationInterface {
$providerId = $className::metadata()->getId();

/*
* For now, we assume API key authentication is used by default.
* In the future, this could be made more flexible by allowing the provider to express a specific type of
* request authentication to use.
*/
$authenticationClass = ApiKeyRequestAuthentication::class;
$providerMetadata = $className::metadata();
$providerId = $providerMetadata->getId();
$authenticationMethod = $providerMetadata->getAuthenticationMethod();

if ($authenticationMethod === null) {
return null;
}

$authenticationClass = $authenticationMethod->getImplementationClass();
if ($authenticationClass === null) {
return null;
}

$authenticationSchema = $authenticationClass::getJsonSchema();

// Iterate over all JSON schema object properties to try to determine the necessary authentication data.
Expand Down Expand Up @@ -535,6 +564,8 @@ private function createDefaultProviderRequestAuthentication(
}
}

/** @var RequestAuthenticationInterface */
/** @var array<string, mixed> $authenticationData */
return $authenticationClass::fromArray($authenticationData);
}

Expand Down
52 changes: 52 additions & 0 deletions tests/mocks/MockNoAuthProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace WordPress\AiClient\Tests\mocks;

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\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;

/**
* Mock provider that does not require authentication.
*/
class MockNoAuthProvider implements ProviderInterface
{
/**
* {@inheritDoc}
*/
public static function metadata(): ProviderMetadata
{
return new ProviderMetadata('no-auth', 'No Auth Provider', ProviderTypeEnum::server());
}

/**
* {@inheritDoc}
*/
public static function availability(): ProviderAvailabilityInterface
{
return new MockProviderAvailability();
}

/**
* {@inheritDoc}
*/
public static function modelMetadataDirectory(): ModelMetadataDirectoryInterface
{
return new MockModelMetadataDirectory([]);
}

/**
* {@inheritDoc}
*/
public static function model(string $modelId, ?ModelConfig $modelConfig = null): ModelInterface
{
return new MockModel(new ModelMetadata('model', 'Model', [], []), new ModelConfig());
}
}
5 changes: 4 additions & 1 deletion tests/mocks/MockProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use WordPress\AiClient\Providers\Contracts\ProviderInterface;
use WordPress\AiClient\Providers\DTO\ProviderMetadata;
use WordPress\AiClient\Providers\Enums\ProviderTypeEnum;
use WordPress\AiClient\Providers\Http\Enums\RequestAuthenticationMethod;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
Expand Down Expand Up @@ -37,7 +38,9 @@ public static function metadata(): ProviderMetadata
return new ProviderMetadata(
'mock',
'Mock Provider',
ProviderTypeEnum::cloud()
ProviderTypeEnum::cloud(),
null,
RequestAuthenticationMethod::apiKey()
);
}

Expand Down
Loading