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 = $(
''
);
@@ -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.