From 1c1e861f2e796dce4851abcdf221193ba8e7cba4 Mon Sep 17 00:00:00 2001 From: peptiderepo Date: Sat, 18 Apr 2026 03:06:17 +0700 Subject: [PATCH] feat: extend model picker to image model (Cloudflare FLUX) Apply the same interactive model picker UI to the image model field. Since Cloudflare models don't come from OpenRouter's registry, the two FLUX models are embedded as static data in the page config and the JS popup renders cost-per-image instead of token pricing. Co-Authored-By: Claude Opus 4.6 --- assets/js/model-picker.js | 39 +++++++++++---- includes/admin/class-admin-page.php | 27 +++++++++++ .../admin/class-settings-fields-extended.php | 7 +-- .../fields/class-open-router-model-field.php | 48 +++++++++++++++---- 4 files changed, 100 insertions(+), 21 deletions(-) diff --git a/assets/js/model-picker.js b/assets/js/model-picker.js index ad25e2c..facc29f 100644 --- a/assets/js/model-picker.js +++ b/assets/js/model-picker.js @@ -22,6 +22,12 @@ * @return {jQuery.Deferred} Resolves with model array. */ function fetchModels(capability) { + // Cloudflare image models are embedded in the page config — no AJAX needed. + if (capability === 'cloudflare_image') { + var cfModels = config.cloudflareModels || []; + return $.Deferred().resolve(cfModels); + } + if (cachedModels) { return $.Deferred().resolve(filterModels(cachedModels, capability)); } @@ -105,6 +111,19 @@ */ function renderModelRow(model) { var deprecated = model.deprecated ? ' ab-mp-deprecated' : ''; + var isImageModel = model.cost_per_image !== undefined && model.cost_per_image > 0; + var priceHtml, priceTitle, metaRight; + + if (isImageModel) { + priceHtml = '$' + model.cost_per_image.toFixed(4) + '/image'; + priceTitle = 'Cost per generated image'; + metaRight = model.description ? '' + escHtml(model.description) + '' : ''; + } else { + priceHtml = formatPrice(model.input_price_per_m) + ' / ' + formatPrice(model.output_price_per_m); + priceTitle = 'Input / Output per 1M tokens'; + metaRight = '' + formatContext(model.context_length) + ''; + } + return ( '
' + '
' + @@ -113,10 +132,8 @@ (model.deprecated ? 'deprecated' : '') + '
' + '
' + - '' + - formatPrice(model.input_price_per_m) + ' / ' + formatPrice(model.output_price_per_m) + - '' + - '' + formatContext(model.context_length) + '' + + '' + priceHtml + '' + + metaRight + '
' + '
' ); @@ -143,6 +160,9 @@ var capability = $trigger.data('capability') || ''; // Build overlay + popup structure. + var isCloudflare = capability === 'cloudflare_image'; + var priceLabel = isCloudflare ? 'Cost per generated image' : 'Prices per 1M tokens (in / out)'; + var $overlay = $('
'); var $popup = $( '
' + @@ -156,7 +176,7 @@ '
Loading models…
' + '' + '
' ); @@ -230,9 +250,12 @@ // Update hidden input + display. $('#' + fieldId).val(model.id); $trigger.find('.ab-mp-display-name').text(model.name || model.id); - $trigger.find('.ab-mp-display-price').text( - formatPrice(model.input_price_per_m) + ' / ' + formatPrice(model.output_price_per_m) - ); + + var isImageModel = model.cost_per_image !== undefined && model.cost_per_image > 0; + var priceText = isImageModel + ? '$' + model.cost_per_image.toFixed(4) + '/image' + : formatPrice(model.input_price_per_m) + ' / ' + formatPrice(model.output_price_per_m); + $trigger.find('.ab-mp-display-price').text(priceText); closePicker(); }); diff --git a/includes/admin/class-admin-page.php b/includes/admin/class-admin-page.php index f2bd518..e5a1ebc 100755 --- a/includes/admin/class-admin-page.php +++ b/includes/admin/class-admin-page.php @@ -80,6 +80,33 @@ class PRAutoBlogger_Admin_Page { 'generateText' => __( 'Generate Now', 'prautoblogger' ), 'testingText' => __( 'Testing...', 'prautoblogger' ), 'testText' => __( 'Test Connections', 'prautoblogger' ), + // Static Cloudflare image models for the picker (no registry API for these). + 'cloudflareModels' => [ + [ + 'id' => 'flux-1-schnell', + 'name' => 'FLUX.1 [schnell]', + 'provider' => 'cloudflare', + 'context_length' => 0, + 'input_price_per_m' => 0, + 'output_price_per_m' => 0, + 'cost_per_image' => 0.0007, + 'capabilities' => [ 'cloudflare_image' ], + 'deprecated' => false, + 'description' => __( 'Fast + cheap (4 steps). Default choice.', 'prautoblogger' ), + ], + [ + 'id' => 'flux-1-dev', + 'name' => 'FLUX.1 [dev]', + 'provider' => 'cloudflare', + 'context_length' => 0, + 'input_price_per_m' => 0, + 'output_price_per_m' => 0, + 'cost_per_image' => 0.0028, + 'capabilities' => [ 'cloudflare_image' ], + 'deprecated' => false, + 'description' => __( 'Higher quality (20 steps), ~4× cost.', 'prautoblogger' ), + ], + ], ] ); } diff --git a/includes/admin/class-settings-fields-extended.php b/includes/admin/class-settings-fields-extended.php index d88e91e..4178c2c 100644 --- a/includes/admin/class-settings-fields-extended.php +++ b/includes/admin/class-settings-fields-extended.php @@ -134,13 +134,10 @@ class PRAutoBlogger_Settings_Fields_Extended { [ 'id' => 'prautoblogger_image_model', 'label' => __( 'Image Model', 'prautoblogger' ), - 'type' => 'select', + 'type' => 'model_select', 'section' => 'prautoblogger_images', 'default' => PRAUTOBLOGGER_DEFAULT_IMAGE_MODEL, - 'options' => [ - 'flux-1-schnell' => __( 'FLUX.1 [schnell] — fast + cheap (default)', 'prautoblogger' ), - 'flux-1-dev' => __( 'FLUX.1 [dev] — higher quality, ~4× cost', 'prautoblogger' ), - ], + 'capability' => 'cloudflare_image', 'description' => __( 'Schnell is the normal choice. Switch to [dev] for higher quality.', 'prautoblogger' ), 'badge' => __( 'Low cost', 'prautoblogger' ), ], diff --git a/includes/admin/fields/class-open-router-model-field.php b/includes/admin/fields/class-open-router-model-field.php index b3d9fbb..3efcba5 100644 --- a/includes/admin/fields/class-open-router-model-field.php +++ b/includes/admin/fields/class-open-router-model-field.php @@ -23,19 +23,51 @@ class PRAutoBlogger_OpenRouter_Model_Field { * @param string $value Current model id (e.g. 'google/gemini-2.5-flash-lite'). * @param array $args Field definition from settings fields. */ + /** + * Static Cloudflare Workers AI model definitions. + * + * These don't come from an API registry — Cloudflare has only two FLUX + * models and pricing is stable. Costs are per-image at 1024×632 (default). + * + * @var array + */ + private const CLOUDFLARE_MODELS = [ + 'flux-1-schnell' => [ + 'name' => 'FLUX.1 [schnell]', + 'provider' => 'Cloudflare', + 'cost_per_image' => '$0.0007', + 'description' => 'Fast + cheap, 4 inference steps', + ], + 'flux-1-dev' => [ + 'name' => 'FLUX.1 [dev]', + 'provider' => 'Cloudflare', + 'cost_per_image' => '$0.0028', + 'description' => 'Higher quality, ~4× cost, 20 steps', + ], + ]; + public static function render( string $id, string $value, array $args ): void { $capability = $args['capability'] ?? 'text→text'; $display_name = $value ?: __( '— Select a model —', 'prautoblogger' ); $display_price = ''; - // Try to resolve display name + price from the cached registry. - $registry = prautoblogger()->get_executor()->get_model_registry(); - $model = $registry->find_model( $value ); - if ( null !== $model ) { - $display_name = $model['name'] ?? $value; - $in = (float) ( $model['input_price_per_m'] ?? 0 ); - $out = (float) ( $model['output_price_per_m'] ?? 0 ); - $display_price = self::format_price( $in ) . ' / ' . self::format_price( $out ); + if ( 'cloudflare_image' === $capability ) { + // Resolve from static Cloudflare model list. + $cf_model = self::CLOUDFLARE_MODELS[ $value ] ?? null; + if ( null !== $cf_model ) { + $display_name = $cf_model['name']; + $display_price = $cf_model['cost_per_image'] . '/image'; + } + } else { + // Resolve from the OpenRouter model registry cache. + $registry = prautoblogger()->get_executor()->get_model_registry(); + $model = $registry->find_model( $value ); + if ( null !== $model ) { + $display_name = $model['name'] ?? $value; + $in = (float) ( $model['input_price_per_m'] ?? 0 ); + $out = (float) ( $model['output_price_per_m'] ?? 0 ); + $display_price = self::format_price( $in ) . ' / ' . self::format_price( $out ); + } } // Hidden input carries the actual value for the form submission.