Files
prautoblogger/includes/admin/class-image-model-registry.php
peptiderepo def070d15a feat: Live Runware model catalog sync — v0.15.0
Implements daily WP-Cron sync from Runware's /v1/models endpoint with 24h TTL caching,
on-demand AJAX refresh button in PRAdmin (Images settings), smart fallback logic, and
pricing merge from the authoritative cost table.

## Key additions:
- class-runware-model-catalog.php: ~300 lines, fetches/normalizes/caches models,
  exposes sync()/get_models()/get_last_synced_at()/is_stale() public API
- AJAX handler: prautoblogger_sync_runware_models_now with nonce + manage_options check
- Daily cron registered on activation, unscheduled on deactivation
- Caching strategy: <24h fresh → use cache; stale → sync; sync fails → fallback
- Pricing merge: costs from class-runware-image-pricing.php injected on every sync
- Fallback never returns empty array (hardcoded 15-model list)
- Unit tests: sync success/failure, stale cache, fallback behavior, pricing merge

## File changes:
- class-image-model-registry.php: refactored to delegate Runware to catalog,
  extracted get_runware_fallback_models() and get_openrouter_models()
- class-settings-fields-extended.php: added runware_catalog_sync field type
  for status display + Sync button in Images settings section
- class-prautoblogger.php: registered daily cron + AJAX hook
- class-activator.php: schedule prautoblogger_sync_runware_models on activation
- class-deactivator.php: unschedule on deactivation
- class-ajax-handlers.php: added on_ajax_sync_runware_models_now() handler
- class-runware-image-pricing.php: added @see cross-reference to catalog
- ARCHITECTURE.md: updated Runware row in External API Integrations table
- prautoblogger.php: version bump to 0.15.0
- CHANGELOG.md: detailed entry describing the fix and why it was necessary

Fixes the pattern from v0.13.8 and v0.14.0 where hardcoded Runware model
expansion without pricing class updates led to silent fallback-to-schnell bugs.
2026-04-26 04:27:17 +00:00

265 lines
10 KiB
PHP

