Skip to content
Merged
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
38 changes: 36 additions & 2 deletions bin/plugin-check-bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,24 @@
\WP_CLI::error( 'Plugin Check engine autoloader not found: ' . $autoload );
}

require_once $autoload;
if ( ! function_exists( 'clawpress_plugin_check_disable_engine_ai_client_autoload' ) ) {
/**
* Prevent Plugin Check's bundled AI client from shadowing WordPress core's scoped AI client.
*
* @param mixed $loader Composer loader returned by the standalone engine.
*/
function clawpress_plugin_check_disable_engine_ai_client_autoload( $loader ): void {
if ( ! is_object( $loader ) || ! method_exists( $loader, 'setPsr4' ) ) {
return;
}

$loader->setPsr4( 'WordPress\\AiClient\\', array() );
$loader->setPsr4( 'WordPress\\AI_Client\\', array() );
}
}

$engine_loader = require_once $autoload;
clawpress_plugin_check_disable_engine_ai_client_autoload( $engine_loader );

if ( ! class_exists( '\WordPress\Plugin_Check\CLI\Plugin_Check_Command' ) ) {
\WP_CLI::error( 'Plugin Check command class could not be loaded from standalone engine.' );
Expand Down Expand Up @@ -108,7 +125,24 @@ function plugin_check_initialize_runner(): void {
return;
}

require_once $autoload;
if ( ! function_exists( 'clawpress_plugin_check_disable_engine_ai_client_autoload' ) ) {
/**
* Prevent Plugin Check's bundled AI client from shadowing WordPress core's scoped AI client.
*
* @param mixed $loader Composer loader returned by the standalone engine.
*/
function clawpress_plugin_check_disable_engine_ai_client_autoload( $loader ): void {
if ( ! is_object( $loader ) || ! method_exists( $loader, 'setPsr4' ) ) {
return;
}

$loader->setPsr4( 'WordPress\\AiClient\\', array() );
$loader->setPsr4( 'WordPress\\AI_Client\\', array() );
}
}

$engine_loader = require_once $autoload;
clawpress_plugin_check_disable_engine_ai_client_autoload( $engine_loader );

if ( class_exists( '\WordPress\Plugin_Check\Utilities\Plugin_Request_Utility' ) ) {
\WordPress\Plugin_Check\Utilities\Plugin_Request_Utility::initialize_runner();
Expand Down
2 changes: 1 addition & 1 deletion build/scripts/admin.asset.php
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-components', 'wp-core-data', 'wp-data', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-primitives'), 'version' => 'e03b8b5cacabfc887813');
<?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-components', 'wp-core-data', 'wp-data', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-primitives'), 'version' => '801d5acd1dc77705c224');
2 changes: 1 addition & 1 deletion build/scripts/admin.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/scripts/style-admin-rtl.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/scripts/style-admin.css

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions clawpress.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
require_once CLAWPRESS_DIR . 'vendor/autoload.php';
}

require_once CLAWPRESS_DIR . 'includes/autoload.php';

if ( function_exists( 'register_activation_hook' ) ) {
register_activation_hook( CLAWPRESS_FILE, [ Plugin::class, 'activate' ] );
}
Expand Down
96 changes: 96 additions & 0 deletions includes/autoload.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php
/**
* Plugin class autoload fallback.
*
* @package ClawPress
*/

declare( strict_types=1 );

namespace ClawPress;

defined( 'ABSPATH' ) || exit;

if ( ! function_exists( __NAMESPACE__ . '\clawpress_autoload_slug' ) ) {
/**
* Normalize a class or namespace segment into its file slug.
*
* @param string $segment Class or namespace segment.
*/
function clawpress_autoload_slug( string $segment ): string {
$slug = strtolower( str_replace( '_', '-', $segment ) );

$directory_map = [
'adminpage' => 'admin-page',
'builtin' => 'built-in',
'posttypes' => 'post-types',
'restapi' => 'rest',
'webfetch' => 'web-fetch',
];

return $directory_map[ $slug ] ?? $slug;
}
}

