feat(images): route Cloudflare Workers AI via AI Gateway by default

Direct probe on 2026-04-21 confirmed the Cloudflare AI Gateway
route to Workers AI (`gateway.ai.cloudflare.com/.../workers-ai/@cf/...`)
is no longer 403ing — the upstream regression noted in v0.8.0's
ARCHITECTURE.md #16 and in thread image-pipeline seq 6 has been
resolved. Routing via the gateway gives us the same benefits we
already get for OpenRouter LLM calls: response caching, cost/latency
dashboard, rate limiting.

Changes:

- `Cloudflare_Image_Support::build_endpoint_url()` now prefers the
  gateway URL derived from the existing `prautoblogger_ai_gateway_base_url`
  option. Direct API remains the fallback.
- New private helper `build_gateway_workers_ai_url()` strips the
  OpenRouter path suffix from the configured gateway URL and appends
  `/workers-ai/{model}`. Returns empty string (fallback to direct) when:
  no gateway URL configured, the new toggle is off, or the URL isn't a
  valid `https://gateway.ai.cloudflare.com/...` shape.
- New admin toggle `prautoblogger_cf_image_via_gateway` (default on) in
  the Images section. Operators can disable to force direct API without
  clearing the LLM-path gateway URL.
- ARCHITECTURE.md #16 rewritten to reflect the v0.8.2+ gateway routing.
- CHANGELOG.md v0.8.2 entry under Changed.

Tests (new `CloudflareImageSupportTest`):

- Direct URL when gateway not configured.
- Gateway URL derived correctly when configured + toggle on.
- Toggle off forces direct URL even with gateway configured.
- Malformed gateway URL falls back to direct (defensive).
- `is_nsfw_error()` only matches error code 3030 (pinned separately so
  a tolerance regression fails early).

Cost impact: near-zero. Gateway adds a few ms of latency and enables
response caching — could *reduce* cost on identical prompts within the
configurable cache TTL.

Uninstall: new option covered by existing prautoblogger_% LIKE sweep.

Implements Commit 1b (narrow AI-Gateway-migration scope) per thread
image-pipeline seq 7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
peptiderepo
2026-04-21 10:04:56 +07:00
parent 33d3ba4ea1
commit cd45ae23f0
6 changed files with 193 additions and 5 deletions

View File

@@ -260,6 +260,14 @@ class PRAutoBlogger_Settings_Fields_Extended {
'default' => '1',
'description' => __( 'When the provider rejects an image prompt as NSFW (e.g. Cloudflare code 3030), retry once with a generic fallback scene built from the article title. Disable to fail fast if the filter gets trigger-happy.', 'prautoblogger' ),
],
[
'id' => 'prautoblogger_cf_image_via_gateway',
'label' => __( 'Route Cloudflare Images via AI Gateway', 'prautoblogger' ),
'type' => 'toggle',
'section' => 'prautoblogger_images',
'default' => '1',
'description' => __( 'Route Cloudflare Workers AI image calls through the Cloudflare AI Gateway (requires the AI Gateway URL configured under API Keys). Adds response caching, cost logging, and rate-limiting. Turn off to hit the direct Workers AI API — only needed if the gateway route regresses.', 'prautoblogger' ),
],
];
}
}

View File

@@ -55,11 +55,26 @@ class PRAutoBlogger_Cloudflare_Image_Support {
/**
* Build the Workers AI run URL for a given account + model.
*
* By default routes through the Cloudflare AI Gateway we already use for
* OpenRouter — the gateway route to Workers AI was observed 403ing in
* April 2026 (see ARCHITECTURE.md #16), but manual probe on 2026-04-21
* shows it's now returning 200. Gateway routing gives us response caching,
* cost logging, rate limiting, and provider fallback for free.
*
* Direct-API fallback kicks in automatically when either the
* `prautoblogger_ai_gateway_base_url` option is empty (no gateway
* configured at all) or the new `prautoblogger_cf_image_via_gateway`
* toggle is off (explicit opt-out for if the gateway route regresses).
*
* @param string $account_id Cloudflare account UUID.
* @param string $model Fully-qualified Workers AI model id.
* @param string $model Fully-qualified Workers AI model id (e.g. @cf/black-forest-labs/flux-1-schnell).
* @return string Fully-formed HTTPS URL.
*/
public function build_endpoint_url( string $account_id, string $model ): string {
$gateway_url = $this->build_gateway_workers_ai_url( $account_id, $model );
if ( '' !== $gateway_url ) {
return $gateway_url;
}
return sprintf(
'https://api.cloudflare.com/client/v4/accounts/%s/ai/run/%s',
rawurlencode( $account_id ),
@@ -67,6 +82,43 @@ class PRAutoBlogger_Cloudflare_Image_Support {
);
}
/**
* Derive a Workers AI URL rooted at the same AI Gateway we already use
* for OpenRouter. Returns empty string when the gateway isn't configured
* or the admin toggle opts out — the caller falls back to direct API.
*
* The existing `prautoblogger_ai_gateway_base_url` option points at
* `https://gateway.ai.cloudflare.com/v1/{account}/{gateway}/openrouter`
* for the LLM path; we strip the `/openrouter` suffix and append
* `/workers-ai/{model}` for the image path. Defensive: only emits a URL
* that pattern-matches, so a bad option value quietly falls back.
*
* @param string $account_id Cloudflare account UUID (kept for symmetry; not used in URL derivation).
* @param string $model Fully-qualified Workers AI model id.
* @return string Gateway URL, or empty string if unavailable.
*/
private function build_gateway_workers_ai_url( string $account_id, string $model ): string {
unset( $account_id );
if ( '1' !== (string) get_option( 'prautoblogger_cf_image_via_gateway', '1' ) ) {
return '';
}
$gateway_base = rtrim( (string) get_option( 'prautoblogger_ai_gateway_base_url', '' ), '/' );
if ( '' === $gateway_base ) {
return '';
}
// Must be an https CF AI Gateway URL.
if ( 0 !== stripos( $gateway_base, 'https://gateway.ai.cloudflare.com/' ) ) {
return '';
}
// Strip the trailing OpenRouter path component (any path component, really);
// everything before the last '/' is the gateway root shared by all provider routes.
$gateway_root = substr( $gateway_base, 0, strrpos( $gateway_base, '/' ) ?: strlen( $gateway_base ) );
if ( '' === $gateway_root ) {
return '';
}
return sprintf( '%s/workers-ai/%s', $gateway_root, ltrim( $model, '/' ) );
}
/**
* Emit a WARN line for a retryable failure.
*