<?php
declare(strict_types=1);
/**
* phpcs:ignore WordPress.Files.FileName.InvalidClassFileName -- class naming convention differs from WordPress standard
*
* Static registry of available image generation models.
*
* What: Returns a merged list of Runware models (from live catalog sync) and
* OpenRouter models (static list). No API discovery for OpenRouter — updated
* manually when new models are available.
* As of 2026-04-26, registry includes 15 Runware models (live) + 6 OpenRouter
* models (static), ordered cheapest-to-most-expensive within each provider.
* Who calls it: PRAutoBlogger_Admin_Page (model picker UI + save-time provider
* derivation) and PRAutoBlogger_Image_Pipeline (provider lookup).
* Dependencies: PRAutoBlogger_Runware_Model_Catalog (live Runware models),
* hardcoded fallback lists for resilience.
*
* @see admin/class-settings-fields-extended.php — Image settings reference this registry.
* @see core/class-image-pipeline.php — Derives provider from picked model.
* @see providers/class-runware-image-provider.php — Runware FLUX backend.
* @see providers/class-runware-model-catalog.php — Live model sync and caching.
*/
class PRAutoBlogger_Image_Model_Registry {
/**
* Get all available image generation models (Runware live + OpenRouter static).
*
* Each entry contains:
* - id: Model identifier used in API calls.
* - name: Human-readable display name.
* - provider: 'runware' | 'openrouter'.
* - cost_per_image: Estimated USD cost per generation.
* - capabilities: Array of capability tags.
* - description: Short description for the admin UI.
*
* Ordered cheapest-to-most-expensive within each provider, with the
* recommended default (Runware schnell) at the top.
*
* @return array<int, array<string, mixed>>
*/
public static function get_models(): array {
$catalog = new PRAutoBlogger_Runware_Model_Catalog();
$runware_models = $catalog->get_models();
$openrouter_models = self::get_openrouter_models();
return array_merge( $runware_models, $openrouter_models );
}
/**
* Return the provider id for a known model, or empty string if the
* model id is not in the registry.
*
* @param string $model_id Model slug from the admin UI.
* @return string Provider id ('runware' | 'openrouter' | '').
*/
public static function provider_for( string $model_id ): string {
foreach ( self::get_models() as $model ) {
if ( ( $model['id'] ?? '' ) === $model_id ) {
return (string) ( $model['provider'] ?? '' );
}
}
return '';
}
/**
* Hardcoded Runware model list used as fallback when the live catalog
* sync fails or API is unreachable. Kept in sync with actual Runware
* pricing page (runware.ai/pricing). This is the source of truth for
* pricing data when the Runware API doesn't expose cost information.
*
* Last verified: 2026-04-26. To update: check runware.ai/pricing for new
* text-to-image models and their costs.
*
* @return array<int, array<string, mixed>>
*/
public static function get_runware_fallback_models(): array {
return array(
array(
'id' => 'runware:100@1',
'name' => 'FLUX.1 schnell (Runware)',
'provider' => 'runware',
'cost_per_image' => 0.0006,
'capabilities' => array( 'image_generation' ),
'description' => __( 'Default. Fast, very cheap, comic-friendly fidelity.', 'prautoblogger' ),
),
array(
'id' => 'runware:twinflow-z-image-turbo@0',
'name' => 'TwinFlow Z-Image-Turbo (Runware)',
'provider' => 'runware',
'cost_per_image' => 0.0006,
'capabilities' => array( 'image_generation' ),
'description' => __( 'Fast text-to-image, same cost as schnell.', 'prautoblogger' ),
),
array(
'id' => 'runware:400@2',
'name' => 'FLUX.2 klein 9B (Runware)',
'provider' => 'runware',
'cost_per_image' => 0.00078,
'capabilities' => array( 'image_generation' ),
'description' => __( 'FLUX.2 compact 9B. Stronger than schnell at similar cost.', 'prautoblogger' ),
),
array(
'id' => 'runware:400@4',
'name' => 'FLUX.2 klein 4B (Runware)',
'provider' => 'runware',
'cost_per_image' => 0.0006,
'capabilities' => array( 'image_generation' ),
'description' => __( 'FLUX.2 compact 4B. Newer architecture at schnell price.', 'prautoblogger' ),
),
array(
'id' => 'runware:5@1',
'name' => 'Stable Diffusion 3 (Runware)',
'provider' => 'runware',
'cost_per_image' => 0.0013,
'capabilities' => array( 'image_generation' ),
'description' => __( 'Stable Diffusion 3. Sharp text, complex scenes.', 'prautoblogger' ),
),
array(
'id' => 'runware:z-image@turbo',
'name' => 'Z-Image Turbo (Runware)',
'provider' => 'runware',
'cost_per_image' => 0.0013,
'capabilities' => array( 'image_generation' ),
'description' => __( 'Fast photorealistic generation.', 'prautoblogger' ),
),
array(
'id' => 'runware:101@1',
'name' => 'FLUX.1 dev (Runware)',
'provider' => 'runware',
'cost_per_image' => 0.02,
'capabilities' => array( 'image_generation' ),
'description' => __( 'Higher fidelity FLUX.1. ~30x schnell cost.', 'prautoblogger' ),
),
array(
'id' => 'runware:97@3',
'name' => 'HiDream-I1 Fast (Runware)',
'provider' => 'runware',
'cost_per_image' => 0.0038,
'capabilities' => array( 'image_generation' ),
'description' => __( 'HiDream-I1 Fast. Low latency, 17B quality.', 'prautoblogger' ),
),
array(
'id' => 'runware:97@2',
'name' => 'HiDream-I1 Dev (Runware)',
'provider' => 'runware',
'cost_per_image' => 0.0045,
'capabilities' => array( 'image_generation' ),
'description' => __( 'HiDream-I1 Dev. Strong prompt alignment.', 'prautoblogger' ),
),
array(
'id' => 'runware:z-image@0',
'name' => 'Z-Image (Runware)',
'provider' => 'runware',
'cost_per_image' => 0.0045,
'capabilities' => array( 'image_generation' ),
'description' => __( 'High-quality Z-Image foundation model.', 'prautoblogger' ),
),
array(
'id' => 'runware:400@1',
'name' => 'FLUX.2 dev (Runware)',
'provider' => 'runware',
'cost_per_image' => 0.0051,
'capabilities' => array( 'image_generation' ),
'description' => __( 'FLUX.2 dev. Controllable open text-to-image. From $0.0051.', 'prautoblogger' ),
),
array(
'id' => 'runware:108@1',
'name' => 'Qwen-Image (Runware)',
'provider' => 'runware',
'cost_per_image' => 0.0058,
'capabilities' => array( 'image_generation' ),
'description' => __( 'Alibaba Qwen-Image. Strong text rendering in generated images.', 'prautoblogger' ),
),
array(
'id' => 'runware:97@1',
'name' => 'HiDream-I1 Full (Runware)',
'provider' => 'runware',
'cost_per_image' => 0.009,
'capabilities' => array( 'image_generation' ),
'description' => __( 'HiDream-I1 Full. 17B high-fidelity, LoRA support.', 'prautoblogger' ),
),
array(
'id' => 'runware:107@1',
'name' => 'FLUX.1 Krea dev (Runware)',
'provider' => 'runware',
'cost_per_image' => 0.0098,
'capabilities' => array( 'image_generation' ),
'description' => __( 'FLUX.1 Krea Dev. Photorealistic open-weight generation.', 'prautoblogger' ),
),
array(
'id' => 'runware:glm-image@0',
'name' => 'GLM-Image (Runware)',
'provider' => 'runware',
'cost_per_image' => 0.0225,
'capabilities' => array( 'image_generation' ),
'description' => __( 'GLM-Image. Hybrid autoregressive+diffusion, excellent text rendering.', 'prautoblogger' ),
),
);
}
/**
* Hardcoded OpenRouter image model list. Left static for now because
* OpenRouter's catalog is large (hundreds of models) with noisy filtering.
* Future iteration will add smart filtering strategy (output_modalities,
* pricing, rate limits).
*
* Last verified: 2026-04-26. To update: check openrouter.ai/api/v1/models
* filtering to output_modalities containing 'image'.
*
* @return array<int, array<string, mixed>>
*/
private static function get_openrouter_models(): array {
return array(
array(
'id' => 'google/gemini-2.5-flash-image',
'name' => 'Gemini 2.5 Flash Image (OpenRouter)',
'provider' => 'openrouter',
'cost_per_image' => 0.005,
'capabilities' => array( 'image_generation' ),
'description' => __( 'Mid-tier quality, proven.', 'prautoblogger' ),
),
array(
'id' => 'google/gemini-3.1-flash-image-preview',
'name' => 'Gemini 3.1 Flash Image (OpenRouter)',
'provider' => 'openrouter',
'cost_per_image' => 0.008,
'capabilities' => array( 'image_generation' ),
'description' => __( 'Latest Google Flash image. Better quality.', 'prautoblogger' ),
),
array(
'id' => 'openai/gpt-5-image-mini',
'name' => 'GPT-5 Image Mini (OpenRouter)',
'provider' => 'openrouter',
'cost_per_image' => 0.02,
'capabilities' => array( 'image_generation' ),
'description' => __( 'OpenAI budget image model.', 'prautoblogger' ),
),
array(
'id' => 'openai/gpt-5.4-image-2',
'name' => 'GPT-5.4 Image 2 (OpenRouter)',
'provider' => 'openrouter',
'cost_per_image' => 0.02,
'capabilities' => array( 'image_generation' ),
'description' => __( 'OpenAI GPT-5.4 image generation.', 'prautoblogger' ),
),
array(
'id' => 'google/gemini-3-pro-image-preview',
'name' => 'Gemini 3 Pro Image (OpenRouter)',
'provider' => 'openrouter',
'cost_per_image' => 0.03,
'capabilities' => array( 'image_generation' ),
'description' => __( 'Highest quality Google image. Premium.', 'prautoblogger' ),
),
array(
'id' => 'openai/gpt-5-image',
'name' => 'GPT-5 Image (OpenRouter)',
'provider' => 'openrouter',
'cost_per_image' => 0.08,
'capabilities' => array( 'image_generation' ),
'description' => __( 'OpenAI premium. Photorealistic.', 'prautoblogger' ),
),
);
}
}