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 <noreply@anthropic.com>
This commit is contained in:
peptiderepo
2026-04-18 03:06:17 +07:00
parent 31f303a831
commit 1c1e861f2e
4 changed files with 100 additions and 21 deletions

View File

@@ -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 ? '<span class="ab-mp-ctx" title="Details">' + escHtml(model.description) + '</span>' : '';
} else {
priceHtml = formatPrice(model.input_price_per_m) + ' / ' + formatPrice(model.output_price_per_m);
priceTitle = 'Input / Output per 1M tokens';
metaRight = '<span class="ab-mp-ctx" title="Context window">' + formatContext(model.context_length) + '</span>';
}
return (
'<div class="ab-mp-row' + deprecated + '" data-model-id="' + escAttr(model.id) + '">' +
'<div class="ab-mp-row-main">' +
@@ -113,10 +132,8 @@
(model.deprecated ? '<span class="ab-mp-dep-badge">deprecated</span>' : '') +
'</div>' +
'<div class="ab-mp-row-meta">' +
'<span class="ab-mp-price" title="Input / Output per 1M tokens">' +
formatPrice(model.input_price_per_m) + ' / ' + formatPrice(model.output_price_per_m) +
'</span>' +
'<span class="ab-mp-ctx" title="Context window">' + formatContext(model.context_length) + '</span>' +
'<span class="ab-mp-price" title="' + priceTitle + '">' + priceHtml + '</span>' +
metaRight +
'</div>' +
'</div>'
);
@@ -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 = $('<div class="ab-mp-overlay"></div>');
var $popup = $(
'<div class="ab-mp-popup">' +
@@ -156,7 +176,7 @@
'<div class="ab-mp-list"><div class="ab-mp-loading">Loading models…</div></div>' +
'<div class="ab-mp-footer">' +
'<span class="ab-mp-count"></span>' +
'<span class="ab-mp-price-label">Prices per 1M tokens (in / out)</span>' +
'<span class="ab-mp-price-label">' + priceLabel + '</span>' +
'</div>' +
'</div>'
);
@@ -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();
});

View File

@@ -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' ),
],
],
] );
}

View File

@@ -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' ),
],

View File

@@ -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<string, mixed> $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<string, array{name: string, cost_per_image: string}>
*/
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.