if ( ! function_exists( __NAMESPACE__ . '\clawpress_autoload_class_file_candidates' ) ) {
/**
* Resolve candidate class file paths for a plugin class.
*
* @param string $class_name Fully qualified class name.
* @return array<int,string>
*/
function clawpress_autoload_class_file_candidates( string $class_name ): array {
$prefix = __NAMESPACE__ . '\\';
if ( ! str_starts_with( $class_name, $prefix ) ) {
return [];
}

$relative_class = substr( $class_name, strlen( $prefix ) );
if ( '' === $relative_class ) {
return [];
}

$parts = explode( '\\', $relative_class );
$short_class_name = array_pop( $parts );
if ( ! is_string( $short_class_name ) || '' === $short_class_name ) {
return [];
}

$class_slug = clawpress_autoload_slug( $short_class_name );
$candidates = [
__DIR__ . '/class-' . $class_slug . '.php',
];

if ( [] !== $parts ) {
$directories = array_map( __NAMESPACE__ . '\clawpress_autoload_slug', $parts );
$candidates[] = __DIR__ . '/' . implode( '/', $directories ) . '/class-' . $class_slug . '.php';

while ( [] !== $directories && in_array( $directories[ count( $directories ) - 1 ], [ 'built-in', 'controllers' ], true ) ) {
array_pop( $directories );
if ( [] !== $directories ) {
$candidates[] = __DIR__ . '/' . implode( '/', $directories ) . '/class-' . $class_slug . '.php';
}
}
}

return array_values( array_unique( $candidates ) );
}
}

if ( ! function_exists( __NAMESPACE__ . '\clawpress_autoload_class' ) ) {
/**
* Load a plugin class when Composer's classmap does not know about it yet.
*
* @param string $class_name Fully qualified class name.
*/
function clawpress_autoload_class( string $class_name ): void {
foreach ( clawpress_autoload_class_file_candidates( $class_name ) as $file ) {
if ( is_readable( $file ) ) {
require_once $file;
return;
}
}
}
}

