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.