feat: consolidate docs, backend/session infra, and settings updates

This commit is contained in:
Dwindi Ramadhana
2026-05-28 00:58:20 +07:00
parent 2424acf726
commit 44e06eed88
102 changed files with 35423 additions and 11181 deletions

View File

@@ -28,45 +28,51 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
/**
* Chat model (discussion, research, recommendations).
* Initialized from WPAW_Model_Registry in constructor.
*
* @var string
*/
private $chat_model = 'google/gemini-2.5-flash';
private $chat_model = '';
/**
* Clarity model (prompt analysis, quiz generation).
* Initialized from WPAW_Model_Registry in constructor.
*
* @var string
*/
private $clarity_model = 'google/gemini-2.5-flash';
private $clarity_model = '';
/**
* Planning model (article outline generation).
* Initialized from WPAW_Model_Registry in constructor.
*
* @var string
*/
private $planning_model = 'google/gemini-2.5-flash';
private $planning_model = '';
/**
* Writing model (article draft generation).
* Initialized from WPAW_Model_Registry in constructor.
*
* @var string
*/
private $writing_model = 'anthropic/claude-3.5-sonnet';
private $writing_model = '';
/**
* Refinement model (paragraph edits, rewrites).
* Initialized from WPAW_Model_Registry in constructor.
*
* @var string
*/
private $refinement_model = 'anthropic/claude-3.5-sonnet';
private $refinement_model = '';
/**
* Image model.
* Initialized from WPAW_Model_Registry in constructor.
*
* @var string
*/
private $image_model = 'openai/gpt-4o';
private $image_model = '';
/**
* Web search enabled.
@@ -98,13 +104,15 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
/**
* Get cached models from OpenRouter API.
* Stores full model objects in a separate transient from the ID list.
*
* @since 0.1.0
* @return array|WP_Error Models array or WP_Error on failure.
*/
public function get_cached_models() {
// Check if we have cached models.
$cached_models = get_transient( 'wpaw_openrouter_models' );
// Check if we have cached models (full objects, not IDs).
$cache_key = 'wpaw_openrouter_model_objects';
$cached_models = get_transient( $cache_key );
if ( false !== $cached_models ) {
return $cached_models;
}
@@ -119,7 +127,7 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
// Fetch all models from OpenRouter API.
$response = wp_remote_get(
'https://openrouter.ai/api/v1/models',
'https://openrouter.ai/api/v1/models?output_modalities=all',
array(
'headers' => array(
'Authorization' => 'Bearer ' . $this->api_key,
@@ -146,13 +154,15 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
// Debug: Log model count and categorize by output_modalities
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( 'OpenRouter API total models: ' . count( $models ) );
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( 'OpenRouter API total models: ' . count( $models ) );
}
// Count models by output modality
$text_count = 0;
$image_count = 0;
$image_model_ids = array();
foreach ( $models as $model ) {
$output_modalities = $model['architecture']['output_modalities'] ?? array();
if ( in_array( 'text', $output_modalities, true ) ) {
@@ -163,13 +173,16 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
$image_model_ids[] = $model['id'] . ' (' . ( $model['name'] ?? 'N/A' ) . ')';
}
}
error_log( "OpenRouter models by output_modalities: TEXT={$text_count}, IMAGE={$image_count}" );
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( "OpenRouter models by output_modalities: TEXT={$text_count}, IMAGE={$image_count}" );
error_log( 'Image generation models: ' . implode( ', ', array_slice( $image_model_ids, 0, 20 ) ) );
}
error_log( 'Image generation models: ' . implode( ', ', array_slice( $image_model_ids, 0, 20 ) ) );
}
// Cache for 24 hours.
set_transient( 'wpaw_openrouter_models', $models, DAY_IN_SECONDS );
// Cache for 24 hours - use separate key for objects.
set_transient( $cache_key, $models, DAY_IN_SECONDS );
return $models;
}
@@ -183,7 +196,9 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
*/
public function fetch_and_cache_models( $force_refresh = false ) {
if ( $force_refresh ) {
delete_transient( 'wpaw_openrouter_models' );
// Delete both transient keys on refresh to ensure clean slate.
delete_transient( 'wpaw_openrouter_model_objects' );
delete_transient( 'wpaw_openrouter_model_ids' );
}
return $this->get_cached_models();
@@ -228,6 +243,277 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
}
}
/**
* Validate that the model is available on OpenRouter before making API calls.
* Uses a cached list of available model IDs to avoid repeated API calls.
*
* @since 0.1.0
* @param string $model Model ID to validate.
* @return true|WP_Error True if valid, WP_Error if model unavailable.
*/
private function validate_model_availability( $model ) {
// Strip :online suffix if present
$base_model = trim( str_replace( ':online', '', (string) $model ) );
if ( $this->is_custom_model_id( $base_model ) ) {
// Custom models are user-managed. Skip strict pre-validation and let
// OpenRouter return the authoritative runtime response.
return true;
}
// Get cached available model IDs (separate from full model objects).
$cache_key = 'wpaw_openrouter_model_ids';
$available_models = get_transient( $cache_key );
if ( false === $available_models ) {
$available_models = $this->fetch_available_models();
// Cache for 6 hours
set_transient( $cache_key, $available_models, 6 * HOUR_IN_SECONDS );
}
// Normalize: if old transient exists with full objects instead of IDs,
// extract just the IDs for safe comparison.
$model_ids = $this->normalize_model_ids( $available_models );
// Check if model is in available list. If missing, force one fresh fetch
// to avoid false negatives from stale cache.
if ( ! in_array( $base_model, $model_ids, true ) ) {
$refreshed_models = $this->fetch_available_models();
if ( is_array( $refreshed_models ) && ! empty( $refreshed_models ) ) {
set_transient( $cache_key, $refreshed_models, 6 * HOUR_IN_SECONDS );
$model_ids = $this->normalize_model_ids( $refreshed_models );
}
}
if ( ! in_array( $base_model, $model_ids, true ) ) {
$suggestion = $this->get_model_suggestion( $base_model );
$error_msg = sprintf(
/* translators: %1$s: current model, %2$s: suggestion */
__( 'Model "%1$s" is not available on OpenRouter. %2$s', 'wp-agentic-writer' ),
$base_model,
$suggestion
);
return new WP_Error(
'model_unavailable',
$error_msg,
array(
'status' => 400,
'code' => 'MODEL_UNAVAILABLE',
'current_model' => $base_model,
)
);
}
return true;
}
/**
* Check whether model ID exists in user-defined custom models list.
*
* @since 0.2.1
* @param string $model_id Model ID.
* @return bool
*/
private function is_custom_model_id( $model_id ) {
$model_id = trim( (string) $model_id );
if ( '' === $model_id ) {
return false;
}
foreach ( $this->get_custom_model_ids() as $custom_id ) {
if ( 0 === strcasecmp( $custom_id, $model_id ) ) {
return true;
}
}
return false;
}
/**
* Get user-defined custom model IDs.
*
* @since 0.2.1
* @return array
*/
private function get_custom_model_ids() {
$custom_models = get_option( 'wp_agentic_writer_custom_models', array() );
if ( ! is_array( $custom_models ) ) {
return array();
}
$ids = array();
foreach ( $custom_models as $custom ) {
if ( ! is_array( $custom ) ) {
continue;
}
$custom_id = isset( $custom['id'] ) ? trim( (string) $custom['id'] ) : '';
if ( '' !== $custom_id ) {
$ids[] = $custom_id;
}
}
return $ids;
}
/**
* Build model availability trace for debugging runtime model selection.
*
* @since 0.2.1
* @param string $model Model ID.
* @return array
*/
private function build_model_trace( $model ) {
$model = trim( str_replace( ':online', '', (string) $model ) );
$settings = get_option( 'wp_agentic_writer_settings', array() );
$cache_key = 'wpaw_openrouter_model_ids';
$cached_models = get_transient( $cache_key );
$cache_was_loaded = false !== $cached_models;
$model_ids = $this->normalize_model_ids( $cached_models );
$cache_has_model = in_array( $model, $model_ids, true );
$refreshed_has_model = null;
if ( ! $cache_has_model ) {
$refreshed_models = $this->fetch_available_models();
if ( is_array( $refreshed_models ) && ! empty( $refreshed_models ) ) {
set_transient( $cache_key, $refreshed_models, 6 * HOUR_IN_SECONDS );
$refreshed_ids = $this->normalize_model_ids( $refreshed_models );
$refreshed_has_model = in_array( $model, $refreshed_ids, true );
}
}
return array(
'selected_model' => $model,
'settings_image_model' => isset( $settings['image_model'] ) ? (string) $settings['image_model'] : '',
'image_task_provider' => isset( $settings['task_providers']['image'] ) ? (string) $settings['task_providers']['image'] : 'openrouter',
'custom_model_ids' => $this->get_custom_model_ids(),
'custom_model_match' => $this->is_custom_model_id( $model ),
'model_cache_loaded' => $cache_was_loaded,
'model_cache_has_model' => $cache_has_model,
'refreshed_has_model' => $refreshed_has_model,
);
}
/**
* Normalize cached data to extract model IDs.
* Handles backward compatibility for old transient data that may contain
* full model objects instead of just IDs.
*
* @since 0.2.0
* @param mixed $data Cached data (may be IDs array or full objects array).
* @return array Normalized array of model ID strings.
*/
private function normalize_model_ids( $data ) {
// If it's not an array, return empty
if ( ! is_array( $data ) ) {
return array();
}
// If array is empty, return empty
if ( empty( $data ) ) {
return array();
}
// Check if it's an array of strings (already normalized) or objects
$first_item = reset( $data );
if ( is_string( $first_item ) ) {
// Already normalized - just IDs as strings
return $data;
}
if ( is_array( $first_item ) ) {
// Old transient: array of model objects with 'id' key
$ids = array();
foreach ( $data as $item ) {
if ( isset( $item['id'] ) && is_string( $item['id'] ) ) {
$ids[] = $item['id'];
}
}
return $ids;
}
if ( is_object( $first_item ) ) {
// Old transient: array of model objects with 'id' property
$ids = array();
foreach ( $data as $item ) {
if ( isset( $item->id ) && is_string( $item->id ) ) {
$ids[] = $item->id;
}
}
return $ids;
}
// Unknown format - return empty to force refresh
return array();
}
/**
* Fetch available model IDs from OpenRouter API.
* Caches only the IDs in a separate transient from full model objects.
*
* @since 0.1.0
* @return array List of available model IDs.
*/
private function fetch_available_models() {
$response = wp_remote_get(
'https://openrouter.ai/api/v1/models?output_modalities=all',
array(
'headers' => array(
'Authorization' => 'Bearer ' . $this->api_key,
),
'timeout' => 30,
)
);
if ( is_wp_error( $response ) ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( 'WPAW: Failed to fetch OpenRouter models: ' . $response->get_error_message() );
}
return array();
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( ! isset( $data['data'] ) || ! is_array( $data['data'] ) ) {
return array();
}
$model_ids = array();
foreach ( $data['data'] as $model ) {
if ( isset( $model['id'] ) ) {
$model_ids[] = $model['id'];
}
}
// Also flush old transient if it exists to prevent shape conflict.
delete_transient( 'wpaw_openrouter_models' );
return $model_ids;
}
/**
* Get a model suggestion based on the requested model.
*
* @since 0.1.0
* @param string $model Requested model ID.
* @return string Suggestion message.
*/
private function get_model_suggestion( $model ) {
$suggestions = array(
'anthropic/claude-3.5-sonnet' => __( 'Try using "anthropic/claude-3.5-haiku" instead, or go to Settings → Models to choose a different Writing model.', 'wp-agentic-writer' ),
'anthropic/claude-3.5-sonnet-v2' => __( 'Try using "anthropic/claude-3.5-haiku" instead, or go to Settings → Models to choose a different Writing model.', 'wp-agentic-writer' ),
'anthropic/claude-3-opus' => __( 'Try using "anthropic/claude-3-haiku" instead, or go to Settings → Models to choose a different Writing model.', 'wp-agentic-writer' ),
'anthropic/claude-3-sonnet' => __( 'Try using "anthropic/claude-3-haiku" instead, or go to Settings → Models to choose a different Writing model.', 'wp-agentic-writer' ),
);
if ( isset( $suggestions[ $model ] ) ) {
return $suggestions[ $model ];
}
return __( 'Please go to Settings → Models and select a different model that is available on OpenRouter.', 'wp-agentic-writer' );
}
/**
* Get singleton instance.
*
@@ -254,13 +540,23 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
$settings = get_option( 'wp_agentic_writer_settings', array() );
$this->api_key = $settings['openrouter_api_key'] ?? '';
// Initialize model defaults from registry (set after settings to allow override).
$registry_defaults = array(
'chat_model' => WPAW_Model_Registry::get_default_model( 'chat' ),
'clarity_model' => WPAW_Model_Registry::get_default_model( 'clarity' ),
'planning_model' => WPAW_Model_Registry::get_default_model( 'planning' ),
'writing_model' => WPAW_Model_Registry::get_default_model( 'writing' ),
'refinement_model' => WPAW_Model_Registry::get_default_model( 'refinement' ),
'image_model' => WPAW_Model_Registry::get_default_model( 'image' ),
);
// Get models from settings (6 models per model-preset-brief.md).
$this->chat_model = $settings['chat_model'] ?? $this->chat_model;
$this->clarity_model = $settings['clarity_model'] ?? $this->clarity_model;
$this->planning_model = $settings['planning_model'] ?? $this->planning_model;
$this->writing_model = $settings['writing_model'] ?? ( $settings['execution_model'] ?? $this->writing_model );
$this->refinement_model = $settings['refinement_model'] ?? $this->refinement_model;
$this->image_model = $settings['image_model'] ?? $this->image_model;
$this->chat_model = $settings['chat_model'] ?? $registry_defaults['chat_model'];
$this->clarity_model = $settings['clarity_model'] ?? $registry_defaults['clarity_model'];
$this->planning_model = $settings['planning_model'] ?? $registry_defaults['planning_model'];
$this->writing_model = $settings['writing_model'] ?? $registry_defaults['writing_model'];
$this->refinement_model = $settings['refinement_model'] ?? $registry_defaults['refinement_model'];
$this->image_model = $settings['image_model'] ?? $registry_defaults['image_model'];
// Get web search settings.
$this->web_search_enabled = isset( $settings['web_search_enabled'] ) && '1' === $settings['web_search_enabled'];
@@ -438,6 +734,12 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
$model .= ':online';
}
// Validate model availability before making API call
$model_validation = $this->validate_model_availability( $model );
if ( is_wp_error( $model_validation ) ) {
return $model_validation;
}
// Build request body.
$body = array(
'model' => $model,
@@ -500,6 +802,10 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
$json_body = wp_json_encode( $body );
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( 'WPAW OpenRouter request: model=' . $model . ', messages_count=' . count( $messages ) . ', first_msg_role=' . (isset( $messages[0]['role'] ) ? $messages[0]['role'] : 'N/A') );
}
// Set up cURL options with write function
curl_setopt_array( $ch, array(
CURLOPT_POST => true,
@@ -525,7 +831,7 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
continue;
}
if ( ! str_starts_with( $line, 'data: ' ) ) {
if ( 0 !== strpos( $line, 'data: ' ) ) {
continue;
}
@@ -566,6 +872,10 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
$curl_error = curl_error( $ch );
curl_close( $ch );
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( 'WPAW OpenRouter response: HTTP=' . $http_code . ', curl_error=' . $curl_error . ', result_type=' . gettype( $result ) . ', buffer_len=' . strlen( $buffer ) . ', accumulated_content_len=' . strlen( $accumulated_content ) );
}
// Check for errors
if ( $result === false && ! empty( $curl_error ) ) {
return new WP_Error(
@@ -575,12 +885,35 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
}
if ( $http_code >= 400 ) {
// Try to extract error message from buffer
$error_msg = 'API error';
$buffer_content = trim( $buffer );
if ( ! empty( $buffer_content ) ) {
$error_data = json_decode( $buffer_content, true );
if ( isset( $error_data['error']['message'] ) ) {
$error_msg = $error_data['error']['message'];
} elseif ( isset( $error_data['message'] ) ) {
$error_msg = $error_data['message'];
} else {
$error_msg = $buffer_content;
}
}
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( 'WPAW OpenRouter API error: HTTP=' . $http_code . ', Buffer: ' . substr( $buffer_content, 0, 500 ) . ', Error: ' . $error_msg );
}
return new WP_Error(
'api_error',
sprintf( __( 'API error: HTTP %d', 'wp-agentic-writer' ), $http_code )
sprintf( __( 'API error: HTTP %d - %s', 'wp-agentic-writer' ), $http_code, $error_msg )
);
}
// Log if content is unexpectedly empty
if ( empty( $accumulated_content ) && ! empty( $buffer ) ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( 'WPAW OpenRouter: Empty content but buffer has data: ' . substr( trim( $buffer ), 0, 500 ) );
}
}
// Calculate cost from usage data
$input_tokens = $accumulated_usage['prompt_tokens'] ?? 0;
$output_tokens = $accumulated_usage['completion_tokens'] ?? 0;
@@ -615,11 +948,41 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
$size = $options['size'] ?? '1024x576';
$quality = $options['quality'] ?? 'hd';
$n = $options['n'] ?? 1;
$model_trace = $this->build_model_trace( $model );
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( 'WPAW image generation model trace: ' . wp_json_encode( $model_trace ) );
}
$start_time = microtime( true );
$image_config = array(
'image_size' => '1K',
);
if ( false !== strpos( (string) $size, 'x' ) ) {
$parts = array_map( 'intval', explode( 'x', (string) $size ) );
if ( 2 === count( $parts ) && $parts[0] > 0 && $parts[1] > 0 ) {
$ratio = $parts[0] / $parts[1];
if ( $ratio > 1.6 && $ratio < 1.9 ) {
$image_config['aspect_ratio'] = '16:9';
}
}
}
$request_body = array(
'model' => $model,
'messages' => array(
array(
'role' => 'user',
'content' => $prompt,
),
),
'modalities' => $this->get_image_generation_modalities( $model ),
'image_config' => $image_config,
'stream' => false,
);
$response = wp_remote_post(
'https://openrouter.ai/api/v1/images/generations',
'https://openrouter.ai/api/v1/chat/completions',
array(
'headers' => array(
'Authorization' => 'Bearer ' . $this->api_key,
@@ -627,15 +990,7 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
'HTTP-Referer' => home_url(),
'X-Title' => get_bloginfo( 'name' ),
),
'body' => wp_json_encode(
array(
'model' => $model,
'prompt' => $prompt,
'n' => $n,
'size' => $size,
'quality' => $quality,
)
),
'body' => wp_json_encode( $request_body ),
'timeout' => 60,
)
);
@@ -643,20 +998,76 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
$generation_time = microtime( true ) - $start_time;
if ( is_wp_error( $response ) ) {
return $response;
return new WP_Error(
$response->get_error_code(),
$response->get_error_message(),
array(
'status' => 500,
'trace' => array_merge(
$model_trace,
array(
'endpoint' => 'https://openrouter.ai/api/v1/chat/completions',
'request_model' => $model,
'request_size' => $size,
'request_quality' => $quality,
'request_n' => $n,
'request_prompt_len' => strlen( (string) $prompt ),
'transport_error' => $response->get_error_message(),
)
),
)
);
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
$raw_body = wp_remote_retrieve_body( $response );
$body = json_decode( $raw_body, true );
$http_code = wp_remote_retrieve_response_code( $response );
$response_trace = array_merge(
$model_trace,
array(
'endpoint' => 'https://openrouter.ai/api/v1/chat/completions',
'request_model' => $model,
'request_size' => $size,
'request_quality' => $quality,
'request_n' => $n,
'request_modalities' => $request_body['modalities'],
'request_image_config' => $request_body['image_config'],
'request_prompt_len' => strlen( (string) $prompt ),
'openrouter_http' => $http_code,
'openrouter_response' => is_array( $body ) ? $body : substr( (string) $raw_body, 0, 2000 ),
)
);
if ( ! isset( $body['data'][0]['url'] ) ) {
// Check for API errors
if ( $http_code >= 400 ) {
$error_msg = $body['error']['message'] ?? 'Image generation failed';
return new WP_Error(
'image_api_error',
sprintf( __( 'Image generation failed (HTTP %d): %s', 'wp-agentic-writer' ), $http_code, $error_msg ),
array(
'status' => $http_code,
'trace' => $response_trace,
)
);
}
$image_url = $body['choices'][0]['message']['images'][0]['image_url']['url']
?? $body['choices'][0]['message']['images'][0]['imageUrl']['url']
?? '';
if ( '' === $image_url ) {
return new WP_Error(
'image_generation_failed',
$body['error']['message'] ?? 'Unknown error'
$body['error']['message'] ?? 'Unknown error - no image URL returned',
array(
'status' => 502,
'trace' => $response_trace,
)
);
}
return array(
'url' => $body['data'][0]['url'],
'url' => $image_url,
'cost' => $body['usage']['cost'] ?? 0.03,
'generation_time' => $generation_time,
'model' => $model,
@@ -664,6 +1075,38 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
);
}
/**
* Determine OpenRouter modalities for an image generation model.
*
* @since 0.2.1
* @param string $model Model ID.
* @return array
*/
private function get_image_generation_modalities( $model ) {
$model = trim( str_replace( ':online', '', (string) $model ) );
$models = $this->get_cached_models();
if ( ! is_wp_error( $models ) && is_array( $models ) ) {
foreach ( $models as $entry ) {
$id = is_array( $entry ) ? ( $entry['id'] ?? '' ) : ( $entry->id ?? '' );
if ( 0 !== strcasecmp( (string) $id, $model ) ) {
continue;
}
$architecture = is_array( $entry ) ? ( $entry['architecture'] ?? array() ) : (array) ( $entry->architecture ?? array() );
$output_modalities = isset( $architecture['output_modalities'] ) && is_array( $architecture['output_modalities'] )
? $architecture['output_modalities']
: array();
if ( in_array( 'image', $output_modalities, true ) && in_array( 'text', $output_modalities, true ) ) {
return array( 'image', 'text' );
}
if ( in_array( 'image', $output_modalities, true ) ) {
return array( 'image' );
}
}
}
return array( 'image' );
}
/**
* Check if provider is configured
*
@@ -684,7 +1127,7 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov
}
$response = wp_remote_get(
'https://openrouter.ai/api/v1/models',
'https://openrouter.ai/api/v1/models?output_modalities=all',
array(
'headers' => array(
'Authorization' => 'Bearer ' . $this->api_key,