spl_autoload_register( __NAMESPACE__ . '\clawpress_autoload_class' );
84 changes: 49 additions & 35 deletions includes/commands/handlers/class-setup-command-handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use ClawPress\Commands\Command_Response;
use ClawPress\Helpers\Agent_File_Helper;
use ClawPress\Helpers\Model_Helper;
use ClawPress\Helpers\Model_Option_Helper;
use ClawPress\Helpers\Provider_Helper;
use ClawPress\Helpers\Settings_Helper;
use ClawPress\Helpers\User_Helper;
Expand Down Expand Up @@ -81,6 +82,13 @@ final class Setup_Command_Handler implements Command_Handler {
*/
private Model_Helper $model_helper;

/**
* Model option helper.
*
* @var Model_Option_Helper
*/
private Model_Option_Helper $model_option_helper;

/**
* User helper.
*
Expand Down Expand Up @@ -123,6 +131,7 @@ public function __construct(
$this->settings_helper = $settings_helper;
$this->provider_helper = $provider_helper;
$this->model_helper = $model_helper;
$this->model_option_helper = Model_Option_Helper::get_instance();
$this->user_helper = $user_helper;
$this->workspace_helper = $workspace_helper;
$this->agent_file_helper = $agent_file_helper;
Expand Down Expand Up @@ -1135,33 +1144,49 @@ private function generate_text_with_explicit_model_fallback(
RequestOptions $request_options,
array $generation_settings
): string {
$builder = $this->build_prompt_builder(
$prompt,
$provider,
$model,
$request_options,
$generation_settings,
false
);

try {
return $builder->generateText();
} catch ( Throwable $throwable ) {
if ( ! $this->should_retry_with_explicit_model( $throwable, $provider, $model ) ) {
throw $throwable;
}
$use_explicit_model = false;
$retried_unsupported_options = [];
$last_throwable = null;

$fallback_builder = $this->build_prompt_builder(
for ( $attempt = 0; $attempt < 8; $attempt++ ) {
$builder = $this->build_prompt_builder(
$prompt,
$provider,
$model,
$request_options,
$generation_settings,
true
$use_explicit_model
);

return $fallback_builder->generateText();
try {
return $builder->generateText();
} catch ( Throwable $throwable ) {
$last_throwable = $throwable;
$unsupported_option = $this->model_option_helper->record_unsupported_parameter_from_error(
$provider,
$model,
$throwable
);

if ( null !== $unsupported_option && ! in_array( $unsupported_option, $retried_unsupported_options, true ) ) {
$retried_unsupported_options[] = $unsupported_option;
continue;
}

if ( ! $use_explicit_model && $this->should_retry_with_explicit_model( $throwable, $provider, $model ) ) {
$use_explicit_model = true;
continue;
}

throw $throwable;
}
}

if ( $last_throwable instanceof Throwable ) {
throw $last_throwable;
}

throw new \RuntimeException( esc_html__( 'AI generation failed.', 'clawpress' ) );
}

/**
Expand Down Expand Up @@ -1259,26 +1284,15 @@ private function apply_generation_settings( object $builder, array $generation_s
/**
* Apply max output token setting to prompt builder.
*
* Uses `max_output_tokens` for OpenAI model families that reject
* legacy `max_tokens` in the Responses API.
*
* @param object $builder Prompt builder instance.
* @param int $max_output_tokens Max output tokens.
* @param string $provider Provider identifier.
* @param string $model Model identifier.
* @return object
*/
private function apply_max_output_tokens( object $builder, int $max_output_tokens, string $provider, string $model ): object {
if ( $this->provider_helper->should_use_max_output_tokens( $provider, $model ) && method_exists( $builder, 'usingModelConfig' ) ) {
$model_config = ModelConfig::fromArray(
[
ModelConfig::KEY_CUSTOM_OPTIONS => [
'max_output_tokens' => $max_output_tokens,
],
]
);

return $builder->usingModelConfig( $model_config );
if ( ! $this->model_option_helper->supports_generation_option( $provider, $model, 'max_output_tokens', $max_output_tokens ) ) {
return $builder;
}

return $builder->usingMaxTokens( $max_output_tokens );
Expand All @@ -1294,7 +1308,7 @@ private function apply_max_output_tokens( object $builder, int $max_output_token
* @return object
*/
private function apply_temperature( object $builder, float $temperature, string $provider, string $model ): object {
if ( ! $this->provider_helper->should_use_temperature( $provider, $model ) ) {
if ( ! $this->model_option_helper->supports_generation_option( $provider, $model, 'temperature', $temperature ) ) {
return $builder;
}

Expand All @@ -1311,7 +1325,7 @@ private function apply_temperature( object $builder, float $temperature, string
* @return object
*/
private function apply_top_p( object $builder, float $top_p, string $provider, string $model ): object {
if ( ! $this->provider_helper->should_use_top_p( $provider, $model ) ) {
if ( ! $this->model_option_helper->supports_generation_option( $provider, $model, 'top_p', $top_p ) ) {
return $builder;
}

Expand All @@ -1328,7 +1342,7 @@ private function apply_top_p( object $builder, float $top_p, string $provider, s
* @return object
*/
private function apply_frequency_penalty( object $builder, float $frequency_penalty, string $provider, string $model ): object {
if ( ! $this->provider_helper->should_use_frequency_penalty( $provider, $model ) ) {
if ( ! $this->model_option_helper->supports_generation_option( $provider, $model, 'frequency_penalty', $frequency_penalty ) ) {
return $builder;
}

Expand All @@ -1345,7 +1359,7 @@ private function apply_frequency_penalty( object $builder, float $frequency_pena
* @return object
*/
private function apply_presence_penalty( object $builder, float $presence_penalty, string $provider, string $model ): object {
if ( ! $this->provider_helper->should_use_presence_penalty( $provider, $model ) ) {
if ( ! $this->model_option_helper->supports_generation_option( $provider, $model, 'presence_penalty', $presence_penalty ) ) {
return $builder;
}

Expand Down
Loading
Loading