Since the runtime gets exposed via metadata, you can check for the availability of the pallets and certain methods on them. These can be storage locations, constants, extrinsics, events, etc.
For instance just using storage & JS API (since that is what I'm familiar with) -
api.query.authorship.author
- authorship pallet
api.query.aura.authorities
- aura
api.query.babe.authorities
- babe
api.query.difficulty.currentDifficulty
- Kulupu POW
api.query.grandpa.pendingChange
- grandpa
So basically something like -
if (api.query.aura?.authorities) {
// aura pallet
} else if (api.query.babe?.authorities) {
// babe
} else if (api.query.authorship?.author) {
// authorship pallet
// can be used in conjunction with aura, see
// e.g. Statemine parachain
} else if ... // anything else, even POW
It only checks for existence of something specific in the runtime, but doesn't actually make calls in those checks.