I've found a solution, and it was surprisingly simple. The answer was to swap out the entire form in a hook form alter implementation.
/**
* Implements hook_form_alter().
*/
function MODULE_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
if ($form_id = [BUNDLE FORM ID]) {
$form_entity = $form_state->getFormObject()->getEntity();
$form = \Drupal::formBuilder()->getForm(FormClassToUseInstead::class, $form_entity);
}
}
This works with ajax, and my form values are saving correctly.
-- Edit --
I'll note, this isn't ideal, as the original form is built before being swapped out. Ideally, core will provide a method of overriding the form by bundle. I have opened a feature request here:
https://www.drupal.org/project/drupal/issues/3491100
buildForm
method to dispatch to other classes based on the bundle of the form object