Add PSR HTTP Message Interfaces and Dependencies

- Implemented StreamInterface, UploadedFileInterface, and UriInterface as per PSR standards.
- Added getallheaders function to retrieve HTTP headers in a compatible manner.
- Included LICENSE files for ralouphie/getallheaders and symfony/deprecation-contracts.
- Introduced function for triggering deprecation notices in Symfony.
This commit is contained in:
2025-12-28 12:44:00 +01:00
parent cf600ae727
commit cd264483f8
410 changed files with 60841 additions and 16 deletions

View File

@@ -0,0 +1,220 @@
<?php
/**
* Class ATFPP\AI_Translate\OpenAI\OpenAI_AI_Service
*
* @since 0.1.0
* @package ai-services
*/
namespace ATFPP\AI_Translate\OpenAI;
use ATFPP\AI_Translate\Services\API\Enums\AI_Capability;
use ATFPP\AI_Translate\Services\API\Types\Model_Metadata;
use ATFPP\AI_Translate\Services\API\Types\Service_Metadata;
use ATFPP\AI_Translate\Services\Base\Abstract_AI_Service;
use ATFPP\AI_Translate\Services\Base\Generic_AI_API_Client;
use ATFPP\AI_Translate\Services\Contracts\Authentication;
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_Model;
use ATFPP\AI_Translate\Services\Contracts\With_API_Client;
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
use ATFPP\AI_Translate\Services\HTTP\HTTP_With_Streams;
use ATFPP\AI_Translate\Services\Traits\With_API_Client_Trait;
use ATFPP\AI_Translate\Services\Util\AI_Capabilities;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\HTTP\Contracts\Request_Handler;
/**
* Class for the OpenAI AI service.
*
* @since 0.1.0
* @since 0.7.0 Now extends `Abstract_AI_Service`.
*/
class OpenAI_AI_Service extends Abstract_AI_Service implements With_API_Client {
use With_API_Client_Trait;
const DEFAULT_API_BASE_URL = 'https://api.openai.com';
const DEFAULT_API_VERSION = 'v1';
/**
* Constructor.
*
* @since 0.1.0
*
* @param Service_Metadata $metadata The service metadata.
* @param Authentication $authentication The authentication credentials.
* @param Request_Handler $request_handler Optional. The request handler instance to use for requests. Default is a
* new HTTP_With_Streams instance.
*/
public function __construct( Service_Metadata $metadata, Authentication $authentication, ?Request_Handler $request_handler = null ) {
$this->set_service_metadata( $metadata );
$this->set_api_client(
new Generic_AI_API_Client(
self::DEFAULT_API_BASE_URL,
self::DEFAULT_API_VERSION,
'OpenAI',
$request_handler ?? new HTTP_With_Streams(),
$authentication
)
);
}
/**
* Lists the available generative model slugs and their metadata.
*
* @since 0.1.0
* @since 0.5.0 Return type changed to a map of model data shapes.
* @since 0.7.0 Return type changed to a map of model metadata objects.
*
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
* @return array<string, Model_Metadata> Metadata for each model, mapped by model slug.
*
* @throws Generative_AI_Exception Thrown if the request fails or the response is invalid.
*/
public function list_models( array $request_options = array() ): array {
$api = $this->get_api_client();
$request = $api->create_get_request( 'models', array(), $request_options );
$response_data = $api->make_request( $request )->get_data();
if ( ! isset( $response_data['data'] ) || ! $response_data['data'] ) {
throw $api->create_missing_response_key_exception( 'data' );
}
// Unfortunately, the OpenAI API does not return model capabilities, so we have to hardcode them here.
$gpt_capabilities = array(
AI_Capability::FUNCTION_CALLING,
AI_Capability::TEXT_GENERATION,
);
$gpt_multimodal_capabilities = array(
AI_Capability::FUNCTION_CALLING,
AI_Capability::MULTIMODAL_INPUT,
AI_Capability::TEXT_GENERATION,
);
$image_capabilities = array(
AI_Capability::IMAGE_GENERATION,
);
$tts_capabilities = array(
AI_Capability::TEXT_TO_SPEECH,
);
return array_reduce(
$response_data['data'],
static function ( array $models_data, array $model_data ) use ( $gpt_capabilities, $gpt_multimodal_capabilities, $image_capabilities, $tts_capabilities ) {
$model_slug = $model_data['id'];
if (
str_starts_with( $model_slug, 'dall-e-' ) ||
str_starts_with( $model_slug, 'gpt-image-' )
) {
$model_caps = $image_capabilities;
} elseif (
str_starts_with( $model_slug, 'tts-' ) ||
str_contains( $model_slug, '-tts' )
) {
$model_caps = $tts_capabilities;
} elseif (
( str_starts_with( $model_slug, 'gpt-' ) || str_starts_with( $model_slug, 'o1-' ) )
&& ! str_contains( $model_slug, '-instruct' )
&& ! str_contains( $model_slug, '-realtime' )
) {
if ( str_starts_with( $model_slug, 'gpt-4o' ) ) {
$model_caps = $gpt_multimodal_capabilities;
// New multimodal output model for audio generation.
if ( str_contains( $model_slug, '-audio' ) ) {
$model_caps[] = AI_Capability::MULTIMODAL_OUTPUT;
}
} elseif ( ! str_contains( $model_slug, '-audio' ) ) {
$model_caps = $gpt_capabilities;
} else {
$model_caps = array();
}
} else {
$model_caps = array();
}
$models_data[ $model_slug ] = Model_Metadata::from_array(
// The OpenAI API does not return a display name, so 'name' is omitted to auto-generate.
array(
'slug' => $model_slug,
'capabilities' => $model_caps,
)
);
return $models_data;
},
array()
);
}
/**
* Creates a new model instance for the provided model metadata and parameters.
*
* @since 0.7.0
*
* @param Model_Metadata $model_metadata The model metadata.
* @param array<string, mixed> $model_params Model parameters. See {@see Generative_AI_Service::get_model()} for
* a list of available parameters.
* @param array<string, mixed> $request_options The request options.
* @return Generative_AI_Model The new model instance.
*/
protected function create_model_instance( Model_Metadata $model_metadata, array $model_params, array $request_options ): Generative_AI_Model {
$model_class = AI_Capabilities::get_model_class_for_capabilities(
array(
OpenAI_AI_Text_Generation_Model::class
),
$model_metadata->get_capabilities()
);
return new $model_class(
$this->get_api_client(),
$model_metadata,
$model_params,
$request_options
);
}
/**
* Sorts model slugs by preference.
*
* @since 0.1.0
*
* @param string[] $model_slugs The model slugs to sort.
* @return string[] The model slugs, sorted by preference.
*/
protected function sort_models_by_preference( array $model_slugs ): array {
// Prioritize latest, non-experimental models, preferring cheaper ones.
$get_preference_group = static function ( $model_slug ) {
if ( str_starts_with( $model_slug, 'gpt-4.1' ) ) {
if ( str_ends_with( $model_slug, '-mini' ) ) {
return 0;
}
return 1;
}
if ( str_starts_with( $model_slug, 'gpt-4o' ) ) {
if ( str_ends_with( $model_slug, '-mini' ) ) {
return 2;
}
return 3;
}
if ( str_starts_with( $model_slug, 'gpt-4' ) ) {
if ( str_ends_with( $model_slug, '-turbo' ) ) {
return 4;
}
return 5;
}
if ( str_starts_with( $model_slug, 'gpt-' ) ) {
if ( str_ends_with( $model_slug, '-turbo' ) ) {
return 6;
}
return 7;
}
return 8;
};
$preference_groups = array_fill( 0, 9, array() );
foreach ( $model_slugs as $model_slug ) {
$group = $get_preference_group( $model_slug );
$preference_groups[ $group ][] = $model_slug;
}
return array_merge( ...$preference_groups );
}
}

