As people in the comments have suggested, to help clean up formatting it would be better to use a proper formatting library like libfmt (which is also effectively in the C++20 standard now)
However, if your requirement is that you have a run-time std::vector<std::string> and you intend to convert this to a compile-time variadic template argument sequence, then a library may not address this for you. 
Since you are passing in a vector which contains a run-time value, there will -- at some point -- require a way to convert this into a compile-time list so that it can be passed to string_format. This is possible, however it requires some infrastructure.
Since variadic arguments are a compile-time effect, we can't just convert args to args... easily. We must, at least, know the high-bound for how many functions we may convert to. However, this is still, at-least, possible using templates.
The idea is to build a jump-map (effectively, a switch-case) at compile-time, and reference into this at run-time. This will require that you have to set an arbitrary limit to the number of arguments that may be passed to string_format however.
#include <string>  // std::string
#include <vector>  // std::vector
#include <cstdlib> // std::snprintf
#include <array>   // std::array
#include <utility> // std::make_index_sequence
#include <cassert> // assert
template<typename ... Args>
std::string string_format(const std::string &format, Args... args) { ... }
template <std::size_t...Idxs>
std::string format_vector_impl(const std::string& format, const std::vector<std::string>& args, std::index_sequence<Idxs...>)
{
    return string_format(format, args[Idxs].c_str()...);
}
template <std::size_t I>
std::string format_vector(const std::string& format, const std::vector<std::string>& args)
{
    assert(I == args.size());
    return format_vector_impl(format, args, std::make_index_sequence<I>{});
}
using converter = std::string(*)(const std::string&, const std::vector<std::string>&);
template <std::size_t...Idxs>
constexpr std::array<converter,sizeof...(Idxs)> make_converter_map(std::index_sequence<Idxs...>)
{
    return {&format_vector<Idxs>...};
}
std::string test_format(const std::string& format, const std::vector<std::string>& args)
{
    // let's make 31 arguments the arbitrary max
    // Note: This is '32' here because '0' arguments is also a viable call
    static constexpr auto s_limit = 32;
    static constexpr auto s_converters = make_converter_map(std::make_index_sequence<s_limit>{});
    assert(args.size() < s_limit); // if this ever gets triggered, change 'limit'
    // use 'args.size()' as the index into the function
    return s_converters[args.size()](format, args);
}
This solution assumes at least C++14 support, however it would work in C++11 support with some tweaks (requires a custom definition of std::index_sequence)
The solution works as follows:
- Normalize all conversions to be just a signature of std::string(*)(const std::string&, const std::vector<std::string>&)viaformat_vector
- format_vectoris templated on the max size of the vector
 
- Delegate format_vector<I>toformat_vector_impl<Idxs...>by creating a sequence from[0...I)
- In format_vector, useIdxs..., a compile-time list of indexes, to unpack the argument vector
- Since all format_vectorfunctions have the same signature, we can use function pointers to build an array. We build this at compile-time (constexpr) and call into this at runtime.
- Find the correct handler by using args.size()in the array (effectively a jump-map).
You can make the number of arguments be as big as you want/need by using this approach -- but keep in mind that each new function will introduce more generated assembly code, which may bloat the executable.
Here's a working example on compiler-explorer
Note: Using a library like libfmt would help address some undefined behavior your code currently has, wherein args... is passing std::string values to snprintf. snprintf uses C-style ... variadics rather than variadic template arguments, and so it unable to understand or use C++ types -- only primitive types like integral values and const char*. 
Edit: One other thing I just noticed is that your current code uses variable-length arrays (VLAs), which are not standard C++. Arrays need to be fixed at compile-time. This cannot be done at runtime in standard C++, but can be done with some compiler extensions. I'm referring to the string_format function here:
    size_t size = 1 + snprintf(nullptr, 0, format.c_str(), args ...);
    char bytes[size] = {0};
For this, I recommend using either std::vector<char> or even just std::string and resizing with the computed size. Something like:
    auto bytes = std::string{};
    bytes.resize(size, ' ');