array(), 'embedding' => array(), ); $cursor = ''; for ( $page = 0; $page < 20; $page++ ) { $url = add_query_arg( array_filter( array( 'limit' => 100, 'after' => $cursor, ), static function ( $value ) { return '' !== $value; } ), \AISupportAssistant\Config\Urls::OPENAI_MODELS ); $response = wp_safe_remote_get( $url, array( 'headers' => array( 'Authorization' => 'Bearer ' . $api_key ), 'timeout' => 20, 'limit_response_size' => 1048576, ) ); $payload = self::get_payload( $response, $api_key, 'OpenAI' ); if ( empty( $payload['data'] ) || ! is_array( $payload['data'] ) ) { break; } foreach ( $payload['data'] as $model ) { if ( empty( $model['id'] ) || ! is_string( $model['id'] ) ) { continue; } $model_id = $model['id']; if ( self::is_openai_chat_model( $model_id ) ) { $models['chat'][] = $model_id; } elseif ( 0 === strpos( $model_id, 'text-embedding-' ) ) { $models['embedding'][] = $model_id; } } if ( empty( $payload['has_more'] ) ) { break; } if ( 19 === $page ) { throw new \RuntimeException( esc_html__( 'OpenAI returned more model pages than can be safely loaded at once.', 'ai-support-assistant' ) ); } $last_model = end( $payload['data'] ); $next_cursor = ! empty( $payload['last_id'] ) ? $payload['last_id'] : ( is_array( $last_model ) && isset( $last_model['id'] ) ? $last_model['id'] : '' ); if ( ! is_string( $next_cursor ) || '' === $next_cursor || $next_cursor === $cursor ) { throw new \RuntimeException( esc_html__( 'OpenAI returned an incomplete model list.', 'ai-support-assistant' ) ); } $cursor = $next_cursor; } self::sort_models( $models, array( 'gpt-4o-mini', 'text-embedding-3-small' ) ); return $models; } /** * Fetch available Gemini chat and embedding models. * * @param string $api_key Provider API key. * @return array * @throws \RuntimeException If the provider request fails. */ private static function fetch_gemini_models( string $api_key ): array { $models = array( 'chat' => array(), 'embedding' => array(), ); $page_token = ''; for ( $page = 0; $page < 20; $page++ ) { $query = array( 'pageSize' => 100 ); if ( '' !== $page_token ) { $query['pageToken'] = $page_token; } // phpcs:ignore PluginCheck.CodeAnalysis.AIProvider.DirectIntegration -- The native prompt API does not provide this credential-specific model catalog. $url = add_query_arg( $query, \AISupportAssistant\Config\Urls::GEMINI_MODELS ); $response = wp_safe_remote_get( $url, array( 'headers' => array( 'x-goog-api-key' => $api_key ), 'timeout' => 20, 'limit_response_size' => 1048576, ) ); $payload = self::get_payload( $response, $api_key, 'Gemini' ); if ( ! empty( $payload['models'] ) && is_array( $payload['models'] ) ) { foreach ( $payload['models'] as $model ) { if ( empty( $model['name'] ) || ! is_string( $model['name'] ) ) { continue; } $model_id = preg_replace( '/^models\//', '', $model['name'] ); if ( 0 !== strpos( $model_id, 'gemini-' ) || empty( $model['supportedGenerationMethods'] ) || ! is_array( $model['supportedGenerationMethods'] ) ) { continue; } $is_text_chat_model = ! preg_match( '/(?:embedding|audio|transcri|tts|image|live)/i', $model_id ); if ( $is_text_chat_model && in_array( 'generateContent', $model['supportedGenerationMethods'], true ) ) { $models['chat'][] = $model_id; } if ( in_array( 'embedContent', $model['supportedGenerationMethods'], true ) ) { $models['embedding'][] = $model_id; } } } if ( empty( $payload['nextPageToken'] ) ) { break; } if ( 19 === $page ) { throw new \RuntimeException( esc_html__( 'Gemini returned more model pages than can be safely loaded at once.', 'ai-support-assistant' ) ); } if ( ! is_string( $payload['nextPageToken'] ) || $payload['nextPageToken'] === $page_token ) { throw new \RuntimeException( esc_html__( 'Gemini returned an incomplete model list.', 'ai-support-assistant' ) ); } $page_token = $payload['nextPageToken']; } self::sort_models( $models, array( 'gemini-3.1-flash-lite', 'gemini-embedding-001' ) ); return $models; } /** * Request and decode a model-list response. * * @param array|\WP_Error $response Remote response. * @param string $api_key API key to redact from errors. * @param string $provider Provider display name. * @return array * @throws \RuntimeException If the response is invalid or unsuccessful. */ private static function get_payload( $response, string $api_key, string $provider ): array { if ( is_wp_error( $response ) ) { $message = str_replace( $api_key, '[redacted]', $response->get_error_message() ); /* translators: 1: AI provider name, 2: Provider error detail. */ throw new \RuntimeException( esc_html( sprintf( __( '%1$s model request failed: %2$s', 'ai-support-assistant' ), $provider, $message ) ) ); } $status = wp_remote_retrieve_response_code( $response ); $body = wp_remote_retrieve_body( $response ); $payload = json_decode( $body, true ); if ( 200 !== $status ) { $message = ! empty( $payload['error']['message'] ) ? $payload['error']['message'] : sprintf( 'HTTP %d', $status ); $message = str_replace( $api_key, '[redacted]', (string) $message ); /* translators: 1: AI provider name, 2: Provider error detail. */ throw new \RuntimeException( esc_html( sprintf( __( '%1$s model request failed: %2$s', 'ai-support-assistant' ), $provider, $message ) ) ); } if ( ! is_array( $payload ) ) { /* translators: %s: AI provider name. */ throw new \RuntimeException( esc_html( sprintf( __( '%s returned an invalid model list.', 'ai-support-assistant' ), $provider ) ) ); } return $payload; } /** * Check if an OpenAI model ID is compatible with the chat-completions API. * * @param string $model_id Model identifier. * @return bool */ private static function is_openai_chat_model( string $model_id ): bool { if ( ! preg_match( '/^(?:ft:)?gpt-/', $model_id ) && ! preg_match( '/^chatgpt-4o(?:-|$)/', $model_id ) ) { return false; } return ! preg_match( '/(?:audio|realtime|transcri|tts|image|embedding|moderation|search|instruct|codex|whisper|dall-e|sora|(?:^|[-:])pro(?:[-:]|$))/i', $model_id ); } /** * Remove duplicates and put recommended models first. * * @param array $models Model categories. * @param array $recommended Preferred model for each category. * @return void */ private static function sort_models( array &$models, array $recommended ): void { foreach ( array( 'chat', 'embedding' ) as $index => $type ) { $models[ $type ] = array_values( array_unique( $models[ $type ] ) ); sort( $models[ $type ], SORT_STRING ); $preferred = $recommended[ $index ]; if ( in_array( $preferred, $models[ $type ], true ) ) { $models[ $type ] = array_merge( array( $preferred ), array_diff( $models[ $type ], array( $preferred ) ) ); } } } }