View File

@@ -0,0 +1,175 @@
<?php
/**
* Class ATFPP\AI_Translate\OpenAI\OpenAI_AI_Text_Generation_Model
*
* @since 0.1.0
* @package ai-services
*/
namespace ATFPP\AI_Translate\OpenAI;
use ATFPP\AI_Translate\Services\API\Types\Content;
use ATFPP\AI_Translate\Services\API\Types\Contracts\Tool;
use ATFPP\AI_Translate\Services\API\Types\Model_Metadata;
use ATFPP\AI_Translate\Services\API\Types\Parts;
use ATFPP\AI_Translate\Services\API\Types\Text_Generation_Config;
use ATFPP\AI_Translate\Services\Base\OpenAI_Compatible_AI_Text_Generation_Model;
use ATFPP\AI_Translate\Services\Contracts\Generative_AI_API_Client;
use ATFPP\AI_Translate\Services\Contracts\With_Function_Calling;
use ATFPP\AI_Translate\Services\Contracts\With_Multimodal_Input;
use ATFPP\AI_Translate\Services\Contracts\With_Multimodal_Output;
use ATFPP\AI_Translate\Services\Exception\Generative_AI_Exception;
use ATFPP\AI_Translate\Services\Traits\OpenAI_Compatible_Text_Generation_With_Function_Calling_Trait;
use InvalidArgumentException;
/**
* Class representing an OpenAI text generation AI model.
*
* @since 0.1.0
* @since 0.5.0 Renamed from `OpenAI_AI_Model`.
* @since 0.7.0 Now extends `OpenAI_Compatible_AI_Text_Generation_Model` instead of `Abstract_AI_Model`.
*/
class OpenAI_AI_Text_Generation_Model extends OpenAI_Compatible_AI_Text_Generation_Model implements With_Function_Calling, With_Multimodal_Input, With_Multimodal_Output {
use OpenAI_Compatible_Text_Generation_With_Function_Calling_Trait {
prepare_generate_text_params as prepare_generate_text_params_with_function_calling;
prepare_tool as prepare_function_declarations_tool;
}
/**
* The expected MIME type of any audio output generated by the model.
*
* Internal temporary storage to not have to pass it around, as it should not be part of the interface.
*
* @since 0.7.0
* @var string
*/
private $expected_audio_mime_type = 'audio/mpeg';
/**
* Constructor.
*
* @since 0.1.0
*
* @param Generative_AI_API_Client $api_client The AI API client instance.
* @param Model_Metadata $metadata The model metadata.
* @param array<string, mixed> $model_params Optional. Additional model parameters. See
* {@see OpenAI_AI_Service::get_model()} for the list of available
* parameters. Default empty array.
* @param array<string, mixed> $request_options Optional. The request options. Default empty array.
*
* @throws InvalidArgumentException Thrown if the model parameters are invalid.
*/
public function __construct( Generative_AI_API_Client $api_client, Model_Metadata $metadata, array $model_params = array(), array $request_options = array() ) {
parent::__construct( $api_client, $metadata, $model_params, $request_options );
$this->set_tool_config_from_model_params( $model_params );
$this->set_tools_from_model_params( $model_params );
}
/**
* Prepares the API request parameters for generating text content.
*
* @since 0.7.0
*
* @param Content[] $contents The contents to generate text for.
* @return array<string, mixed> The parameters for generating text content.
*
* @throws InvalidArgumentException Thrown if an invalid tool is provided.
*/
protected function prepare_generate_text_params( array $contents ): array {
$params = $this->prepare_generate_text_params_with_function_calling( $contents );
// If 'audio' output is requested, the OpenAI API requires the 'audio' parameter to be set.
if (
isset( $params['modalities'] ) &&
is_array( $params['modalities'] ) &&
in_array( 'audio', $params['modalities'], true ) &&
! isset( $params['audio'] )
) {
$params['audio'] = array(
'voice' => 'alloy',
'format' => 'mp3',
);
}
if ( isset( $params['audio']['format'] ) ) {
// Hack: Store the expected MIME type for audio output, as the OpenAI API does not return it.
$this->expected_audio_mime_type = 'mp3' === $params['audio']['format'] ? 'audio/mpeg' : 'audio/' . $params['audio']['format'];
}
return $params;
}
/**
* Prepares a single tool for the API request, amending the provided parameters as needed.
*
* @since 0.7.0
*
* @param array<string, mixed> $params The parameters to prepare the tools for. Passed by reference.
* @param Tool $tool The tool to prepare.
* @return bool True if the tool was successfully prepared, false otherwise.
*/
protected function prepare_tool( array &$params, Tool $tool ): bool {
$result = $this->prepare_function_declarations_tool( $params, $tool );
if ( ! $result ) {
return false;
}
/*
* The OpenAI API supports a 'strict' argument for function tools, which is not part of the standard OpenAI API
* specification and therefore may not be supported by other providers.
* Since it makes sense to always use it for OpenAI, we add it here if not set.
*/
if ( isset( $params['tools'] ) && is_array( $params['tools'] ) ) {
$params['tools'] = array_map(
function ( $openai_tool_data ) {
if ( ! isset( $openai_tool_data['type'] ) || 'function' !== $openai_tool_data['type'] ) {
return $openai_tool_data;
}
// Add the 'strict' argument to the function tool if not set.
if ( ! isset( $openai_tool_data['function']['strict'] ) ) {
$openai_tool_data['function']['strict'] = true;
}
return $openai_tool_data;
},
$params['tools']
);
}
return true;
}
/**
* Transforms a given candidate from the API response into a Parts instance.
*
* @since 0.7.0
*
* @param array<string, mixed> $candidate_data The API response candidate data.
* @return Parts The Parts instance.
*
* @throws Generative_AI_Exception Thrown if the response is invalid.
*/
protected function prepare_response_candidate_content_parts( array $candidate_data ): Parts {
$parts = parent::prepare_response_candidate_content_parts( $candidate_data );
return $parts;
}
/**
* Gets the generation configuration transformers.
*
* @since 0.7.0
*
* @return array<string, callable> The generation configuration transformers.
*/
protected function get_generation_config_transformers(): array {
$transformers = parent::get_generation_config_transformers();
// Support multimodal output (e.g. for speech generation).
$transformers['modalities'] = static function ( Text_Generation_Config $config ) {
return $config->get_output_modalities();
};
return $transformers;
}
}