I think moving Bytes into ByteString.scala is the right way.

Speaking of the Bytes interface, (I didn't comment earlier because of the structure discussion), I think it needs a bit of slimming down:

Just thoughts:

def hasFileBytes: Boolean // This method name is terrible, don't you agree? Not only does it leak implementations into the interface, but it also doesn't convey what the documentation about it says. Also, the method is not used, not in code, not in tests
def longLength: Long
def copyToArray(xs: Array[Byte], sourceOffset: Long = 0, targetOffset: Int = 0, span: Int = longLength.toInt): Unit // silent overflow (long.toInt)
def sliceBytes(offset: Long = 0, span: Int = longLength.toInt): ByteString // I find it confusing on many levels, not only does it silently overflow (Long.toInt) but also, it returns a ByteString, which is weird considering the name of the method
def slice(offset: Long = 0, span: Long = longLength): Bytes // Why is this needed if you already have "toChunkStream"?
def toByteArray: Array[Byte] // This method seems dubious in the face of "longLength"
def toByteString: ByteString // I don't see the value of this method if it is OK to throw IllegalArgumentException
def toChunkStream(maxChunkSize: Long): Stream[Bytes] // I do not like Stream, IIRC we aren't using it anywhere else in Akka. Just return immutable.Iterable[Bytes]
def ++(other: Bytes): Bytes
def asString: String = asString(Charset.forName("utf8")) // Isn't it called "toString"?
def asString(charset: Charset): String = new String(toByteArray, charset) // Why is this needed (and it is not tested in the PR)