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
43 changes: 42 additions & 1 deletion includes/Intelligence/Admin/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,53 @@
use WeDevs\Dokan\Intelligence\Services\ChatgptResponseService;
use WeDevs\Dokan\Intelligence\Services\GeminiResponseService;
use WeDevs\Dokan\Intelligence\Services\Model;
use WeDevs\Dokan\Intelligence\Services\Provider;

class Settings implements Hookable {
public function register_hooks(): void {
add_filter( 'dokan_settings_sections', [ $this, 'render_appearance_section' ] );
add_filter( 'dokan_settings_fields', [ $this, 'render_ai_settings' ] );
add_filter( 'dokan_rest_api_class_map', [ $this, 'rest_api_class_map' ] );
add_filter( 'dokan_get_settings_values', [ $this, 'remap_retired_model_values' ], 10, 2 );
}

/**
* Replace saved model ids that a provider has since retired.
*
* @since DOKAN_SINCE
*
* @param array $values Section setting values.
* @param string $section_id Section id.
*
* @return mixed Untouched when the section is not the AI section.
*/
public function remap_retired_model_values( $values, $section_id ) {
if ( 'dokan_ai' !== $section_id || ! is_array( $values ) ) {
return $values;
}

$manager = dokan_get_container()->get( Manager::class );

foreach ( [ Model::SUPPORTS_TEXT, Model::SUPPORTS_IMAGE ] as $type ) {
$prefix = $manager->get_type_prefix( $type );

foreach ( $manager->get_engines( $type ) as $provider_id => $provider ) {
$key = 'dokan_ai_' . $prefix . $provider_id . '_model';

if ( ! $provider instanceof Provider || empty( $values[ $key ] ) || ! is_scalar( $values[ $key ] ) ) {
continue;
}

$models = $provider->get_models_by_type( $type );

// A retired model id is not among the select options, so it would render blank.
if ( ! isset( $models[ $values[ $key ] ] ) ) {
$values[ $key ] = $provider->get_default_model_id_by_type( $type );
}
}
}

return $values;
}

/**
Expand Down Expand Up @@ -90,7 +131,7 @@ public function render_ai_settings( array $settings_fields ): array {
'type' => 'select',
'options' => array_map( fn( $model ) => $model->get_title(), $provider->get_models_by_type( Model::SUPPORTS_TEXT ) ),
'desc' => __( 'More advanced models provide higher quality output but may cost more per generation.', 'dokan-lite' ),
'default' => $provider->get_default_model_id(),
'default' => $provider instanceof Provider ? $provider->get_default_model_id_by_type( Model::SUPPORTS_TEXT ) : '',
'is_lite' => true,
'show_if' => [
'dokan_ai_engine' => [
Expand Down
82 changes: 62 additions & 20 deletions includes/Intelligence/REST/AIRequestController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

namespace WeDevs\Dokan\Intelligence\REST;

use Exception;
use Throwable;
use WeDevs\Dokan\Intelligence\Manager;
use WeDevs\Dokan\Intelligence\Services\AIModelInterface;
use WeDevs\Dokan\Intelligence\Services\AIProviderInterface;
use WeDevs\Dokan\Intelligence\Services\Model;
use WeDevs\Dokan\Intelligence\Services\Provider;
use WeDevs\Dokan\Intelligence\Utils\PromptUtils;
use WeDevs\Dokan\REST\DokanBaseVendorController;
use WP_Error;
Expand Down Expand Up @@ -67,45 +70,71 @@ public function get_request_args(): array {

public function handle_request( $request ) {
$prompt = $request->get_param( 'prompt' );
$args = wp_parse_args(
$request->get_param( 'payload' ), [
$args = wp_parse_args(
(array) $request->get_param( 'payload' ), [
'field' => '',
'type' => Model::SUPPORTS_TEXT,
]
);
$type = $args['type'];

// The payload schema declares no properties, so coerce before any string API touches these.
$type = is_scalar( $args['type'] ) ? sanitize_key( (string) $args['type'] ) : '';
$type = '' !== $type ? $type : Model::SUPPORTS_TEXT;

$args['type'] = $type;
$args['field'] = is_scalar( $args['field'] ) ? (string) $args['field'] : '';

// Resolve the appropriate service based on the AI engine.
try {
$manager = dokan_get_container()->get( Manager::class );
$prefix = $type === Model::SUPPORTS_TEXT ? '' : sanitize_key( $type ) . '_';
$provider_id = dokan_get_option( 'dokan_ai_' . $prefix . 'engine', 'dokan_ai', '' );
$manager = dokan_get_container()->get( Manager::class );

// Image generation is billed to the marketplace owner, so the admin toggle must hold at the API too.
if ( Model::SUPPORTS_IMAGE === $type && 'on' !== dokan_get_option( 'dokan_ai_image_gen_availability', 'dokan_ai', 'off' ) ) {
return $this->configuration_error( __( 'AI image generation is not enabled for this marketplace.', 'dokan-lite' ) );
}

// active_engine() falls back to openai, so emptiness alone no longer proves a working setup.
if ( ! $manager->is_configured( $type ) ) {
return $this->configuration_error( __( 'AI is not configured for this marketplace. Kindly reach out to Marketplace Owner.', 'dokan-lite' ) );
}

$prefix = $manager->get_type_prefix( $type );
$provider_id = $manager->active_engine( $type );
$model_id = dokan_get_option( 'dokan_ai_' . $prefix . $provider_id . '_model', 'dokan_ai', '' );
$model_id = is_scalar( $model_id ) ? (string) $model_id : '';
$provider = $manager->get_provider( $provider_id );

if ( ! $provider instanceof AIProviderInterface ) {
return $this->configuration_error( __( 'The configured AI provider is no longer available. Kindly reach out to Marketplace Owner.', 'dokan-lite' ) );
}

// A model id saved before the provider retired that model must not break generation.
$model = $provider instanceof Provider
? $provider->resolve_model( $type, $model_id )
: $provider->get_model( $model_id );

if ( empty( $provider_id ) || empty( $model_id ) ) {
throw new Exception(
esc_html__( 'AI provider or model is not configured properly.', 'dokan-lite' ),
400
if ( ! $model instanceof AIModelInterface ) {
return $this->configuration_error(
// translators: %s: type of generation.
sprintf( __( 'No AI model is available for %s generation. Kindly reach out to Marketplace Owner.', 'dokan-lite' ), $type )
);
}

$provider = $manager->get_provider( $provider_id );
$model = $provider->get_model( $model_id );
$prompt = PromptUtils::prepare_prompt( $args['field'], $prompt );
$process_call = 'process_' . sanitize_key( $type );
$process_call = 'process_' . $type;

if ( ! method_exists( $model, $process_call ) ) {
throw new Exception(
return $this->configuration_error(
// translators: %s: type of generation.
sprintf( esc_html__( 'Model does not support %s generation.', 'dokan-lite' ), $type ),
400
sprintf( __( 'Model does not support %s generation.', 'dokan-lite' ), $type )
);
}

$response = $model->{$process_call}( $prompt, $args );
$response = $model->{$process_call}( PromptUtils::prepare_prompt( $args['field'], $prompt ), $args );

return rest_ensure_response( $response );
} catch ( Exception $e ) {
} catch ( Throwable $e ) {
dokan_log( 'AI generation request failed: ' . $e->getMessage(), 'error' );

if ( in_array( $e->getCode(), [ 401, 429 ], true ) ) {
return new WP_Error(
'dokan_ai_service_error',
Expand All @@ -120,4 +149,17 @@ public function handle_request( $request ) {
);
}
}

/**
* Build a misconfiguration error response.
*
* @since DOKAN_SINCE
*
* @param string $message Human readable reason.
*
* @return WP_Error
*/
protected function configuration_error( string $message ): WP_Error {
return new WP_Error( 'dokan_ai_not_configured', esc_html( $message ), [ 'status' => 400 ] );
}
}
43 changes: 43 additions & 0 deletions includes/Intelligence/Services/Provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,49 @@ public function get_default_model(): ?AIModelInterface {

abstract public function get_default_model_id(): string;

/**
* Resolve the model to use for a generation type, tolerating stale saved ids.
*
* @since DOKAN_SINCE
*
* @param string $type The type of generation (e.g., 'text', 'image').
* @param string $model_id Optional. Saved model id to prefer.
*
* @return AIModelInterface|null
*/
public function resolve_model( string $type, string $model_id = '' ): ?AIModelInterface {
// Keyed by model id, so every lookup below is an array read rather than another filter dispatch.
$models = $this->get_models_by_type( $type );

if ( '' !== $model_id && isset( $models[ $model_id ] ) ) {
return $models[ $model_id ];
}

// A model id saved before the provider retired that model must not break generation.
$default_id = $this->get_default_model_id();

if ( isset( $models[ $default_id ] ) ) {
return $models[ $default_id ];
}

return ! empty( $models ) ? reset( $models ) : null;
}

/**
* Get the default model id for a generation type.
*
* @since DOKAN_SINCE
*
* @param string $type The type of generation (e.g., 'text', 'image').
*
* @return string Empty string when the provider has no model of that type.
*/
public function get_default_model_id_by_type( string $type ): string {
$model = $this->resolve_model( $type );

return $model instanceof AIModelInterface ? $model->get_id() : '';
}

/**
* @inheritDoc
*/
Expand Down
2 changes: 1 addition & 1 deletion includes/Intelligence/Services/Providers/Gemini.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function get_description(): string {
}

public function get_default_model_id(): string {
return 'gemini-2.0-flash';
return 'gemini-2.5-flash';
}


Expand Down
19 changes: 7 additions & 12 deletions src/admin/pages/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -308,21 +308,16 @@
if ( resp.success ) {

Object.keys( self.settingFields ).forEach( function( section, index ) {
Object.keys( self.settingFields[section] ).forEach( function( field, i ) {
// Copy the saved section once up front — re-assigning it inside the loop wiped defaults already applied to earlier fields.
self.settingValues[section] = jQuery.extend( {}, resp.data[section] );

if (!self.settingValues[section]) {
self.settingValues[section] = {};
Object.keys( self.settingFields[section] ).forEach( function( field ) {
if ( typeof( self.settingValues[section][field] ) !== 'undefined' ) {
return;
}

if ( typeof( resp.data[section][field] ) === 'undefined' ) {
if ( typeof( self.settingFields[section][field].default ) === 'undefined' ) {
self.settingValues[section][field] = '';
} else {
self.settingValues[section][field] = self.settingFields[section][field].default;
}
} else {
self.settingValues[section] = resp.data[section];
}
var fieldDefault = self.settingFields[section][field].default;
self.settingValues[section][field] = typeof fieldDefault === 'undefined' ? '' : fieldDefault;
});
});

Expand Down
Loading