First commit of the image + Instagram A/B pipeline workstream. Ships the Cloudflare Workers AI image provider, its pricing + validator helpers, four new settings fields in a new "Images" admin section, and unit tests with mocked HTTP. Nothing calls the provider yet from the article pipeline — integration lands in commit 1b. Why: CEO-approved on 2026-04-15 (see convo/prautoblogger/threads/2026-04-image-pipeline/01-cto-handoff.md). Style is locked to the 90s infomercial pastiche; default model is FLUX.1 [schnell] at ~$0.0011/MP (~$0.01/article for 8 images across 4 placements x 2 variants). Risk flags: - Direct call to Cloudflare Workers AI bypassing AI Gateway (decision D-001, documented in convo/prautoblogger/decisions/). Gateway route to Workers AI currently 403s; OpenRouter gateway path unaffected. - prab_event_log structured writes deferred to commit 1b when the table lands (decision D-002). Logger-based logging is in place now. - No schema changes. No existing code paths touched outside settings. - Backward compatible: provider is instantiable but not wired to any consumer yet. Test plan: - CI runs PHPUnit including the new CloudflareImageProviderTest. - After merge: verify the new Images section appears on the settings page, the style-suffix textarea pre-fills with the infomercial prompt, and entering the existing CLOUDFLARE_AI_TOKEN + account UUID lets a future "Test Connection" button return OK (button ships in a later commit; the underlying validate_credentials_detailed() works today). Files: - includes/providers/interface-image-provider.php (adopted from CTO draft) - includes/providers/class-cloudflare-image-provider.php (283 lines) - includes/providers/class-cloudflare-image-pricing.php (78 lines) - includes/providers/class-cloudflare-image-validator.php (107 lines) - includes/admin/class-settings-fields.php (+ Images section, 4 fields) - prautoblogger.php (+ PRAUTOBLOGGER_DEFAULT_IMAGE_MODEL, _STYLE_SUFFIX) - tests/unit/Providers/CloudflareImageProviderTest.php - ARCHITECTURE.md (file tree, integrations table, options table, decision #16) - CONVENTIONS.md (How To: Add a New Image Provider) - CHANGELOG.md (Unreleased section) Agent-Session: cowork-2026-04-15-image-pipeline-commit-1a
79 lines
2.8 KiB
PHP
79 lines
2.8 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Pricing + model-id resolution helpers for the Cloudflare image provider.
|
|
*
|
|
* Kept separate from the provider so the provider stays under the 300-line
|
|
* cap and so cost estimation can be unit-tested without spinning up HTTP
|
|
* mocks. Mirrors the `class-open-router-pricing.php` split.
|
|
*
|
|
* Triggered by: PRAutoBlogger_Cloudflare_Image_Provider (both generate_image()
|
|
* and estimate_cost() delegate here).
|
|
* Dependencies: None — pure calculation + option reads.
|
|
*
|
|
* @see class-cloudflare-image-provider.php — Caller.
|
|
* @see class-open-router-pricing.php — Sibling pattern for LLM pricing.
|
|
*/
|
|
class PRAutoBlogger_Cloudflare_Image_Pricing {
|
|
|
|
/**
|
|
* Cloudflare Workers AI FLUX pricing (USD per megapixel), April 2026.
|
|
* Single source of truth — update here if Cloudflare repricies.
|
|
*/
|
|
private const COST_PER_MP_SCHNELL_USD = 0.0011;
|
|
private const COST_MULTIPLIER_DEV = 4.0;
|
|
|
|
/**
|
|
* Short-alias → fully-qualified Workers AI model id.
|
|
*
|
|
* @var array<string, string>
|
|
*/
|
|
private const MODEL_ALIAS_MAP = [
|
|
'flux-1-schnell' => '@cf/black-forest-labs/flux-1-schnell',
|
|
'flux-1-dev' => '@cf/black-forest-labs/flux-1-dev',
|
|
];
|
|
|
|
/**
|
|
* Normalize a caller's model hint to a fully-qualified Workers AI id.
|
|
*
|
|
* Accepts short aliases (`flux-1-schnell`), fully-qualified names
|
|
* (`@cf/black-forest-labs/...`), or empty. Empty input falls back to
|
|
* the site option, then to the hardcoded default.
|
|
*
|
|
* @param string $hint Caller-supplied model identifier (may be empty).
|
|
* @return string Fully-qualified Workers AI model id suitable for the URL path.
|
|
*/
|
|
public function resolve_model( string $hint ): string {
|
|
$hint = trim( $hint );
|
|
if ( '' === $hint ) {
|
|
$hint = (string) get_option( 'prautoblogger_image_model', PRAUTOBLOGGER_DEFAULT_IMAGE_MODEL );
|
|
}
|
|
if ( 0 === strpos( $hint, '@cf/' ) ) {
|
|
return $hint;
|
|
}
|
|
return self::MODEL_ALIAS_MAP[ $hint ] ?? self::MODEL_ALIAS_MAP['flux-1-schnell'];
|
|
}
|
|
|
|
/**
|
|
* Estimate USD cost for a single image at the given dimensions.
|
|
*
|
|
* FLUX is priced per megapixel of output. Schnell is the baseline;
|
|
* [dev] is a ~4x multiplier. We clamp the MP floor at 0.01 so a
|
|
* tiny thumbnail doesn't round to zero and bypass the budget check.
|
|
*
|
|
* @param int $width Width in pixels.
|
|
* @param int $height Height in pixels.
|
|
* @param string $model Fully-qualified Workers AI model id (call resolve_model first).
|
|
* @return float USD cost rounded to 6 decimals.
|
|
*/
|
|
public function estimate_cost( int $width, int $height, string $model ): float {
|
|
$mp = max( 0.01, ( $width * $height ) / 1_000_000 );
|
|
$rate = self::COST_PER_MP_SCHNELL_USD;
|
|
if ( false !== strpos( $model, 'flux-1-dev' ) ) {
|
|
$rate *= self::COST_MULTIPLIER_DEV;
|
|
}
|
|
return round( $mp * $rate, 6 );
|
|
}
|
|
}
|