*/ private const MODEL_PRICING = array( 'anthropic/claude-3.5-haiku' => array( 'prompt' => 0.80, 'completion' => 4.00, ), 'anthropic/claude-sonnet-4' => array( 'prompt' => 3.00, 'completion' => 15.00, ), 'anthropic/claude-opus-4' => array( 'prompt' => 15.00, 'completion' => 75.00, ), 'openai/gpt-4o' => array( 'prompt' => 2.50, 'completion' => 10.00, ), 'openai/gpt-4o-mini' => array( 'prompt' => 0.15, 'completion' => 0.60, ), 'google/gemini-2.0-flash' => array( 'prompt' => 0.10, 'completion' => 0.40, ), 'meta-llama/llama-3.3-70b' => array( 'prompt' => 0.40, 'completion' => 0.40, ), ); /** * Get available models from OpenRouter's /models endpoint. * * Caches the result in a transient for 24 hours to avoid repeated API calls. * * Side effects: HTTP request (if cache miss), sets transient. * * @return array */ public function get_available_models(): array { $cached = get_transient( 'prautoblogger_openrouter_models' ); if ( false !== $cached && is_array( $cached ) ) { return $cached; } $api_key = $this->get_api_key(); if ( '' === $api_key ) { return array(); } // Belt-and-suspenders: inject Authorization at cURL level (see OpenRouter_Provider). $auth_header_value = 'Bearer ' . $api_key; $curl_auth_filter_mod = function ( $handle, $parsed_args, $url ) use ( $auth_header_value ): void { if ( false === strpos( $url, 'openrouter.ai' ) ) { return; } // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt curl_setopt( $handle, CURLOPT_HTTPHEADER, array( 'Authorization: ' . $auth_header_value, ) ); }; add_action( 'http_api_curl', $curl_auth_filter_mod, 99, 3 ); $response = wp_remote_get( 'https://openrouter.ai/api/v1/models', array( 'timeout' => 30, 'headers' => array( 'Authorization' => 'Bearer ' . $api_key, ), ) ); remove_action( 'http_api_curl', $curl_auth_filter_mod, 99 ); if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { PRAutoBlogger_Logger::instance()->warning( 'Failed to fetch OpenRouter models list.', 'openrouter' ); return array(); } $data = json_decode( wp_remote_retrieve_body( $response ), true ); $models = array(); if ( isset( $data['data'] ) && is_array( $data['data'] ) ) { foreach ( $data['data'] as $model ) { $models[] = array( 'id' => $model['id'] ?? '', 'name' => $model['name'] ?? $model['id'] ?? '', 'context_length' => (int) ( $model['context_length'] ?? 0 ), 'pricing' => array( 'prompt' => (float) ( $model['pricing']['prompt'] ?? 0 ) * 1000000, 'completion' => (float) ( $model['pricing']['completion'] ?? 0 ) * 1000000, ), ); } } set_transient( 'prautoblogger_openrouter_models', $models, DAY_IN_SECONDS ); return $models; } /** * Estimate the cost of an API call in USD. * * Uses hardcoded pricing first, falls back to cached model data. * * @param string $model Model identifier. * @param int $prompt_tokens Input tokens. * @param int $completion_tokens Output tokens. * * @return float Estimated cost in USD. */ public function estimate_cost( string $model, int $prompt_tokens, int $completion_tokens ): float { // OpenRouter free models: the `openrouter/free` router and any `:free` suffix model. if ( 'openrouter/free' === $model || ':free' === substr( $model, -5 ) ) { return 0.0; } $pricing = self::MODEL_PRICING[ $model ] ?? null; // Fall back to cached model list pricing. if ( null === $pricing ) { $models = $this->get_available_models(); foreach ( $models as $m ) { if ( $m['id'] === $model && isset( $m['pricing'] ) ) { $pricing = $m['pricing']; break; } } } if ( null === $pricing ) { // Unknown model — use a conservative estimate. PRAutoBlogger_Logger::instance()->warning( "Unknown model pricing for '{$model}'. Using conservative estimate.", 'openrouter' ); $pricing = array( 'prompt' => 10.0, 'completion' => 30.0, ); } return ( $prompt_tokens * $pricing['prompt'] / 1000000 ) + ( $completion_tokens * $pricing['completion'] / 1000000 ); } /** * Get the decrypted API key from options. * * @return string Decrypted API key, or empty string if not set. */ private function get_api_key(): string { $encrypted = get_option( 'prautoblogger_openrouter_api_key', '' ); if ( '' === $encrypted ) { return ''; } return PRAutoBlogger_Encryption::decrypt( $encrypted ); } }